From 301543616b5376585a7caff097499421acdf1806 Mon Sep 17 00:00:00 2001 From: tobi <31960611+tsmethurst@users.noreply.github.com> Date: Thu, 21 Nov 2024 14:09:58 +0100 Subject: [feature] Add domain permission drafts and excludes (#3547) * [feature] Add domain permission drafts and excludes * fix typescript complaining * lint * make filenames more consistent * test own domain excluded --- .../views/admin/actions/keys/expireremote.tsx | 25 +- .../views/admin/http-header-permissions/detail.tsx | 70 ++--- .../admin/http-header-permissions/overview.tsx | 5 +- .../views/moderation/accounts/pending/index.tsx | 4 +- .../views/moderation/accounts/search/index.tsx | 29 +- .../views/moderation/domain-permissions/detail.tsx | 118 ++++----- .../domain-permissions/drafts/common.tsx | 43 +++ .../domain-permissions/drafts/detail.tsx | 210 +++++++++++++++ .../moderation/domain-permissions/drafts/index.tsx | 293 +++++++++++++++++++++ .../moderation/domain-permissions/drafts/new.tsx | 119 +++++++++ .../domain-permissions/excludes/common.tsx | 54 ++++ .../domain-permissions/excludes/detail.tsx | 119 +++++++++ .../domain-permissions/excludes/index.tsx | 235 +++++++++++++++++ .../moderation/domain-permissions/excludes/new.tsx | 90 +++++++ .../moderation/domain-permissions/overview.tsx | 5 +- web/source/settings/views/moderation/menu.tsx | 34 +++ .../settings/views/moderation/reports/detail.tsx | 8 +- .../settings/views/moderation/reports/search.tsx | 6 +- web/source/settings/views/moderation/router.tsx | 12 + 19 files changed, 1301 insertions(+), 178 deletions(-) create mode 100644 web/source/settings/views/moderation/domain-permissions/drafts/common.tsx create mode 100644 web/source/settings/views/moderation/domain-permissions/drafts/detail.tsx create mode 100644 web/source/settings/views/moderation/domain-permissions/drafts/index.tsx create mode 100644 web/source/settings/views/moderation/domain-permissions/drafts/new.tsx create mode 100644 web/source/settings/views/moderation/domain-permissions/excludes/common.tsx create mode 100644 web/source/settings/views/moderation/domain-permissions/excludes/detail.tsx create mode 100644 web/source/settings/views/moderation/domain-permissions/excludes/index.tsx create mode 100644 web/source/settings/views/moderation/domain-permissions/excludes/new.tsx (limited to 'web/source/settings/views') diff --git a/web/source/settings/views/admin/actions/keys/expireremote.tsx b/web/source/settings/views/admin/actions/keys/expireremote.tsx index 1d62f9439..082f1fdff 100644 --- a/web/source/settings/views/admin/actions/keys/expireremote.tsx +++ b/web/source/settings/views/admin/actions/keys/expireremote.tsx @@ -22,32 +22,11 @@ import { TextInput } from "../../../../components/form/inputs"; import MutationButton from "../../../../components/form/mutation-button"; import { useTextInput } from "../../../../lib/form"; import { useInstanceKeysExpireMutation } from "../../../../lib/query/admin/actions"; -import isValidDomain from "is-valid-domain"; +import { formDomainValidator } from "../../../../lib/util/formvalidators"; export default function ExpireRemote({}) { const domainField = useTextInput("domain", { - validator: (v: string) => { - if (v.length === 0) { - return ""; - } - - if (v[v.length-1] === ".") { - return "invalid domain"; - } - - const valid = isValidDomain(v, { - subdomain: true, - wildcard: false, - allowUnicode: true, - topLevel: false, - }); - - if (valid) { - return ""; - } - - return "invalid domain"; - } + validator: formDomainValidator, }); const [expire, expireResult] = useInstanceKeysExpireMutation(); diff --git a/web/source/settings/views/admin/http-header-permissions/detail.tsx b/web/source/settings/views/admin/http-header-permissions/detail.tsx index 522f2dba2..e0d49ffd2 100644 --- a/web/source/settings/views/admin/http-header-permissions/detail.tsx +++ b/web/source/settings/views/admin/http-header-permissions/detail.tsx @@ -17,7 +17,7 @@ along with this program. If not, see . */ -import React, { useEffect, useMemo } from "react"; +import React, { useMemo } from "react"; import { useLocation, useParams } from "wouter"; import { PermType } from "../../../lib/types/perm"; import { useDeleteHeaderAllowMutation, useDeleteHeaderBlockMutation, useGetHeaderAllowQuery, useGetHeaderBlockQuery } from "../../../lib/query/admin/http-header-permissions"; @@ -26,8 +26,7 @@ import { FetchBaseQueryError } from "@reduxjs/toolkit/query"; import { SerializedError } from "@reduxjs/toolkit"; import Loading from "../../../components/loading"; import { Error } from "../../../components/error"; -import { useLazyGetAccountQuery } from "../../../lib/query/admin"; -import Username from "../../../components/username"; +import UsernameLozenge from "../../../components/username-lozenge"; import { useBaseUrl } from "../../../lib/navigation/util"; import BackButton from "../../../components/back-button"; import MutationButton from "../../../components/form/mutation-button"; @@ -92,58 +91,19 @@ interface PermDeetsProps { function PermDeets({ permType, data: perm, - isLoading: isLoadingPerm, - isFetching: isFetchingPerm, - isError: isErrorPerm, - error: errorPerm, + isLoading, + isFetching, + isError, + error, }: PermDeetsProps) { const [ location ] = useLocation(); const baseUrl = useBaseUrl(); - - // Once we've loaded the perm, trigger - // getting the account that created it. - const [ getAccount, getAccountRes ] = useLazyGetAccountQuery(); - useEffect(() => { - if (!perm) { - return; - } - getAccount(perm.created_by, true); - }, [getAccount, perm]); - - // Load the createdByAccount if possible, - // returning a username lozenge with - // a link to the account. - const createdByAccount = useMemo(() => { - const { - data: account, - isLoading: isLoadingAccount, - isFetching: isFetchingAccount, - isError: isErrorAccount, - } = getAccountRes; - - // Wait for query to finish, returning - // loading spinner in the meantime. - if (isLoadingAccount || isFetchingAccount || !perm) { - return ; - } else if (isErrorAccount || account === undefined) { - // Fall back to account ID. - return perm?.created_by; - } - - return ( - - ); - }, [getAccountRes, perm, baseUrl, location]); - - // Now wait til the perm itself is loaded. - if (isLoadingPerm || isFetchingPerm) { + + // Wait til the perm itself is loaded. + if (isLoading || isFetching) { return ; - } else if (isErrorPerm) { - return ; + } else if (isError) { + return ; } else if (perm === undefined) { throw "perm undefined"; } @@ -172,7 +132,13 @@ function PermDeets({
Created By
-
{createdByAccount}
+
+ +
Header Name
diff --git a/web/source/settings/views/admin/http-header-permissions/overview.tsx b/web/source/settings/views/admin/http-header-permissions/overview.tsx index 54b58b642..b2d8b7372 100644 --- a/web/source/settings/views/admin/http-header-permissions/overview.tsx +++ b/web/source/settings/views/admin/http-header-permissions/overview.tsx @@ -27,6 +27,7 @@ import { PermType } from "../../../lib/types/perm"; import { FetchBaseQueryError } from "@reduxjs/toolkit/query"; import { SerializedError } from "@reduxjs/toolkit"; import HeaderPermCreateForm from "./create"; +import { useCapitalize } from "../../../lib/util"; export default function HeaderPermsOverview() { const [ location, setLocation ] = useLocation(); @@ -41,9 +42,7 @@ export default function HeaderPermsOverview() { }, [params]); // Uppercase first letter of given permType. - const permTypeUpper = useMemo(() => { - return permType.charAt(0).toUpperCase() + permType.slice(1); - }, [permType]); + const permTypeUpper = useCapitalize(permType); // Fetch desired perms, skipping // the ones we don't want. diff --git a/web/source/settings/views/moderation/accounts/pending/index.tsx b/web/source/settings/views/moderation/accounts/pending/index.tsx index f03c4800c..10f7d726a 100644 --- a/web/source/settings/views/moderation/accounts/pending/index.tsx +++ b/web/source/settings/views/moderation/accounts/pending/index.tsx @@ -21,7 +21,7 @@ import React, { ReactNode } from "react"; import { useSearchAccountsQuery } from "../../../../lib/query/admin"; import { PageableList } from "../../../../components/pageable-list"; import { useLocation } from "wouter"; -import Username from "../../../../components/username"; +import UsernameLozenge from "../../../../components/username-lozenge"; import { AdminAccount } from "../../../../lib/types/account"; export default function AccountsPending() { @@ -32,7 +32,7 @@ export default function AccountsPending() { function itemToEntry(account: AdminAccount): ReactNode { const acc = account.account; return ( - { - if (v.length === 0) { - return ""; - } - - if (v[v.length-1] === ".") { - return "invalid domain"; - } - - const valid = isValidDomain(v, { - subdomain: true, - wildcard: false, - allowUnicode: true, - topLevel: false, - }); - - if (valid) { - return ""; - } - - return "invalid domain"; - } + validator: formDomainValidator, }), email: useTextInput("email", { defaultValue: urlQueryParams.get("email") ?? ""}), ip: useTextInput("ip", { defaultValue: urlQueryParams.get("ip") ?? ""}), @@ -114,7 +93,7 @@ export function AccountSearchForm() { function itemToEntry(account: AdminAccount): ReactNode { const acc = account.account; return ( - "block" and "allows" => "allow". + const params = useParams(); + const permTypeRaw = params.permType; + if (permTypeRaw !== "blocks" && permTypeRaw !== "allows") { throw "unrecognized perm type " + params.permType; } - const permType = params.permType.slice(0, -1) as PermType; + const permType = useMemo(() => { + return permTypeRaw.slice(0, -1) as PermType; + }, [permTypeRaw]); - 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"; + // Conditionally fetch either domain blocks or domain + // allows depending on which perm type we're looking at. + const { + data: blocks = {}, + isLoading: loadingBlocks, + isFetching: fetchingBlocks, + } = useDomainBlocksQuery(NoArg, { skip: permType !== "block" }); + const { + data: allows = {}, + isLoading: loadingAllows, + isFetching: fetchingAllows, + } = useDomainAllowsQuery(NoArg, { skip: permType !== "allow" }); + + // Wait until we're done loading. + const loading = permType === "block" + ? loadingBlocks || fetchingBlocks + : loadingAllows || fetchingAllows; + if (loading) { + return ; } // Parse domain from routing params. let domain = params.domain ?? "unknown"; - - const search = useSearch(); if (domain === "view") { // Retrieve domain from form field submission. const searchParams = new URLSearchParams(search); @@ -81,36 +91,41 @@ export default function DomainPermDetail() { domain = searchDomain; } - // Normalize / decode domain (it may be URL-encoded). + // 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]); - + // Check if we already have a perm + // of the desired type for this domain. + const existingPerm = permType === "block" + ? blocks[domain] + : allows[domain]; + + // Render different into content depending on + // if we have a perm already for this domain. let infoContent: React.JSX.Element; - - if (isLoading) { - infoContent = ; - } else if (existingPerm == undefined) { - infoContent = No stored {permType} yet, you can add one below:; + if (existingPerm === undefined) { + infoContent = ( + + No stored {permType} yet, you can add one below: + + ); } else { infoContent = (
- Editing domain permissions isn't implemented yet, check here for progress + Editing existing domain {permTypeRaw} isn't implemented yet, check here for progress
); } return (
-

Domain {permType} for: {domain}

+

+ + {" "} + Domain {permType} for {domain} +

{infoContent} { - if (v.length === 0) { - return ""; - } - - if (v[v.length-1] === ".") { - return "invalid domain"; - } - - const valid = isValidDomain(v, { - subdomain: true, - wildcard: false, - allowUnicode: true, - topLevel: false, - }); - - if (valid) { - return ""; - } - - return "invalid domain"; - } + validator: formDomainValidator, }), obfuscate: useBoolInput("obfuscate", { source: perm }), commentPrivate: useTextInput("private_comment", { source: perm }), @@ -209,9 +203,7 @@ function DomainPermForm({ defaultDomain, perm, permType }: DomainPermFormProps) 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 permTypeUpper = useCapitalize(permType); const [location, setLocation] = useLocation(); diff --git a/web/source/settings/views/moderation/domain-permissions/drafts/common.tsx b/web/source/settings/views/moderation/domain-permissions/drafts/common.tsx new file mode 100644 index 000000000..af919dc57 --- /dev/null +++ b/web/source/settings/views/moderation/domain-permissions/drafts/common.tsx @@ -0,0 +1,43 @@ +/* + 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 . +*/ + +import React from "react"; + +export function DomainPermissionDraftHelpText() { + return ( + <> + Domain permission drafts are domain block or domain allow entries that are not yet in force. +
+ You can choose to accept or remove a draft. + + ); +} + +export function DomainPermissionDraftDocsLink() { + return ( + + Learn more about domain permission drafts (opens in a new tab) + + ); +} diff --git a/web/source/settings/views/moderation/domain-permissions/drafts/detail.tsx b/web/source/settings/views/moderation/domain-permissions/drafts/detail.tsx new file mode 100644 index 000000000..a5ba325f0 --- /dev/null +++ b/web/source/settings/views/moderation/domain-permissions/drafts/detail.tsx @@ -0,0 +1,210 @@ +/* + 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 . +*/ + +import React from "react"; +import { useLocation, useParams } from "wouter"; +import Loading from "../../../../components/loading"; +import { useBaseUrl } from "../../../../lib/navigation/util"; +import BackButton from "../../../../components/back-button"; +import { + useAcceptDomainPermissionDraftMutation, + useGetDomainPermissionDraftQuery, + useRemoveDomainPermissionDraftMutation +} from "../../../../lib/query/admin/domain-permissions/drafts"; +import { Error as ErrorC } from "../../../../components/error"; +import UsernameLozenge from "../../../../components/username-lozenge"; +import MutationButton from "../../../../components/form/mutation-button"; +import { useBoolInput, useTextInput } from "../../../../lib/form"; +import { Checkbox, Select } from "../../../../components/form/inputs"; +import { PermType } from "../../../../lib/types/perm"; + +export default function DomainPermissionDraftDetail() { + const baseUrl = useBaseUrl(); + const backLocation: string = history.state?.backLocation ?? `~${baseUrl}`; + const params = useParams(); + + let id = params.permDraftId as string | undefined; + if (!id) { + throw "no perm ID"; + } + + const { + data: permDraft, + isLoading, + isFetching, + isError, + error, + } = useGetDomainPermissionDraftQuery(id); + + if (isLoading || isFetching) { + return ; + } else if (isError) { + return ; + } else if (permDraft === undefined) { + return ; + } + + const created = permDraft.created_at ? new Date(permDraft.created_at).toDateString(): "unknown"; + const domain = permDraft.domain; + const permType = permDraft.permission_type; + if (!permType) { + return ; + } + const publicComment = permDraft.public_comment ?? "[none]"; + const privateComment = permDraft.private_comment ?? "[none]"; + const subscriptionID = permDraft.subscription_id ?? "[none]"; + + return ( +
+

Domain Permission Draft Detail

+
+
+
Created
+
+
+
+
Created By
+
+ +
+
+
+
Domain
+
{domain}
+
+
+
Permission type
+
+ + {permType} +
+
+
+
Private comment
+
{privateComment}
+
+
+
Public comment
+
{publicComment}
+
+
+
Subscription ID
+
{subscriptionID}
+
+
+ +
+ ); +} + +function HandleDraft({ id, permType, backLocation }: { id: string, permType: PermType, backLocation: string }) { + const [ accept, acceptResult ] = useAcceptDomainPermissionDraftMutation(); + const [ remove, removeResult ] = useRemoveDomainPermissionDraftMutation(); + const [_location, setLocation] = useLocation(); + const form = { + acceptOrRemove: useTextInput("accept_or_remove", { defaultValue: "accept" }), + overwrite: useBoolInput("overwrite"), + exclude_target: useBoolInput("exclude_target"), + }; + + const onClick = (e) => { + e.preventDefault(); + if (form.acceptOrRemove.value === "accept") { + const overwrite = form.overwrite.value; + accept({id, overwrite, permType}).then(res => { + if ("data" in res) { + setLocation(backLocation); + } + }); + } else { + const exclude_target = form.exclude_target.value; + remove({id, exclude_target}).then(res => { + if ("data" in res) { + setLocation(backLocation); + } + }); + } + }; + + return ( +
+ + + { form.acceptOrRemove.value === "accept" && + <> + + + } + + { form.acceptOrRemove.value === "remove" && + <> + + + } + + + + ); +} diff --git a/web/source/settings/views/moderation/domain-permissions/drafts/index.tsx b/web/source/settings/views/moderation/domain-permissions/drafts/index.tsx new file mode 100644 index 000000000..19dbe0d88 --- /dev/null +++ b/web/source/settings/views/moderation/domain-permissions/drafts/index.tsx @@ -0,0 +1,293 @@ +/* + 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 . +*/ + +import React, { ReactNode, useEffect, useMemo } from "react"; + +import { useTextInput } from "../../../../lib/form"; +import { PageableList } from "../../../../components/pageable-list"; +import MutationButton from "../../../../components/form/mutation-button"; +import { useLocation, useSearch } from "wouter"; +import { useAcceptDomainPermissionDraftMutation, useLazySearchDomainPermissionDraftsQuery, useRemoveDomainPermissionDraftMutation } from "../../../../lib/query/admin/domain-permissions/drafts"; +import { DomainPerm } from "../../../../lib/types/domain-permission"; +import { Error as ErrorC } from "../../../../components/error"; +import { Select, TextInput } from "../../../../components/form/inputs"; +import { formDomainValidator } from "../../../../lib/util/formvalidators"; +import { useCapitalize } from "../../../../lib/util"; +import { DomainPermissionDraftDocsLink, DomainPermissionDraftHelpText } from "./common"; + +export default function DomainPermissionDraftsSearch() { + return ( +
+
+

Domain Permission Drafts

+

+ You can use the form below to search through domain permission drafts. +
+ +

+ +
+ +
+ ); +} + +function DomainPermissionDraftsSearchForm() { + const [ location, setLocation ] = useLocation(); + const search = useSearch(); + const urlQueryParams = useMemo(() => new URLSearchParams(search), [search]); + const hasParams = urlQueryParams.size != 0; + const [ searchDrafts, searchRes ] = useLazySearchDomainPermissionDraftsQuery(); + + const form = { + subscription_id: useTextInput("subscription_id", { defaultValue: urlQueryParams.get("subscription_id") ?? "" }), + domain: useTextInput("domain", { + defaultValue: urlQueryParams.get("domain") ?? "", + validator: formDomainValidator, + }), + permission_type: useTextInput("permission_type", { defaultValue: urlQueryParams.get("permission_type") ?? "" }), + limit: useTextInput("limit", { defaultValue: urlQueryParams.get("limit") ?? "20" }) + }; + + // On mount, if urlQueryParams were provided, + // trigger the search. For example, if page + // was accessed at /search?origin=local&limit=20, + // then run a search with origin=local and + // limit=20 and immediately render the results. + // + // If no urlQueryParams set, trigger default + // search (first page, no filtering). + useEffect(() => { + if (hasParams) { + searchDrafts(Object.fromEntries(urlQueryParams)); + } else { + setLocation(location + "?limit=20"); + } + }, [ + urlQueryParams, + hasParams, + searchDrafts, + location, + setLocation, + ]); + + // Rather than triggering the search directly, + // the "submit" button changes the location + // based on form field params, and lets the + // useEffect hook above actually do the search. + function submitQuery(e) { + e.preventDefault(); + + // Parse query parameters. + const entries = Object.entries(form).map(([k, v]) => { + // Take only defined form fields. + if (v.value === undefined || v.value.length === 0 || v.value === "any") { + return null; + } + return [[k, v.value]]; + }).flatMap(kv => { + // Remove any nulls. + return kv || []; + }); + + const searchParams = new URLSearchParams(entries); + setLocation(location + "?" + searchParams.toString()); + } + + // Location to return to when user clicks "back" on the detail view. + const backLocation = location + (hasParams ? `?${urlQueryParams}` : ""); + + // Function to map an item to a list entry. + function itemToEntry(draft: DomainPerm): ReactNode { + return ( + + ); + } + + return ( + <> +
+ + + + + + No drafts found that match your query.} + prevNextLinks={searchRes.data?.links} + /> + + ); +} + +interface DraftEntryProps { + permDraft: DomainPerm; + linkTo: string; + backLocation: string; +} + +function DraftListEntry({ permDraft, linkTo, backLocation }: DraftEntryProps) { + const [ _location, setLocation ] = useLocation(); + const [ accept, acceptResult ] = useAcceptDomainPermissionDraftMutation(); + const [ remove, removeResult ] = useRemoveDomainPermissionDraftMutation(); + + const domain = permDraft.domain; + const permType = permDraft.permission_type; + const permTypeUpper = useCapitalize(permType); + if (!permType) { + return ; + } + + const publicComment = permDraft.public_comment ?? "[none]"; + const privateComment = permDraft.private_comment ?? "[none]"; + const subscriptionID = permDraft.subscription_id ?? "[none]"; + const id = permDraft.id; + if (!id) { + return ; + } + + const title = `${permTypeUpper} ${domain}`; + + return ( + { + // When clicking on a draft, direct + // to the detail view for that draft. + setLocation(linkTo, { + // Store the back location in history so + // the detail view can use it to return to + // this page (including query parameters). + state: { backLocation: backLocation } + }); + }} + role="link" + tabIndex={0} + > +

{title}

+
+
+
Domain:
+
{domain}
+
+
+
Permission type:
+
+ + {permType} +
+
+
+
Private comment:
+
{privateComment}
+
+
+
Public comment:
+
{publicComment}
+
+
+
Subscription:
+
{subscriptionID}
+
+
+
+ { + e.preventDefault(); + e.stopPropagation(); + accept({ id, permType }); + }} + disabled={false} + showError={true} + result={acceptResult} + /> + { + e.preventDefault(); + e.stopPropagation(); + remove({ id }); + }} + disabled={false} + showError={true} + result={removeResult} + /> +
+
+ ); +} diff --git a/web/source/settings/views/moderation/domain-permissions/drafts/new.tsx b/web/source/settings/views/moderation/domain-permissions/drafts/new.tsx new file mode 100644 index 000000000..c78f8192a --- /dev/null +++ b/web/source/settings/views/moderation/domain-permissions/drafts/new.tsx @@ -0,0 +1,119 @@ +/* + 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 . +*/ + +import React from "react"; +import useFormSubmit from "../../../../lib/form/submit"; +import { useCreateDomainPermissionDraftMutation } from "../../../../lib/query/admin/domain-permissions/drafts"; +import { useBoolInput, useRadioInput, useTextInput } from "../../../../lib/form"; +import { formDomainValidator } from "../../../../lib/util/formvalidators"; +import MutationButton from "../../../../components/form/mutation-button"; +import { Checkbox, RadioGroup, TextArea, TextInput } from "../../../../components/form/inputs"; +import { useLocation } from "wouter"; +import { DomainPermissionDraftDocsLink, DomainPermissionDraftHelpText } from "./common"; + +export default function DomainPermissionDraftNew() { + const [ _location, setLocation ] = useLocation(); + + const form = { + domain: useTextInput("domain", { + validator: formDomainValidator, + }), + permission_type: useRadioInput("permission_type", { + options: { + block: "Block domain", + allow: "Allow domain", + } + }), + obfuscate: useBoolInput("obfuscate"), + public_comment: useTextInput("public_comment"), + private_comment: useTextInput("private_comment"), + }; + + const [formSubmit, result] = useFormSubmit( + form, + useCreateDomainPermissionDraftMutation(), + { + changedOnly: false, + onFinish: (res) => { + if (res.data) { + // Creation successful, + // redirect to drafts overview. + setLocation(`/drafts/search`); + } + }, + }); + + return ( +
+
+

New Domain Permission Draft

+

+ +
+ + + + + +