[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:
tobi 2023-10-17 12:46:06 +02:00 committed by GitHub
commit 637f188ebe
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
77 changed files with 4154 additions and 1690 deletions

View file

@ -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());

View 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>
);
}

View file

@ -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>
</>
);
};
}

View file

@ -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>
);
}

View file

@ -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>
);
};
}

View 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}&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>
);
}

View file

@ -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) {

View file

@ -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);

View file

@ -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} />;

View file

@ -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) {

View file

@ -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;

View file

@ -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));

View file

@ -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));

View file

@ -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 }) => {

View file

@ -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>
);
}

View file

@ -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>
);
};

View file

@ -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>
</>
);
};

View file

@ -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">

View file

@ -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}
/>
</>

View file

@ -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}>

View file

@ -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");