mirror of
https://github.com/superseriousbusiness/gotosocial.git
synced 2025-11-02 05:02:24 -06:00
[feature] Allow import/export/creation of domain allows via admin panel (#2264)
* it's happening! * aaa * fix silly whoopsie * it's working pa! it's working ma! * model report parameters * shuffle some more stuff around * getting there * oo hoo * finish tidying up for now * aaa * fix use form submit errors * peepee poo poo * aaaaa * ffff * they see me typin', they hatin' * boop * aaa * oooo * typing typing tappa tappa * almost done typing * weee * alright * push it push it real good doo doo doo doo doo doo * thingy no worky * almost done * mutation modifers not quite right * hmm * it works * view blocks + allows nicely * it works! * typia install * the old linterino * linter plz
This commit is contained in:
parent
48725f7228
commit
637f188ebe
77 changed files with 4154 additions and 1690 deletions
|
|
@ -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());
|
||||
|
|
|
|||
254
web/source/settings/admin/domain-permissions/detail.tsx
Normal file
254
web/source/settings/admin/domain-permissions/detail.tsx
Normal file
|
|
@ -0,0 +1,254 @@
|
|||
/*
|
||||
GoToSocial
|
||||
Copyright (C) GoToSocial Authors admin@gotosocial.org
|
||||
SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
|
||||
This program is free software: you can redistribute it and/or modify
|
||||
it under the terms of the GNU Affero General Public License as published by
|
||||
the Free Software Foundation, either version 3 of the License, or
|
||||
(at your option) any later version.
|
||||
|
||||
This program is distributed in the hope that it will be useful,
|
||||
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
GNU Affero General Public License for more details.
|
||||
|
||||
You should have received a copy of the GNU Affero General Public License
|
||||
along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
import React from "react";
|
||||
|
||||
import { useMemo } from "react";
|
||||
import { useLocation } from "wouter";
|
||||
|
||||
import { useTextInput, useBoolInput } from "../../lib/form";
|
||||
|
||||
import useFormSubmit from "../../lib/form/submit";
|
||||
|
||||
import { TextInput, Checkbox, TextArea } from "../../components/form/inputs";
|
||||
|
||||
import Loading from "../../components/loading";
|
||||
import BackButton from "../../components/back-button";
|
||||
import MutationButton from "../../components/form/mutation-button";
|
||||
|
||||
import { useDomainAllowsQuery, useDomainBlocksQuery } from "../../lib/query/admin/domain-permissions/get";
|
||||
import { useAddDomainAllowMutation, useAddDomainBlockMutation, useRemoveDomainAllowMutation, useRemoveDomainBlockMutation } from "../../lib/query/admin/domain-permissions/update";
|
||||
import { DomainPerm, PermType } from "../../lib/types/domain-permission";
|
||||
import { NoArg } from "../../lib/types/query";
|
||||
import { Error } from "../../components/error";
|
||||
|
||||
export interface DomainPermDetailProps {
|
||||
baseUrl: string;
|
||||
permType: PermType;
|
||||
domain: string;
|
||||
}
|
||||
|
||||
export default function DomainPermDetail({ baseUrl, permType, domain }: DomainPermDetailProps) {
|
||||
const { data: domainBlocks = {}, isLoading: isLoadingDomainBlocks } = useDomainBlocksQuery(NoArg, { skip: permType !== "block" });
|
||||
const { data: domainAllows = {}, isLoading: isLoadingDomainAllows } = useDomainAllowsQuery(NoArg, { skip: permType !== "allow" });
|
||||
|
||||
let isLoading;
|
||||
switch (permType) {
|
||||
case "block":
|
||||
isLoading = isLoadingDomainBlocks;
|
||||
break;
|
||||
case "allow":
|
||||
isLoading = isLoadingDomainAllows;
|
||||
break;
|
||||
default:
|
||||
throw "perm type unknown";
|
||||
}
|
||||
|
||||
if (domain == "view") {
|
||||
// Retrieve domain from form field submission.
|
||||
domain = (new URL(document.location.toString())).searchParams.get("domain")?? "unknown";
|
||||
}
|
||||
|
||||
if (domain == "unknown") {
|
||||
throw "unknown domain";
|
||||
}
|
||||
|
||||
// Normalize / decode domain (it may be URL-encoded).
|
||||
domain = decodeURIComponent(domain);
|
||||
|
||||
// Check if we already have a perm of the desired type for this domain.
|
||||
const existingPerm: DomainPerm | undefined = useMemo(() => {
|
||||
if (permType == "block") {
|
||||
return domainBlocks[domain];
|
||||
} else {
|
||||
return domainAllows[domain];
|
||||
}
|
||||
}, [domainBlocks, domainAllows, domain, permType]);
|
||||
|
||||
let infoContent: React.JSX.Element;
|
||||
|
||||
if (isLoading) {
|
||||
infoContent = <Loading />;
|
||||
} else if (existingPerm == undefined) {
|
||||
infoContent = <span>No stored {permType} yet, you can add one below:</span>;
|
||||
} else {
|
||||
infoContent = (
|
||||
<div className="info">
|
||||
<i className="fa fa-fw fa-exclamation-triangle" aria-hidden="true"></i>
|
||||
<b>Editing domain permissions isn't implemented yet, <a href="https://github.com/superseriousbusiness/gotosocial/issues/1198" target="_blank" rel="noopener noreferrer">check here for progress</a></b>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div>
|
||||
<h1 className="text-cutoff"><BackButton to={baseUrl} /> Domain {permType} for: <span title={domain}>{domain}</span></h1>
|
||||
{infoContent}
|
||||
<DomainPermForm
|
||||
defaultDomain={domain}
|
||||
perm={existingPerm}
|
||||
permType={permType}
|
||||
baseUrl={baseUrl}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
interface DomainPermFormProps {
|
||||
defaultDomain: string;
|
||||
perm?: DomainPerm;
|
||||
permType: PermType;
|
||||
baseUrl: string;
|
||||
}
|
||||
|
||||
function DomainPermForm({ defaultDomain, perm, permType, baseUrl }: DomainPermFormProps) {
|
||||
const isExistingPerm = perm !== undefined;
|
||||
const disabledForm = isExistingPerm
|
||||
? {
|
||||
disabled: true,
|
||||
title: "Domain permissions currently cannot be edited."
|
||||
}
|
||||
: {
|
||||
disabled: false,
|
||||
title: "",
|
||||
};
|
||||
|
||||
const form = {
|
||||
domain: useTextInput("domain", { source: perm, defaultValue: defaultDomain }),
|
||||
obfuscate: useBoolInput("obfuscate", { source: perm }),
|
||||
commentPrivate: useTextInput("private_comment", { source: perm }),
|
||||
commentPublic: useTextInput("public_comment", { source: perm })
|
||||
};
|
||||
|
||||
// Check which perm type we're meant to be handling
|
||||
// here, and use appropriate mutations and results.
|
||||
// We can't call these hooks conditionally because
|
||||
// react is like "weh" (mood), but we can decide
|
||||
// which ones to use conditionally.
|
||||
const [ addBlock, addBlockResult ] = useAddDomainBlockMutation();
|
||||
const [ removeBlock, removeBlockResult] = useRemoveDomainBlockMutation({ fixedCacheKey: perm?.id });
|
||||
const [ addAllow, addAllowResult ] = useAddDomainAllowMutation();
|
||||
const [ removeAllow, removeAllowResult ] = useRemoveDomainAllowMutation({ fixedCacheKey: perm?.id });
|
||||
|
||||
const [
|
||||
addTrigger,
|
||||
addResult,
|
||||
removeTrigger,
|
||||
removeResult,
|
||||
] = useMemo(() => {
|
||||
return permType == "block"
|
||||
? [
|
||||
addBlock,
|
||||
addBlockResult,
|
||||
removeBlock,
|
||||
removeBlockResult,
|
||||
]
|
||||
: [
|
||||
addAllow,
|
||||
addAllowResult,
|
||||
removeAllow,
|
||||
removeAllowResult,
|
||||
];
|
||||
}, [permType,
|
||||
addBlock, addBlockResult, removeBlock, removeBlockResult,
|
||||
addAllow, addAllowResult, removeAllow, removeAllowResult,
|
||||
]);
|
||||
|
||||
// Use appropriate submission params for this permType.
|
||||
const [submitForm, submitFormResult] = useFormSubmit(form, [addTrigger, addResult], { changedOnly: false });
|
||||
|
||||
// Uppercase first letter of given permType.
|
||||
const permTypeUpper = useMemo(() => {
|
||||
return permType.charAt(0).toUpperCase() + permType.slice(1);
|
||||
}, [permType]);
|
||||
|
||||
const [location, setLocation] = useLocation();
|
||||
|
||||
function verifyUrlThenSubmit(e) {
|
||||
// Adding a new domain permissions happens on a url like
|
||||
// "/settings/admin/domain-permissions/:permType/domain.com",
|
||||
// but if domain input changes, that doesn't match anymore
|
||||
// and causes issues later on so, before submitting the form,
|
||||
// silently change url, and THEN submit.
|
||||
let correctUrl = `${baseUrl}/${form.domain.value}`;
|
||||
if (location != correctUrl) {
|
||||
setLocation(correctUrl);
|
||||
}
|
||||
return submitForm(e);
|
||||
}
|
||||
|
||||
return (
|
||||
<form onSubmit={verifyUrlThenSubmit}>
|
||||
<TextInput
|
||||
field={form.domain}
|
||||
label="Domain"
|
||||
placeholder="example.com"
|
||||
{...disabledForm}
|
||||
/>
|
||||
|
||||
<Checkbox
|
||||
field={form.obfuscate}
|
||||
label="Obfuscate domain in public lists"
|
||||
{...disabledForm}
|
||||
/>
|
||||
|
||||
<TextArea
|
||||
field={form.commentPrivate}
|
||||
label="Private comment"
|
||||
rows={3}
|
||||
{...disabledForm}
|
||||
/>
|
||||
|
||||
<TextArea
|
||||
field={form.commentPublic}
|
||||
label="Public comment"
|
||||
rows={3}
|
||||
{...disabledForm}
|
||||
/>
|
||||
|
||||
<div className="action-buttons row">
|
||||
<MutationButton
|
||||
label={permTypeUpper}
|
||||
result={submitFormResult}
|
||||
showError={false}
|
||||
{...disabledForm}
|
||||
/>
|
||||
|
||||
{
|
||||
isExistingPerm &&
|
||||
<MutationButton
|
||||
type="button"
|
||||
onClick={() => removeTrigger(perm.id?? "")}
|
||||
label="Remove"
|
||||
result={removeResult}
|
||||
className="button danger"
|
||||
showError={false}
|
||||
disabled={!isExistingPerm}
|
||||
/>
|
||||
}
|
||||
</div>
|
||||
|
||||
<>
|
||||
{addResult.error && <Error error={addResult.error} />}
|
||||
{removeResult.error && <Error error={removeResult.error} />}
|
||||
</>
|
||||
|
||||
</form>
|
||||
);
|
||||
}
|
||||
|
|
@ -17,34 +17,57 @@
|
|||
along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
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";
|
||||
|
||||
const MutationButton = require("../../../components/form/mutation-button");
|
||||
import MutationButton from "../../components/form/mutation-button";
|
||||
|
||||
const { Error } = require("../../../components/error");
|
||||
const ExportFormatTable = require("./export-format-table");
|
||||
import { Error } from "../../components/error";
|
||||
import ExportFormatTable from "./export-format-table";
|
||||
|
||||
module.exports = function ImportExportForm({ form, submitParse, parseResult }) {
|
||||
const [submitExport, exportResult] = useFormSubmit(form, query.useExportDomainListMutation());
|
||||
import type {
|
||||
FormSubmitFunction,
|
||||
FormSubmitResult,
|
||||
RadioFormInputHook,
|
||||
TextFormInputHook,
|
||||
} from "../../lib/form/types";
|
||||
|
||||
export interface ImportExportFormProps {
|
||||
form: {
|
||||
domains: TextFormInputHook;
|
||||
exportType: TextFormInputHook;
|
||||
permType: RadioFormInputHook;
|
||||
};
|
||||
submitParse: FormSubmitFunction;
|
||||
parseResult: FormSubmitResult;
|
||||
}
|
||||
|
||||
export default function ImportExportForm({ form, submitParse, parseResult }: ImportExportFormProps) {
|
||||
const [submitExport, exportResult] = useFormSubmit(form, useExportDomainListMutation());
|
||||
|
||||
function fileChanged(e) {
|
||||
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>
|
||||
</>
|
||||
);
|
||||
};
|
||||
}
|
||||
|
|
@ -0,0 +1,90 @@
|
|||
/*
|
||||
GoToSocial
|
||||
Copyright (C) GoToSocial Authors admin@gotosocial.org
|
||||
SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
|
||||
This program is free software: you can redistribute it and/or modify
|
||||
it under the terms of the GNU Affero General Public License as published by
|
||||
the Free Software Foundation, either version 3 of the License, or
|
||||
(at your option) any later version.
|
||||
|
||||
This program is distributed in the hope that it will be useful,
|
||||
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
GNU Affero General Public License for more details.
|
||||
|
||||
You should have received a copy of the GNU Affero General Public License
|
||||
along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
import React from "react";
|
||||
|
||||
import { Switch, Route, Redirect, useLocation } from "wouter";
|
||||
|
||||
import { useProcessDomainPermissionsMutation } from "../../lib/query/admin/domain-permissions/process";
|
||||
|
||||
import { useTextInput, useRadioInput } from "../../lib/form";
|
||||
|
||||
import useFormSubmit from "../../lib/form/submit";
|
||||
|
||||
import { ProcessImport } from "./process";
|
||||
import ImportExportForm from "./form";
|
||||
|
||||
export default function ImportExport({ baseUrl }) {
|
||||
const form = {
|
||||
domains: useTextInput("domains"),
|
||||
exportType: useTextInput("exportType", { defaultValue: "plain", dontReset: true }),
|
||||
permType: useRadioInput("permType", {
|
||||
options: {
|
||||
block: "Domain blocks",
|
||||
allow: "Domain allows",
|
||||
}
|
||||
})
|
||||
};
|
||||
|
||||
const [submitParse, parseResult] = useFormSubmit(form, useProcessDomainPermissionsMutation(), { changedOnly: false });
|
||||
|
||||
const [_location, setLocation] = useLocation();
|
||||
|
||||
return (
|
||||
<Switch>
|
||||
<Route path={`${baseUrl}/process`}>
|
||||
{
|
||||
parseResult.isSuccess
|
||||
? (
|
||||
<>
|
||||
<h1>
|
||||
<span
|
||||
className="button"
|
||||
onClick={() => {
|
||||
parseResult.reset();
|
||||
setLocation(baseUrl);
|
||||
}}
|
||||
>
|
||||
< back
|
||||
</span>
|
||||
Confirm import of domain {form.permType.value}s:
|
||||
</h1>
|
||||
<ProcessImport
|
||||
list={parseResult.data}
|
||||
permType={form.permType}
|
||||
/>
|
||||
</>
|
||||
)
|
||||
: <Redirect to={baseUrl} />
|
||||
}
|
||||
</Route>
|
||||
<Route>
|
||||
{
|
||||
parseResult.isSuccess
|
||||
? <Redirect to={`${baseUrl}/process`} />
|
||||
: <ImportExportForm
|
||||
form={form}
|
||||
submitParse={submitParse}
|
||||
parseResult={parseResult}
|
||||
/>
|
||||
}
|
||||
</Route>
|
||||
</Switch>
|
||||
);
|
||||
}
|
||||
|
|
@ -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");
|
||||
import React from "react";
|
||||
import { Switch, Route } from "wouter";
|
||||
|
||||
const InstanceOverview = require("./overview");
|
||||
const InstanceDetail = require("./detail");
|
||||
const InstanceImportExport = require("./import-export");
|
||||
import DomainPermissionsOverview from "./overview";
|
||||
import { PermType } from "../../lib/types/domain-permission";
|
||||
import DomainPermDetail from "./detail";
|
||||
|
||||
module.exports = function Federation({ baseUrl }) {
|
||||
export default function DomainPermissions({ baseUrl }: { baseUrl: string }) {
|
||||
return (
|
||||
<Switch>
|
||||
<Route path={`${baseUrl}/import-export/:list?`}>
|
||||
<InstanceImportExport />
|
||||
<Route path="/settings/admin/domain-permissions/:permType/:domain">
|
||||
{params => (
|
||||
<DomainPermDetail
|
||||
permType={params.permType as PermType}
|
||||
baseUrl={baseUrl}
|
||||
domain={params.domain}
|
||||
/>
|
||||
)}
|
||||
</Route>
|
||||
|
||||
<Route path={`${baseUrl}/:domain`}>
|
||||
<InstanceDetail baseUrl={baseUrl} />
|
||||
<Route path="/settings/admin/domain-permissions/:permType">
|
||||
{params => (
|
||||
<DomainPermissionsOverview
|
||||
permType={params.permType as PermType}
|
||||
baseUrl={baseUrl}
|
||||
/>
|
||||
)}
|
||||
</Route>
|
||||
|
||||
<InstanceOverview baseUrl={baseUrl} />
|
||||
</Switch>
|
||||
);
|
||||
};
|
||||
}
|
||||
198
web/source/settings/admin/domain-permissions/overview.tsx
Normal file
198
web/source/settings/admin/domain-permissions/overview.tsx
Normal file
|
|
@ -0,0 +1,198 @@
|
|||
/*
|
||||
GoToSocial
|
||||
Copyright (C) GoToSocial Authors admin@gotosocial.org
|
||||
SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
|
||||
This program is free software: you can redistribute it and/or modify
|
||||
it under the terms of the GNU Affero General Public License as published by
|
||||
the Free Software Foundation, either version 3 of the License, or
|
||||
(at your option) any later version.
|
||||
|
||||
This program is distributed in the hope that it will be useful,
|
||||
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
GNU Affero General Public License for more details.
|
||||
|
||||
You should have received a copy of the GNU Affero General Public License
|
||||
along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
import React from "react";
|
||||
|
||||
import { useMemo } from "react";
|
||||
import { Link, useLocation } from "wouter";
|
||||
import { matchSorter } from "match-sorter";
|
||||
|
||||
import { useTextInput } from "../../lib/form";
|
||||
|
||||
import { TextInput } from "../../components/form/inputs";
|
||||
|
||||
import Loading from "../../components/loading";
|
||||
import { useDomainAllowsQuery, useDomainBlocksQuery } from "../../lib/query/admin/domain-permissions/get";
|
||||
import type { MappedDomainPerms, PermType } from "../../lib/types/domain-permission";
|
||||
import { NoArg } from "../../lib/types/query";
|
||||
|
||||
export interface DomainPermissionsOverviewProps {
|
||||
// Params injected by
|
||||
// the wouter router.
|
||||
permType: PermType;
|
||||
baseUrl: string,
|
||||
}
|
||||
|
||||
export default function DomainPermissionsOverview({ permType, baseUrl }: DomainPermissionsOverviewProps) {
|
||||
if (permType !== "block" && permType !== "allow") {
|
||||
throw "unrecognized perm type " + permType;
|
||||
}
|
||||
|
||||
// Uppercase first letter of given permType.
|
||||
const permTypeUpper = useMemo(() => {
|
||||
return permType.charAt(0).toUpperCase() + permType.slice(1);
|
||||
}, [permType]);
|
||||
|
||||
// Fetch / wait for desired perms to load.
|
||||
const { data: blocks, isLoading: isLoadingBlocks } = useDomainBlocksQuery(NoArg, { skip: permType !== "block" });
|
||||
const { data: allows, isLoading: isLoadingAllows } = useDomainAllowsQuery(NoArg, { skip: permType !== "allow" });
|
||||
|
||||
let data: MappedDomainPerms | undefined;
|
||||
let isLoading: boolean;
|
||||
|
||||
if (permType == "block") {
|
||||
data = blocks;
|
||||
isLoading = isLoadingBlocks;
|
||||
} else {
|
||||
data = allows;
|
||||
isLoading = isLoadingAllows;
|
||||
}
|
||||
|
||||
if (isLoading || data === undefined) {
|
||||
return <Loading />;
|
||||
}
|
||||
|
||||
return (
|
||||
<div>
|
||||
<h1>Domain {permTypeUpper}s</h1>
|
||||
{ permType == "block" ? <BlockHelperText/> : <AllowHelperText/> }
|
||||
<DomainPermsList
|
||||
data={data}
|
||||
baseUrl={baseUrl}
|
||||
permType={permType}
|
||||
permTypeUpper={permTypeUpper}
|
||||
/>
|
||||
<Link to={`${baseUrl}/import-export`}>
|
||||
<a>Or use the bulk import/export interface</a>
|
||||
</Link>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
interface DomainPermsListProps {
|
||||
data: MappedDomainPerms;
|
||||
baseUrl: string;
|
||||
permType: PermType;
|
||||
permTypeUpper: string;
|
||||
}
|
||||
|
||||
function DomainPermsList({ data, baseUrl, permType, permTypeUpper }: DomainPermsListProps) {
|
||||
// Format perms into a list.
|
||||
const perms = useMemo(() => {
|
||||
return Object.values(data);
|
||||
}, [data]);
|
||||
|
||||
const [_location, setLocation] = useLocation();
|
||||
const filterField = useTextInput("filter");
|
||||
|
||||
function filterFormSubmit(e) {
|
||||
e.preventDefault();
|
||||
setLocation(`${baseUrl}/${filter}`);
|
||||
}
|
||||
|
||||
const filter = filterField.value ?? "";
|
||||
const filteredPerms = useMemo(() => {
|
||||
return matchSorter(perms, filter, { keys: ["domain"] });
|
||||
}, [perms, filter]);
|
||||
const filtered = perms.length - filteredPerms.length;
|
||||
|
||||
const filterInfo = (
|
||||
<span>
|
||||
{perms.length} {permType}ed domain{perms.length != 1 ? "s" : ""} {filtered > 0 && `(${filtered} filtered by search)`}
|
||||
</span>
|
||||
);
|
||||
|
||||
const entries = filteredPerms.map((entry) => {
|
||||
return (
|
||||
<Link key={entry.domain} to={`${baseUrl}/${entry.domain}`}>
|
||||
<a className="entry nounderline">
|
||||
<span id="domain">{entry.domain}</span>
|
||||
<span id="date">{new Date(entry.created_at ?? "").toLocaleString()}</span>
|
||||
</a>
|
||||
</Link>
|
||||
);
|
||||
});
|
||||
|
||||
return (
|
||||
<div className="domain-permissions-list">
|
||||
<form className="filter" role="search" onSubmit={filterFormSubmit}>
|
||||
<TextInput
|
||||
field={filterField}
|
||||
placeholder="example.org"
|
||||
label={`Search or add domain ${permType}`}
|
||||
/>
|
||||
<Link to={`${baseUrl}/${filter}`}>
|
||||
<a className="button">{permTypeUpper} {filter}</a>
|
||||
</Link>
|
||||
</form>
|
||||
<div>
|
||||
{filterInfo}
|
||||
<div className="list">
|
||||
<div className="entries scrolling">
|
||||
{entries}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function BlockHelperText() {
|
||||
return (
|
||||
<p>
|
||||
Blocking a domain blocks interaction between your instance, and all current and future accounts on
|
||||
instance(s) running on the blocked domain. Stored content will be removed, and no more data is sent to
|
||||
the remote server. This extends to all subdomains as well, so blocking 'example.com' also blocks 'social.example.com'.
|
||||
<br/>
|
||||
<a
|
||||
href="https://docs.gotosocial.org/en/latest/admin/domain_blocks/"
|
||||
target="_blank"
|
||||
className="docslink"
|
||||
rel="noreferrer"
|
||||
>
|
||||
Learn more about domain blocks (opens in a new tab)
|
||||
</a>
|
||||
<br/>
|
||||
</p>
|
||||
);
|
||||
}
|
||||
|
||||
function AllowHelperText() {
|
||||
return (
|
||||
<p>
|
||||
Allowing a domain explicitly allows instance(s) running on that domain to interact with your instance.
|
||||
If you're running in allowlist mode, this is how you "allow" instances through.
|
||||
If you're running in blocklist mode (the default federation mode), you can use explicit domain allows
|
||||
to override domain blocks. In blocklist mode, explicitly allowed instances will be able to interact with
|
||||
your instance regardless of any domain blocks in place. This extends to all subdomains as well, so allowing
|
||||
'example.com' also allows 'social.example.com'. This is useful when you're importing a block list but
|
||||
there are some domains on the list you don't want to block: just create an explicit allow for those domains
|
||||
before importing the list.
|
||||
<br/>
|
||||
<a
|
||||
href="https://docs.gotosocial.org/en/latest/admin/federation_modes/"
|
||||
target="_blank"
|
||||
className="docslink"
|
||||
rel="noreferrer"
|
||||
>
|
||||
Learn more about federation modes (opens in a new tab)
|
||||
</a>
|
||||
</p>
|
||||
);
|
||||
}
|
||||
|
|
@ -17,57 +17,81 @@
|
|||
along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
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");
|
||||
useCheckListInput,
|
||||
} from "../../lib/form";
|
||||
|
||||
const useFormSubmit = require("../../../lib/form/submit");
|
||||
|
||||
const {
|
||||
TextInput,
|
||||
TextArea,
|
||||
Checkbox,
|
||||
import {
|
||||
Select,
|
||||
RadioGroup
|
||||
} = require("../../../components/form/inputs");
|
||||
TextArea,
|
||||
RadioGroup,
|
||||
Checkbox,
|
||||
TextInput,
|
||||
} from "../../components/form/inputs";
|
||||
|
||||
const CheckList = require("../../../components/check-list");
|
||||
const MutationButton = require("../../../components/form/mutation-button");
|
||||
const FormWithData = require("../../../lib/form/form-with-data");
|
||||
import useFormSubmit from "../../lib/form/submit";
|
||||
|
||||
module.exports = React.memo(
|
||||
function ProcessImport({ list }) {
|
||||
import CheckList from "../../components/check-list";
|
||||
import MutationButton from "../../components/form/mutation-button";
|
||||
import FormWithData from "../../lib/form/form-with-data";
|
||||
|
||||
import { useImportDomainPermsMutation } from "../../lib/query/admin/domain-permissions/import";
|
||||
import {
|
||||
useDomainAllowsQuery,
|
||||
useDomainBlocksQuery
|
||||
} from "../../lib/query/admin/domain-permissions/get";
|
||||
|
||||
import type { DomainPerm, MappedDomainPerms } from "../../lib/types/domain-permission";
|
||||
import type { ChecklistInputHook, RadioFormInputHook } from "../../lib/form/types";
|
||||
|
||||
export interface ProcessImportProps {
|
||||
list: DomainPerm[],
|
||||
permType: RadioFormInputHook,
|
||||
}
|
||||
|
||||
export const ProcessImport = memo(
|
||||
function ProcessImport({ list, permType }: ProcessImportProps) {
|
||||
return (
|
||||
<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) {
|
||||
|
|
@ -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);
|
||||
|
||||
|
|
|
|||
|
|
@ -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>< 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} />;
|
||||
|
|
|
|||
|
|
@ -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) {
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
||||
|
|
|
|||
|
|
@ -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));
|
||||
|
|
|
|||
|
|
@ -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));
|
||||
|
|
|
|||
|
|
@ -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 }) => {
|
||||
|
|
|
|||
|
|
@ -1,168 +0,0 @@
|
|||
/*
|
||||
GoToSocial
|
||||
Copyright (C) GoToSocial Authors admin@gotosocial.org
|
||||
SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
|
||||
This program is free software: you can redistribute it and/or modify
|
||||
it under the terms of the GNU Affero General Public License as published by
|
||||
the Free Software Foundation, either version 3 of the License, or
|
||||
(at your option) any later version.
|
||||
|
||||
This program is distributed in the hope that it will be useful,
|
||||
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
GNU Affero General Public License for more details.
|
||||
|
||||
You should have received a copy of the GNU Affero General Public License
|
||||
along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
const React = require("react");
|
||||
const { useRoute, Redirect, useLocation } = require("wouter");
|
||||
|
||||
const query = require("../../lib/query");
|
||||
|
||||
const { useTextInput, useBoolInput } = require("../../lib/form");
|
||||
|
||||
const useFormSubmit = require("../../lib/form/submit");
|
||||
|
||||
const { TextInput, Checkbox, TextArea } = require("../../components/form/inputs");
|
||||
|
||||
const Loading = require("../../components/loading");
|
||||
const BackButton = require("../../components/back-button");
|
||||
const MutationButton = require("../../components/form/mutation-button");
|
||||
|
||||
module.exports = function InstanceDetail({ baseUrl }) {
|
||||
const { data: blockedInstances = {}, isLoading } = query.useInstanceBlocksQuery();
|
||||
|
||||
let [_match, { domain }] = useRoute(`${baseUrl}/:domain`);
|
||||
if (domain == "view") {
|
||||
// Retrieve domain from form field submission.
|
||||
domain = (new URL(document.location)).searchParams.get("domain");
|
||||
}
|
||||
|
||||
// Normalize / decode domain (it may be URL-encoded).
|
||||
domain = decodeURIComponent(domain);
|
||||
|
||||
const existingBlock = React.useMemo(() => {
|
||||
return blockedInstances[domain];
|
||||
}, [blockedInstances, domain]);
|
||||
|
||||
if (domain == undefined) {
|
||||
return <Redirect to={baseUrl} />;
|
||||
}
|
||||
|
||||
let infoContent = null;
|
||||
|
||||
if (isLoading) {
|
||||
infoContent = <Loading />;
|
||||
} else if (existingBlock == undefined) {
|
||||
infoContent = <span>No stored block yet, you can add one below:</span>;
|
||||
} else {
|
||||
infoContent = (
|
||||
<div className="info">
|
||||
<i className="fa fa-fw fa-exclamation-triangle" aria-hidden="true"></i>
|
||||
<b>Editing domain blocks isn't implemented yet, <a href="https://github.com/superseriousbusiness/gotosocial/issues/1198" target="_blank" rel="noopener noreferrer">check here for progress</a></b>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div>
|
||||
<h1 className="text-cutoff"><BackButton to={baseUrl} /> Federation settings for: <span title={domain}>{domain}</span></h1>
|
||||
{infoContent}
|
||||
<DomainBlockForm defaultDomain={domain} block={existingBlock} baseUrl={baseUrl} />
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
function DomainBlockForm({ defaultDomain, block = {}, baseUrl }) {
|
||||
const isExistingBlock = block.domain != undefined;
|
||||
|
||||
const disabledForm = isExistingBlock
|
||||
? {
|
||||
disabled: true,
|
||||
title: "Domain suspensions currently cannot be edited."
|
||||
}
|
||||
: {};
|
||||
|
||||
const form = {
|
||||
domain: useTextInput("domain", { source: block, defaultValue: defaultDomain }),
|
||||
obfuscate: useBoolInput("obfuscate", { source: block }),
|
||||
commentPrivate: useTextInput("private_comment", { source: block }),
|
||||
commentPublic: useTextInput("public_comment", { source: block })
|
||||
};
|
||||
|
||||
const [submitForm, addResult] = useFormSubmit(form, query.useAddInstanceBlockMutation(), { changedOnly: false });
|
||||
|
||||
const [removeBlock, removeResult] = query.useRemoveInstanceBlockMutation({ fixedCacheKey: block.id });
|
||||
|
||||
const [location, setLocation] = useLocation();
|
||||
|
||||
function verifyUrlThenSubmit(e) {
|
||||
// Adding a new block happens on /settings/admin/federation/domain.com
|
||||
// but if domain input changes, that doesn't match anymore and causes issues later on
|
||||
// so, before submitting the form, silently change url, then submit
|
||||
let correctUrl = `${baseUrl}/${form.domain.value}`;
|
||||
if (location != correctUrl) {
|
||||
setLocation(correctUrl);
|
||||
}
|
||||
return submitForm(e);
|
||||
}
|
||||
|
||||
return (
|
||||
<form onSubmit={verifyUrlThenSubmit}>
|
||||
<TextInput
|
||||
field={form.domain}
|
||||
label="Domain"
|
||||
placeholder="example.com"
|
||||
{...disabledForm}
|
||||
/>
|
||||
|
||||
<Checkbox
|
||||
field={form.obfuscate}
|
||||
label="Obfuscate domain in public lists"
|
||||
{...disabledForm}
|
||||
/>
|
||||
|
||||
<TextArea
|
||||
field={form.commentPrivate}
|
||||
label="Private comment"
|
||||
rows={3}
|
||||
{...disabledForm}
|
||||
/>
|
||||
|
||||
<TextArea
|
||||
field={form.commentPublic}
|
||||
label="Public comment"
|
||||
rows={3}
|
||||
{...disabledForm}
|
||||
/>
|
||||
|
||||
<div className="action-buttons row">
|
||||
<MutationButton
|
||||
label="Suspend"
|
||||
result={addResult}
|
||||
showError={false}
|
||||
{...disabledForm}
|
||||
/>
|
||||
|
||||
{
|
||||
isExistingBlock &&
|
||||
<MutationButton
|
||||
type="button"
|
||||
onClick={() => removeBlock(block.id)}
|
||||
label="Remove"
|
||||
result={removeResult}
|
||||
className="button danger"
|
||||
showError={false}
|
||||
/>
|
||||
}
|
||||
</div>
|
||||
|
||||
{addResult.error && <Error error={addResult.error} />}
|
||||
{removeResult.error && <Error error={removeResult.error} />}
|
||||
|
||||
</form>
|
||||
);
|
||||
}
|
||||
|
|
@ -1,75 +0,0 @@
|
|||
/*
|
||||
GoToSocial
|
||||
Copyright (C) GoToSocial Authors admin@gotosocial.org
|
||||
SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
|
||||
This program is free software: you can redistribute it and/or modify
|
||||
it under the terms of the GNU Affero General Public License as published by
|
||||
the Free Software Foundation, either version 3 of the License, or
|
||||
(at your option) any later version.
|
||||
|
||||
This program is distributed in the hope that it will be useful,
|
||||
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
GNU Affero General Public License for more details.
|
||||
|
||||
You should have received a copy of the GNU Affero General Public License
|
||||
along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
const React = require("react");
|
||||
const { Switch, Route, Redirect, useLocation } = require("wouter");
|
||||
|
||||
const query = require("../../../lib/query");
|
||||
|
||||
const {
|
||||
useTextInput,
|
||||
} = require("../../../lib/form");
|
||||
|
||||
const useFormSubmit = require("../../../lib/form/submit");
|
||||
|
||||
const ProcessImport = require("./process");
|
||||
const ImportExportForm = require("./form");
|
||||
|
||||
module.exports = function ImportExport({ baseUrl }) {
|
||||
const form = {
|
||||
domains: useTextInput("domains"),
|
||||
exportType: useTextInput("exportType", { defaultValue: "plain", dontReset: true })
|
||||
};
|
||||
|
||||
const [submitParse, parseResult] = useFormSubmit(form, query.useProcessDomainListMutation(), { changedOnly: false });
|
||||
|
||||
const [_location, setLocation] = useLocation();
|
||||
|
||||
return (
|
||||
<Switch>
|
||||
<Route path={`${baseUrl}/process`}>
|
||||
{parseResult.isSuccess ? (
|
||||
<>
|
||||
<h1>
|
||||
<span className="button" onClick={() => {
|
||||
parseResult.reset();
|
||||
setLocation(baseUrl);
|
||||
}}>
|
||||
< back
|
||||
</span> Confirm import:
|
||||
</h1>
|
||||
<ProcessImport
|
||||
list={parseResult.data}
|
||||
/>
|
||||
</>
|
||||
) : <Redirect to={baseUrl} />}
|
||||
</Route>
|
||||
|
||||
<Route>
|
||||
{!parseResult.isSuccess ? (
|
||||
<ImportExportForm
|
||||
form={form}
|
||||
submitParse={submitParse}
|
||||
parseResult={parseResult}
|
||||
/>
|
||||
) : <Redirect to={`${baseUrl}/process`} />}
|
||||
</Route>
|
||||
</Switch>
|
||||
);
|
||||
};
|
||||
|
|
@ -1,101 +0,0 @@
|
|||
/*
|
||||
GoToSocial
|
||||
Copyright (C) GoToSocial Authors admin@gotosocial.org
|
||||
SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
|
||||
This program is free software: you can redistribute it and/or modify
|
||||
it under the terms of the GNU Affero General Public License as published by
|
||||
the Free Software Foundation, either version 3 of the License, or
|
||||
(at your option) any later version.
|
||||
|
||||
This program is distributed in the hope that it will be useful,
|
||||
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
GNU Affero General Public License for more details.
|
||||
|
||||
You should have received a copy of the GNU Affero General Public License
|
||||
along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
const React = require("react");
|
||||
const { Link, useLocation } = require("wouter");
|
||||
const { matchSorter } = require("match-sorter");
|
||||
|
||||
const { useTextInput } = require("../../lib/form");
|
||||
|
||||
const { TextInput } = require("../../components/form/inputs");
|
||||
|
||||
const query = require("../../lib/query");
|
||||
|
||||
const Loading = require("../../components/loading");
|
||||
|
||||
module.exports = function InstanceOverview({ baseUrl }) {
|
||||
const { data: blockedInstances = [], isLoading } = query.useInstanceBlocksQuery();
|
||||
|
||||
const [_location, setLocation] = useLocation();
|
||||
|
||||
const filterField = useTextInput("filter");
|
||||
const filter = filterField.value;
|
||||
|
||||
const blockedInstancesList = React.useMemo(() => {
|
||||
return Object.values(blockedInstances);
|
||||
}, [blockedInstances]);
|
||||
|
||||
const filteredInstances = React.useMemo(() => {
|
||||
return matchSorter(blockedInstancesList, filter, { keys: ["domain"] });
|
||||
}, [blockedInstancesList, filter]);
|
||||
|
||||
let filtered = blockedInstancesList.length - filteredInstances.length;
|
||||
|
||||
function filterFormSubmit(e) {
|
||||
e.preventDefault();
|
||||
setLocation(`${baseUrl}/${filter}`);
|
||||
}
|
||||
|
||||
if (isLoading) {
|
||||
return <Loading />;
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<h1>Federation</h1>
|
||||
|
||||
<div className="instance-list">
|
||||
<h2>Suspended instances</h2>
|
||||
<p>
|
||||
Suspending a domain blocks all current and future accounts on that instance. Stored content will be removed,
|
||||
and no more data is sent to the remote server.<br />
|
||||
This extends to all subdomains as well, so blocking 'example.com' also includes 'social.example.com'.
|
||||
</p>
|
||||
<form className="filter" role="search" onSubmit={filterFormSubmit}>
|
||||
<TextInput field={filterField} placeholder="example.com" label="Search or add domain suspension" />
|
||||
<Link to={`${baseUrl}/${filter}`}><a className="button">Suspend</a></Link>
|
||||
</form>
|
||||
<div>
|
||||
<span>
|
||||
{blockedInstancesList.length} blocked instance{blockedInstancesList.length != 1 ? "s" : ""} {filtered > 0 && `(${filtered} filtered by search)`}
|
||||
</span>
|
||||
<div className="list">
|
||||
<div className="entries scrolling">
|
||||
{filteredInstances.map((entry) => {
|
||||
return (
|
||||
<Link key={entry.domain} to={`${baseUrl}/${entry.domain}`}>
|
||||
<a className="entry nounderline">
|
||||
<span id="domain">
|
||||
{entry.domain}
|
||||
</span>
|
||||
<span id="date">
|
||||
{new Date(entry.created_at).toLocaleString()}
|
||||
</span>
|
||||
</a>
|
||||
</Link>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<Link to={`${baseUrl}/import-export`}><a>Or use the bulk import/export interface</a></Link>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
|
@ -20,19 +20,21 @@
|
|||
const React = require("react");
|
||||
const { 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">
|
||||
|
|
|
|||
|
|
@ -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}
|
||||
/>
|
||||
</>
|
||||
|
|
|
|||
|
|
@ -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}>
|
||||
|
|
|
|||
|
|
@ -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");
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue