summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--.drone.yml3
-rw-r--r--.goreleaser.yml1
-rw-r--r--CONTRIBUTING.md6
-rw-r--r--Dockerfile1
-rw-r--r--internal/api/client/admin/domainpermission.go2
-rw-r--r--web/source/package.json9
-rw-r--r--web/source/settings/admin/accounts/detail.jsx6
-rw-r--r--web/source/settings/admin/domain-permissions/detail.tsx254
-rw-r--r--web/source/settings/admin/domain-permissions/export-format-table.jsx (renamed from web/source/settings/admin/federation/import-export/export-format-table.jsx)0
-rw-r--r--web/source/settings/admin/domain-permissions/form.tsx (renamed from web/source/settings/admin/federation/import-export/form.jsx)68
-rw-r--r--web/source/settings/admin/domain-permissions/import-export.tsx90
-rw-r--r--web/source/settings/admin/domain-permissions/index.tsx49
-rw-r--r--web/source/settings/admin/domain-permissions/overview.tsx198
-rw-r--r--web/source/settings/admin/domain-permissions/process.tsx (renamed from web/source/settings/admin/federation/import-export/process.jsx)177
-rw-r--r--web/source/settings/admin/emoji/category-select.jsx5
-rw-r--r--web/source/settings/admin/emoji/local/detail.js18
-rw-r--r--web/source/settings/admin/emoji/local/new-emoji.js10
-rw-r--r--web/source/settings/admin/emoji/local/overview.js4
-rw-r--r--web/source/settings/admin/emoji/local/use-shortcode.js8
-rw-r--r--web/source/settings/admin/emoji/remote/index.js4
-rw-r--r--web/source/settings/admin/emoji/remote/parse-from-toot.js14
-rw-r--r--web/source/settings/admin/federation/detail.js168
-rw-r--r--web/source/settings/admin/federation/import-export/index.jsx75
-rw-r--r--web/source/settings/admin/federation/overview.js101
-rw-r--r--web/source/settings/admin/reports/detail.jsx14
-rw-r--r--web/source/settings/admin/reports/index.jsx7
-rw-r--r--web/source/settings/admin/settings/index.jsx13
-rw-r--r--web/source/settings/admin/settings/rules.jsx4
-rw-r--r--web/source/settings/components/authorization/index.tsx7
-rw-r--r--web/source/settings/components/authorization/login.tsx15
-rw-r--r--web/source/settings/components/check-list.tsx (renamed from web/source/settings/components/check-list.jsx)46
-rw-r--r--web/source/settings/components/form/inputs.tsx (renamed from web/source/settings/components/form/inputs.jsx)106
-rw-r--r--web/source/settings/components/user-logout-card.jsx14
-rw-r--r--web/source/settings/index.js12
-rw-r--r--web/source/settings/lib/form/bool.tsx (renamed from web/source/settings/lib/form/bool.jsx)17
-rw-r--r--web/source/settings/lib/form/check-list.tsx (renamed from web/source/settings/lib/form/check-list.jsx)158
-rw-r--r--web/source/settings/lib/form/combo-box.tsx (renamed from web/source/settings/lib/form/combo-box.jsx)23
-rw-r--r--web/source/settings/lib/form/field-array.tsx (renamed from web/source/settings/lib/form/field-array.jsx)50
-rw-r--r--web/source/settings/lib/form/file.tsx (renamed from web/source/settings/lib/form/file.jsx)88
-rw-r--r--web/source/settings/lib/form/form-with-data.tsx (renamed from web/source/settings/lib/form/form-with-data.jsx)33
-rw-r--r--web/source/settings/lib/form/get-form-mutations.ts (renamed from web/source/settings/lib/form/get-form-mutations.js)46
-rw-r--r--web/source/settings/lib/form/index.js83
-rw-r--r--web/source/settings/lib/form/index.ts114
-rw-r--r--web/source/settings/lib/form/radio.tsx (renamed from web/source/settings/lib/form/radio.jsx)18
-rw-r--r--web/source/settings/lib/form/submit.js67
-rw-r--r--web/source/settings/lib/form/submit.ts140
-rw-r--r--web/source/settings/lib/form/text.tsx (renamed from web/source/settings/lib/form/text.jsx)51
-rw-r--r--web/source/settings/lib/form/types.ts264
-rw-r--r--web/source/settings/lib/query/admin/custom-emoji.js194
-rw-r--r--web/source/settings/lib/query/admin/custom-emoji/index.ts307
-rw-r--r--web/source/settings/lib/query/admin/domain-permissions/export.ts155
-rw-r--r--web/source/settings/lib/query/admin/domain-permissions/get.ts56
-rw-r--r--web/source/settings/lib/query/admin/domain-permissions/import.ts140
-rw-r--r--web/source/settings/lib/query/admin/domain-permissions/process.ts163
-rw-r--r--web/source/settings/lib/query/admin/domain-permissions/update.ts109
-rw-r--r--web/source/settings/lib/query/admin/import-export.js264
-rw-r--r--web/source/settings/lib/query/admin/index.js165
-rw-r--r--web/source/settings/lib/query/admin/index.ts148
-rw-r--r--web/source/settings/lib/query/admin/reports.js51
-rw-r--r--web/source/settings/lib/query/admin/reports/index.ts83
-rw-r--r--web/source/settings/lib/query/gts-api.ts16
-rw-r--r--web/source/settings/lib/query/lib.js81
-rw-r--r--web/source/settings/lib/query/oauth/index.ts10
-rw-r--r--web/source/settings/lib/query/query-modifiers.ts150
-rw-r--r--web/source/settings/lib/query/transforms.ts78
-rw-r--r--web/source/settings/lib/query/user/index.ts8
-rw-r--r--web/source/settings/lib/types/custom-emoji.ts (renamed from web/source/settings/admin/federation/index.js)52
-rw-r--r--web/source/settings/lib/types/domain-permission.ts97
-rw-r--r--web/source/settings/lib/types/instance.ts91
-rw-r--r--web/source/settings/lib/types/query.ts95
-rw-r--r--web/source/settings/lib/types/report.ts144
-rw-r--r--web/source/settings/lib/util/domain-permission.ts (renamed from web/source/settings/lib/domain-block.js)37
-rw-r--r--web/source/settings/style.css14
-rw-r--r--web/source/settings/user/profile.js16
-rw-r--r--web/source/settings/user/settings.js4
-rw-r--r--web/source/tsconfig.json8
-rw-r--r--web/source/yarn.lock447
77 files changed, 4154 insertions, 1690 deletions
diff --git a/.drone.yml b/.drone.yml
index c398db390..8e2aebb86 100644
--- a/.drone.yml
+++ b/.drone.yml
@@ -54,6 +54,7 @@ steps:
path: /tmp/cache
commands:
- 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
image: node:18-alpine
@@ -191,6 +192,6 @@ steps:
---
kind: signature
-hmac: c3efbd528a76016562f88ae435141cfb5fd6d4d07b6ad2a24ecc23cb529cc1c6
+hmac: d7b93470276a0df7e4d862941489f00da107df3d085200009b776d33599e6043
...
diff --git a/.goreleaser.yml b/.goreleaser.yml
index 1b49136c7..a49bb32e8 100644
--- a/.goreleaser.yml
+++ b/.goreleaser.yml
@@ -8,6 +8,7 @@ before:
- sed -i "s/REPLACE_ME/{{ incpatch .Version }}/" web/assets/swagger.yaml
# Install web deps + bundle web assets
- yarn --cwd ./web/source install
+ - yarn --cwd ./web/source ts-patch install # https://typia.io/docs/setup/#manual-setup
- yarn --cwd ./web/source build
builds:
# https://goreleaser.com/customization/build/
diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md
index c8218564d..628832e1c 100644
--- a/CONTRIBUTING.md
+++ b/CONTRIBUTING.md
@@ -229,13 +229,15 @@ Using [NVM](https://github.com/nvm-sh/nvm) is one convenient way to install them
To install frontend dependencies:
```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`:
```bash
-yarn --cwd web/source build
+yarn --cwd ./web/source build
```
#### Live Reloading
diff --git a/Dockerfile b/Dockerfile
index d772f7497..7c1cce4d2 100644
--- a/Dockerfile
+++ b/Dockerfile
@@ -16,6 +16,7 @@ FROM --platform=${BUILDPLATFORM} node:18-alpine AS bundler
COPY web web
RUN yarn --cwd ./web/source install && \
+ yarn --cwd ./web/source ts-patch install && \
yarn --cwd ./web/source build && \
rm -rf ./web/source
diff --git a/internal/api/client/admin/domainpermission.go b/internal/api/client/admin/domainpermission.go
index bd6b83425..203eddc8b 100644
--- a/internal/api/client/admin/domainpermission.go
+++ b/internal/api/client/admin/domainpermission.go
@@ -95,7 +95,7 @@ func (m *Module) createDomainPermissions(
if importing && form.Domains.Size == 0 {
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")
}
diff --git a/web/source/package.json b/web/source/package.json
index d3c1cbe2b..20f525228 100644
--- a/web/source/package.json
+++ b/web/source/package.json
@@ -45,6 +45,10 @@
"@browserify/envify": "^6.0.0",
"@browserify/uglifyify": "^6.0.0",
"@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",
"@typescript-eslint/eslint-plugin": "^6.7.4",
"@typescript-eslint/parser": "^6.7.4",
@@ -63,7 +67,10 @@
"postcss-nested": "^6.0.0",
"source-map-loader": "^4.0.1",
"ts-loader": "^9.4.4",
+ "ts-node": "^10.9.1",
+ "ts-patch": "^3.0.2",
"tsify": "^5.0.4",
- "typescript": "^5.2.2"
+ "typescript": "^5.2.2",
+ "typia": "^5.1.6"
}
}
diff --git a/web/source/settings/admin/accounts/detail.jsx b/web/source/settings/admin/accounts/detail.jsx
index 0e906cd1c..63049c149 100644
--- a/web/source/settings/admin/accounts/detail.jsx
+++ b/web/source/settings/admin/accounts/detail.jsx
@@ -22,13 +22,13 @@ const { useRoute, Redirect } = require("wouter");
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 FakeProfile = require("../../components/fake-profile");
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 { TextInput } = require("../../components/form/inputs");
@@ -77,7 +77,7 @@ function AccountDetailForm({ data: account }) {
function ModifyAccount({ account }) {
const form = {
id: useValue("id", account.id),
- reason: useTextInput("text", {})
+ reason: useTextInput("text")
};
const [modifyAccount, result] = useFormSubmit(form, query.useActionAccountMutation());
diff --git a/web/source/settings/admin/domain-permissions/detail.tsx b/web/source/settings/admin/domain-permissions/detail.tsx
new file mode 100644
index 000000000..f74802666
--- /dev/null
+++ b/web/source/settings/admin/domain-permissions/detail.tsx
@@ -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>
+ );
+}
diff --git a/web/source/settings/admin/federation/import-export/export-format-table.jsx b/web/source/settings/admin/domain-permissions/export-format-table.jsx
index 7fcffa348..7fcffa348 100644
--- a/web/source/settings/admin/federation/import-export/export-format-table.jsx
+++ b/web/source/settings/admin/domain-permissions/export-format-table.jsx
diff --git a/web/source/settings/admin/federation/import-export/form.jsx b/web/source/settings/admin/domain-permissions/form.tsx
index 2086739e3..fb639202d 100644
--- a/web/source/settings/admin/federation/import-export/form.jsx
+++ b/web/source/settings/admin/domain-permissions/form.tsx
@@ -17,34 +17,57 @@
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");
-const useFormSubmit = require("../../../lib/form/submit");
+import { useEffect } from "react";
-const {
+import { useExportDomainListMutation } from "../../lib/query/admin/domain-permissions/export";
+import useFormSubmit from "../../lib/form/submit";
+
+import {
+ RadioGroup,
TextArea,
Select,
-} = require("../../../components/form/inputs");
+} from "../../components/form/inputs";
+
+import MutationButton from "../../components/form/mutation-button";
+
+import { Error } from "../../components/error";
+import ExportFormatTable from "./export-format-table";
-const MutationButton = require("../../../components/form/mutation-button");
+import type {
+ FormSubmitFunction,
+ FormSubmitResult,
+ RadioFormInputHook,
+ TextFormInputHook,
+} from "../../lib/form/types";
-const { Error } = require("../../../components/error");
-const ExportFormatTable = require("./export-format-table");
+export interface ImportExportFormProps {
+ form: {
+ domains: TextFormInputHook;
+ exportType: TextFormInputHook;
+ permType: RadioFormInputHook;
+ };
+ submitParse: FormSubmitFunction;
+ parseResult: FormSubmitResult;
+}
-module.exports = function ImportExportForm({ form, submitParse, parseResult }) {
- const [submitExport, exportResult] = useFormSubmit(form, query.useExportDomainListMutation());
+export default function ImportExportForm({ form, submitParse, parseResult }: ImportExportFormProps) {
+ const [submitExport, exportResult] = useFormSubmit(form, useExportDomainListMutation());
function fileChanged(e) {
const reader = new FileReader();
reader.onload = function (read) {
- form.domains.value = read.target.result;
- submitParse();
+ const res = read.target?.result;
+ if (typeof res === "string") {
+ form.domains.value = res;
+ submitParse();
+ }
};
reader.readAsText(e.target.files[0]);
}
- React.useEffect(() => {
+ useEffect(() => {
if (exportResult.isSuccess) {
form.domains.setter(exportResult.data);
}
@@ -53,12 +76,10 @@ module.exports = function ImportExportForm({ form, submitParse, parseResult }) {
return (
<>
- <h1>Import / Export suspended domains</h1>
- <p>
- This page can be used to import and export lists of domains to suspend.
- Exports can be done in various formats, with varying functionality and support in other software.
- Imports will automatically detect what format is being processed.
- </p>
+ <h1>Import / Export domain permissions</h1>
+ <p>This page can be used to import and export lists of domain permissions.</p>
+ <p>Exports can be done in various formats, with varying functionality and support in other software.</p>
+ <p>Imports will automatically detect what format is being processed.</p>
<ExportFormatTable />
<div className="import-export">
<TextArea
@@ -68,6 +89,10 @@ module.exports = function ImportExportForm({ form, submitParse, parseResult }) {
rows={8}
/>
+ <RadioGroup
+ field={form.permType}
+ />
+
<div className="button-grid">
<MutationButton
label="Import"
@@ -75,6 +100,7 @@ module.exports = function ImportExportForm({ form, submitParse, parseResult }) {
onClick={() => submitParse()}
result={parseResult}
showError={false}
+ disabled={false}
/>
<label className="button with-icon">
<i className="fa fa-fw " aria-hidden="true" />
@@ -92,6 +118,7 @@ module.exports = function ImportExportForm({ form, submitParse, parseResult }) {
type="button"
onClick={() => submitExport("export")}
result={exportResult} showError={false}
+ disabled={false}
/>
<MutationButton
label="Export to file"
@@ -100,6 +127,7 @@ module.exports = function ImportExportForm({ form, submitParse, parseResult }) {
onClick={() => submitExport("export-file")}
result={exportResult}
showError={false}
+ disabled={false}
/>
<div className="export-file">
<span>
@@ -121,4 +149,4 @@ module.exports = function ImportExportForm({ form, submitParse, parseResult }) {
</div>
</>
);
-}; \ No newline at end of file
+}
diff --git a/web/source/settings/admin/domain-permissions/import-export.tsx b/web/source/settings/admin/domain-permissions/import-export.tsx
new file mode 100644
index 000000000..871bca131
--- /dev/null
+++ b/web/source/settings/admin/domain-permissions/import-export.tsx
@@ -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);
+ }}
+ >
+ &lt; back
+ </span>
+ &nbsp; 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>
+ );
+}
diff --git a/web/source/settings/admin/domain-permissions/index.tsx b/web/source/settings/admin/domain-permissions/index.tsx
new file mode 100644
index 000000000..7d790cfc8
--- /dev/null
+++ b/web/source/settings/admin/domain-permissions/index.tsx
@@ -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>
+ );
+}
diff --git a/web/source/settings/admin/domain-permissions/overview.tsx b/web/source/settings/admin/domain-permissions/overview.tsx
new file mode 100644
index 000000000..a37ec9184
--- /dev/null
+++ b/web/source/settings/admin/domain-permissions/overview.tsx
@@ -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}&nbsp;{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>
+ );
+}
diff --git a/web/source/settings/admin/federation/import-export/process.jsx b/web/source/settings/admin/domain-permissions/process.tsx
index b39410605..bb9411b9d 100644
--- a/web/source/settings/admin/federation/import-export/process.jsx
+++ b/web/source/settings/admin/domain-permissions/process.tsx
@@ -17,57 +17,81 @@
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");
-const { isValidDomainBlock, hasBetterScope } = require("../../../lib/domain-block");
+import { memo, useMemo, useCallback, useEffect } from "react";
-const {
+import { isValidDomainPermission, hasBetterScope } from "../../lib/util/domain-permission";
+
+import {
useTextInput,
useBoolInput,
useRadioInput,
- useCheckListInput
-} = require("../../../lib/form");
-
-const useFormSubmit = require("../../../lib/form/submit");
+ useCheckListInput,
+} from "../../lib/form";
-const {
- TextInput,
+import {
+ Select,
TextArea,
+ RadioGroup,
Checkbox,
- Select,
- RadioGroup
-} = require("../../../components/form/inputs");
+ TextInput,
+} from "../../components/form/inputs";
+
+import useFormSubmit from "../../lib/form/submit";
-const CheckList = require("../../../components/check-list");
-const MutationButton = require("../../../components/form/mutation-button");
-const FormWithData = require("../../../lib/form/form-with-data");
+import CheckList from "../../components/check-list";
+import MutationButton from "../../components/form/mutation-button";
+import FormWithData from "../../lib/form/form-with-data";
-module.exports = React.memo(
- function ProcessImport({ list }) {
+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 (
<div className="without-border">
<FormWithData
- dataQuery={query.useInstanceBlocksQuery}
+ dataQuery={permType.value == "allow"
+ ? useDomainAllowsQuery
+ : useDomainBlocksQuery
+ }
DataForm={ImportList}
- list={list}
+ {...{ list, permType }}
/>
</div>
);
}
);
-function ImportList({ list, data: blockedInstances }) {
- const hasComment = React.useMemo(() => {
+export interface ImportListProps {
+ list: Array<DomainPerm>,
+ data: MappedDomainPerms,
+ permType: RadioFormInputHook,
+}
+
+function ImportList({ list, data: domainPerms, permType }: ImportListProps) {
+ const hasComment = useMemo(() => {
let hasPublic = false;
let hasPrivate = false;
list.some((entry) => {
- if (entry.public_comment?.length > 0) {
+ if (entry.public_comment) {
hasPublic = true;
}
- if (entry.private_comment?.length > 0) {
+ if (entry.private_comment) {
hasPrivate = true;
}
@@ -88,7 +112,7 @@ function ImportList({ list, data: blockedInstances }) {
const showComment = useTextInput("showComment", { defaultValue: hasComment.type ?? "public_comment" });
const form = {
- domains: useCheckListInput("domains", { entries: list }),
+ domains: useCheckListInput("domains", { entries: list }), // DomainPerm is actually also a Checkable.
obfuscate: useBoolInput("obfuscate"),
privateComment: useTextInput("private_comment", {
defaultValue: `Imported on ${new Date().toLocaleString()}`
@@ -108,13 +132,17 @@ function ImportList({ list, data: blockedInstances }) {
replace: "Replace"
}
}),
+ permType: permType,
};
- const [importDomains, importResult] = useFormSubmit(form, query.useImportDomainListMutation(), { changedOnly: false });
+ const [importDomains, importResult] = useFormSubmit(form, useImportDomainPermsMutation(), { changedOnly: false });
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>
{hasComment.both &&
@@ -129,8 +157,9 @@ function ImportList({ list, data: blockedInstances }) {
<div className="checkbox-list-wrapper">
<DomainCheckList
field={form.domains}
- blockedInstances={blockedInstances}
- commentType={showComment.value}
+ domainPerms={domainPerms}
+ commentType={showComment.value as "public_comment" | "private_comment"}
+ permType={form.permType}
/>
</div>
@@ -159,28 +188,41 @@ function ImportList({ list, data: blockedInstances }) {
label="Obfuscate domains in public lists"
/>
- <MutationButton label="Import" result={importResult} />
+ <MutationButton
+ label="Import"
+ disabled={false}
+ result={importResult}
+ />
</form>
</>
);
}
-function DomainCheckList({ field, blockedInstances, commentType }) {
- const getExtraProps = React.useCallback((entry) => {
+interface DomainCheckListProps {
+ field: ChecklistInputHook,
+ domainPerms: MappedDomainPerms,
+ commentType: "public_comment" | "private_comment",
+ permType: RadioFormInputHook,
+}
+
+function DomainCheckList({ field, domainPerms, commentType, permType }: DomainCheckListProps) {
+ const getExtraProps = useCallback((entry: DomainPerm) => {
return {
comment: entry[commentType],
- alreadyExists: blockedInstances[entry.domain] != undefined
+ alreadyExists: entry.domain in domainPerms,
+ permType: permType,
};
- }, [blockedInstances, commentType]);
+ }, [domainPerms, commentType, permType]);
- const entriesWithSuggestions = React.useMemo(() => (
- Object.values(field.value).filter((entry) => entry.suggest)
- ), [field.value]);
+ const entriesWithSuggestions = useMemo(() => {
+ const fieldValue = (field.value ?? {}) as { [k: string]: DomainPerm; };
+ return Object.values(fieldValue).filter((entry) => entry.suggest);
+ }, [field.value]);
return (
<>
<CheckList
- field={field}
+ field={field as ChecklistInputHook}
header={<>
<b>Domain</b>
<b>
@@ -200,8 +242,14 @@ function DomainCheckList({ field, blockedInstances, commentType }) {
);
}
-const UpdateHint = React.memo(
- function UpdateHint({ entries, updateEntry, updateMultiple }) {
+interface UpdateHintProps {
+ entries,
+ updateEntry,
+ updateMultiple,
+}
+
+const UpdateHint = memo(
+ function UpdateHint({ entries, updateEntry, updateMultiple }: UpdateHintProps) {
if (entries.length == 0) {
return null;
}
@@ -229,8 +277,13 @@ const UpdateHint = React.memo(
}
);
-const UpdateableEntry = React.memo(
- function UpdateableEntry({ entry, updateEntry }) {
+interface UpdateableEntryProps {
+ entry,
+ updateEntry,
+}
+
+const UpdateableEntry = memo(
+ function UpdateableEntry({ entry, updateEntry }: UpdateableEntryProps) {
return (
<>
<span className="text-cutoff">{entry.domain}</span>
@@ -248,21 +301,31 @@ function domainValidationError(isValid) {
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", {
defaultValue: entry.domain,
showValidation: entry.checked,
initValidation: domainValidationError(entry.valid),
- validator: (value) => domainValidationError(isValidDomainBlock(value))
+ validator: (value) => domainValidationError(isValidDomainPermission(value))
});
- React.useEffect(() => {
+ useEffect(() => {
if (entry.valid != domainField.valid) {
onChange({ valid: domainField.valid });
}
}, [onChange, entry.valid, domainField.valid]);
- React.useEffect(() => {
+ useEffect(() => {
if (entry.domain != domainField.value) {
domainField.setter(entry.domain);
}
@@ -270,8 +333,8 @@ function DomainEntry({ entry, onChange, extraProps: { alreadyExists, comment } }
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [entry.domain, domainField.setter]);
- React.useEffect(() => {
- onChange({ suggest: hasBetterScope(domainField.value) });
+ useEffect(() => {
+ onChange({ suggest: hasBetterScope(domainField.value ?? "") });
// only need this update if it's the entry.checked that updated, not onChange
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [domainField.value]);
@@ -296,7 +359,11 @@ function DomainEntry({ entry, onChange, extraProps: { alreadyExists, comment } }
}}
/>
<span id="icon" onClick={clickIcon}>
- <DomainEntryIcon alreadyExists={alreadyExists} suggestion={entry.suggest} onChange={onChange} />
+ <DomainEntryIcon
+ alreadyExists={alreadyExists}
+ suggestion={entry.suggest}
+ permTypeString={permType.value?? ""}
+ />
</span>
</div>
<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 text;
@@ -312,8 +385,8 @@ function DomainEntryIcon({ alreadyExists, suggestion }) {
icon = "fa-info-circle suggest-changes";
text = `Entry targets a specific subdomain, consider changing it to '${suggestion}'.`;
} else if (alreadyExists) {
- icon = "fa-history already-blocked";
- text = "Domain block already exists.";
+ icon = "fa-history permission-already-exists";
+ text = `Domain ${permTypeString} already exists.`;
}
if (!icon) {
diff --git a/web/source/settings/admin/emoji/category-select.jsx b/web/source/settings/admin/emoji/category-select.jsx
index da2604602..e5cf29939 100644
--- a/web/source/settings/admin/emoji/category-select.jsx
+++ b/web/source/settings/admin/emoji/category-select.jsx
@@ -22,9 +22,8 @@ const splitFilterN = require("split-filter-n");
const syncpipe = require('syncpipe');
const { matchSorter } = require("match-sorter");
-const query = require("../../lib/query");
-
const ComboBox = require("../../components/combo-box");
+const { useListEmojiQuery } = require("../../lib/query/admin/custom-emoji");
function useEmojiByCategory(emoji) {
// split all emoji over an object keyed by the category names (or Unsorted)
@@ -43,7 +42,7 @@ function CategorySelect({ field, children }) {
isLoading,
isSuccess,
error
- } = query.useListEmojiQuery({ filter: "domain:local" });
+ } = useListEmojiQuery({ filter: "domain:local" });
const emojiByCategory = useEmojiByCategory(emoji);
diff --git a/web/source/settings/admin/emoji/local/detail.js b/web/source/settings/admin/emoji/local/detail.js
index daf7a2dac..18a681b6e 100644
--- a/web/source/settings/admin/emoji/local/detail.js
+++ b/web/source/settings/admin/emoji/local/detail.js
@@ -20,21 +20,25 @@
const React = require("react");
const { useRoute, Link, Redirect } = require("wouter");
-const query = require("../../../lib/query");
-
const { useComboBoxInput, useFileInput, useValue } = require("../../../lib/form");
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 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 { FileInput } = require("../../../components/form/inputs");
const MutationButton = require("../../../components/form/mutation-button");
const { Error } = require("../../../components/error");
+const {
+ useGetEmojiQuery,
+ useEditEmojiMutation,
+ useDeleteEmojiMutation,
+} = require("../../../lib/query/admin/custom-emoji");
+
module.exports = function EmojiDetailRoute({ }) {
const baseUrl = useBaseUrl();
let [_match, params] = useRoute(`${baseUrl}/:emojiId`);
@@ -44,7 +48,7 @@ module.exports = function EmojiDetailRoute({ }) {
return (
<div className="emoji-detail">
<Link to={baseUrl}><a>&lt; go back</a></Link>
- <FormWithData dataQuery={query.useGetEmojiQuery} queryArg={params.emojiId} DataForm={EmojiDetailForm} />
+ <FormWithData dataQuery={useGetEmojiQuery} queryArg={params.emojiId} DataForm={EmojiDetailForm} />
</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
React.useEffect(() => {
@@ -74,7 +78,7 @@ function EmojiDetailForm({ data: emoji }) {
/* eslint-disable-next-line react-hooks/exhaustive-deps */
}, [form.category.hasChanged(), form.category.isNew, form.category.state.open]);
- const [deleteEmoji, deleteResult] = query.useDeleteEmojiMutation();
+ const [deleteEmoji, deleteResult] = useDeleteEmojiMutation();
if (deleteResult.isSuccess) {
return <Redirect to={baseUrl} />;
diff --git a/web/source/settings/admin/emoji/local/new-emoji.js b/web/source/settings/admin/emoji/local/new-emoji.js
index 439d09e62..ecb0465cb 100644
--- a/web/source/settings/admin/emoji/local/new-emoji.js
+++ b/web/source/settings/admin/emoji/local/new-emoji.js
@@ -19,15 +19,13 @@
const React = require("react");
-const query = require("../../../lib/query");
-
const {
useFileInput,
useComboBoxInput
} = require("../../../lib/form");
const useShortcode = require("./use-shortcode");
-const useFormSubmit = require("../../../lib/form/submit");
+const useFormSubmit = require("../../../lib/form/submit").default;
const {
TextInput, FileInput
@@ -36,11 +34,13 @@ const {
const { CategorySelect } = require('../category-select');
const FakeToot = require("../../../components/fake-toot");
const MutationButton = require("../../../components/form/mutation-button");
+const { useAddEmojiMutation } = require("../../../lib/query/admin/custom-emoji");
+const { useInstanceV1Query } = require("../../../lib/query");
module.exports = function NewEmojiForm() {
const shortcode = useShortcode();
- const { data: instance } = query.useInstanceQuery();
+ const { data: instance } = useInstanceV1Query();
const emojiMaxSize = React.useMemo(() => {
return instance?.configuration?.emojis?.emoji_size_limit ?? 50 * 1024;
}, [instance]);
@@ -54,7 +54,7 @@ module.exports = function NewEmojiForm() {
const [submitForm, result] = useFormSubmit({
shortcode, image, category
- }, query.useAddEmojiMutation());
+ }, useAddEmojiMutation());
React.useEffect(() => {
if (shortcode.value.length == 0) {
diff --git a/web/source/settings/admin/emoji/local/overview.js b/web/source/settings/admin/emoji/local/overview.js
index 38dc1feba..757f07c43 100644
--- a/web/source/settings/admin/emoji/local/overview.js
+++ b/web/source/settings/admin/emoji/local/overview.js
@@ -25,13 +25,13 @@ const { matchSorter } = require("match-sorter");
const NewEmojiForm = require("./new-emoji");
const { useTextInput } = require("../../../lib/form");
-const query = require("../../../lib/query");
const { useEmojiByCategory } = require("../category-select");
const { useBaseUrl } = require("../../../lib/navigation/util");
const Loading = require("../../../components/loading");
const { Error } = require("../../../components/error");
const { TextInput } = require("../../../components/form/inputs");
+const { useListEmojiQuery } = require("../../../lib/query/admin/custom-emoji");
module.exports = function EmojiOverview({ }) {
const {
@@ -39,7 +39,7 @@ module.exports = function EmojiOverview({ }) {
isLoading,
isError,
error
- } = query.useListEmojiQuery({ filter: "domain:local" });
+ } = useListEmojiQuery({ filter: "domain:local" });
let content = null;
diff --git a/web/source/settings/admin/emoji/local/use-shortcode.js b/web/source/settings/admin/emoji/local/use-shortcode.js
index 7e1bae0ad..67255860f 100644
--- a/web/source/settings/admin/emoji/local/use-shortcode.js
+++ b/web/source/settings/admin/emoji/local/use-shortcode.js
@@ -19,15 +19,15 @@
const React = require("react");
-const query = require("../../../lib/query");
const { useTextInput } = require("../../../lib/form");
+const { useListEmojiQuery } = require("../../../lib/query/admin/custom-emoji");
const shortcodeRegex = /^\w{2,30}$/;
module.exports = function useShortcode() {
- const {
- data: emoji = []
- } = query.useListEmojiQuery({ filter: "domain:local" });
+ const { data: emoji = [] } = useListEmojiQuery({
+ filter: "domain:local"
+ });
const emojiCodes = React.useMemo(() => {
return new Set(emoji.map((e) => e.shortcode));
diff --git a/web/source/settings/admin/emoji/remote/index.js b/web/source/settings/admin/emoji/remote/index.js
index e877efb89..1a8c719dd 100644
--- a/web/source/settings/admin/emoji/remote/index.js
+++ b/web/source/settings/admin/emoji/remote/index.js
@@ -21,9 +21,9 @@ const React = require("react");
const ParseFromToot = require("./parse-from-toot");
-const query = require("../../../lib/query");
const Loading = require("../../../components/loading");
const { Error } = require("../../../components/error");
+const { useListEmojiQuery } = require("../../../lib/query/admin/custom-emoji");
module.exports = function RemoteEmoji() {
// local emoji are queried for shortcode collision detection
@@ -31,7 +31,7 @@ module.exports = function RemoteEmoji() {
data: emoji = [],
isLoading,
error
- } = query.useListEmojiQuery({ filter: "domain:local" });
+ } = useListEmojiQuery({ filter: "domain:local" });
const emojiCodes = React.useMemo(() => {
return new Set(emoji.map((e) => e.shortcode));
diff --git a/web/source/settings/admin/emoji/remote/parse-from-toot.js b/web/source/settings/admin/emoji/remote/parse-from-toot.js
index e6438a4d2..503a341c8 100644
--- a/web/source/settings/admin/emoji/remote/parse-from-toot.js
+++ b/web/source/settings/admin/emoji/remote/parse-from-toot.js
@@ -19,25 +19,27 @@
const React = require("react");
-const query = require("../../../lib/query");
-
const {
useTextInput,
useComboBoxInput,
useCheckListInput
} = 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 { TextInput } = require("../../../components/form/inputs");
const MutationButton = require("../../../components/form/mutation-button");
const { Error } = require("../../../components/error");
+const {
+ useSearchItemForEmojiMutation,
+ usePatchRemoteEmojisMutation
+} = require("../../../lib/query/admin/custom-emoji");
module.exports = function ParseFromToot({ emojiCodes }) {
- const [searchStatus, result] = query.useSearchStatusForEmojiMutation();
+ const [searchStatus, result] = useSearchItemForEmojiMutation();
const [onURLChange, _resetURL, { url }] = useTextInput("url");
@@ -121,7 +123,7 @@ function CopyEmojiForm({ localEmojiCodes, type, emojiList }) {
const [formSubmit, result] = useFormSubmit(
form,
- query.usePatchRemoteEmojisMutation(),
+ usePatchRemoteEmojisMutation(),
{
changedOnly: false,
onFinish: ({ data }) => {
diff --git a/web/source/settings/admin/federation/detail.js b/web/source/settings/admin/federation/detail.js
deleted file mode 100644
index 7bdee66cf..000000000
--- a/web/source/settings/admin/federation/detail.js
+++ /dev/null
@@ -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>
- );
-} \ No newline at end of file
diff --git a/web/source/settings/admin/federation/import-export/index.jsx b/web/source/settings/admin/federation/import-export/index.jsx
deleted file mode 100644
index bff14b939..000000000
--- a/web/source/settings/admin/federation/import-export/index.jsx
+++ /dev/null
@@ -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);
- }}>
- &lt; 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>
- );
-}; \ No newline at end of file
diff --git a/web/source/settings/admin/federation/overview.js b/web/source/settings/admin/federation/overview.js
deleted file mode 100644
index c09289284..000000000
--- a/web/source/settings/admin/federation/overview.js
+++ /dev/null
@@ -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>
- </>
- );
-}; \ No newline at end of file
diff --git a/web/source/settings/admin/reports/detail.jsx b/web/source/settings/admin/reports/detail.jsx
index 6b85872c4..d686b92bd 100644
--- a/web/source/settings/admin/reports/detail.jsx
+++ b/web/source/settings/admin/reports/detail.jsx
@@ -20,19 +20,21 @@
const React = require("react");
const { useRoute, Redirect } = require("wouter");
-const query = require("../../lib/query");
-
-const FormWithData = require("../../lib/form/form-with-data");
+const FormWithData = require("../../lib/form/form-with-data").default;
const BackButton = require("../../components/back-button");
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 MutationButton = require("../../components/form/mutation-button");
const Username = require("./username");
const { useBaseUrl } = require("../../lib/navigation/util");
+const {
+ useGetReportQuery,
+ useResolveReportMutation,
+} = require("../../lib/query/admin/reports");
module.exports = function ReportDetail({ }) {
const baseUrl = useBaseUrl();
@@ -46,7 +48,7 @@ module.exports = function ReportDetail({ }) {
<BackButton to={baseUrl} /> Report Details
</h1>
<FormWithData
- dataQuery={query.useGetReportQuery}
+ dataQuery={useGetReportQuery}
queryArg={params.reportId}
DataForm={ReportDetailForm}
/>
@@ -115,7 +117,7 @@ function ReportActionForm({ report }) {
comment: useTextInput("action_taken_comment")
};
- const [submit, result] = useFormSubmit(form, query.useResolveReportMutation(), { changedOnly: false });
+ const [submit, result] = useFormSubmit(form, useResolveReportMutation(), { changedOnly: false });
return (
<form onSubmit={submit} className="info-block">
diff --git a/web/source/settings/admin/reports/index.jsx b/web/source/settings/admin/reports/index.jsx
index 2f7a09517..5ffbfd3a0 100644
--- a/web/source/settings/admin/reports/index.jsx
+++ b/web/source/settings/admin/reports/index.jsx
@@ -20,13 +20,12 @@
const React = require("react");
const { Link, Switch, Route } = require("wouter");
-const query = require("../../lib/query");
-
-const FormWithData = require("../../lib/form/form-with-data");
+const FormWithData = require("../../lib/form/form-with-data").default;
const ReportDetail = require("./detail");
const Username = require("./username");
const { useBaseUrl } = require("../../lib/navigation/util");
+const { useListReportsQuery } = require("../../lib/query/admin/reports");
module.exports = function Reports({ baseUrl }) {
return (
@@ -51,7 +50,7 @@ function ReportOverview({ }) {
</p>
</div>
<FormWithData
- dataQuery={query.useListReportsQuery}
+ dataQuery={useListReportsQuery}
DataForm={ReportsList}
/>
</>
diff --git a/web/source/settings/admin/settings/index.jsx b/web/source/settings/admin/settings/index.jsx
index 5ea227fb1..c0da83a2a 100644
--- a/web/source/settings/admin/settings/index.jsx
+++ b/web/source/settings/admin/settings/index.jsx
@@ -19,14 +19,12 @@
const React = require("react");
-const query = require("../../lib/query");
-
const {
useTextInput,
useFileInput
} = require("../../lib/form");
-const useFormSubmit = require("../../lib/form/submit");
+const useFormSubmit = require("../../lib/form/submit").default;
const {
TextInput,
@@ -34,13 +32,16 @@ const {
FileInput
} = 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 { useInstanceV1Query } = require("../../lib/query");
+const { useUpdateInstanceMutation } = require("../../lib/query/admin");
+
module.exports = function AdminSettings() {
return (
<FormWithData
- dataQuery={query.useInstanceQuery}
+ dataQuery={useInstanceV1Query}
DataForm={AdminSettingsForm}
/>
);
@@ -61,7 +62,7 @@ function AdminSettingsForm({ data: instance }) {
terms: useTextInput("terms", { source: instance })
};
- const [submitForm, result] = useFormSubmit(form, query.useUpdateInstanceMutation());
+ const [submitForm, result] = useFormSubmit(form, useUpdateInstanceMutation());
return (
<form onSubmit={submitForm}>
diff --git a/web/source/settings/admin/settings/rules.jsx b/web/source/settings/admin/settings/rules.jsx
index 7c78e6601..4280ccea7 100644
--- a/web/source/settings/admin/settings/rules.jsx
+++ b/web/source/settings/admin/settings/rules.jsx
@@ -21,11 +21,11 @@ const React = require("react");
const { Switch, Route, Link, Redirect, useRoute } = require("wouter");
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 { 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 MutationButton = require("../../components/form/mutation-button");
diff --git a/web/source/settings/components/authorization/index.tsx b/web/source/settings/components/authorization/index.tsx
index 321bb03eb..22a0d24b7 100644
--- a/web/source/settings/components/authorization/index.tsx
+++ b/web/source/settings/components/authorization/index.tsx
@@ -25,6 +25,7 @@ import React from "react";
import Login from "./login";
import Loading from "../loading";
import { Error } from "../error";
+import { NoArg } from "../../lib/types/query";
export function Authorization({ App }) {
const { loginState, expectingRedirect } = store.getState().oauth;
@@ -35,15 +36,15 @@ export function Authorization({ App }) {
isSuccess,
data: account,
error,
- } = useVerifyCredentialsQuery(null, { skip: skip });
+ } = useVerifyCredentialsQuery(NoArg, { skip: skip });
let showLogin = true;
- let content = null;
+ let content: React.JSX.Element | null = null;
if (isLoading) {
showLogin = false;
- let loadingInfo;
+ let loadingInfo = "";
if (loginState == "callback") {
loadingInfo = "Processing OAUTH callback.";
} else if (loginState == "login") {
diff --git a/web/source/settings/components/authorization/login.tsx b/web/source/settings/components/authorization/login.tsx
index 76bfccf43..870e9c343 100644
--- a/web/source/settings/components/authorization/login.tsx
+++ b/web/source/settings/components/authorization/login.tsx
@@ -22,26 +22,21 @@ import React from "react";
import { useAuthorizeFlowMutation } from "../../lib/query/oauth";
import { useTextInput, useValue } from "../../lib/form";
import useFormSubmit from "../../lib/form/submit";
-import { TextInput } from "../form/inputs";
import MutationButton from "../form/mutation-button";
import Loading from "../loading";
+import { TextInput } from "../form/inputs";
export default function Login({ }) {
const form = {
instance: useTextInput("instance", {
defaultValue: window.location.origin
}),
- scopes: useValue("scopes", "user admin")
+ scopes: useValue("scopes", "user admin"),
};
- const [formSubmit, result] = useFormSubmit(
- form,
- useAuthorizeFlowMutation(),
- {
- changedOnly: false,
- onFinish: undefined,
- }
- );
+ const [formSubmit, result] = useFormSubmit(form, useAuthorizeFlowMutation(), {
+ changedOnly: false,
+ });
if (result.isLoading) {
return (
diff --git a/web/source/settings/components/check-list.jsx b/web/source/settings/components/check-list.tsx
index de42a56a5..aec57e758 100644
--- a/web/source/settings/components/check-list.jsx
+++ b/web/source/settings/components/check-list.tsx
@@ -17,21 +17,31 @@
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 (
<div className="checkbox-list list">
<CheckListHeader toggleAll={field.toggleAll}> {header}</CheckListHeader>
<CheckListEntries
- entries={field.value}
+ entries={field.value ?? {}}
updateValue={field.onChange}
EntryComponent={EntryComponent}
getExtraProps={getExtraProps}
/>
</div>
);
-};
+}
function CheckListHeader({ toggleAll, children }) {
return (
@@ -45,9 +55,16 @@ function CheckListHeader({ toggleAll, children }) {
);
}
-const CheckListEntries = React.memo(
- function CheckListEntries({ entries, updateValue, EntryComponent, getExtraProps }) {
- const deferredEntries = React.useDeferredValue(entries);
+interface CheckListEntriesProps {
+ entries: { [_: string]: Checkable },
+ updateValue,
+ EntryComponent,
+ getExtraProps,
+}
+
+const CheckListEntries = memo(
+ function CheckListEntries({ entries, updateValue, EntryComponent, getExtraProps }: CheckListEntriesProps) {
+ const deferredEntries = useDeferredValue(entries);
return Object.values(deferredEntries).map((entry) => (
<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
when it's props actually change, instead of every time anything
in the list (CheckListEntries) updates
*/
-const CheckListEntry = React.memo(
- function CheckListEntry({ entry, updateValue, getExtraProps, EntryComponent }) {
- const onChange = React.useCallback(
+const CheckListEntry = memo(
+ function CheckListEntry({ entry, updateValue, getExtraProps, EntryComponent }: CheckListEntryProps) {
+ const onChange = useCallback(
(value) => updateValue(entry.key, value),
[updateValue, entry.key]
);
- const extraProps = React.useMemo(() => getExtraProps?.(entry), [getExtraProps, entry]);
+ const extraProps = useMemo(() => getExtraProps?.(entry), [getExtraProps, entry]);
return (
<label className="entry">
diff --git a/web/source/settings/components/form/inputs.jsx b/web/source/settings/components/form/inputs.tsx
index f7a6beeda..1e0d8eaab 100644
--- a/web/source/settings/components/form/inputs.jsx
+++ b/web/source/settings/components/form/inputs.tsx
@@ -17,9 +17,28 @@
along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
-const React = require("react");
+import React from "react";
+
+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;
+}
-function TextInput({ label, field, ...inputProps }) {
+export function TextInput({label, field, ...props}: TextInputProps) {
const { onChange, value, ref } = field;
return (
@@ -27,16 +46,25 @@ function TextInput({ label, field, ...inputProps }) {
<label>
{label}
<input
- type="text"
- {...{ onChange, value, ref }}
- {...inputProps}
+ onChange={onChange}
+ value={value}
+ ref={ref as RefObject<HTMLInputElement>}
+ {...props}
/>
</label>
</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;
return (
@@ -44,16 +72,25 @@ function TextArea({ label, field, ...inputProps }) {
<label>
{label}
<textarea
- type="text"
- {...{ onChange, value, ref }}
- {...inputProps}
+ onChange={onChange}
+ value={value}
+ ref={ref as RefObject<HTMLTextAreaElement>}
+ {...props}
/>
</label>
</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;
return (
@@ -66,15 +103,16 @@ function FileInput({ label, field, ...inputProps }) {
<input
type="file"
className="hidden"
- {...{ onChange, ref }}
- {...inputProps}
+ onChange={onChange}
+ ref={ref ? ref as RefObject<HTMLInputElement> : undefined}
+ {...props}
/>
</label>
</div>
);
}
-function Checkbox({ label, field, ...inputProps }) {
+export function Checkbox({ label, field, ...inputProps }) {
const { onChange, value } = field;
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;
return (
<div className="form-field select">
<label>
- {label} {children}
+ {label}
+ {children}
<select
- {...{ onChange, value, ref }}
- {...inputProps}
+ onChange={onChange}
+ value={value}
+ ref={ref as RefObject<HTMLSelectElement>}
+ {...props}
>
{options}
</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 (
<div className="form-field radio">
{Object.entries(field.options).map(([value, radioLabel]) => (
@@ -120,7 +179,7 @@ function RadioGroup({ field, label, ...inputProps }) {
value={value}
checked={field.value == value}
onChange={field.onChange}
- {...inputProps}
+ {...props}
/>
{radioLabel}
</label>
@@ -129,12 +188,3 @@ function RadioGroup({ field, label, ...inputProps }) {
</div>
);
}
-
-module.exports = {
- TextInput,
- TextArea,
- FileInput,
- Checkbox,
- Select,
- RadioGroup
-}; \ No newline at end of file
diff --git a/web/source/settings/components/user-logout-card.jsx b/web/source/settings/components/user-logout-card.jsx
index de77f0485..9d88642a5 100644
--- a/web/source/settings/components/user-logout-card.jsx
+++ b/web/source/settings/components/user-logout-card.jsx
@@ -18,15 +18,17 @@
*/
const React = require("react");
-
-const query = require("../lib/query");
-
const Loading = require("./loading");
+const {
+ useVerifyCredentialsQuery,
+ useLogoutMutation,
+} = require("../lib/query/oauth");
+const { useInstanceV1Query } = require("../lib/query");
module.exports = function UserLogoutCard() {
- const { data: profile, isLoading } = query.useVerifyCredentialsQuery();
- const { data: instance } = query.useInstanceQuery();
- const [logoutQuery] = query.useLogoutMutation();
+ const { data: profile, isLoading } = useVerifyCredentialsQuery();
+ const { data: instance } = useInstanceV1Query();
+ const [logoutQuery] = useLogoutMutation();
if (isLoading) {
return <Loading />;
diff --git a/web/source/settings/index.js b/web/source/settings/index.js
index bc3a925c2..57c89be6f 100644
--- a/web/source/settings/index.js
+++ b/web/source/settings/index.js
@@ -30,6 +30,9 @@ const Loading = require("./components/loading");
const UserLogoutCard = require("./components/user-logout-card");
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");
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("Accounts", { icon: "fa-users", wildcard: true }, require("./admin/accounts")),
- Menu("Federation", { icon: "fa-hubzilla" }, [
- Item("Federation", { icon: "fa-hubzilla", url: "", wildcard: true }, require("./admin/federation")),
- Item("Import/Export", { icon: "fa-floppy-o", wildcard: true }, require("./admin/federation/import-export")),
- ])
+ Menu("Domain Permissions", { icon: "fa-hubzilla" }, [
+ Item("Blocks", { icon: "fa-close", url: "block", wildcard: true }, DomainPerms),
+ 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", {
url: "admin",
diff --git a/web/source/settings/lib/form/bool.jsx b/web/source/settings/lib/form/bool.tsx
index 47a4bbd1b..815b17bd3 100644
--- a/web/source/settings/lib/form/bool.jsx
+++ b/web/source/settings/lib/form/bool.tsx
@@ -17,11 +17,19 @@
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;
-module.exports = function useBoolInput({ name, Name }, { initialValue = _default }) {
- const [value, setValue] = React.useState(initialValue);
+export default function useBoolInput(
+ { name, Name }: CreateHookNames,
+ { initialValue = _default }: HookOpts<boolean>
+): BoolFormInputHook {
+ const [value, setValue] = useState(initialValue);
function onChange(e) {
setValue(e.target.checked);
@@ -41,6 +49,7 @@ module.exports = function useBoolInput({ name, Name }, { initialValue = _default
}
], {
name,
+ Name: "",
onChange,
reset,
value,
@@ -48,4 +57,4 @@ module.exports = function useBoolInput({ name, Name }, { initialValue = _default
hasChanged: () => value != initialValue,
_default
});
-}; \ No newline at end of file
+}
diff --git a/web/source/settings/lib/form/check-list.jsx b/web/source/settings/lib/form/check-list.tsx
index 2f649dba6..c08e5022f 100644
--- a/web/source/settings/lib/form/check-list.jsx
+++ b/web/source/settings/lib/form/check-list.tsx
@@ -17,37 +17,58 @@
along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
-const React = require("react");
-const syncpipe = require("syncpipe");
-const { createSlice } = require("@reduxjs/toolkit");
-const { enableMapSet } = require("immer");
+import {
+ useReducer,
+ useRef,
+ useEffect,
+ useCallback,
+ useMemo,
+} from "react";
+
+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>,
+}
-enableMapSet(); // for use in reducers
+const initialState: ChecklistState = {
+ entries: {},
+ selectedEntries: new Set(),
+};
const { reducer, actions } = createSlice({
name: "checklist",
- initialState: {}, // not handled by slice itself
+ initialState, // not handled by slice itself
reducers: {
- updateAll: (state, { payload: checked }) => {
- const selectedEntries = new Set();
- return {
- entries: syncpipe(state.entries, [
- (_) => Object.values(_),
- (_) => _.map((entry) => {
- if (checked) {
- selectedEntries.add(entry.key);
- }
- return [entry.key, {
- ...entry,
- checked
- }];
- }),
- (_) => Object.fromEntries(_)
- ]),
- selectedEntries
- };
+ updateAll: (state, { payload: checked }: PayloadAction<boolean>) => {
+ const selectedEntries = new Set<string>();
+ const entries = Object.fromEntries(
+ Object.values(state.entries).map((entry) => {
+ if (checked) {
+ // Cheekily add this to selected
+ // entries while we're here.
+ selectedEntries.add(entry.key);
+ }
+
+ return [entry.key, { ...entry, checked } ];
+ })
+ );
+
+ return { entries, selectedEntries };
},
- update: (state, { payload: { key, value } }) => {
+ update: (state, { payload: { key, value } }: PayloadAction<{key: string, value: Checkable}>) => {
if (value.checked !== undefined) {
if (value.checked === true) {
state.selectedEntries.add(key);
@@ -61,7 +82,7 @@ const { reducer, actions } = createSlice({
...value
};
},
- updateMultiple: (state, { payload }) => {
+ updateMultiple: (state, { payload }: PayloadAction<Array<[key: string, value: Checkable]>>) => {
payload.forEach(([key, value]) => {
if (value.checked !== undefined) {
if (value.checked === true) {
@@ -80,43 +101,57 @@ const { reducer, actions } = createSlice({
}
});
-function initialState({ entries, uniqueKey, initialValue }) {
- const selectedEntries = new Set();
- return {
- entries: syncpipe(entries, [
- (_) => _.map((entry) => {
- let key = entry[uniqueKey];
- let checked = entry.checked ?? initialValue;
-
- if (checked) {
- selectedEntries.add(key);
- } else {
- selectedEntries.delete(key);
- }
+function initialHookState({
+ 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 [
- key,
- {
- ...entry,
- key,
- checked
- }
- ];
- }),
- (_) => Object.fromEntries(_)
- ]),
+ return {
+ entries: mappedEntries,
selectedEntries
};
}
-module.exports = function useCheckListInput({ name }, { entries, uniqueKey = "key", initialValue = false }) {
- const [state, dispatch] = React.useReducer(reducer, null,
- () => initialState({ entries, uniqueKey, initialValue }) // initial state
+const _default: { [k: string]: Checkable } = {};
+
+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) {
let some = state.selectedEntries.size > 0;
let all = false;
@@ -130,22 +165,22 @@ module.exports = function useCheckListInput({ name }, { entries, uniqueKey = "ke
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [state.selectedEntries]);
- const reset = React.useCallback(
+ const reset = useCallback(
() => dispatch(actions.updateAll(initialValue)),
[initialValue]
);
- const onChange = React.useCallback(
+ const onChange = useCallback(
(key, value) => dispatch(actions.update({ key, value })),
[]
);
- const updateMultiple = React.useCallback(
+ const updateMultiple = useCallback(
(entries) => dispatch(actions.updateMultiple(entries)),
[]
);
- return React.useMemo(() => {
+ return useMemo(() => {
function toggleAll(e) {
let checked = e.target.checked;
if (e.target.indeterminate) {
@@ -165,7 +200,10 @@ module.exports = function useCheckListInput({ name }, { entries, uniqueKey = "ke
reset,
{ name }
], {
+ _default,
+ hasChanged: () => true,
name,
+ Name: "",
value: state.entries,
onChange,
selectedValues,
@@ -178,4 +216,4 @@ module.exports = function useCheckListInput({ name }, { entries, uniqueKey = "ke
}
});
}, [state, reset, name, onChange, updateMultiple]);
-}; \ No newline at end of file
+}
diff --git a/web/source/settings/lib/form/combo-box.jsx b/web/source/settings/lib/form/combo-box.tsx
index 985c262d8..e558d298a 100644
--- a/web/source/settings/lib/form/combo-box.jsx
+++ b/web/source/settings/lib/form/combo-box.tsx
@@ -17,13 +17,21 @@
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 = "";
-module.exports = function useComboBoxInput({ name, Name }, { initialValue = _default }) {
- const [isNew, setIsNew] = React.useState(false);
+export default function useComboBoxInput(
+ { name, Name }: CreateHookNames,
+ { initialValue = _default }: HookOpts<string>
+): ComboboxFormInputHook {
+ const [isNew, setIsNew] = useState(false);
const state = useComboboxState({
defaultValue: initialValue,
@@ -45,14 +53,15 @@ module.exports = function useComboBoxInput({ name, Name }, { initialValue = _def
[`set${Name}IsNew`]: setIsNew
}
], {
+ reset,
name,
+ Name: "", // Will be set by inputHook function.
state,
value: state.value,
- setter: (val) => state.setValue(val),
+ setter: (val: string) => state.setValue(val),
hasChanged: () => state.value != initialValue,
isNew,
setIsNew,
- reset,
_default
});
-}; \ No newline at end of file
+}
diff --git a/web/source/settings/lib/form/field-array.jsx b/web/source/settings/lib/form/field-array.tsx
index f2d7bc7ce..275bf2b1b 100644
--- a/web/source/settings/lib/form/field-array.jsx
+++ b/web/source/settings/lib/form/field-array.tsx
@@ -17,12 +17,19 @@
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) {
- const fields = [];
+import type {
+ CreateHookNames,
+ HookOpts,
+ FieldArrayInputHook,
+ HookedForm,
+} from "./types";
+
+function parseFields(entries: HookedForm[], length: number): HookedForm[] {
+ const fields: HookedForm[] = [];
for (let i = 0; i < length; i++) {
if (entries[i] != undefined) {
@@ -35,23 +42,38 @@ function parseFields(entries, length) {
return fields;
}
-module.exports = function useArrayInput({ name, _Name }, { initialValue, length = 0 }) {
- const fields = React.useRef({});
+export default function useArrayInput(
+ { name }: CreateHookNames,
+ {
+ initialValue,
+ length = 0,
+ }: HookOpts,
+): FieldArrayInputHook {
+ const _default: HookedForm[] = Array(length);
+ const fields = useRef<HookedForm[]>(_default);
+
+ const value = useMemo(
+ () => parseFields(initialValue, length),
+ [initialValue, length],
+ );
- const value = React.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 {
+ _default,
name,
+ Name: "",
value,
ctx: fields.current,
maxLength: length,
+ hasChanged: hasUpdate,
selectedValues() {
- // if any form field changed, we need to re-send everything
- const hasUpdate = Object.values(fields.current).some((fieldSet) => {
- const { updatedFields } = getFormMutations(fieldSet, { changedOnly: true });
- return updatedFields.length > 0;
- });
- if (hasUpdate) {
+ if (hasUpdate()) {
return Object.values(fields.current).map((fieldSet) => {
return getFormMutations(fieldSet, { changedOnly: false }).mutationData;
});
@@ -60,4 +82,4 @@ module.exports = function useArrayInput({ name, _Name }, { initialValue, length
}
}
};
-}; \ No newline at end of file
+}
diff --git a/web/source/settings/lib/form/file.jsx b/web/source/settings/lib/form/file.tsx
index a9e96dc97..944d77ae1 100644
--- a/web/source/settings/lib/form/file.jsx
+++ b/web/source/settings/lib/form/file.tsx
@@ -17,47 +17,67 @@
along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
-const React = require("react");
-const prettierBytes = require("prettier-bytes");
+import React from "react";
-module.exports = function useFileInput({ name, _Name }, {
- withPreview,
- maxSize,
- initialInfo = "no file selected"
-} = {}) {
- const [file, setFile] = React.useState();
- const [imageURL, setImageURL] = React.useState();
- const [info, setInfo] = React.useState();
+import { useState } from "react";
+import prettierBytes from "prettier-bytes";
- function onChange(e) {
- let file = e.target.files[0];
- setFile(file);
+import type {
+ CreateHookNames,
+ HookOpts,
+ FileFormInputHook,
+} from "./types";
- URL.revokeObjectURL(imageURL);
+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>();
- if (file != undefined) {
- if (withPreview) {
- setImageURL(URL.createObjectURL(file));
- }
+ function onChange(e: React.ChangeEvent<HTMLInputElement>) {
+ const files = e.target.files;
+ if (!files) {
+ setInfo(undefined);
+ return;
+ }
- let size = prettierBytes(file.size);
- if (maxSize && file.size > maxSize) {
- size = <span className="error-text">{size}</span>;
- }
+ let file = files[0];
+ setFile(file);
- setInfo(<>
- {file.name} ({size})
- </>);
- } else {
- setInfo();
+ if (imageURL) {
+ URL.revokeObjectURL(imageURL);
}
+
+ 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() {
- URL.revokeObjectURL(imageURL);
- setImageURL();
- setFile();
- setInfo();
+ if (imageURL) {
+ URL.revokeObjectURL(imageURL);
+ }
+ setImageURL(undefined);
+ setFile(undefined);
+ setInfo(undefined);
}
const infoComponent = (
@@ -82,9 +102,11 @@ module.exports = function useFileInput({ name, _Name }, {
onChange,
reset,
name,
+ Name: "", // Will be set by inputHook function.
value: file,
previewValue: imageURL,
hasChanged: () => file != undefined,
- infoComponent
+ infoComponent,
+ _default,
});
-}; \ No newline at end of file
+}
diff --git a/web/source/settings/lib/form/form-with-data.jsx b/web/source/settings/lib/form/form-with-data.tsx
index ef05c46c0..70a162fb0 100644
--- a/web/source/settings/lib/form/form-with-data.jsx
+++ b/web/source/settings/lib/form/form-with-data.tsx
@@ -17,14 +17,31 @@
along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
-const React = require("react");
-const { Error } = require("../../components/error");
+/* eslint-disable no-unused-vars */
-const Loading = require("../../components/loading");
+import React from "react";
-// 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
-module.exports = function FormWithData({ dataQuery, DataForm, queryArg, ...formProps }) {
+import { Error } from "../../components/error";
+import Loading from "../../components/loading";
+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);
if (isLoading) {
@@ -38,6 +55,6 @@ module.exports = function FormWithData({ dataQuery, DataForm, queryArg, ...formP
<Error error={error} />
);
} else {
- return <DataForm data={data} {...formProps} />;
+ return <DataForm data={data} {...props} />;
}
-}; \ No newline at end of file
+}
diff --git a/web/source/settings/lib/form/get-form-mutations.js b/web/source/settings/lib/form/get-form-mutations.ts
index b0ae6e9b0..6e1bfa02d 100644
--- a/web/source/settings/lib/form/get-form-mutations.js
+++ b/web/source/settings/lib/form/get-form-mutations.ts
@@ -17,29 +17,31 @@
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 {
updatedFields,
- mutationData: syncpipe(form, [
- (_) => 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(_)
- ])
+ mutationData: Object.fromEntries(mutationData),
};
-}; \ No newline at end of file
+}
diff --git a/web/source/settings/lib/form/index.js b/web/source/settings/lib/form/index.js
deleted file mode 100644
index 99537ae7f..000000000
--- a/web/source/settings/lib/form/index.js
+++ /dev/null
@@ -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
- };
- }
-}; \ No newline at end of file
diff --git a/web/source/settings/lib/form/index.ts b/web/source/settings/lib/form/index.ts
new file mode 100644
index 000000000..20de33eda
--- /dev/null
+++ b/web/source/settings/lib/form/index.ts
@@ -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>;
diff --git a/web/source/settings/lib/form/radio.jsx b/web/source/settings/lib/form/radio.tsx
index 4bb061f4b..164abab9d 100644
--- a/web/source/settings/lib/form/radio.jsx
+++ b/web/source/settings/lib/form/radio.tsx
@@ -17,11 +17,18 @@
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 = "";
-module.exports = function useRadioInput({ name, Name }, { initialValue = _default, options }) {
- const [value, setValue] = React.useState(initialValue);
+export default function useRadioInput(
+ { name, Name }: CreateHookNames,
+ {
+ initialValue = _default,
+ options = {},
+ }: HookOpts<string>
+): RadioFormInputHook {
+ const [value, setValue] = useState(initialValue);
function onChange(e) {
setValue(e.target.value);
@@ -40,13 +47,14 @@ module.exports = function useRadioInput({ name, Name }, { initialValue = _defaul
[`set${Name}`]: setValue
}
], {
- name,
onChange,
reset,
+ name,
+ Name: "",
value,
setter: setValue,
options,
hasChanged: () => value != initialValue,
_default
});
-}; \ No newline at end of file
+}
diff --git a/web/source/settings/lib/form/submit.js b/web/source/settings/lib/form/submit.js
deleted file mode 100644
index ab2945812..000000000
--- a/web/source/settings/lib/form/submit.js
+++ /dev/null
@@ -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
- }
- ];
-}; \ No newline at end of file
diff --git a/web/source/settings/lib/form/submit.ts b/web/source/settings/lib/form/submit.ts
new file mode 100644
index 000000000..d5636a587
--- /dev/null
+++ b/web/source/settings/lib/form/submit.ts
@@ -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
+ }
+ ];
+}
diff --git a/web/source/settings/lib/form/text.jsx b/web/source/settings/lib/form/text.tsx
index f9c096ac8..c0b9b93c6 100644
--- a/web/source/settings/lib/form/text.jsx
+++ b/web/source/settings/lib/form/text.tsx
@@ -17,26 +17,40 @@
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 = "";
-module.exports = function useTextInput({ name, Name }, {
- initialValue = _default,
- dontReset = false,
- validator,
- showValidation = true,
- initValidation
-} = {}) {
- const [text, setText] = React.useState(initialValue);
- const textRef = React.useRef(null);
+export default function useTextInput(
+ { 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 [_isValidating, startValidation] = React.useTransition();
- let valid = validation == "";
+ const [validation, setValidation] = useState(initValidation ?? "");
+ const [_isValidating, startValidation] = useTransition();
+ const valid = validation == "";
- function onChange(e) {
- let input = e.target.value;
+ function onChange(e: React.ChangeEvent<HTMLInputElement>) {
+ const input = e.target.value;
setText(input);
if (validator) {
@@ -52,7 +66,7 @@ module.exports = function useTextInput({ name, Name }, {
}
}
- React.useEffect(() => {
+ useEffect(() => {
if (validator && textRef.current) {
if (showValidation) {
textRef.current.setCustomValidity(validation);
@@ -76,12 +90,13 @@ module.exports = function useTextInput({ name, Name }, {
onChange,
reset,
name,
+ Name: "", // Will be set by inputHook function.
value: text,
ref: textRef,
setter: setText,
valid,
- validate: () => setValidation(validator(text)),
+ validate: () => setValidation(validator ? validator(text): ""),
hasChanged: () => text != initialValue,
_default
});
-}; \ No newline at end of file
+}
diff --git a/web/source/settings/lib/form/types.ts b/web/source/settings/lib/form/types.ts
new file mode 100644
index 000000000..45db9e0b8
--- /dev/null
+++ b/web/source/settings/lib/form/types.ts
@@ -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;
+}
diff --git a/web/source/settings/lib/query/admin/custom-emoji.js b/web/source/settings/lib/query/admin/custom-emoji.js
deleted file mode 100644
index 6e7c772a2..000000000
--- a/web/source/settings/lib/query/admin/custom-emoji.js
+++ /dev/null
@@ -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
- };
-} \ No newline at end of file
diff --git a/web/source/settings/lib/query/admin/custom-emoji/index.ts b/web/source/settings/lib/query/admin/custom-emoji/index.ts
new file mode 100644
index 000000000..d624b0580
--- /dev/null
+++ b/web/source/settings/lib/query/admin/custom-emoji/index.ts
@@ -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,
+};
diff --git a/web/source/settings/lib/query/admin/domain-permissions/export.ts b/web/source/settings/lib/query/admin/domain-permissions/export.ts
new file mode 100644
index 000000000..b6ef560f4
--- /dev/null
+++ b/web/source/settings/lib/query/admin/domain-permissions/export.ts
@@ -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 };
diff --git a/web/source/settings/lib/query/admin/domain-permissions/get.ts b/web/source/settings/lib/query/admin/domain-permissions/get.ts
new file mode 100644
index 000000000..3e27742d4
--- /dev/null
+++ b/web/source/settings/lib/query/admin/domain-permissions/get.ts
@@ -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,
+};
diff --git a/web/source/settings/lib/query/admin/domain-permissions/import.ts b/web/source/settings/lib/query/admin/domain-permissions/import.ts
new file mode 100644
index 000000000..dde488625
--- /dev/null
+++ b/web/source/settings/lib/query/admin/domain-permissions/import.ts
@@ -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,
+};
diff --git a/web/source/settings/lib/query/admin/domain-permissions/process.ts b/web/source/settings/lib/query/admin/domain-permissions/process.ts
new file mode 100644
index 000000000..017d02bb4
--- /dev/null
+++ b/web/source/settings/lib/query/admin/domain-permissions/process.ts
@@ -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 };
diff --git a/web/source/settings/lib/query/admin/domain-permissions/update.ts b/web/source/settings/lib/query/admin/domain-permissions/update.ts
new file mode 100644
index 000000000..a6b4b2039
--- /dev/null
+++ b/web/source/settings/lib/query/admin/domain-permissions/update.ts
@@ -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
+};
diff --git a/web/source/settings/lib/query/admin/import-export.js b/web/source/settings/lib/query/admin/import-export.js
deleted file mode 100644
index 9a04438c2..000000000
--- a/web/source/settings/lib/query/admin/import-export.js
+++ /dev/null
@@ -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];
- }
- });
- };
-} \ No newline at end of file
diff --git a/web/source/settings/lib/query/admin/index.js b/web/source/settings/lib/query/admin/index.js
deleted file mode 100644
index 7a55389d3..000000000
--- a/web/source/settings/lib/query/admin/index.js
+++ /dev/null
@@ -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 }); \ No newline at end of file
diff --git a/web/source/settings/lib/query/admin/index.ts b/web/source/settings/lib/query/admin/index.ts
new file mode 100644
index 000000000..e61179216
--- /dev/null
+++ b/web/source/settings/lib/query/admin/index.ts
@@ -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;
diff --git a/web/source/settings/lib/query/admin/reports.js b/web/source/settings/lib/query/admin/reports.js
deleted file mode 100644
index 1c45bb7bc..000000000
--- a/web/source/settings/lib/query/admin/reports.js
+++ /dev/null
@@ -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" }]
- })
-}); \ No newline at end of file
diff --git a/web/source/settings/lib/query/admin/reports/index.ts b/web/source/settings/lib/query/admin/reports/index.ts
new file mode 100644
index 000000000..253e8238c
--- /dev/null
+++ b/web/source/settings/lib/query/admin/reports/index.ts
@@ -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,
+};
diff --git a/web/source/settings/lib/query/gts-api.ts b/web/source/settings/lib/query/gts-api.ts
index 9e043137c..a07f5ff1e 100644
--- a/web/source/settings/lib/query/gts-api.ts
+++ b/web/source/settings/lib/query/gts-api.ts
@@ -26,6 +26,7 @@ import type {
import { serialize as serializeForm } from "object-to-formdata";
import type { RootState } from '../../redux/store';
+import { InstanceV1 } from '../types/instance';
/**
* GTSFetchArgs extends standard FetchArgs used by
@@ -72,7 +73,7 @@ const gtsBaseQuery: BaseQueryFn<
const { instanceUrl, token } = state.oauth;
// Derive baseUrl dynamically.
- let baseUrl: string;
+ let baseUrl: string | undefined;
// Check if simple string baseUrl provided
// as args, or if more complex args provided.
@@ -137,8 +138,8 @@ export const gtsApi = createApi({
"Account",
"InstanceRules",
],
- endpoints: (builder) => ({
- instance: builder.query<any, void>({
+ endpoints: (build) => ({
+ instanceV1: build.query<InstanceV1, void>({
query: () => ({
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 };
diff --git a/web/source/settings/lib/query/lib.js b/web/source/settings/lib/query/lib.js
deleted file mode 100644
index 1025ca3a7..000000000
--- a/web/source/settings/lib/query/lib.js
+++ /dev/null
@@ -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 });
- }));
- });
- }
- };
- };
-} \ No newline at end of file
diff --git a/web/source/settings/lib/query/oauth/index.ts b/web/source/settings/lib/query/oauth/index.ts
index 9af2dd5fb..f62a29596 100644
--- a/web/source/settings/lib/query/oauth/index.ts
+++ b/web/source/settings/lib/query/oauth/index.ts
@@ -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
const extended = gtsApi.injectEndpoints({
- endpoints: (builder) => ({
- verifyCredentials: builder.query<any, void>({
+ endpoints: (build) => ({
+ verifyCredentials: build.query<any, void>({
providesTags: (_res, error) =>
error == undefined ? ["Auth"] : [],
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) {
const state = api.getState() as RootState;
const oauthState = state.oauth;
@@ -187,7 +187,7 @@ const extended = gtsApi.injectEndpoints({
return { data: null };
},
}),
- logout: builder.mutation({
+ logout: build.mutation({
queryFn: (_arg, api) => {
api.dispatch(oauthRemove());
return { data: null };
@@ -201,4 +201,4 @@ export const {
useVerifyCredentialsQuery,
useAuthorizeFlowMutation,
useLogoutMutation,
-} = extended; \ No newline at end of file
+} = extended;
diff --git a/web/source/settings/lib/query/query-modifiers.ts b/web/source/settings/lib/query/query-modifiers.ts
new file mode 100644
index 000000000..d6bf0b6ae
--- /dev/null
+++ b/web/source/settings/lib/query/query-modifiers.ts
@@ -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,
+};
diff --git a/web/source/settings/lib/query/transforms.ts b/web/source/settings/lib/query/transforms.ts
new file mode 100644
index 000000000..d915e0b13
--- /dev/null
+++ b/web/source/settings/lib/query/transforms.ts
@@ -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);
+ };
+}
diff --git a/web/source/settings/lib/query/user/index.ts b/web/source/settings/lib/query/user/index.ts
index 751e38e5b..a7cdad2fd 100644
--- a/web/source/settings/lib/query/user/index.ts
+++ b/web/source/settings/lib/query/user/index.ts
@@ -17,12 +17,12 @@
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";
const extended = gtsApi.injectEndpoints({
- endpoints: (builder) => ({
- updateCredentials: builder.mutation({
+ endpoints: (build) => ({
+ updateCredentials: build.mutation({
query: (formData) => ({
method: "PATCH",
url: `/api/v1/accounts/update_credentials`,
@@ -32,7 +32,7 @@ const extended = gtsApi.injectEndpoints({
}),
...replaceCacheOnMutation("verifyCredentials")
}),
- passwordChange: builder.mutation({
+ passwordChange: build.mutation({
query: (data) => ({
method: "POST",
url: `/api/v1/user/password_change`,
diff --git a/web/source/settings/admin/federation/index.js b/web/source/settings/lib/types/custom-emoji.ts
index ec536c0be..f54e9e2a0 100644
--- a/web/source/settings/admin/federation/index.js
+++ b/web/source/settings/lib/types/custom-emoji.ts
@@ -17,25 +17,33 @@
along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
-const React = require("react");
-const { Switch, Route } = require("wouter");
-
-const InstanceOverview = require("./overview");
-const InstanceDetail = require("./detail");
-const InstanceImportExport = require("./import-export");
-
-module.exports = function Federation({ baseUrl }) {
- return (
- <Switch>
- <Route path={`${baseUrl}/import-export/:list?`}>
- <InstanceImportExport />
- </Route>
-
- <Route path={`${baseUrl}/:domain`}>
- <InstanceDetail baseUrl={baseUrl} />
- </Route>
-
- <InstanceOverview baseUrl={baseUrl} />
- </Switch>
- );
-}; \ No newline at end of file
+export interface CustomEmoji {
+ id?: string;
+ shortcode: string;
+ category?: string;
+}
+
+/**
+ * Query parameters for GET to /api/v1/admin/custom_emojis.
+ */
+export interface ListEmojiParams {
+
+}
+
+/**
+ * Result of searchItemForEmoji mutation.
+ */
+export interface EmojisFromItem {
+ /**
+ * Type of the search item result.
+ */
+ type: "statuses" | "accounts";
+ /**
+ * Domain of the returned emojis.
+ */
+ domain: string;
+ /**
+ * Discovered emojis.
+ */
+ list: CustomEmoji[];
+}
diff --git a/web/source/settings/lib/types/domain-permission.ts b/web/source/settings/lib/types/domain-permission.ts
new file mode 100644
index 000000000..f90c8d8a9
--- /dev/null
+++ b/web/source/settings/lib/types/domain-permission.ts
@@ -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";
+}
diff --git a/web/source/settings/lib/types/instance.ts b/web/source/settings/lib/types/instance.ts
new file mode 100644
index 000000000..a0a75366e
--- /dev/null
+++ b/web/source/settings/lib/types/instance.ts
@@ -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;
+}
+
diff --git a/web/source/settings/lib/types/query.ts b/web/source/settings/lib/types/query.ts
new file mode 100644
index 000000000..8e6901b76
--- /dev/null
+++ b/web/source/settings/lib/types/query.ts
@@ -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 }
diff --git a/web/source/settings/lib/types/report.ts b/web/source/settings/lib/types/report.ts
new file mode 100644
index 000000000..bb3d53c27
--- /dev/null
+++ b/web/source/settings/lib/types/report.ts
@@ -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;
+}
diff --git a/web/source/settings/lib/domain-block.js b/web/source/settings/lib/util/domain-permission.ts
index e1cbd4c22..b8dcbc8aa 100644
--- a/web/source/settings/lib/domain-block.js
+++ b/web/source/settings/lib/util/domain-permission.ts
@@ -17,33 +17,32 @@
along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
-const isValidDomain = require("is-valid-domain");
-const psl = require("psl");
+import isValidDomain from "is-valid-domain";
+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, {
- /*
- 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,
allowUnicode: true
});
}
-/*
- Still can't think of a better function name for this,
- but we're checking a domain against the Public Suffix List <https://publicsuffix.org/>
- to see if we should suggest removing subdomain(s) since they're likely owned/ran by the same party
- social.example.com -> suggests example.com
-*/
-function hasBetterScope(domain) {
- const lookup = psl.get(domain);
+/**
+ * Checks a domain against the Public Suffix List <https://publicsuffix.org/> 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".
+ * @param domain
+ * @returns
+ */
+export function hasBetterScope(domain: string): string | undefined {
+ const lookup = get(domain);
if (lookup && lookup != domain) {
return lookup;
- } else {
- return false;
}
}
-
-module.exports = { isValidDomainBlock, hasBetterScope }; \ No newline at end of file
diff --git a/web/source/settings/style.css b/web/source/settings/style.css
index 505e3bbfc..524f5e4ab 100644
--- a/web/source/settings/style.css
+++ b/web/source/settings/style.css
@@ -498,7 +498,7 @@ span.form-info {
}
}
-.instance-list {
+.domain-permissions-list {
p {
margin-top: 0;
}
@@ -612,7 +612,7 @@ span.form-info {
padding: 0.75rem;
}
- .instance-list .filter {
+ .domain-permissions-list .filter {
flex-direction: column;
}
}
@@ -809,7 +809,7 @@ button.with-padding {
animation-fill-mode: forwards;
}
-.suspend-import-list {
+.domain-perm-import-list {
.checkbox-list-wrapper {
overflow-x: auto;
display: grid;
@@ -844,7 +844,7 @@ button.with-padding {
#icon {
align-self: center;
- .already-blocked {
+ .permission-already-exists {
color: $green1;
}
@@ -875,6 +875,12 @@ button.with-padding {
align-items: center;
}
+ .form-field.radio {
+ display: flex;
+ flex-direction: column;
+ margin-left: 0.5rem;
+ }
+
.button-grid {
display: inline-grid;
grid-template-columns: auto auto auto;
diff --git a/web/source/settings/user/profile.js b/web/source/settings/user/profile.js
index 6c26bb406..b6daf175b 100644
--- a/web/source/settings/user/profile.js
+++ b/web/source/settings/user/profile.js
@@ -19,8 +19,6 @@
const React = require("react");
-const query = require("../lib/query");
-
const {
useTextInput,
useFileInput,
@@ -28,7 +26,7 @@ const {
useFieldArrayInput
} = require("../lib/form");
-const useFormSubmit = require("../lib/form/submit");
+const useFormSubmit = require("../lib/form/submit").default;
const { useWithFormContext, FormContext } = require("../lib/form/context");
const {
@@ -38,14 +36,18 @@ const {
Checkbox
} = 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 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() {
return (
<FormWithData
- dataQuery={query.useVerifyCredentialsQuery}
+ dataQuery={useVerifyCredentialsQuery}
DataForm={UserProfileForm}
/>
);
@@ -64,7 +66,7 @@ function UserProfileForm({ data: profile }) {
- string custom_css (if enabled)
*/
- const { data: instance } = query.useInstanceQuery();
+ const { data: instance } = useInstanceV1Query();
const instanceConfig = React.useMemo(() => {
return {
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: () => {
form.avatar.reset();
form.header.reset();
diff --git a/web/source/settings/user/settings.js b/web/source/settings/user/settings.js
index 2a23a87e0..31ea8c39a 100644
--- a/web/source/settings/user/settings.js
+++ b/web/source/settings/user/settings.js
@@ -26,7 +26,7 @@ const {
useBoolInput
} = require("../lib/form");
-const useFormSubmit = require("../lib/form/submit");
+const useFormSubmit = require("../lib/form/submit").default;
const {
Select,
@@ -34,7 +34,7 @@ const {
Checkbox
} = 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 MutationButton = require("../components/form/mutation-button");
diff --git a/web/source/tsconfig.json b/web/source/tsconfig.json
index 2f85c03b2..f8720e2b6 100644
--- a/web/source/tsconfig.json
+++ b/web/source/tsconfig.json
@@ -84,7 +84,7 @@
/* Type Checking */
"strict": false, /* Enable all strict type-checking options. */
// "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. */
// "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. */
@@ -104,6 +104,10 @@
/* Completeness */
// "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" }
+ ]
}
}
diff --git a/web/source/yarn.lock b/web/source/yarn.lock
index bce379dac..b9ff73bb5 100644
--- a/web/source/yarn.lock
+++ b/web/source/yarn.lock
@@ -1040,6 +1040,13 @@
through2 "^4.0.2"
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":
version "4.4.0"
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/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"
resolved "https://registry.yarnpkg.com/@jridgewell/resolve-uri/-/resolve-uri-3.1.1.tgz#c08679063f279615a3326583ba3a90d1d82cc721"
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"
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":
version "0.3.19"
resolved "https://registry.yarnpkg.com/@jridgewell/trace-mapping/-/trace-mapping-0.3.19.tgz#f8a3249862f91be48d3127c3cfe992f79b4b8811"
@@ -1194,6 +1209,31 @@
redux-thunk "^2.4.2"
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":
version "3.3.2"
resolved "https://registry.yarnpkg.com/@types/hoist-non-react-statics/-/hoist-non-react-statics-3.3.2.tgz#dc1e9ded53375d37603c479cc12c693b0878aa2a"
@@ -1209,6 +1249,11 @@
dependencies:
"@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":
version "7.0.13"
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"
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@*":
version "15.7.8"
resolved "https://registry.yarnpkg.com/@types/prop-types/-/prop-types-15.7.8.tgz#805eae6e8f41bd19e88917d2ea200dc992f405d3"
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":
version "18.2.10"
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"
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:
version "7.4.1"
resolved "https://registry.yarnpkg.com/acorn/-/acorn-7.4.1.tgz#feaed255973d2e77555b83dbc08851a6c63520fa"
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"
resolved "https://registry.yarnpkg.com/acorn/-/acorn-8.10.0.tgz#8be5b3907a67221a81ab23c7889c4c5526b62ec5"
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"
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:
version "5.0.1"
resolved "https://registry.yarnpkg.com/ansi-regex/-/ansi-regex-5.0.1.tgz#082cb2c89c9fe8659a311a53bd6a4dc5301db304"
@@ -1818,7 +1887,7 @@ ansi-styles@^3.2.1:
dependencies:
color-convert "^1.9.0"
-ansi-styles@^4.1.0:
+ansi-styles@^4.0.0, ansi-styles@^4.1.0:
version "4.3.0"
resolved "https://registry.yarnpkg.com/ansi-styles/-/ansi-styles-4.3.0.tgz#edd803628ae71c04c85ae7a0906edad34b648937"
integrity sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==
@@ -1838,6 +1907,11 @@ anymatch@^3.1.0, anymatch@~3.1.2:
normalize-path "^3.0.0"
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:
version "2.0.1"
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"
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:
version "2.1.0"
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"
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"
resolved "https://registry.yarnpkg.com/bl/-/bl-4.1.0.tgz#451535264182bec2fbbc83a62ab98cf11d9f7b3a"
integrity sha512-1W07cM9gS6DcLperZfFSj+bWLtaPGSOHWhPiGzXmvVJbRLdG82sH/Kn8EtW1VqWVA54AKf2h5k5BbnIbwF3h6w==
@@ -2343,7 +2422,7 @@ chalk@^2.4.2:
escape-string-regexp "^1.0.5"
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"
resolved "https://registry.yarnpkg.com/chalk/-/chalk-4.1.2.tgz#aac4e2b7734a740867aeb16bf02aad556a1e7a01"
integrity sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==
@@ -2351,6 +2430,11 @@ chalk@^4.0.0, chalk@^4.1.0:
ansi-styles "^4.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:
version "3.5.3"
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"
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:
version "2.2.0"
resolved "https://registry.yarnpkg.com/clone-regexp/-/clone-regexp-2.2.0.tgz#7d65e00885cd8796405c35a737e7a86b7429e36f"
@@ -2381,6 +2482,11 @@ clone-regexp@^2.1.0:
dependencies:
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:
version "1.9.3"
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"
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:
version "2.20.3"
resolved "https://registry.yarnpkg.com/commander/-/commander-2.20.3.tgz#fd485e84c03eb4881c20722ba48035e8531aeb33"
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:
version "0.0.1"
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"
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"
resolved "https://registry.yarnpkg.com/core-util-is/-/core-util-is-1.0.3.tgz#a6042d3634c2b27e9328f837b965fac83808db85"
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"
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:
version "7.0.3"
resolved "https://registry.yarnpkg.com/cross-spawn/-/cross-spawn-7.0.3.tgz#f73a85b9d5d41d045551c177e2882d4ac85728a6"
@@ -2658,6 +2785,13 @@ default-value@^1.0.0:
dependencies:
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:
version "1.1.0"
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"
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:
version "5.0.3"
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"
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:
version "0.1.4"
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-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:
version "3.0.0"
resolved "https://registry.yarnpkg.com/emojis-list/-/emojis-list-3.0.0.tgz#5570662046ad29e2e916e71aae260abdff4f6a78"
@@ -3231,6 +3380,15 @@ ext@^1.1.2:
dependencies:
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:
version "2.5.0"
resolved "https://registry.yarnpkg.com/factor-bundle/-/factor-bundle-2.5.0.tgz#8ea8957da39d7586283cc3ee353cd9911a45e779"
@@ -3296,6 +3454,13 @@ faye-websocket@^0.11.3:
dependencies:
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:
version "6.0.1"
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"
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:
version "11.12.0"
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"
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:
version "1.0.0"
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"
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"
resolved "https://registry.yarnpkg.com/iconv-lite/-/iconv-lite-0.4.24.tgz#2022b4b25fbddc21d2f524974a474aafe733908b"
integrity sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA==
@@ -3779,6 +3958,11 @@ inherits@2.0.3:
resolved "https://registry.yarnpkg.com/inherits/-/inherits-2.0.3.tgz#633c2c83e3da42a502f52466022480f4208261de"
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:
version "2.2.0"
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:
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:
version "2.0.0"
resolved "https://registry.yarnpkg.com/insert-css/-/insert-css-2.0.0.tgz#eb5d1097b7542f4c79ea3060d3aee07d053880f4"
@@ -3922,6 +4127,11 @@ is-finalizationregistry@^1.0.2:
dependencies:
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:
version "1.0.10"
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:
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:
version "2.0.2"
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:
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:
version "0.2.1"
resolved "https://registry.yarnpkg.com/is-utf8/-/is-utf8-0.2.1.tgz#4b0da1442104d1b336340e80797e865cf39f7d72"
@@ -4168,6 +4388,11 @@ keyv@^4.5.3:
dependencies:
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:
version "1.0.2"
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"
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:
version "1.4.0"
resolved "https://registry.yarnpkg.com/loose-envify/-/loose-envify-1.4.0.tgz#71ee51fa7be4caec1a63839f7e682d8132d30caf"
@@ -4280,6 +4518,11 @@ magic-string@0.25.1:
dependencies:
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:
version "4.3.0"
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"
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:
version "1.0.1"
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"
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"
resolved "https://registry.yarnpkg.com/minimist/-/minimist-1.2.8.tgz#c1a464e7693302e082a075cee0c057741ac4772c"
integrity sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==
@@ -4452,6 +4700,11 @@ multisplice@^1.0.0:
resolved "https://registry.yarnpkg.com/multisplice/-/multisplice-1.0.0.tgz#e74cf2948dcb51a6c317fc5e22980a652f7830e9"
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:
version "3.3.6"
resolved "https://registry.yarnpkg.com/nanoid/-/nanoid-3.3.6.tgz#443380c856d6e9f9824267d960b4236ad583ea4c"
@@ -4576,6 +4829,13 @@ once@^1.3.0:
dependencies:
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:
version "0.8.3"
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"
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:
version "0.3.0"
resolved "https://registry.yarnpkg.com/os-browserify/-/os-browserify-0.3.0.tgz#854373c7f5c2315914fc9bfc6bd8238fdda1ec27"
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:
version "1.1.1"
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"
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:
version "2.1.0"
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"
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:
version "1.2.0"
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"
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"
resolved "https://registry.yarnpkg.com/resolve/-/resolve-1.22.6.tgz#dd209739eca3aef739c626fea1b4f3c506195362"
integrity sha512-njhxM7mV12JfufShqGy3Rz8j11RPdLy4xi15UurGJeoHLfJpVXKdh3ueuOqbYUcDZnffr6X739JBo5LzyahEsw==
@@ -5218,6 +5511,19 @@ resp-modifier@^6.0.0:
debug "^2.2.0"
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:
version "1.0.4"
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"
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:
version "1.2.0"
resolved "https://registry.yarnpkg.com/run-parallel/-/run-parallel-1.2.0.tgz#66d1368da7bdf921eb9d95bd1a9229e7f21a43ee"
@@ -5250,6 +5561,13 @@ run-parallel@^1.1.9:
dependencies:
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:
version "1.0.1"
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"
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"
resolved "https://registry.yarnpkg.com/semver/-/semver-7.5.4.tgz#483986ec4ed38e1c6c48c34894a9182dbff68a6e"
integrity sha512-1bCSESV6Pv+i21Hvpxp3Dx+pSD8lIPt8uVjRrxAUt/nbswYc+tK6Y2btiULjd4+fnq15PX+nqQDC7Oft7WkwcA==
@@ -5410,6 +5728,11 @@ side-channel@^1.0.4:
get-intrinsic "^1.0.2"
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:
version "1.0.1"
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"
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:
version "4.0.10"
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:
safe-buffer "~5.1.0"
-strip-ansi@^6.0.1:
+strip-ansi@^6.0.0, strip-ansi@^6.0.1:
version "6.0.1"
resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-6.0.1.tgz#9e26c63d30f53443e9489495b2105d37b67a85d9"
integrity sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==
@@ -5790,7 +6122,7 @@ through2@^4.0.2:
dependencies:
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"
resolved "https://registry.yarnpkg.com/through/-/through-2.3.8.tgz#0dd4c9ffaabc357960b1b724115d7e0e86a2e1f5"
integrity sha512-w89qg7PI8wAdvX60bMDP+bFoD5Dvhm9oLheFp5O4a2QF0cSBGsBX4qZmadPMvVqlLJBBci+WqGGOAPvcDeNSVg==
@@ -5814,6 +6146,13 @@ tiny-lr@^2.0.0:
object-assign "^4.1.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:
version "2.0.0"
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"
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:
version "5.0.3"
resolved "https://registry.yarnpkg.com/tsconfig/-/tsconfig-5.0.3.tgz#5f4278e701800967a8fc383fd19648878f2a6e3a"
@@ -5868,6 +6238,11 @@ tsify@^5.0.4:
through2 "^2.0.0"
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:
version "0.0.1"
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"
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:
version "1.6.18"
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"
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:
version "3.0.3"
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"
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:
version "0.0.1"
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"
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:
version "0.7.4"
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"
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:
version "2.0.2"
resolved "https://registry.yarnpkg.com/which/-/which-2.0.2.tgz#7c6a8dd0a636a0327e10b59c9286eee93f3f51b1"
@@ -6190,6 +6599,15 @@ wouter@^2.8.0-alpha.2:
dependencies:
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:
version "1.0.2"
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"
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:
version "0.1.0"
resolved "https://registry.yarnpkg.com/yocto-queue/-/yocto-queue-0.1.0.tgz#0294eb3dee05028d31ee1a5fa2c556a6aaf10a1b"