[feature] Allow import/export/creation of domain allows via admin panel (#2264)
* it's happening! * aaa * fix silly whoopsie * it's working pa! it's working ma! * model report parameters * shuffle some more stuff around * getting there * oo hoo * finish tidying up for now * aaa * fix use form submit errors * peepee poo poo * aaaaa * ffff * they see me typin', they hatin' * boop * aaa * oooo * typing typing tappa tappa * almost done typing * weee * alright * push it push it real good doo doo doo doo doo doo * thingy no worky * almost done * mutation modifers not quite right * hmm * it works * view blocks + allows nicely * it works! * typia install * the old linterino * linter plz
This commit is contained in:
parent
48725f7228
commit
637f188ebe
|
@ -54,6 +54,7 @@ steps:
|
||||||
path: /tmp/cache
|
path: /tmp/cache
|
||||||
commands:
|
commands:
|
||||||
- yarn --cwd ./web/source install --frozen-lockfile --cache-folder /tmp/cache
|
- yarn --cwd ./web/source install --frozen-lockfile --cache-folder /tmp/cache
|
||||||
|
- yarn --cwd ./web/source ts-patch install # https://typia.io/docs/setup/#manual-setup
|
||||||
|
|
||||||
- name: web-lint
|
- name: web-lint
|
||||||
image: node:18-alpine
|
image: node:18-alpine
|
||||||
|
@ -191,6 +192,6 @@ steps:
|
||||||
|
|
||||||
---
|
---
|
||||||
kind: signature
|
kind: signature
|
||||||
hmac: c3efbd528a76016562f88ae435141cfb5fd6d4d07b6ad2a24ecc23cb529cc1c6
|
hmac: d7b93470276a0df7e4d862941489f00da107df3d085200009b776d33599e6043
|
||||||
|
|
||||||
...
|
...
|
||||||
|
|
|
@ -8,6 +8,7 @@ before:
|
||||||
- sed -i "s/REPLACE_ME/{{ incpatch .Version }}/" web/assets/swagger.yaml
|
- sed -i "s/REPLACE_ME/{{ incpatch .Version }}/" web/assets/swagger.yaml
|
||||||
# Install web deps + bundle web assets
|
# Install web deps + bundle web assets
|
||||||
- yarn --cwd ./web/source install
|
- yarn --cwd ./web/source install
|
||||||
|
- yarn --cwd ./web/source ts-patch install # https://typia.io/docs/setup/#manual-setup
|
||||||
- yarn --cwd ./web/source build
|
- yarn --cwd ./web/source build
|
||||||
builds:
|
builds:
|
||||||
# https://goreleaser.com/customization/build/
|
# https://goreleaser.com/customization/build/
|
||||||
|
|
|
@ -229,13 +229,15 @@ Using [NVM](https://github.com/nvm-sh/nvm) is one convenient way to install them
|
||||||
To install frontend dependencies:
|
To install frontend dependencies:
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
yarn --cwd web/source
|
yarn --cwd ./web/source install && yarn --cwd ./web/source ts-patch install
|
||||||
```
|
```
|
||||||
|
|
||||||
|
The `ts-patch` step is necessary because of Typia, which we use for some type validation: see [Typia install docs](https://typia.io/docs/setup/#manual-setup).
|
||||||
|
|
||||||
To recompile frontend bundles into `web/assets/dist`:
|
To recompile frontend bundles into `web/assets/dist`:
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
yarn --cwd web/source build
|
yarn --cwd ./web/source build
|
||||||
```
|
```
|
||||||
|
|
||||||
#### Live Reloading
|
#### Live Reloading
|
||||||
|
|
|
@ -16,6 +16,7 @@ FROM --platform=${BUILDPLATFORM} node:18-alpine AS bundler
|
||||||
|
|
||||||
COPY web web
|
COPY web web
|
||||||
RUN yarn --cwd ./web/source install && \
|
RUN yarn --cwd ./web/source install && \
|
||||||
|
yarn --cwd ./web/source ts-patch install && \
|
||||||
yarn --cwd ./web/source build && \
|
yarn --cwd ./web/source build && \
|
||||||
rm -rf ./web/source
|
rm -rf ./web/source
|
||||||
|
|
||||||
|
|
|
@ -95,7 +95,7 @@ func (m *Module) createDomainPermissions(
|
||||||
|
|
||||||
if importing && form.Domains.Size == 0 {
|
if importing && form.Domains.Size == 0 {
|
||||||
err = errors.New("import was specified but list of domains is empty")
|
err = errors.New("import was specified but list of domains is empty")
|
||||||
} else if form.Domain == "" {
|
} else if !importing && form.Domain == "" {
|
||||||
err = errors.New("empty domain provided")
|
err = errors.New("empty domain provided")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -45,6 +45,10 @@
|
||||||
"@browserify/envify": "^6.0.0",
|
"@browserify/envify": "^6.0.0",
|
||||||
"@browserify/uglifyify": "^6.0.0",
|
"@browserify/uglifyify": "^6.0.0",
|
||||||
"@joepie91/eslint-config": "^1.1.1",
|
"@joepie91/eslint-config": "^1.1.1",
|
||||||
|
"@types/bluebird": "^3.5.39",
|
||||||
|
"@types/is-valid-domain": "^0.0.2",
|
||||||
|
"@types/papaparse": "^5.3.9",
|
||||||
|
"@types/psl": "^1.1.1",
|
||||||
"@types/react-dom": "^18.2.8",
|
"@types/react-dom": "^18.2.8",
|
||||||
"@typescript-eslint/eslint-plugin": "^6.7.4",
|
"@typescript-eslint/eslint-plugin": "^6.7.4",
|
||||||
"@typescript-eslint/parser": "^6.7.4",
|
"@typescript-eslint/parser": "^6.7.4",
|
||||||
|
@ -63,7 +67,10 @@
|
||||||
"postcss-nested": "^6.0.0",
|
"postcss-nested": "^6.0.0",
|
||||||
"source-map-loader": "^4.0.1",
|
"source-map-loader": "^4.0.1",
|
||||||
"ts-loader": "^9.4.4",
|
"ts-loader": "^9.4.4",
|
||||||
|
"ts-node": "^10.9.1",
|
||||||
|
"ts-patch": "^3.0.2",
|
||||||
"tsify": "^5.0.4",
|
"tsify": "^5.0.4",
|
||||||
"typescript": "^5.2.2"
|
"typescript": "^5.2.2",
|
||||||
|
"typia": "^5.1.6"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -22,13 +22,13 @@ const { useRoute, Redirect } = require("wouter");
|
||||||
|
|
||||||
const query = require("../../lib/query");
|
const query = require("../../lib/query");
|
||||||
|
|
||||||
const FormWithData = require("../../lib/form/form-with-data");
|
const FormWithData = require("../../lib/form/form-with-data").default;
|
||||||
|
|
||||||
const { useBaseUrl } = require("../../lib/navigation/util");
|
const { useBaseUrl } = require("../../lib/navigation/util");
|
||||||
const FakeProfile = require("../../components/fake-profile");
|
const FakeProfile = require("../../components/fake-profile");
|
||||||
const MutationButton = require("../../components/form/mutation-button");
|
const MutationButton = require("../../components/form/mutation-button");
|
||||||
|
|
||||||
const useFormSubmit = require("../../lib/form/submit");
|
const useFormSubmit = require("../../lib/form/submit").default;
|
||||||
const { useValue, useTextInput } = require("../../lib/form");
|
const { useValue, useTextInput } = require("../../lib/form");
|
||||||
const { TextInput } = require("../../components/form/inputs");
|
const { TextInput } = require("../../components/form/inputs");
|
||||||
|
|
||||||
|
@ -77,7 +77,7 @@ function AccountDetailForm({ data: account }) {
|
||||||
function ModifyAccount({ account }) {
|
function ModifyAccount({ account }) {
|
||||||
const form = {
|
const form = {
|
||||||
id: useValue("id", account.id),
|
id: useValue("id", account.id),
|
||||||
reason: useTextInput("text", {})
|
reason: useTextInput("text")
|
||||||
};
|
};
|
||||||
|
|
||||||
const [modifyAccount, result] = useFormSubmit(form, query.useActionAccountMutation());
|
const [modifyAccount, result] = useFormSubmit(form, query.useActionAccountMutation());
|
||||||
|
|
|
@ -0,0 +1,254 @@
|
||||||
|
/*
|
||||||
|
GoToSocial
|
||||||
|
Copyright (C) GoToSocial Authors admin@gotosocial.org
|
||||||
|
SPDX-License-Identifier: AGPL-3.0-or-later
|
||||||
|
|
||||||
|
This program is free software: you can redistribute it and/or modify
|
||||||
|
it under the terms of the GNU Affero General Public License as published by
|
||||||
|
the Free Software Foundation, either version 3 of the License, or
|
||||||
|
(at your option) any later version.
|
||||||
|
|
||||||
|
This program is distributed in the hope that it will be useful,
|
||||||
|
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
GNU Affero General Public License for more details.
|
||||||
|
|
||||||
|
You should have received a copy of the GNU Affero General Public License
|
||||||
|
along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import React from "react";
|
||||||
|
|
||||||
|
import { useMemo } from "react";
|
||||||
|
import { useLocation } from "wouter";
|
||||||
|
|
||||||
|
import { useTextInput, useBoolInput } from "../../lib/form";
|
||||||
|
|
||||||
|
import useFormSubmit from "../../lib/form/submit";
|
||||||
|
|
||||||
|
import { TextInput, Checkbox, TextArea } from "../../components/form/inputs";
|
||||||
|
|
||||||
|
import Loading from "../../components/loading";
|
||||||
|
import BackButton from "../../components/back-button";
|
||||||
|
import MutationButton from "../../components/form/mutation-button";
|
||||||
|
|
||||||
|
import { useDomainAllowsQuery, useDomainBlocksQuery } from "../../lib/query/admin/domain-permissions/get";
|
||||||
|
import { useAddDomainAllowMutation, useAddDomainBlockMutation, useRemoveDomainAllowMutation, useRemoveDomainBlockMutation } from "../../lib/query/admin/domain-permissions/update";
|
||||||
|
import { DomainPerm, PermType } from "../../lib/types/domain-permission";
|
||||||
|
import { NoArg } from "../../lib/types/query";
|
||||||
|
import { Error } from "../../components/error";
|
||||||
|
|
||||||
|
export interface DomainPermDetailProps {
|
||||||
|
baseUrl: string;
|
||||||
|
permType: PermType;
|
||||||
|
domain: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function DomainPermDetail({ baseUrl, permType, domain }: DomainPermDetailProps) {
|
||||||
|
const { data: domainBlocks = {}, isLoading: isLoadingDomainBlocks } = useDomainBlocksQuery(NoArg, { skip: permType !== "block" });
|
||||||
|
const { data: domainAllows = {}, isLoading: isLoadingDomainAllows } = useDomainAllowsQuery(NoArg, { skip: permType !== "allow" });
|
||||||
|
|
||||||
|
let isLoading;
|
||||||
|
switch (permType) {
|
||||||
|
case "block":
|
||||||
|
isLoading = isLoadingDomainBlocks;
|
||||||
|
break;
|
||||||
|
case "allow":
|
||||||
|
isLoading = isLoadingDomainAllows;
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
throw "perm type unknown";
|
||||||
|
}
|
||||||
|
|
||||||
|
if (domain == "view") {
|
||||||
|
// Retrieve domain from form field submission.
|
||||||
|
domain = (new URL(document.location.toString())).searchParams.get("domain")?? "unknown";
|
||||||
|
}
|
||||||
|
|
||||||
|
if (domain == "unknown") {
|
||||||
|
throw "unknown domain";
|
||||||
|
}
|
||||||
|
|
||||||
|
// Normalize / decode domain (it may be URL-encoded).
|
||||||
|
domain = decodeURIComponent(domain);
|
||||||
|
|
||||||
|
// Check if we already have a perm of the desired type for this domain.
|
||||||
|
const existingPerm: DomainPerm | undefined = useMemo(() => {
|
||||||
|
if (permType == "block") {
|
||||||
|
return domainBlocks[domain];
|
||||||
|
} else {
|
||||||
|
return domainAllows[domain];
|
||||||
|
}
|
||||||
|
}, [domainBlocks, domainAllows, domain, permType]);
|
||||||
|
|
||||||
|
let infoContent: React.JSX.Element;
|
||||||
|
|
||||||
|
if (isLoading) {
|
||||||
|
infoContent = <Loading />;
|
||||||
|
} else if (existingPerm == undefined) {
|
||||||
|
infoContent = <span>No stored {permType} yet, you can add one below:</span>;
|
||||||
|
} else {
|
||||||
|
infoContent = (
|
||||||
|
<div className="info">
|
||||||
|
<i className="fa fa-fw fa-exclamation-triangle" aria-hidden="true"></i>
|
||||||
|
<b>Editing domain permissions isn't implemented yet, <a href="https://github.com/superseriousbusiness/gotosocial/issues/1198" target="_blank" rel="noopener noreferrer">check here for progress</a></b>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
<h1 className="text-cutoff"><BackButton to={baseUrl} /> Domain {permType} for: <span title={domain}>{domain}</span></h1>
|
||||||
|
{infoContent}
|
||||||
|
<DomainPermForm
|
||||||
|
defaultDomain={domain}
|
||||||
|
perm={existingPerm}
|
||||||
|
permType={permType}
|
||||||
|
baseUrl={baseUrl}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
interface DomainPermFormProps {
|
||||||
|
defaultDomain: string;
|
||||||
|
perm?: DomainPerm;
|
||||||
|
permType: PermType;
|
||||||
|
baseUrl: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
function DomainPermForm({ defaultDomain, perm, permType, baseUrl }: DomainPermFormProps) {
|
||||||
|
const isExistingPerm = perm !== undefined;
|
||||||
|
const disabledForm = isExistingPerm
|
||||||
|
? {
|
||||||
|
disabled: true,
|
||||||
|
title: "Domain permissions currently cannot be edited."
|
||||||
|
}
|
||||||
|
: {
|
||||||
|
disabled: false,
|
||||||
|
title: "",
|
||||||
|
};
|
||||||
|
|
||||||
|
const form = {
|
||||||
|
domain: useTextInput("domain", { source: perm, defaultValue: defaultDomain }),
|
||||||
|
obfuscate: useBoolInput("obfuscate", { source: perm }),
|
||||||
|
commentPrivate: useTextInput("private_comment", { source: perm }),
|
||||||
|
commentPublic: useTextInput("public_comment", { source: perm })
|
||||||
|
};
|
||||||
|
|
||||||
|
// Check which perm type we're meant to be handling
|
||||||
|
// here, and use appropriate mutations and results.
|
||||||
|
// We can't call these hooks conditionally because
|
||||||
|
// react is like "weh" (mood), but we can decide
|
||||||
|
// which ones to use conditionally.
|
||||||
|
const [ addBlock, addBlockResult ] = useAddDomainBlockMutation();
|
||||||
|
const [ removeBlock, removeBlockResult] = useRemoveDomainBlockMutation({ fixedCacheKey: perm?.id });
|
||||||
|
const [ addAllow, addAllowResult ] = useAddDomainAllowMutation();
|
||||||
|
const [ removeAllow, removeAllowResult ] = useRemoveDomainAllowMutation({ fixedCacheKey: perm?.id });
|
||||||
|
|
||||||
|
const [
|
||||||
|
addTrigger,
|
||||||
|
addResult,
|
||||||
|
removeTrigger,
|
||||||
|
removeResult,
|
||||||
|
] = useMemo(() => {
|
||||||
|
return permType == "block"
|
||||||
|
? [
|
||||||
|
addBlock,
|
||||||
|
addBlockResult,
|
||||||
|
removeBlock,
|
||||||
|
removeBlockResult,
|
||||||
|
]
|
||||||
|
: [
|
||||||
|
addAllow,
|
||||||
|
addAllowResult,
|
||||||
|
removeAllow,
|
||||||
|
removeAllowResult,
|
||||||
|
];
|
||||||
|
}, [permType,
|
||||||
|
addBlock, addBlockResult, removeBlock, removeBlockResult,
|
||||||
|
addAllow, addAllowResult, removeAllow, removeAllowResult,
|
||||||
|
]);
|
||||||
|
|
||||||
|
// Use appropriate submission params for this permType.
|
||||||
|
const [submitForm, submitFormResult] = useFormSubmit(form, [addTrigger, addResult], { changedOnly: false });
|
||||||
|
|
||||||
|
// Uppercase first letter of given permType.
|
||||||
|
const permTypeUpper = useMemo(() => {
|
||||||
|
return permType.charAt(0).toUpperCase() + permType.slice(1);
|
||||||
|
}, [permType]);
|
||||||
|
|
||||||
|
const [location, setLocation] = useLocation();
|
||||||
|
|
||||||
|
function verifyUrlThenSubmit(e) {
|
||||||
|
// Adding a new domain permissions happens on a url like
|
||||||
|
// "/settings/admin/domain-permissions/:permType/domain.com",
|
||||||
|
// but if domain input changes, that doesn't match anymore
|
||||||
|
// and causes issues later on so, before submitting the form,
|
||||||
|
// silently change url, and THEN submit.
|
||||||
|
let correctUrl = `${baseUrl}/${form.domain.value}`;
|
||||||
|
if (location != correctUrl) {
|
||||||
|
setLocation(correctUrl);
|
||||||
|
}
|
||||||
|
return submitForm(e);
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<form onSubmit={verifyUrlThenSubmit}>
|
||||||
|
<TextInput
|
||||||
|
field={form.domain}
|
||||||
|
label="Domain"
|
||||||
|
placeholder="example.com"
|
||||||
|
{...disabledForm}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<Checkbox
|
||||||
|
field={form.obfuscate}
|
||||||
|
label="Obfuscate domain in public lists"
|
||||||
|
{...disabledForm}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<TextArea
|
||||||
|
field={form.commentPrivate}
|
||||||
|
label="Private comment"
|
||||||
|
rows={3}
|
||||||
|
{...disabledForm}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<TextArea
|
||||||
|
field={form.commentPublic}
|
||||||
|
label="Public comment"
|
||||||
|
rows={3}
|
||||||
|
{...disabledForm}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<div className="action-buttons row">
|
||||||
|
<MutationButton
|
||||||
|
label={permTypeUpper}
|
||||||
|
result={submitFormResult}
|
||||||
|
showError={false}
|
||||||
|
{...disabledForm}
|
||||||
|
/>
|
||||||
|
|
||||||
|
{
|
||||||
|
isExistingPerm &&
|
||||||
|
<MutationButton
|
||||||
|
type="button"
|
||||||
|
onClick={() => removeTrigger(perm.id?? "")}
|
||||||
|
label="Remove"
|
||||||
|
result={removeResult}
|
||||||
|
className="button danger"
|
||||||
|
showError={false}
|
||||||
|
disabled={!isExistingPerm}
|
||||||
|
/>
|
||||||
|
}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<>
|
||||||
|
{addResult.error && <Error error={addResult.error} />}
|
||||||
|
{removeResult.error && <Error error={removeResult.error} />}
|
||||||
|
</>
|
||||||
|
|
||||||
|
</form>
|
||||||
|
);
|
||||||
|
}
|
|
@ -17,34 +17,57 @@
|
||||||
along with this program. If not, see <http://www.gnu.org/licenses/>.
|
along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
const React = require("react");
|
import React from "react";
|
||||||
|
|
||||||
const query = require("../../../lib/query");
|
import { useEffect } from "react";
|
||||||
const useFormSubmit = require("../../../lib/form/submit");
|
|
||||||
|
|
||||||
const {
|
import { useExportDomainListMutation } from "../../lib/query/admin/domain-permissions/export";
|
||||||
|
import useFormSubmit from "../../lib/form/submit";
|
||||||
|
|
||||||
|
import {
|
||||||
|
RadioGroup,
|
||||||
TextArea,
|
TextArea,
|
||||||
Select,
|
Select,
|
||||||
} = require("../../../components/form/inputs");
|
} from "../../components/form/inputs";
|
||||||
|
|
||||||
const MutationButton = require("../../../components/form/mutation-button");
|
import MutationButton from "../../components/form/mutation-button";
|
||||||
|
|
||||||
const { Error } = require("../../../components/error");
|
import { Error } from "../../components/error";
|
||||||
const ExportFormatTable = require("./export-format-table");
|
import ExportFormatTable from "./export-format-table";
|
||||||
|
|
||||||
module.exports = function ImportExportForm({ form, submitParse, parseResult }) {
|
import type {
|
||||||
const [submitExport, exportResult] = useFormSubmit(form, query.useExportDomainListMutation());
|
FormSubmitFunction,
|
||||||
|
FormSubmitResult,
|
||||||
|
RadioFormInputHook,
|
||||||
|
TextFormInputHook,
|
||||||
|
} from "../../lib/form/types";
|
||||||
|
|
||||||
|
export interface ImportExportFormProps {
|
||||||
|
form: {
|
||||||
|
domains: TextFormInputHook;
|
||||||
|
exportType: TextFormInputHook;
|
||||||
|
permType: RadioFormInputHook;
|
||||||
|
};
|
||||||
|
submitParse: FormSubmitFunction;
|
||||||
|
parseResult: FormSubmitResult;
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function ImportExportForm({ form, submitParse, parseResult }: ImportExportFormProps) {
|
||||||
|
const [submitExport, exportResult] = useFormSubmit(form, useExportDomainListMutation());
|
||||||
|
|
||||||
function fileChanged(e) {
|
function fileChanged(e) {
|
||||||
const reader = new FileReader();
|
const reader = new FileReader();
|
||||||
reader.onload = function (read) {
|
reader.onload = function (read) {
|
||||||
form.domains.value = read.target.result;
|
const res = read.target?.result;
|
||||||
submitParse();
|
if (typeof res === "string") {
|
||||||
|
form.domains.value = res;
|
||||||
|
submitParse();
|
||||||
|
}
|
||||||
};
|
};
|
||||||
reader.readAsText(e.target.files[0]);
|
reader.readAsText(e.target.files[0]);
|
||||||
}
|
}
|
||||||
|
|
||||||
React.useEffect(() => {
|
useEffect(() => {
|
||||||
if (exportResult.isSuccess) {
|
if (exportResult.isSuccess) {
|
||||||
form.domains.setter(exportResult.data);
|
form.domains.setter(exportResult.data);
|
||||||
}
|
}
|
||||||
|
@ -53,12 +76,10 @@ module.exports = function ImportExportForm({ form, submitParse, parseResult }) {
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<h1>Import / Export suspended domains</h1>
|
<h1>Import / Export domain permissions</h1>
|
||||||
<p>
|
<p>This page can be used to import and export lists of domain permissions.</p>
|
||||||
This page can be used to import and export lists of domains to suspend.
|
<p>Exports can be done in various formats, with varying functionality and support in other software.</p>
|
||||||
Exports can be done in various formats, with varying functionality and support in other software.
|
<p>Imports will automatically detect what format is being processed.</p>
|
||||||
Imports will automatically detect what format is being processed.
|
|
||||||
</p>
|
|
||||||
<ExportFormatTable />
|
<ExportFormatTable />
|
||||||
<div className="import-export">
|
<div className="import-export">
|
||||||
<TextArea
|
<TextArea
|
||||||
|
@ -68,6 +89,10 @@ module.exports = function ImportExportForm({ form, submitParse, parseResult }) {
|
||||||
rows={8}
|
rows={8}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
|
<RadioGroup
|
||||||
|
field={form.permType}
|
||||||
|
/>
|
||||||
|
|
||||||
<div className="button-grid">
|
<div className="button-grid">
|
||||||
<MutationButton
|
<MutationButton
|
||||||
label="Import"
|
label="Import"
|
||||||
|
@ -75,6 +100,7 @@ module.exports = function ImportExportForm({ form, submitParse, parseResult }) {
|
||||||
onClick={() => submitParse()}
|
onClick={() => submitParse()}
|
||||||
result={parseResult}
|
result={parseResult}
|
||||||
showError={false}
|
showError={false}
|
||||||
|
disabled={false}
|
||||||
/>
|
/>
|
||||||
<label className="button with-icon">
|
<label className="button with-icon">
|
||||||
<i className="fa fa-fw " aria-hidden="true" />
|
<i className="fa fa-fw " aria-hidden="true" />
|
||||||
|
@ -92,6 +118,7 @@ module.exports = function ImportExportForm({ form, submitParse, parseResult }) {
|
||||||
type="button"
|
type="button"
|
||||||
onClick={() => submitExport("export")}
|
onClick={() => submitExport("export")}
|
||||||
result={exportResult} showError={false}
|
result={exportResult} showError={false}
|
||||||
|
disabled={false}
|
||||||
/>
|
/>
|
||||||
<MutationButton
|
<MutationButton
|
||||||
label="Export to file"
|
label="Export to file"
|
||||||
|
@ -100,6 +127,7 @@ module.exports = function ImportExportForm({ form, submitParse, parseResult }) {
|
||||||
onClick={() => submitExport("export-file")}
|
onClick={() => submitExport("export-file")}
|
||||||
result={exportResult}
|
result={exportResult}
|
||||||
showError={false}
|
showError={false}
|
||||||
|
disabled={false}
|
||||||
/>
|
/>
|
||||||
<div className="export-file">
|
<div className="export-file">
|
||||||
<span>
|
<span>
|
||||||
|
@ -121,4 +149,4 @@ module.exports = function ImportExportForm({ form, submitParse, parseResult }) {
|
||||||
</div>
|
</div>
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
};
|
}
|
|
@ -0,0 +1,90 @@
|
||||||
|
/*
|
||||||
|
GoToSocial
|
||||||
|
Copyright (C) GoToSocial Authors admin@gotosocial.org
|
||||||
|
SPDX-License-Identifier: AGPL-3.0-or-later
|
||||||
|
|
||||||
|
This program is free software: you can redistribute it and/or modify
|
||||||
|
it under the terms of the GNU Affero General Public License as published by
|
||||||
|
the Free Software Foundation, either version 3 of the License, or
|
||||||
|
(at your option) any later version.
|
||||||
|
|
||||||
|
This program is distributed in the hope that it will be useful,
|
||||||
|
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
GNU Affero General Public License for more details.
|
||||||
|
|
||||||
|
You should have received a copy of the GNU Affero General Public License
|
||||||
|
along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import React from "react";
|
||||||
|
|
||||||
|
import { Switch, Route, Redirect, useLocation } from "wouter";
|
||||||
|
|
||||||
|
import { useProcessDomainPermissionsMutation } from "../../lib/query/admin/domain-permissions/process";
|
||||||
|
|
||||||
|
import { useTextInput, useRadioInput } from "../../lib/form";
|
||||||
|
|
||||||
|
import useFormSubmit from "../../lib/form/submit";
|
||||||
|
|
||||||
|
import { ProcessImport } from "./process";
|
||||||
|
import ImportExportForm from "./form";
|
||||||
|
|
||||||
|
export default function ImportExport({ baseUrl }) {
|
||||||
|
const form = {
|
||||||
|
domains: useTextInput("domains"),
|
||||||
|
exportType: useTextInput("exportType", { defaultValue: "plain", dontReset: true }),
|
||||||
|
permType: useRadioInput("permType", {
|
||||||
|
options: {
|
||||||
|
block: "Domain blocks",
|
||||||
|
allow: "Domain allows",
|
||||||
|
}
|
||||||
|
})
|
||||||
|
};
|
||||||
|
|
||||||
|
const [submitParse, parseResult] = useFormSubmit(form, useProcessDomainPermissionsMutation(), { changedOnly: false });
|
||||||
|
|
||||||
|
const [_location, setLocation] = useLocation();
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Switch>
|
||||||
|
<Route path={`${baseUrl}/process`}>
|
||||||
|
{
|
||||||
|
parseResult.isSuccess
|
||||||
|
? (
|
||||||
|
<>
|
||||||
|
<h1>
|
||||||
|
<span
|
||||||
|
className="button"
|
||||||
|
onClick={() => {
|
||||||
|
parseResult.reset();
|
||||||
|
setLocation(baseUrl);
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
< back
|
||||||
|
</span>
|
||||||
|
Confirm import of domain {form.permType.value}s:
|
||||||
|
</h1>
|
||||||
|
<ProcessImport
|
||||||
|
list={parseResult.data}
|
||||||
|
permType={form.permType}
|
||||||
|
/>
|
||||||
|
</>
|
||||||
|
)
|
||||||
|
: <Redirect to={baseUrl} />
|
||||||
|
}
|
||||||
|
</Route>
|
||||||
|
<Route>
|
||||||
|
{
|
||||||
|
parseResult.isSuccess
|
||||||
|
? <Redirect to={`${baseUrl}/process`} />
|
||||||
|
: <ImportExportForm
|
||||||
|
form={form}
|
||||||
|
submitParse={submitParse}
|
||||||
|
parseResult={parseResult}
|
||||||
|
/>
|
||||||
|
}
|
||||||
|
</Route>
|
||||||
|
</Switch>
|
||||||
|
);
|
||||||
|
}
|
|
@ -0,0 +1,49 @@
|
||||||
|
/*
|
||||||
|
GoToSocial
|
||||||
|
Copyright (C) GoToSocial Authors admin@gotosocial.org
|
||||||
|
SPDX-License-Identifier: AGPL-3.0-or-later
|
||||||
|
|
||||||
|
This program is free software: you can redistribute it and/or modify
|
||||||
|
it under the terms of the GNU Affero General Public License as published by
|
||||||
|
the Free Software Foundation, either version 3 of the License, or
|
||||||
|
(at your option) any later version.
|
||||||
|
|
||||||
|
This program is distributed in the hope that it will be useful,
|
||||||
|
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
GNU Affero General Public License for more details.
|
||||||
|
|
||||||
|
You should have received a copy of the GNU Affero General Public License
|
||||||
|
along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import React from "react";
|
||||||
|
import { Switch, Route } from "wouter";
|
||||||
|
|
||||||
|
import DomainPermissionsOverview from "./overview";
|
||||||
|
import { PermType } from "../../lib/types/domain-permission";
|
||||||
|
import DomainPermDetail from "./detail";
|
||||||
|
|
||||||
|
export default function DomainPermissions({ baseUrl }: { baseUrl: string }) {
|
||||||
|
return (
|
||||||
|
<Switch>
|
||||||
|
<Route path="/settings/admin/domain-permissions/:permType/:domain">
|
||||||
|
{params => (
|
||||||
|
<DomainPermDetail
|
||||||
|
permType={params.permType as PermType}
|
||||||
|
baseUrl={baseUrl}
|
||||||
|
domain={params.domain}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</Route>
|
||||||
|
<Route path="/settings/admin/domain-permissions/:permType">
|
||||||
|
{params => (
|
||||||
|
<DomainPermissionsOverview
|
||||||
|
permType={params.permType as PermType}
|
||||||
|
baseUrl={baseUrl}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</Route>
|
||||||
|
</Switch>
|
||||||
|
);
|
||||||
|
}
|
|
@ -0,0 +1,198 @@
|
||||||
|
/*
|
||||||
|
GoToSocial
|
||||||
|
Copyright (C) GoToSocial Authors admin@gotosocial.org
|
||||||
|
SPDX-License-Identifier: AGPL-3.0-or-later
|
||||||
|
|
||||||
|
This program is free software: you can redistribute it and/or modify
|
||||||
|
it under the terms of the GNU Affero General Public License as published by
|
||||||
|
the Free Software Foundation, either version 3 of the License, or
|
||||||
|
(at your option) any later version.
|
||||||
|
|
||||||
|
This program is distributed in the hope that it will be useful,
|
||||||
|
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
GNU Affero General Public License for more details.
|
||||||
|
|
||||||
|
You should have received a copy of the GNU Affero General Public License
|
||||||
|
along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import React from "react";
|
||||||
|
|
||||||
|
import { useMemo } from "react";
|
||||||
|
import { Link, useLocation } from "wouter";
|
||||||
|
import { matchSorter } from "match-sorter";
|
||||||
|
|
||||||
|
import { useTextInput } from "../../lib/form";
|
||||||
|
|
||||||
|
import { TextInput } from "../../components/form/inputs";
|
||||||
|
|
||||||
|
import Loading from "../../components/loading";
|
||||||
|
import { useDomainAllowsQuery, useDomainBlocksQuery } from "../../lib/query/admin/domain-permissions/get";
|
||||||
|
import type { MappedDomainPerms, PermType } from "../../lib/types/domain-permission";
|
||||||
|
import { NoArg } from "../../lib/types/query";
|
||||||
|
|
||||||
|
export interface DomainPermissionsOverviewProps {
|
||||||
|
// Params injected by
|
||||||
|
// the wouter router.
|
||||||
|
permType: PermType;
|
||||||
|
baseUrl: string,
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function DomainPermissionsOverview({ permType, baseUrl }: DomainPermissionsOverviewProps) {
|
||||||
|
if (permType !== "block" && permType !== "allow") {
|
||||||
|
throw "unrecognized perm type " + permType;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Uppercase first letter of given permType.
|
||||||
|
const permTypeUpper = useMemo(() => {
|
||||||
|
return permType.charAt(0).toUpperCase() + permType.slice(1);
|
||||||
|
}, [permType]);
|
||||||
|
|
||||||
|
// Fetch / wait for desired perms to load.
|
||||||
|
const { data: blocks, isLoading: isLoadingBlocks } = useDomainBlocksQuery(NoArg, { skip: permType !== "block" });
|
||||||
|
const { data: allows, isLoading: isLoadingAllows } = useDomainAllowsQuery(NoArg, { skip: permType !== "allow" });
|
||||||
|
|
||||||
|
let data: MappedDomainPerms | undefined;
|
||||||
|
let isLoading: boolean;
|
||||||
|
|
||||||
|
if (permType == "block") {
|
||||||
|
data = blocks;
|
||||||
|
isLoading = isLoadingBlocks;
|
||||||
|
} else {
|
||||||
|
data = allows;
|
||||||
|
isLoading = isLoadingAllows;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (isLoading || data === undefined) {
|
||||||
|
return <Loading />;
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
<h1>Domain {permTypeUpper}s</h1>
|
||||||
|
{ permType == "block" ? <BlockHelperText/> : <AllowHelperText/> }
|
||||||
|
<DomainPermsList
|
||||||
|
data={data}
|
||||||
|
baseUrl={baseUrl}
|
||||||
|
permType={permType}
|
||||||
|
permTypeUpper={permTypeUpper}
|
||||||
|
/>
|
||||||
|
<Link to={`${baseUrl}/import-export`}>
|
||||||
|
<a>Or use the bulk import/export interface</a>
|
||||||
|
</Link>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
interface DomainPermsListProps {
|
||||||
|
data: MappedDomainPerms;
|
||||||
|
baseUrl: string;
|
||||||
|
permType: PermType;
|
||||||
|
permTypeUpper: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
function DomainPermsList({ data, baseUrl, permType, permTypeUpper }: DomainPermsListProps) {
|
||||||
|
// Format perms into a list.
|
||||||
|
const perms = useMemo(() => {
|
||||||
|
return Object.values(data);
|
||||||
|
}, [data]);
|
||||||
|
|
||||||
|
const [_location, setLocation] = useLocation();
|
||||||
|
const filterField = useTextInput("filter");
|
||||||
|
|
||||||
|
function filterFormSubmit(e) {
|
||||||
|
e.preventDefault();
|
||||||
|
setLocation(`${baseUrl}/${filter}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
const filter = filterField.value ?? "";
|
||||||
|
const filteredPerms = useMemo(() => {
|
||||||
|
return matchSorter(perms, filter, { keys: ["domain"] });
|
||||||
|
}, [perms, filter]);
|
||||||
|
const filtered = perms.length - filteredPerms.length;
|
||||||
|
|
||||||
|
const filterInfo = (
|
||||||
|
<span>
|
||||||
|
{perms.length} {permType}ed domain{perms.length != 1 ? "s" : ""} {filtered > 0 && `(${filtered} filtered by search)`}
|
||||||
|
</span>
|
||||||
|
);
|
||||||
|
|
||||||
|
const entries = filteredPerms.map((entry) => {
|
||||||
|
return (
|
||||||
|
<Link key={entry.domain} to={`${baseUrl}/${entry.domain}`}>
|
||||||
|
<a className="entry nounderline">
|
||||||
|
<span id="domain">{entry.domain}</span>
|
||||||
|
<span id="date">{new Date(entry.created_at ?? "").toLocaleString()}</span>
|
||||||
|
</a>
|
||||||
|
</Link>
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="domain-permissions-list">
|
||||||
|
<form className="filter" role="search" onSubmit={filterFormSubmit}>
|
||||||
|
<TextInput
|
||||||
|
field={filterField}
|
||||||
|
placeholder="example.org"
|
||||||
|
label={`Search or add domain ${permType}`}
|
||||||
|
/>
|
||||||
|
<Link to={`${baseUrl}/${filter}`}>
|
||||||
|
<a className="button">{permTypeUpper} {filter}</a>
|
||||||
|
</Link>
|
||||||
|
</form>
|
||||||
|
<div>
|
||||||
|
{filterInfo}
|
||||||
|
<div className="list">
|
||||||
|
<div className="entries scrolling">
|
||||||
|
{entries}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function BlockHelperText() {
|
||||||
|
return (
|
||||||
|
<p>
|
||||||
|
Blocking a domain blocks interaction between your instance, and all current and future accounts on
|
||||||
|
instance(s) running on the blocked domain. Stored content will be removed, and no more data is sent to
|
||||||
|
the remote server. This extends to all subdomains as well, so blocking 'example.com' also blocks 'social.example.com'.
|
||||||
|
<br/>
|
||||||
|
<a
|
||||||
|
href="https://docs.gotosocial.org/en/latest/admin/domain_blocks/"
|
||||||
|
target="_blank"
|
||||||
|
className="docslink"
|
||||||
|
rel="noreferrer"
|
||||||
|
>
|
||||||
|
Learn more about domain blocks (opens in a new tab)
|
||||||
|
</a>
|
||||||
|
<br/>
|
||||||
|
</p>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function AllowHelperText() {
|
||||||
|
return (
|
||||||
|
<p>
|
||||||
|
Allowing a domain explicitly allows instance(s) running on that domain to interact with your instance.
|
||||||
|
If you're running in allowlist mode, this is how you "allow" instances through.
|
||||||
|
If you're running in blocklist mode (the default federation mode), you can use explicit domain allows
|
||||||
|
to override domain blocks. In blocklist mode, explicitly allowed instances will be able to interact with
|
||||||
|
your instance regardless of any domain blocks in place. This extends to all subdomains as well, so allowing
|
||||||
|
'example.com' also allows 'social.example.com'. This is useful when you're importing a block list but
|
||||||
|
there are some domains on the list you don't want to block: just create an explicit allow for those domains
|
||||||
|
before importing the list.
|
||||||
|
<br/>
|
||||||
|
<a
|
||||||
|
href="https://docs.gotosocial.org/en/latest/admin/federation_modes/"
|
||||||
|
target="_blank"
|
||||||
|
className="docslink"
|
||||||
|
rel="noreferrer"
|
||||||
|
>
|
||||||
|
Learn more about federation modes (opens in a new tab)
|
||||||
|
</a>
|
||||||
|
</p>
|
||||||
|
);
|
||||||
|
}
|
|
@ -17,57 +17,81 @@
|
||||||
along with this program. If not, see <http://www.gnu.org/licenses/>.
|
along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
const React = require("react");
|
import React from "react";
|
||||||
|
|
||||||
const query = require("../../../lib/query");
|
import { memo, useMemo, useCallback, useEffect } from "react";
|
||||||
const { isValidDomainBlock, hasBetterScope } = require("../../../lib/domain-block");
|
|
||||||
|
|
||||||
const {
|
import { isValidDomainPermission, hasBetterScope } from "../../lib/util/domain-permission";
|
||||||
|
|
||||||
|
import {
|
||||||
useTextInput,
|
useTextInput,
|
||||||
useBoolInput,
|
useBoolInput,
|
||||||
useRadioInput,
|
useRadioInput,
|
||||||
useCheckListInput
|
useCheckListInput,
|
||||||
} = require("../../../lib/form");
|
} from "../../lib/form";
|
||||||
|
|
||||||
const useFormSubmit = require("../../../lib/form/submit");
|
import {
|
||||||
|
|
||||||
const {
|
|
||||||
TextInput,
|
|
||||||
TextArea,
|
|
||||||
Checkbox,
|
|
||||||
Select,
|
Select,
|
||||||
RadioGroup
|
TextArea,
|
||||||
} = require("../../../components/form/inputs");
|
RadioGroup,
|
||||||
|
Checkbox,
|
||||||
|
TextInput,
|
||||||
|
} from "../../components/form/inputs";
|
||||||
|
|
||||||
const CheckList = require("../../../components/check-list");
|
import useFormSubmit from "../../lib/form/submit";
|
||||||
const MutationButton = require("../../../components/form/mutation-button");
|
|
||||||
const FormWithData = require("../../../lib/form/form-with-data");
|
|
||||||
|
|
||||||
module.exports = React.memo(
|
import CheckList from "../../components/check-list";
|
||||||
function ProcessImport({ list }) {
|
import MutationButton from "../../components/form/mutation-button";
|
||||||
|
import FormWithData from "../../lib/form/form-with-data";
|
||||||
|
|
||||||
|
import { useImportDomainPermsMutation } from "../../lib/query/admin/domain-permissions/import";
|
||||||
|
import {
|
||||||
|
useDomainAllowsQuery,
|
||||||
|
useDomainBlocksQuery
|
||||||
|
} from "../../lib/query/admin/domain-permissions/get";
|
||||||
|
|
||||||
|
import type { DomainPerm, MappedDomainPerms } from "../../lib/types/domain-permission";
|
||||||
|
import type { ChecklistInputHook, RadioFormInputHook } from "../../lib/form/types";
|
||||||
|
|
||||||
|
export interface ProcessImportProps {
|
||||||
|
list: DomainPerm[],
|
||||||
|
permType: RadioFormInputHook,
|
||||||
|
}
|
||||||
|
|
||||||
|
export const ProcessImport = memo(
|
||||||
|
function ProcessImport({ list, permType }: ProcessImportProps) {
|
||||||
return (
|
return (
|
||||||
<div className="without-border">
|
<div className="without-border">
|
||||||
<FormWithData
|
<FormWithData
|
||||||
dataQuery={query.useInstanceBlocksQuery}
|
dataQuery={permType.value == "allow"
|
||||||
|
? useDomainAllowsQuery
|
||||||
|
: useDomainBlocksQuery
|
||||||
|
}
|
||||||
DataForm={ImportList}
|
DataForm={ImportList}
|
||||||
list={list}
|
{...{ list, permType }}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
|
||||||
function ImportList({ list, data: blockedInstances }) {
|
export interface ImportListProps {
|
||||||
const hasComment = React.useMemo(() => {
|
list: Array<DomainPerm>,
|
||||||
|
data: MappedDomainPerms,
|
||||||
|
permType: RadioFormInputHook,
|
||||||
|
}
|
||||||
|
|
||||||
|
function ImportList({ list, data: domainPerms, permType }: ImportListProps) {
|
||||||
|
const hasComment = useMemo(() => {
|
||||||
let hasPublic = false;
|
let hasPublic = false;
|
||||||
let hasPrivate = false;
|
let hasPrivate = false;
|
||||||
|
|
||||||
list.some((entry) => {
|
list.some((entry) => {
|
||||||
if (entry.public_comment?.length > 0) {
|
if (entry.public_comment) {
|
||||||
hasPublic = true;
|
hasPublic = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (entry.private_comment?.length > 0) {
|
if (entry.private_comment) {
|
||||||
hasPrivate = true;
|
hasPrivate = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -88,7 +112,7 @@ function ImportList({ list, data: blockedInstances }) {
|
||||||
const showComment = useTextInput("showComment", { defaultValue: hasComment.type ?? "public_comment" });
|
const showComment = useTextInput("showComment", { defaultValue: hasComment.type ?? "public_comment" });
|
||||||
|
|
||||||
const form = {
|
const form = {
|
||||||
domains: useCheckListInput("domains", { entries: list }),
|
domains: useCheckListInput("domains", { entries: list }), // DomainPerm is actually also a Checkable.
|
||||||
obfuscate: useBoolInput("obfuscate"),
|
obfuscate: useBoolInput("obfuscate"),
|
||||||
privateComment: useTextInput("private_comment", {
|
privateComment: useTextInput("private_comment", {
|
||||||
defaultValue: `Imported on ${new Date().toLocaleString()}`
|
defaultValue: `Imported on ${new Date().toLocaleString()}`
|
||||||
|
@ -108,13 +132,17 @@ function ImportList({ list, data: blockedInstances }) {
|
||||||
replace: "Replace"
|
replace: "Replace"
|
||||||
}
|
}
|
||||||
}),
|
}),
|
||||||
|
permType: permType,
|
||||||
};
|
};
|
||||||
|
|
||||||
const [importDomains, importResult] = useFormSubmit(form, query.useImportDomainListMutation(), { changedOnly: false });
|
const [importDomains, importResult] = useFormSubmit(form, useImportDomainPermsMutation(), { changedOnly: false });
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<form onSubmit={importDomains} className="suspend-import-list">
|
<form
|
||||||
|
onSubmit={importDomains}
|
||||||
|
className="domain-perm-import-list"
|
||||||
|
>
|
||||||
<span>{list.length} domain{list.length != 1 ? "s" : ""} in this list</span>
|
<span>{list.length} domain{list.length != 1 ? "s" : ""} in this list</span>
|
||||||
|
|
||||||
{hasComment.both &&
|
{hasComment.both &&
|
||||||
|
@ -129,8 +157,9 @@ function ImportList({ list, data: blockedInstances }) {
|
||||||
<div className="checkbox-list-wrapper">
|
<div className="checkbox-list-wrapper">
|
||||||
<DomainCheckList
|
<DomainCheckList
|
||||||
field={form.domains}
|
field={form.domains}
|
||||||
blockedInstances={blockedInstances}
|
domainPerms={domainPerms}
|
||||||
commentType={showComment.value}
|
commentType={showComment.value as "public_comment" | "private_comment"}
|
||||||
|
permType={form.permType}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
@ -159,28 +188,41 @@ function ImportList({ list, data: blockedInstances }) {
|
||||||
label="Obfuscate domains in public lists"
|
label="Obfuscate domains in public lists"
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<MutationButton label="Import" result={importResult} />
|
<MutationButton
|
||||||
|
label="Import"
|
||||||
|
disabled={false}
|
||||||
|
result={importResult}
|
||||||
|
/>
|
||||||
</form>
|
</form>
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
function DomainCheckList({ field, blockedInstances, commentType }) {
|
interface DomainCheckListProps {
|
||||||
const getExtraProps = React.useCallback((entry) => {
|
field: ChecklistInputHook,
|
||||||
|
domainPerms: MappedDomainPerms,
|
||||||
|
commentType: "public_comment" | "private_comment",
|
||||||
|
permType: RadioFormInputHook,
|
||||||
|
}
|
||||||
|
|
||||||
|
function DomainCheckList({ field, domainPerms, commentType, permType }: DomainCheckListProps) {
|
||||||
|
const getExtraProps = useCallback((entry: DomainPerm) => {
|
||||||
return {
|
return {
|
||||||
comment: entry[commentType],
|
comment: entry[commentType],
|
||||||
alreadyExists: blockedInstances[entry.domain] != undefined
|
alreadyExists: entry.domain in domainPerms,
|
||||||
|
permType: permType,
|
||||||
};
|
};
|
||||||
}, [blockedInstances, commentType]);
|
}, [domainPerms, commentType, permType]);
|
||||||
|
|
||||||
const entriesWithSuggestions = React.useMemo(() => (
|
const entriesWithSuggestions = useMemo(() => {
|
||||||
Object.values(field.value).filter((entry) => entry.suggest)
|
const fieldValue = (field.value ?? {}) as { [k: string]: DomainPerm; };
|
||||||
), [field.value]);
|
return Object.values(fieldValue).filter((entry) => entry.suggest);
|
||||||
|
}, [field.value]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<CheckList
|
<CheckList
|
||||||
field={field}
|
field={field as ChecklistInputHook}
|
||||||
header={<>
|
header={<>
|
||||||
<b>Domain</b>
|
<b>Domain</b>
|
||||||
<b>
|
<b>
|
||||||
|
@ -200,8 +242,14 @@ function DomainCheckList({ field, blockedInstances, commentType }) {
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
const UpdateHint = React.memo(
|
interface UpdateHintProps {
|
||||||
function UpdateHint({ entries, updateEntry, updateMultiple }) {
|
entries,
|
||||||
|
updateEntry,
|
||||||
|
updateMultiple,
|
||||||
|
}
|
||||||
|
|
||||||
|
const UpdateHint = memo(
|
||||||
|
function UpdateHint({ entries, updateEntry, updateMultiple }: UpdateHintProps) {
|
||||||
if (entries.length == 0) {
|
if (entries.length == 0) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
@ -229,8 +277,13 @@ const UpdateHint = React.memo(
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
|
||||||
const UpdateableEntry = React.memo(
|
interface UpdateableEntryProps {
|
||||||
function UpdateableEntry({ entry, updateEntry }) {
|
entry,
|
||||||
|
updateEntry,
|
||||||
|
}
|
||||||
|
|
||||||
|
const UpdateableEntry = memo(
|
||||||
|
function UpdateableEntry({ entry, updateEntry }: UpdateableEntryProps) {
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<span className="text-cutoff">{entry.domain}</span>
|
<span className="text-cutoff">{entry.domain}</span>
|
||||||
|
@ -248,21 +301,31 @@ function domainValidationError(isValid) {
|
||||||
return isValid ? "" : "Invalid domain";
|
return isValid ? "" : "Invalid domain";
|
||||||
}
|
}
|
||||||
|
|
||||||
function DomainEntry({ entry, onChange, extraProps: { alreadyExists, comment } }) {
|
interface DomainEntryProps {
|
||||||
|
entry;
|
||||||
|
onChange;
|
||||||
|
extraProps: {
|
||||||
|
alreadyExists: boolean;
|
||||||
|
comment: string;
|
||||||
|
permType: RadioFormInputHook;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function DomainEntry({ entry, onChange, extraProps: { alreadyExists, comment, permType } }: DomainEntryProps) {
|
||||||
const domainField = useTextInput("domain", {
|
const domainField = useTextInput("domain", {
|
||||||
defaultValue: entry.domain,
|
defaultValue: entry.domain,
|
||||||
showValidation: entry.checked,
|
showValidation: entry.checked,
|
||||||
initValidation: domainValidationError(entry.valid),
|
initValidation: domainValidationError(entry.valid),
|
||||||
validator: (value) => domainValidationError(isValidDomainBlock(value))
|
validator: (value) => domainValidationError(isValidDomainPermission(value))
|
||||||
});
|
});
|
||||||
|
|
||||||
React.useEffect(() => {
|
useEffect(() => {
|
||||||
if (entry.valid != domainField.valid) {
|
if (entry.valid != domainField.valid) {
|
||||||
onChange({ valid: domainField.valid });
|
onChange({ valid: domainField.valid });
|
||||||
}
|
}
|
||||||
}, [onChange, entry.valid, domainField.valid]);
|
}, [onChange, entry.valid, domainField.valid]);
|
||||||
|
|
||||||
React.useEffect(() => {
|
useEffect(() => {
|
||||||
if (entry.domain != domainField.value) {
|
if (entry.domain != domainField.value) {
|
||||||
domainField.setter(entry.domain);
|
domainField.setter(entry.domain);
|
||||||
}
|
}
|
||||||
|
@ -270,8 +333,8 @@ function DomainEntry({ entry, onChange, extraProps: { alreadyExists, comment } }
|
||||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||||
}, [entry.domain, domainField.setter]);
|
}, [entry.domain, domainField.setter]);
|
||||||
|
|
||||||
React.useEffect(() => {
|
useEffect(() => {
|
||||||
onChange({ suggest: hasBetterScope(domainField.value) });
|
onChange({ suggest: hasBetterScope(domainField.value ?? "") });
|
||||||
// only need this update if it's the entry.checked that updated, not onChange
|
// only need this update if it's the entry.checked that updated, not onChange
|
||||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||||
}, [domainField.value]);
|
}, [domainField.value]);
|
||||||
|
@ -296,7 +359,11 @@ function DomainEntry({ entry, onChange, extraProps: { alreadyExists, comment } }
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
<span id="icon" onClick={clickIcon}>
|
<span id="icon" onClick={clickIcon}>
|
||||||
<DomainEntryIcon alreadyExists={alreadyExists} suggestion={entry.suggest} onChange={onChange} />
|
<DomainEntryIcon
|
||||||
|
alreadyExists={alreadyExists}
|
||||||
|
suggestion={entry.suggest}
|
||||||
|
permTypeString={permType.value?? ""}
|
||||||
|
/>
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
<p>{comment}</p>
|
<p>{comment}</p>
|
||||||
|
@ -304,7 +371,13 @@ function DomainEntry({ entry, onChange, extraProps: { alreadyExists, comment } }
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
function DomainEntryIcon({ alreadyExists, suggestion }) {
|
interface DomainEntryIconProps {
|
||||||
|
alreadyExists: boolean;
|
||||||
|
suggestion: string;
|
||||||
|
permTypeString: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
function DomainEntryIcon({ alreadyExists, suggestion, permTypeString }: DomainEntryIconProps) {
|
||||||
let icon;
|
let icon;
|
||||||
let text;
|
let text;
|
||||||
|
|
||||||
|
@ -312,8 +385,8 @@ function DomainEntryIcon({ alreadyExists, suggestion }) {
|
||||||
icon = "fa-info-circle suggest-changes";
|
icon = "fa-info-circle suggest-changes";
|
||||||
text = `Entry targets a specific subdomain, consider changing it to '${suggestion}'.`;
|
text = `Entry targets a specific subdomain, consider changing it to '${suggestion}'.`;
|
||||||
} else if (alreadyExists) {
|
} else if (alreadyExists) {
|
||||||
icon = "fa-history already-blocked";
|
icon = "fa-history permission-already-exists";
|
||||||
text = "Domain block already exists.";
|
text = `Domain ${permTypeString} already exists.`;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!icon) {
|
if (!icon) {
|
|
@ -22,9 +22,8 @@ const splitFilterN = require("split-filter-n");
|
||||||
const syncpipe = require('syncpipe');
|
const syncpipe = require('syncpipe');
|
||||||
const { matchSorter } = require("match-sorter");
|
const { matchSorter } = require("match-sorter");
|
||||||
|
|
||||||
const query = require("../../lib/query");
|
|
||||||
|
|
||||||
const ComboBox = require("../../components/combo-box");
|
const ComboBox = require("../../components/combo-box");
|
||||||
|
const { useListEmojiQuery } = require("../../lib/query/admin/custom-emoji");
|
||||||
|
|
||||||
function useEmojiByCategory(emoji) {
|
function useEmojiByCategory(emoji) {
|
||||||
// split all emoji over an object keyed by the category names (or Unsorted)
|
// split all emoji over an object keyed by the category names (or Unsorted)
|
||||||
|
@ -43,7 +42,7 @@ function CategorySelect({ field, children }) {
|
||||||
isLoading,
|
isLoading,
|
||||||
isSuccess,
|
isSuccess,
|
||||||
error
|
error
|
||||||
} = query.useListEmojiQuery({ filter: "domain:local" });
|
} = useListEmojiQuery({ filter: "domain:local" });
|
||||||
|
|
||||||
const emojiByCategory = useEmojiByCategory(emoji);
|
const emojiByCategory = useEmojiByCategory(emoji);
|
||||||
|
|
||||||
|
|
|
@ -20,21 +20,25 @@
|
||||||
const React = require("react");
|
const React = require("react");
|
||||||
const { useRoute, Link, Redirect } = require("wouter");
|
const { useRoute, Link, Redirect } = require("wouter");
|
||||||
|
|
||||||
const query = require("../../../lib/query");
|
|
||||||
|
|
||||||
const { useComboBoxInput, useFileInput, useValue } = require("../../../lib/form");
|
const { useComboBoxInput, useFileInput, useValue } = require("../../../lib/form");
|
||||||
const { CategorySelect } = require("../category-select");
|
const { CategorySelect } = require("../category-select");
|
||||||
|
|
||||||
const useFormSubmit = require("../../../lib/form/submit");
|
const useFormSubmit = require("../../../lib/form/submit").default;
|
||||||
const { useBaseUrl } = require("../../../lib/navigation/util");
|
const { useBaseUrl } = require("../../../lib/navigation/util");
|
||||||
|
|
||||||
const FakeToot = require("../../../components/fake-toot");
|
const FakeToot = require("../../../components/fake-toot");
|
||||||
const FormWithData = require("../../../lib/form/form-with-data");
|
const FormWithData = require("../../../lib/form/form-with-data").default;
|
||||||
const Loading = require("../../../components/loading");
|
const Loading = require("../../../components/loading");
|
||||||
const { FileInput } = require("../../../components/form/inputs");
|
const { FileInput } = require("../../../components/form/inputs");
|
||||||
const MutationButton = require("../../../components/form/mutation-button");
|
const MutationButton = require("../../../components/form/mutation-button");
|
||||||
const { Error } = require("../../../components/error");
|
const { Error } = require("../../../components/error");
|
||||||
|
|
||||||
|
const {
|
||||||
|
useGetEmojiQuery,
|
||||||
|
useEditEmojiMutation,
|
||||||
|
useDeleteEmojiMutation,
|
||||||
|
} = require("../../../lib/query/admin/custom-emoji");
|
||||||
|
|
||||||
module.exports = function EmojiDetailRoute({ }) {
|
module.exports = function EmojiDetailRoute({ }) {
|
||||||
const baseUrl = useBaseUrl();
|
const baseUrl = useBaseUrl();
|
||||||
let [_match, params] = useRoute(`${baseUrl}/:emojiId`);
|
let [_match, params] = useRoute(`${baseUrl}/:emojiId`);
|
||||||
|
@ -44,7 +48,7 @@ module.exports = function EmojiDetailRoute({ }) {
|
||||||
return (
|
return (
|
||||||
<div className="emoji-detail">
|
<div className="emoji-detail">
|
||||||
<Link to={baseUrl}><a>< go back</a></Link>
|
<Link to={baseUrl}><a>< go back</a></Link>
|
||||||
<FormWithData dataQuery={query.useGetEmojiQuery} queryArg={params.emojiId} DataForm={EmojiDetailForm} />
|
<FormWithData dataQuery={useGetEmojiQuery} queryArg={params.emojiId} DataForm={EmojiDetailForm} />
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
@ -61,7 +65,7 @@ function EmojiDetailForm({ data: emoji }) {
|
||||||
})
|
})
|
||||||
};
|
};
|
||||||
|
|
||||||
const [modifyEmoji, result] = useFormSubmit(form, query.useEditEmojiMutation());
|
const [modifyEmoji, result] = useFormSubmit(form, useEditEmojiMutation());
|
||||||
|
|
||||||
// Automatic submitting of category change
|
// Automatic submitting of category change
|
||||||
React.useEffect(() => {
|
React.useEffect(() => {
|
||||||
|
@ -74,7 +78,7 @@ function EmojiDetailForm({ data: emoji }) {
|
||||||
/* eslint-disable-next-line react-hooks/exhaustive-deps */
|
/* eslint-disable-next-line react-hooks/exhaustive-deps */
|
||||||
}, [form.category.hasChanged(), form.category.isNew, form.category.state.open]);
|
}, [form.category.hasChanged(), form.category.isNew, form.category.state.open]);
|
||||||
|
|
||||||
const [deleteEmoji, deleteResult] = query.useDeleteEmojiMutation();
|
const [deleteEmoji, deleteResult] = useDeleteEmojiMutation();
|
||||||
|
|
||||||
if (deleteResult.isSuccess) {
|
if (deleteResult.isSuccess) {
|
||||||
return <Redirect to={baseUrl} />;
|
return <Redirect to={baseUrl} />;
|
||||||
|
|
|
@ -19,15 +19,13 @@
|
||||||
|
|
||||||
const React = require("react");
|
const React = require("react");
|
||||||
|
|
||||||
const query = require("../../../lib/query");
|
|
||||||
|
|
||||||
const {
|
const {
|
||||||
useFileInput,
|
useFileInput,
|
||||||
useComboBoxInput
|
useComboBoxInput
|
||||||
} = require("../../../lib/form");
|
} = require("../../../lib/form");
|
||||||
const useShortcode = require("./use-shortcode");
|
const useShortcode = require("./use-shortcode");
|
||||||
|
|
||||||
const useFormSubmit = require("../../../lib/form/submit");
|
const useFormSubmit = require("../../../lib/form/submit").default;
|
||||||
|
|
||||||
const {
|
const {
|
||||||
TextInput, FileInput
|
TextInput, FileInput
|
||||||
|
@ -36,11 +34,13 @@ const {
|
||||||
const { CategorySelect } = require('../category-select');
|
const { CategorySelect } = require('../category-select');
|
||||||
const FakeToot = require("../../../components/fake-toot");
|
const FakeToot = require("../../../components/fake-toot");
|
||||||
const MutationButton = require("../../../components/form/mutation-button");
|
const MutationButton = require("../../../components/form/mutation-button");
|
||||||
|
const { useAddEmojiMutation } = require("../../../lib/query/admin/custom-emoji");
|
||||||
|
const { useInstanceV1Query } = require("../../../lib/query");
|
||||||
|
|
||||||
module.exports = function NewEmojiForm() {
|
module.exports = function NewEmojiForm() {
|
||||||
const shortcode = useShortcode();
|
const shortcode = useShortcode();
|
||||||
|
|
||||||
const { data: instance } = query.useInstanceQuery();
|
const { data: instance } = useInstanceV1Query();
|
||||||
const emojiMaxSize = React.useMemo(() => {
|
const emojiMaxSize = React.useMemo(() => {
|
||||||
return instance?.configuration?.emojis?.emoji_size_limit ?? 50 * 1024;
|
return instance?.configuration?.emojis?.emoji_size_limit ?? 50 * 1024;
|
||||||
}, [instance]);
|
}, [instance]);
|
||||||
|
@ -54,7 +54,7 @@ module.exports = function NewEmojiForm() {
|
||||||
|
|
||||||
const [submitForm, result] = useFormSubmit({
|
const [submitForm, result] = useFormSubmit({
|
||||||
shortcode, image, category
|
shortcode, image, category
|
||||||
}, query.useAddEmojiMutation());
|
}, useAddEmojiMutation());
|
||||||
|
|
||||||
React.useEffect(() => {
|
React.useEffect(() => {
|
||||||
if (shortcode.value.length == 0) {
|
if (shortcode.value.length == 0) {
|
||||||
|
|
|
@ -25,13 +25,13 @@ const { matchSorter } = require("match-sorter");
|
||||||
const NewEmojiForm = require("./new-emoji");
|
const NewEmojiForm = require("./new-emoji");
|
||||||
const { useTextInput } = require("../../../lib/form");
|
const { useTextInput } = require("../../../lib/form");
|
||||||
|
|
||||||
const query = require("../../../lib/query");
|
|
||||||
const { useEmojiByCategory } = require("../category-select");
|
const { useEmojiByCategory } = require("../category-select");
|
||||||
const { useBaseUrl } = require("../../../lib/navigation/util");
|
const { useBaseUrl } = require("../../../lib/navigation/util");
|
||||||
|
|
||||||
const Loading = require("../../../components/loading");
|
const Loading = require("../../../components/loading");
|
||||||
const { Error } = require("../../../components/error");
|
const { Error } = require("../../../components/error");
|
||||||
const { TextInput } = require("../../../components/form/inputs");
|
const { TextInput } = require("../../../components/form/inputs");
|
||||||
|
const { useListEmojiQuery } = require("../../../lib/query/admin/custom-emoji");
|
||||||
|
|
||||||
module.exports = function EmojiOverview({ }) {
|
module.exports = function EmojiOverview({ }) {
|
||||||
const {
|
const {
|
||||||
|
@ -39,7 +39,7 @@ module.exports = function EmojiOverview({ }) {
|
||||||
isLoading,
|
isLoading,
|
||||||
isError,
|
isError,
|
||||||
error
|
error
|
||||||
} = query.useListEmojiQuery({ filter: "domain:local" });
|
} = useListEmojiQuery({ filter: "domain:local" });
|
||||||
|
|
||||||
let content = null;
|
let content = null;
|
||||||
|
|
||||||
|
|
|
@ -19,15 +19,15 @@
|
||||||
|
|
||||||
const React = require("react");
|
const React = require("react");
|
||||||
|
|
||||||
const query = require("../../../lib/query");
|
|
||||||
const { useTextInput } = require("../../../lib/form");
|
const { useTextInput } = require("../../../lib/form");
|
||||||
|
const { useListEmojiQuery } = require("../../../lib/query/admin/custom-emoji");
|
||||||
|
|
||||||
const shortcodeRegex = /^\w{2,30}$/;
|
const shortcodeRegex = /^\w{2,30}$/;
|
||||||
|
|
||||||
module.exports = function useShortcode() {
|
module.exports = function useShortcode() {
|
||||||
const {
|
const { data: emoji = [] } = useListEmojiQuery({
|
||||||
data: emoji = []
|
filter: "domain:local"
|
||||||
} = query.useListEmojiQuery({ filter: "domain:local" });
|
});
|
||||||
|
|
||||||
const emojiCodes = React.useMemo(() => {
|
const emojiCodes = React.useMemo(() => {
|
||||||
return new Set(emoji.map((e) => e.shortcode));
|
return new Set(emoji.map((e) => e.shortcode));
|
||||||
|
|
|
@ -21,9 +21,9 @@ const React = require("react");
|
||||||
|
|
||||||
const ParseFromToot = require("./parse-from-toot");
|
const ParseFromToot = require("./parse-from-toot");
|
||||||
|
|
||||||
const query = require("../../../lib/query");
|
|
||||||
const Loading = require("../../../components/loading");
|
const Loading = require("../../../components/loading");
|
||||||
const { Error } = require("../../../components/error");
|
const { Error } = require("../../../components/error");
|
||||||
|
const { useListEmojiQuery } = require("../../../lib/query/admin/custom-emoji");
|
||||||
|
|
||||||
module.exports = function RemoteEmoji() {
|
module.exports = function RemoteEmoji() {
|
||||||
// local emoji are queried for shortcode collision detection
|
// local emoji are queried for shortcode collision detection
|
||||||
|
@ -31,7 +31,7 @@ module.exports = function RemoteEmoji() {
|
||||||
data: emoji = [],
|
data: emoji = [],
|
||||||
isLoading,
|
isLoading,
|
||||||
error
|
error
|
||||||
} = query.useListEmojiQuery({ filter: "domain:local" });
|
} = useListEmojiQuery({ filter: "domain:local" });
|
||||||
|
|
||||||
const emojiCodes = React.useMemo(() => {
|
const emojiCodes = React.useMemo(() => {
|
||||||
return new Set(emoji.map((e) => e.shortcode));
|
return new Set(emoji.map((e) => e.shortcode));
|
||||||
|
|
|
@ -19,25 +19,27 @@
|
||||||
|
|
||||||
const React = require("react");
|
const React = require("react");
|
||||||
|
|
||||||
const query = require("../../../lib/query");
|
|
||||||
|
|
||||||
const {
|
const {
|
||||||
useTextInput,
|
useTextInput,
|
||||||
useComboBoxInput,
|
useComboBoxInput,
|
||||||
useCheckListInput
|
useCheckListInput
|
||||||
} = require("../../../lib/form");
|
} = require("../../../lib/form");
|
||||||
|
|
||||||
const useFormSubmit = require("../../../lib/form/submit");
|
const useFormSubmit = require("../../../lib/form/submit").default;
|
||||||
|
|
||||||
const CheckList = require("../../../components/check-list");
|
const CheckList = require("../../../components/check-list").default;
|
||||||
const { CategorySelect } = require('../category-select');
|
const { CategorySelect } = require('../category-select');
|
||||||
|
|
||||||
const { TextInput } = require("../../../components/form/inputs");
|
const { TextInput } = require("../../../components/form/inputs");
|
||||||
const MutationButton = require("../../../components/form/mutation-button");
|
const MutationButton = require("../../../components/form/mutation-button");
|
||||||
const { Error } = require("../../../components/error");
|
const { Error } = require("../../../components/error");
|
||||||
|
const {
|
||||||
|
useSearchItemForEmojiMutation,
|
||||||
|
usePatchRemoteEmojisMutation
|
||||||
|
} = require("../../../lib/query/admin/custom-emoji");
|
||||||
|
|
||||||
module.exports = function ParseFromToot({ emojiCodes }) {
|
module.exports = function ParseFromToot({ emojiCodes }) {
|
||||||
const [searchStatus, result] = query.useSearchStatusForEmojiMutation();
|
const [searchStatus, result] = useSearchItemForEmojiMutation();
|
||||||
|
|
||||||
const [onURLChange, _resetURL, { url }] = useTextInput("url");
|
const [onURLChange, _resetURL, { url }] = useTextInput("url");
|
||||||
|
|
||||||
|
@ -121,7 +123,7 @@ function CopyEmojiForm({ localEmojiCodes, type, emojiList }) {
|
||||||
|
|
||||||
const [formSubmit, result] = useFormSubmit(
|
const [formSubmit, result] = useFormSubmit(
|
||||||
form,
|
form,
|
||||||
query.usePatchRemoteEmojisMutation(),
|
usePatchRemoteEmojisMutation(),
|
||||||
{
|
{
|
||||||
changedOnly: false,
|
changedOnly: false,
|
||||||
onFinish: ({ data }) => {
|
onFinish: ({ data }) => {
|
||||||
|
|
|
@ -1,168 +0,0 @@
|
||||||
/*
|
|
||||||
GoToSocial
|
|
||||||
Copyright (C) GoToSocial Authors admin@gotosocial.org
|
|
||||||
SPDX-License-Identifier: AGPL-3.0-or-later
|
|
||||||
|
|
||||||
This program is free software: you can redistribute it and/or modify
|
|
||||||
it under the terms of the GNU Affero General Public License as published by
|
|
||||||
the Free Software Foundation, either version 3 of the License, or
|
|
||||||
(at your option) any later version.
|
|
||||||
|
|
||||||
This program is distributed in the hope that it will be useful,
|
|
||||||
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
||||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
||||||
GNU Affero General Public License for more details.
|
|
||||||
|
|
||||||
You should have received a copy of the GNU Affero General Public License
|
|
||||||
along with this program. If not, see <http://www.gnu.org/licenses/>.
|
|
||||||
*/
|
|
||||||
|
|
||||||
const React = require("react");
|
|
||||||
const { useRoute, Redirect, useLocation } = require("wouter");
|
|
||||||
|
|
||||||
const query = require("../../lib/query");
|
|
||||||
|
|
||||||
const { useTextInput, useBoolInput } = require("../../lib/form");
|
|
||||||
|
|
||||||
const useFormSubmit = require("../../lib/form/submit");
|
|
||||||
|
|
||||||
const { TextInput, Checkbox, TextArea } = require("../../components/form/inputs");
|
|
||||||
|
|
||||||
const Loading = require("../../components/loading");
|
|
||||||
const BackButton = require("../../components/back-button");
|
|
||||||
const MutationButton = require("../../components/form/mutation-button");
|
|
||||||
|
|
||||||
module.exports = function InstanceDetail({ baseUrl }) {
|
|
||||||
const { data: blockedInstances = {}, isLoading } = query.useInstanceBlocksQuery();
|
|
||||||
|
|
||||||
let [_match, { domain }] = useRoute(`${baseUrl}/:domain`);
|
|
||||||
if (domain == "view") {
|
|
||||||
// Retrieve domain from form field submission.
|
|
||||||
domain = (new URL(document.location)).searchParams.get("domain");
|
|
||||||
}
|
|
||||||
|
|
||||||
// Normalize / decode domain (it may be URL-encoded).
|
|
||||||
domain = decodeURIComponent(domain);
|
|
||||||
|
|
||||||
const existingBlock = React.useMemo(() => {
|
|
||||||
return blockedInstances[domain];
|
|
||||||
}, [blockedInstances, domain]);
|
|
||||||
|
|
||||||
if (domain == undefined) {
|
|
||||||
return <Redirect to={baseUrl} />;
|
|
||||||
}
|
|
||||||
|
|
||||||
let infoContent = null;
|
|
||||||
|
|
||||||
if (isLoading) {
|
|
||||||
infoContent = <Loading />;
|
|
||||||
} else if (existingBlock == undefined) {
|
|
||||||
infoContent = <span>No stored block yet, you can add one below:</span>;
|
|
||||||
} else {
|
|
||||||
infoContent = (
|
|
||||||
<div className="info">
|
|
||||||
<i className="fa fa-fw fa-exclamation-triangle" aria-hidden="true"></i>
|
|
||||||
<b>Editing domain blocks isn't implemented yet, <a href="https://github.com/superseriousbusiness/gotosocial/issues/1198" target="_blank" rel="noopener noreferrer">check here for progress</a></b>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div>
|
|
||||||
<h1 className="text-cutoff"><BackButton to={baseUrl} /> Federation settings for: <span title={domain}>{domain}</span></h1>
|
|
||||||
{infoContent}
|
|
||||||
<DomainBlockForm defaultDomain={domain} block={existingBlock} baseUrl={baseUrl} />
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
function DomainBlockForm({ defaultDomain, block = {}, baseUrl }) {
|
|
||||||
const isExistingBlock = block.domain != undefined;
|
|
||||||
|
|
||||||
const disabledForm = isExistingBlock
|
|
||||||
? {
|
|
||||||
disabled: true,
|
|
||||||
title: "Domain suspensions currently cannot be edited."
|
|
||||||
}
|
|
||||||
: {};
|
|
||||||
|
|
||||||
const form = {
|
|
||||||
domain: useTextInput("domain", { source: block, defaultValue: defaultDomain }),
|
|
||||||
obfuscate: useBoolInput("obfuscate", { source: block }),
|
|
||||||
commentPrivate: useTextInput("private_comment", { source: block }),
|
|
||||||
commentPublic: useTextInput("public_comment", { source: block })
|
|
||||||
};
|
|
||||||
|
|
||||||
const [submitForm, addResult] = useFormSubmit(form, query.useAddInstanceBlockMutation(), { changedOnly: false });
|
|
||||||
|
|
||||||
const [removeBlock, removeResult] = query.useRemoveInstanceBlockMutation({ fixedCacheKey: block.id });
|
|
||||||
|
|
||||||
const [location, setLocation] = useLocation();
|
|
||||||
|
|
||||||
function verifyUrlThenSubmit(e) {
|
|
||||||
// Adding a new block happens on /settings/admin/federation/domain.com
|
|
||||||
// but if domain input changes, that doesn't match anymore and causes issues later on
|
|
||||||
// so, before submitting the form, silently change url, then submit
|
|
||||||
let correctUrl = `${baseUrl}/${form.domain.value}`;
|
|
||||||
if (location != correctUrl) {
|
|
||||||
setLocation(correctUrl);
|
|
||||||
}
|
|
||||||
return submitForm(e);
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
|
||||||
<form onSubmit={verifyUrlThenSubmit}>
|
|
||||||
<TextInput
|
|
||||||
field={form.domain}
|
|
||||||
label="Domain"
|
|
||||||
placeholder="example.com"
|
|
||||||
{...disabledForm}
|
|
||||||
/>
|
|
||||||
|
|
||||||
<Checkbox
|
|
||||||
field={form.obfuscate}
|
|
||||||
label="Obfuscate domain in public lists"
|
|
||||||
{...disabledForm}
|
|
||||||
/>
|
|
||||||
|
|
||||||
<TextArea
|
|
||||||
field={form.commentPrivate}
|
|
||||||
label="Private comment"
|
|
||||||
rows={3}
|
|
||||||
{...disabledForm}
|
|
||||||
/>
|
|
||||||
|
|
||||||
<TextArea
|
|
||||||
field={form.commentPublic}
|
|
||||||
label="Public comment"
|
|
||||||
rows={3}
|
|
||||||
{...disabledForm}
|
|
||||||
/>
|
|
||||||
|
|
||||||
<div className="action-buttons row">
|
|
||||||
<MutationButton
|
|
||||||
label="Suspend"
|
|
||||||
result={addResult}
|
|
||||||
showError={false}
|
|
||||||
{...disabledForm}
|
|
||||||
/>
|
|
||||||
|
|
||||||
{
|
|
||||||
isExistingBlock &&
|
|
||||||
<MutationButton
|
|
||||||
type="button"
|
|
||||||
onClick={() => removeBlock(block.id)}
|
|
||||||
label="Remove"
|
|
||||||
result={removeResult}
|
|
||||||
className="button danger"
|
|
||||||
showError={false}
|
|
||||||
/>
|
|
||||||
}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{addResult.error && <Error error={addResult.error} />}
|
|
||||||
{removeResult.error && <Error error={removeResult.error} />}
|
|
||||||
|
|
||||||
</form>
|
|
||||||
);
|
|
||||||
}
|
|
|
@ -1,75 +0,0 @@
|
||||||
/*
|
|
||||||
GoToSocial
|
|
||||||
Copyright (C) GoToSocial Authors admin@gotosocial.org
|
|
||||||
SPDX-License-Identifier: AGPL-3.0-or-later
|
|
||||||
|
|
||||||
This program is free software: you can redistribute it and/or modify
|
|
||||||
it under the terms of the GNU Affero General Public License as published by
|
|
||||||
the Free Software Foundation, either version 3 of the License, or
|
|
||||||
(at your option) any later version.
|
|
||||||
|
|
||||||
This program is distributed in the hope that it will be useful,
|
|
||||||
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
||||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
||||||
GNU Affero General Public License for more details.
|
|
||||||
|
|
||||||
You should have received a copy of the GNU Affero General Public License
|
|
||||||
along with this program. If not, see <http://www.gnu.org/licenses/>.
|
|
||||||
*/
|
|
||||||
|
|
||||||
const React = require("react");
|
|
||||||
const { Switch, Route, Redirect, useLocation } = require("wouter");
|
|
||||||
|
|
||||||
const query = require("../../../lib/query");
|
|
||||||
|
|
||||||
const {
|
|
||||||
useTextInput,
|
|
||||||
} = require("../../../lib/form");
|
|
||||||
|
|
||||||
const useFormSubmit = require("../../../lib/form/submit");
|
|
||||||
|
|
||||||
const ProcessImport = require("./process");
|
|
||||||
const ImportExportForm = require("./form");
|
|
||||||
|
|
||||||
module.exports = function ImportExport({ baseUrl }) {
|
|
||||||
const form = {
|
|
||||||
domains: useTextInput("domains"),
|
|
||||||
exportType: useTextInput("exportType", { defaultValue: "plain", dontReset: true })
|
|
||||||
};
|
|
||||||
|
|
||||||
const [submitParse, parseResult] = useFormSubmit(form, query.useProcessDomainListMutation(), { changedOnly: false });
|
|
||||||
|
|
||||||
const [_location, setLocation] = useLocation();
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Switch>
|
|
||||||
<Route path={`${baseUrl}/process`}>
|
|
||||||
{parseResult.isSuccess ? (
|
|
||||||
<>
|
|
||||||
<h1>
|
|
||||||
<span className="button" onClick={() => {
|
|
||||||
parseResult.reset();
|
|
||||||
setLocation(baseUrl);
|
|
||||||
}}>
|
|
||||||
< back
|
|
||||||
</span> Confirm import:
|
|
||||||
</h1>
|
|
||||||
<ProcessImport
|
|
||||||
list={parseResult.data}
|
|
||||||
/>
|
|
||||||
</>
|
|
||||||
) : <Redirect to={baseUrl} />}
|
|
||||||
</Route>
|
|
||||||
|
|
||||||
<Route>
|
|
||||||
{!parseResult.isSuccess ? (
|
|
||||||
<ImportExportForm
|
|
||||||
form={form}
|
|
||||||
submitParse={submitParse}
|
|
||||||
parseResult={parseResult}
|
|
||||||
/>
|
|
||||||
) : <Redirect to={`${baseUrl}/process`} />}
|
|
||||||
</Route>
|
|
||||||
</Switch>
|
|
||||||
);
|
|
||||||
};
|
|
|
@ -1,101 +0,0 @@
|
||||||
/*
|
|
||||||
GoToSocial
|
|
||||||
Copyright (C) GoToSocial Authors admin@gotosocial.org
|
|
||||||
SPDX-License-Identifier: AGPL-3.0-or-later
|
|
||||||
|
|
||||||
This program is free software: you can redistribute it and/or modify
|
|
||||||
it under the terms of the GNU Affero General Public License as published by
|
|
||||||
the Free Software Foundation, either version 3 of the License, or
|
|
||||||
(at your option) any later version.
|
|
||||||
|
|
||||||
This program is distributed in the hope that it will be useful,
|
|
||||||
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
||||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
||||||
GNU Affero General Public License for more details.
|
|
||||||
|
|
||||||
You should have received a copy of the GNU Affero General Public License
|
|
||||||
along with this program. If not, see <http://www.gnu.org/licenses/>.
|
|
||||||
*/
|
|
||||||
|
|
||||||
const React = require("react");
|
|
||||||
const { Link, useLocation } = require("wouter");
|
|
||||||
const { matchSorter } = require("match-sorter");
|
|
||||||
|
|
||||||
const { useTextInput } = require("../../lib/form");
|
|
||||||
|
|
||||||
const { TextInput } = require("../../components/form/inputs");
|
|
||||||
|
|
||||||
const query = require("../../lib/query");
|
|
||||||
|
|
||||||
const Loading = require("../../components/loading");
|
|
||||||
|
|
||||||
module.exports = function InstanceOverview({ baseUrl }) {
|
|
||||||
const { data: blockedInstances = [], isLoading } = query.useInstanceBlocksQuery();
|
|
||||||
|
|
||||||
const [_location, setLocation] = useLocation();
|
|
||||||
|
|
||||||
const filterField = useTextInput("filter");
|
|
||||||
const filter = filterField.value;
|
|
||||||
|
|
||||||
const blockedInstancesList = React.useMemo(() => {
|
|
||||||
return Object.values(blockedInstances);
|
|
||||||
}, [blockedInstances]);
|
|
||||||
|
|
||||||
const filteredInstances = React.useMemo(() => {
|
|
||||||
return matchSorter(blockedInstancesList, filter, { keys: ["domain"] });
|
|
||||||
}, [blockedInstancesList, filter]);
|
|
||||||
|
|
||||||
let filtered = blockedInstancesList.length - filteredInstances.length;
|
|
||||||
|
|
||||||
function filterFormSubmit(e) {
|
|
||||||
e.preventDefault();
|
|
||||||
setLocation(`${baseUrl}/${filter}`);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (isLoading) {
|
|
||||||
return <Loading />;
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
|
||||||
<>
|
|
||||||
<h1>Federation</h1>
|
|
||||||
|
|
||||||
<div className="instance-list">
|
|
||||||
<h2>Suspended instances</h2>
|
|
||||||
<p>
|
|
||||||
Suspending a domain blocks all current and future accounts on that instance. Stored content will be removed,
|
|
||||||
and no more data is sent to the remote server.<br />
|
|
||||||
This extends to all subdomains as well, so blocking 'example.com' also includes 'social.example.com'.
|
|
||||||
</p>
|
|
||||||
<form className="filter" role="search" onSubmit={filterFormSubmit}>
|
|
||||||
<TextInput field={filterField} placeholder="example.com" label="Search or add domain suspension" />
|
|
||||||
<Link to={`${baseUrl}/${filter}`}><a className="button">Suspend</a></Link>
|
|
||||||
</form>
|
|
||||||
<div>
|
|
||||||
<span>
|
|
||||||
{blockedInstancesList.length} blocked instance{blockedInstancesList.length != 1 ? "s" : ""} {filtered > 0 && `(${filtered} filtered by search)`}
|
|
||||||
</span>
|
|
||||||
<div className="list">
|
|
||||||
<div className="entries scrolling">
|
|
||||||
{filteredInstances.map((entry) => {
|
|
||||||
return (
|
|
||||||
<Link key={entry.domain} to={`${baseUrl}/${entry.domain}`}>
|
|
||||||
<a className="entry nounderline">
|
|
||||||
<span id="domain">
|
|
||||||
{entry.domain}
|
|
||||||
</span>
|
|
||||||
<span id="date">
|
|
||||||
{new Date(entry.created_at).toLocaleString()}
|
|
||||||
</span>
|
|
||||||
</a>
|
|
||||||
</Link>
|
|
||||||
);
|
|
||||||
})}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<Link to={`${baseUrl}/import-export`}><a>Or use the bulk import/export interface</a></Link>
|
|
||||||
</>
|
|
||||||
);
|
|
||||||
};
|
|
|
@ -20,19 +20,21 @@
|
||||||
const React = require("react");
|
const React = require("react");
|
||||||
const { useRoute, Redirect } = require("wouter");
|
const { useRoute, Redirect } = require("wouter");
|
||||||
|
|
||||||
const query = require("../../lib/query");
|
const FormWithData = require("../../lib/form/form-with-data").default;
|
||||||
|
|
||||||
const FormWithData = require("../../lib/form/form-with-data");
|
|
||||||
const BackButton = require("../../components/back-button");
|
const BackButton = require("../../components/back-button");
|
||||||
|
|
||||||
const { useValue, useTextInput } = require("../../lib/form");
|
const { useValue, useTextInput } = require("../../lib/form");
|
||||||
const useFormSubmit = require("../../lib/form/submit");
|
const useFormSubmit = require("../../lib/form/submit").default;
|
||||||
|
|
||||||
const { TextArea } = require("../../components/form/inputs");
|
const { TextArea } = require("../../components/form/inputs");
|
||||||
|
|
||||||
const MutationButton = require("../../components/form/mutation-button");
|
const MutationButton = require("../../components/form/mutation-button");
|
||||||
const Username = require("./username");
|
const Username = require("./username");
|
||||||
const { useBaseUrl } = require("../../lib/navigation/util");
|
const { useBaseUrl } = require("../../lib/navigation/util");
|
||||||
|
const {
|
||||||
|
useGetReportQuery,
|
||||||
|
useResolveReportMutation,
|
||||||
|
} = require("../../lib/query/admin/reports");
|
||||||
|
|
||||||
module.exports = function ReportDetail({ }) {
|
module.exports = function ReportDetail({ }) {
|
||||||
const baseUrl = useBaseUrl();
|
const baseUrl = useBaseUrl();
|
||||||
|
@ -46,7 +48,7 @@ module.exports = function ReportDetail({ }) {
|
||||||
<BackButton to={baseUrl} /> Report Details
|
<BackButton to={baseUrl} /> Report Details
|
||||||
</h1>
|
</h1>
|
||||||
<FormWithData
|
<FormWithData
|
||||||
dataQuery={query.useGetReportQuery}
|
dataQuery={useGetReportQuery}
|
||||||
queryArg={params.reportId}
|
queryArg={params.reportId}
|
||||||
DataForm={ReportDetailForm}
|
DataForm={ReportDetailForm}
|
||||||
/>
|
/>
|
||||||
|
@ -115,7 +117,7 @@ function ReportActionForm({ report }) {
|
||||||
comment: useTextInput("action_taken_comment")
|
comment: useTextInput("action_taken_comment")
|
||||||
};
|
};
|
||||||
|
|
||||||
const [submit, result] = useFormSubmit(form, query.useResolveReportMutation(), { changedOnly: false });
|
const [submit, result] = useFormSubmit(form, useResolveReportMutation(), { changedOnly: false });
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<form onSubmit={submit} className="info-block">
|
<form onSubmit={submit} className="info-block">
|
||||||
|
|
|
@ -20,13 +20,12 @@
|
||||||
const React = require("react");
|
const React = require("react");
|
||||||
const { Link, Switch, Route } = require("wouter");
|
const { Link, Switch, Route } = require("wouter");
|
||||||
|
|
||||||
const query = require("../../lib/query");
|
const FormWithData = require("../../lib/form/form-with-data").default;
|
||||||
|
|
||||||
const FormWithData = require("../../lib/form/form-with-data");
|
|
||||||
|
|
||||||
const ReportDetail = require("./detail");
|
const ReportDetail = require("./detail");
|
||||||
const Username = require("./username");
|
const Username = require("./username");
|
||||||
const { useBaseUrl } = require("../../lib/navigation/util");
|
const { useBaseUrl } = require("../../lib/navigation/util");
|
||||||
|
const { useListReportsQuery } = require("../../lib/query/admin/reports");
|
||||||
|
|
||||||
module.exports = function Reports({ baseUrl }) {
|
module.exports = function Reports({ baseUrl }) {
|
||||||
return (
|
return (
|
||||||
|
@ -51,7 +50,7 @@ function ReportOverview({ }) {
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
<FormWithData
|
<FormWithData
|
||||||
dataQuery={query.useListReportsQuery}
|
dataQuery={useListReportsQuery}
|
||||||
DataForm={ReportsList}
|
DataForm={ReportsList}
|
||||||
/>
|
/>
|
||||||
</>
|
</>
|
||||||
|
|
|
@ -19,14 +19,12 @@
|
||||||
|
|
||||||
const React = require("react");
|
const React = require("react");
|
||||||
|
|
||||||
const query = require("../../lib/query");
|
|
||||||
|
|
||||||
const {
|
const {
|
||||||
useTextInput,
|
useTextInput,
|
||||||
useFileInput
|
useFileInput
|
||||||
} = require("../../lib/form");
|
} = require("../../lib/form");
|
||||||
|
|
||||||
const useFormSubmit = require("../../lib/form/submit");
|
const useFormSubmit = require("../../lib/form/submit").default;
|
||||||
|
|
||||||
const {
|
const {
|
||||||
TextInput,
|
TextInput,
|
||||||
|
@ -34,13 +32,16 @@ const {
|
||||||
FileInput
|
FileInput
|
||||||
} = require("../../components/form/inputs");
|
} = require("../../components/form/inputs");
|
||||||
|
|
||||||
const FormWithData = require("../../lib/form/form-with-data");
|
const FormWithData = require("../../lib/form/form-with-data").default;
|
||||||
const MutationButton = require("../../components/form/mutation-button");
|
const MutationButton = require("../../components/form/mutation-button");
|
||||||
|
|
||||||
|
const { useInstanceV1Query } = require("../../lib/query");
|
||||||
|
const { useUpdateInstanceMutation } = require("../../lib/query/admin");
|
||||||
|
|
||||||
module.exports = function AdminSettings() {
|
module.exports = function AdminSettings() {
|
||||||
return (
|
return (
|
||||||
<FormWithData
|
<FormWithData
|
||||||
dataQuery={query.useInstanceQuery}
|
dataQuery={useInstanceV1Query}
|
||||||
DataForm={AdminSettingsForm}
|
DataForm={AdminSettingsForm}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
|
@ -61,7 +62,7 @@ function AdminSettingsForm({ data: instance }) {
|
||||||
terms: useTextInput("terms", { source: instance })
|
terms: useTextInput("terms", { source: instance })
|
||||||
};
|
};
|
||||||
|
|
||||||
const [submitForm, result] = useFormSubmit(form, query.useUpdateInstanceMutation());
|
const [submitForm, result] = useFormSubmit(form, useUpdateInstanceMutation());
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<form onSubmit={submitForm}>
|
<form onSubmit={submitForm}>
|
||||||
|
|
|
@ -21,11 +21,11 @@ const React = require("react");
|
||||||
const { Switch, Route, Link, Redirect, useRoute } = require("wouter");
|
const { Switch, Route, Link, Redirect, useRoute } = require("wouter");
|
||||||
|
|
||||||
const query = require("../../lib/query");
|
const query = require("../../lib/query");
|
||||||
const FormWithData = require("../../lib/form/form-with-data");
|
const FormWithData = require("../../lib/form/form-with-data").default;
|
||||||
const { useBaseUrl } = require("../../lib/navigation/util");
|
const { useBaseUrl } = require("../../lib/navigation/util");
|
||||||
|
|
||||||
const { useValue, useTextInput } = require("../../lib/form");
|
const { useValue, useTextInput } = require("../../lib/form");
|
||||||
const useFormSubmit = require("../../lib/form/submit");
|
const useFormSubmit = require("../../lib/form/submit").default;
|
||||||
|
|
||||||
const { TextArea } = require("../../components/form/inputs");
|
const { TextArea } = require("../../components/form/inputs");
|
||||||
const MutationButton = require("../../components/form/mutation-button");
|
const MutationButton = require("../../components/form/mutation-button");
|
||||||
|
|
|
@ -25,6 +25,7 @@ import React from "react";
|
||||||
import Login from "./login";
|
import Login from "./login";
|
||||||
import Loading from "../loading";
|
import Loading from "../loading";
|
||||||
import { Error } from "../error";
|
import { Error } from "../error";
|
||||||
|
import { NoArg } from "../../lib/types/query";
|
||||||
|
|
||||||
export function Authorization({ App }) {
|
export function Authorization({ App }) {
|
||||||
const { loginState, expectingRedirect } = store.getState().oauth;
|
const { loginState, expectingRedirect } = store.getState().oauth;
|
||||||
|
@ -35,15 +36,15 @@ export function Authorization({ App }) {
|
||||||
isSuccess,
|
isSuccess,
|
||||||
data: account,
|
data: account,
|
||||||
error,
|
error,
|
||||||
} = useVerifyCredentialsQuery(null, { skip: skip });
|
} = useVerifyCredentialsQuery(NoArg, { skip: skip });
|
||||||
|
|
||||||
let showLogin = true;
|
let showLogin = true;
|
||||||
let content = null;
|
let content: React.JSX.Element | null = null;
|
||||||
|
|
||||||
if (isLoading) {
|
if (isLoading) {
|
||||||
showLogin = false;
|
showLogin = false;
|
||||||
|
|
||||||
let loadingInfo;
|
let loadingInfo = "";
|
||||||
if (loginState == "callback") {
|
if (loginState == "callback") {
|
||||||
loadingInfo = "Processing OAUTH callback.";
|
loadingInfo = "Processing OAUTH callback.";
|
||||||
} else if (loginState == "login") {
|
} else if (loginState == "login") {
|
||||||
|
|
|
@ -22,26 +22,21 @@ import React from "react";
|
||||||
import { useAuthorizeFlowMutation } from "../../lib/query/oauth";
|
import { useAuthorizeFlowMutation } from "../../lib/query/oauth";
|
||||||
import { useTextInput, useValue } from "../../lib/form";
|
import { useTextInput, useValue } from "../../lib/form";
|
||||||
import useFormSubmit from "../../lib/form/submit";
|
import useFormSubmit from "../../lib/form/submit";
|
||||||
import { TextInput } from "../form/inputs";
|
|
||||||
import MutationButton from "../form/mutation-button";
|
import MutationButton from "../form/mutation-button";
|
||||||
import Loading from "../loading";
|
import Loading from "../loading";
|
||||||
|
import { TextInput } from "../form/inputs";
|
||||||
|
|
||||||
export default function Login({ }) {
|
export default function Login({ }) {
|
||||||
const form = {
|
const form = {
|
||||||
instance: useTextInput("instance", {
|
instance: useTextInput("instance", {
|
||||||
defaultValue: window.location.origin
|
defaultValue: window.location.origin
|
||||||
}),
|
}),
|
||||||
scopes: useValue("scopes", "user admin")
|
scopes: useValue("scopes", "user admin"),
|
||||||
};
|
};
|
||||||
|
|
||||||
const [formSubmit, result] = useFormSubmit(
|
const [formSubmit, result] = useFormSubmit(form, useAuthorizeFlowMutation(), {
|
||||||
form,
|
changedOnly: false,
|
||||||
useAuthorizeFlowMutation(),
|
});
|
||||||
{
|
|
||||||
changedOnly: false,
|
|
||||||
onFinish: undefined,
|
|
||||||
}
|
|
||||||
);
|
|
||||||
|
|
||||||
if (result.isLoading) {
|
if (result.isLoading) {
|
||||||
return (
|
return (
|
||||||
|
|
|
@ -17,21 +17,31 @@
|
||||||
along with this program. If not, see <http://www.gnu.org/licenses/>.
|
along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
const React = require("react");
|
import React from "react";
|
||||||
|
|
||||||
module.exports = function CheckList({ field, header = "All", EntryComponent, getExtraProps }) {
|
import { memo, useDeferredValue, useCallback, useMemo } from "react";
|
||||||
|
import { Checkable, ChecklistInputHook } from "../lib/form/types";
|
||||||
|
|
||||||
|
interface CheckListProps {
|
||||||
|
field: ChecklistInputHook;
|
||||||
|
header: string | React.JSX.Element;
|
||||||
|
EntryComponent: React.FunctionComponent;
|
||||||
|
getExtraProps;
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function CheckList({ field, header = "All", EntryComponent, getExtraProps }: CheckListProps) {
|
||||||
return (
|
return (
|
||||||
<div className="checkbox-list list">
|
<div className="checkbox-list list">
|
||||||
<CheckListHeader toggleAll={field.toggleAll}> {header}</CheckListHeader>
|
<CheckListHeader toggleAll={field.toggleAll}> {header}</CheckListHeader>
|
||||||
<CheckListEntries
|
<CheckListEntries
|
||||||
entries={field.value}
|
entries={field.value ?? {}}
|
||||||
updateValue={field.onChange}
|
updateValue={field.onChange}
|
||||||
EntryComponent={EntryComponent}
|
EntryComponent={EntryComponent}
|
||||||
getExtraProps={getExtraProps}
|
getExtraProps={getExtraProps}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
};
|
}
|
||||||
|
|
||||||
function CheckListHeader({ toggleAll, children }) {
|
function CheckListHeader({ toggleAll, children }) {
|
||||||
return (
|
return (
|
||||||
|
@ -45,9 +55,16 @@ function CheckListHeader({ toggleAll, children }) {
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
const CheckListEntries = React.memo(
|
interface CheckListEntriesProps {
|
||||||
function CheckListEntries({ entries, updateValue, EntryComponent, getExtraProps }) {
|
entries: { [_: string]: Checkable },
|
||||||
const deferredEntries = React.useDeferredValue(entries);
|
updateValue,
|
||||||
|
EntryComponent,
|
||||||
|
getExtraProps,
|
||||||
|
}
|
||||||
|
|
||||||
|
const CheckListEntries = memo(
|
||||||
|
function CheckListEntries({ entries, updateValue, EntryComponent, getExtraProps }: CheckListEntriesProps) {
|
||||||
|
const deferredEntries = useDeferredValue(entries);
|
||||||
|
|
||||||
return Object.values(deferredEntries).map((entry) => (
|
return Object.values(deferredEntries).map((entry) => (
|
||||||
<CheckListEntry
|
<CheckListEntry
|
||||||
|
@ -61,19 +78,26 @@ const CheckListEntries = React.memo(
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
|
||||||
|
interface CheckListEntryProps {
|
||||||
|
entry: Checkable,
|
||||||
|
updateValue,
|
||||||
|
getExtraProps,
|
||||||
|
EntryComponent,
|
||||||
|
}
|
||||||
|
|
||||||
/*
|
/*
|
||||||
React.memo is a performance optimization that only re-renders a CheckListEntry
|
React.memo is a performance optimization that only re-renders a CheckListEntry
|
||||||
when it's props actually change, instead of every time anything
|
when it's props actually change, instead of every time anything
|
||||||
in the list (CheckListEntries) updates
|
in the list (CheckListEntries) updates
|
||||||
*/
|
*/
|
||||||
const CheckListEntry = React.memo(
|
const CheckListEntry = memo(
|
||||||
function CheckListEntry({ entry, updateValue, getExtraProps, EntryComponent }) {
|
function CheckListEntry({ entry, updateValue, getExtraProps, EntryComponent }: CheckListEntryProps) {
|
||||||
const onChange = React.useCallback(
|
const onChange = useCallback(
|
||||||
(value) => updateValue(entry.key, value),
|
(value) => updateValue(entry.key, value),
|
||||||
[updateValue, entry.key]
|
[updateValue, entry.key]
|
||||||
);
|
);
|
||||||
|
|
||||||
const extraProps = React.useMemo(() => getExtraProps?.(entry), [getExtraProps, entry]);
|
const extraProps = useMemo(() => getExtraProps?.(entry), [getExtraProps, entry]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<label className="entry">
|
<label className="entry">
|
|
@ -17,9 +17,28 @@
|
||||||
along with this program. If not, see <http://www.gnu.org/licenses/>.
|
along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
const React = require("react");
|
import React from "react";
|
||||||
|
|
||||||
function TextInput({ label, field, ...inputProps }) {
|
import type {
|
||||||
|
ReactNode,
|
||||||
|
RefObject,
|
||||||
|
} from "react";
|
||||||
|
|
||||||
|
import type {
|
||||||
|
FileFormInputHook,
|
||||||
|
RadioFormInputHook,
|
||||||
|
TextFormInputHook,
|
||||||
|
} from "../../lib/form/types";
|
||||||
|
|
||||||
|
export interface TextInputProps extends React.DetailedHTMLProps<
|
||||||
|
React.InputHTMLAttributes<HTMLInputElement>,
|
||||||
|
HTMLInputElement
|
||||||
|
> {
|
||||||
|
label?: string;
|
||||||
|
field: TextFormInputHook;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function TextInput({label, field, ...props}: TextInputProps) {
|
||||||
const { onChange, value, ref } = field;
|
const { onChange, value, ref } = field;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
@ -27,16 +46,25 @@ function TextInput({ label, field, ...inputProps }) {
|
||||||
<label>
|
<label>
|
||||||
{label}
|
{label}
|
||||||
<input
|
<input
|
||||||
type="text"
|
onChange={onChange}
|
||||||
{...{ onChange, value, ref }}
|
value={value}
|
||||||
{...inputProps}
|
ref={ref as RefObject<HTMLInputElement>}
|
||||||
|
{...props}
|
||||||
/>
|
/>
|
||||||
</label>
|
</label>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
function TextArea({ label, field, ...inputProps }) {
|
export interface TextAreaProps extends React.DetailedHTMLProps<
|
||||||
|
React.TextareaHTMLAttributes<HTMLTextAreaElement>,
|
||||||
|
HTMLTextAreaElement
|
||||||
|
> {
|
||||||
|
label?: string;
|
||||||
|
field: TextFormInputHook;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function TextArea({label, field, ...props}: TextAreaProps) {
|
||||||
const { onChange, value, ref } = field;
|
const { onChange, value, ref } = field;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
@ -44,16 +72,25 @@ function TextArea({ label, field, ...inputProps }) {
|
||||||
<label>
|
<label>
|
||||||
{label}
|
{label}
|
||||||
<textarea
|
<textarea
|
||||||
type="text"
|
onChange={onChange}
|
||||||
{...{ onChange, value, ref }}
|
value={value}
|
||||||
{...inputProps}
|
ref={ref as RefObject<HTMLTextAreaElement>}
|
||||||
|
{...props}
|
||||||
/>
|
/>
|
||||||
</label>
|
</label>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
function FileInput({ label, field, ...inputProps }) {
|
export interface FileInputProps extends React.DetailedHTMLProps<
|
||||||
|
React.InputHTMLAttributes<HTMLInputElement>,
|
||||||
|
HTMLInputElement
|
||||||
|
> {
|
||||||
|
label?: string;
|
||||||
|
field: FileFormInputHook;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function FileInput({ label, field, ...props }: FileInputProps) {
|
||||||
const { onChange, ref, infoComponent } = field;
|
const { onChange, ref, infoComponent } = field;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
@ -66,15 +103,16 @@ function FileInput({ label, field, ...inputProps }) {
|
||||||
<input
|
<input
|
||||||
type="file"
|
type="file"
|
||||||
className="hidden"
|
className="hidden"
|
||||||
{...{ onChange, ref }}
|
onChange={onChange}
|
||||||
{...inputProps}
|
ref={ref ? ref as RefObject<HTMLInputElement> : undefined}
|
||||||
|
{...props}
|
||||||
/>
|
/>
|
||||||
</label>
|
</label>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
function Checkbox({ label, field, ...inputProps }) {
|
export function Checkbox({ label, field, ...inputProps }) {
|
||||||
const { onChange, value } = field;
|
const { onChange, value } = field;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
@ -91,16 +129,29 @@ function Checkbox({ label, field, ...inputProps }) {
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
function Select({ label, field, options, children, ...inputProps }) {
|
export interface SelectProps extends React.DetailedHTMLProps<
|
||||||
|
React.SelectHTMLAttributes<HTMLSelectElement>,
|
||||||
|
HTMLSelectElement
|
||||||
|
> {
|
||||||
|
label?: string;
|
||||||
|
field: TextFormInputHook;
|
||||||
|
children?: ReactNode;
|
||||||
|
options: React.JSX.Element;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function Select({ label, field, children, options, ...props }: SelectProps) {
|
||||||
const { onChange, value, ref } = field;
|
const { onChange, value, ref } = field;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="form-field select">
|
<div className="form-field select">
|
||||||
<label>
|
<label>
|
||||||
{label} {children}
|
{label}
|
||||||
|
{children}
|
||||||
<select
|
<select
|
||||||
{...{ onChange, value, ref }}
|
onChange={onChange}
|
||||||
{...inputProps}
|
value={value}
|
||||||
|
ref={ref as RefObject<HTMLSelectElement>}
|
||||||
|
{...props}
|
||||||
>
|
>
|
||||||
{options}
|
{options}
|
||||||
</select>
|
</select>
|
||||||
|
@ -109,7 +160,15 @@ function Select({ label, field, options, children, ...inputProps }) {
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
function RadioGroup({ field, label, ...inputProps }) {
|
export interface RadioGroupProps extends React.DetailedHTMLProps<
|
||||||
|
React.InputHTMLAttributes<HTMLInputElement>,
|
||||||
|
HTMLInputElement
|
||||||
|
> {
|
||||||
|
label?: string;
|
||||||
|
field: RadioFormInputHook;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function RadioGroup({ label, field, ...props }: RadioGroupProps) {
|
||||||
return (
|
return (
|
||||||
<div className="form-field radio">
|
<div className="form-field radio">
|
||||||
{Object.entries(field.options).map(([value, radioLabel]) => (
|
{Object.entries(field.options).map(([value, radioLabel]) => (
|
||||||
|
@ -120,7 +179,7 @@ function RadioGroup({ field, label, ...inputProps }) {
|
||||||
value={value}
|
value={value}
|
||||||
checked={field.value == value}
|
checked={field.value == value}
|
||||||
onChange={field.onChange}
|
onChange={field.onChange}
|
||||||
{...inputProps}
|
{...props}
|
||||||
/>
|
/>
|
||||||
{radioLabel}
|
{radioLabel}
|
||||||
</label>
|
</label>
|
||||||
|
@ -129,12 +188,3 @@ function RadioGroup({ field, label, ...inputProps }) {
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
module.exports = {
|
|
||||||
TextInput,
|
|
||||||
TextArea,
|
|
||||||
FileInput,
|
|
||||||
Checkbox,
|
|
||||||
Select,
|
|
||||||
RadioGroup
|
|
||||||
};
|
|
|
@ -18,15 +18,17 @@
|
||||||
*/
|
*/
|
||||||
|
|
||||||
const React = require("react");
|
const React = require("react");
|
||||||
|
|
||||||
const query = require("../lib/query");
|
|
||||||
|
|
||||||
const Loading = require("./loading");
|
const Loading = require("./loading");
|
||||||
|
const {
|
||||||
|
useVerifyCredentialsQuery,
|
||||||
|
useLogoutMutation,
|
||||||
|
} = require("../lib/query/oauth");
|
||||||
|
const { useInstanceV1Query } = require("../lib/query");
|
||||||
|
|
||||||
module.exports = function UserLogoutCard() {
|
module.exports = function UserLogoutCard() {
|
||||||
const { data: profile, isLoading } = query.useVerifyCredentialsQuery();
|
const { data: profile, isLoading } = useVerifyCredentialsQuery();
|
||||||
const { data: instance } = query.useInstanceQuery();
|
const { data: instance } = useInstanceV1Query();
|
||||||
const [logoutQuery] = query.useLogoutMutation();
|
const [logoutQuery] = useLogoutMutation();
|
||||||
|
|
||||||
if (isLoading) {
|
if (isLoading) {
|
||||||
return <Loading />;
|
return <Loading />;
|
||||||
|
|
|
@ -30,6 +30,9 @@ const Loading = require("./components/loading");
|
||||||
const UserLogoutCard = require("./components/user-logout-card");
|
const UserLogoutCard = require("./components/user-logout-card");
|
||||||
const { RoleContext } = require("./lib/navigation/util");
|
const { RoleContext } = require("./lib/navigation/util");
|
||||||
|
|
||||||
|
const DomainPerms = require("./admin/domain-permissions").default;
|
||||||
|
const DomainPermsImportExport = require("./admin/domain-permissions/import-export").default;
|
||||||
|
|
||||||
require("./style.css");
|
require("./style.css");
|
||||||
|
|
||||||
const { Sidebar, ViewRouter } = createNavigation("/settings", [
|
const { Sidebar, ViewRouter } = createNavigation("/settings", [
|
||||||
|
@ -43,10 +46,11 @@ const { Sidebar, ViewRouter } = createNavigation("/settings", [
|
||||||
}, [
|
}, [
|
||||||
Item("Reports", { icon: "fa-flag", wildcard: true }, require("./admin/reports")),
|
Item("Reports", { icon: "fa-flag", wildcard: true }, require("./admin/reports")),
|
||||||
Item("Accounts", { icon: "fa-users", wildcard: true }, require("./admin/accounts")),
|
Item("Accounts", { icon: "fa-users", wildcard: true }, require("./admin/accounts")),
|
||||||
Menu("Federation", { icon: "fa-hubzilla" }, [
|
Menu("Domain Permissions", { icon: "fa-hubzilla" }, [
|
||||||
Item("Federation", { icon: "fa-hubzilla", url: "", wildcard: true }, require("./admin/federation")),
|
Item("Blocks", { icon: "fa-close", url: "block", wildcard: true }, DomainPerms),
|
||||||
Item("Import/Export", { icon: "fa-floppy-o", wildcard: true }, require("./admin/federation/import-export")),
|
Item("Allows", { icon: "fa-check", url: "allow", wildcard: true }, DomainPerms),
|
||||||
])
|
Item("Import/Export", { icon: "fa-floppy-o", url: "import-export", wildcard: true }, DomainPermsImportExport),
|
||||||
|
]),
|
||||||
]),
|
]),
|
||||||
Menu("Administration", {
|
Menu("Administration", {
|
||||||
url: "admin",
|
url: "admin",
|
||||||
|
|
|
@ -17,11 +17,19 @@
|
||||||
along with this program. If not, see <http://www.gnu.org/licenses/>.
|
along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
const React = require("react");
|
import { useState } from "react";
|
||||||
|
import type {
|
||||||
|
BoolFormInputHook,
|
||||||
|
CreateHookNames,
|
||||||
|
HookOpts,
|
||||||
|
} from "./types";
|
||||||
|
|
||||||
const _default = false;
|
const _default = false;
|
||||||
module.exports = function useBoolInput({ name, Name }, { initialValue = _default }) {
|
export default function useBoolInput(
|
||||||
const [value, setValue] = React.useState(initialValue);
|
{ name, Name }: CreateHookNames,
|
||||||
|
{ initialValue = _default }: HookOpts<boolean>
|
||||||
|
): BoolFormInputHook {
|
||||||
|
const [value, setValue] = useState(initialValue);
|
||||||
|
|
||||||
function onChange(e) {
|
function onChange(e) {
|
||||||
setValue(e.target.checked);
|
setValue(e.target.checked);
|
||||||
|
@ -41,6 +49,7 @@ module.exports = function useBoolInput({ name, Name }, { initialValue = _default
|
||||||
}
|
}
|
||||||
], {
|
], {
|
||||||
name,
|
name,
|
||||||
|
Name: "",
|
||||||
onChange,
|
onChange,
|
||||||
reset,
|
reset,
|
||||||
value,
|
value,
|
||||||
|
@ -48,4 +57,4 @@ module.exports = function useBoolInput({ name, Name }, { initialValue = _default
|
||||||
hasChanged: () => value != initialValue,
|
hasChanged: () => value != initialValue,
|
||||||
_default
|
_default
|
||||||
});
|
});
|
||||||
};
|
}
|
|
@ -17,37 +17,58 @@
|
||||||
along with this program. If not, see <http://www.gnu.org/licenses/>.
|
along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
const React = require("react");
|
import {
|
||||||
const syncpipe = require("syncpipe");
|
useReducer,
|
||||||
const { createSlice } = require("@reduxjs/toolkit");
|
useRef,
|
||||||
const { enableMapSet } = require("immer");
|
useEffect,
|
||||||
|
useCallback,
|
||||||
|
useMemo,
|
||||||
|
} from "react";
|
||||||
|
|
||||||
enableMapSet(); // for use in reducers
|
import { PayloadAction, createSlice } from "@reduxjs/toolkit";
|
||||||
|
|
||||||
|
import type {
|
||||||
|
Checkable,
|
||||||
|
ChecklistInputHook,
|
||||||
|
CreateHookNames,
|
||||||
|
HookOpts,
|
||||||
|
} from "./types";
|
||||||
|
|
||||||
|
// https://immerjs.github.io/immer/installation#pick-your-immer-version
|
||||||
|
import { enableMapSet } from "immer";
|
||||||
|
enableMapSet();
|
||||||
|
|
||||||
|
interface ChecklistState {
|
||||||
|
entries: { [k: string]: Checkable },
|
||||||
|
selectedEntries: Set<string>,
|
||||||
|
}
|
||||||
|
|
||||||
|
const initialState: ChecklistState = {
|
||||||
|
entries: {},
|
||||||
|
selectedEntries: new Set(),
|
||||||
|
};
|
||||||
|
|
||||||
const { reducer, actions } = createSlice({
|
const { reducer, actions } = createSlice({
|
||||||
name: "checklist",
|
name: "checklist",
|
||||||
initialState: {}, // not handled by slice itself
|
initialState, // not handled by slice itself
|
||||||
reducers: {
|
reducers: {
|
||||||
updateAll: (state, { payload: checked }) => {
|
updateAll: (state, { payload: checked }: PayloadAction<boolean>) => {
|
||||||
const selectedEntries = new Set();
|
const selectedEntries = new Set<string>();
|
||||||
return {
|
const entries = Object.fromEntries(
|
||||||
entries: syncpipe(state.entries, [
|
Object.values(state.entries).map((entry) => {
|
||||||
(_) => Object.values(_),
|
if (checked) {
|
||||||
(_) => _.map((entry) => {
|
// Cheekily add this to selected
|
||||||
if (checked) {
|
// entries while we're here.
|
||||||
selectedEntries.add(entry.key);
|
selectedEntries.add(entry.key);
|
||||||
}
|
}
|
||||||
return [entry.key, {
|
|
||||||
...entry,
|
return [entry.key, { ...entry, checked } ];
|
||||||
checked
|
})
|
||||||
}];
|
);
|
||||||
}),
|
|
||||||
(_) => Object.fromEntries(_)
|
return { entries, selectedEntries };
|
||||||
]),
|
|
||||||
selectedEntries
|
|
||||||
};
|
|
||||||
},
|
},
|
||||||
update: (state, { payload: { key, value } }) => {
|
update: (state, { payload: { key, value } }: PayloadAction<{key: string, value: Checkable}>) => {
|
||||||
if (value.checked !== undefined) {
|
if (value.checked !== undefined) {
|
||||||
if (value.checked === true) {
|
if (value.checked === true) {
|
||||||
state.selectedEntries.add(key);
|
state.selectedEntries.add(key);
|
||||||
|
@ -61,7 +82,7 @@ const { reducer, actions } = createSlice({
|
||||||
...value
|
...value
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
updateMultiple: (state, { payload }) => {
|
updateMultiple: (state, { payload }: PayloadAction<Array<[key: string, value: Checkable]>>) => {
|
||||||
payload.forEach(([key, value]) => {
|
payload.forEach(([key, value]) => {
|
||||||
if (value.checked !== undefined) {
|
if (value.checked !== undefined) {
|
||||||
if (value.checked === true) {
|
if (value.checked === true) {
|
||||||
|
@ -80,43 +101,57 @@ const { reducer, actions } = createSlice({
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
function initialState({ entries, uniqueKey, initialValue }) {
|
function initialHookState({
|
||||||
const selectedEntries = new Set();
|
entries,
|
||||||
|
uniqueKey,
|
||||||
|
initialValue,
|
||||||
|
}: {
|
||||||
|
entries: Checkable[],
|
||||||
|
uniqueKey: string,
|
||||||
|
initialValue: boolean,
|
||||||
|
}): ChecklistState {
|
||||||
|
const selectedEntries = new Set<string>();
|
||||||
|
const mappedEntries = Object.fromEntries(
|
||||||
|
entries.map((entry) => {
|
||||||
|
const key = entry[uniqueKey];
|
||||||
|
const checked = entry.checked ?? initialValue;
|
||||||
|
|
||||||
|
if (checked) {
|
||||||
|
selectedEntries.add(key);
|
||||||
|
} else {
|
||||||
|
selectedEntries.delete(key);
|
||||||
|
}
|
||||||
|
|
||||||
|
return [ key, { ...entry, key, checked } ];
|
||||||
|
})
|
||||||
|
);
|
||||||
|
|
||||||
return {
|
return {
|
||||||
entries: syncpipe(entries, [
|
entries: mappedEntries,
|
||||||
(_) => _.map((entry) => {
|
|
||||||
let key = entry[uniqueKey];
|
|
||||||
let checked = entry.checked ?? initialValue;
|
|
||||||
|
|
||||||
if (checked) {
|
|
||||||
selectedEntries.add(key);
|
|
||||||
} else {
|
|
||||||
selectedEntries.delete(key);
|
|
||||||
}
|
|
||||||
|
|
||||||
return [
|
|
||||||
key,
|
|
||||||
{
|
|
||||||
...entry,
|
|
||||||
key,
|
|
||||||
checked
|
|
||||||
}
|
|
||||||
];
|
|
||||||
}),
|
|
||||||
(_) => Object.fromEntries(_)
|
|
||||||
]),
|
|
||||||
selectedEntries
|
selectedEntries
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
module.exports = function useCheckListInput({ name }, { entries, uniqueKey = "key", initialValue = false }) {
|
const _default: { [k: string]: Checkable } = {};
|
||||||
const [state, dispatch] = React.useReducer(reducer, null,
|
|
||||||
() => initialState({ entries, uniqueKey, initialValue }) // initial state
|
export default function useCheckListInput(
|
||||||
|
/* eslint-disable no-unused-vars */
|
||||||
|
{ name, Name }: CreateHookNames,
|
||||||
|
{
|
||||||
|
entries = [],
|
||||||
|
uniqueKey = "key",
|
||||||
|
initialValue = false,
|
||||||
|
}: HookOpts<boolean>
|
||||||
|
): ChecklistInputHook {
|
||||||
|
const [state, dispatch] = useReducer(
|
||||||
|
reducer,
|
||||||
|
initialState,
|
||||||
|
(_) => initialHookState({ entries, uniqueKey, initialValue }) // initial state
|
||||||
);
|
);
|
||||||
|
|
||||||
const toggleAllRef = React.useRef(null);
|
const toggleAllRef = useRef<any>(null);
|
||||||
|
|
||||||
React.useEffect(() => {
|
useEffect(() => {
|
||||||
if (toggleAllRef.current != null) {
|
if (toggleAllRef.current != null) {
|
||||||
let some = state.selectedEntries.size > 0;
|
let some = state.selectedEntries.size > 0;
|
||||||
let all = false;
|
let all = false;
|
||||||
|
@ -130,22 +165,22 @@ module.exports = function useCheckListInput({ name }, { entries, uniqueKey = "ke
|
||||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||||
}, [state.selectedEntries]);
|
}, [state.selectedEntries]);
|
||||||
|
|
||||||
const reset = React.useCallback(
|
const reset = useCallback(
|
||||||
() => dispatch(actions.updateAll(initialValue)),
|
() => dispatch(actions.updateAll(initialValue)),
|
||||||
[initialValue]
|
[initialValue]
|
||||||
);
|
);
|
||||||
|
|
||||||
const onChange = React.useCallback(
|
const onChange = useCallback(
|
||||||
(key, value) => dispatch(actions.update({ key, value })),
|
(key, value) => dispatch(actions.update({ key, value })),
|
||||||
[]
|
[]
|
||||||
);
|
);
|
||||||
|
|
||||||
const updateMultiple = React.useCallback(
|
const updateMultiple = useCallback(
|
||||||
(entries) => dispatch(actions.updateMultiple(entries)),
|
(entries) => dispatch(actions.updateMultiple(entries)),
|
||||||
[]
|
[]
|
||||||
);
|
);
|
||||||
|
|
||||||
return React.useMemo(() => {
|
return useMemo(() => {
|
||||||
function toggleAll(e) {
|
function toggleAll(e) {
|
||||||
let checked = e.target.checked;
|
let checked = e.target.checked;
|
||||||
if (e.target.indeterminate) {
|
if (e.target.indeterminate) {
|
||||||
|
@ -165,7 +200,10 @@ module.exports = function useCheckListInput({ name }, { entries, uniqueKey = "ke
|
||||||
reset,
|
reset,
|
||||||
{ name }
|
{ name }
|
||||||
], {
|
], {
|
||||||
|
_default,
|
||||||
|
hasChanged: () => true,
|
||||||
name,
|
name,
|
||||||
|
Name: "",
|
||||||
value: state.entries,
|
value: state.entries,
|
||||||
onChange,
|
onChange,
|
||||||
selectedValues,
|
selectedValues,
|
||||||
|
@ -178,4 +216,4 @@ module.exports = function useCheckListInput({ name }, { entries, uniqueKey = "ke
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}, [state, reset, name, onChange, updateMultiple]);
|
}, [state, reset, name, onChange, updateMultiple]);
|
||||||
};
|
}
|
|
@ -17,13 +17,21 @@
|
||||||
along with this program. If not, see <http://www.gnu.org/licenses/>.
|
along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
const React = require("react");
|
import { useState } from "react";
|
||||||
|
|
||||||
const { useComboboxState } = require("ariakit/combobox");
|
import { useComboboxState } from "ariakit/combobox";
|
||||||
|
import {
|
||||||
|
ComboboxFormInputHook,
|
||||||
|
CreateHookNames,
|
||||||
|
HookOpts,
|
||||||
|
} from "./types";
|
||||||
|
|
||||||
const _default = "";
|
const _default = "";
|
||||||
module.exports = function useComboBoxInput({ name, Name }, { initialValue = _default }) {
|
export default function useComboBoxInput(
|
||||||
const [isNew, setIsNew] = React.useState(false);
|
{ name, Name }: CreateHookNames,
|
||||||
|
{ initialValue = _default }: HookOpts<string>
|
||||||
|
): ComboboxFormInputHook {
|
||||||
|
const [isNew, setIsNew] = useState(false);
|
||||||
|
|
||||||
const state = useComboboxState({
|
const state = useComboboxState({
|
||||||
defaultValue: initialValue,
|
defaultValue: initialValue,
|
||||||
|
@ -45,14 +53,15 @@ module.exports = function useComboBoxInput({ name, Name }, { initialValue = _def
|
||||||
[`set${Name}IsNew`]: setIsNew
|
[`set${Name}IsNew`]: setIsNew
|
||||||
}
|
}
|
||||||
], {
|
], {
|
||||||
|
reset,
|
||||||
name,
|
name,
|
||||||
|
Name: "", // Will be set by inputHook function.
|
||||||
state,
|
state,
|
||||||
value: state.value,
|
value: state.value,
|
||||||
setter: (val) => state.setValue(val),
|
setter: (val: string) => state.setValue(val),
|
||||||
hasChanged: () => state.value != initialValue,
|
hasChanged: () => state.value != initialValue,
|
||||||
isNew,
|
isNew,
|
||||||
setIsNew,
|
setIsNew,
|
||||||
reset,
|
|
||||||
_default
|
_default
|
||||||
});
|
});
|
||||||
};
|
}
|
|
@ -17,12 +17,19 @@
|
||||||
along with this program. If not, see <http://www.gnu.org/licenses/>.
|
along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
const React = require("react");
|
import { useRef, useMemo } from "react";
|
||||||
|
|
||||||
const getFormMutations = require("./get-form-mutations");
|
import getFormMutations from "./get-form-mutations";
|
||||||
|
|
||||||
function parseFields(entries, length) {
|
import type {
|
||||||
const fields = [];
|
CreateHookNames,
|
||||||
|
HookOpts,
|
||||||
|
FieldArrayInputHook,
|
||||||
|
HookedForm,
|
||||||
|
} from "./types";
|
||||||
|
|
||||||
|
function parseFields(entries: HookedForm[], length: number): HookedForm[] {
|
||||||
|
const fields: HookedForm[] = [];
|
||||||
|
|
||||||
for (let i = 0; i < length; i++) {
|
for (let i = 0; i < length; i++) {
|
||||||
if (entries[i] != undefined) {
|
if (entries[i] != undefined) {
|
||||||
|
@ -35,23 +42,38 @@ function parseFields(entries, length) {
|
||||||
return fields;
|
return fields;
|
||||||
}
|
}
|
||||||
|
|
||||||
module.exports = function useArrayInput({ name, _Name }, { initialValue, length = 0 }) {
|
export default function useArrayInput(
|
||||||
const fields = React.useRef({});
|
{ name }: CreateHookNames,
|
||||||
|
{
|
||||||
|
initialValue,
|
||||||
|
length = 0,
|
||||||
|
}: HookOpts,
|
||||||
|
): FieldArrayInputHook {
|
||||||
|
const _default: HookedForm[] = Array(length);
|
||||||
|
const fields = useRef<HookedForm[]>(_default);
|
||||||
|
|
||||||
const value = React.useMemo(() => parseFields(initialValue, length), [initialValue, length]);
|
const value = useMemo(
|
||||||
|
() => parseFields(initialValue, length),
|
||||||
|
[initialValue, length],
|
||||||
|
);
|
||||||
|
|
||||||
|
function hasUpdate() {
|
||||||
|
return Object.values(fields.current).some((fieldSet) => {
|
||||||
|
const { updatedFields } = getFormMutations(fieldSet, { changedOnly: true });
|
||||||
|
return updatedFields.length > 0;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
|
_default,
|
||||||
name,
|
name,
|
||||||
|
Name: "",
|
||||||
value,
|
value,
|
||||||
ctx: fields.current,
|
ctx: fields.current,
|
||||||
maxLength: length,
|
maxLength: length,
|
||||||
|
hasChanged: hasUpdate,
|
||||||
selectedValues() {
|
selectedValues() {
|
||||||
// if any form field changed, we need to re-send everything
|
if (hasUpdate()) {
|
||||||
const hasUpdate = Object.values(fields.current).some((fieldSet) => {
|
|
||||||
const { updatedFields } = getFormMutations(fieldSet, { changedOnly: true });
|
|
||||||
return updatedFields.length > 0;
|
|
||||||
});
|
|
||||||
if (hasUpdate) {
|
|
||||||
return Object.values(fields.current).map((fieldSet) => {
|
return Object.values(fields.current).map((fieldSet) => {
|
||||||
return getFormMutations(fieldSet, { changedOnly: false }).mutationData;
|
return getFormMutations(fieldSet, { changedOnly: false }).mutationData;
|
||||||
});
|
});
|
||||||
|
@ -60,4 +82,4 @@ module.exports = function useArrayInput({ name, _Name }, { initialValue, length
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
};
|
}
|
|
@ -17,47 +17,67 @@
|
||||||
along with this program. If not, see <http://www.gnu.org/licenses/>.
|
along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
const React = require("react");
|
import React from "react";
|
||||||
const prettierBytes = require("prettier-bytes");
|
|
||||||
|
|
||||||
module.exports = function useFileInput({ name, _Name }, {
|
import { useState } from "react";
|
||||||
withPreview,
|
import prettierBytes from "prettier-bytes";
|
||||||
maxSize,
|
|
||||||
initialInfo = "no file selected"
|
|
||||||
} = {}) {
|
|
||||||
const [file, setFile] = React.useState();
|
|
||||||
const [imageURL, setImageURL] = React.useState();
|
|
||||||
const [info, setInfo] = React.useState();
|
|
||||||
|
|
||||||
function onChange(e) {
|
import type {
|
||||||
let file = e.target.files[0];
|
CreateHookNames,
|
||||||
|
HookOpts,
|
||||||
|
FileFormInputHook,
|
||||||
|
} from "./types";
|
||||||
|
|
||||||
|
const _default = undefined;
|
||||||
|
export default function useFileInput(
|
||||||
|
{ name }: CreateHookNames,
|
||||||
|
{
|
||||||
|
withPreview,
|
||||||
|
maxSize,
|
||||||
|
initialInfo = "no file selected"
|
||||||
|
}: HookOpts<File>
|
||||||
|
): FileFormInputHook {
|
||||||
|
const [file, setFile] = useState<File>();
|
||||||
|
const [imageURL, setImageURL] = useState<string>();
|
||||||
|
const [info, setInfo] = useState<React.JSX.Element>();
|
||||||
|
|
||||||
|
function onChange(e: React.ChangeEvent<HTMLInputElement>) {
|
||||||
|
const files = e.target.files;
|
||||||
|
if (!files) {
|
||||||
|
setInfo(undefined);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
let file = files[0];
|
||||||
setFile(file);
|
setFile(file);
|
||||||
|
|
||||||
URL.revokeObjectURL(imageURL);
|
if (imageURL) {
|
||||||
|
URL.revokeObjectURL(imageURL);
|
||||||
if (file != undefined) {
|
|
||||||
if (withPreview) {
|
|
||||||
setImageURL(URL.createObjectURL(file));
|
|
||||||
}
|
|
||||||
|
|
||||||
let size = prettierBytes(file.size);
|
|
||||||
if (maxSize && file.size > maxSize) {
|
|
||||||
size = <span className="error-text">{size}</span>;
|
|
||||||
}
|
|
||||||
|
|
||||||
setInfo(<>
|
|
||||||
{file.name} ({size})
|
|
||||||
</>);
|
|
||||||
} else {
|
|
||||||
setInfo();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (withPreview) {
|
||||||
|
setImageURL(URL.createObjectURL(file));
|
||||||
|
}
|
||||||
|
|
||||||
|
let size = prettierBytes(file.size);
|
||||||
|
if (maxSize && file.size > maxSize) {
|
||||||
|
size = <span className="error-text">{size}</span>;
|
||||||
|
}
|
||||||
|
|
||||||
|
setInfo(
|
||||||
|
<>
|
||||||
|
{file.name} ({size})
|
||||||
|
</>
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
function reset() {
|
function reset() {
|
||||||
URL.revokeObjectURL(imageURL);
|
if (imageURL) {
|
||||||
setImageURL();
|
URL.revokeObjectURL(imageURL);
|
||||||
setFile();
|
}
|
||||||
setInfo();
|
setImageURL(undefined);
|
||||||
|
setFile(undefined);
|
||||||
|
setInfo(undefined);
|
||||||
}
|
}
|
||||||
|
|
||||||
const infoComponent = (
|
const infoComponent = (
|
||||||
|
@ -82,9 +102,11 @@ module.exports = function useFileInput({ name, _Name }, {
|
||||||
onChange,
|
onChange,
|
||||||
reset,
|
reset,
|
||||||
name,
|
name,
|
||||||
|
Name: "", // Will be set by inputHook function.
|
||||||
value: file,
|
value: file,
|
||||||
previewValue: imageURL,
|
previewValue: imageURL,
|
||||||
hasChanged: () => file != undefined,
|
hasChanged: () => file != undefined,
|
||||||
infoComponent
|
infoComponent,
|
||||||
|
_default,
|
||||||
});
|
});
|
||||||
};
|
}
|
|
@ -17,14 +17,31 @@
|
||||||
along with this program. If not, see <http://www.gnu.org/licenses/>.
|
along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
const React = require("react");
|
/* eslint-disable no-unused-vars */
|
||||||
const { Error } = require("../../components/error");
|
|
||||||
|
|
||||||
const Loading = require("../../components/loading");
|
import React from "react";
|
||||||
|
|
||||||
// Wrap Form component inside component that fires the RTK Query call,
|
import { Error } from "../../components/error";
|
||||||
// so Form will only be rendered when data is available to generate form-fields for
|
import Loading from "../../components/loading";
|
||||||
module.exports = function FormWithData({ dataQuery, DataForm, queryArg, ...formProps }) {
|
import { NoArg } from "../types/query";
|
||||||
|
import { FormWithDataQuery } from "./types";
|
||||||
|
|
||||||
|
export interface FormWithDataProps {
|
||||||
|
dataQuery: FormWithDataQuery,
|
||||||
|
DataForm: ({ data, ...props }) => React.JSX.Element,
|
||||||
|
queryArg?: any,
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Wrap Form component inside component that fires the RTK Query call, so Form
|
||||||
|
* will only be rendered when data is available to generate form-fields for.
|
||||||
|
*/
|
||||||
|
export default function FormWithData({ dataQuery, DataForm, queryArg, ...props }: FormWithDataProps) {
|
||||||
|
if (!queryArg) {
|
||||||
|
queryArg = NoArg;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Trigger provided query.
|
||||||
const { data, isLoading, isError, error } = dataQuery(queryArg);
|
const { data, isLoading, isError, error } = dataQuery(queryArg);
|
||||||
|
|
||||||
if (isLoading) {
|
if (isLoading) {
|
||||||
|
@ -38,6 +55,6 @@ module.exports = function FormWithData({ dataQuery, DataForm, queryArg, ...formP
|
||||||
<Error error={error} />
|
<Error error={error} />
|
||||||
);
|
);
|
||||||
} else {
|
} else {
|
||||||
return <DataForm data={data} {...formProps} />;
|
return <DataForm data={data} {...props} />;
|
||||||
}
|
}
|
||||||
};
|
}
|
|
@ -17,29 +17,31 @@
|
||||||
along with this program. If not, see <http://www.gnu.org/licenses/>.
|
along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
const syncpipe = require("syncpipe");
|
import { FormInputHook, HookedForm } from "./types";
|
||||||
|
|
||||||
|
export default function getFormMutations(
|
||||||
|
form: HookedForm,
|
||||||
|
{ changedOnly }: { changedOnly: boolean },
|
||||||
|
) {
|
||||||
|
const updatedFields: FormInputHook[] = [];
|
||||||
|
const mutationData: Array<[string, any]> = [];
|
||||||
|
|
||||||
|
Object.values(form).forEach((field) => {
|
||||||
|
if ("selectedValues" in field) {
|
||||||
|
// FieldArrayInputHook.
|
||||||
|
const selected = field.selectedValues();
|
||||||
|
if (!changedOnly || selected.length > 0) {
|
||||||
|
updatedFields.push(field);
|
||||||
|
mutationData.push([field.name, selected]);
|
||||||
|
}
|
||||||
|
} else if (!changedOnly || field.hasChanged()) {
|
||||||
|
updatedFields.push(field);
|
||||||
|
mutationData.push([field.name, field.value]);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
module.exports = function getFormMutations(form, { changedOnly }) {
|
|
||||||
let updatedFields = [];
|
|
||||||
return {
|
return {
|
||||||
updatedFields,
|
updatedFields,
|
||||||
mutationData: syncpipe(form, [
|
mutationData: Object.fromEntries(mutationData),
|
||||||
(_) => Object.values(_),
|
|
||||||
(_) => _.map((field) => {
|
|
||||||
if (field.selectedValues != undefined) {
|
|
||||||
let selected = field.selectedValues();
|
|
||||||
if (!changedOnly || selected.length > 0) {
|
|
||||||
updatedFields.push(field);
|
|
||||||
return [field.name, selected];
|
|
||||||
}
|
|
||||||
} else if (!changedOnly || field.hasChanged()) {
|
|
||||||
updatedFields.push(field);
|
|
||||||
return [field.name, field.value];
|
|
||||||
}
|
|
||||||
return null;
|
|
||||||
}),
|
|
||||||
(_) => _.filter((value) => value != null),
|
|
||||||
(_) => Object.fromEntries(_)
|
|
||||||
])
|
|
||||||
};
|
};
|
||||||
};
|
}
|
|
@ -1,83 +0,0 @@
|
||||||
/*
|
|
||||||
GoToSocial
|
|
||||||
Copyright (C) GoToSocial Authors admin@gotosocial.org
|
|
||||||
SPDX-License-Identifier: AGPL-3.0-or-later
|
|
||||||
|
|
||||||
This program is free software: you can redistribute it and/or modify
|
|
||||||
it under the terms of the GNU Affero General Public License as published by
|
|
||||||
the Free Software Foundation, either version 3 of the License, or
|
|
||||||
(at your option) any later version.
|
|
||||||
|
|
||||||
This program is distributed in the hope that it will be useful,
|
|
||||||
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
||||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
||||||
GNU Affero General Public License for more details.
|
|
||||||
|
|
||||||
You should have received a copy of the GNU Affero General Public License
|
|
||||||
along with this program. If not, see <http://www.gnu.org/licenses/>.
|
|
||||||
*/
|
|
||||||
|
|
||||||
const React = require("react");
|
|
||||||
const getByDot = require("get-by-dot").default;
|
|
||||||
|
|
||||||
function capitalizeFirst(str) {
|
|
||||||
return str.slice(0, 1).toUpperCase + str.slice(1);
|
|
||||||
}
|
|
||||||
|
|
||||||
function selectorByKey(key) {
|
|
||||||
if (key.includes("[")) {
|
|
||||||
// get-by-dot does not support 'nested[deeper][key]' notation, convert to 'nested.deeper.key'
|
|
||||||
key = key
|
|
||||||
.replace(/\[/g, ".") // nested.deeper].key]
|
|
||||||
.replace(/\]/g, ""); // nested.deeper.key
|
|
||||||
}
|
|
||||||
|
|
||||||
return function selector(obj) {
|
|
||||||
if (obj == undefined) {
|
|
||||||
return undefined;
|
|
||||||
} else {
|
|
||||||
return getByDot(obj, key);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
function makeHook(hookFunction) {
|
|
||||||
return function (name, opts = {}) {
|
|
||||||
// for dynamically generating attributes like 'setName'
|
|
||||||
const Name = React.useMemo(() => capitalizeFirst(name), [name]);
|
|
||||||
|
|
||||||
const selector = React.useMemo(() => selectorByKey(name), [name]);
|
|
||||||
const valueSelector = opts.valueSelector ?? selector;
|
|
||||||
|
|
||||||
opts.initialValue = React.useMemo(() => {
|
|
||||||
if (opts.source == undefined) {
|
|
||||||
return opts.defaultValue;
|
|
||||||
} else {
|
|
||||||
return valueSelector(opts.source) ?? opts.defaultValue;
|
|
||||||
}
|
|
||||||
}, [opts.source, opts.defaultValue, valueSelector]);
|
|
||||||
|
|
||||||
const hook = hookFunction({ name, Name }, opts);
|
|
||||||
|
|
||||||
return Object.assign(hook, {
|
|
||||||
name, Name,
|
|
||||||
});
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
module.exports = {
|
|
||||||
useTextInput: makeHook(require("./text")),
|
|
||||||
useFileInput: makeHook(require("./file")),
|
|
||||||
useBoolInput: makeHook(require("./bool")),
|
|
||||||
useRadioInput: makeHook(require("./radio")),
|
|
||||||
useComboBoxInput: makeHook(require("./combo-box")),
|
|
||||||
useCheckListInput: makeHook(require("./check-list")),
|
|
||||||
useFieldArrayInput: makeHook(require("./field-array")),
|
|
||||||
useValue: function (name, value) {
|
|
||||||
return {
|
|
||||||
name,
|
|
||||||
value,
|
|
||||||
hasChanged: () => true // always included
|
|
||||||
};
|
|
||||||
}
|
|
||||||
};
|
|
|
@ -0,0 +1,114 @@
|
||||||
|
/*
|
||||||
|
GoToSocial
|
||||||
|
Copyright (C) GoToSocial Authors admin@gotosocial.org
|
||||||
|
SPDX-License-Identifier: AGPL-3.0-or-later
|
||||||
|
|
||||||
|
This program is free software: you can redistribute it and/or modify
|
||||||
|
it under the terms of the GNU Affero General Public License as published by
|
||||||
|
the Free Software Foundation, either version 3 of the License, or
|
||||||
|
(at your option) any later version.
|
||||||
|
|
||||||
|
This program is distributed in the hope that it will be useful,
|
||||||
|
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
GNU Affero General Public License for more details.
|
||||||
|
|
||||||
|
You should have received a copy of the GNU Affero General Public License
|
||||||
|
along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { useMemo } from "react";
|
||||||
|
import getByDot from "get-by-dot";
|
||||||
|
|
||||||
|
import text from "./text";
|
||||||
|
import file from "./file";
|
||||||
|
import bool from "./bool";
|
||||||
|
import radio from "./radio";
|
||||||
|
import combobox from "./combo-box";
|
||||||
|
import checklist from "./check-list";
|
||||||
|
import fieldarray from "./field-array";
|
||||||
|
|
||||||
|
import type {
|
||||||
|
CreateHook,
|
||||||
|
FormInputHook,
|
||||||
|
HookOpts,
|
||||||
|
TextFormInputHook,
|
||||||
|
RadioFormInputHook,
|
||||||
|
FileFormInputHook,
|
||||||
|
BoolFormInputHook,
|
||||||
|
ComboboxFormInputHook,
|
||||||
|
FieldArrayInputHook,
|
||||||
|
ChecklistInputHook,
|
||||||
|
} from "./types";
|
||||||
|
|
||||||
|
function capitalizeFirst(str: string) {
|
||||||
|
return str.slice(0, 1).toUpperCase + str.slice(1);
|
||||||
|
}
|
||||||
|
|
||||||
|
function selectorByKey(key: string) {
|
||||||
|
if (key.includes("[")) {
|
||||||
|
// get-by-dot does not support 'nested[deeper][key]' notation, convert to 'nested.deeper.key'
|
||||||
|
key = key
|
||||||
|
.replace(/\[/g, ".") // nested.deeper].key]
|
||||||
|
.replace(/\]/g, ""); // nested.deeper.key
|
||||||
|
}
|
||||||
|
|
||||||
|
return function selector(obj) {
|
||||||
|
if (obj == undefined) {
|
||||||
|
return undefined;
|
||||||
|
} else {
|
||||||
|
return getByDot(obj, key);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Memoized hook generator function. Take a createHook
|
||||||
|
* function and use it to return a new FormInputHook function.
|
||||||
|
*
|
||||||
|
* @param createHook
|
||||||
|
* @returns
|
||||||
|
*/
|
||||||
|
function inputHook(createHook: CreateHook): (_name: string, _opts: HookOpts) => FormInputHook {
|
||||||
|
return (name: string, opts?: HookOpts): FormInputHook => {
|
||||||
|
// for dynamically generating attributes like 'setName'
|
||||||
|
const Name = useMemo(() => capitalizeFirst(name), [name]);
|
||||||
|
const selector = useMemo(() => selectorByKey(name), [name]);
|
||||||
|
const valueSelector = opts?.valueSelector?? selector;
|
||||||
|
|
||||||
|
if (opts) {
|
||||||
|
opts.initialValue = useMemo(() => {
|
||||||
|
if (opts.source == undefined) {
|
||||||
|
return opts.defaultValue;
|
||||||
|
} else {
|
||||||
|
return valueSelector(opts.source) ?? opts.defaultValue;
|
||||||
|
}
|
||||||
|
}, [opts.source, opts.defaultValue, valueSelector]);
|
||||||
|
}
|
||||||
|
|
||||||
|
const hook = createHook({ name, Name }, opts ?? {});
|
||||||
|
return Object.assign(hook, { name, Name });
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Simplest form hook type in town.
|
||||||
|
*/
|
||||||
|
function value<T>(name: string, initialValue: T) {
|
||||||
|
return {
|
||||||
|
_default: initialValue,
|
||||||
|
name,
|
||||||
|
Name: "",
|
||||||
|
value: initialValue,
|
||||||
|
hasChanged: () => true, // always included
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export const useTextInput = inputHook(text) as (_name: string, _opts?: HookOpts<string>) => TextFormInputHook;
|
||||||
|
export const useFileInput = inputHook(file) as (_name: string, _opts?: HookOpts<File>) => FileFormInputHook;
|
||||||
|
export const useBoolInput = inputHook(bool) as (_name: string, _opts?: HookOpts<boolean>) => BoolFormInputHook;
|
||||||
|
export const useRadioInput = inputHook(radio) as (_name: string, _opts?: HookOpts<string>) => RadioFormInputHook;
|
||||||
|
export const useComboBoxInput = inputHook(combobox) as (_name: string, _opts?: HookOpts<string>) => ComboboxFormInputHook;
|
||||||
|
export const useCheckListInput = inputHook(checklist) as (_name: string, _opts?: HookOpts<boolean>) => ChecklistInputHook;
|
||||||
|
export const useFieldArrayInput = inputHook(fieldarray) as (_name: string, _opts?: HookOpts<string>) => FieldArrayInputHook;
|
||||||
|
export const useValue = value as <T>(_name: string, _initialValue: T) => FormInputHook<T>;
|
|
@ -17,11 +17,18 @@
|
||||||
along with this program. If not, see <http://www.gnu.org/licenses/>.
|
along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
const React = require("react");
|
import { useState } from "react";
|
||||||
|
import { CreateHookNames, HookOpts, RadioFormInputHook } from "./types";
|
||||||
|
|
||||||
const _default = "";
|
const _default = "";
|
||||||
module.exports = function useRadioInput({ name, Name }, { initialValue = _default, options }) {
|
export default function useRadioInput(
|
||||||
const [value, setValue] = React.useState(initialValue);
|
{ name, Name }: CreateHookNames,
|
||||||
|
{
|
||||||
|
initialValue = _default,
|
||||||
|
options = {},
|
||||||
|
}: HookOpts<string>
|
||||||
|
): RadioFormInputHook {
|
||||||
|
const [value, setValue] = useState(initialValue);
|
||||||
|
|
||||||
function onChange(e) {
|
function onChange(e) {
|
||||||
setValue(e.target.value);
|
setValue(e.target.value);
|
||||||
|
@ -40,13 +47,14 @@ module.exports = function useRadioInput({ name, Name }, { initialValue = _defaul
|
||||||
[`set${Name}`]: setValue
|
[`set${Name}`]: setValue
|
||||||
}
|
}
|
||||||
], {
|
], {
|
||||||
name,
|
|
||||||
onChange,
|
onChange,
|
||||||
reset,
|
reset,
|
||||||
|
name,
|
||||||
|
Name: "",
|
||||||
value,
|
value,
|
||||||
setter: setValue,
|
setter: setValue,
|
||||||
options,
|
options,
|
||||||
hasChanged: () => value != initialValue,
|
hasChanged: () => value != initialValue,
|
||||||
_default
|
_default
|
||||||
});
|
});
|
||||||
};
|
}
|
|
@ -1,67 +0,0 @@
|
||||||
/*
|
|
||||||
GoToSocial
|
|
||||||
Copyright (C) GoToSocial Authors admin@gotosocial.org
|
|
||||||
SPDX-License-Identifier: AGPL-3.0-or-later
|
|
||||||
|
|
||||||
This program is free software: you can redistribute it and/or modify
|
|
||||||
it under the terms of the GNU Affero General Public License as published by
|
|
||||||
the Free Software Foundation, either version 3 of the License, or
|
|
||||||
(at your option) any later version.
|
|
||||||
|
|
||||||
This program is distributed in the hope that it will be useful,
|
|
||||||
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
||||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
||||||
GNU Affero General Public License for more details.
|
|
||||||
|
|
||||||
You should have received a copy of the GNU Affero General Public License
|
|
||||||
along with this program. If not, see <http://www.gnu.org/licenses/>.
|
|
||||||
*/
|
|
||||||
|
|
||||||
const Promise = require("bluebird");
|
|
||||||
const React = require("react");
|
|
||||||
const getFormMutations = require("./get-form-mutations");
|
|
||||||
|
|
||||||
module.exports = function useFormSubmit(form, mutationQuery, { changedOnly = true, onFinish } = {}) {
|
|
||||||
if (!Array.isArray(mutationQuery)) {
|
|
||||||
throw new ("useFormSubmit: mutationQuery was not an Array. Is a valid useMutation RTK Query provided?");
|
|
||||||
}
|
|
||||||
const [runMutation, result] = mutationQuery;
|
|
||||||
const usedAction = React.useRef(null);
|
|
||||||
return [
|
|
||||||
function submitForm(e) {
|
|
||||||
let action;
|
|
||||||
if (e?.preventDefault) {
|
|
||||||
e.preventDefault();
|
|
||||||
action = e.nativeEvent.submitter.name;
|
|
||||||
} else {
|
|
||||||
action = e;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (action == "") {
|
|
||||||
action = undefined;
|
|
||||||
}
|
|
||||||
usedAction.current = action;
|
|
||||||
// transform the field definitions into an object with just their values
|
|
||||||
|
|
||||||
const { mutationData, updatedFields } = getFormMutations(form, { changedOnly });
|
|
||||||
|
|
||||||
if (updatedFields.length == 0) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
mutationData.action = action;
|
|
||||||
|
|
||||||
return Promise.try(() => {
|
|
||||||
return runMutation(mutationData);
|
|
||||||
}).then((res) => {
|
|
||||||
if (onFinish) {
|
|
||||||
return onFinish(res);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
},
|
|
||||||
{
|
|
||||||
...result,
|
|
||||||
action: usedAction.current
|
|
||||||
}
|
|
||||||
];
|
|
||||||
};
|
|
|
@ -0,0 +1,140 @@
|
||||||
|
/*
|
||||||
|
GoToSocial
|
||||||
|
Copyright (C) GoToSocial Authors admin@gotosocial.org
|
||||||
|
SPDX-License-Identifier: AGPL-3.0-or-later
|
||||||
|
|
||||||
|
This program is free software: you can redistribute it and/or modify
|
||||||
|
it under the terms of the GNU Affero General Public License as published by
|
||||||
|
the Free Software Foundation, either version 3 of the License, or
|
||||||
|
(at your option) any later version.
|
||||||
|
|
||||||
|
This program is distributed in the hope that it will be useful,
|
||||||
|
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
GNU Affero General Public License for more details.
|
||||||
|
|
||||||
|
You should have received a copy of the GNU Affero General Public License
|
||||||
|
along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import getFormMutations from "./get-form-mutations";
|
||||||
|
|
||||||
|
import { useRef } from "react";
|
||||||
|
|
||||||
|
import type {
|
||||||
|
MutationTrigger,
|
||||||
|
UseMutationStateResult,
|
||||||
|
} from "@reduxjs/toolkit/dist/query/react/buildHooks";
|
||||||
|
|
||||||
|
import type {
|
||||||
|
FormSubmitEvent,
|
||||||
|
FormSubmitFunction,
|
||||||
|
FormSubmitResult,
|
||||||
|
HookedForm,
|
||||||
|
} from "./types";
|
||||||
|
|
||||||
|
interface UseFormSubmitOptions {
|
||||||
|
changedOnly: boolean;
|
||||||
|
onFinish?: ((_res: any) => void);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Parse changed values from the hooked form into a request
|
||||||
|
* body, and submit it using the given mutation trigger.
|
||||||
|
*
|
||||||
|
* This function basically wraps RTK Query's submit methods to
|
||||||
|
* work with our hooked form interface.
|
||||||
|
*
|
||||||
|
* An `onFinish` callback function can be provided, which will
|
||||||
|
* be executed on a **successful** run of the given MutationTrigger,
|
||||||
|
* with the mutation result passed into it.
|
||||||
|
*
|
||||||
|
* If `changedOnly` is false, then **all** fields of the given HookedForm
|
||||||
|
* will be submitted to the mutation endpoint, not just changed ones.
|
||||||
|
*
|
||||||
|
* The returned function and result can be triggered and read
|
||||||
|
* from just like an RTK Query mutation hook result would be.
|
||||||
|
*
|
||||||
|
* See: https://redux-toolkit.js.org/rtk-query/usage/mutations#mutation-hook-behavior
|
||||||
|
*/
|
||||||
|
export default function useFormSubmit(
|
||||||
|
form: HookedForm,
|
||||||
|
mutationQuery: readonly [MutationTrigger<any>, UseMutationStateResult<any, any>],
|
||||||
|
opts: UseFormSubmitOptions = { changedOnly: true }
|
||||||
|
): [ FormSubmitFunction, FormSubmitResult ] {
|
||||||
|
if (!Array.isArray(mutationQuery)) {
|
||||||
|
throw "useFormSubmit: mutationQuery was not an Array. Is a valid useMutation RTK Query provided?";
|
||||||
|
}
|
||||||
|
|
||||||
|
const { changedOnly, onFinish } = opts;
|
||||||
|
const [runMutation, mutationResult] = mutationQuery;
|
||||||
|
const usedAction = useRef<FormSubmitEvent>(undefined);
|
||||||
|
|
||||||
|
const submitForm = async(e: FormSubmitEvent) => {
|
||||||
|
let action: FormSubmitEvent;
|
||||||
|
|
||||||
|
if (typeof e === "string") {
|
||||||
|
if (e !== "") {
|
||||||
|
// String action name was provided.
|
||||||
|
action = e;
|
||||||
|
} else {
|
||||||
|
// Empty string action name was provided.
|
||||||
|
action = undefined;
|
||||||
|
}
|
||||||
|
} else if (e) {
|
||||||
|
// Submit event action was provided.
|
||||||
|
e.preventDefault();
|
||||||
|
if (e.nativeEvent.submitter) {
|
||||||
|
// We want the name of the element that was invoked to submit this form,
|
||||||
|
// which will be something that extends HTMLElement, though we don't know
|
||||||
|
// what at this point.
|
||||||
|
//
|
||||||
|
// See: https://developer.mozilla.org/en-US/docs/Web/API/SubmitEvent/submitter
|
||||||
|
action = (e.nativeEvent.submitter as Object as { name: string }).name;
|
||||||
|
} else {
|
||||||
|
// No submitter defined. Fall back
|
||||||
|
// to just use the FormSubmitEvent.
|
||||||
|
action = e;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// Void or null or something
|
||||||
|
// else was provided.
|
||||||
|
action = undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
usedAction.current = action;
|
||||||
|
|
||||||
|
// Transform the hooked form into an object.
|
||||||
|
const {
|
||||||
|
mutationData,
|
||||||
|
updatedFields,
|
||||||
|
} = getFormMutations(form, { changedOnly });
|
||||||
|
|
||||||
|
// If there were no updated fields according to
|
||||||
|
// the form parsing then there's nothing for us
|
||||||
|
// to do, since remote and desired state match.
|
||||||
|
if (updatedFields.length == 0) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
mutationData.action = action;
|
||||||
|
|
||||||
|
try {
|
||||||
|
const res = await runMutation(mutationData);
|
||||||
|
if (onFinish) {
|
||||||
|
onFinish(res);
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
// eslint-disable-next-line no-console
|
||||||
|
console.error(`caught error running mutation: ${e}`);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
return [
|
||||||
|
submitForm,
|
||||||
|
{
|
||||||
|
...mutationResult,
|
||||||
|
action: usedAction.current
|
||||||
|
}
|
||||||
|
];
|
||||||
|
}
|
|
@ -17,26 +17,40 @@
|
||||||
along with this program. If not, see <http://www.gnu.org/licenses/>.
|
along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
const React = require("react");
|
import React, {
|
||||||
|
useState,
|
||||||
|
useRef,
|
||||||
|
useTransition,
|
||||||
|
useEffect,
|
||||||
|
} from "react";
|
||||||
|
|
||||||
|
import type {
|
||||||
|
CreateHookNames,
|
||||||
|
HookOpts,
|
||||||
|
TextFormInputHook,
|
||||||
|
} from "./types";
|
||||||
|
|
||||||
const _default = "";
|
const _default = "";
|
||||||
module.exports = function useTextInput({ name, Name }, {
|
|
||||||
initialValue = _default,
|
|
||||||
dontReset = false,
|
|
||||||
validator,
|
|
||||||
showValidation = true,
|
|
||||||
initValidation
|
|
||||||
} = {}) {
|
|
||||||
|
|
||||||
const [text, setText] = React.useState(initialValue);
|
export default function useTextInput(
|
||||||
const textRef = React.useRef(null);
|
{ name, Name }: CreateHookNames,
|
||||||
|
{
|
||||||
|
initialValue = _default,
|
||||||
|
dontReset = false,
|
||||||
|
validator,
|
||||||
|
showValidation = true,
|
||||||
|
initValidation
|
||||||
|
}: HookOpts<string>
|
||||||
|
): TextFormInputHook {
|
||||||
|
const [text, setText] = useState(initialValue);
|
||||||
|
const textRef = useRef<HTMLInputElement>(null);
|
||||||
|
|
||||||
const [validation, setValidation] = React.useState(initValidation ?? "");
|
const [validation, setValidation] = useState(initValidation ?? "");
|
||||||
const [_isValidating, startValidation] = React.useTransition();
|
const [_isValidating, startValidation] = useTransition();
|
||||||
let valid = validation == "";
|
const valid = validation == "";
|
||||||
|
|
||||||
function onChange(e) {
|
function onChange(e: React.ChangeEvent<HTMLInputElement>) {
|
||||||
let input = e.target.value;
|
const input = e.target.value;
|
||||||
setText(input);
|
setText(input);
|
||||||
|
|
||||||
if (validator) {
|
if (validator) {
|
||||||
|
@ -52,7 +66,7 @@ module.exports = function useTextInput({ name, Name }, {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
React.useEffect(() => {
|
useEffect(() => {
|
||||||
if (validator && textRef.current) {
|
if (validator && textRef.current) {
|
||||||
if (showValidation) {
|
if (showValidation) {
|
||||||
textRef.current.setCustomValidity(validation);
|
textRef.current.setCustomValidity(validation);
|
||||||
|
@ -76,12 +90,13 @@ module.exports = function useTextInput({ name, Name }, {
|
||||||
onChange,
|
onChange,
|
||||||
reset,
|
reset,
|
||||||
name,
|
name,
|
||||||
|
Name: "", // Will be set by inputHook function.
|
||||||
value: text,
|
value: text,
|
||||||
ref: textRef,
|
ref: textRef,
|
||||||
setter: setText,
|
setter: setText,
|
||||||
valid,
|
valid,
|
||||||
validate: () => setValidation(validator(text)),
|
validate: () => setValidation(validator ? validator(text): ""),
|
||||||
hasChanged: () => text != initialValue,
|
hasChanged: () => text != initialValue,
|
||||||
_default
|
_default
|
||||||
});
|
});
|
||||||
};
|
}
|
|
@ -0,0 +1,264 @@
|
||||||
|
/*
|
||||||
|
GoToSocial
|
||||||
|
Copyright (C) GoToSocial Authors admin@gotosocial.org
|
||||||
|
SPDX-License-Identifier: AGPL-3.0-or-later
|
||||||
|
|
||||||
|
This program is free software: you can redistribute it and/or modify
|
||||||
|
it under the terms of the GNU Affero General Public License as published by
|
||||||
|
the Free Software Foundation, either version 3 of the License, or
|
||||||
|
(at your option) any later version.
|
||||||
|
|
||||||
|
This program is distributed in the hope that it will be useful,
|
||||||
|
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
GNU Affero General Public License for more details.
|
||||||
|
|
||||||
|
You should have received a copy of the GNU Affero General Public License
|
||||||
|
along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||||
|
*/
|
||||||
|
|
||||||
|
/* eslint-disable no-unused-vars */
|
||||||
|
|
||||||
|
import { ComboboxState } from "ariakit";
|
||||||
|
import React from "react";
|
||||||
|
|
||||||
|
import {
|
||||||
|
ChangeEventHandler,
|
||||||
|
Dispatch,
|
||||||
|
RefObject,
|
||||||
|
SetStateAction,
|
||||||
|
SyntheticEvent,
|
||||||
|
} from "react";
|
||||||
|
|
||||||
|
export interface CreateHookNames {
|
||||||
|
name: string;
|
||||||
|
Name: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface HookOpts<T = any> {
|
||||||
|
initialValue?: T,
|
||||||
|
defaultValue?: T,
|
||||||
|
|
||||||
|
dontReset?: boolean,
|
||||||
|
validator?,
|
||||||
|
showValidation?: boolean,
|
||||||
|
initValidation?: string,
|
||||||
|
length?: number;
|
||||||
|
options?: { [_: string]: string },
|
||||||
|
withPreview?: boolean,
|
||||||
|
maxSize?,
|
||||||
|
initialInfo?: string;
|
||||||
|
valueSelector?: Function,
|
||||||
|
source?,
|
||||||
|
|
||||||
|
// checklist input types
|
||||||
|
entries?: any[];
|
||||||
|
uniqueKey?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export type CreateHook = (
|
||||||
|
name: CreateHookNames,
|
||||||
|
opts: HookOpts,
|
||||||
|
) => FormInputHook;
|
||||||
|
|
||||||
|
export interface FormInputHook<T = any> {
|
||||||
|
/**
|
||||||
|
* Name of this FormInputHook, as provided
|
||||||
|
* in the UseFormInputHook options.
|
||||||
|
*/
|
||||||
|
name: string;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* `name` with first letter capitalized.
|
||||||
|
*/
|
||||||
|
Name: string;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Current value of this FormInputHook.
|
||||||
|
*/
|
||||||
|
value?: T;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Default value of this FormInputHook.
|
||||||
|
*/
|
||||||
|
_default: T;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Return true if the values of this hook is considered
|
||||||
|
* to have been changed from the default / initial value.
|
||||||
|
*/
|
||||||
|
hasChanged: () => boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface _withReset {
|
||||||
|
reset: () => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface _withOnChange {
|
||||||
|
onChange: ChangeEventHandler;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface _withSetter<T> {
|
||||||
|
setter: Dispatch<SetStateAction<T>>;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface _withValidate {
|
||||||
|
valid: boolean;
|
||||||
|
validate: () => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface _withRef {
|
||||||
|
ref: RefObject<HTMLElement>;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface _withFile {
|
||||||
|
previewValue?: string;
|
||||||
|
infoComponent: React.JSX.Element;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface _withComboboxState {
|
||||||
|
state: ComboboxState;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface _withNew {
|
||||||
|
isNew: boolean;
|
||||||
|
setIsNew: Dispatch<SetStateAction<boolean>>;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface _withSelectedValues {
|
||||||
|
selectedValues: () => {
|
||||||
|
[_: string]: any;
|
||||||
|
}[]
|
||||||
|
}
|
||||||
|
|
||||||
|
interface _withCtx {
|
||||||
|
ctx
|
||||||
|
}
|
||||||
|
|
||||||
|
interface _withMaxLength {
|
||||||
|
maxLength: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface _withOptions {
|
||||||
|
options: { [_: string]: string };
|
||||||
|
}
|
||||||
|
|
||||||
|
interface _withToggleAll {
|
||||||
|
toggleAll: _withRef & _withOnChange
|
||||||
|
}
|
||||||
|
|
||||||
|
interface _withSomeSelected {
|
||||||
|
someSelected: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface _withUpdateMultiple {
|
||||||
|
updateMultiple: (_entries: any) => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface TextFormInputHook extends FormInputHook<string>,
|
||||||
|
_withSetter<string>,
|
||||||
|
_withOnChange,
|
||||||
|
_withReset,
|
||||||
|
_withValidate,
|
||||||
|
_withRef {}
|
||||||
|
|
||||||
|
export interface RadioFormInputHook extends FormInputHook<string>,
|
||||||
|
_withSetter<string>,
|
||||||
|
_withOnChange,
|
||||||
|
_withOptions,
|
||||||
|
_withReset {}
|
||||||
|
|
||||||
|
export interface FileFormInputHook extends FormInputHook<File | undefined>,
|
||||||
|
_withOnChange,
|
||||||
|
_withReset,
|
||||||
|
Partial<_withRef>,
|
||||||
|
_withFile {}
|
||||||
|
|
||||||
|
export interface BoolFormInputHook extends FormInputHook<boolean>,
|
||||||
|
_withSetter<boolean>,
|
||||||
|
_withOnChange,
|
||||||
|
_withReset {}
|
||||||
|
|
||||||
|
export interface ComboboxFormInputHook extends FormInputHook<string>,
|
||||||
|
_withSetter<string>,
|
||||||
|
_withComboboxState,
|
||||||
|
_withNew,
|
||||||
|
_withReset {}
|
||||||
|
|
||||||
|
export interface FieldArrayInputHook extends FormInputHook<HookedForm[]>,
|
||||||
|
_withSelectedValues,
|
||||||
|
_withMaxLength,
|
||||||
|
_withCtx {}
|
||||||
|
|
||||||
|
export interface Checkable {
|
||||||
|
key: string;
|
||||||
|
checked?: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface ChecklistInputHook<T = Checkable> extends FormInputHook<{[k: string]: T}>,
|
||||||
|
_withReset,
|
||||||
|
_withToggleAll,
|
||||||
|
_withSelectedValues,
|
||||||
|
_withSomeSelected,
|
||||||
|
_withUpdateMultiple {
|
||||||
|
// Uses its own funky onChange handler.
|
||||||
|
onChange: (key: any, value: any) => void
|
||||||
|
}
|
||||||
|
|
||||||
|
export type AnyFormInputHook =
|
||||||
|
FormInputHook |
|
||||||
|
TextFormInputHook |
|
||||||
|
RadioFormInputHook |
|
||||||
|
FileFormInputHook |
|
||||||
|
BoolFormInputHook |
|
||||||
|
ComboboxFormInputHook |
|
||||||
|
FieldArrayInputHook |
|
||||||
|
ChecklistInputHook;
|
||||||
|
|
||||||
|
export interface HookedForm {
|
||||||
|
[_: string]: AnyFormInputHook
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Parameters for FormSubmitFunction.
|
||||||
|
*/
|
||||||
|
export type FormSubmitEvent = (string | SyntheticEvent<HTMLFormElement, Partial<SubmitEvent>> | undefined | void)
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Shadows "trigger" function for useMutation, but can also
|
||||||
|
* be passed to onSubmit property of forms as a handler.
|
||||||
|
*
|
||||||
|
* See: https://redux-toolkit.js.org/rtk-query/usage/mutations#mutation-hook-behavior
|
||||||
|
*/
|
||||||
|
export type FormSubmitFunction = ((_e: FormSubmitEvent) => void)
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Shadows redux mutation hook return values.
|
||||||
|
*
|
||||||
|
* See: https://redux-toolkit.js.org/rtk-query/usage/mutations#frequently-used-mutation-hook-return-values
|
||||||
|
*/
|
||||||
|
export interface FormSubmitResult {
|
||||||
|
/**
|
||||||
|
* Action used to submit the form, if any.
|
||||||
|
*/
|
||||||
|
action: FormSubmitEvent;
|
||||||
|
data: any;
|
||||||
|
error: any;
|
||||||
|
isLoading: boolean;
|
||||||
|
isSuccess: boolean;
|
||||||
|
isError: boolean;
|
||||||
|
reset: () => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Shadows redux query hook return values.
|
||||||
|
*
|
||||||
|
* See: https://redux-toolkit.js.org/rtk-query/usage/queries#frequently-used-query-hook-return-values
|
||||||
|
*/
|
||||||
|
export type FormWithDataQuery = (_queryArg: any) => {
|
||||||
|
data?: any;
|
||||||
|
isLoading: boolean;
|
||||||
|
isError: boolean;
|
||||||
|
error?: any;
|
||||||
|
}
|
|
@ -1,194 +0,0 @@
|
||||||
/*
|
|
||||||
GoToSocial
|
|
||||||
Copyright (C) GoToSocial Authors admin@gotosocial.org
|
|
||||||
SPDX-License-Identifier: AGPL-3.0-or-later
|
|
||||||
|
|
||||||
This program is free software: you can redistribute it and/or modify
|
|
||||||
it under the terms of the GNU Affero General Public License as published by
|
|
||||||
the Free Software Foundation, either version 3 of the License, or
|
|
||||||
(at your option) any later version.
|
|
||||||
|
|
||||||
This program is distributed in the hope that it will be useful,
|
|
||||||
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
||||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
||||||
GNU Affero General Public License for more details.
|
|
||||||
|
|
||||||
You should have received a copy of the GNU Affero General Public License
|
|
||||||
along with this program. If not, see <http://www.gnu.org/licenses/>.
|
|
||||||
*/
|
|
||||||
|
|
||||||
const Promise = require("bluebird");
|
|
||||||
|
|
||||||
const { unwrapRes } = require("../lib");
|
|
||||||
|
|
||||||
module.exports = (build) => ({
|
|
||||||
listEmoji: build.query({
|
|
||||||
query: (params = {}) => ({
|
|
||||||
url: "/api/v1/admin/custom_emojis",
|
|
||||||
params: {
|
|
||||||
limit: 0,
|
|
||||||
...params
|
|
||||||
}
|
|
||||||
}),
|
|
||||||
providesTags: (res) =>
|
|
||||||
res
|
|
||||||
? [...res.map((emoji) => ({ type: "Emoji", id: emoji.id })), { type: "Emoji", id: "LIST" }]
|
|
||||||
: [{ type: "Emoji", id: "LIST" }]
|
|
||||||
}),
|
|
||||||
|
|
||||||
getEmoji: build.query({
|
|
||||||
query: (id) => ({
|
|
||||||
url: `/api/v1/admin/custom_emojis/${id}`
|
|
||||||
}),
|
|
||||||
providesTags: (res, error, id) => [{ type: "Emoji", id }]
|
|
||||||
}),
|
|
||||||
|
|
||||||
addEmoji: build.mutation({
|
|
||||||
query: (form) => {
|
|
||||||
return {
|
|
||||||
method: "POST",
|
|
||||||
url: `/api/v1/admin/custom_emojis`,
|
|
||||||
asForm: true,
|
|
||||||
body: form,
|
|
||||||
discardEmpty: true
|
|
||||||
};
|
|
||||||
},
|
|
||||||
invalidatesTags: (res) =>
|
|
||||||
res
|
|
||||||
? [{ type: "Emoji", id: "LIST" }, { type: "Emoji", id: res.id }]
|
|
||||||
: [{ type: "Emoji", id: "LIST" }]
|
|
||||||
}),
|
|
||||||
|
|
||||||
editEmoji: build.mutation({
|
|
||||||
query: ({ id, ...patch }) => {
|
|
||||||
return {
|
|
||||||
method: "PATCH",
|
|
||||||
url: `/api/v1/admin/custom_emojis/${id}`,
|
|
||||||
asForm: true,
|
|
||||||
body: {
|
|
||||||
type: "modify",
|
|
||||||
...patch
|
|
||||||
}
|
|
||||||
};
|
|
||||||
},
|
|
||||||
invalidatesTags: (res) =>
|
|
||||||
res
|
|
||||||
? [{ type: "Emoji", id: "LIST" }, { type: "Emoji", id: res.id }]
|
|
||||||
: [{ type: "Emoji", id: "LIST" }]
|
|
||||||
}),
|
|
||||||
|
|
||||||
deleteEmoji: build.mutation({
|
|
||||||
query: (id) => ({
|
|
||||||
method: "DELETE",
|
|
||||||
url: `/api/v1/admin/custom_emojis/${id}`
|
|
||||||
}),
|
|
||||||
invalidatesTags: (res, error, id) => [{ type: "Emoji", id }]
|
|
||||||
}),
|
|
||||||
|
|
||||||
searchStatusForEmoji: build.mutation({
|
|
||||||
queryFn: (url, api, _extraOpts, baseQuery) => {
|
|
||||||
return Promise.try(() => {
|
|
||||||
return baseQuery({
|
|
||||||
url: `/api/v2/search?q=${encodeURIComponent(url)}&resolve=true&limit=1`
|
|
||||||
}).then(unwrapRes);
|
|
||||||
}).then((searchRes) => {
|
|
||||||
return emojiFromSearchResult(searchRes);
|
|
||||||
}).then(({ type, domain, list }) => {
|
|
||||||
const state = api.getState();
|
|
||||||
if (domain == new URL(state.oauth.instance).host) {
|
|
||||||
throw "LOCAL_INSTANCE";
|
|
||||||
}
|
|
||||||
|
|
||||||
// search for every mentioned emoji with the admin api to get their ID
|
|
||||||
return Promise.map(list, (emoji) => {
|
|
||||||
return baseQuery({
|
|
||||||
url: `/api/v1/admin/custom_emojis`,
|
|
||||||
params: {
|
|
||||||
filter: `domain:${domain},shortcode:${emoji.shortcode}`,
|
|
||||||
limit: 1
|
|
||||||
}
|
|
||||||
}).then((unwrapRes)).then((list) => list[0]);
|
|
||||||
}, { concurrency: 5 }).then((listWithIDs) => {
|
|
||||||
return {
|
|
||||||
data: {
|
|
||||||
type,
|
|
||||||
domain,
|
|
||||||
list: listWithIDs
|
|
||||||
}
|
|
||||||
};
|
|
||||||
});
|
|
||||||
}).catch((e) => {
|
|
||||||
return { error: e };
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}),
|
|
||||||
|
|
||||||
patchRemoteEmojis: build.mutation({
|
|
||||||
queryFn: ({ action, ...formData }, _api, _extraOpts, baseQuery) => {
|
|
||||||
const data = [];
|
|
||||||
const errors = [];
|
|
||||||
|
|
||||||
return Promise.each(formData.selectedEmoji, (emoji) => {
|
|
||||||
return Promise.try(() => {
|
|
||||||
let body = {
|
|
||||||
type: action
|
|
||||||
};
|
|
||||||
|
|
||||||
if (action == "copy") {
|
|
||||||
body.shortcode = emoji.shortcode;
|
|
||||||
if (formData.category.trim().length != 0) {
|
|
||||||
body.category = formData.category;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return baseQuery({
|
|
||||||
method: "PATCH",
|
|
||||||
url: `/api/v1/admin/custom_emojis/${emoji.id}`,
|
|
||||||
asForm: true,
|
|
||||||
body: body
|
|
||||||
}).then(unwrapRes);
|
|
||||||
}).then((res) => {
|
|
||||||
data.push([emoji.id, res]);
|
|
||||||
}).catch((e) => {
|
|
||||||
let msg = e.message ?? e;
|
|
||||||
if (e.data.error) {
|
|
||||||
msg = e.data.error;
|
|
||||||
}
|
|
||||||
errors.push([emoji.shortcode, msg]);
|
|
||||||
});
|
|
||||||
}).then(() => {
|
|
||||||
if (errors.length == 0) {
|
|
||||||
return { data };
|
|
||||||
} else {
|
|
||||||
return {
|
|
||||||
error: errors
|
|
||||||
};
|
|
||||||
}
|
|
||||||
});
|
|
||||||
},
|
|
||||||
invalidatesTags: () => [{ type: "Emoji", id: "LIST" }]
|
|
||||||
})
|
|
||||||
});
|
|
||||||
|
|
||||||
function emojiFromSearchResult(searchRes) {
|
|
||||||
/* Parses the search response, prioritizing a toot result,
|
|
||||||
and returns referenced custom emoji
|
|
||||||
*/
|
|
||||||
let type;
|
|
||||||
|
|
||||||
if (searchRes.statuses.length > 0) {
|
|
||||||
type = "statuses";
|
|
||||||
} else if (searchRes.accounts.length > 0) {
|
|
||||||
type = "accounts";
|
|
||||||
} else {
|
|
||||||
throw "NONE_FOUND";
|
|
||||||
}
|
|
||||||
|
|
||||||
let data = searchRes[type][0];
|
|
||||||
|
|
||||||
return {
|
|
||||||
type,
|
|
||||||
domain: (new URL(data.url)).host, // to get WEB_DOMAIN, see https://github.com/superseriousbusiness/gotosocial/issues/1225
|
|
||||||
list: data.emojis
|
|
||||||
};
|
|
||||||
}
|
|
|
@ -0,0 +1,307 @@
|
||||||
|
/*
|
||||||
|
GoToSocial
|
||||||
|
Copyright (C) GoToSocial Authors admin@gotosocial.org
|
||||||
|
SPDX-License-Identifier: AGPL-3.0-or-later
|
||||||
|
|
||||||
|
This program is free software: you can redistribute it and/or modify
|
||||||
|
it under the terms of the GNU Affero General Public License as published by
|
||||||
|
the Free Software Foundation, either version 3 of the License, or
|
||||||
|
(at your option) any later version.
|
||||||
|
|
||||||
|
This program is distributed in the hope that it will be useful,
|
||||||
|
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
GNU Affero General Public License for more details.
|
||||||
|
|
||||||
|
You should have received a copy of the GNU Affero General Public License
|
||||||
|
along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { gtsApi } from "../../gts-api";
|
||||||
|
import { FetchBaseQueryError } from "@reduxjs/toolkit/query";
|
||||||
|
import { RootState } from "../../../../redux/store";
|
||||||
|
|
||||||
|
import type { CustomEmoji, EmojisFromItem, ListEmojiParams } from "../../../types/custom-emoji";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Parses the search response, prioritizing a status
|
||||||
|
* result, and returns any referenced custom emoji.
|
||||||
|
*
|
||||||
|
* Due to current API constraints, the returned emojis
|
||||||
|
* will not have their ID property set, so further
|
||||||
|
* processing is required to retrieve the IDs.
|
||||||
|
*
|
||||||
|
* @param searchRes
|
||||||
|
* @returns
|
||||||
|
*/
|
||||||
|
function emojisFromSearchResult(searchRes): EmojisFromItem {
|
||||||
|
// We don't know in advance whether a searched URL
|
||||||
|
// is the URL for a status, or the URL for an account,
|
||||||
|
// but we can derive this by looking at which search
|
||||||
|
// result field actually has entries in it (if any).
|
||||||
|
let type: "statuses" | "accounts";
|
||||||
|
if (searchRes.statuses.length > 0) {
|
||||||
|
// We had status results,
|
||||||
|
// so this was a status URL.
|
||||||
|
type = "statuses";
|
||||||
|
} else if (searchRes.accounts.length > 0) {
|
||||||
|
// We had account results,
|
||||||
|
// so this was an account URL.
|
||||||
|
type = "accounts";
|
||||||
|
} else {
|
||||||
|
// Nada, zilch, we can't do
|
||||||
|
// anything with this.
|
||||||
|
throw "NONE_FOUND";
|
||||||
|
}
|
||||||
|
|
||||||
|
// Narrow type to discard all the other
|
||||||
|
// data on the result that we don't need.
|
||||||
|
const data: {
|
||||||
|
url: string;
|
||||||
|
emojis: CustomEmoji[];
|
||||||
|
} = searchRes[type][0];
|
||||||
|
|
||||||
|
return {
|
||||||
|
type,
|
||||||
|
// Workaround to get host rather than account domain.
|
||||||
|
// See https://github.com/superseriousbusiness/gotosocial/issues/1225.
|
||||||
|
domain: (new URL(data.url)).host,
|
||||||
|
list: data.emojis,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
const extended = gtsApi.injectEndpoints({
|
||||||
|
endpoints: (build) => ({
|
||||||
|
listEmoji: build.query<CustomEmoji[], ListEmojiParams | void>({
|
||||||
|
query: (params = {}) => ({
|
||||||
|
url: "/api/v1/admin/custom_emojis",
|
||||||
|
params: {
|
||||||
|
limit: 0,
|
||||||
|
...params
|
||||||
|
}
|
||||||
|
}),
|
||||||
|
providesTags: (res, _error, _arg) =>
|
||||||
|
res
|
||||||
|
? [
|
||||||
|
...res.map((emoji) => ({ type: "Emoji" as const, id: emoji.id })),
|
||||||
|
{ type: "Emoji", id: "LIST" }
|
||||||
|
]
|
||||||
|
: [{ type: "Emoji", id: "LIST" }]
|
||||||
|
}),
|
||||||
|
|
||||||
|
getEmoji: build.query<CustomEmoji, string>({
|
||||||
|
query: (id) => ({
|
||||||
|
url: `/api/v1/admin/custom_emojis/${id}`
|
||||||
|
}),
|
||||||
|
providesTags: (_res, _error, id) => [{ type: "Emoji", id }]
|
||||||
|
}),
|
||||||
|
|
||||||
|
addEmoji: build.mutation<CustomEmoji, Object>({
|
||||||
|
query: (form) => {
|
||||||
|
return {
|
||||||
|
method: "POST",
|
||||||
|
url: `/api/v1/admin/custom_emojis`,
|
||||||
|
asForm: true,
|
||||||
|
body: form,
|
||||||
|
discardEmpty: true
|
||||||
|
};
|
||||||
|
},
|
||||||
|
invalidatesTags: (res) =>
|
||||||
|
res
|
||||||
|
? [{ type: "Emoji", id: "LIST" }, { type: "Emoji", id: res.id }]
|
||||||
|
: [{ type: "Emoji", id: "LIST" }]
|
||||||
|
}),
|
||||||
|
|
||||||
|
editEmoji: build.mutation<CustomEmoji, any>({
|
||||||
|
query: ({ id, ...patch }) => {
|
||||||
|
return {
|
||||||
|
method: "PATCH",
|
||||||
|
url: `/api/v1/admin/custom_emojis/${id}`,
|
||||||
|
asForm: true,
|
||||||
|
body: {
|
||||||
|
type: "modify",
|
||||||
|
...patch
|
||||||
|
}
|
||||||
|
};
|
||||||
|
},
|
||||||
|
invalidatesTags: (res) =>
|
||||||
|
res
|
||||||
|
? [{ type: "Emoji", id: "LIST" }, { type: "Emoji", id: res.id }]
|
||||||
|
: [{ type: "Emoji", id: "LIST" }]
|
||||||
|
}),
|
||||||
|
|
||||||
|
deleteEmoji: build.mutation<any, string>({
|
||||||
|
query: (id) => ({
|
||||||
|
method: "DELETE",
|
||||||
|
url: `/api/v1/admin/custom_emojis/${id}`
|
||||||
|
}),
|
||||||
|
invalidatesTags: (_res, _error, id) => [{ type: "Emoji", id }]
|
||||||
|
}),
|
||||||
|
|
||||||
|
searchItemForEmoji: build.mutation<EmojisFromItem, string>({
|
||||||
|
async queryFn(url, api, _extraOpts, fetchWithBQ) {
|
||||||
|
const state = api.getState() as RootState;
|
||||||
|
const oauthState = state.oauth;
|
||||||
|
|
||||||
|
// First search for given url.
|
||||||
|
const searchRes = await fetchWithBQ({
|
||||||
|
url: `/api/v2/search?q=${encodeURIComponent(url)}&resolve=true&limit=1`
|
||||||
|
});
|
||||||
|
if (searchRes.error) {
|
||||||
|
return { error: searchRes.error as FetchBaseQueryError };
|
||||||
|
}
|
||||||
|
|
||||||
|
// Parse initial results of search.
|
||||||
|
// These emojis will not have IDs set.
|
||||||
|
const {
|
||||||
|
type,
|
||||||
|
domain,
|
||||||
|
list: withoutIDs,
|
||||||
|
} = emojisFromSearchResult(searchRes.data);
|
||||||
|
|
||||||
|
// Ensure emojis domain is not OUR domain. If it
|
||||||
|
// is, we already have the emojis by definition.
|
||||||
|
if (oauthState.instanceUrl !== undefined) {
|
||||||
|
if (domain == new URL(oauthState.instanceUrl).host) {
|
||||||
|
throw "LOCAL_INSTANCE";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Search for each listed emoji with the admin
|
||||||
|
// api to get the version that includes an ID.
|
||||||
|
const withIDs: CustomEmoji[] = [];
|
||||||
|
const errors: FetchBaseQueryError[] = [];
|
||||||
|
|
||||||
|
withoutIDs.forEach(async(emoji) => {
|
||||||
|
// Request admin view of this emoji.
|
||||||
|
const emojiRes = await fetchWithBQ({
|
||||||
|
url: `/api/v1/admin/custom_emojis`,
|
||||||
|
params: {
|
||||||
|
filter: `domain:${domain},shortcode:${emoji.shortcode}`,
|
||||||
|
limit: 1
|
||||||
|
}
|
||||||
|
});
|
||||||
|
if (emojiRes.error) {
|
||||||
|
errors.push(emojiRes.error);
|
||||||
|
} else {
|
||||||
|
// Got it!
|
||||||
|
withIDs.push(emojiRes.data as CustomEmoji);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
if (errors.length !== 0) {
|
||||||
|
return {
|
||||||
|
error: {
|
||||||
|
status: 400,
|
||||||
|
statusText: 'Bad Request',
|
||||||
|
data: {"error":`One or more errors fetching custom emojis: ${errors}`},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
// Return our ID'd
|
||||||
|
// emojis list.
|
||||||
|
return {
|
||||||
|
data: {
|
||||||
|
type,
|
||||||
|
domain,
|
||||||
|
list: withIDs,
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}),
|
||||||
|
|
||||||
|
patchRemoteEmojis: build.mutation({
|
||||||
|
async queryFn({ action, ...formData }, _api, _extraOpts, fetchWithBQ) {
|
||||||
|
const data: CustomEmoji[] = [];
|
||||||
|
const errors: FetchBaseQueryError[] = [];
|
||||||
|
|
||||||
|
formData.selectEmoji.forEach(async(emoji: CustomEmoji) => {
|
||||||
|
let body = {
|
||||||
|
type: action,
|
||||||
|
shortcode: "",
|
||||||
|
category: "",
|
||||||
|
};
|
||||||
|
|
||||||
|
if (action == "copy") {
|
||||||
|
body.shortcode = emoji.shortcode;
|
||||||
|
if (formData.category.trim().length != 0) {
|
||||||
|
body.category = formData.category;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const emojiRes = await fetchWithBQ({
|
||||||
|
method: "PATCH",
|
||||||
|
url: `/api/v1/admin/custom_emojis/${emoji.id}`,
|
||||||
|
asForm: true,
|
||||||
|
body: body
|
||||||
|
});
|
||||||
|
if (emojiRes.error) {
|
||||||
|
errors.push(emojiRes.error);
|
||||||
|
} else {
|
||||||
|
// Got it!
|
||||||
|
data.push(emojiRes.data as CustomEmoji);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
if (errors.length !== 0) {
|
||||||
|
return {
|
||||||
|
error: {
|
||||||
|
status: 400,
|
||||||
|
statusText: 'Bad Request',
|
||||||
|
data: {"error":`One or more errors patching custom emojis: ${errors}`},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
return { data };
|
||||||
|
},
|
||||||
|
invalidatesTags: () => [{ type: "Emoji", id: "LIST" }]
|
||||||
|
})
|
||||||
|
})
|
||||||
|
});
|
||||||
|
|
||||||
|
/**
|
||||||
|
* List all custom emojis uploaded on our local instance.
|
||||||
|
*/
|
||||||
|
const useListEmojiQuery = extended.useListEmojiQuery;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get a single custom emoji uploaded on our local instance, by its ID.
|
||||||
|
*/
|
||||||
|
const useGetEmojiQuery = extended.useGetEmojiQuery;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Add a new custom emoji by uploading it to our local instance.
|
||||||
|
*/
|
||||||
|
const useAddEmojiMutation = extended.useAddEmojiMutation;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Edit an existing custom emoji that's already been uploaded to our local instance.
|
||||||
|
*/
|
||||||
|
const useEditEmojiMutation = extended.useEditEmojiMutation;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Delete a single custom emoji from our local instance using its id.
|
||||||
|
*/
|
||||||
|
const useDeleteEmojiMutation = extended.useDeleteEmojiMutation;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* "Steal this look" function for selecting remote emoji from a status or account.
|
||||||
|
*/
|
||||||
|
const useSearchItemForEmojiMutation = extended.useSearchItemForEmojiMutation;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Update/patch a bunch of remote emojis.
|
||||||
|
*/
|
||||||
|
const usePatchRemoteEmojisMutation = extended.usePatchRemoteEmojisMutation;
|
||||||
|
|
||||||
|
export {
|
||||||
|
useListEmojiQuery,
|
||||||
|
useGetEmojiQuery,
|
||||||
|
useAddEmojiMutation,
|
||||||
|
useEditEmojiMutation,
|
||||||
|
useDeleteEmojiMutation,
|
||||||
|
useSearchItemForEmojiMutation,
|
||||||
|
usePatchRemoteEmojisMutation,
|
||||||
|
};
|
|
@ -0,0 +1,155 @@
|
||||||
|
/*
|
||||||
|
GoToSocial
|
||||||
|
Copyright (C) GoToSocial Authors admin@gotosocial.org
|
||||||
|
SPDX-License-Identifier: AGPL-3.0-or-later
|
||||||
|
|
||||||
|
This program is free software: you can redistribute it and/or modify
|
||||||
|
it under the terms of the GNU Affero General Public License as published by
|
||||||
|
the Free Software Foundation, either version 3 of the License, or
|
||||||
|
(at your option) any later version.
|
||||||
|
|
||||||
|
This program is distributed in the hope that it will be useful,
|
||||||
|
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
GNU Affero General Public License for more details.
|
||||||
|
|
||||||
|
You should have received a copy of the GNU Affero General Public License
|
||||||
|
along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import fileDownload from "js-file-download";
|
||||||
|
import { unparse as csvUnparse } from "papaparse";
|
||||||
|
|
||||||
|
import { gtsApi } from "../../gts-api";
|
||||||
|
import { RootState } from "../../../../redux/store";
|
||||||
|
import { FetchBaseQueryError } from "@reduxjs/toolkit/query";
|
||||||
|
import { DomainPerm, ExportDomainPermsParams } from "../../../types/domain-permission";
|
||||||
|
|
||||||
|
interface _exportProcess {
|
||||||
|
transformEntry: (_entry: DomainPerm) => any;
|
||||||
|
stringify: (_list: any[]) => string;
|
||||||
|
extension: string;
|
||||||
|
mime: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Derive process functions and metadata
|
||||||
|
* from provided export request form.
|
||||||
|
*
|
||||||
|
* @param formData
|
||||||
|
* @returns
|
||||||
|
*/
|
||||||
|
function exportProcess(formData: ExportDomainPermsParams): _exportProcess {
|
||||||
|
if (formData.exportType == "json") {
|
||||||
|
return {
|
||||||
|
transformEntry: (entry) => ({
|
||||||
|
domain: entry.domain,
|
||||||
|
public_comment: entry.public_comment,
|
||||||
|
obfuscate: entry.obfuscate
|
||||||
|
}),
|
||||||
|
stringify: (list) => JSON.stringify(list),
|
||||||
|
extension: ".json",
|
||||||
|
mime: "application/json"
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
if (formData.exportType == "csv") {
|
||||||
|
return {
|
||||||
|
transformEntry: (entry) => [
|
||||||
|
entry.domain, // #domain
|
||||||
|
"suspend", // #severity
|
||||||
|
false, // #reject_media
|
||||||
|
false, // #reject_reports
|
||||||
|
entry.public_comment, // #public_comment
|
||||||
|
entry.obfuscate ?? false // #obfuscate
|
||||||
|
],
|
||||||
|
stringify: (list) => csvUnparse({
|
||||||
|
fields: [
|
||||||
|
"#domain",
|
||||||
|
"#severity",
|
||||||
|
"#reject_media",
|
||||||
|
"#reject_reports",
|
||||||
|
"#public_comment",
|
||||||
|
"#obfuscate",
|
||||||
|
],
|
||||||
|
data: list
|
||||||
|
}),
|
||||||
|
extension: ".csv",
|
||||||
|
mime: "text/csv"
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
// Fall back to plain text export.
|
||||||
|
return {
|
||||||
|
transformEntry: (entry) => entry.domain,
|
||||||
|
stringify: (list) => list.join("\n"),
|
||||||
|
extension: ".txt",
|
||||||
|
mime: "text/plain"
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
const extended = gtsApi.injectEndpoints({
|
||||||
|
endpoints: (build) => ({
|
||||||
|
exportDomainList: build.mutation<string | null, ExportDomainPermsParams>({
|
||||||
|
async queryFn(formData, api, _extraOpts, fetchWithBQ) {
|
||||||
|
// Fetch domain perms from relevant endpoint.
|
||||||
|
// We could have used 'useDomainBlocksQuery'
|
||||||
|
// or 'useDomainAllowsQuery' for this, but
|
||||||
|
// we want the untransformed array version.
|
||||||
|
const permsRes = await fetchWithBQ({ url: `/api/v1/admin/domain_${formData.permType}s` });
|
||||||
|
if (permsRes.error) {
|
||||||
|
return { error: permsRes.error as FetchBaseQueryError };
|
||||||
|
}
|
||||||
|
|
||||||
|
// Process perms into desired export format.
|
||||||
|
const process = exportProcess(formData);
|
||||||
|
const transformed = (permsRes.data as DomainPerm[]).map(process.transformEntry);
|
||||||
|
const exportAsString = process.stringify(transformed);
|
||||||
|
|
||||||
|
if (formData.action == "export") {
|
||||||
|
// Data will just be exported
|
||||||
|
// to the domains text field.
|
||||||
|
return { data: exportAsString };
|
||||||
|
}
|
||||||
|
|
||||||
|
// File export has been requested.
|
||||||
|
// Parse filename to something like:
|
||||||
|
// `example.org-blocklist-2023-10-09.json`.
|
||||||
|
const state = api.getState() as RootState;
|
||||||
|
const instanceUrl = state.oauth.instanceUrl?? "unknown";
|
||||||
|
const domain = new URL(instanceUrl).host;
|
||||||
|
const date = new Date();
|
||||||
|
const filename = [
|
||||||
|
domain,
|
||||||
|
"blocklist",
|
||||||
|
date.getFullYear(),
|
||||||
|
(date.getMonth() + 1).toString().padStart(2, "0"),
|
||||||
|
date.getDate().toString().padStart(2, "0"),
|
||||||
|
].join("-");
|
||||||
|
|
||||||
|
fileDownload(
|
||||||
|
exportAsString,
|
||||||
|
filename + process.extension,
|
||||||
|
process.mime
|
||||||
|
);
|
||||||
|
|
||||||
|
// js-file-download handles the
|
||||||
|
// nitty gritty for us, so we can
|
||||||
|
// just return null data.
|
||||||
|
return { data: null };
|
||||||
|
}
|
||||||
|
}),
|
||||||
|
})
|
||||||
|
});
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Makes a GET to `/api/v1/admin/domain_{perm_type}s`
|
||||||
|
* and exports the result in the requested format.
|
||||||
|
*
|
||||||
|
* Return type will be string if `action` is "export",
|
||||||
|
* else it will be null, since the file downloader handles
|
||||||
|
* the rest of the request then.
|
||||||
|
*/
|
||||||
|
const useExportDomainListMutation = extended.useExportDomainListMutation;
|
||||||
|
|
||||||
|
export { useExportDomainListMutation };
|
|
@ -0,0 +1,56 @@
|
||||||
|
/*
|
||||||
|
GoToSocial
|
||||||
|
Copyright (C) GoToSocial Authors admin@gotosocial.org
|
||||||
|
SPDX-License-Identifier: AGPL-3.0-or-later
|
||||||
|
|
||||||
|
This program is free software: you can redistribute it and/or modify
|
||||||
|
it under the terms of the GNU Affero General Public License as published by
|
||||||
|
the Free Software Foundation, either version 3 of the License, or
|
||||||
|
(at your option) any later version.
|
||||||
|
|
||||||
|
This program is distributed in the hope that it will be useful,
|
||||||
|
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
GNU Affero General Public License for more details.
|
||||||
|
|
||||||
|
You should have received a copy of the GNU Affero General Public License
|
||||||
|
along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { gtsApi } from "../../gts-api";
|
||||||
|
|
||||||
|
import type { DomainPerm, MappedDomainPerms } from "../../../types/domain-permission";
|
||||||
|
import { listToKeyedObject } from "../../transforms";
|
||||||
|
|
||||||
|
const extended = gtsApi.injectEndpoints({
|
||||||
|
endpoints: (build) => ({
|
||||||
|
domainBlocks: build.query<MappedDomainPerms, void>({
|
||||||
|
query: () => ({
|
||||||
|
url: `/api/v1/admin/domain_blocks`
|
||||||
|
}),
|
||||||
|
transformResponse: listToKeyedObject<DomainPerm>("domain"),
|
||||||
|
}),
|
||||||
|
|
||||||
|
domainAllows: build.query<MappedDomainPerms, void>({
|
||||||
|
query: () => ({
|
||||||
|
url: `/api/v1/admin/domain_allows`
|
||||||
|
}),
|
||||||
|
transformResponse: listToKeyedObject<DomainPerm>("domain"),
|
||||||
|
}),
|
||||||
|
}),
|
||||||
|
});
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get admin view of all explicitly blocked domains.
|
||||||
|
*/
|
||||||
|
const useDomainBlocksQuery = extended.useDomainBlocksQuery;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get admin view of all explicitly allowed domains.
|
||||||
|
*/
|
||||||
|
const useDomainAllowsQuery = extended.useDomainAllowsQuery;
|
||||||
|
|
||||||
|
export {
|
||||||
|
useDomainBlocksQuery,
|
||||||
|
useDomainAllowsQuery,
|
||||||
|
};
|
|
@ -0,0 +1,140 @@
|
||||||
|
/*
|
||||||
|
GoToSocial
|
||||||
|
Copyright (C) GoToSocial Authors admin@gotosocial.org
|
||||||
|
SPDX-License-Identifier: AGPL-3.0-or-later
|
||||||
|
|
||||||
|
This program is free software: you can redistribute it and/or modify
|
||||||
|
it under the terms of the GNU Affero General Public License as published by
|
||||||
|
the Free Software Foundation, either version 3 of the License, or
|
||||||
|
(at your option) any later version.
|
||||||
|
|
||||||
|
This program is distributed in the hope that it will be useful,
|
||||||
|
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
GNU Affero General Public License for more details.
|
||||||
|
|
||||||
|
You should have received a copy of the GNU Affero General Public License
|
||||||
|
along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { replaceCacheOnMutation } from "../../query-modifiers";
|
||||||
|
import { gtsApi } from "../../gts-api";
|
||||||
|
|
||||||
|
import {
|
||||||
|
type DomainPerm,
|
||||||
|
type ImportDomainPermsParams,
|
||||||
|
type MappedDomainPerms,
|
||||||
|
isDomainPermInternalKey,
|
||||||
|
} from "../../../types/domain-permission";
|
||||||
|
import { listToKeyedObject } from "../../transforms";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Builds up a map function that can be applied to a
|
||||||
|
* list of DomainPermission entries in order to normalize
|
||||||
|
* them before submission to the API.
|
||||||
|
* @param formData
|
||||||
|
* @returns
|
||||||
|
*/
|
||||||
|
function importEntriesProcessor(formData: ImportDomainPermsParams): (_entry: DomainPerm) => DomainPerm {
|
||||||
|
let processingFuncs: { (_entry: DomainPerm): void; }[] = [];
|
||||||
|
|
||||||
|
// Override each obfuscate entry if necessary.
|
||||||
|
if (formData.obfuscate !== undefined) {
|
||||||
|
const obfuscateEntry = (entry: DomainPerm) => {
|
||||||
|
entry.obfuscate = formData.obfuscate;
|
||||||
|
};
|
||||||
|
processingFuncs.push(obfuscateEntry);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check whether we need to append or replace
|
||||||
|
// private_comment and public_comment.
|
||||||
|
["private_comment","public_comment"].forEach((commentType) => {
|
||||||
|
let text = formData.commentType?.trim();
|
||||||
|
if (!text) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
switch(formData[`${commentType}_behavior`]) {
|
||||||
|
case "append":
|
||||||
|
const appendComment = (entry: DomainPerm) => {
|
||||||
|
if (entry.commentType == undefined) {
|
||||||
|
entry.commentType = text;
|
||||||
|
} else {
|
||||||
|
entry.commentType = [entry.commentType, text].join("\n");
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
processingFuncs.push(appendComment);
|
||||||
|
break;
|
||||||
|
case "replace":
|
||||||
|
const replaceComment = (entry: DomainPerm) => {
|
||||||
|
entry.commentType = text;
|
||||||
|
};
|
||||||
|
|
||||||
|
processingFuncs.push(replaceComment);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
return function process(entry) {
|
||||||
|
// Call all the assembled processing functions.
|
||||||
|
processingFuncs.forEach((f) => f(entry));
|
||||||
|
|
||||||
|
// Unset all internal processing keys
|
||||||
|
// and any undefined keys on this entry.
|
||||||
|
Object.entries(entry).forEach(([key, val]: [keyof DomainPerm, any]) => {
|
||||||
|
if (val == undefined || isDomainPermInternalKey(key)) {
|
||||||
|
delete entry[key];
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
return entry;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
const extended = gtsApi.injectEndpoints({
|
||||||
|
endpoints: (build) => ({
|
||||||
|
importDomainPerms: build.mutation<MappedDomainPerms, ImportDomainPermsParams>({
|
||||||
|
query: (formData) => {
|
||||||
|
// Add/replace comments, remove internal keys.
|
||||||
|
const process = importEntriesProcessor(formData);
|
||||||
|
const domains = formData.domains.map(process);
|
||||||
|
|
||||||
|
return {
|
||||||
|
method: "POST",
|
||||||
|
url: `/api/v1/admin/domain_${formData.permType}s?import=true`,
|
||||||
|
asForm: true,
|
||||||
|
discardEmpty: true,
|
||||||
|
body: {
|
||||||
|
import: true,
|
||||||
|
domains: new Blob(
|
||||||
|
[JSON.stringify(domains)],
|
||||||
|
{ type: "application/json" },
|
||||||
|
),
|
||||||
|
}
|
||||||
|
};
|
||||||
|
},
|
||||||
|
transformResponse: listToKeyedObject<DomainPerm>("domain"),
|
||||||
|
...replaceCacheOnMutation((formData: ImportDomainPermsParams) => {
|
||||||
|
// Query names for blocks and allows are like
|
||||||
|
// `domainBlocks` and `domainAllows`, so we need
|
||||||
|
// to convert `block` -> `Block` or `allow` -> `Allow`
|
||||||
|
// to do proper cache invalidation.
|
||||||
|
const permType =
|
||||||
|
formData.permType.charAt(0).toUpperCase() +
|
||||||
|
formData.permType.slice(1);
|
||||||
|
return `domain${permType}s`;
|
||||||
|
}),
|
||||||
|
})
|
||||||
|
})
|
||||||
|
});
|
||||||
|
|
||||||
|
/**
|
||||||
|
* POST domain permissions to /api/v1/admin/domain_{permType}s.
|
||||||
|
* Returns the newly created permissions.
|
||||||
|
*/
|
||||||
|
const useImportDomainPermsMutation = extended.useImportDomainPermsMutation;
|
||||||
|
|
||||||
|
export {
|
||||||
|
useImportDomainPermsMutation,
|
||||||
|
};
|
|
@ -0,0 +1,163 @@
|
||||||
|
/*
|
||||||
|
GoToSocial
|
||||||
|
Copyright (C) GoToSocial Authors admin@gotosocial.org
|
||||||
|
SPDX-License-Identifier: AGPL-3.0-or-later
|
||||||
|
|
||||||
|
This program is free software: you can redistribute it and/or modify
|
||||||
|
it under the terms of the GNU Affero General Public License as published by
|
||||||
|
the Free Software Foundation, either version 3 of the License, or
|
||||||
|
(at your option) any later version.
|
||||||
|
|
||||||
|
This program is distributed in the hope that it will be useful,
|
||||||
|
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
GNU Affero General Public License for more details.
|
||||||
|
|
||||||
|
You should have received a copy of the GNU Affero General Public License
|
||||||
|
along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import {
|
||||||
|
ParseConfig as CSVParseConfig,
|
||||||
|
parse as csvParse
|
||||||
|
} from "papaparse";
|
||||||
|
import { nanoid } from "nanoid";
|
||||||
|
|
||||||
|
import { isValidDomainPermission, hasBetterScope } from "../../../util/domain-permission";
|
||||||
|
import { gtsApi } from "../../gts-api";
|
||||||
|
|
||||||
|
import {
|
||||||
|
isDomainPerms,
|
||||||
|
type DomainPerm,
|
||||||
|
} from "../../../types/domain-permission";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Parse the given string of domain permissions and return it as an array.
|
||||||
|
* Accepts input as a JSON array string, a CSV, or newline-separated domain names.
|
||||||
|
* Will throw an error if input is invalid.
|
||||||
|
* @param list
|
||||||
|
* @returns
|
||||||
|
* @throws
|
||||||
|
*/
|
||||||
|
function parseDomainList(list: string): DomainPerm[] {
|
||||||
|
if (list.startsWith("[")) {
|
||||||
|
// Assume JSON array.
|
||||||
|
const data = JSON.parse(list);
|
||||||
|
if (!isDomainPerms(data)) {
|
||||||
|
throw "parsed JSON was not array of DomainPermission";
|
||||||
|
}
|
||||||
|
|
||||||
|
return data;
|
||||||
|
} else if (list.startsWith("#domain") || list.startsWith("domain,severity")) {
|
||||||
|
// Assume Mastodon-style CSV.
|
||||||
|
const csvParseCfg: CSVParseConfig = {
|
||||||
|
header: true,
|
||||||
|
// Remove leading '#' if present.
|
||||||
|
transformHeader: (header) => header.startsWith("#") ? header.slice(1) : header,
|
||||||
|
skipEmptyLines: true,
|
||||||
|
dynamicTyping: true
|
||||||
|
};
|
||||||
|
|
||||||
|
const { data, errors } = csvParse(list, csvParseCfg);
|
||||||
|
if (errors.length > 0) {
|
||||||
|
let error = "";
|
||||||
|
errors.forEach((err) => {
|
||||||
|
error += `${err.message} (line ${err.row})`;
|
||||||
|
});
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!isDomainPerms(data)) {
|
||||||
|
throw "parsed CSV was not array of DomainPermission";
|
||||||
|
}
|
||||||
|
|
||||||
|
return data;
|
||||||
|
} else {
|
||||||
|
// Fallback: assume newline-separated
|
||||||
|
// list of simple domain strings.
|
||||||
|
const data: DomainPerm[] = [];
|
||||||
|
list.split("\n").forEach((line) => {
|
||||||
|
let domain = line.trim();
|
||||||
|
let valid = true;
|
||||||
|
|
||||||
|
if (domain.startsWith("http")) {
|
||||||
|
try {
|
||||||
|
domain = new URL(domain).hostname;
|
||||||
|
} catch (e) {
|
||||||
|
valid = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (domain.length > 0) {
|
||||||
|
data.push({ domain, valid });
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
return data;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function deduplicateDomainList(list: DomainPerm[]): DomainPerm[] {
|
||||||
|
let domains = new Set();
|
||||||
|
return list.filter((entry) => {
|
||||||
|
if (domains.has(entry.domain)) {
|
||||||
|
return false;
|
||||||
|
} else {
|
||||||
|
domains.add(entry.domain);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function validateDomainList(list: DomainPerm[]) {
|
||||||
|
list.forEach((entry) => {
|
||||||
|
if (entry.domain.startsWith("*.")) {
|
||||||
|
// A domain permission always includes
|
||||||
|
// all subdomains, wildcard is meaningless here
|
||||||
|
entry.domain = entry.domain.slice(2);
|
||||||
|
}
|
||||||
|
|
||||||
|
entry.valid = (entry.valid !== false) && isValidDomainPermission(entry.domain);
|
||||||
|
if (entry.valid) {
|
||||||
|
entry.suggest = hasBetterScope(entry.domain);
|
||||||
|
}
|
||||||
|
entry.checked = entry.valid;
|
||||||
|
});
|
||||||
|
|
||||||
|
return list;
|
||||||
|
}
|
||||||
|
|
||||||
|
const extended = gtsApi.injectEndpoints({
|
||||||
|
endpoints: (build) => ({
|
||||||
|
processDomainPermissions: build.mutation<DomainPerm[], any>({
|
||||||
|
async queryFn(formData, _api, _extraOpts, _fetchWithBQ) {
|
||||||
|
if (formData.domains == undefined || formData.domains.length == 0) {
|
||||||
|
throw "No domains entered";
|
||||||
|
}
|
||||||
|
|
||||||
|
// Parse + tidy up the form data.
|
||||||
|
const permissions = parseDomainList(formData.domains);
|
||||||
|
const deduped = deduplicateDomainList(permissions);
|
||||||
|
const validated = validateDomainList(deduped);
|
||||||
|
|
||||||
|
validated.forEach((entry) => {
|
||||||
|
// Set unique key that stays stable
|
||||||
|
// even if domain gets modified by user.
|
||||||
|
entry.key = nanoid();
|
||||||
|
});
|
||||||
|
|
||||||
|
return { data: validated };
|
||||||
|
}
|
||||||
|
})
|
||||||
|
})
|
||||||
|
});
|
||||||
|
|
||||||
|
/**
|
||||||
|
* useProcessDomainPermissionsMutation uses the RTK Query API without actually
|
||||||
|
* hitting the GtS API, it's purely an internal function for our own convenience.
|
||||||
|
*
|
||||||
|
* It returns the validated and deduplicated domain permission list.
|
||||||
|
*/
|
||||||
|
const useProcessDomainPermissionsMutation = extended.useProcessDomainPermissionsMutation;
|
||||||
|
|
||||||
|
export { useProcessDomainPermissionsMutation };
|
|
@ -0,0 +1,109 @@
|
||||||
|
/*
|
||||||
|
GoToSocial
|
||||||
|
Copyright (C) GoToSocial Authors admin@gotosocial.org
|
||||||
|
SPDX-License-Identifier: AGPL-3.0-or-later
|
||||||
|
|
||||||
|
This program is free software: you can redistribute it and/or modify
|
||||||
|
it under the terms of the GNU Affero General Public License as published by
|
||||||
|
the Free Software Foundation, either version 3 of the License, or
|
||||||
|
(at your option) any later version.
|
||||||
|
|
||||||
|
This program is distributed in the hope that it will be useful,
|
||||||
|
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
GNU Affero General Public License for more details.
|
||||||
|
|
||||||
|
You should have received a copy of the GNU Affero General Public License
|
||||||
|
along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { gtsApi } from "../../gts-api";
|
||||||
|
|
||||||
|
import {
|
||||||
|
replaceCacheOnMutation,
|
||||||
|
removeFromCacheOnMutation,
|
||||||
|
} from "../../query-modifiers";
|
||||||
|
import { listToKeyedObject } from "../../transforms";
|
||||||
|
import type {
|
||||||
|
DomainPerm,
|
||||||
|
MappedDomainPerms
|
||||||
|
} from "../../../types/domain-permission";
|
||||||
|
|
||||||
|
const extended = gtsApi.injectEndpoints({
|
||||||
|
endpoints: (build) => ({
|
||||||
|
addDomainBlock: build.mutation<MappedDomainPerms, any>({
|
||||||
|
query: (formData) => ({
|
||||||
|
method: "POST",
|
||||||
|
url: `/api/v1/admin/domain_blocks`,
|
||||||
|
asForm: true,
|
||||||
|
body: formData,
|
||||||
|
discardEmpty: true
|
||||||
|
}),
|
||||||
|
transformResponse: listToKeyedObject<DomainPerm>("domain"),
|
||||||
|
...replaceCacheOnMutation("domainBlocks"),
|
||||||
|
}),
|
||||||
|
|
||||||
|
addDomainAllow: build.mutation<MappedDomainPerms, any>({
|
||||||
|
query: (formData) => ({
|
||||||
|
method: "POST",
|
||||||
|
url: `/api/v1/admin/domain_allows`,
|
||||||
|
asForm: true,
|
||||||
|
body: formData,
|
||||||
|
discardEmpty: true
|
||||||
|
}),
|
||||||
|
transformResponse: listToKeyedObject<DomainPerm>("domain"),
|
||||||
|
...replaceCacheOnMutation("domainAllows")
|
||||||
|
}),
|
||||||
|
|
||||||
|
removeDomainBlock: build.mutation<DomainPerm, string>({
|
||||||
|
query: (id) => ({
|
||||||
|
method: "DELETE",
|
||||||
|
url: `/api/v1/admin/domain_blocks/${id}`,
|
||||||
|
}),
|
||||||
|
...removeFromCacheOnMutation("domainBlocks", {
|
||||||
|
key: (_draft, newData) => {
|
||||||
|
return newData.domain;
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}),
|
||||||
|
|
||||||
|
removeDomainAllow: build.mutation<DomainPerm, string>({
|
||||||
|
query: (id) => ({
|
||||||
|
method: "DELETE",
|
||||||
|
url: `/api/v1/admin/domain_allows/${id}`,
|
||||||
|
}),
|
||||||
|
...removeFromCacheOnMutation("domainAllows", {
|
||||||
|
key: (_draft, newData) => {
|
||||||
|
return newData.domain;
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}),
|
||||||
|
}),
|
||||||
|
});
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Add a single domain permission (block) by POSTing to `/api/v1/admin/domain_blocks`.
|
||||||
|
*/
|
||||||
|
const useAddDomainBlockMutation = extended.useAddDomainBlockMutation;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Add a single domain permission (allow) by POSTing to `/api/v1/admin/domain_allows`.
|
||||||
|
*/
|
||||||
|
const useAddDomainAllowMutation = extended.useAddDomainAllowMutation;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Remove a single domain permission (block) by DELETEing to `/api/v1/admin/domain_blocks/{id}`.
|
||||||
|
*/
|
||||||
|
const useRemoveDomainBlockMutation = extended.useRemoveDomainBlockMutation;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Remove a single domain permission (allow) by DELETEing to `/api/v1/admin/domain_allows/{id}`.
|
||||||
|
*/
|
||||||
|
const useRemoveDomainAllowMutation = extended.useRemoveDomainAllowMutation;
|
||||||
|
|
||||||
|
export {
|
||||||
|
useAddDomainBlockMutation,
|
||||||
|
useAddDomainAllowMutation,
|
||||||
|
useRemoveDomainBlockMutation,
|
||||||
|
useRemoveDomainAllowMutation
|
||||||
|
};
|
|
@ -1,264 +0,0 @@
|
||||||
/*
|
|
||||||
GoToSocial
|
|
||||||
Copyright (C) GoToSocial Authors admin@gotosocial.org
|
|
||||||
SPDX-License-Identifier: AGPL-3.0-or-later
|
|
||||||
|
|
||||||
This program is free software: you can redistribute it and/or modify
|
|
||||||
it under the terms of the GNU Affero General Public License as published by
|
|
||||||
the Free Software Foundation, either version 3 of the License, or
|
|
||||||
(at your option) any later version.
|
|
||||||
|
|
||||||
This program is distributed in the hope that it will be useful,
|
|
||||||
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
||||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
||||||
GNU Affero General Public License for more details.
|
|
||||||
|
|
||||||
You should have received a copy of the GNU Affero General Public License
|
|
||||||
along with this program. If not, see <http://www.gnu.org/licenses/>.
|
|
||||||
*/
|
|
||||||
|
|
||||||
const Promise = require("bluebird");
|
|
||||||
const fileDownload = require("js-file-download");
|
|
||||||
const csv = require("papaparse");
|
|
||||||
const { nanoid } = require("nanoid");
|
|
||||||
|
|
||||||
const { isValidDomainBlock, hasBetterScope } = require("../../domain-block");
|
|
||||||
|
|
||||||
const {
|
|
||||||
replaceCacheOnMutation,
|
|
||||||
domainListToObject,
|
|
||||||
unwrapRes
|
|
||||||
} = require("../lib");
|
|
||||||
|
|
||||||
function parseDomainList(list) {
|
|
||||||
if (list[0] == "[") {
|
|
||||||
return JSON.parse(list);
|
|
||||||
} else if (list.startsWith("#domain")) { // Mastodon CSV
|
|
||||||
const { data, errors } = csv.parse(list, {
|
|
||||||
header: true,
|
|
||||||
transformHeader: (header) => header.slice(1), // removes starting '#'
|
|
||||||
skipEmptyLines: true,
|
|
||||||
dynamicTyping: true
|
|
||||||
});
|
|
||||||
|
|
||||||
if (errors.length > 0) {
|
|
||||||
let error = "";
|
|
||||||
errors.forEach((err) => {
|
|
||||||
error += `${err.message} (line ${err.row})`;
|
|
||||||
});
|
|
||||||
throw error;
|
|
||||||
}
|
|
||||||
|
|
||||||
return data;
|
|
||||||
} else {
|
|
||||||
return list.split("\n").map((line) => {
|
|
||||||
let domain = line.trim();
|
|
||||||
let valid = true;
|
|
||||||
if (domain.startsWith("http")) {
|
|
||||||
try {
|
|
||||||
domain = new URL(domain).hostname;
|
|
||||||
} catch (e) {
|
|
||||||
valid = false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return domain.length > 0
|
|
||||||
? { domain, valid }
|
|
||||||
: null;
|
|
||||||
}).filter((a) => a); // not `null`
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function validateDomainList(list) {
|
|
||||||
list.forEach((entry) => {
|
|
||||||
if (entry.domain.startsWith("*.")) {
|
|
||||||
// domain block always includes all subdomains, wildcard is meaningless here
|
|
||||||
entry.domain = entry.domain.slice(2);
|
|
||||||
}
|
|
||||||
|
|
||||||
entry.valid = (entry.valid !== false) && isValidDomainBlock(entry.domain);
|
|
||||||
if (entry.valid) {
|
|
||||||
entry.suggest = hasBetterScope(entry.domain);
|
|
||||||
}
|
|
||||||
entry.checked = entry.valid;
|
|
||||||
});
|
|
||||||
|
|
||||||
return list;
|
|
||||||
}
|
|
||||||
|
|
||||||
function deduplicateDomainList(list) {
|
|
||||||
let domains = new Set();
|
|
||||||
return list.filter((entry) => {
|
|
||||||
if (domains.has(entry.domain)) {
|
|
||||||
return false;
|
|
||||||
} else {
|
|
||||||
domains.add(entry.domain);
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
module.exports = (build) => ({
|
|
||||||
processDomainList: build.mutation({
|
|
||||||
queryFn: (formData) => {
|
|
||||||
return Promise.try(() => {
|
|
||||||
if (formData.domains == undefined || formData.domains.length == 0) {
|
|
||||||
throw "No domains entered";
|
|
||||||
}
|
|
||||||
return parseDomainList(formData.domains);
|
|
||||||
}).then((parsed) => {
|
|
||||||
return deduplicateDomainList(parsed);
|
|
||||||
}).then((deduped) => {
|
|
||||||
return validateDomainList(deduped);
|
|
||||||
}).then((data) => {
|
|
||||||
data.forEach((entry) => {
|
|
||||||
entry.key = nanoid(); // unique id that stays stable even if domain gets modified by user
|
|
||||||
});
|
|
||||||
return { data };
|
|
||||||
}).catch((e) => {
|
|
||||||
return { error: e.toString() };
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}),
|
|
||||||
exportDomainList: build.mutation({
|
|
||||||
queryFn: (formData, api, _extraOpts, baseQuery) => {
|
|
||||||
let process;
|
|
||||||
|
|
||||||
if (formData.exportType == "json") {
|
|
||||||
process = {
|
|
||||||
transformEntry: (entry) => ({
|
|
||||||
domain: entry.domain,
|
|
||||||
public_comment: entry.public_comment,
|
|
||||||
obfuscate: entry.obfuscate
|
|
||||||
}),
|
|
||||||
stringify: (list) => JSON.stringify(list),
|
|
||||||
extension: ".json",
|
|
||||||
mime: "application/json"
|
|
||||||
};
|
|
||||||
} else if (formData.exportType == "csv") {
|
|
||||||
process = {
|
|
||||||
transformEntry: (entry) => [
|
|
||||||
entry.domain,
|
|
||||||
"suspend", // severity
|
|
||||||
false, // reject_media
|
|
||||||
false, // reject_reports
|
|
||||||
entry.public_comment,
|
|
||||||
entry.obfuscate ?? false
|
|
||||||
],
|
|
||||||
stringify: (list) => csv.unparse({
|
|
||||||
fields: "#domain,#severity,#reject_media,#reject_reports,#public_comment,#obfuscate".split(","),
|
|
||||||
data: list
|
|
||||||
}),
|
|
||||||
extension: ".csv",
|
|
||||||
mime: "text/csv"
|
|
||||||
};
|
|
||||||
} else {
|
|
||||||
process = {
|
|
||||||
transformEntry: (entry) => entry.domain,
|
|
||||||
stringify: (list) => list.join("\n"),
|
|
||||||
extension: ".txt",
|
|
||||||
mime: "text/plain"
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
return Promise.try(() => {
|
|
||||||
return baseQuery({
|
|
||||||
url: `/api/v1/admin/domain_blocks`
|
|
||||||
});
|
|
||||||
}).then(unwrapRes).then((blockedInstances) => {
|
|
||||||
return blockedInstances.map(process.transformEntry);
|
|
||||||
}).then((exportList) => {
|
|
||||||
return process.stringify(exportList);
|
|
||||||
}).then((exportAsString) => {
|
|
||||||
if (formData.action == "export") {
|
|
||||||
return {
|
|
||||||
data: exportAsString
|
|
||||||
};
|
|
||||||
} else if (formData.action == "export-file") {
|
|
||||||
let domain = new URL(api.getState().oauth.instance).host;
|
|
||||||
let date = new Date();
|
|
||||||
|
|
||||||
let filename = [
|
|
||||||
domain,
|
|
||||||
"blocklist",
|
|
||||||
date.getFullYear(),
|
|
||||||
(date.getMonth() + 1).toString().padStart(2, "0"),
|
|
||||||
date.getDate().toString().padStart(2, "0"),
|
|
||||||
].join("-");
|
|
||||||
|
|
||||||
fileDownload(
|
|
||||||
exportAsString,
|
|
||||||
filename + process.extension,
|
|
||||||
process.mime
|
|
||||||
);
|
|
||||||
}
|
|
||||||
return { data: null };
|
|
||||||
}).catch((e) => {
|
|
||||||
return { error: e };
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}),
|
|
||||||
importDomainList: build.mutation({
|
|
||||||
query: (formData) => {
|
|
||||||
const { domains } = formData;
|
|
||||||
|
|
||||||
// add/replace comments, obfuscation data
|
|
||||||
let process = entryProcessor(formData);
|
|
||||||
domains.forEach((entry) => {
|
|
||||||
process(entry);
|
|
||||||
});
|
|
||||||
|
|
||||||
return {
|
|
||||||
method: "POST",
|
|
||||||
url: `/api/v1/admin/domain_blocks?import=true`,
|
|
||||||
asForm: true,
|
|
||||||
discardEmpty: true,
|
|
||||||
body: {
|
|
||||||
domains: new Blob([JSON.stringify(domains)], { type: "application/json" })
|
|
||||||
}
|
|
||||||
};
|
|
||||||
},
|
|
||||||
transformResponse: domainListToObject,
|
|
||||||
...replaceCacheOnMutation("instanceBlocks")
|
|
||||||
})
|
|
||||||
});
|
|
||||||
|
|
||||||
const internalKeys = new Set("key,suggest,valid,checked".split(","));
|
|
||||||
function entryProcessor(formData) {
|
|
||||||
let funcs = [];
|
|
||||||
|
|
||||||
["private_comment", "public_comment"].forEach((type) => {
|
|
||||||
let text = formData[type].trim();
|
|
||||||
|
|
||||||
if (text.length > 0) {
|
|
||||||
let behavior = formData[`${type}_behavior`];
|
|
||||||
|
|
||||||
if (behavior == "append") {
|
|
||||||
funcs.push(function appendComment(entry) {
|
|
||||||
if (entry[type] == undefined) {
|
|
||||||
entry[type] = text;
|
|
||||||
} else {
|
|
||||||
entry[type] = [entry[type], text].join("\n");
|
|
||||||
}
|
|
||||||
});
|
|
||||||
} else if (behavior == "replace") {
|
|
||||||
funcs.push(function replaceComment(entry) {
|
|
||||||
entry[type] = text;
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
return function process(entry) {
|
|
||||||
funcs.forEach((func) => {
|
|
||||||
func(entry);
|
|
||||||
});
|
|
||||||
|
|
||||||
entry.obfuscate = formData.obfuscate;
|
|
||||||
|
|
||||||
Object.entries(entry).forEach(([key, val]) => {
|
|
||||||
if (internalKeys.has(key) || val == undefined) {
|
|
||||||
delete entry[key];
|
|
||||||
}
|
|
||||||
});
|
|
||||||
};
|
|
||||||
}
|
|
|
@ -1,165 +0,0 @@
|
||||||
/*
|
|
||||||
GoToSocial
|
|
||||||
Copyright (C) GoToSocial Authors admin@gotosocial.org
|
|
||||||
SPDX-License-Identifier: AGPL-3.0-or-later
|
|
||||||
|
|
||||||
This program is free software: you can redistribute it and/or modify
|
|
||||||
it under the terms of the GNU Affero General Public License as published by
|
|
||||||
the Free Software Foundation, either version 3 of the License, or
|
|
||||||
(at your option) any later version.
|
|
||||||
|
|
||||||
This program is distributed in the hope that it will be useful,
|
|
||||||
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
||||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
||||||
GNU Affero General Public License for more details.
|
|
||||||
|
|
||||||
You should have received a copy of the GNU Affero General Public License
|
|
||||||
along with this program. If not, see <http://www.gnu.org/licenses/>.
|
|
||||||
*/
|
|
||||||
|
|
||||||
const {
|
|
||||||
replaceCacheOnMutation,
|
|
||||||
removeFromCacheOnMutation,
|
|
||||||
domainListToObject,
|
|
||||||
idListToObject
|
|
||||||
} = require("../lib");
|
|
||||||
const { gtsApi } = require("../gts-api");
|
|
||||||
|
|
||||||
const endpoints = (build) => ({
|
|
||||||
updateInstance: build.mutation({
|
|
||||||
query: (formData) => ({
|
|
||||||
method: "PATCH",
|
|
||||||
url: `/api/v1/instance`,
|
|
||||||
asForm: true,
|
|
||||||
body: formData,
|
|
||||||
discardEmpty: true
|
|
||||||
}),
|
|
||||||
...replaceCacheOnMutation("instance")
|
|
||||||
}),
|
|
||||||
mediaCleanup: build.mutation({
|
|
||||||
query: (days) => ({
|
|
||||||
method: "POST",
|
|
||||||
url: `/api/v1/admin/media_cleanup`,
|
|
||||||
params: {
|
|
||||||
remote_cache_days: days
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}),
|
|
||||||
instanceKeysExpire: build.mutation({
|
|
||||||
query: (domain) => ({
|
|
||||||
method: "POST",
|
|
||||||
url: `/api/v1/admin/domain_keys_expire`,
|
|
||||||
params: {
|
|
||||||
domain: domain
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}),
|
|
||||||
instanceBlocks: build.query({
|
|
||||||
query: () => ({
|
|
||||||
url: `/api/v1/admin/domain_blocks`
|
|
||||||
}),
|
|
||||||
transformResponse: domainListToObject
|
|
||||||
}),
|
|
||||||
addInstanceBlock: build.mutation({
|
|
||||||
query: (formData) => ({
|
|
||||||
method: "POST",
|
|
||||||
url: `/api/v1/admin/domain_blocks`,
|
|
||||||
asForm: true,
|
|
||||||
body: formData,
|
|
||||||
discardEmpty: true
|
|
||||||
}),
|
|
||||||
transformResponse: (data) => {
|
|
||||||
return {
|
|
||||||
[data.domain]: data
|
|
||||||
};
|
|
||||||
},
|
|
||||||
...replaceCacheOnMutation("instanceBlocks")
|
|
||||||
}),
|
|
||||||
removeInstanceBlock: build.mutation({
|
|
||||||
query: (id) => ({
|
|
||||||
method: "DELETE",
|
|
||||||
url: `/api/v1/admin/domain_blocks/${id}`,
|
|
||||||
}),
|
|
||||||
...removeFromCacheOnMutation("instanceBlocks", {
|
|
||||||
findKey: (_draft, newData) => {
|
|
||||||
return newData.domain;
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}),
|
|
||||||
getAccount: build.query({
|
|
||||||
query: (id) => ({
|
|
||||||
url: `/api/v1/accounts/${id}`
|
|
||||||
}),
|
|
||||||
providesTags: (_, __, id) => [{ type: "Account", id }]
|
|
||||||
}),
|
|
||||||
actionAccount: build.mutation({
|
|
||||||
query: ({ id, action, reason }) => ({
|
|
||||||
method: "POST",
|
|
||||||
url: `/api/v1/admin/accounts/${id}/action`,
|
|
||||||
asForm: true,
|
|
||||||
body: {
|
|
||||||
type: action,
|
|
||||||
text: reason
|
|
||||||
}
|
|
||||||
}),
|
|
||||||
invalidatesTags: (_, __, { id }) => [{ type: "Account", id }]
|
|
||||||
}),
|
|
||||||
searchAccount: build.mutation({
|
|
||||||
query: (username) => ({
|
|
||||||
url: `/api/v2/search?q=${encodeURIComponent(username)}&resolve=true`
|
|
||||||
}),
|
|
||||||
transformResponse: (res) => {
|
|
||||||
return res.accounts ?? [];
|
|
||||||
}
|
|
||||||
}),
|
|
||||||
instanceRules: build.query({
|
|
||||||
query: () => ({
|
|
||||||
url: `/api/v1/admin/instance/rules`
|
|
||||||
}),
|
|
||||||
transformResponse: idListToObject
|
|
||||||
}),
|
|
||||||
addInstanceRule: build.mutation({
|
|
||||||
query: (formData) => ({
|
|
||||||
method: "POST",
|
|
||||||
url: `/api/v1/admin/instance/rules`,
|
|
||||||
asForm: true,
|
|
||||||
body: formData,
|
|
||||||
discardEmpty: true
|
|
||||||
}),
|
|
||||||
transformResponse: (data) => {
|
|
||||||
return {
|
|
||||||
[data.id]: data
|
|
||||||
};
|
|
||||||
},
|
|
||||||
...replaceCacheOnMutation("instanceRules")
|
|
||||||
}),
|
|
||||||
updateInstanceRule: build.mutation({
|
|
||||||
query: ({ id, ...edit }) => ({
|
|
||||||
method: "PATCH",
|
|
||||||
url: `/api/v1/admin/instance/rules/${id}`,
|
|
||||||
asForm: true,
|
|
||||||
body: edit,
|
|
||||||
discardEmpty: true
|
|
||||||
}),
|
|
||||||
transformResponse: (data) => {
|
|
||||||
return {
|
|
||||||
[data.id]: data
|
|
||||||
};
|
|
||||||
},
|
|
||||||
...replaceCacheOnMutation("instanceRules")
|
|
||||||
}),
|
|
||||||
deleteInstanceRule: build.mutation({
|
|
||||||
query: (id) => ({
|
|
||||||
method: "DELETE",
|
|
||||||
url: `/api/v1/admin/instance/rules/${id}`
|
|
||||||
}),
|
|
||||||
...removeFromCacheOnMutation("instanceRules", {
|
|
||||||
findKey: (_draft, rule) => rule.id
|
|
||||||
})
|
|
||||||
}),
|
|
||||||
...require("./import-export")(build),
|
|
||||||
...require("./custom-emoji")(build),
|
|
||||||
...require("./reports")(build)
|
|
||||||
});
|
|
||||||
|
|
||||||
module.exports = gtsApi.injectEndpoints({ endpoints });
|
|
|
@ -0,0 +1,148 @@
|
||||||
|
/*
|
||||||
|
GoToSocial
|
||||||
|
Copyright (C) GoToSocial Authors admin@gotosocial.org
|
||||||
|
SPDX-License-Identifier: AGPL-3.0-or-later
|
||||||
|
|
||||||
|
This program is free software: you can redistribute it and/or modify
|
||||||
|
it under the terms of the GNU Affero General Public License as published by
|
||||||
|
the Free Software Foundation, either version 3 of the License, or
|
||||||
|
(at your option) any later version.
|
||||||
|
|
||||||
|
This program is distributed in the hope that it will be useful,
|
||||||
|
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
GNU Affero General Public License for more details.
|
||||||
|
|
||||||
|
You should have received a copy of the GNU Affero General Public License
|
||||||
|
along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { replaceCacheOnMutation, removeFromCacheOnMutation } from "../query-modifiers";
|
||||||
|
import { gtsApi } from "../gts-api";
|
||||||
|
import { listToKeyedObject } from "../transforms";
|
||||||
|
|
||||||
|
const extended = gtsApi.injectEndpoints({
|
||||||
|
endpoints: (build) => ({
|
||||||
|
updateInstance: build.mutation({
|
||||||
|
query: (formData) => ({
|
||||||
|
method: "PATCH",
|
||||||
|
url: `/api/v1/instance`,
|
||||||
|
asForm: true,
|
||||||
|
body: formData,
|
||||||
|
discardEmpty: true
|
||||||
|
}),
|
||||||
|
...replaceCacheOnMutation("instanceV1"),
|
||||||
|
}),
|
||||||
|
|
||||||
|
mediaCleanup: build.mutation({
|
||||||
|
query: (days) => ({
|
||||||
|
method: "POST",
|
||||||
|
url: `/api/v1/admin/media_cleanup`,
|
||||||
|
params: {
|
||||||
|
remote_cache_days: days
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}),
|
||||||
|
|
||||||
|
instanceKeysExpire: build.mutation({
|
||||||
|
query: (domain) => ({
|
||||||
|
method: "POST",
|
||||||
|
url: `/api/v1/admin/domain_keys_expire`,
|
||||||
|
params: {
|
||||||
|
domain: domain
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}),
|
||||||
|
|
||||||
|
getAccount: build.query({
|
||||||
|
query: (id) => ({
|
||||||
|
url: `/api/v1/accounts/${id}`
|
||||||
|
}),
|
||||||
|
providesTags: (_, __, id) => [{ type: "Account", id }]
|
||||||
|
}),
|
||||||
|
|
||||||
|
actionAccount: build.mutation({
|
||||||
|
query: ({ id, action, reason }) => ({
|
||||||
|
method: "POST",
|
||||||
|
url: `/api/v1/admin/accounts/${id}/action`,
|
||||||
|
asForm: true,
|
||||||
|
body: {
|
||||||
|
type: action,
|
||||||
|
text: reason
|
||||||
|
}
|
||||||
|
}),
|
||||||
|
invalidatesTags: (_, __, { id }) => [{ type: "Account", id }]
|
||||||
|
}),
|
||||||
|
|
||||||
|
searchAccount: build.mutation({
|
||||||
|
query: (username) => ({
|
||||||
|
url: `/api/v2/search?q=${encodeURIComponent(username)}&resolve=true`
|
||||||
|
}),
|
||||||
|
transformResponse: (res) => {
|
||||||
|
return res.accounts ?? [];
|
||||||
|
}
|
||||||
|
}),
|
||||||
|
|
||||||
|
instanceRules: build.query({
|
||||||
|
query: () => ({
|
||||||
|
url: `/api/v1/admin/instance/rules`
|
||||||
|
}),
|
||||||
|
transformResponse: listToKeyedObject<any>("id")
|
||||||
|
}),
|
||||||
|
|
||||||
|
addInstanceRule: build.mutation({
|
||||||
|
query: (formData) => ({
|
||||||
|
method: "POST",
|
||||||
|
url: `/api/v1/admin/instance/rules`,
|
||||||
|
asForm: true,
|
||||||
|
body: formData,
|
||||||
|
discardEmpty: true
|
||||||
|
}),
|
||||||
|
transformResponse: (data) => {
|
||||||
|
return {
|
||||||
|
[data.id]: data
|
||||||
|
};
|
||||||
|
},
|
||||||
|
...replaceCacheOnMutation("instanceRules"),
|
||||||
|
}),
|
||||||
|
|
||||||
|
updateInstanceRule: build.mutation({
|
||||||
|
query: ({ id, ...edit }) => ({
|
||||||
|
method: "PATCH",
|
||||||
|
url: `/api/v1/admin/instance/rules/${id}`,
|
||||||
|
asForm: true,
|
||||||
|
body: edit,
|
||||||
|
discardEmpty: true
|
||||||
|
}),
|
||||||
|
transformResponse: (data) => {
|
||||||
|
return {
|
||||||
|
[data.id]: data
|
||||||
|
};
|
||||||
|
},
|
||||||
|
...replaceCacheOnMutation("instanceRules"),
|
||||||
|
}),
|
||||||
|
|
||||||
|
deleteInstanceRule: build.mutation({
|
||||||
|
query: (id) => ({
|
||||||
|
method: "DELETE",
|
||||||
|
url: `/api/v1/admin/instance/rules/${id}`
|
||||||
|
}),
|
||||||
|
...removeFromCacheOnMutation("instanceRules", {
|
||||||
|
key: (_draft, rule) => rule.id,
|
||||||
|
})
|
||||||
|
})
|
||||||
|
})
|
||||||
|
});
|
||||||
|
|
||||||
|
export const {
|
||||||
|
useUpdateInstanceMutation,
|
||||||
|
useMediaCleanupMutation,
|
||||||
|
useInstanceKeysExpireMutation,
|
||||||
|
useGetAccountQuery,
|
||||||
|
useActionAccountMutation,
|
||||||
|
useSearchAccountMutation,
|
||||||
|
useInstanceRulesQuery,
|
||||||
|
useAddInstanceRuleMutation,
|
||||||
|
useUpdateInstanceRuleMutation,
|
||||||
|
useDeleteInstanceRuleMutation,
|
||||||
|
} = extended;
|
|
@ -1,51 +0,0 @@
|
||||||
/*
|
|
||||||
GoToSocial
|
|
||||||
Copyright (C) GoToSocial Authors admin@gotosocial.org
|
|
||||||
SPDX-License-Identifier: AGPL-3.0-or-later
|
|
||||||
|
|
||||||
This program is free software: you can redistribute it and/or modify
|
|
||||||
it under the terms of the GNU Affero General Public License as published by
|
|
||||||
the Free Software Foundation, either version 3 of the License, or
|
|
||||||
(at your option) any later version.
|
|
||||||
|
|
||||||
This program is distributed in the hope that it will be useful,
|
|
||||||
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
||||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
||||||
GNU Affero General Public License for more details.
|
|
||||||
|
|
||||||
You should have received a copy of the GNU Affero General Public License
|
|
||||||
along with this program. If not, see <http://www.gnu.org/licenses/>.
|
|
||||||
*/
|
|
||||||
|
|
||||||
module.exports = (build) => ({
|
|
||||||
listReports: build.query({
|
|
||||||
query: (params = {}) => ({
|
|
||||||
url: "/api/v1/admin/reports",
|
|
||||||
params: {
|
|
||||||
limit: 100,
|
|
||||||
...params
|
|
||||||
}
|
|
||||||
}),
|
|
||||||
providesTags: ["Reports"]
|
|
||||||
}),
|
|
||||||
|
|
||||||
getReport: build.query({
|
|
||||||
query: (id) => ({
|
|
||||||
url: `/api/v1/admin/reports/${id}`
|
|
||||||
}),
|
|
||||||
providesTags: (res, error, id) => [{ type: "Reports", id }]
|
|
||||||
}),
|
|
||||||
|
|
||||||
resolveReport: build.mutation({
|
|
||||||
query: (formData) => ({
|
|
||||||
url: `/api/v1/admin/reports/${formData.id}/resolve`,
|
|
||||||
method: "POST",
|
|
||||||
asForm: true,
|
|
||||||
body: formData
|
|
||||||
}),
|
|
||||||
invalidatesTags: (res) =>
|
|
||||||
res
|
|
||||||
? [{ type: "Reports", id: "LIST" }, { type: "Reports", id: res.id }]
|
|
||||||
: [{ type: "Reports", id: "LIST" }]
|
|
||||||
})
|
|
||||||
});
|
|
|
@ -0,0 +1,83 @@
|
||||||
|
/*
|
||||||
|
GoToSocial
|
||||||
|
Copyright (C) GoToSocial Authors admin@gotosocial.org
|
||||||
|
SPDX-License-Identifier: AGPL-3.0-or-later
|
||||||
|
|
||||||
|
This program is free software: you can redistribute it and/or modify
|
||||||
|
it under the terms of the GNU Affero General Public License as published by
|
||||||
|
the Free Software Foundation, either version 3 of the License, or
|
||||||
|
(at your option) any later version.
|
||||||
|
|
||||||
|
This program is distributed in the hope that it will be useful,
|
||||||
|
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
GNU Affero General Public License for more details.
|
||||||
|
|
||||||
|
You should have received a copy of the GNU Affero General Public License
|
||||||
|
along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { gtsApi } from "../../gts-api";
|
||||||
|
|
||||||
|
import type {
|
||||||
|
AdminReport,
|
||||||
|
AdminReportListParams,
|
||||||
|
AdminReportResolveParams,
|
||||||
|
} from "../../../types/report";
|
||||||
|
|
||||||
|
const extended = gtsApi.injectEndpoints({
|
||||||
|
endpoints: (build) => ({
|
||||||
|
listReports: build.query<AdminReport[], AdminReportListParams | void>({
|
||||||
|
query: (params) => ({
|
||||||
|
url: "/api/v1/admin/reports",
|
||||||
|
params: {
|
||||||
|
// Override provided limit.
|
||||||
|
limit: 100,
|
||||||
|
...params
|
||||||
|
}
|
||||||
|
}),
|
||||||
|
providesTags: ["Reports"]
|
||||||
|
}),
|
||||||
|
|
||||||
|
getReport: build.query<AdminReport, string>({
|
||||||
|
query: (id) => ({
|
||||||
|
url: `/api/v1/admin/reports/${id}`
|
||||||
|
}),
|
||||||
|
providesTags: (_res, _error, id) => [{ type: "Reports", id }]
|
||||||
|
}),
|
||||||
|
|
||||||
|
resolveReport: build.mutation<AdminReport, AdminReportResolveParams>({
|
||||||
|
query: (formData) => ({
|
||||||
|
url: `/api/v1/admin/reports/${formData.id}/resolve`,
|
||||||
|
method: "POST",
|
||||||
|
asForm: true,
|
||||||
|
body: formData
|
||||||
|
}),
|
||||||
|
invalidatesTags: (res) =>
|
||||||
|
res
|
||||||
|
? [{ type: "Reports", id: "LIST" }, { type: "Reports", id: res.id }]
|
||||||
|
: [{ type: "Reports", id: "LIST" }]
|
||||||
|
})
|
||||||
|
})
|
||||||
|
});
|
||||||
|
|
||||||
|
/**
|
||||||
|
* List reports received on this instance, filtered using given parameters.
|
||||||
|
*/
|
||||||
|
const useListReportsQuery = extended.useListReportsQuery;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get a single report by its ID.
|
||||||
|
*/
|
||||||
|
const useGetReportQuery = extended.useGetReportQuery;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Mark an open report as resolved.
|
||||||
|
*/
|
||||||
|
const useResolveReportMutation = extended.useResolveReportMutation;
|
||||||
|
|
||||||
|
export {
|
||||||
|
useListReportsQuery,
|
||||||
|
useGetReportQuery,
|
||||||
|
useResolveReportMutation,
|
||||||
|
};
|
|
@ -26,6 +26,7 @@ import type {
|
||||||
import { serialize as serializeForm } from "object-to-formdata";
|
import { serialize as serializeForm } from "object-to-formdata";
|
||||||
|
|
||||||
import type { RootState } from '../../redux/store';
|
import type { RootState } from '../../redux/store';
|
||||||
|
import { InstanceV1 } from '../types/instance';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* GTSFetchArgs extends standard FetchArgs used by
|
* GTSFetchArgs extends standard FetchArgs used by
|
||||||
|
@ -72,7 +73,7 @@ const gtsBaseQuery: BaseQueryFn<
|
||||||
const { instanceUrl, token } = state.oauth;
|
const { instanceUrl, token } = state.oauth;
|
||||||
|
|
||||||
// Derive baseUrl dynamically.
|
// Derive baseUrl dynamically.
|
||||||
let baseUrl: string;
|
let baseUrl: string | undefined;
|
||||||
|
|
||||||
// Check if simple string baseUrl provided
|
// Check if simple string baseUrl provided
|
||||||
// as args, or if more complex args provided.
|
// as args, or if more complex args provided.
|
||||||
|
@ -137,8 +138,8 @@ export const gtsApi = createApi({
|
||||||
"Account",
|
"Account",
|
||||||
"InstanceRules",
|
"InstanceRules",
|
||||||
],
|
],
|
||||||
endpoints: (builder) => ({
|
endpoints: (build) => ({
|
||||||
instance: builder.query<any, void>({
|
instanceV1: build.query<InstanceV1, void>({
|
||||||
query: () => ({
|
query: () => ({
|
||||||
url: `/api/v1/instance`
|
url: `/api/v1/instance`
|
||||||
})
|
})
|
||||||
|
@ -146,4 +147,11 @@ export const gtsApi = createApi({
|
||||||
})
|
})
|
||||||
});
|
});
|
||||||
|
|
||||||
export const { useInstanceQuery } = gtsApi;
|
/**
|
||||||
|
* Query /api/v1/instance to retrieve basic instance information.
|
||||||
|
* This endpoint does not require authentication/authorization.
|
||||||
|
* TODO: move this to ./instance.
|
||||||
|
*/
|
||||||
|
const useInstanceV1Query = gtsApi.useInstanceV1Query;
|
||||||
|
|
||||||
|
export { useInstanceV1Query };
|
||||||
|
|
|
@ -1,81 +0,0 @@
|
||||||
/*
|
|
||||||
GoToSocial
|
|
||||||
Copyright (C) GoToSocial Authors admin@gotosocial.org
|
|
||||||
SPDX-License-Identifier: AGPL-3.0-or-later
|
|
||||||
|
|
||||||
This program is free software: you can redistribute it and/or modify
|
|
||||||
it under the terms of the GNU Affero General Public License as published by
|
|
||||||
the Free Software Foundation, either version 3 of the License, or
|
|
||||||
(at your option) any later version.
|
|
||||||
|
|
||||||
This program is distributed in the hope that it will be useful,
|
|
||||||
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
||||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
||||||
GNU Affero General Public License for more details.
|
|
||||||
|
|
||||||
You should have received a copy of the GNU Affero General Public License
|
|
||||||
along with this program. If not, see <http://www.gnu.org/licenses/>.
|
|
||||||
*/
|
|
||||||
|
|
||||||
const syncpipe = require("syncpipe");
|
|
||||||
const { gtsApi } = require("./gts-api");
|
|
||||||
|
|
||||||
module.exports = {
|
|
||||||
unwrapRes(res) {
|
|
||||||
if (res.error != undefined) {
|
|
||||||
throw res.error;
|
|
||||||
} else {
|
|
||||||
return res.data;
|
|
||||||
}
|
|
||||||
},
|
|
||||||
domainListToObject: (data) => {
|
|
||||||
// Turn flat Array into Object keyed by block's domain
|
|
||||||
return syncpipe(data, [
|
|
||||||
(_) => _.map((entry) => [entry.domain, entry]),
|
|
||||||
(_) => Object.fromEntries(_)
|
|
||||||
]);
|
|
||||||
},
|
|
||||||
idListToObject: (data) => {
|
|
||||||
// Turn flat Array into Object keyed by entry id field
|
|
||||||
return syncpipe(data, [
|
|
||||||
(_) => _.map((entry) => [entry.id, entry]),
|
|
||||||
(_) => Object.fromEntries(_)
|
|
||||||
]);
|
|
||||||
},
|
|
||||||
replaceCacheOnMutation: makeCacheMutation((draft, newData) => {
|
|
||||||
Object.assign(draft, newData);
|
|
||||||
}),
|
|
||||||
appendCacheOnMutation: makeCacheMutation((draft, newData) => {
|
|
||||||
draft.push(newData);
|
|
||||||
}),
|
|
||||||
spliceCacheOnMutation: makeCacheMutation((draft, newData, { key }) => {
|
|
||||||
draft.splice(key, 1);
|
|
||||||
}),
|
|
||||||
updateCacheOnMutation: makeCacheMutation((draft, newData, { key }) => {
|
|
||||||
draft[key] = newData;
|
|
||||||
}),
|
|
||||||
removeFromCacheOnMutation: makeCacheMutation((draft, newData, { key }) => {
|
|
||||||
delete draft[key];
|
|
||||||
}),
|
|
||||||
editCacheOnMutation: makeCacheMutation((draft, newData, { update }) => {
|
|
||||||
update(draft, newData);
|
|
||||||
})
|
|
||||||
};
|
|
||||||
|
|
||||||
// https://redux-toolkit.js.org/rtk-query/usage/manual-cache-updates#pessimistic-updates
|
|
||||||
function makeCacheMutation(action) {
|
|
||||||
return function cacheMutation(queryName, { key, findKey, arg, ...opts } = {}) {
|
|
||||||
return {
|
|
||||||
onQueryStarted: (_, { dispatch, queryFulfilled }) => {
|
|
||||||
queryFulfilled.then(({ data: newData }) => {
|
|
||||||
dispatch(gtsApi.util.updateQueryData(queryName, arg, (draft) => {
|
|
||||||
if (findKey != undefined) {
|
|
||||||
key = findKey(draft, newData);
|
|
||||||
}
|
|
||||||
action(draft, newData, { key, ...opts });
|
|
||||||
}));
|
|
||||||
});
|
|
||||||
}
|
|
||||||
};
|
|
||||||
};
|
|
||||||
}
|
|
|
@ -57,8 +57,8 @@ const SETTINGS_URL = (getSettingsURL());
|
||||||
//
|
//
|
||||||
// https://redux-toolkit.js.org/rtk-query/usage/customizing-queries#performing-multiple-requests-with-a-single-query
|
// https://redux-toolkit.js.org/rtk-query/usage/customizing-queries#performing-multiple-requests-with-a-single-query
|
||||||
const extended = gtsApi.injectEndpoints({
|
const extended = gtsApi.injectEndpoints({
|
||||||
endpoints: (builder) => ({
|
endpoints: (build) => ({
|
||||||
verifyCredentials: builder.query<any, void>({
|
verifyCredentials: build.query<any, void>({
|
||||||
providesTags: (_res, error) =>
|
providesTags: (_res, error) =>
|
||||||
error == undefined ? ["Auth"] : [],
|
error == undefined ? ["Auth"] : [],
|
||||||
async queryFn(_arg, api, _extraOpts, fetchWithBQ) {
|
async queryFn(_arg, api, _extraOpts, fetchWithBQ) {
|
||||||
|
@ -135,7 +135,7 @@ const extended = gtsApi.injectEndpoints({
|
||||||
}
|
}
|
||||||
}),
|
}),
|
||||||
|
|
||||||
authorizeFlow: builder.mutation({
|
authorizeFlow: build.mutation({
|
||||||
async queryFn(formData, api, _extraOpts, fetchWithBQ) {
|
async queryFn(formData, api, _extraOpts, fetchWithBQ) {
|
||||||
const state = api.getState() as RootState;
|
const state = api.getState() as RootState;
|
||||||
const oauthState = state.oauth;
|
const oauthState = state.oauth;
|
||||||
|
@ -187,7 +187,7 @@ const extended = gtsApi.injectEndpoints({
|
||||||
return { data: null };
|
return { data: null };
|
||||||
},
|
},
|
||||||
}),
|
}),
|
||||||
logout: builder.mutation({
|
logout: build.mutation({
|
||||||
queryFn: (_arg, api) => {
|
queryFn: (_arg, api) => {
|
||||||
api.dispatch(oauthRemove());
|
api.dispatch(oauthRemove());
|
||||||
return { data: null };
|
return { data: null };
|
||||||
|
|
|
@ -0,0 +1,150 @@
|
||||||
|
/*
|
||||||
|
GoToSocial
|
||||||
|
Copyright (C) GoToSocial Authors admin@gotosocial.org
|
||||||
|
SPDX-License-Identifier: AGPL-3.0-or-later
|
||||||
|
|
||||||
|
This program is free software: you can redistribute it and/or modify
|
||||||
|
it under the terms of the GNU Affero General Public License as published by
|
||||||
|
the Free Software Foundation, either version 3 of the License, or
|
||||||
|
(at your option) any later version.
|
||||||
|
|
||||||
|
This program is distributed in the hope that it will be useful,
|
||||||
|
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
GNU Affero General Public License for more details.
|
||||||
|
|
||||||
|
You should have received a copy of the GNU Affero General Public License
|
||||||
|
along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { gtsApi } from "./gts-api";
|
||||||
|
|
||||||
|
import type {
|
||||||
|
Action,
|
||||||
|
CacheMutation,
|
||||||
|
} from "../types/query";
|
||||||
|
|
||||||
|
import { NoArg } from "../types/query";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Cache mutation creator for pessimistic updates.
|
||||||
|
*
|
||||||
|
* Feed it a function that you want to perform on the
|
||||||
|
* given draft and updated data, using the given parameters.
|
||||||
|
*
|
||||||
|
* https://redux-toolkit.js.org/rtk-query/api/createApi#onquerystarted
|
||||||
|
* https://redux-toolkit.js.org/rtk-query/usage/manual-cache-updates#pessimistic-updates
|
||||||
|
*/
|
||||||
|
function makeCacheMutation(action: Action): CacheMutation {
|
||||||
|
return function cacheMutation(
|
||||||
|
queryName: string | ((_arg: any) => string),
|
||||||
|
{ key } = {},
|
||||||
|
) {
|
||||||
|
return {
|
||||||
|
onQueryStarted: async(mutationData, { dispatch, queryFulfilled }) => {
|
||||||
|
// queryName might be a function that returns
|
||||||
|
// a query name; trigger it if so. The returned
|
||||||
|
// queryName has to match one of the API endpoints
|
||||||
|
// we've defined. So if we have endpoints called
|
||||||
|
// (for example) `instanceV1` and `getPosts` then
|
||||||
|
// the queryName provided here has to line up with
|
||||||
|
// one of those in order to actually do anything.
|
||||||
|
if (typeof queryName !== "string") {
|
||||||
|
queryName = queryName(mutationData);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (queryName == "") {
|
||||||
|
throw (
|
||||||
|
"provided queryName resolved to an empty string;" +
|
||||||
|
"double check your mutation definition!"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
// Wait for the mutation to finish (this
|
||||||
|
// is why it's a pessimistic update).
|
||||||
|
const { data: newData } = await queryFulfilled;
|
||||||
|
|
||||||
|
// In order for `gtsApi.util.updateQueryData` to
|
||||||
|
// actually do something within a dispatch, the
|
||||||
|
// first two arguments passed into it have to line
|
||||||
|
// up with arguments that were used earlier to
|
||||||
|
// fetch the data whose cached version we're now
|
||||||
|
// trying to modify.
|
||||||
|
//
|
||||||
|
// So, if we earlier fetched all reports with
|
||||||
|
// queryName `getReports`, and arg `undefined`,
|
||||||
|
// then we now need match those parameters in
|
||||||
|
// `updateQueryData` in order to modify the cache.
|
||||||
|
//
|
||||||
|
// If you pass something like `null` or `""` here
|
||||||
|
// instead, then the cache will not get modified!
|
||||||
|
// Redux will just quietly discard the thunk action.
|
||||||
|
dispatch(
|
||||||
|
gtsApi.util.updateQueryData(queryName as any, NoArg, (draft) => {
|
||||||
|
if (key != undefined && typeof key !== "string") {
|
||||||
|
key = key(draft, newData);
|
||||||
|
}
|
||||||
|
action(draft, newData, { key });
|
||||||
|
})
|
||||||
|
);
|
||||||
|
} catch (e) {
|
||||||
|
// eslint-disable-next-line no-console
|
||||||
|
console.error(`rolling back pessimistic update of ${queryName}: ${e}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
*
|
||||||
|
*/
|
||||||
|
const replaceCacheOnMutation: CacheMutation = makeCacheMutation((draft, newData, _params) => {
|
||||||
|
Object.assign(draft, newData);
|
||||||
|
});
|
||||||
|
|
||||||
|
const appendCacheOnMutation: CacheMutation = makeCacheMutation((draft, newData, _params) => {
|
||||||
|
draft.push(newData);
|
||||||
|
});
|
||||||
|
|
||||||
|
const spliceCacheOnMutation: CacheMutation = makeCacheMutation((draft, _newData, { key }) => {
|
||||||
|
if (key === undefined) {
|
||||||
|
throw ("key undefined");
|
||||||
|
}
|
||||||
|
|
||||||
|
draft.splice(key, 1);
|
||||||
|
});
|
||||||
|
|
||||||
|
const updateCacheOnMutation: CacheMutation = makeCacheMutation((draft, newData, { key }) => {
|
||||||
|
if (key === undefined) {
|
||||||
|
throw ("key undefined");
|
||||||
|
}
|
||||||
|
|
||||||
|
if (typeof key !== "string") {
|
||||||
|
key = key(draft, newData);
|
||||||
|
}
|
||||||
|
|
||||||
|
draft[key] = newData;
|
||||||
|
});
|
||||||
|
|
||||||
|
const removeFromCacheOnMutation: CacheMutation = makeCacheMutation((draft, newData, { key }) => {
|
||||||
|
if (key === undefined) {
|
||||||
|
throw ("key undefined");
|
||||||
|
}
|
||||||
|
|
||||||
|
if (typeof key !== "string") {
|
||||||
|
key = key(draft, newData);
|
||||||
|
}
|
||||||
|
|
||||||
|
delete draft[key];
|
||||||
|
});
|
||||||
|
|
||||||
|
|
||||||
|
export {
|
||||||
|
replaceCacheOnMutation,
|
||||||
|
appendCacheOnMutation,
|
||||||
|
spliceCacheOnMutation,
|
||||||
|
updateCacheOnMutation,
|
||||||
|
removeFromCacheOnMutation,
|
||||||
|
};
|
|
@ -0,0 +1,78 @@
|
||||||
|
/*
|
||||||
|
GoToSocial
|
||||||
|
Copyright (C) GoToSocial Authors admin@gotosocial.org
|
||||||
|
SPDX-License-Identifier: AGPL-3.0-or-later
|
||||||
|
|
||||||
|
This program is free software: you can redistribute it and/or modify
|
||||||
|
it under the terms of the GNU Affero General Public License as published by
|
||||||
|
the Free Software Foundation, either version 3 of the License, or
|
||||||
|
(at your option) any later version.
|
||||||
|
|
||||||
|
This program is distributed in the hope that it will be useful,
|
||||||
|
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
GNU Affero General Public License for more details.
|
||||||
|
|
||||||
|
You should have received a copy of the GNU Affero General Public License
|
||||||
|
along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||||
|
*/
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Map a list of items into an object.
|
||||||
|
*
|
||||||
|
* In the following example, a list of DomainPerms like the following:
|
||||||
|
*
|
||||||
|
* ```json
|
||||||
|
* [
|
||||||
|
* {
|
||||||
|
* "domain": "example.org",
|
||||||
|
* "public_comment": "aaaaa!!"
|
||||||
|
* },
|
||||||
|
* {
|
||||||
|
* "domain": "another.domain",
|
||||||
|
* "public_comment": "they are poo"
|
||||||
|
* }
|
||||||
|
* ]
|
||||||
|
* ```
|
||||||
|
*
|
||||||
|
* Would be converted into an Object like the following:
|
||||||
|
*
|
||||||
|
* ```json
|
||||||
|
* {
|
||||||
|
* "example.org": {
|
||||||
|
* "domain": "example.org",
|
||||||
|
* "public_comment": "aaaaa!!"
|
||||||
|
* },
|
||||||
|
* "another.domain": {
|
||||||
|
* "domain": "another.domain",
|
||||||
|
* "public_comment": "they are poo"
|
||||||
|
* },
|
||||||
|
* }
|
||||||
|
* ```
|
||||||
|
*
|
||||||
|
* If you pass a non-array type into this function it
|
||||||
|
* will be converted into an array first, as a treat.
|
||||||
|
*
|
||||||
|
* @example
|
||||||
|
* const extended = gtsApi.injectEndpoints({
|
||||||
|
* endpoints: (build) => ({
|
||||||
|
* getDomainBlocks: build.query<MappedDomainPerms, void>({
|
||||||
|
* query: () => ({
|
||||||
|
* url: `/api/v1/admin/domain_blocks`
|
||||||
|
* }),
|
||||||
|
* transformResponse: listToKeyedObject<DomainPerm>("domain"),
|
||||||
|
* }),
|
||||||
|
* });
|
||||||
|
*/
|
||||||
|
export function listToKeyedObject<T>(key: keyof T) {
|
||||||
|
return (list: T[] | T): { [_ in keyof T]: T } => {
|
||||||
|
// Ensure we're actually
|
||||||
|
// dealing with an array.
|
||||||
|
if (!Array.isArray(list)) {
|
||||||
|
list = [list];
|
||||||
|
}
|
||||||
|
|
||||||
|
const entries = list.map((entry) => [entry[key], entry]);
|
||||||
|
return Object.fromEntries(entries);
|
||||||
|
};
|
||||||
|
}
|
|
@ -17,12 +17,12 @@
|
||||||
along with this program. If not, see <http://www.gnu.org/licenses/>.
|
along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { replaceCacheOnMutation } from "../lib";
|
import { replaceCacheOnMutation } from "../query-modifiers";
|
||||||
import { gtsApi } from "../gts-api";
|
import { gtsApi } from "../gts-api";
|
||||||
|
|
||||||
const extended = gtsApi.injectEndpoints({
|
const extended = gtsApi.injectEndpoints({
|
||||||
endpoints: (builder) => ({
|
endpoints: (build) => ({
|
||||||
updateCredentials: builder.mutation({
|
updateCredentials: build.mutation({
|
||||||
query: (formData) => ({
|
query: (formData) => ({
|
||||||
method: "PATCH",
|
method: "PATCH",
|
||||||
url: `/api/v1/accounts/update_credentials`,
|
url: `/api/v1/accounts/update_credentials`,
|
||||||
|
@ -32,7 +32,7 @@ const extended = gtsApi.injectEndpoints({
|
||||||
}),
|
}),
|
||||||
...replaceCacheOnMutation("verifyCredentials")
|
...replaceCacheOnMutation("verifyCredentials")
|
||||||
}),
|
}),
|
||||||
passwordChange: builder.mutation({
|
passwordChange: build.mutation({
|
||||||
query: (data) => ({
|
query: (data) => ({
|
||||||
method: "POST",
|
method: "POST",
|
||||||
url: `/api/v1/user/password_change`,
|
url: `/api/v1/user/password_change`,
|
||||||
|
|
|
@ -17,25 +17,33 @@
|
||||||
along with this program. If not, see <http://www.gnu.org/licenses/>.
|
along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
const React = require("react");
|
export interface CustomEmoji {
|
||||||
const { Switch, Route } = require("wouter");
|
id?: string;
|
||||||
|
shortcode: string;
|
||||||
|
category?: string;
|
||||||
|
}
|
||||||
|
|
||||||
const InstanceOverview = require("./overview");
|
/**
|
||||||
const InstanceDetail = require("./detail");
|
* Query parameters for GET to /api/v1/admin/custom_emojis.
|
||||||
const InstanceImportExport = require("./import-export");
|
*/
|
||||||
|
export interface ListEmojiParams {
|
||||||
|
|
||||||
module.exports = function Federation({ baseUrl }) {
|
}
|
||||||
return (
|
|
||||||
<Switch>
|
|
||||||
<Route path={`${baseUrl}/import-export/:list?`}>
|
|
||||||
<InstanceImportExport />
|
|
||||||
</Route>
|
|
||||||
|
|
||||||
<Route path={`${baseUrl}/:domain`}>
|
/**
|
||||||
<InstanceDetail baseUrl={baseUrl} />
|
* Result of searchItemForEmoji mutation.
|
||||||
</Route>
|
*/
|
||||||
|
export interface EmojisFromItem {
|
||||||
<InstanceOverview baseUrl={baseUrl} />
|
/**
|
||||||
</Switch>
|
* Type of the search item result.
|
||||||
);
|
*/
|
||||||
};
|
type: "statuses" | "accounts";
|
||||||
|
/**
|
||||||
|
* Domain of the returned emojis.
|
||||||
|
*/
|
||||||
|
domain: string;
|
||||||
|
/**
|
||||||
|
* Discovered emojis.
|
||||||
|
*/
|
||||||
|
list: CustomEmoji[];
|
||||||
|
}
|
|
@ -0,0 +1,97 @@
|
||||||
|
/*
|
||||||
|
GoToSocial
|
||||||
|
Copyright (C) GoToSocial Authors admin@gotosocial.org
|
||||||
|
SPDX-License-Identifier: AGPL-3.0-or-later
|
||||||
|
|
||||||
|
This program is free software: you can redistribute it and/or modify
|
||||||
|
it under the terms of the GNU Affero General Public License as published by
|
||||||
|
the Free Software Foundation, either version 3 of the License, or
|
||||||
|
(at your option) any later version.
|
||||||
|
|
||||||
|
This program is distributed in the hope that it will be useful,
|
||||||
|
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
GNU Affero General Public License for more details.
|
||||||
|
|
||||||
|
You should have received a copy of the GNU Affero General Public License
|
||||||
|
along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import typia from "typia";
|
||||||
|
|
||||||
|
export const isDomainPerms = typia.createIs<DomainPerm[]>();
|
||||||
|
|
||||||
|
export type PermType = "block" | "allow";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A single domain permission entry (block or allow).
|
||||||
|
*/
|
||||||
|
export interface DomainPerm {
|
||||||
|
id?: string;
|
||||||
|
domain: string;
|
||||||
|
obfuscate?: boolean;
|
||||||
|
private_comment?: string;
|
||||||
|
public_comment?: string;
|
||||||
|
created_at?: string;
|
||||||
|
|
||||||
|
// Internal processing keys; remove
|
||||||
|
// before serdes of domain perm.
|
||||||
|
key?: string;
|
||||||
|
permType?: PermType;
|
||||||
|
suggest?: string;
|
||||||
|
valid?: boolean;
|
||||||
|
checked?: boolean;
|
||||||
|
commentType?: string;
|
||||||
|
private_comment_behavior?: "append" | "replace";
|
||||||
|
public_comment_behavior?: "append" | "replace";
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Domain permissions mapped to an Object where the Object
|
||||||
|
* keys are the "domain" value of each DomainPerm.
|
||||||
|
*/
|
||||||
|
export interface MappedDomainPerms {
|
||||||
|
[key: string]: DomainPerm;
|
||||||
|
}
|
||||||
|
|
||||||
|
const domainPermInternalKeys: Set<keyof DomainPerm> = new Set([
|
||||||
|
"key",
|
||||||
|
"permType",
|
||||||
|
"suggest",
|
||||||
|
"valid",
|
||||||
|
"checked",
|
||||||
|
"commentType",
|
||||||
|
"private_comment_behavior",
|
||||||
|
"public_comment_behavior",
|
||||||
|
]);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns true if provided DomainPerm Object key is
|
||||||
|
* "internal"; ie., it's just for our use, and it shouldn't
|
||||||
|
* be serialized to or deserialized from the GtS API.
|
||||||
|
*
|
||||||
|
* @param key
|
||||||
|
* @returns
|
||||||
|
*/
|
||||||
|
export function isDomainPermInternalKey(key: keyof DomainPerm) {
|
||||||
|
return domainPermInternalKeys.has(key);
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface ImportDomainPermsParams {
|
||||||
|
domains: DomainPerm[];
|
||||||
|
|
||||||
|
// Internal processing keys;
|
||||||
|
// remove before serdes of form.
|
||||||
|
obfuscate?: boolean;
|
||||||
|
commentType?: string;
|
||||||
|
permType: PermType;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Model domain permissions bulk export params.
|
||||||
|
*/
|
||||||
|
export interface ExportDomainPermsParams {
|
||||||
|
permType: PermType;
|
||||||
|
action: "export" | "export-file";
|
||||||
|
exportType: "json" | "csv" | "plain";
|
||||||
|
}
|
|
@ -0,0 +1,91 @@
|
||||||
|
/*
|
||||||
|
GoToSocial
|
||||||
|
Copyright (C) GoToSocial Authors admin@gotosocial.org
|
||||||
|
SPDX-License-Identifier: AGPL-3.0-or-later
|
||||||
|
|
||||||
|
This program is free software: you can redistribute it and/or modify
|
||||||
|
it under the terms of the GNU Affero General Public License as published by
|
||||||
|
the Free Software Foundation, either version 3 of the License, or
|
||||||
|
(at your option) any later version.
|
||||||
|
|
||||||
|
This program is distributed in the hope that it will be useful,
|
||||||
|
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
GNU Affero General Public License for more details.
|
||||||
|
|
||||||
|
You should have received a copy of the GNU Affero General Public License
|
||||||
|
along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||||
|
*/
|
||||||
|
|
||||||
|
export interface InstanceV1 {
|
||||||
|
uri: string;
|
||||||
|
account_domain: string;
|
||||||
|
title: string;
|
||||||
|
description: string;
|
||||||
|
short_description: string;
|
||||||
|
email: string;
|
||||||
|
version: string;
|
||||||
|
languages: any[]; // TODO: define this
|
||||||
|
registrations: boolean;
|
||||||
|
approval_required: boolean;
|
||||||
|
invites_enabled: boolean;
|
||||||
|
configuration: InstanceConfiguration;
|
||||||
|
urls: InstanceUrls;
|
||||||
|
stats: InstanceStats;
|
||||||
|
thumbnail: string;
|
||||||
|
contact_account: Object; // TODO: define this.
|
||||||
|
max_toot_chars: number;
|
||||||
|
rules: any[]; // TODO: define this
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface InstanceConfiguration {
|
||||||
|
statuses: InstanceStatuses;
|
||||||
|
media_attachments: InstanceMediaAttachments;
|
||||||
|
polls: InstancePolls;
|
||||||
|
accounts: InstanceAccounts;
|
||||||
|
emojis: InstanceEmojis;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface InstanceAccounts {
|
||||||
|
allow_custom_css: boolean;
|
||||||
|
max_featured_tags: number;
|
||||||
|
max_profile_fields: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface InstanceEmojis {
|
||||||
|
emoji_size_limit: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface InstanceMediaAttachments {
|
||||||
|
supported_mime_types: string[];
|
||||||
|
image_size_limit: number;
|
||||||
|
image_matrix_limit: number;
|
||||||
|
video_size_limit: number;
|
||||||
|
video_frame_rate_limit: number;
|
||||||
|
video_matrix_limit: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface InstancePolls {
|
||||||
|
max_options: number;
|
||||||
|
max_characters_per_option: number;
|
||||||
|
min_expiration: number;
|
||||||
|
max_expiration: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface InstanceStatuses {
|
||||||
|
max_characters: number;
|
||||||
|
max_media_attachments: number;
|
||||||
|
characters_reserved_per_url: number;
|
||||||
|
supported_mime_types: string[];
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface InstanceStats {
|
||||||
|
domain_count: number;
|
||||||
|
status_count: number;
|
||||||
|
user_count: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface InstanceUrls {
|
||||||
|
streaming_api: string;
|
||||||
|
}
|
||||||
|
|
|
@ -0,0 +1,95 @@
|
||||||
|
/*
|
||||||
|
GoToSocial
|
||||||
|
Copyright (C) GoToSocial Authors admin@gotosocial.org
|
||||||
|
SPDX-License-Identifier: AGPL-3.0-or-later
|
||||||
|
|
||||||
|
This program is free software: you can redistribute it and/or modify
|
||||||
|
it under the terms of the GNU Affero General Public License as published by
|
||||||
|
the Free Software Foundation, either version 3 of the License, or
|
||||||
|
(at your option) any later version.
|
||||||
|
|
||||||
|
This program is distributed in the hope that it will be useful,
|
||||||
|
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
GNU Affero General Public License for more details.
|
||||||
|
|
||||||
|
You should have received a copy of the GNU Affero General Public License
|
||||||
|
along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { Draft } from "@reduxjs/toolkit";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Pass into a query when you don't
|
||||||
|
* want to provide an argument to it.
|
||||||
|
*/
|
||||||
|
export const NoArg = undefined;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Shadow the redux onQueryStarted function for mutations.
|
||||||
|
* https://redux-toolkit.js.org/rtk-query/api/createApi#onquerystarted
|
||||||
|
*/
|
||||||
|
type OnMutationStarted = (
|
||||||
|
_arg: any,
|
||||||
|
_params: MutationStartedParams
|
||||||
|
) => Promise<void>;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Shadow the redux onQueryStarted function parameters for mutations.
|
||||||
|
* https://redux-toolkit.js.org/rtk-query/api/createApi#onquerystarted
|
||||||
|
*/
|
||||||
|
interface MutationStartedParams {
|
||||||
|
/**
|
||||||
|
* The dispatch method for the store.
|
||||||
|
*/
|
||||||
|
dispatch,
|
||||||
|
/**
|
||||||
|
* A method to get the current state for the store.
|
||||||
|
*/
|
||||||
|
getState,
|
||||||
|
/**
|
||||||
|
* extra as provided as thunk.extraArgument to the configureStore getDefaultMiddleware option.
|
||||||
|
*/
|
||||||
|
extra,
|
||||||
|
/**
|
||||||
|
* A unique ID generated for the query/mutation.
|
||||||
|
*/
|
||||||
|
requestId,
|
||||||
|
/**
|
||||||
|
* A Promise that will resolve with a data property (the transformed query result), and a
|
||||||
|
* meta property (meta returned by the baseQuery). If the query fails, this Promise will
|
||||||
|
* reject with the error. This allows you to await for the query to finish.
|
||||||
|
*/
|
||||||
|
queryFulfilled,
|
||||||
|
/**
|
||||||
|
* A function that gets the current value of the cache entry.
|
||||||
|
*/
|
||||||
|
getCacheEntry,
|
||||||
|
}
|
||||||
|
|
||||||
|
export type Action = (
|
||||||
|
_draft: Draft<any>,
|
||||||
|
_updated: any,
|
||||||
|
_params: ActionParams,
|
||||||
|
) => void;
|
||||||
|
|
||||||
|
export interface ActionParams {
|
||||||
|
/**
|
||||||
|
* Either a normal old string, or a custom
|
||||||
|
* function to derive the key to change based
|
||||||
|
* on the draft and updated data.
|
||||||
|
*
|
||||||
|
* @param _draft
|
||||||
|
* @param _updated
|
||||||
|
* @returns
|
||||||
|
*/
|
||||||
|
key?: string | ((_draft: Draft<any>, _updated: any) => string),
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Custom cache mutation.
|
||||||
|
*/
|
||||||
|
export type CacheMutation = (
|
||||||
|
_queryName: string | ((_arg: any) => string),
|
||||||
|
_params?: ActionParams,
|
||||||
|
) => { onQueryStarted: OnMutationStarted }
|
|
@ -0,0 +1,144 @@
|
||||||
|
/*
|
||||||
|
GoToSocial
|
||||||
|
Copyright (C) GoToSocial Authors admin@gotosocial.org
|
||||||
|
SPDX-License-Identifier: AGPL-3.0-or-later
|
||||||
|
|
||||||
|
This program is free software: you can redistribute it and/or modify
|
||||||
|
it under the terms of the GNU Affero General Public License as published by
|
||||||
|
the Free Software Foundation, either version 3 of the License, or
|
||||||
|
(at your option) any later version.
|
||||||
|
|
||||||
|
This program is distributed in the hope that it will be useful,
|
||||||
|
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
GNU Affero General Public License for more details.
|
||||||
|
|
||||||
|
You should have received a copy of the GNU Affero General Public License
|
||||||
|
along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||||
|
*/
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Admin model of a report. Differs from the client
|
||||||
|
* model, which contains less detailed information.
|
||||||
|
*/
|
||||||
|
export interface AdminReport {
|
||||||
|
/**
|
||||||
|
* ID of the report.
|
||||||
|
*/
|
||||||
|
id: string;
|
||||||
|
/**
|
||||||
|
* Whether an action has been taken by an admin in response to this report.
|
||||||
|
*/
|
||||||
|
action_taken: boolean;
|
||||||
|
/**
|
||||||
|
* Time action was taken, if at all.
|
||||||
|
*/
|
||||||
|
action_taken_at?: string;
|
||||||
|
/**
|
||||||
|
* Category under which this report was created.
|
||||||
|
*/
|
||||||
|
category: string;
|
||||||
|
/**
|
||||||
|
* Comment submitted by the report creator.
|
||||||
|
*/
|
||||||
|
comment: string;
|
||||||
|
/**
|
||||||
|
* Report was/should be federated to remote instance.
|
||||||
|
*/
|
||||||
|
forwarded: boolean;
|
||||||
|
/**
|
||||||
|
* Time when the report was created.
|
||||||
|
*/
|
||||||
|
created_at: string;
|
||||||
|
/**
|
||||||
|
* Time when the report was last updated.
|
||||||
|
*/
|
||||||
|
updated_at: string;
|
||||||
|
/**
|
||||||
|
* Account that created the report.
|
||||||
|
* TODO: model this properly.
|
||||||
|
*/
|
||||||
|
account: Object;
|
||||||
|
/**
|
||||||
|
* Reported account.
|
||||||
|
* TODO: model this properly.
|
||||||
|
*/
|
||||||
|
target_account: Object;
|
||||||
|
/**
|
||||||
|
* Admin account assigned to handle this report, if any.
|
||||||
|
* TODO: model this properly.
|
||||||
|
*/
|
||||||
|
assigned_account?: Object;
|
||||||
|
/**
|
||||||
|
* Admin account that has taken action on this report, if any.
|
||||||
|
* TODO: model this properly.
|
||||||
|
*/
|
||||||
|
action_taken_by_account?: Object;
|
||||||
|
/**
|
||||||
|
* Statuses cited by this report, if any.
|
||||||
|
* TODO: model this properly.
|
||||||
|
*/
|
||||||
|
statuses: Object[];
|
||||||
|
/**
|
||||||
|
* Rules broken according to the reporter, if any.
|
||||||
|
* TODO: model this properly.
|
||||||
|
*/
|
||||||
|
rules: Object[];
|
||||||
|
/**
|
||||||
|
* Comment stored about what action (if any) was taken.
|
||||||
|
*/
|
||||||
|
action_taken_comment?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Parameters for POST to /api/v1/admin/reports/{id}/resolve.
|
||||||
|
*/
|
||||||
|
export interface AdminReportResolveParams {
|
||||||
|
/**
|
||||||
|
* The ID of the report to resolve.
|
||||||
|
*/
|
||||||
|
id: string;
|
||||||
|
/**
|
||||||
|
* Comment to store about what action (if any) was taken.
|
||||||
|
* Will be shown to the user who created the report (if local).
|
||||||
|
*/
|
||||||
|
action_taken_comment?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Parameters for GET to /api/v1/admin/reports.
|
||||||
|
*/
|
||||||
|
export interface AdminReportListParams {
|
||||||
|
/**
|
||||||
|
* If set, show only resolved (true) or only unresolved (false) reports.
|
||||||
|
*/
|
||||||
|
resolved?: boolean;
|
||||||
|
/**
|
||||||
|
* If set, show only reports created by the given account ID.
|
||||||
|
*/
|
||||||
|
account_id?: string;
|
||||||
|
/**
|
||||||
|
* If set, show only reports that target the given account ID.
|
||||||
|
*/
|
||||||
|
target_account_id?: string;
|
||||||
|
/**
|
||||||
|
* If set, show only reports older (ie., lower) than the given ID.
|
||||||
|
* Report with the given ID will not be included in response.
|
||||||
|
*/
|
||||||
|
max_id?: string;
|
||||||
|
/**
|
||||||
|
* If set, show only reports newer (ie., higher) than the given ID.
|
||||||
|
* Report with the given ID will not be included in response.
|
||||||
|
*/
|
||||||
|
since_id?: string;
|
||||||
|
/**
|
||||||
|
* If set, show only reports *immediately newer* than the given ID.
|
||||||
|
* Report with the given ID will not be included in response.
|
||||||
|
*/
|
||||||
|
min_id?: string;
|
||||||
|
/**
|
||||||
|
* If set, limit returned reports to this number.
|
||||||
|
* Else, fall back to GtS API defaults.
|
||||||
|
*/
|
||||||
|
limit?: number;
|
||||||
|
}
|
|
@ -17,33 +17,32 @@
|
||||||
along with this program. If not, see <http://www.gnu.org/licenses/>.
|
along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
const isValidDomain = require("is-valid-domain");
|
import isValidDomain from "is-valid-domain";
|
||||||
const psl = require("psl");
|
import { get } from "psl";
|
||||||
|
|
||||||
function isValidDomainBlock(domain) {
|
/**
|
||||||
|
* Check the input string to ensure it's a valid
|
||||||
|
* domain that doesn't include a wildcard ("*").
|
||||||
|
* @param domain
|
||||||
|
* @returns
|
||||||
|
*/
|
||||||
|
export function isValidDomainPermission(domain: string): boolean {
|
||||||
return isValidDomain(domain, {
|
return isValidDomain(domain, {
|
||||||
/*
|
|
||||||
Wildcard prefix *. can be stripped since it's equivalent to not having it,
|
|
||||||
but wildcard anywhere else in the domain is not handled by the backend so it's invalid.
|
|
||||||
*/
|
|
||||||
wildcard: false,
|
wildcard: false,
|
||||||
allowUnicode: true
|
allowUnicode: true
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
/*
|
/**
|
||||||
Still can't think of a better function name for this,
|
* Checks a domain against the Public Suffix List <https://publicsuffix.org/> to see if we
|
||||||
but we're checking a domain against the Public Suffix List <https://publicsuffix.org/>
|
* should suggest removing subdomain(s), since they're likely owned/ran by the same party.
|
||||||
to see if we should suggest removing subdomain(s) since they're likely owned/ran by the same party
|
* Eg., "social.example.com" suggests "example.com".
|
||||||
social.example.com -> suggests example.com
|
* @param domain
|
||||||
*/
|
* @returns
|
||||||
function hasBetterScope(domain) {
|
*/
|
||||||
const lookup = psl.get(domain);
|
export function hasBetterScope(domain: string): string | undefined {
|
||||||
|
const lookup = get(domain);
|
||||||
if (lookup && lookup != domain) {
|
if (lookup && lookup != domain) {
|
||||||
return lookup;
|
return lookup;
|
||||||
} else {
|
|
||||||
return false;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
module.exports = { isValidDomainBlock, hasBetterScope };
|
|
|
@ -498,7 +498,7 @@ span.form-info {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.instance-list {
|
.domain-permissions-list {
|
||||||
p {
|
p {
|
||||||
margin-top: 0;
|
margin-top: 0;
|
||||||
}
|
}
|
||||||
|
@ -612,7 +612,7 @@ span.form-info {
|
||||||
padding: 0.75rem;
|
padding: 0.75rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
.instance-list .filter {
|
.domain-permissions-list .filter {
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -809,7 +809,7 @@ button.with-padding {
|
||||||
animation-fill-mode: forwards;
|
animation-fill-mode: forwards;
|
||||||
}
|
}
|
||||||
|
|
||||||
.suspend-import-list {
|
.domain-perm-import-list {
|
||||||
.checkbox-list-wrapper {
|
.checkbox-list-wrapper {
|
||||||
overflow-x: auto;
|
overflow-x: auto;
|
||||||
display: grid;
|
display: grid;
|
||||||
|
@ -844,7 +844,7 @@ button.with-padding {
|
||||||
#icon {
|
#icon {
|
||||||
align-self: center;
|
align-self: center;
|
||||||
|
|
||||||
.already-blocked {
|
.permission-already-exists {
|
||||||
color: $green1;
|
color: $green1;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -875,6 +875,12 @@ button.with-padding {
|
||||||
align-items: center;
|
align-items: center;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.form-field.radio {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
margin-left: 0.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
.button-grid {
|
.button-grid {
|
||||||
display: inline-grid;
|
display: inline-grid;
|
||||||
grid-template-columns: auto auto auto;
|
grid-template-columns: auto auto auto;
|
||||||
|
|
|
@ -19,8 +19,6 @@
|
||||||
|
|
||||||
const React = require("react");
|
const React = require("react");
|
||||||
|
|
||||||
const query = require("../lib/query");
|
|
||||||
|
|
||||||
const {
|
const {
|
||||||
useTextInput,
|
useTextInput,
|
||||||
useFileInput,
|
useFileInput,
|
||||||
|
@ -28,7 +26,7 @@ const {
|
||||||
useFieldArrayInput
|
useFieldArrayInput
|
||||||
} = require("../lib/form");
|
} = require("../lib/form");
|
||||||
|
|
||||||
const useFormSubmit = require("../lib/form/submit");
|
const useFormSubmit = require("../lib/form/submit").default;
|
||||||
const { useWithFormContext, FormContext } = require("../lib/form/context");
|
const { useWithFormContext, FormContext } = require("../lib/form/context");
|
||||||
|
|
||||||
const {
|
const {
|
||||||
|
@ -38,14 +36,18 @@ const {
|
||||||
Checkbox
|
Checkbox
|
||||||
} = require("../components/form/inputs");
|
} = require("../components/form/inputs");
|
||||||
|
|
||||||
const FormWithData = require("../lib/form/form-with-data");
|
const FormWithData = require("../lib/form/form-with-data").default;
|
||||||
const FakeProfile = require("../components/fake-profile");
|
const FakeProfile = require("../components/fake-profile");
|
||||||
const MutationButton = require("../components/form/mutation-button");
|
const MutationButton = require("../components/form/mutation-button");
|
||||||
|
|
||||||
|
const { useInstanceV1Query } = require("../lib/query");
|
||||||
|
const { useUpdateCredentialsMutation } = require("../lib/query/user");
|
||||||
|
const { useVerifyCredentialsQuery } = require("../lib/query/oauth");
|
||||||
|
|
||||||
module.exports = function UserProfile() {
|
module.exports = function UserProfile() {
|
||||||
return (
|
return (
|
||||||
<FormWithData
|
<FormWithData
|
||||||
dataQuery={query.useVerifyCredentialsQuery}
|
dataQuery={useVerifyCredentialsQuery}
|
||||||
DataForm={UserProfileForm}
|
DataForm={UserProfileForm}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
|
@ -64,7 +66,7 @@ function UserProfileForm({ data: profile }) {
|
||||||
- string custom_css (if enabled)
|
- string custom_css (if enabled)
|
||||||
*/
|
*/
|
||||||
|
|
||||||
const { data: instance } = query.useInstanceQuery();
|
const { data: instance } = useInstanceV1Query();
|
||||||
const instanceConfig = React.useMemo(() => {
|
const instanceConfig = React.useMemo(() => {
|
||||||
return {
|
return {
|
||||||
allowCustomCSS: instance?.configuration?.accounts?.allow_custom_css === true,
|
allowCustomCSS: instance?.configuration?.accounts?.allow_custom_css === true,
|
||||||
|
@ -88,7 +90,7 @@ function UserProfileForm({ data: profile }) {
|
||||||
}),
|
}),
|
||||||
};
|
};
|
||||||
|
|
||||||
const [submitForm, result] = useFormSubmit(form, query.useUpdateCredentialsMutation(), {
|
const [submitForm, result] = useFormSubmit(form, useUpdateCredentialsMutation(), {
|
||||||
onFinish: () => {
|
onFinish: () => {
|
||||||
form.avatar.reset();
|
form.avatar.reset();
|
||||||
form.header.reset();
|
form.header.reset();
|
||||||
|
|
|
@ -26,7 +26,7 @@ const {
|
||||||
useBoolInput
|
useBoolInput
|
||||||
} = require("../lib/form");
|
} = require("../lib/form");
|
||||||
|
|
||||||
const useFormSubmit = require("../lib/form/submit");
|
const useFormSubmit = require("../lib/form/submit").default;
|
||||||
|
|
||||||
const {
|
const {
|
||||||
Select,
|
Select,
|
||||||
|
@ -34,7 +34,7 @@ const {
|
||||||
Checkbox
|
Checkbox
|
||||||
} = require("../components/form/inputs");
|
} = require("../components/form/inputs");
|
||||||
|
|
||||||
const FormWithData = require("../lib/form/form-with-data");
|
const FormWithData = require("../lib/form/form-with-data").default;
|
||||||
const Languages = require("../components/languages");
|
const Languages = require("../components/languages");
|
||||||
const MutationButton = require("../components/form/mutation-button");
|
const MutationButton = require("../components/form/mutation-button");
|
||||||
|
|
||||||
|
|
|
@ -84,7 +84,7 @@
|
||||||
/* Type Checking */
|
/* Type Checking */
|
||||||
"strict": false, /* Enable all strict type-checking options. */
|
"strict": false, /* Enable all strict type-checking options. */
|
||||||
// "noImplicitAny": true, /* Enable error reporting for expressions and declarations with an implied 'any' type. */
|
// "noImplicitAny": true, /* Enable error reporting for expressions and declarations with an implied 'any' type. */
|
||||||
// "strictNullChecks": true, /* When type checking, take into account 'null' and 'undefined'. */
|
"strictNullChecks": true, /* When type checking, take into account 'null' and 'undefined'. */
|
||||||
// "strictFunctionTypes": true, /* When assigning functions, check to ensure parameters and the return values are subtype-compatible. */
|
// "strictFunctionTypes": true, /* When assigning functions, check to ensure parameters and the return values are subtype-compatible. */
|
||||||
// "strictBindCallApply": true, /* Check that the arguments for 'bind', 'call', and 'apply' methods match the original function. */
|
// "strictBindCallApply": true, /* Check that the arguments for 'bind', 'call', and 'apply' methods match the original function. */
|
||||||
// "strictPropertyInitialization": true, /* Check for class properties that are declared but not set in the constructor. */
|
// "strictPropertyInitialization": true, /* Check for class properties that are declared but not set in the constructor. */
|
||||||
|
@ -104,6 +104,10 @@
|
||||||
|
|
||||||
/* Completeness */
|
/* Completeness */
|
||||||
// "skipDefaultLibCheck": true, /* Skip type checking .d.ts files that are included with TypeScript. */
|
// "skipDefaultLibCheck": true, /* Skip type checking .d.ts files that are included with TypeScript. */
|
||||||
"skipLibCheck": true /* Skip type checking all .d.ts files. */
|
"skipLibCheck": true, /* Skip type checking all .d.ts files. */
|
||||||
|
|
||||||
|
"plugins": [
|
||||||
|
{ "transform": "typia/lib/transform" }
|
||||||
|
]
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1040,6 +1040,13 @@
|
||||||
through2 "^4.0.2"
|
through2 "^4.0.2"
|
||||||
xtend "^4.0.1"
|
xtend "^4.0.1"
|
||||||
|
|
||||||
|
"@cspotcode/source-map-support@^0.8.0":
|
||||||
|
version "0.8.1"
|
||||||
|
resolved "https://registry.yarnpkg.com/@cspotcode/source-map-support/-/source-map-support-0.8.1.tgz#00629c35a688e05a88b1cda684fb9d5e73f000a1"
|
||||||
|
integrity sha512-IchNf6dN4tHoMFIn/7OE8LWZ19Y6q/67Bmf6vnGREv8RSbBVb9LPJxEcnwrcwX6ixSvaiGoomAUvu4YSxXrVgw==
|
||||||
|
dependencies:
|
||||||
|
"@jridgewell/trace-mapping" "0.3.9"
|
||||||
|
|
||||||
"@eslint-community/eslint-utils@^4.2.0", "@eslint-community/eslint-utils@^4.4.0":
|
"@eslint-community/eslint-utils@^4.2.0", "@eslint-community/eslint-utils@^4.4.0":
|
||||||
version "4.4.0"
|
version "4.4.0"
|
||||||
resolved "https://registry.yarnpkg.com/@eslint-community/eslint-utils/-/eslint-utils-4.4.0.tgz#a23514e8fb9af1269d5f7788aa556798d61c6b59"
|
resolved "https://registry.yarnpkg.com/@eslint-community/eslint-utils/-/eslint-utils-4.4.0.tgz#a23514e8fb9af1269d5f7788aa556798d61c6b59"
|
||||||
|
@ -1132,7 +1139,7 @@
|
||||||
"@jridgewell/sourcemap-codec" "^1.4.10"
|
"@jridgewell/sourcemap-codec" "^1.4.10"
|
||||||
"@jridgewell/trace-mapping" "^0.3.9"
|
"@jridgewell/trace-mapping" "^0.3.9"
|
||||||
|
|
||||||
"@jridgewell/resolve-uri@^3.1.0":
|
"@jridgewell/resolve-uri@^3.0.3", "@jridgewell/resolve-uri@^3.1.0":
|
||||||
version "3.1.1"
|
version "3.1.1"
|
||||||
resolved "https://registry.yarnpkg.com/@jridgewell/resolve-uri/-/resolve-uri-3.1.1.tgz#c08679063f279615a3326583ba3a90d1d82cc721"
|
resolved "https://registry.yarnpkg.com/@jridgewell/resolve-uri/-/resolve-uri-3.1.1.tgz#c08679063f279615a3326583ba3a90d1d82cc721"
|
||||||
integrity sha512-dSYZh7HhCDtCKm4QakX0xFpsRDqjjtZf/kjI/v3T3Nwt5r8/qz/M19F9ySyOqU94SXBmeG9ttTul+YnR4LOxFA==
|
integrity sha512-dSYZh7HhCDtCKm4QakX0xFpsRDqjjtZf/kjI/v3T3Nwt5r8/qz/M19F9ySyOqU94SXBmeG9ttTul+YnR4LOxFA==
|
||||||
|
@ -1155,6 +1162,14 @@
|
||||||
resolved "https://registry.yarnpkg.com/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.4.15.tgz#d7c6e6755c78567a951e04ab52ef0fd26de59f32"
|
resolved "https://registry.yarnpkg.com/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.4.15.tgz#d7c6e6755c78567a951e04ab52ef0fd26de59f32"
|
||||||
integrity sha512-eF2rxCRulEKXHTRiDrDy6erMYWqNw4LPdQ8UQA4huuxaQsVeRPFl2oM8oDGxMFhJUWZf9McpLtJasDDZb/Bpeg==
|
integrity sha512-eF2rxCRulEKXHTRiDrDy6erMYWqNw4LPdQ8UQA4huuxaQsVeRPFl2oM8oDGxMFhJUWZf9McpLtJasDDZb/Bpeg==
|
||||||
|
|
||||||
|
"@jridgewell/trace-mapping@0.3.9":
|
||||||
|
version "0.3.9"
|
||||||
|
resolved "https://registry.yarnpkg.com/@jridgewell/trace-mapping/-/trace-mapping-0.3.9.tgz#6534fd5933a53ba7cbf3a17615e273a0d1273ff9"
|
||||||
|
integrity sha512-3Belt6tdc8bPgAtbcmdtNJlirVoTmEb5e2gC94PnkwEW9jI6CAHUeoG85tjWP5WquqfavoMtMwiG4P926ZKKuQ==
|
||||||
|
dependencies:
|
||||||
|
"@jridgewell/resolve-uri" "^3.0.3"
|
||||||
|
"@jridgewell/sourcemap-codec" "^1.4.10"
|
||||||
|
|
||||||
"@jridgewell/trace-mapping@^0.3.17", "@jridgewell/trace-mapping@^0.3.9":
|
"@jridgewell/trace-mapping@^0.3.17", "@jridgewell/trace-mapping@^0.3.9":
|
||||||
version "0.3.19"
|
version "0.3.19"
|
||||||
resolved "https://registry.yarnpkg.com/@jridgewell/trace-mapping/-/trace-mapping-0.3.19.tgz#f8a3249862f91be48d3127c3cfe992f79b4b8811"
|
resolved "https://registry.yarnpkg.com/@jridgewell/trace-mapping/-/trace-mapping-0.3.19.tgz#f8a3249862f91be48d3127c3cfe992f79b4b8811"
|
||||||
|
@ -1194,6 +1209,31 @@
|
||||||
redux-thunk "^2.4.2"
|
redux-thunk "^2.4.2"
|
||||||
reselect "^4.1.8"
|
reselect "^4.1.8"
|
||||||
|
|
||||||
|
"@tsconfig/node10@^1.0.7":
|
||||||
|
version "1.0.9"
|
||||||
|
resolved "https://registry.yarnpkg.com/@tsconfig/node10/-/node10-1.0.9.tgz#df4907fc07a886922637b15e02d4cebc4c0021b2"
|
||||||
|
integrity sha512-jNsYVVxU8v5g43Erja32laIDHXeoNvFEpX33OK4d6hljo3jDhCBDhx5dhCCTMWUojscpAagGiRkBKxpdl9fxqA==
|
||||||
|
|
||||||
|
"@tsconfig/node12@^1.0.7":
|
||||||
|
version "1.0.11"
|
||||||
|
resolved "https://registry.yarnpkg.com/@tsconfig/node12/-/node12-1.0.11.tgz#ee3def1f27d9ed66dac6e46a295cffb0152e058d"
|
||||||
|
integrity sha512-cqefuRsh12pWyGsIoBKJA9luFu3mRxCA+ORZvA4ktLSzIuCUtWVxGIuXigEwO5/ywWFMZ2QEGKWvkZG1zDMTag==
|
||||||
|
|
||||||
|
"@tsconfig/node14@^1.0.0":
|
||||||
|
version "1.0.3"
|
||||||
|
resolved "https://registry.yarnpkg.com/@tsconfig/node14/-/node14-1.0.3.tgz#e4386316284f00b98435bf40f72f75a09dabf6c1"
|
||||||
|
integrity sha512-ysT8mhdixWK6Hw3i1V2AeRqZ5WfXg1G43mqoYlM2nc6388Fq5jcXyr5mRsqViLx/GJYdoL0bfXD8nmF+Zn/Iow==
|
||||||
|
|
||||||
|
"@tsconfig/node16@^1.0.2":
|
||||||
|
version "1.0.4"
|
||||||
|
resolved "https://registry.yarnpkg.com/@tsconfig/node16/-/node16-1.0.4.tgz#0b92dcc0cc1c81f6f306a381f28e31b1a56536e9"
|
||||||
|
integrity sha512-vxhUy4J8lyeyinH7Azl1pdd43GJhZH/tP2weN8TntQblOY+A0XbT8DJk1/oCPuOOyg/Ja757rG0CgHcWC8OfMA==
|
||||||
|
|
||||||
|
"@types/bluebird@^3.5.39":
|
||||||
|
version "3.5.39"
|
||||||
|
resolved "https://registry.yarnpkg.com/@types/bluebird/-/bluebird-3.5.39.tgz#6aaf8bcbf005bb091d06ddaa0f620be078bf6a73"
|
||||||
|
integrity sha512-0h2lKudcFwHih8NHAgt/uyAIUQDO0AdfJYlWBXD8r+gFDulUi2CMZoQSh2Q5ol1FMaHV9k7/4HtcbA8ABtexmA==
|
||||||
|
|
||||||
"@types/hoist-non-react-statics@^3.3.1":
|
"@types/hoist-non-react-statics@^3.3.1":
|
||||||
version "3.3.2"
|
version "3.3.2"
|
||||||
resolved "https://registry.yarnpkg.com/@types/hoist-non-react-statics/-/hoist-non-react-statics-3.3.2.tgz#dc1e9ded53375d37603c479cc12c693b0878aa2a"
|
resolved "https://registry.yarnpkg.com/@types/hoist-non-react-statics/-/hoist-non-react-statics-3.3.2.tgz#dc1e9ded53375d37603c479cc12c693b0878aa2a"
|
||||||
|
@ -1209,6 +1249,11 @@
|
||||||
dependencies:
|
dependencies:
|
||||||
"@types/node" "*"
|
"@types/node" "*"
|
||||||
|
|
||||||
|
"@types/is-valid-domain@^0.0.2":
|
||||||
|
version "0.0.2"
|
||||||
|
resolved "https://registry.yarnpkg.com/@types/is-valid-domain/-/is-valid-domain-0.0.2.tgz#78b236f05da281213481c4af0a7ce452d4ff810a"
|
||||||
|
integrity sha512-18CgqfDjh0m+GFfekGz1q3g32XESx7vutfBFnPkIdpDtuvgvOac8lrghRiw3SLI19vNa/XdPKIhL6CQpFMIDug==
|
||||||
|
|
||||||
"@types/json-schema@^7.0.12":
|
"@types/json-schema@^7.0.12":
|
||||||
version "7.0.13"
|
version "7.0.13"
|
||||||
resolved "https://registry.yarnpkg.com/@types/json-schema/-/json-schema-7.0.13.tgz#02c24f4363176d2d18fc8b70b9f3c54aba178a85"
|
resolved "https://registry.yarnpkg.com/@types/json-schema/-/json-schema-7.0.13.tgz#02c24f4363176d2d18fc8b70b9f3c54aba178a85"
|
||||||
|
@ -1219,11 +1264,23 @@
|
||||||
resolved "https://registry.yarnpkg.com/@types/node/-/node-20.8.2.tgz#d76fb80d87d0d8abfe334fc6d292e83e5524efc4"
|
resolved "https://registry.yarnpkg.com/@types/node/-/node-20.8.2.tgz#d76fb80d87d0d8abfe334fc6d292e83e5524efc4"
|
||||||
integrity sha512-Vvycsc9FQdwhxE3y3DzeIxuEJbWGDsnrxvMADzTDF/lcdR9/K+AQIeAghTQsHtotg/q0j3WEOYS/jQgSdWue3w==
|
integrity sha512-Vvycsc9FQdwhxE3y3DzeIxuEJbWGDsnrxvMADzTDF/lcdR9/K+AQIeAghTQsHtotg/q0j3WEOYS/jQgSdWue3w==
|
||||||
|
|
||||||
|
"@types/papaparse@^5.3.9":
|
||||||
|
version "5.3.9"
|
||||||
|
resolved "https://registry.yarnpkg.com/@types/papaparse/-/papaparse-5.3.9.tgz#5f955949eae512c1eec70bba4bfeb2e7f4396564"
|
||||||
|
integrity sha512-sZcrKD63qA4/6GyBcVvX6AIp0AkpfyYk00CUQHMBvb4+OVXTZWyXUvidUZaai1wyKUVyJoxO7mgREam/pMRrDw==
|
||||||
|
dependencies:
|
||||||
|
"@types/node" "*"
|
||||||
|
|
||||||
"@types/prop-types@*":
|
"@types/prop-types@*":
|
||||||
version "15.7.8"
|
version "15.7.8"
|
||||||
resolved "https://registry.yarnpkg.com/@types/prop-types/-/prop-types-15.7.8.tgz#805eae6e8f41bd19e88917d2ea200dc992f405d3"
|
resolved "https://registry.yarnpkg.com/@types/prop-types/-/prop-types-15.7.8.tgz#805eae6e8f41bd19e88917d2ea200dc992f405d3"
|
||||||
integrity sha512-kMpQpfZKSCBqltAJwskgePRaYRFukDkm1oItcAbC3gNELR20XIBcN9VRgg4+m8DKsTfkWeA4m4Imp4DDuWy7FQ==
|
integrity sha512-kMpQpfZKSCBqltAJwskgePRaYRFukDkm1oItcAbC3gNELR20XIBcN9VRgg4+m8DKsTfkWeA4m4Imp4DDuWy7FQ==
|
||||||
|
|
||||||
|
"@types/psl@^1.1.1":
|
||||||
|
version "1.1.1"
|
||||||
|
resolved "https://registry.yarnpkg.com/@types/psl/-/psl-1.1.1.tgz#3ba9e6d4bd2a32652a639fd5df7e539151d0a3b2"
|
||||||
|
integrity sha512-nHPbucWhAfVSuJ+xVc4AjjtM/y6U/eLHeXxyjzPHzKVr+j8uHvGg2wlXjmReSE2p851ltEWKGNQOtBK0beF/Eg==
|
||||||
|
|
||||||
"@types/react-dom@^18.2.8":
|
"@types/react-dom@^18.2.8":
|
||||||
version "18.2.10"
|
version "18.2.10"
|
||||||
resolved "https://registry.yarnpkg.com/@types/react-dom/-/react-dom-18.2.10.tgz#06247cb600e39b63a0a385f6a5014c44bab296f2"
|
resolved "https://registry.yarnpkg.com/@types/react-dom/-/react-dom-18.2.10.tgz#06247cb600e39b63a0a385f6a5014c44bab296f2"
|
||||||
|
@ -1781,12 +1838,17 @@ acorn-walk@^7.0.0:
|
||||||
resolved "https://registry.yarnpkg.com/acorn-walk/-/acorn-walk-7.2.0.tgz#0de889a601203909b0fbe07b8938dc21d2e967bc"
|
resolved "https://registry.yarnpkg.com/acorn-walk/-/acorn-walk-7.2.0.tgz#0de889a601203909b0fbe07b8938dc21d2e967bc"
|
||||||
integrity sha512-OPdCF6GsMIP+Az+aWfAAOEt2/+iVDKE7oy6lJ098aoe59oAmK76qV6Gw60SbZ8jHuG2wH058GF4pLFbYamYrVA==
|
integrity sha512-OPdCF6GsMIP+Az+aWfAAOEt2/+iVDKE7oy6lJ098aoe59oAmK76qV6Gw60SbZ8jHuG2wH058GF4pLFbYamYrVA==
|
||||||
|
|
||||||
|
acorn-walk@^8.1.1:
|
||||||
|
version "8.2.0"
|
||||||
|
resolved "https://registry.yarnpkg.com/acorn-walk/-/acorn-walk-8.2.0.tgz#741210f2e2426454508853a2f44d0ab83b7f69c1"
|
||||||
|
integrity sha512-k+iyHEuPgSw6SbuDpGQM+06HQUa04DZ3o+F6CSzXMvvI5KMvnaEqXe+YVe555R9nn6GPt404fos4wcgpw12SDA==
|
||||||
|
|
||||||
acorn@^7.0.0:
|
acorn@^7.0.0:
|
||||||
version "7.4.1"
|
version "7.4.1"
|
||||||
resolved "https://registry.yarnpkg.com/acorn/-/acorn-7.4.1.tgz#feaed255973d2e77555b83dbc08851a6c63520fa"
|
resolved "https://registry.yarnpkg.com/acorn/-/acorn-7.4.1.tgz#feaed255973d2e77555b83dbc08851a6c63520fa"
|
||||||
integrity sha512-nQyp0o1/mNdbTO1PO6kHkwSrmgZ0MT/jCCpNiwbUjGoRN4dlBhqJtoQuCnEOKzgTVwg0ZWiCoQy6SxMebQVh8A==
|
integrity sha512-nQyp0o1/mNdbTO1PO6kHkwSrmgZ0MT/jCCpNiwbUjGoRN4dlBhqJtoQuCnEOKzgTVwg0ZWiCoQy6SxMebQVh8A==
|
||||||
|
|
||||||
acorn@^8.8.2, acorn@^8.9.0:
|
acorn@^8.4.1, acorn@^8.8.2, acorn@^8.9.0:
|
||||||
version "8.10.0"
|
version "8.10.0"
|
||||||
resolved "https://registry.yarnpkg.com/acorn/-/acorn-8.10.0.tgz#8be5b3907a67221a81ab23c7889c4c5526b62ec5"
|
resolved "https://registry.yarnpkg.com/acorn/-/acorn-8.10.0.tgz#8be5b3907a67221a81ab23c7889c4c5526b62ec5"
|
||||||
integrity sha512-F0SAmZ8iUtS//m8DmCTA0jlh6TDKkHQyK6xc6V4KDTyZKA9dnvX9/3sRTVQrWm79glUAZbnmmNcdYwUIHWVybw==
|
integrity sha512-F0SAmZ8iUtS//m8DmCTA0jlh6TDKkHQyK6xc6V4KDTyZKA9dnvX9/3sRTVQrWm79glUAZbnmmNcdYwUIHWVybw==
|
||||||
|
@ -1806,6 +1868,13 @@ amdefine@>=0.0.4:
|
||||||
resolved "https://registry.yarnpkg.com/amdefine/-/amdefine-1.0.1.tgz#4a5282ac164729e93619bcfd3ad151f817ce91f5"
|
resolved "https://registry.yarnpkg.com/amdefine/-/amdefine-1.0.1.tgz#4a5282ac164729e93619bcfd3ad151f817ce91f5"
|
||||||
integrity sha512-S2Hw0TtNkMJhIabBwIojKL9YHO5T0n5eNqWJ7Lrlel/zDbftQpxpapi8tZs3X1HWa+u+QeydGmzzNU0m09+Rcg==
|
integrity sha512-S2Hw0TtNkMJhIabBwIojKL9YHO5T0n5eNqWJ7Lrlel/zDbftQpxpapi8tZs3X1HWa+u+QeydGmzzNU0m09+Rcg==
|
||||||
|
|
||||||
|
ansi-escapes@^4.2.1:
|
||||||
|
version "4.3.2"
|
||||||
|
resolved "https://registry.yarnpkg.com/ansi-escapes/-/ansi-escapes-4.3.2.tgz#6b2291d1db7d98b6521d5f1efa42d0f3a9feb65e"
|
||||||
|
integrity sha512-gKXj5ALrKWQLsYG9jlTRmR/xKluxHV+Z9QEwNIgCfM1/uwPMCuzVVnh5mwTd+OuBZcwSIMbqssNWRm1lE51QaQ==
|
||||||
|
dependencies:
|
||||||
|
type-fest "^0.21.3"
|
||||||
|
|
||||||
ansi-regex@^5.0.1:
|
ansi-regex@^5.0.1:
|
||||||
version "5.0.1"
|
version "5.0.1"
|
||||||
resolved "https://registry.yarnpkg.com/ansi-regex/-/ansi-regex-5.0.1.tgz#082cb2c89c9fe8659a311a53bd6a4dc5301db304"
|
resolved "https://registry.yarnpkg.com/ansi-regex/-/ansi-regex-5.0.1.tgz#082cb2c89c9fe8659a311a53bd6a4dc5301db304"
|
||||||
|
@ -1818,7 +1887,7 @@ ansi-styles@^3.2.1:
|
||||||
dependencies:
|
dependencies:
|
||||||
color-convert "^1.9.0"
|
color-convert "^1.9.0"
|
||||||
|
|
||||||
ansi-styles@^4.1.0:
|
ansi-styles@^4.0.0, ansi-styles@^4.1.0:
|
||||||
version "4.3.0"
|
version "4.3.0"
|
||||||
resolved "https://registry.yarnpkg.com/ansi-styles/-/ansi-styles-4.3.0.tgz#edd803628ae71c04c85ae7a0906edad34b648937"
|
resolved "https://registry.yarnpkg.com/ansi-styles/-/ansi-styles-4.3.0.tgz#edd803628ae71c04c85ae7a0906edad34b648937"
|
||||||
integrity sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==
|
integrity sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==
|
||||||
|
@ -1838,6 +1907,11 @@ anymatch@^3.1.0, anymatch@~3.1.2:
|
||||||
normalize-path "^3.0.0"
|
normalize-path "^3.0.0"
|
||||||
picomatch "^2.0.4"
|
picomatch "^2.0.4"
|
||||||
|
|
||||||
|
arg@^4.1.0:
|
||||||
|
version "4.1.3"
|
||||||
|
resolved "https://registry.yarnpkg.com/arg/-/arg-4.1.3.tgz#269fc7ad5b8e42cb63c896d5666017261c144089"
|
||||||
|
integrity sha512-58S9QDqG0Xx27YwPSt9fJxivjYl432YCwfDMfZ+71RAqUrZef7LrKQZ3LHLOwCS4FLNBplP533Zx895SeOCHvA==
|
||||||
|
|
||||||
argparse@^2.0.1:
|
argparse@^2.0.1:
|
||||||
version "2.0.1"
|
version "2.0.1"
|
||||||
resolved "https://registry.yarnpkg.com/argparse/-/argparse-2.0.1.tgz#246f50f3ca78a3240f6c997e8a9bd1eac49e4b38"
|
resolved "https://registry.yarnpkg.com/argparse/-/argparse-2.0.1.tgz#246f50f3ca78a3240f6c997e8a9bd1eac49e4b38"
|
||||||
|
@ -1893,6 +1967,11 @@ array-includes@^3.1.6:
|
||||||
get-intrinsic "^1.2.1"
|
get-intrinsic "^1.2.1"
|
||||||
is-string "^1.0.7"
|
is-string "^1.0.7"
|
||||||
|
|
||||||
|
array-timsort@^1.0.3:
|
||||||
|
version "1.0.3"
|
||||||
|
resolved "https://registry.yarnpkg.com/array-timsort/-/array-timsort-1.0.3.tgz#3c9e4199e54fb2b9c3fe5976396a21614ef0d926"
|
||||||
|
integrity sha512-/+3GRL7dDAGEfM6TseQk/U+mi18TU2Ms9I3UlLdUMhz2hbvGNTKdj9xniwXfUqgYhHxRx0+8UnKkvlNwVU+cWQ==
|
||||||
|
|
||||||
array-union@^2.1.0:
|
array-union@^2.1.0:
|
||||||
version "2.1.0"
|
version "2.1.0"
|
||||||
resolved "https://registry.yarnpkg.com/array-union/-/array-union-2.1.0.tgz#b798420adbeb1de828d84acd8a2e23d3efe85e8d"
|
resolved "https://registry.yarnpkg.com/array-union/-/array-union-2.1.0.tgz#b798420adbeb1de828d84acd8a2e23d3efe85e8d"
|
||||||
|
@ -2043,7 +2122,7 @@ binary-extensions@^2.0.0:
|
||||||
resolved "https://registry.yarnpkg.com/binary-extensions/-/binary-extensions-2.2.0.tgz#75f502eeaf9ffde42fc98829645be4ea76bd9e2d"
|
resolved "https://registry.yarnpkg.com/binary-extensions/-/binary-extensions-2.2.0.tgz#75f502eeaf9ffde42fc98829645be4ea76bd9e2d"
|
||||||
integrity sha512-jDctJ/IVQbZoJykoeHbhXpOlNBqGNcwXJKJog42E5HDPUwQTSdjCHdihjj0DlnheQ7blbT6dHOafNAiS8ooQKA==
|
integrity sha512-jDctJ/IVQbZoJykoeHbhXpOlNBqGNcwXJKJog42E5HDPUwQTSdjCHdihjj0DlnheQ7blbT6dHOafNAiS8ooQKA==
|
||||||
|
|
||||||
bl@^4.0.0, bl@^4.0.2:
|
bl@^4.0.0, bl@^4.0.2, bl@^4.1.0:
|
||||||
version "4.1.0"
|
version "4.1.0"
|
||||||
resolved "https://registry.yarnpkg.com/bl/-/bl-4.1.0.tgz#451535264182bec2fbbc83a62ab98cf11d9f7b3a"
|
resolved "https://registry.yarnpkg.com/bl/-/bl-4.1.0.tgz#451535264182bec2fbbc83a62ab98cf11d9f7b3a"
|
||||||
integrity sha512-1W07cM9gS6DcLperZfFSj+bWLtaPGSOHWhPiGzXmvVJbRLdG82sH/Kn8EtW1VqWVA54AKf2h5k5BbnIbwF3h6w==
|
integrity sha512-1W07cM9gS6DcLperZfFSj+bWLtaPGSOHWhPiGzXmvVJbRLdG82sH/Kn8EtW1VqWVA54AKf2h5k5BbnIbwF3h6w==
|
||||||
|
@ -2343,7 +2422,7 @@ chalk@^2.4.2:
|
||||||
escape-string-regexp "^1.0.5"
|
escape-string-regexp "^1.0.5"
|
||||||
supports-color "^5.3.0"
|
supports-color "^5.3.0"
|
||||||
|
|
||||||
chalk@^4.0.0, chalk@^4.1.0:
|
chalk@^4.0.0, chalk@^4.1.0, chalk@^4.1.1, chalk@^4.1.2:
|
||||||
version "4.1.2"
|
version "4.1.2"
|
||||||
resolved "https://registry.yarnpkg.com/chalk/-/chalk-4.1.2.tgz#aac4e2b7734a740867aeb16bf02aad556a1e7a01"
|
resolved "https://registry.yarnpkg.com/chalk/-/chalk-4.1.2.tgz#aac4e2b7734a740867aeb16bf02aad556a1e7a01"
|
||||||
integrity sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==
|
integrity sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==
|
||||||
|
@ -2351,6 +2430,11 @@ chalk@^4.0.0, chalk@^4.1.0:
|
||||||
ansi-styles "^4.1.0"
|
ansi-styles "^4.1.0"
|
||||||
supports-color "^7.1.0"
|
supports-color "^7.1.0"
|
||||||
|
|
||||||
|
chardet@^0.7.0:
|
||||||
|
version "0.7.0"
|
||||||
|
resolved "https://registry.yarnpkg.com/chardet/-/chardet-0.7.0.tgz#90094849f0937f2eedc2425d0d28a9e5f0cbad9e"
|
||||||
|
integrity sha512-mT8iDcrh03qDGRRmoA2hmBJnxpllMR+0/0qlzjqZES6NdiWDcZkCNAk4rPFZ9Q85r27unkiNNg8ZOiwZXBHwcA==
|
||||||
|
|
||||||
chokidar@^3.4.0:
|
chokidar@^3.4.0:
|
||||||
version "3.5.3"
|
version "3.5.3"
|
||||||
resolved "https://registry.yarnpkg.com/chokidar/-/chokidar-3.5.3.tgz#1cf37c8707b932bd1af1ae22c0432e2acd1903bd"
|
resolved "https://registry.yarnpkg.com/chokidar/-/chokidar-3.5.3.tgz#1cf37c8707b932bd1af1ae22c0432e2acd1903bd"
|
||||||
|
@ -2374,6 +2458,23 @@ cipher-base@^1.0.0, cipher-base@^1.0.1, cipher-base@^1.0.3:
|
||||||
inherits "^2.0.1"
|
inherits "^2.0.1"
|
||||||
safe-buffer "^5.0.1"
|
safe-buffer "^5.0.1"
|
||||||
|
|
||||||
|
cli-cursor@^3.1.0:
|
||||||
|
version "3.1.0"
|
||||||
|
resolved "https://registry.yarnpkg.com/cli-cursor/-/cli-cursor-3.1.0.tgz#264305a7ae490d1d03bf0c9ba7c925d1753af307"
|
||||||
|
integrity sha512-I/zHAwsKf9FqGoXM4WWRACob9+SNukZTd94DWF57E4toouRulbCxcUh6RKUEOQlYTHJnzkPMySvPNaaSLNfLZw==
|
||||||
|
dependencies:
|
||||||
|
restore-cursor "^3.1.0"
|
||||||
|
|
||||||
|
cli-spinners@^2.5.0:
|
||||||
|
version "2.9.1"
|
||||||
|
resolved "https://registry.yarnpkg.com/cli-spinners/-/cli-spinners-2.9.1.tgz#9c0b9dad69a6d47cbb4333c14319b060ed395a35"
|
||||||
|
integrity sha512-jHgecW0pxkonBJdrKsqxgRX9AcG+u/5k0Q7WPDfi8AogLAdwxEkyYYNWwZ5GvVFoFx2uiY1eNcSK00fh+1+FyQ==
|
||||||
|
|
||||||
|
cli-width@^3.0.0:
|
||||||
|
version "3.0.0"
|
||||||
|
resolved "https://registry.yarnpkg.com/cli-width/-/cli-width-3.0.0.tgz#a2f48437a2caa9a22436e794bf071ec9e61cedf6"
|
||||||
|
integrity sha512-FxqpkPPwu1HjuN93Omfm4h8uIanXofW0RxVEW3k5RKx+mJJYSthzNhp32Kzxxy3YAEZ/Dc/EWN1vZRY0+kOhbw==
|
||||||
|
|
||||||
clone-regexp@^2.1.0:
|
clone-regexp@^2.1.0:
|
||||||
version "2.2.0"
|
version "2.2.0"
|
||||||
resolved "https://registry.yarnpkg.com/clone-regexp/-/clone-regexp-2.2.0.tgz#7d65e00885cd8796405c35a737e7a86b7429e36f"
|
resolved "https://registry.yarnpkg.com/clone-regexp/-/clone-regexp-2.2.0.tgz#7d65e00885cd8796405c35a737e7a86b7429e36f"
|
||||||
|
@ -2381,6 +2482,11 @@ clone-regexp@^2.1.0:
|
||||||
dependencies:
|
dependencies:
|
||||||
is-regexp "^2.0.0"
|
is-regexp "^2.0.0"
|
||||||
|
|
||||||
|
clone@^1.0.2:
|
||||||
|
version "1.0.4"
|
||||||
|
resolved "https://registry.yarnpkg.com/clone/-/clone-1.0.4.tgz#da309cc263df15994c688ca902179ca3c7cd7c7e"
|
||||||
|
integrity sha512-JQHZ2QMW6l3aH/j6xCqQThY/9OH4D/9ls34cgkUBiEeocRTU04tHfKPBsUK1PqZCUQM7GiA0IIXJSuXHI64Kbg==
|
||||||
|
|
||||||
color-convert@^1.9.0:
|
color-convert@^1.9.0:
|
||||||
version "1.9.3"
|
version "1.9.3"
|
||||||
resolved "https://registry.yarnpkg.com/color-convert/-/color-convert-1.9.3.tgz#bb71850690e1f136567de629d2d5471deda4c1e8"
|
resolved "https://registry.yarnpkg.com/color-convert/-/color-convert-1.9.3.tgz#bb71850690e1f136567de629d2d5471deda4c1e8"
|
||||||
|
@ -2425,11 +2531,27 @@ combine-source-map@~0.6.1:
|
||||||
lodash.memoize "~3.0.3"
|
lodash.memoize "~3.0.3"
|
||||||
source-map "~0.4.2"
|
source-map "~0.4.2"
|
||||||
|
|
||||||
|
commander@^10.0.0:
|
||||||
|
version "10.0.1"
|
||||||
|
resolved "https://registry.yarnpkg.com/commander/-/commander-10.0.1.tgz#881ee46b4f77d1c1dccc5823433aa39b022cbe06"
|
||||||
|
integrity sha512-y4Mg2tXshplEbSGzx7amzPwKKOCGuoSRP/CjEdwwk0FOGlUbq6lKuoyDZTNZkmxHdJtp54hdfY/JUrdL7Xfdug==
|
||||||
|
|
||||||
commander@^2.20.0:
|
commander@^2.20.0:
|
||||||
version "2.20.3"
|
version "2.20.3"
|
||||||
resolved "https://registry.yarnpkg.com/commander/-/commander-2.20.3.tgz#fd485e84c03eb4881c20722ba48035e8531aeb33"
|
resolved "https://registry.yarnpkg.com/commander/-/commander-2.20.3.tgz#fd485e84c03eb4881c20722ba48035e8531aeb33"
|
||||||
integrity sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ==
|
integrity sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ==
|
||||||
|
|
||||||
|
comment-json@^4.2.3:
|
||||||
|
version "4.2.3"
|
||||||
|
resolved "https://registry.yarnpkg.com/comment-json/-/comment-json-4.2.3.tgz#50b487ebbf43abe44431f575ebda07d30d015365"
|
||||||
|
integrity sha512-SsxdiOf064DWoZLH799Ata6u7iV658A11PlWtZATDlXPpKGJnbJZ5Z24ybixAi+LUUqJ/GKowAejtC5GFUG7Tw==
|
||||||
|
dependencies:
|
||||||
|
array-timsort "^1.0.3"
|
||||||
|
core-util-is "^1.0.3"
|
||||||
|
esprima "^4.0.1"
|
||||||
|
has-own-prop "^2.0.0"
|
||||||
|
repeat-string "^1.6.1"
|
||||||
|
|
||||||
concat-map@0.0.1:
|
concat-map@0.0.1:
|
||||||
version "0.0.1"
|
version "0.0.1"
|
||||||
resolved "https://registry.yarnpkg.com/concat-map/-/concat-map-0.0.1.tgz#d8a96bd77fd68df7793a73036a3ba0d5405d477b"
|
resolved "https://registry.yarnpkg.com/concat-map/-/concat-map-0.0.1.tgz#d8a96bd77fd68df7793a73036a3ba0d5405d477b"
|
||||||
|
@ -2509,7 +2631,7 @@ core-js@^3.26.1:
|
||||||
resolved "https://registry.yarnpkg.com/core-js/-/core-js-3.33.0.tgz#70366dbf737134761edb017990cf5ce6c6369c40"
|
resolved "https://registry.yarnpkg.com/core-js/-/core-js-3.33.0.tgz#70366dbf737134761edb017990cf5ce6c6369c40"
|
||||||
integrity sha512-HoZr92+ZjFEKar5HS6MC776gYslNOKHt75mEBKWKnPeFDpZ6nH5OeF3S6HFT1mUAUZKrzkez05VboaX8myjSuw==
|
integrity sha512-HoZr92+ZjFEKar5HS6MC776gYslNOKHt75mEBKWKnPeFDpZ6nH5OeF3S6HFT1mUAUZKrzkez05VboaX8myjSuw==
|
||||||
|
|
||||||
core-util-is@~1.0.0:
|
core-util-is@^1.0.3, core-util-is@~1.0.0:
|
||||||
version "1.0.3"
|
version "1.0.3"
|
||||||
resolved "https://registry.yarnpkg.com/core-util-is/-/core-util-is-1.0.3.tgz#a6042d3634c2b27e9328f837b965fac83808db85"
|
resolved "https://registry.yarnpkg.com/core-util-is/-/core-util-is-1.0.3.tgz#a6042d3634c2b27e9328f837b965fac83808db85"
|
||||||
integrity sha512-ZQBvi1DcpJ4GDqanjucZ2Hj3wEO5pZDS89BWbkcrvdxksJorwUDDZamX9ldFkp9aw2lmBDLgkObEA4DWNJ9FYQ==
|
integrity sha512-ZQBvi1DcpJ4GDqanjucZ2Hj3wEO5pZDS89BWbkcrvdxksJorwUDDZamX9ldFkp9aw2lmBDLgkObEA4DWNJ9FYQ==
|
||||||
|
@ -2550,6 +2672,11 @@ create-hmac@^1.1.0, create-hmac@^1.1.4, create-hmac@^1.1.7:
|
||||||
safe-buffer "^5.0.1"
|
safe-buffer "^5.0.1"
|
||||||
sha.js "^2.4.8"
|
sha.js "^2.4.8"
|
||||||
|
|
||||||
|
create-require@^1.1.0:
|
||||||
|
version "1.1.1"
|
||||||
|
resolved "https://registry.yarnpkg.com/create-require/-/create-require-1.1.1.tgz#c1d7e8f1e5f6cfc9ff65f9cd352d37348756c333"
|
||||||
|
integrity sha512-dcKFX3jn0MpIaXjisoRvexIJVEKzaq7z2rZKxf+MSr9TkdmHmsU4m2lcLojrj/FHl8mk5VxMmYA+ftRkP/3oKQ==
|
||||||
|
|
||||||
cross-spawn@^7.0.2:
|
cross-spawn@^7.0.2:
|
||||||
version "7.0.3"
|
version "7.0.3"
|
||||||
resolved "https://registry.yarnpkg.com/cross-spawn/-/cross-spawn-7.0.3.tgz#f73a85b9d5d41d045551c177e2882d4ac85728a6"
|
resolved "https://registry.yarnpkg.com/cross-spawn/-/cross-spawn-7.0.3.tgz#f73a85b9d5d41d045551c177e2882d4ac85728a6"
|
||||||
|
@ -2658,6 +2785,13 @@ default-value@^1.0.0:
|
||||||
dependencies:
|
dependencies:
|
||||||
es6-promise-try "0.0.1"
|
es6-promise-try "0.0.1"
|
||||||
|
|
||||||
|
defaults@^1.0.3:
|
||||||
|
version "1.0.4"
|
||||||
|
resolved "https://registry.yarnpkg.com/defaults/-/defaults-1.0.4.tgz#b0b02062c1e2aa62ff5d9528f0f98baa90978d7a"
|
||||||
|
integrity sha512-eFuaLoy/Rxalv2kr+lqMlUnrDWV+3j4pljOIJgLIhI058IQfWJ7vXhyEIHu+HtC738klGALYxOKDO0bQP3tg8A==
|
||||||
|
dependencies:
|
||||||
|
clone "^1.0.2"
|
||||||
|
|
||||||
define-data-property@^1.0.1:
|
define-data-property@^1.0.1:
|
||||||
version "1.1.0"
|
version "1.1.0"
|
||||||
resolved "https://registry.yarnpkg.com/define-data-property/-/define-data-property-1.1.0.tgz#0db13540704e1d8d479a0656cf781267531b9451"
|
resolved "https://registry.yarnpkg.com/define-data-property/-/define-data-property-1.1.0.tgz#0db13540704e1d8d479a0656cf781267531b9451"
|
||||||
|
@ -2732,6 +2866,11 @@ detective@^5.2.0:
|
||||||
defined "^1.0.0"
|
defined "^1.0.0"
|
||||||
minimist "^1.2.6"
|
minimist "^1.2.6"
|
||||||
|
|
||||||
|
diff@^4.0.1:
|
||||||
|
version "4.0.2"
|
||||||
|
resolved "https://registry.yarnpkg.com/diff/-/diff-4.0.2.tgz#60f3aecb89d5fae520c11aa19efc2bb982aade7d"
|
||||||
|
integrity sha512-58lmxKSA4BNyLz+HHMUzlOEpg09FV+ev6ZMe3vJihgdxzgcwZ8VoEEPmALCZG9LmqfVoNMMKpttIYTVG6uDY7A==
|
||||||
|
|
||||||
diffie-hellman@^5.0.0:
|
diffie-hellman@^5.0.0:
|
||||||
version "5.0.3"
|
version "5.0.3"
|
||||||
resolved "https://registry.yarnpkg.com/diffie-hellman/-/diffie-hellman-5.0.3.tgz#40e8ee98f55a2149607146921c63e1ae5f3d2875"
|
resolved "https://registry.yarnpkg.com/diffie-hellman/-/diffie-hellman-5.0.3.tgz#40e8ee98f55a2149607146921c63e1ae5f3d2875"
|
||||||
|
@ -2767,6 +2906,11 @@ domain-browser@^1.2.0:
|
||||||
resolved "https://registry.yarnpkg.com/domain-browser/-/domain-browser-1.2.0.tgz#3d31f50191a6749dd1375a7f522e823d42e54eda"
|
resolved "https://registry.yarnpkg.com/domain-browser/-/domain-browser-1.2.0.tgz#3d31f50191a6749dd1375a7f522e823d42e54eda"
|
||||||
integrity sha512-jnjyiM6eRyZl2H+W8Q/zLMA481hzi0eszAaBUzIVnmYVDBbnLxVNnfu1HgEBvCbL+71FrxMl3E6lpKH7Ge3OXA==
|
integrity sha512-jnjyiM6eRyZl2H+W8Q/zLMA481hzi0eszAaBUzIVnmYVDBbnLxVNnfu1HgEBvCbL+71FrxMl3E6lpKH7Ge3OXA==
|
||||||
|
|
||||||
|
drange@^1.0.2:
|
||||||
|
version "1.1.1"
|
||||||
|
resolved "https://registry.yarnpkg.com/drange/-/drange-1.1.1.tgz#b2aecec2aab82fcef11dbbd7b9e32b83f8f6c0b8"
|
||||||
|
integrity sha512-pYxfDYpued//QpnLIm4Avk7rsNtAtQkUES2cwAYSvD/wd2pKD71gN2Ebj3e7klzXwjocvE8c5vx/1fxwpqmSxA==
|
||||||
|
|
||||||
duplexer2@^0.1.2, duplexer2@~0.1.0, duplexer2@~0.1.2, duplexer2@~0.1.4:
|
duplexer2@^0.1.2, duplexer2@~0.1.0, duplexer2@~0.1.2, duplexer2@~0.1.4:
|
||||||
version "0.1.4"
|
version "0.1.4"
|
||||||
resolved "https://registry.yarnpkg.com/duplexer2/-/duplexer2-0.1.4.tgz#8b12dab878c0d69e3e7891051662a32fc6bddcc1"
|
resolved "https://registry.yarnpkg.com/duplexer2/-/duplexer2-0.1.4.tgz#8b12dab878c0d69e3e7891051662a32fc6bddcc1"
|
||||||
|
@ -2802,6 +2946,11 @@ elliptic@^6.5.3:
|
||||||
minimalistic-assert "^1.0.1"
|
minimalistic-assert "^1.0.1"
|
||||||
minimalistic-crypto-utils "^1.0.1"
|
minimalistic-crypto-utils "^1.0.1"
|
||||||
|
|
||||||
|
emoji-regex@^8.0.0:
|
||||||
|
version "8.0.0"
|
||||||
|
resolved "https://registry.yarnpkg.com/emoji-regex/-/emoji-regex-8.0.0.tgz#e818fd69ce5ccfcb404594f842963bf53164cc37"
|
||||||
|
integrity sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==
|
||||||
|
|
||||||
emojis-list@^3.0.0:
|
emojis-list@^3.0.0:
|
||||||
version "3.0.0"
|
version "3.0.0"
|
||||||
resolved "https://registry.yarnpkg.com/emojis-list/-/emojis-list-3.0.0.tgz#5570662046ad29e2e916e71aae260abdff4f6a78"
|
resolved "https://registry.yarnpkg.com/emojis-list/-/emojis-list-3.0.0.tgz#5570662046ad29e2e916e71aae260abdff4f6a78"
|
||||||
|
@ -3231,6 +3380,15 @@ ext@^1.1.2:
|
||||||
dependencies:
|
dependencies:
|
||||||
type "^2.7.2"
|
type "^2.7.2"
|
||||||
|
|
||||||
|
external-editor@^3.0.3:
|
||||||
|
version "3.1.0"
|
||||||
|
resolved "https://registry.yarnpkg.com/external-editor/-/external-editor-3.1.0.tgz#cb03f740befae03ea4d283caed2741a83f335495"
|
||||||
|
integrity sha512-hMQ4CX1p1izmuLYyZqLMO/qGNw10wSv9QDCPfzXfyFrOaCSSoRfqE1Kf1s5an66J5JZC62NewG+mK49jOCtQew==
|
||||||
|
dependencies:
|
||||||
|
chardet "^0.7.0"
|
||||||
|
iconv-lite "^0.4.24"
|
||||||
|
tmp "^0.0.33"
|
||||||
|
|
||||||
factor-bundle@^2.5.0:
|
factor-bundle@^2.5.0:
|
||||||
version "2.5.0"
|
version "2.5.0"
|
||||||
resolved "https://registry.yarnpkg.com/factor-bundle/-/factor-bundle-2.5.0.tgz#8ea8957da39d7586283cc3ee353cd9911a45e779"
|
resolved "https://registry.yarnpkg.com/factor-bundle/-/factor-bundle-2.5.0.tgz#8ea8957da39d7586283cc3ee353cd9911a45e779"
|
||||||
|
@ -3296,6 +3454,13 @@ faye-websocket@^0.11.3:
|
||||||
dependencies:
|
dependencies:
|
||||||
websocket-driver ">=0.5.1"
|
websocket-driver ">=0.5.1"
|
||||||
|
|
||||||
|
figures@^3.0.0:
|
||||||
|
version "3.2.0"
|
||||||
|
resolved "https://registry.yarnpkg.com/figures/-/figures-3.2.0.tgz#625c18bd293c604dc4a8ddb2febf0c88341746af"
|
||||||
|
integrity sha512-yaduQFRKLXYOGgEn6AZau90j3ggSOyiqXU0F9JZfeXYhNa+Jk4X+s45A2zg5jns87GAFa34BBm2kXw4XpNcbdg==
|
||||||
|
dependencies:
|
||||||
|
escape-string-regexp "^1.0.5"
|
||||||
|
|
||||||
file-entry-cache@^6.0.1:
|
file-entry-cache@^6.0.1:
|
||||||
version "6.0.1"
|
version "6.0.1"
|
||||||
resolved "https://registry.yarnpkg.com/file-entry-cache/-/file-entry-cache-6.0.1.tgz#211b2dd9659cb0394b073e7323ac3c933d522027"
|
resolved "https://registry.yarnpkg.com/file-entry-cache/-/file-entry-cache-6.0.1.tgz#211b2dd9659cb0394b073e7323ac3c933d522027"
|
||||||
|
@ -3503,6 +3668,15 @@ glob@^7.1.0, glob@^7.1.3:
|
||||||
once "^1.3.0"
|
once "^1.3.0"
|
||||||
path-is-absolute "^1.0.0"
|
path-is-absolute "^1.0.0"
|
||||||
|
|
||||||
|
global-prefix@^3.0.0:
|
||||||
|
version "3.0.0"
|
||||||
|
resolved "https://registry.yarnpkg.com/global-prefix/-/global-prefix-3.0.0.tgz#fc85f73064df69f50421f47f883fe5b913ba9b97"
|
||||||
|
integrity sha512-awConJSVCHVGND6x3tmMaKcQvwXLhjdkmomy2W+Goaui8YPgYgXJZewhg3fWC+DlfqqQuWg8AwqjGTD2nAPVWg==
|
||||||
|
dependencies:
|
||||||
|
ini "^1.3.5"
|
||||||
|
kind-of "^6.0.2"
|
||||||
|
which "^1.3.1"
|
||||||
|
|
||||||
globals@^11.1.0:
|
globals@^11.1.0:
|
||||||
version "11.12.0"
|
version "11.12.0"
|
||||||
resolved "https://registry.yarnpkg.com/globals/-/globals-11.12.0.tgz#ab8795338868a0babd8525758018c2a7eb95c42e"
|
resolved "https://registry.yarnpkg.com/globals/-/globals-11.12.0.tgz#ab8795338868a0babd8525758018c2a7eb95c42e"
|
||||||
|
@ -3566,6 +3740,11 @@ has-flag@^4.0.0:
|
||||||
resolved "https://registry.yarnpkg.com/has-flag/-/has-flag-4.0.0.tgz#944771fd9c81c81265c4d6941860da06bb59479b"
|
resolved "https://registry.yarnpkg.com/has-flag/-/has-flag-4.0.0.tgz#944771fd9c81c81265c4d6941860da06bb59479b"
|
||||||
integrity sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==
|
integrity sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==
|
||||||
|
|
||||||
|
has-own-prop@^2.0.0:
|
||||||
|
version "2.0.0"
|
||||||
|
resolved "https://registry.yarnpkg.com/has-own-prop/-/has-own-prop-2.0.0.tgz#f0f95d58f65804f5d218db32563bb85b8e0417af"
|
||||||
|
integrity sha512-Pq0h+hvsVm6dDEa8x82GnLSYHOzNDt7f0ddFa3FqcQlgzEiptPqL+XrOJNavjOzSYiYWIrgeVYYgGlLmnxwilQ==
|
||||||
|
|
||||||
has-property-descriptors@^1.0.0:
|
has-property-descriptors@^1.0.0:
|
||||||
version "1.0.0"
|
version "1.0.0"
|
||||||
resolved "https://registry.yarnpkg.com/has-property-descriptors/-/has-property-descriptors-1.0.0.tgz#610708600606d36961ed04c196193b6a607fa861"
|
resolved "https://registry.yarnpkg.com/has-property-descriptors/-/has-property-descriptors-1.0.0.tgz#610708600606d36961ed04c196193b6a607fa861"
|
||||||
|
@ -3674,7 +3853,7 @@ https-browserify@^1.0.0:
|
||||||
resolved "https://registry.yarnpkg.com/https-browserify/-/https-browserify-1.0.0.tgz#ec06c10e0a34c0f2faf199f7fd7fc78fffd03c73"
|
resolved "https://registry.yarnpkg.com/https-browserify/-/https-browserify-1.0.0.tgz#ec06c10e0a34c0f2faf199f7fd7fc78fffd03c73"
|
||||||
integrity sha512-J+FkSdyD+0mA0N+81tMotaRMfSL9SGi+xpD3T6YApKsc3bGSXJlfXri3VyFOeYkfLRQisDk1W+jIFFKBeUBbBg==
|
integrity sha512-J+FkSdyD+0mA0N+81tMotaRMfSL9SGi+xpD3T6YApKsc3bGSXJlfXri3VyFOeYkfLRQisDk1W+jIFFKBeUBbBg==
|
||||||
|
|
||||||
iconv-lite@0.4.24:
|
iconv-lite@0.4.24, iconv-lite@^0.4.24:
|
||||||
version "0.4.24"
|
version "0.4.24"
|
||||||
resolved "https://registry.yarnpkg.com/iconv-lite/-/iconv-lite-0.4.24.tgz#2022b4b25fbddc21d2f524974a474aafe733908b"
|
resolved "https://registry.yarnpkg.com/iconv-lite/-/iconv-lite-0.4.24.tgz#2022b4b25fbddc21d2f524974a474aafe733908b"
|
||||||
integrity sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA==
|
integrity sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA==
|
||||||
|
@ -3779,6 +3958,11 @@ inherits@2.0.3:
|
||||||
resolved "https://registry.yarnpkg.com/inherits/-/inherits-2.0.3.tgz#633c2c83e3da42a502f52466022480f4208261de"
|
resolved "https://registry.yarnpkg.com/inherits/-/inherits-2.0.3.tgz#633c2c83e3da42a502f52466022480f4208261de"
|
||||||
integrity sha512-x00IRNXNy63jwGkJmzPigoySHbaqpNuzKbBOmzK+g2OdZpQ9w+sxCN+VSB3ja7IAge2OP2qpfxTjeNcyjmW1uw==
|
integrity sha512-x00IRNXNy63jwGkJmzPigoySHbaqpNuzKbBOmzK+g2OdZpQ9w+sxCN+VSB3ja7IAge2OP2qpfxTjeNcyjmW1uw==
|
||||||
|
|
||||||
|
ini@^1.3.5:
|
||||||
|
version "1.3.8"
|
||||||
|
resolved "https://registry.yarnpkg.com/ini/-/ini-1.3.8.tgz#a29da425b48806f34767a4efce397269af28432c"
|
||||||
|
integrity sha512-JV/yugV2uzW5iMRSiZAyDtQd+nxtUnjeLt0acNdw98kKLrvuRVyB80tsREOE7yvGVgalhZ6RNXCmEHkUKBKxew==
|
||||||
|
|
||||||
inject-lr-script@^2.2.0:
|
inject-lr-script@^2.2.0:
|
||||||
version "2.2.0"
|
version "2.2.0"
|
||||||
resolved "https://registry.yarnpkg.com/inject-lr-script/-/inject-lr-script-2.2.0.tgz#58d91cd99e5de1a3f172aa076f7db8651ee72db2"
|
resolved "https://registry.yarnpkg.com/inject-lr-script/-/inject-lr-script-2.2.0.tgz#58d91cd99e5de1a3f172aa076f7db8651ee72db2"
|
||||||
|
@ -3800,6 +3984,27 @@ inline-source-map@~0.6.0:
|
||||||
dependencies:
|
dependencies:
|
||||||
source-map "~0.5.3"
|
source-map "~0.5.3"
|
||||||
|
|
||||||
|
inquirer@^8.2.5:
|
||||||
|
version "8.2.6"
|
||||||
|
resolved "https://registry.yarnpkg.com/inquirer/-/inquirer-8.2.6.tgz#733b74888195d8d400a67ac332011b5fae5ea562"
|
||||||
|
integrity sha512-M1WuAmb7pn9zdFRtQYk26ZBoY043Sse0wVDdk4Bppr+JOXyQYybdtvK+l9wUibhtjdjvtoiNy8tk+EgsYIUqKg==
|
||||||
|
dependencies:
|
||||||
|
ansi-escapes "^4.2.1"
|
||||||
|
chalk "^4.1.1"
|
||||||
|
cli-cursor "^3.1.0"
|
||||||
|
cli-width "^3.0.0"
|
||||||
|
external-editor "^3.0.3"
|
||||||
|
figures "^3.0.0"
|
||||||
|
lodash "^4.17.21"
|
||||||
|
mute-stream "0.0.8"
|
||||||
|
ora "^5.4.1"
|
||||||
|
run-async "^2.4.0"
|
||||||
|
rxjs "^7.5.5"
|
||||||
|
string-width "^4.1.0"
|
||||||
|
strip-ansi "^6.0.0"
|
||||||
|
through "^2.3.6"
|
||||||
|
wrap-ansi "^6.0.1"
|
||||||
|
|
||||||
insert-css@^2.0.0:
|
insert-css@^2.0.0:
|
||||||
version "2.0.0"
|
version "2.0.0"
|
||||||
resolved "https://registry.yarnpkg.com/insert-css/-/insert-css-2.0.0.tgz#eb5d1097b7542f4c79ea3060d3aee07d053880f4"
|
resolved "https://registry.yarnpkg.com/insert-css/-/insert-css-2.0.0.tgz#eb5d1097b7542f4c79ea3060d3aee07d053880f4"
|
||||||
|
@ -3922,6 +4127,11 @@ is-finalizationregistry@^1.0.2:
|
||||||
dependencies:
|
dependencies:
|
||||||
call-bind "^1.0.2"
|
call-bind "^1.0.2"
|
||||||
|
|
||||||
|
is-fullwidth-code-point@^3.0.0:
|
||||||
|
version "3.0.0"
|
||||||
|
resolved "https://registry.yarnpkg.com/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz#f116f8064fe90b3f7844a38997c0b75051269f1d"
|
||||||
|
integrity sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==
|
||||||
|
|
||||||
is-generator-function@^1.0.10, is-generator-function@^1.0.7:
|
is-generator-function@^1.0.10, is-generator-function@^1.0.7:
|
||||||
version "1.0.10"
|
version "1.0.10"
|
||||||
resolved "https://registry.yarnpkg.com/is-generator-function/-/is-generator-function-1.0.10.tgz#f1558baf1ac17e0deea7c0415c438351ff2b3c72"
|
resolved "https://registry.yarnpkg.com/is-generator-function/-/is-generator-function-1.0.10.tgz#f1558baf1ac17e0deea7c0415c438351ff2b3c72"
|
||||||
|
@ -3936,6 +4146,11 @@ is-glob@^4.0.0, is-glob@^4.0.1, is-glob@^4.0.3, is-glob@~4.0.1:
|
||||||
dependencies:
|
dependencies:
|
||||||
is-extglob "^2.1.1"
|
is-extglob "^2.1.1"
|
||||||
|
|
||||||
|
is-interactive@^1.0.0:
|
||||||
|
version "1.0.0"
|
||||||
|
resolved "https://registry.yarnpkg.com/is-interactive/-/is-interactive-1.0.0.tgz#cea6e6ae5c870a7b0a0004070b7b587e0252912e"
|
||||||
|
integrity sha512-2HvIEKRoqS62guEC+qBjpvRubdX910WCMuJTZ+I9yvqKU2/12eSL549HMwtabb4oupdj2sMP50k+XJfB/8JE6w==
|
||||||
|
|
||||||
is-map@^2.0.1:
|
is-map@^2.0.1:
|
||||||
version "2.0.2"
|
version "2.0.2"
|
||||||
resolved "https://registry.yarnpkg.com/is-map/-/is-map-2.0.2.tgz#00922db8c9bf73e81b7a335827bc2a43f2b91127"
|
resolved "https://registry.yarnpkg.com/is-map/-/is-map-2.0.2.tgz#00922db8c9bf73e81b7a335827bc2a43f2b91127"
|
||||||
|
@ -4024,6 +4239,11 @@ is-typed-array@^1.1.10, is-typed-array@^1.1.12, is-typed-array@^1.1.3, is-typed-
|
||||||
dependencies:
|
dependencies:
|
||||||
which-typed-array "^1.1.11"
|
which-typed-array "^1.1.11"
|
||||||
|
|
||||||
|
is-unicode-supported@^0.1.0:
|
||||||
|
version "0.1.0"
|
||||||
|
resolved "https://registry.yarnpkg.com/is-unicode-supported/-/is-unicode-supported-0.1.0.tgz#3f26c76a809593b52bfa2ecb5710ed2779b522a7"
|
||||||
|
integrity sha512-knxG2q4UC3u8stRGyAVJCOdxFmv5DZiRcdlIaAQXAbSfJya+OhopNotLQrstBhququ4ZpuKbDc/8S6mgXgPFPw==
|
||||||
|
|
||||||
is-utf8@^0.2.0:
|
is-utf8@^0.2.0:
|
||||||
version "0.2.1"
|
version "0.2.1"
|
||||||
resolved "https://registry.yarnpkg.com/is-utf8/-/is-utf8-0.2.1.tgz#4b0da1442104d1b336340e80797e865cf39f7d72"
|
resolved "https://registry.yarnpkg.com/is-utf8/-/is-utf8-0.2.1.tgz#4b0da1442104d1b336340e80797e865cf39f7d72"
|
||||||
|
@ -4168,6 +4388,11 @@ keyv@^4.5.3:
|
||||||
dependencies:
|
dependencies:
|
||||||
json-buffer "3.0.1"
|
json-buffer "3.0.1"
|
||||||
|
|
||||||
|
kind-of@^6.0.2:
|
||||||
|
version "6.0.3"
|
||||||
|
resolved "https://registry.yarnpkg.com/kind-of/-/kind-of-6.0.3.tgz#07c05034a6c349fa06e24fa35aa76db4580ce4dd"
|
||||||
|
integrity sha512-dcS1ul+9tmeD95T+x28/ehLgd9mENa3LsvDTtzm3vyBEO7RPptvAD+t44WVXaUjTBRcrpFeFlC8WCruUR456hw==
|
||||||
|
|
||||||
labeled-stream-splicer@^1.0.0:
|
labeled-stream-splicer@^1.0.0:
|
||||||
version "1.0.2"
|
version "1.0.2"
|
||||||
resolved "https://registry.yarnpkg.com/labeled-stream-splicer/-/labeled-stream-splicer-1.0.2.tgz#4615331537784981e8fd264e1f3a434c4e0ddd65"
|
resolved "https://registry.yarnpkg.com/labeled-stream-splicer/-/labeled-stream-splicer-1.0.2.tgz#4615331537784981e8fd264e1f3a434c4e0ddd65"
|
||||||
|
@ -4252,6 +4477,19 @@ lodash.merge@^4.6.2:
|
||||||
resolved "https://registry.yarnpkg.com/lodash.merge/-/lodash.merge-4.6.2.tgz#558aa53b43b661e1925a0afdfa36a9a1085fe57a"
|
resolved "https://registry.yarnpkg.com/lodash.merge/-/lodash.merge-4.6.2.tgz#558aa53b43b661e1925a0afdfa36a9a1085fe57a"
|
||||||
integrity sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==
|
integrity sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==
|
||||||
|
|
||||||
|
lodash@^4.17.21:
|
||||||
|
version "4.17.21"
|
||||||
|
resolved "https://registry.yarnpkg.com/lodash/-/lodash-4.17.21.tgz#679591c564c3bffaae8454cf0b3df370c3d6911c"
|
||||||
|
integrity sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==
|
||||||
|
|
||||||
|
log-symbols@^4.1.0:
|
||||||
|
version "4.1.0"
|
||||||
|
resolved "https://registry.yarnpkg.com/log-symbols/-/log-symbols-4.1.0.tgz#3fbdbb95b4683ac9fc785111e792e558d4abd503"
|
||||||
|
integrity sha512-8XPvpAA8uyhfteu8pIvQxpJZ7SYYdpUivZpGy6sFsBuKRY/7rQGavedeB8aK+Zkyq6upMFVL/9AW6vOYzfRyLg==
|
||||||
|
dependencies:
|
||||||
|
chalk "^4.1.0"
|
||||||
|
is-unicode-supported "^0.1.0"
|
||||||
|
|
||||||
loose-envify@^1.1.0, loose-envify@^1.4.0:
|
loose-envify@^1.1.0, loose-envify@^1.4.0:
|
||||||
version "1.4.0"
|
version "1.4.0"
|
||||||
resolved "https://registry.yarnpkg.com/loose-envify/-/loose-envify-1.4.0.tgz#71ee51fa7be4caec1a63839f7e682d8132d30caf"
|
resolved "https://registry.yarnpkg.com/loose-envify/-/loose-envify-1.4.0.tgz#71ee51fa7be4caec1a63839f7e682d8132d30caf"
|
||||||
|
@ -4280,6 +4518,11 @@ magic-string@0.25.1:
|
||||||
dependencies:
|
dependencies:
|
||||||
sourcemap-codec "^1.4.1"
|
sourcemap-codec "^1.4.1"
|
||||||
|
|
||||||
|
make-error@^1.1.1:
|
||||||
|
version "1.3.6"
|
||||||
|
resolved "https://registry.yarnpkg.com/make-error/-/make-error-1.3.6.tgz#2eb2e37ea9b67c4891f684a1394799af484cf7a2"
|
||||||
|
integrity sha512-s8UhlNe7vPKomQhC1qFelMokr/Sc3AgNbso3n74mVPA5LTZwkB9NlXf4XPamLxJE8h0gh73rM94xvwRT2CVInw==
|
||||||
|
|
||||||
map-obj@^4.1.0:
|
map-obj@^4.1.0:
|
||||||
version "4.3.0"
|
version "4.3.0"
|
||||||
resolved "https://registry.yarnpkg.com/map-obj/-/map-obj-4.3.0.tgz#9304f906e93faae70880da102a9f1df0ea8bb05a"
|
resolved "https://registry.yarnpkg.com/map-obj/-/map-obj-4.3.0.tgz#9304f906e93faae70880da102a9f1df0ea8bb05a"
|
||||||
|
@ -4369,6 +4612,11 @@ mime@1.6.0:
|
||||||
resolved "https://registry.yarnpkg.com/mime/-/mime-1.6.0.tgz#32cd9e5c64553bd58d19a568af452acff04981b1"
|
resolved "https://registry.yarnpkg.com/mime/-/mime-1.6.0.tgz#32cd9e5c64553bd58d19a568af452acff04981b1"
|
||||||
integrity sha512-x0Vn8spI+wuJ1O6S7gnbaQg8Pxh4NNHb7KSINmEWKiPE4RKOplvijn+NkmYmmRgP68mc70j2EbeTFRsrswaQeg==
|
integrity sha512-x0Vn8spI+wuJ1O6S7gnbaQg8Pxh4NNHb7KSINmEWKiPE4RKOplvijn+NkmYmmRgP68mc70j2EbeTFRsrswaQeg==
|
||||||
|
|
||||||
|
mimic-fn@^2.1.0:
|
||||||
|
version "2.1.0"
|
||||||
|
resolved "https://registry.yarnpkg.com/mimic-fn/-/mimic-fn-2.1.0.tgz#7ed2c2ccccaf84d3ffcb7a69b57711fc2083401b"
|
||||||
|
integrity sha512-OqbOk5oEQeAZ8WXWydlu9HJjz9WVdEIvamMCcXmuqUYjTknH/sqsWvhQ3vgwKFRR1HpjvNBKQ37nbJgYzGqGcg==
|
||||||
|
|
||||||
minimalistic-assert@^1.0.0, minimalistic-assert@^1.0.1:
|
minimalistic-assert@^1.0.0, minimalistic-assert@^1.0.1:
|
||||||
version "1.0.1"
|
version "1.0.1"
|
||||||
resolved "https://registry.yarnpkg.com/minimalistic-assert/-/minimalistic-assert-1.0.1.tgz#2e194de044626d4a10e7f7fbc00ce73e83e4d5c7"
|
resolved "https://registry.yarnpkg.com/minimalistic-assert/-/minimalistic-assert-1.0.1.tgz#2e194de044626d4a10e7f7fbc00ce73e83e4d5c7"
|
||||||
|
@ -4391,7 +4639,7 @@ minimist@0.0.5:
|
||||||
resolved "https://registry.yarnpkg.com/minimist/-/minimist-0.0.5.tgz#d7aa327bcecf518f9106ac6b8f003fa3bcea8566"
|
resolved "https://registry.yarnpkg.com/minimist/-/minimist-0.0.5.tgz#d7aa327bcecf518f9106ac6b8f003fa3bcea8566"
|
||||||
integrity sha512-rSJ0cdmCj3qmKdObcnMcWgPVOyaOWlazLhZAJW0s6G6lx1ZEuFkraWmEH5LTvX90btkfHPclQBjvjU7A/kYRFg==
|
integrity sha512-rSJ0cdmCj3qmKdObcnMcWgPVOyaOWlazLhZAJW0s6G6lx1ZEuFkraWmEH5LTvX90btkfHPclQBjvjU7A/kYRFg==
|
||||||
|
|
||||||
minimist@^1.1.0, minimist@^1.2.0, minimist@^1.2.6:
|
minimist@^1.1.0, minimist@^1.2.0, minimist@^1.2.6, minimist@^1.2.8:
|
||||||
version "1.2.8"
|
version "1.2.8"
|
||||||
resolved "https://registry.yarnpkg.com/minimist/-/minimist-1.2.8.tgz#c1a464e7693302e082a075cee0c057741ac4772c"
|
resolved "https://registry.yarnpkg.com/minimist/-/minimist-1.2.8.tgz#c1a464e7693302e082a075cee0c057741ac4772c"
|
||||||
integrity sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==
|
integrity sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==
|
||||||
|
@ -4452,6 +4700,11 @@ multisplice@^1.0.0:
|
||||||
resolved "https://registry.yarnpkg.com/multisplice/-/multisplice-1.0.0.tgz#e74cf2948dcb51a6c317fc5e22980a652f7830e9"
|
resolved "https://registry.yarnpkg.com/multisplice/-/multisplice-1.0.0.tgz#e74cf2948dcb51a6c317fc5e22980a652f7830e9"
|
||||||
integrity sha512-KU5tVjIdTGsMb92JlWwEZCGrvtI1ku9G9GuNbWdQT/Ici1ztFXX0L8lWpbbC3pISVMfBNL56wdqplHvva2XSlA==
|
integrity sha512-KU5tVjIdTGsMb92JlWwEZCGrvtI1ku9G9GuNbWdQT/Ici1ztFXX0L8lWpbbC3pISVMfBNL56wdqplHvva2XSlA==
|
||||||
|
|
||||||
|
mute-stream@0.0.8:
|
||||||
|
version "0.0.8"
|
||||||
|
resolved "https://registry.yarnpkg.com/mute-stream/-/mute-stream-0.0.8.tgz#1630c42b2251ff81e2a283de96a5497ea92e5e0d"
|
||||||
|
integrity sha512-nnbWWOkoWyUsTjKrhgD0dcz22mdkSnpYqbEjIm2nhwhuxlSkpywJmBo8h0ZqJdkp73mb90SssHkN4rsRaBAfAA==
|
||||||
|
|
||||||
nanoid@^3.3.6:
|
nanoid@^3.3.6:
|
||||||
version "3.3.6"
|
version "3.3.6"
|
||||||
resolved "https://registry.yarnpkg.com/nanoid/-/nanoid-3.3.6.tgz#443380c856d6e9f9824267d960b4236ad583ea4c"
|
resolved "https://registry.yarnpkg.com/nanoid/-/nanoid-3.3.6.tgz#443380c856d6e9f9824267d960b4236ad583ea4c"
|
||||||
|
@ -4576,6 +4829,13 @@ once@^1.3.0:
|
||||||
dependencies:
|
dependencies:
|
||||||
wrappy "1"
|
wrappy "1"
|
||||||
|
|
||||||
|
onetime@^5.1.0:
|
||||||
|
version "5.1.2"
|
||||||
|
resolved "https://registry.yarnpkg.com/onetime/-/onetime-5.1.2.tgz#d0e96ebb56b07476df1dd9c4806e5237985ca45e"
|
||||||
|
integrity sha512-kbpaSSGJTWdAY5KPVeMOKXSrPtr8C8C7wodJbcsd51jRnmD+GZu8Y0VoU6Dm5Z4vWr0Ig/1NKuWRKf7j5aaYSg==
|
||||||
|
dependencies:
|
||||||
|
mimic-fn "^2.1.0"
|
||||||
|
|
||||||
optionator@^0.8.1:
|
optionator@^0.8.1:
|
||||||
version "0.8.3"
|
version "0.8.3"
|
||||||
resolved "https://registry.yarnpkg.com/optionator/-/optionator-0.8.3.tgz#84fa1d036fe9d3c7e21d99884b601167ec8fb495"
|
resolved "https://registry.yarnpkg.com/optionator/-/optionator-0.8.3.tgz#84fa1d036fe9d3c7e21d99884b601167ec8fb495"
|
||||||
|
@ -4600,11 +4860,31 @@ optionator@^0.9.3:
|
||||||
prelude-ls "^1.2.1"
|
prelude-ls "^1.2.1"
|
||||||
type-check "^0.4.0"
|
type-check "^0.4.0"
|
||||||
|
|
||||||
|
ora@^5.4.1:
|
||||||
|
version "5.4.1"
|
||||||
|
resolved "https://registry.yarnpkg.com/ora/-/ora-5.4.1.tgz#1b2678426af4ac4a509008e5e4ac9e9959db9e18"
|
||||||
|
integrity sha512-5b6Y85tPxZZ7QytO+BQzysW31HJku27cRIlkbAXaNx+BdcVi+LlRFmVXzeF6a7JCwJpyw5c4b+YSVImQIrBpuQ==
|
||||||
|
dependencies:
|
||||||
|
bl "^4.1.0"
|
||||||
|
chalk "^4.1.0"
|
||||||
|
cli-cursor "^3.1.0"
|
||||||
|
cli-spinners "^2.5.0"
|
||||||
|
is-interactive "^1.0.0"
|
||||||
|
is-unicode-supported "^0.1.0"
|
||||||
|
log-symbols "^4.1.0"
|
||||||
|
strip-ansi "^6.0.0"
|
||||||
|
wcwidth "^1.0.1"
|
||||||
|
|
||||||
os-browserify@~0.3.0:
|
os-browserify@~0.3.0:
|
||||||
version "0.3.0"
|
version "0.3.0"
|
||||||
resolved "https://registry.yarnpkg.com/os-browserify/-/os-browserify-0.3.0.tgz#854373c7f5c2315914fc9bfc6bd8238fdda1ec27"
|
resolved "https://registry.yarnpkg.com/os-browserify/-/os-browserify-0.3.0.tgz#854373c7f5c2315914fc9bfc6bd8238fdda1ec27"
|
||||||
integrity sha512-gjcpUc3clBf9+210TRaDWbf+rZZZEshZ+DlXMRCeAjp0xhTrnQsKHypIy1J3d5hKdUzj69t708EHtU8P6bUn0A==
|
integrity sha512-gjcpUc3clBf9+210TRaDWbf+rZZZEshZ+DlXMRCeAjp0xhTrnQsKHypIy1J3d5hKdUzj69t708EHtU8P6bUn0A==
|
||||||
|
|
||||||
|
os-tmpdir@~1.0.2:
|
||||||
|
version "1.0.2"
|
||||||
|
resolved "https://registry.yarnpkg.com/os-tmpdir/-/os-tmpdir-1.0.2.tgz#bbe67406c79aa85c5cfec766fe5734555dfa1274"
|
||||||
|
integrity sha512-D2FR03Vir7FIu45XBY20mTb+/ZSWB00sjU9jdQXt83gDrI4Ztz5Fs7/yy74g2N5SVQY4xY1qDr4rNddwYRVX0g==
|
||||||
|
|
||||||
outpipe@^1.1.0:
|
outpipe@^1.1.0:
|
||||||
version "1.1.1"
|
version "1.1.1"
|
||||||
resolved "https://registry.yarnpkg.com/outpipe/-/outpipe-1.1.1.tgz#50cf8616365e87e031e29a5ec9339a3da4725fa2"
|
resolved "https://registry.yarnpkg.com/outpipe/-/outpipe-1.1.1.tgz#50cf8616365e87e031e29a5ec9339a3da4725fa2"
|
||||||
|
@ -4936,6 +5216,14 @@ queue-microtask@^1.2.2:
|
||||||
resolved "https://registry.yarnpkg.com/queue-microtask/-/queue-microtask-1.2.3.tgz#4929228bbc724dfac43e0efb058caf7b6cfb6243"
|
resolved "https://registry.yarnpkg.com/queue-microtask/-/queue-microtask-1.2.3.tgz#4929228bbc724dfac43e0efb058caf7b6cfb6243"
|
||||||
integrity sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==
|
integrity sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==
|
||||||
|
|
||||||
|
randexp@^0.5.3:
|
||||||
|
version "0.5.3"
|
||||||
|
resolved "https://registry.yarnpkg.com/randexp/-/randexp-0.5.3.tgz#f31c2de3148b30bdeb84b7c3f59b0ebb9fec3738"
|
||||||
|
integrity sha512-U+5l2KrcMNOUPYvazA3h5ekF80FHTUG+87SEAmHZmolh1M+i/WyTCxVzmi+tidIa1tM4BSe8g2Y/D3loWDjj+w==
|
||||||
|
dependencies:
|
||||||
|
drange "^1.0.2"
|
||||||
|
ret "^0.2.0"
|
||||||
|
|
||||||
randombytes@^2.0.0, randombytes@^2.0.1, randombytes@^2.0.5:
|
randombytes@^2.0.0, randombytes@^2.0.1, randombytes@^2.0.5:
|
||||||
version "2.1.0"
|
version "2.1.0"
|
||||||
resolved "https://registry.yarnpkg.com/randombytes/-/randombytes-2.1.0.tgz#df6f84372f0270dc65cdf6291349ab7a473d4f2a"
|
resolved "https://registry.yarnpkg.com/randombytes/-/randombytes-2.1.0.tgz#df6f84372f0270dc65cdf6291349ab7a473d4f2a"
|
||||||
|
@ -5172,6 +5460,11 @@ remove-accents@0.4.2:
|
||||||
resolved "https://registry.yarnpkg.com/remove-accents/-/remove-accents-0.4.2.tgz#0a43d3aaae1e80db919e07ae254b285d9e1c7bb5"
|
resolved "https://registry.yarnpkg.com/remove-accents/-/remove-accents-0.4.2.tgz#0a43d3aaae1e80db919e07ae254b285d9e1c7bb5"
|
||||||
integrity sha512-7pXIJqJOq5tFgG1A2Zxti3Ht8jJF337m4sowbuHsW30ZnkQFnDzy9qBNhgzX8ZLW4+UBcXiiR7SwR6pokHsxiA==
|
integrity sha512-7pXIJqJOq5tFgG1A2Zxti3Ht8jJF337m4sowbuHsW30ZnkQFnDzy9qBNhgzX8ZLW4+UBcXiiR7SwR6pokHsxiA==
|
||||||
|
|
||||||
|
repeat-string@^1.6.1:
|
||||||
|
version "1.6.1"
|
||||||
|
resolved "https://registry.yarnpkg.com/repeat-string/-/repeat-string-1.6.1.tgz#8dcae470e1c88abc2d600fff4a776286da75e637"
|
||||||
|
integrity sha512-PV0dzCYDNfRi1jCDbJzpW7jNNDRuCOG/jI5ctQcGKt/clZD+YcPS3yIlWuTJMmESC8aevCFmWJy5wjAFgNqN6w==
|
||||||
|
|
||||||
requireindex@^1.2.0:
|
requireindex@^1.2.0:
|
||||||
version "1.2.0"
|
version "1.2.0"
|
||||||
resolved "https://registry.yarnpkg.com/requireindex/-/requireindex-1.2.0.tgz#3463cdb22ee151902635aa6c9535d4de9c2ef1ef"
|
resolved "https://registry.yarnpkg.com/requireindex/-/requireindex-1.2.0.tgz#3463cdb22ee151902635aa6c9535d4de9c2ef1ef"
|
||||||
|
@ -5192,7 +5485,7 @@ resolve-from@^4.0.0:
|
||||||
resolved "https://registry.yarnpkg.com/resolve-from/-/resolve-from-4.0.0.tgz#4abcd852ad32dd7baabfe9b40e00a36db5f392e6"
|
resolved "https://registry.yarnpkg.com/resolve-from/-/resolve-from-4.0.0.tgz#4abcd852ad32dd7baabfe9b40e00a36db5f392e6"
|
||||||
integrity sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==
|
integrity sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==
|
||||||
|
|
||||||
resolve@^1.1.4, resolve@^1.1.7, resolve@^1.14.2, resolve@^1.17.0, resolve@^1.4.0:
|
resolve@^1.1.4, resolve@^1.1.7, resolve@^1.14.2, resolve@^1.17.0, resolve@^1.22.2, resolve@^1.4.0:
|
||||||
version "1.22.6"
|
version "1.22.6"
|
||||||
resolved "https://registry.yarnpkg.com/resolve/-/resolve-1.22.6.tgz#dd209739eca3aef739c626fea1b4f3c506195362"
|
resolved "https://registry.yarnpkg.com/resolve/-/resolve-1.22.6.tgz#dd209739eca3aef739c626fea1b4f3c506195362"
|
||||||
integrity sha512-njhxM7mV12JfufShqGy3Rz8j11RPdLy4xi15UurGJeoHLfJpVXKdh3ueuOqbYUcDZnffr6X739JBo5LzyahEsw==
|
integrity sha512-njhxM7mV12JfufShqGy3Rz8j11RPdLy4xi15UurGJeoHLfJpVXKdh3ueuOqbYUcDZnffr6X739JBo5LzyahEsw==
|
||||||
|
@ -5218,6 +5511,19 @@ resp-modifier@^6.0.0:
|
||||||
debug "^2.2.0"
|
debug "^2.2.0"
|
||||||
minimatch "^3.0.2"
|
minimatch "^3.0.2"
|
||||||
|
|
||||||
|
restore-cursor@^3.1.0:
|
||||||
|
version "3.1.0"
|
||||||
|
resolved "https://registry.yarnpkg.com/restore-cursor/-/restore-cursor-3.1.0.tgz#39f67c54b3a7a58cea5236d95cf0034239631f7e"
|
||||||
|
integrity sha512-l+sSefzHpj5qimhFSE5a8nufZYAM3sBSVMAPtYkmC+4EH2anSGaEMXSD0izRQbu9nfyQ9y5JrVmp7E8oZrUjvA==
|
||||||
|
dependencies:
|
||||||
|
onetime "^5.1.0"
|
||||||
|
signal-exit "^3.0.2"
|
||||||
|
|
||||||
|
ret@^0.2.0:
|
||||||
|
version "0.2.2"
|
||||||
|
resolved "https://registry.yarnpkg.com/ret/-/ret-0.2.2.tgz#b6861782a1f4762dce43402a71eb7a283f44573c"
|
||||||
|
integrity sha512-M0b3YWQs7R3Z917WRQy1HHA7Ba7D8hvZg6UE5mLykJxQVE2ju0IXbGlaHPPlkY+WN7wFP+wUMXmBFA0aV6vYGQ==
|
||||||
|
|
||||||
reusify@^1.0.4:
|
reusify@^1.0.4:
|
||||||
version "1.0.4"
|
version "1.0.4"
|
||||||
resolved "https://registry.yarnpkg.com/reusify/-/reusify-1.0.4.tgz#90da382b1e126efc02146e90845a88db12925d76"
|
resolved "https://registry.yarnpkg.com/reusify/-/reusify-1.0.4.tgz#90da382b1e126efc02146e90845a88db12925d76"
|
||||||
|
@ -5243,6 +5549,11 @@ ripemd160@^2.0.0, ripemd160@^2.0.1:
|
||||||
hash-base "^3.0.0"
|
hash-base "^3.0.0"
|
||||||
inherits "^2.0.1"
|
inherits "^2.0.1"
|
||||||
|
|
||||||
|
run-async@^2.4.0:
|
||||||
|
version "2.4.1"
|
||||||
|
resolved "https://registry.yarnpkg.com/run-async/-/run-async-2.4.1.tgz#8440eccf99ea3e70bd409d49aab88e10c189a455"
|
||||||
|
integrity sha512-tvVnVv01b8c1RrA6Ep7JkStj85Guv/YrMcwqYQnwjsAS2cTmmPGBBjAjpCW7RrSodNSoE2/qg9O4bceNvUuDgQ==
|
||||||
|
|
||||||
run-parallel@^1.1.9:
|
run-parallel@^1.1.9:
|
||||||
version "1.2.0"
|
version "1.2.0"
|
||||||
resolved "https://registry.yarnpkg.com/run-parallel/-/run-parallel-1.2.0.tgz#66d1368da7bdf921eb9d95bd1a9229e7f21a43ee"
|
resolved "https://registry.yarnpkg.com/run-parallel/-/run-parallel-1.2.0.tgz#66d1368da7bdf921eb9d95bd1a9229e7f21a43ee"
|
||||||
|
@ -5250,6 +5561,13 @@ run-parallel@^1.1.9:
|
||||||
dependencies:
|
dependencies:
|
||||||
queue-microtask "^1.2.2"
|
queue-microtask "^1.2.2"
|
||||||
|
|
||||||
|
rxjs@^7.5.5:
|
||||||
|
version "7.8.1"
|
||||||
|
resolved "https://registry.yarnpkg.com/rxjs/-/rxjs-7.8.1.tgz#6f6f3d99ea8044291efd92e7c7fcf562c4057543"
|
||||||
|
integrity sha512-AA3TVj+0A2iuIoQkWEK/tqFjBq2j+6PO6Y0zJcvzLAFhEFIO3HL0vls9hWLncZbAAbK0mar7oZ4V079I/qPMxg==
|
||||||
|
dependencies:
|
||||||
|
tslib "^2.1.0"
|
||||||
|
|
||||||
safe-array-concat@^1.0.1:
|
safe-array-concat@^1.0.1:
|
||||||
version "1.0.1"
|
version "1.0.1"
|
||||||
resolved "https://registry.yarnpkg.com/safe-array-concat/-/safe-array-concat-1.0.1.tgz#91686a63ce3adbea14d61b14c99572a8ff84754c"
|
resolved "https://registry.yarnpkg.com/safe-array-concat/-/safe-array-concat-1.0.1.tgz#91686a63ce3adbea14d61b14c99572a8ff84754c"
|
||||||
|
@ -5314,7 +5632,7 @@ semver@^6.1.0, semver@^6.3.1:
|
||||||
resolved "https://registry.yarnpkg.com/semver/-/semver-6.3.1.tgz#556d2ef8689146e46dcea4bfdd095f3434dffcb4"
|
resolved "https://registry.yarnpkg.com/semver/-/semver-6.3.1.tgz#556d2ef8689146e46dcea4bfdd095f3434dffcb4"
|
||||||
integrity sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==
|
integrity sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==
|
||||||
|
|
||||||
semver@^7.3.4, semver@^7.5.4:
|
semver@^7.3.4, semver@^7.3.8, semver@^7.5.4:
|
||||||
version "7.5.4"
|
version "7.5.4"
|
||||||
resolved "https://registry.yarnpkg.com/semver/-/semver-7.5.4.tgz#483986ec4ed38e1c6c48c34894a9182dbff68a6e"
|
resolved "https://registry.yarnpkg.com/semver/-/semver-7.5.4.tgz#483986ec4ed38e1c6c48c34894a9182dbff68a6e"
|
||||||
integrity sha512-1bCSESV6Pv+i21Hvpxp3Dx+pSD8lIPt8uVjRrxAUt/nbswYc+tK6Y2btiULjd4+fnq15PX+nqQDC7Oft7WkwcA==
|
integrity sha512-1bCSESV6Pv+i21Hvpxp3Dx+pSD8lIPt8uVjRrxAUt/nbswYc+tK6Y2btiULjd4+fnq15PX+nqQDC7Oft7WkwcA==
|
||||||
|
@ -5410,6 +5728,11 @@ side-channel@^1.0.4:
|
||||||
get-intrinsic "^1.0.2"
|
get-intrinsic "^1.0.2"
|
||||||
object-inspect "^1.9.0"
|
object-inspect "^1.9.0"
|
||||||
|
|
||||||
|
signal-exit@^3.0.2:
|
||||||
|
version "3.0.7"
|
||||||
|
resolved "https://registry.yarnpkg.com/signal-exit/-/signal-exit-3.0.7.tgz#a9a1767f8af84155114eaabd73f99273c8f59ad9"
|
||||||
|
integrity sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ==
|
||||||
|
|
||||||
simple-concat@^1.0.0:
|
simple-concat@^1.0.0:
|
||||||
version "1.0.1"
|
version "1.0.1"
|
||||||
resolved "https://registry.yarnpkg.com/simple-concat/-/simple-concat-1.0.1.tgz#f46976082ba35c2263f1c8ab5edfe26c41c9552f"
|
resolved "https://registry.yarnpkg.com/simple-concat/-/simple-concat-1.0.1.tgz#f46976082ba35c2263f1c8ab5edfe26c41c9552f"
|
||||||
|
@ -5614,6 +5937,15 @@ string-template@~0.2.1:
|
||||||
resolved "https://registry.yarnpkg.com/string-template/-/string-template-0.2.1.tgz#42932e598a352d01fc22ec3367d9d84eec6c9add"
|
resolved "https://registry.yarnpkg.com/string-template/-/string-template-0.2.1.tgz#42932e598a352d01fc22ec3367d9d84eec6c9add"
|
||||||
integrity sha512-Yptehjogou2xm4UJbxJ4CxgZx12HBfeystp0y3x7s4Dj32ltVVG1Gg8YhKjHZkHicuKpZX/ffilA8505VbUbpw==
|
integrity sha512-Yptehjogou2xm4UJbxJ4CxgZx12HBfeystp0y3x7s4Dj32ltVVG1Gg8YhKjHZkHicuKpZX/ffilA8505VbUbpw==
|
||||||
|
|
||||||
|
string-width@^4.1.0:
|
||||||
|
version "4.2.3"
|
||||||
|
resolved "https://registry.yarnpkg.com/string-width/-/string-width-4.2.3.tgz#269c7117d27b05ad2e536830a8ec895ef9c6d010"
|
||||||
|
integrity sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==
|
||||||
|
dependencies:
|
||||||
|
emoji-regex "^8.0.0"
|
||||||
|
is-fullwidth-code-point "^3.0.0"
|
||||||
|
strip-ansi "^6.0.1"
|
||||||
|
|
||||||
string.prototype.matchall@^4.0.8:
|
string.prototype.matchall@^4.0.8:
|
||||||
version "4.0.10"
|
version "4.0.10"
|
||||||
resolved "https://registry.yarnpkg.com/string.prototype.matchall/-/string.prototype.matchall-4.0.10.tgz#a1553eb532221d4180c51581d6072cd65d1ee100"
|
resolved "https://registry.yarnpkg.com/string.prototype.matchall/-/string.prototype.matchall-4.0.10.tgz#a1553eb532221d4180c51581d6072cd65d1ee100"
|
||||||
|
@ -5675,7 +6007,7 @@ string_decoder@~1.1.1:
|
||||||
dependencies:
|
dependencies:
|
||||||
safe-buffer "~5.1.0"
|
safe-buffer "~5.1.0"
|
||||||
|
|
||||||
strip-ansi@^6.0.1:
|
strip-ansi@^6.0.0, strip-ansi@^6.0.1:
|
||||||
version "6.0.1"
|
version "6.0.1"
|
||||||
resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-6.0.1.tgz#9e26c63d30f53443e9489495b2105d37b67a85d9"
|
resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-6.0.1.tgz#9e26c63d30f53443e9489495b2105d37b67a85d9"
|
||||||
integrity sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==
|
integrity sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==
|
||||||
|
@ -5790,7 +6122,7 @@ through2@^4.0.2:
|
||||||
dependencies:
|
dependencies:
|
||||||
readable-stream "3"
|
readable-stream "3"
|
||||||
|
|
||||||
"through@>=2.2.7 <3", through@~2.3.4:
|
"through@>=2.2.7 <3", through@^2.3.6, through@~2.3.4:
|
||||||
version "2.3.8"
|
version "2.3.8"
|
||||||
resolved "https://registry.yarnpkg.com/through/-/through-2.3.8.tgz#0dd4c9ffaabc357960b1b724115d7e0e86a2e1f5"
|
resolved "https://registry.yarnpkg.com/through/-/through-2.3.8.tgz#0dd4c9ffaabc357960b1b724115d7e0e86a2e1f5"
|
||||||
integrity sha512-w89qg7PI8wAdvX60bMDP+bFoD5Dvhm9oLheFp5O4a2QF0cSBGsBX4qZmadPMvVqlLJBBci+WqGGOAPvcDeNSVg==
|
integrity sha512-w89qg7PI8wAdvX60bMDP+bFoD5Dvhm9oLheFp5O4a2QF0cSBGsBX4qZmadPMvVqlLJBBci+WqGGOAPvcDeNSVg==
|
||||||
|
@ -5814,6 +6146,13 @@ tiny-lr@^2.0.0:
|
||||||
object-assign "^4.1.0"
|
object-assign "^4.1.0"
|
||||||
qs "^6.4.0"
|
qs "^6.4.0"
|
||||||
|
|
||||||
|
tmp@^0.0.33:
|
||||||
|
version "0.0.33"
|
||||||
|
resolved "https://registry.yarnpkg.com/tmp/-/tmp-0.0.33.tgz#6d34335889768d21b2bcda0aa277ced3b1bfadf9"
|
||||||
|
integrity sha512-jRCJlojKnZ3addtTOjdIqoRuPEKBvNXcGYqzO6zWZX8KfKEpnGY5jfggJQ3EjKuu8D4bJRr0y+cYJFmYbImXGw==
|
||||||
|
dependencies:
|
||||||
|
os-tmpdir "~1.0.2"
|
||||||
|
|
||||||
to-fast-properties@^2.0.0:
|
to-fast-properties@^2.0.0:
|
||||||
version "2.0.0"
|
version "2.0.0"
|
||||||
resolved "https://registry.yarnpkg.com/to-fast-properties/-/to-fast-properties-2.0.0.tgz#dc5e698cbd079265bc73e0377681a4e4e83f616e"
|
resolved "https://registry.yarnpkg.com/to-fast-properties/-/to-fast-properties-2.0.0.tgz#dc5e698cbd079265bc73e0377681a4e4e83f616e"
|
||||||
|
@ -5846,6 +6185,37 @@ ts-loader@^9.4.4:
|
||||||
micromatch "^4.0.0"
|
micromatch "^4.0.0"
|
||||||
semver "^7.3.4"
|
semver "^7.3.4"
|
||||||
|
|
||||||
|
ts-node@^10.9.1:
|
||||||
|
version "10.9.1"
|
||||||
|
resolved "https://registry.yarnpkg.com/ts-node/-/ts-node-10.9.1.tgz#e73de9102958af9e1f0b168a6ff320e25adcff4b"
|
||||||
|
integrity sha512-NtVysVPkxxrwFGUUxGYhfux8k78pQB3JqYBXlLRZgdGUqTO5wU/UyHop5p70iEbGhB7q5KmiZiU0Y3KlJrScEw==
|
||||||
|
dependencies:
|
||||||
|
"@cspotcode/source-map-support" "^0.8.0"
|
||||||
|
"@tsconfig/node10" "^1.0.7"
|
||||||
|
"@tsconfig/node12" "^1.0.7"
|
||||||
|
"@tsconfig/node14" "^1.0.0"
|
||||||
|
"@tsconfig/node16" "^1.0.2"
|
||||||
|
acorn "^8.4.1"
|
||||||
|
acorn-walk "^8.1.1"
|
||||||
|
arg "^4.1.0"
|
||||||
|
create-require "^1.1.0"
|
||||||
|
diff "^4.0.1"
|
||||||
|
make-error "^1.1.1"
|
||||||
|
v8-compile-cache-lib "^3.0.1"
|
||||||
|
yn "3.1.1"
|
||||||
|
|
||||||
|
ts-patch@^3.0.2:
|
||||||
|
version "3.0.2"
|
||||||
|
resolved "https://registry.yarnpkg.com/ts-patch/-/ts-patch-3.0.2.tgz#cbdf88e4dfb596e4dab8f2c8269361d33270a0ba"
|
||||||
|
integrity sha512-iTg8euqiNsNM1VDfOsVIsP0bM4kAVXU38n7TGQSkky7YQX/syh6sDPIRkvSS0HjT8ZOr0pq1h+5Le6jdB3hiJQ==
|
||||||
|
dependencies:
|
||||||
|
chalk "^4.1.2"
|
||||||
|
global-prefix "^3.0.0"
|
||||||
|
minimist "^1.2.8"
|
||||||
|
resolve "^1.22.2"
|
||||||
|
semver "^7.3.8"
|
||||||
|
strip-ansi "^6.0.1"
|
||||||
|
|
||||||
tsconfig@^5.0.3:
|
tsconfig@^5.0.3:
|
||||||
version "5.0.3"
|
version "5.0.3"
|
||||||
resolved "https://registry.yarnpkg.com/tsconfig/-/tsconfig-5.0.3.tgz#5f4278e701800967a8fc383fd19648878f2a6e3a"
|
resolved "https://registry.yarnpkg.com/tsconfig/-/tsconfig-5.0.3.tgz#5f4278e701800967a8fc383fd19648878f2a6e3a"
|
||||||
|
@ -5868,6 +6238,11 @@ tsify@^5.0.4:
|
||||||
through2 "^2.0.0"
|
through2 "^2.0.0"
|
||||||
tsconfig "^5.0.3"
|
tsconfig "^5.0.3"
|
||||||
|
|
||||||
|
tslib@^2.1.0:
|
||||||
|
version "2.6.2"
|
||||||
|
resolved "https://registry.yarnpkg.com/tslib/-/tslib-2.6.2.tgz#703ac29425e7b37cd6fd456e92404d46d1f3e4ae"
|
||||||
|
integrity sha512-AEYxH93jGFPn/a2iVAwW87VuUIkR1FVUKB77NwMF7nBTDkDrrT/Hpt/IrCJ0QXhW27jTBDcf5ZY7w6RiqTMw2Q==
|
||||||
|
|
||||||
tty-browserify@0.0.1:
|
tty-browserify@0.0.1:
|
||||||
version "0.0.1"
|
version "0.0.1"
|
||||||
resolved "https://registry.yarnpkg.com/tty-browserify/-/tty-browserify-0.0.1.tgz#3f05251ee17904dfd0677546670db9651682b811"
|
resolved "https://registry.yarnpkg.com/tty-browserify/-/tty-browserify-0.0.1.tgz#3f05251ee17904dfd0677546670db9651682b811"
|
||||||
|
@ -5892,6 +6267,11 @@ type-fest@^0.20.2:
|
||||||
resolved "https://registry.yarnpkg.com/type-fest/-/type-fest-0.20.2.tgz#1bf207f4b28f91583666cb5fbd327887301cd5f4"
|
resolved "https://registry.yarnpkg.com/type-fest/-/type-fest-0.20.2.tgz#1bf207f4b28f91583666cb5fbd327887301cd5f4"
|
||||||
integrity sha512-Ne+eE4r0/iWnpAxD852z3A+N0Bt5RN//NjJwRd2VFHEmrywxf5vsZlh4R6lixl6B+wz/8d+maTSAkN1FIkI3LQ==
|
integrity sha512-Ne+eE4r0/iWnpAxD852z3A+N0Bt5RN//NjJwRd2VFHEmrywxf5vsZlh4R6lixl6B+wz/8d+maTSAkN1FIkI3LQ==
|
||||||
|
|
||||||
|
type-fest@^0.21.3:
|
||||||
|
version "0.21.3"
|
||||||
|
resolved "https://registry.yarnpkg.com/type-fest/-/type-fest-0.21.3.tgz#d260a24b0198436e133fa26a524a6d65fa3b2e37"
|
||||||
|
integrity sha512-t0rzBq87m3fVcduHDUFhKmyyX+9eo6WQjZvf51Ea/M0Q7+T374Jp1aUiyUl0GKxp8M/OETVHSDvmkyPgvX+X2w==
|
||||||
|
|
||||||
type-is@~1.6.18:
|
type-is@~1.6.18:
|
||||||
version "1.6.18"
|
version "1.6.18"
|
||||||
resolved "https://registry.yarnpkg.com/type-is/-/type-is-1.6.18.tgz#4e552cd05df09467dcbc4ef739de89f2cf37c131"
|
resolved "https://registry.yarnpkg.com/type-is/-/type-is-1.6.18.tgz#4e552cd05df09467dcbc4ef739de89f2cf37c131"
|
||||||
|
@ -5959,6 +6339,16 @@ typescript@^5.2.2:
|
||||||
resolved "https://registry.yarnpkg.com/typescript/-/typescript-5.2.2.tgz#5ebb5e5a5b75f085f22bc3f8460fba308310fa78"
|
resolved "https://registry.yarnpkg.com/typescript/-/typescript-5.2.2.tgz#5ebb5e5a5b75f085f22bc3f8460fba308310fa78"
|
||||||
integrity sha512-mI4WrpHsbCIcwT9cF4FZvr80QUeKvsUsUvKDoR+X/7XHQH98xYD8YHZg7ANtz2GtZt/CBq2QJ0thkGJMHfqc1w==
|
integrity sha512-mI4WrpHsbCIcwT9cF4FZvr80QUeKvsUsUvKDoR+X/7XHQH98xYD8YHZg7ANtz2GtZt/CBq2QJ0thkGJMHfqc1w==
|
||||||
|
|
||||||
|
typia@^5.1.6:
|
||||||
|
version "5.1.6"
|
||||||
|
resolved "https://registry.yarnpkg.com/typia/-/typia-5.1.6.tgz#ee380512ee737bd704ddb1e3ef792b0a16f61639"
|
||||||
|
integrity sha512-in/m6hhsoS4jDfztT/hMlWVS670+0BcQNR0AX/sVctqrY/VnVs8cNdJiFn8iZdQ/QvLqWaT/FW1WUuibn8prMw==
|
||||||
|
dependencies:
|
||||||
|
commander "^10.0.0"
|
||||||
|
comment-json "^4.2.3"
|
||||||
|
inquirer "^8.2.5"
|
||||||
|
randexp "^0.5.3"
|
||||||
|
|
||||||
umd@^3.0.0:
|
umd@^3.0.0:
|
||||||
version "3.0.3"
|
version "3.0.3"
|
||||||
resolved "https://registry.yarnpkg.com/umd/-/umd-3.0.3.tgz#aa9fe653c42b9097678489c01000acb69f0b26cf"
|
resolved "https://registry.yarnpkg.com/umd/-/umd-3.0.3.tgz#aa9fe653c42b9097678489c01000acb69f0b26cf"
|
||||||
|
@ -6074,6 +6464,11 @@ utils-merge@1.0.1:
|
||||||
resolved "https://registry.yarnpkg.com/utils-merge/-/utils-merge-1.0.1.tgz#9f95710f50a267947b2ccc124741c1028427e713"
|
resolved "https://registry.yarnpkg.com/utils-merge/-/utils-merge-1.0.1.tgz#9f95710f50a267947b2ccc124741c1028427e713"
|
||||||
integrity sha512-pMZTvIkT1d+TFGvDOqodOclx0QWkkgi6Tdoa8gC8ffGAAqz9pzPTZWAybbsHHoED/ztMtkv/VoYTYyShUn81hA==
|
integrity sha512-pMZTvIkT1d+TFGvDOqodOclx0QWkkgi6Tdoa8gC8ffGAAqz9pzPTZWAybbsHHoED/ztMtkv/VoYTYyShUn81hA==
|
||||||
|
|
||||||
|
v8-compile-cache-lib@^3.0.1:
|
||||||
|
version "3.0.1"
|
||||||
|
resolved "https://registry.yarnpkg.com/v8-compile-cache-lib/-/v8-compile-cache-lib-3.0.1.tgz#6336e8d71965cb3d35a1bbb7868445a7c05264bf"
|
||||||
|
integrity sha512-wa7YjyUGfNZngI/vtK0UHAN+lgDCxBPCylVXGp0zu59Fz5aiGtNXaq3DhIov063MorB+VfufLh3JlF2KdTK3xg==
|
||||||
|
|
||||||
validatem-as-array-of@^0.0.1:
|
validatem-as-array-of@^0.0.1:
|
||||||
version "0.0.1"
|
version "0.0.1"
|
||||||
resolved "https://registry.yarnpkg.com/validatem-as-array-of/-/validatem-as-array-of-0.0.1.tgz#08ea8f5bd813bdffa703095f406290095b8bfd5a"
|
resolved "https://registry.yarnpkg.com/validatem-as-array-of/-/validatem-as-array-of-0.0.1.tgz#08ea8f5bd813bdffa703095f406290095b8bfd5a"
|
||||||
|
@ -6107,6 +6502,13 @@ watchify@^4.0.0:
|
||||||
through2 "^4.0.2"
|
through2 "^4.0.2"
|
||||||
xtend "^4.0.2"
|
xtend "^4.0.2"
|
||||||
|
|
||||||
|
wcwidth@^1.0.1:
|
||||||
|
version "1.0.1"
|
||||||
|
resolved "https://registry.yarnpkg.com/wcwidth/-/wcwidth-1.0.1.tgz#f0b0dcf915bc5ff1528afadb2c0e17b532da2fe8"
|
||||||
|
integrity sha512-XHPEwS0q6TaxcvG85+8EYkbiCux2XtWG2mkc47Ng2A77BQu9+DqIOJldST4HgPkuea7dvKSj5VgX3P1d4rW8Tg==
|
||||||
|
dependencies:
|
||||||
|
defaults "^1.0.3"
|
||||||
|
|
||||||
websocket-driver@>=0.5.1:
|
websocket-driver@>=0.5.1:
|
||||||
version "0.7.4"
|
version "0.7.4"
|
||||||
resolved "https://registry.yarnpkg.com/websocket-driver/-/websocket-driver-0.7.4.tgz#89ad5295bbf64b480abcba31e4953aca706f5760"
|
resolved "https://registry.yarnpkg.com/websocket-driver/-/websocket-driver-0.7.4.tgz#89ad5295bbf64b480abcba31e4953aca706f5760"
|
||||||
|
@ -6171,6 +6573,13 @@ which-typed-array@^1.1.11, which-typed-array@^1.1.2, which-typed-array@^1.1.9:
|
||||||
gopd "^1.0.1"
|
gopd "^1.0.1"
|
||||||
has-tostringtag "^1.0.0"
|
has-tostringtag "^1.0.0"
|
||||||
|
|
||||||
|
which@^1.3.1:
|
||||||
|
version "1.3.1"
|
||||||
|
resolved "https://registry.yarnpkg.com/which/-/which-1.3.1.tgz#a45043d54f5805316da8d62f9f50918d3da70b0a"
|
||||||
|
integrity sha512-HxJdYWq1MTIQbJ3nw0cqssHoTNU267KlrDuGZ1WYlxDStUtKUhOaJmh112/TZmHxxUfuJqPXSOm7tDyas0OSIQ==
|
||||||
|
dependencies:
|
||||||
|
isexe "^2.0.0"
|
||||||
|
|
||||||
which@^2.0.1:
|
which@^2.0.1:
|
||||||
version "2.0.2"
|
version "2.0.2"
|
||||||
resolved "https://registry.yarnpkg.com/which/-/which-2.0.2.tgz#7c6a8dd0a636a0327e10b59c9286eee93f3f51b1"
|
resolved "https://registry.yarnpkg.com/which/-/which-2.0.2.tgz#7c6a8dd0a636a0327e10b59c9286eee93f3f51b1"
|
||||||
|
@ -6190,6 +6599,15 @@ wouter@^2.8.0-alpha.2:
|
||||||
dependencies:
|
dependencies:
|
||||||
use-sync-external-store "^1.0.0"
|
use-sync-external-store "^1.0.0"
|
||||||
|
|
||||||
|
wrap-ansi@^6.0.1:
|
||||||
|
version "6.2.0"
|
||||||
|
resolved "https://registry.yarnpkg.com/wrap-ansi/-/wrap-ansi-6.2.0.tgz#e9393ba07102e6c91a3b221478f0257cd2856e53"
|
||||||
|
integrity sha512-r6lPcBGxZXlIcymEu7InxDMhdW0KDxpLgoFLcguasxCaJ/SOIZwINatK9KY/tf+ZrlywOKU0UDj3ATXUBfxJXA==
|
||||||
|
dependencies:
|
||||||
|
ansi-styles "^4.0.0"
|
||||||
|
string-width "^4.1.0"
|
||||||
|
strip-ansi "^6.0.0"
|
||||||
|
|
||||||
wrappy@1:
|
wrappy@1:
|
||||||
version "1.0.2"
|
version "1.0.2"
|
||||||
resolved "https://registry.yarnpkg.com/wrappy/-/wrappy-1.0.2.tgz#b5243d8f3ec1aa35f1364605bc0d1036e30ab69f"
|
resolved "https://registry.yarnpkg.com/wrappy/-/wrappy-1.0.2.tgz#b5243d8f3ec1aa35f1364605bc0d1036e30ab69f"
|
||||||
|
@ -6215,6 +6633,11 @@ yallist@^4.0.0:
|
||||||
resolved "https://registry.yarnpkg.com/yallist/-/yallist-4.0.0.tgz#9bb92790d9c0effec63be73519e11a35019a3a72"
|
resolved "https://registry.yarnpkg.com/yallist/-/yallist-4.0.0.tgz#9bb92790d9c0effec63be73519e11a35019a3a72"
|
||||||
integrity sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==
|
integrity sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==
|
||||||
|
|
||||||
|
yn@3.1.1:
|
||||||
|
version "3.1.1"
|
||||||
|
resolved "https://registry.yarnpkg.com/yn/-/yn-3.1.1.tgz#1e87401a09d767c1d5eab26a6e4c185182d2eb50"
|
||||||
|
integrity sha512-Ux4ygGWsu2c7isFWe8Yu1YluJmqVhxqK2cLXNQA5AcC3QfbGNpM7fu0Y8b/z16pXLnFxZYvWhd3fhBY9DLmC6Q==
|
||||||
|
|
||||||
yocto-queue@^0.1.0:
|
yocto-queue@^0.1.0:
|
||||||
version "0.1.0"
|
version "0.1.0"
|
||||||
resolved "https://registry.yarnpkg.com/yocto-queue/-/yocto-queue-0.1.0.tgz#0294eb3dee05028d31ee1a5fa2c556a6aaf10a1b"
|
resolved "https://registry.yarnpkg.com/yocto-queue/-/yocto-queue-0.1.0.tgz#0294eb3dee05028d31ee1a5fa2c556a6aaf10a1b"
|
||||||
|
|
Loading…
Reference in New Issue