mirror of
https://github.com/superseriousbusiness/gotosocial.git
synced 2025-11-25 14:53:32 -06:00
[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
This commit is contained in:
parent
c2029df9bc
commit
301543616b
69 changed files with 5664 additions and 264 deletions
|
|
@ -39,37 +39,47 @@ import { NoArg } from "../../../lib/types/query";
|
|||
import { Error } from "../../../components/error";
|
||||
import { useBaseUrl } from "../../../lib/navigation/util";
|
||||
import { PermType } from "../../../lib/types/perm";
|
||||
import isValidDomain from "is-valid-domain";
|
||||
import { useCapitalize } from "../../../lib/util";
|
||||
import { formDomainValidator } from "../../../lib/util/formvalidators";
|
||||
|
||||
export default function DomainPermDetail() {
|
||||
const baseUrl = useBaseUrl();
|
||||
|
||||
// Parse perm type from routing params.
|
||||
let params = useParams();
|
||||
if (params.permType !== "blocks" && params.permType !== "allows") {
|
||||
const search = useSearch();
|
||||
|
||||
// Parse perm type from routing params, converting
|
||||
// "blocks" => "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" });
|
||||
// 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" });
|
||||
|
||||
let isLoading;
|
||||
switch (permType) {
|
||||
case "block":
|
||||
isLoading = isLoadingDomainBlocks;
|
||||
break;
|
||||
case "allow":
|
||||
isLoading = isLoadingDomainAllows;
|
||||
break;
|
||||
default:
|
||||
throw "perm type unknown";
|
||||
// Wait until we're done loading.
|
||||
const loading = permType === "block"
|
||||
? loadingBlocks || fetchingBlocks
|
||||
: loadingAllows || fetchingAllows;
|
||||
if (loading) {
|
||||
return <Loading />;
|
||||
}
|
||||
|
||||
// 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 = <Loading />;
|
||||
} else if (existingPerm == undefined) {
|
||||
infoContent = <span>No stored {permType} yet, you can add one below:</span>;
|
||||
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>
|
||||
<b>Editing existing domain {permTypeRaw} 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}/${permType}s`}/> Domain {permType} for: <span title={domain}>{domain}</span></h1>
|
||||
<h1 className="text-cutoff">
|
||||
<BackButton to={`~${baseUrl}/${permTypeRaw}`} />
|
||||
{" "}
|
||||
Domain {permType} for {domain}
|
||||
</h1>
|
||||
{infoContent}
|
||||
<DomainPermForm
|
||||
defaultDomain={domain}
|
||||
|
|
@ -143,28 +158,7 @@ function DomainPermForm({ defaultDomain, perm, permType }: DomainPermFormProps)
|
|||
domain: useTextInput("domain", {
|
||||
source: perm,
|
||||
defaultValue: defaultDomain,
|
||||
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,
|
||||
}),
|
||||
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();
|
||||
|
||||
|
|
|
|||
|
|
@ -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 <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
import React from "react";
|
||||
|
||||
export function DomainPermissionDraftHelpText() {
|
||||
return (
|
||||
<>
|
||||
Domain permission drafts are domain block or domain allow entries that are not yet in force.
|
||||
<br/>
|
||||
You can choose to accept or remove a draft.
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
export function DomainPermissionDraftDocsLink() {
|
||||
return (
|
||||
<a
|
||||
href="https://docs.gotosocial.org/en/latest/admin/settings/#domain-permission-drafts"
|
||||
target="_blank"
|
||||
className="docslink"
|
||||
rel="noreferrer"
|
||||
>
|
||||
Learn more about domain permission drafts (opens in a new tab)
|
||||
</a>
|
||||
);
|
||||
}
|
||||
|
|
@ -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 <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
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 <Loading />;
|
||||
} else if (isError) {
|
||||
return <ErrorC error={error} />;
|
||||
} else if (permDraft === undefined) {
|
||||
return <ErrorC error={new Error("permission draft was undefined")} />;
|
||||
}
|
||||
|
||||
const created = permDraft.created_at ? new Date(permDraft.created_at).toDateString(): "unknown";
|
||||
const domain = permDraft.domain;
|
||||
const permType = permDraft.permission_type;
|
||||
if (!permType) {
|
||||
return <ErrorC error={new Error("permission_type was undefined")} />;
|
||||
}
|
||||
const publicComment = permDraft.public_comment ?? "[none]";
|
||||
const privateComment = permDraft.private_comment ?? "[none]";
|
||||
const subscriptionID = permDraft.subscription_id ?? "[none]";
|
||||
|
||||
return (
|
||||
<div className="domain-permission-draft-details">
|
||||
<h1><BackButton to={backLocation} /> Domain Permission Draft Detail</h1>
|
||||
<dl className="info-list">
|
||||
<div className="info-list-entry">
|
||||
<dt>Created</dt>
|
||||
<dd><time dateTime={permDraft.created_at}>{created}</time></dd>
|
||||
</div>
|
||||
<div className="info-list-entry">
|
||||
<dt>Created By</dt>
|
||||
<dd>
|
||||
<UsernameLozenge
|
||||
account={permDraft.created_by}
|
||||
linkTo={`~/settings/moderation/accounts/${permDraft.created_by}`}
|
||||
backLocation={`~${location}`}
|
||||
/>
|
||||
</dd>
|
||||
</div>
|
||||
<div className="info-list-entry">
|
||||
<dt>Domain</dt>
|
||||
<dd>{domain}</dd>
|
||||
</div>
|
||||
<div className="info-list-entry">
|
||||
<dt>Permission type</dt>
|
||||
<dd className={`permission-type ${permType}`}>
|
||||
<i
|
||||
aria-hidden={true}
|
||||
className={`fa fa-${permType === "allow" ? "check" : "close"}`}
|
||||
></i>
|
||||
{permType}
|
||||
</dd>
|
||||
</div>
|
||||
<div className="info-list-entry">
|
||||
<dt>Private comment</dt>
|
||||
<dd>{privateComment}</dd>
|
||||
</div>
|
||||
<div className="info-list-entry">
|
||||
<dt>Public comment</dt>
|
||||
<dd>{publicComment}</dd>
|
||||
</div>
|
||||
<div className="info-list-entry">
|
||||
<dt>Subscription ID</dt>
|
||||
<dd>{subscriptionID}</dd>
|
||||
</div>
|
||||
</dl>
|
||||
<HandleDraft
|
||||
id={id}
|
||||
permType={permType}
|
||||
backLocation={backLocation}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
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>
|
||||
<Select
|
||||
field={form.acceptOrRemove}
|
||||
label="Accept or remove draft"
|
||||
options={
|
||||
<>
|
||||
<option value="accept">Accept</option>
|
||||
<option value="remove">Remove</option>
|
||||
</>
|
||||
}
|
||||
></Select>
|
||||
|
||||
{ form.acceptOrRemove.value === "accept" &&
|
||||
<>
|
||||
<Checkbox
|
||||
field={form.overwrite}
|
||||
label={`Overwrite any existing ${permType} for this domain`}
|
||||
/>
|
||||
</>
|
||||
}
|
||||
|
||||
{ form.acceptOrRemove.value === "remove" &&
|
||||
<>
|
||||
<Checkbox
|
||||
field={form.exclude_target}
|
||||
label={`Add a domain permission exclude for this domain`}
|
||||
/>
|
||||
</>
|
||||
}
|
||||
|
||||
<MutationButton
|
||||
label={
|
||||
form.acceptOrRemove.value === "accept"
|
||||
? `Accept ${permType}`
|
||||
: "Remove draft"
|
||||
}
|
||||
type="button"
|
||||
className={
|
||||
form.acceptOrRemove.value === "accept"
|
||||
? "button"
|
||||
: "button danger"
|
||||
}
|
||||
onClick={onClick}
|
||||
disabled={false}
|
||||
showError={true}
|
||||
result={
|
||||
form.acceptOrRemove.value === "accept"
|
||||
? acceptResult
|
||||
: removeResult
|
||||
}
|
||||
/>
|
||||
</form>
|
||||
);
|
||||
}
|
||||
|
|
@ -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 <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
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 (
|
||||
<div className="domain-permission-drafts-view">
|
||||
<div className="form-section-docs">
|
||||
<h1>Domain Permission Drafts</h1>
|
||||
<p>
|
||||
You can use the form below to search through domain permission drafts.
|
||||
<br/>
|
||||
<DomainPermissionDraftHelpText />
|
||||
</p>
|
||||
<DomainPermissionDraftDocsLink />
|
||||
</div>
|
||||
<DomainPermissionDraftsSearchForm />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
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 (
|
||||
<DraftListEntry
|
||||
key={draft.id}
|
||||
permDraft={draft}
|
||||
linkTo={`/drafts/${draft.id}`}
|
||||
backLocation={backLocation}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<form
|
||||
onSubmit={submitQuery}
|
||||
// Prevent password managers
|
||||
// trying to fill in fields.
|
||||
autoComplete="off"
|
||||
>
|
||||
<Select
|
||||
field={form.permission_type}
|
||||
label="Permission type"
|
||||
options={
|
||||
<>
|
||||
<option value="">Any</option>
|
||||
<option value="block">Block</option>
|
||||
<option value="allow">Allow</option>
|
||||
</>
|
||||
}
|
||||
></Select>
|
||||
<TextInput
|
||||
field={form.domain}
|
||||
label={`Domain (without "https://" prefix)`}
|
||||
placeholder="example.org"
|
||||
autoCapitalize="none"
|
||||
spellCheck="false"
|
||||
/>
|
||||
<Select
|
||||
field={form.limit}
|
||||
label="Items per page"
|
||||
options={
|
||||
<>
|
||||
<option value="20">20</option>
|
||||
<option value="50">50</option>
|
||||
<option value="100">100</option>
|
||||
</>
|
||||
}
|
||||
></Select>
|
||||
<MutationButton
|
||||
disabled={false}
|
||||
label={"Search"}
|
||||
result={searchRes}
|
||||
/>
|
||||
</form>
|
||||
<PageableList
|
||||
isLoading={searchRes.isLoading}
|
||||
isFetching={searchRes.isFetching}
|
||||
isSuccess={searchRes.isSuccess}
|
||||
items={searchRes.data?.drafts}
|
||||
itemToEntry={itemToEntry}
|
||||
isError={searchRes.isError}
|
||||
error={searchRes.error}
|
||||
emptyMessage={<b>No drafts found that match your query.</b>}
|
||||
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 <ErrorC error={new Error("permission_type was undefined")} />;
|
||||
}
|
||||
|
||||
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 <ErrorC error={new Error("id was undefined")} />;
|
||||
}
|
||||
|
||||
const title = `${permTypeUpper} ${domain}`;
|
||||
|
||||
return (
|
||||
<span
|
||||
className={`pseudolink domain-permission-draft entry ${permType}`}
|
||||
aria-label={title}
|
||||
title={title}
|
||||
onClick={() => {
|
||||
// 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}
|
||||
>
|
||||
<h3>{title}</h3>
|
||||
<dl className="info-list">
|
||||
<div className="info-list-entry">
|
||||
<dt>Domain:</dt>
|
||||
<dd className="text-cutoff">{domain}</dd>
|
||||
</div>
|
||||
<div className="info-list-entry">
|
||||
<dt>Permission type:</dt>
|
||||
<dd className={`permission-type ${permType}`}>
|
||||
<i
|
||||
aria-hidden={true}
|
||||
className={`fa fa-${permType === "allow" ? "check" : "close"}`}
|
||||
></i>
|
||||
{permType}
|
||||
</dd>
|
||||
</div>
|
||||
<div className="info-list-entry">
|
||||
<dt>Private comment:</dt>
|
||||
<dd className="text-cutoff">{privateComment}</dd>
|
||||
</div>
|
||||
<div className="info-list-entry">
|
||||
<dt>Public comment:</dt>
|
||||
<dd>{publicComment}</dd>
|
||||
</div>
|
||||
<div className="info-list-entry">
|
||||
<dt>Subscription:</dt>
|
||||
<dd className="text-cutoff">{subscriptionID}</dd>
|
||||
</div>
|
||||
</dl>
|
||||
<div className="action-buttons">
|
||||
<MutationButton
|
||||
label={`Accept ${permType}`}
|
||||
title={`Accept ${permType}`}
|
||||
type="button"
|
||||
className="button"
|
||||
onClick={(e) => {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
accept({ id, permType });
|
||||
}}
|
||||
disabled={false}
|
||||
showError={true}
|
||||
result={acceptResult}
|
||||
/>
|
||||
<MutationButton
|
||||
label={`Remove draft`}
|
||||
title={`Remove draft`}
|
||||
type="button"
|
||||
className="button danger"
|
||||
onClick={(e) => {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
remove({ id });
|
||||
}}
|
||||
disabled={false}
|
||||
showError={true}
|
||||
result={removeResult}
|
||||
/>
|
||||
</div>
|
||||
</span>
|
||||
);
|
||||
}
|
||||
|
|
@ -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 <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
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 (
|
||||
<form
|
||||
onSubmit={formSubmit}
|
||||
// Prevent password managers
|
||||
// trying to fill in fields.
|
||||
autoComplete="off"
|
||||
>
|
||||
<div className="form-section-docs">
|
||||
<h2>New Domain Permission Draft</h2>
|
||||
<p><DomainPermissionDraftHelpText /></p>
|
||||
<DomainPermissionDraftDocsLink />
|
||||
</div>
|
||||
|
||||
<RadioGroup
|
||||
field={form.permission_type}
|
||||
/>
|
||||
|
||||
<TextInput
|
||||
field={form.domain}
|
||||
label={`Domain (without "https://" prefix)`}
|
||||
placeholder="example.org"
|
||||
autoCapitalize="none"
|
||||
spellCheck="false"
|
||||
/>
|
||||
|
||||
<TextArea
|
||||
field={form.private_comment}
|
||||
label={"Private comment"}
|
||||
placeholder="This domain is like unto a clown car full of clowns, I suggest we block it forthwith."
|
||||
autoCapitalize="sentences"
|
||||
rows={3}
|
||||
/>
|
||||
|
||||
<TextArea
|
||||
field={form.public_comment}
|
||||
label={"Public comment"}
|
||||
placeholder="Bad posters"
|
||||
autoCapitalize="sentences"
|
||||
rows={3}
|
||||
/>
|
||||
|
||||
<Checkbox
|
||||
field={form.obfuscate}
|
||||
label="Obfuscate domain in public lists"
|
||||
/>
|
||||
|
||||
<MutationButton
|
||||
label="Save"
|
||||
result={result}
|
||||
disabled={
|
||||
!form.domain.value ||
|
||||
!form.domain.valid ||
|
||||
!form.permission_type.value
|
||||
}
|
||||
/>
|
||||
</form>
|
||||
);
|
||||
}
|
||||
|
|
@ -0,0 +1,54 @@
|
|||
/*
|
||||
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";
|
||||
|
||||
export function DomainPermissionExcludeHelpText() {
|
||||
return (
|
||||
<>
|
||||
Domain permission excludes prevent permissions for a domain (and all
|
||||
subdomains) from being auomatically managed by domain permission subscriptions.
|
||||
<br/>
|
||||
For example, if you create an exclude entry for <code>example.org</code>, then
|
||||
a blocklist or allowlist subscription will <em>exclude</em> entries for <code>example.org</code>
|
||||
and any of its subdomains (<code>sub.example.org</code>, <code>another.sub.example.org</code> etc.)
|
||||
when creating domain permission drafts and domain blocks/allows.
|
||||
<br/>
|
||||
This functionality allows you to manually manage permissions for excluded domains,
|
||||
in cases where you know you definitely do or don't want to federate with a given domain,
|
||||
no matter what entries are contained in a domain permission subscription.
|
||||
<br/>
|
||||
Note that by itself, creation of an exclude entry for a given domain does not affect
|
||||
federation with that domain at all, it is only useful in combination with permission subscriptions.
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
export function DomainPermissionExcludeDocsLink() {
|
||||
return (
|
||||
<a
|
||||
href="https://docs.gotosocial.org/en/latest/admin/settings/#domain-permission-excludes"
|
||||
target="_blank"
|
||||
className="docslink"
|
||||
rel="noreferrer"
|
||||
>
|
||||
Learn more about domain permission excludes (opens in a new tab)
|
||||
</a>
|
||||
);
|
||||
}
|
||||
|
|
@ -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 <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
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 { Error as ErrorC } from "../../../../components/error";
|
||||
import UsernameLozenge from "../../../../components/username-lozenge";
|
||||
import { useDeleteDomainPermissionExcludeMutation, useGetDomainPermissionExcludeQuery } from "../../../../lib/query/admin/domain-permissions/excludes";
|
||||
import MutationButton from "../../../../components/form/mutation-button";
|
||||
|
||||
export default function DomainPermissionExcludeDetail() {
|
||||
const baseUrl = useBaseUrl();
|
||||
const backLocation: string = history.state?.backLocation ?? `~${baseUrl}`;
|
||||
|
||||
const params = useParams();
|
||||
let id = params.excludeId as string | undefined;
|
||||
if (!id) {
|
||||
throw "no perm ID";
|
||||
}
|
||||
|
||||
const {
|
||||
data: permExclude,
|
||||
isLoading,
|
||||
isFetching,
|
||||
isError,
|
||||
error,
|
||||
} = useGetDomainPermissionExcludeQuery(id);
|
||||
|
||||
if (isLoading || isFetching) {
|
||||
return <Loading />;
|
||||
} else if (isError) {
|
||||
return <ErrorC error={error} />;
|
||||
} else if (permExclude === undefined) {
|
||||
return <ErrorC error={new Error("permission exclude was undefined")} />;
|
||||
}
|
||||
|
||||
const created = permExclude.created_at ? new Date(permExclude.created_at).toDateString(): "unknown";
|
||||
const domain = permExclude.domain;
|
||||
const privateComment = permExclude.private_comment ?? "[none]";
|
||||
|
||||
return (
|
||||
<div className="domain-permission-exclude-details">
|
||||
<h1><BackButton to={backLocation} /> Domain Permission Exclude Detail</h1>
|
||||
<dl className="info-list">
|
||||
<div className="info-list-entry">
|
||||
<dt>Created</dt>
|
||||
<dd><time dateTime={permExclude.created_at}>{created}</time></dd>
|
||||
</div>
|
||||
<div className="info-list-entry">
|
||||
<dt>Created By</dt>
|
||||
<dd>
|
||||
<UsernameLozenge
|
||||
account={permExclude.created_by}
|
||||
linkTo={`~/settings/moderation/accounts/${permExclude.created_by}`}
|
||||
backLocation={`~${location}`}
|
||||
/>
|
||||
</dd>
|
||||
</div>
|
||||
<div className="info-list-entry">
|
||||
<dt>Domain</dt>
|
||||
<dd>{domain}</dd>
|
||||
</div>
|
||||
<div className="info-list-entry">
|
||||
<dt>Private comment</dt>
|
||||
<dd>{privateComment}</dd>
|
||||
</div>
|
||||
</dl>
|
||||
<HandleExclude
|
||||
id={id}
|
||||
backLocation={backLocation}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function HandleExclude({ id, backLocation}: {id: string, backLocation: string}) {
|
||||
const [_location, setLocation] = useLocation();
|
||||
const [ deleteExclude, deleteResult ] = useDeleteDomainPermissionExcludeMutation();
|
||||
|
||||
return (
|
||||
<MutationButton
|
||||
label={`Delete exclude`}
|
||||
title={`Delete exclude`}
|
||||
type="button"
|
||||
className="button danger"
|
||||
onClick={(e) => {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
deleteExclude(id).then(res => {
|
||||
if ("data" in res) {
|
||||
setLocation(backLocation);
|
||||
}
|
||||
});
|
||||
}}
|
||||
disabled={false}
|
||||
showError={true}
|
||||
result={deleteResult}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
|
@ -0,0 +1,235 @@
|
|||
/*
|
||||
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, { 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 { useDeleteDomainPermissionExcludeMutation, useLazySearchDomainPermissionExcludesQuery } from "../../../../lib/query/admin/domain-permissions/excludes";
|
||||
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 { DomainPermissionExcludeDocsLink, DomainPermissionExcludeHelpText } from "./common";
|
||||
|
||||
export default function DomainPermissionExcludesSearch() {
|
||||
return (
|
||||
<div className="domain-permission-excludes-view">
|
||||
<div className="form-section-docs">
|
||||
<h1>Domain Permission Excludes</h1>
|
||||
<p>
|
||||
You can use the form below to search through domain permission excludes.
|
||||
<br/>
|
||||
<DomainPermissionExcludeHelpText />
|
||||
</p>
|
||||
<DomainPermissionExcludeDocsLink />
|
||||
</div>
|
||||
<DomainPermissionExcludesSearchForm />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function DomainPermissionExcludesSearchForm() {
|
||||
const [ location, setLocation ] = useLocation();
|
||||
const search = useSearch();
|
||||
const urlQueryParams = useMemo(() => new URLSearchParams(search), [search]);
|
||||
const hasParams = urlQueryParams.size != 0;
|
||||
const [ searchExcludes, searchRes ] = useLazySearchDomainPermissionExcludesQuery();
|
||||
|
||||
const form = {
|
||||
domain: useTextInput("domain", {
|
||||
defaultValue: urlQueryParams.get("domain") ?? "",
|
||||
validator: formDomainValidator,
|
||||
}),
|
||||
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) {
|
||||
searchExcludes(Object.fromEntries(urlQueryParams));
|
||||
} else {
|
||||
setLocation(location + "?limit=20");
|
||||
}
|
||||
}, [
|
||||
urlQueryParams,
|
||||
hasParams,
|
||||
searchExcludes,
|
||||
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(exclude: DomainPerm): ReactNode {
|
||||
return (
|
||||
<ExcludeListEntry
|
||||
key={exclude.id}
|
||||
permExclude={exclude}
|
||||
linkTo={`/excludes/${exclude.id}`}
|
||||
backLocation={backLocation}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<form
|
||||
onSubmit={submitQuery}
|
||||
// Prevent password managers
|
||||
// trying to fill in fields.
|
||||
autoComplete="off"
|
||||
>
|
||||
<TextInput
|
||||
field={form.domain}
|
||||
label={`Domain (without "https://" prefix)`}
|
||||
placeholder="example.org"
|
||||
autoCapitalize="none"
|
||||
spellCheck="false"
|
||||
/>
|
||||
<Select
|
||||
field={form.limit}
|
||||
label="Items per page"
|
||||
options={
|
||||
<>
|
||||
<option value="20">20</option>
|
||||
<option value="50">50</option>
|
||||
<option value="100">100</option>
|
||||
</>
|
||||
}
|
||||
></Select>
|
||||
<MutationButton
|
||||
disabled={false}
|
||||
label={"Search"}
|
||||
result={searchRes}
|
||||
/>
|
||||
</form>
|
||||
<PageableList
|
||||
isLoading={searchRes.isLoading}
|
||||
isFetching={searchRes.isFetching}
|
||||
isSuccess={searchRes.isSuccess}
|
||||
items={searchRes.data?.excludes}
|
||||
itemToEntry={itemToEntry}
|
||||
isError={searchRes.isError}
|
||||
error={searchRes.error}
|
||||
emptyMessage={<b>No excludes found that match your query.</b>}
|
||||
prevNextLinks={searchRes.data?.links}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
interface ExcludeEntryProps {
|
||||
permExclude: DomainPerm;
|
||||
linkTo: string;
|
||||
backLocation: string;
|
||||
}
|
||||
|
||||
function ExcludeListEntry({ permExclude, linkTo, backLocation }: ExcludeEntryProps) {
|
||||
const [ _location, setLocation ] = useLocation();
|
||||
const [ deleteExclude, deleteResult ] = useDeleteDomainPermissionExcludeMutation();
|
||||
|
||||
const domain = permExclude.domain;
|
||||
const privateComment = permExclude.private_comment ?? "[none]";
|
||||
const id = permExclude.id;
|
||||
if (!id) {
|
||||
return <ErrorC error={new Error("id was undefined")} />;
|
||||
}
|
||||
|
||||
return (
|
||||
<span
|
||||
className={`pseudolink domain-permission-exclude entry`}
|
||||
aria-label={`Exclude ${domain}`}
|
||||
title={`Exclude ${domain}`}
|
||||
onClick={() => {
|
||||
// When clicking on a exclude, direct
|
||||
// to the detail view for that exclude.
|
||||
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}
|
||||
>
|
||||
<dl className="info-list">
|
||||
<div className="info-list-entry">
|
||||
<dt>Domain:</dt>
|
||||
<dd className="text-cutoff">{domain}</dd>
|
||||
</div>
|
||||
<div className="info-list-entry">
|
||||
<dt>Private comment:</dt>
|
||||
<dd className="text-cutoff">{privateComment}</dd>
|
||||
</div>
|
||||
</dl>
|
||||
<div className="action-buttons">
|
||||
<MutationButton
|
||||
label={`Delete exclude`}
|
||||
title={`Delete exclude`}
|
||||
type="button"
|
||||
className="button danger"
|
||||
onClick={(e) => {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
deleteExclude(id);
|
||||
}}
|
||||
disabled={false}
|
||||
showError={true}
|
||||
result={deleteResult}
|
||||
/>
|
||||
</div>
|
||||
</span>
|
||||
);
|
||||
}
|
||||
|
|
@ -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 useFormSubmit from "../../../../lib/form/submit";
|
||||
import { useCreateDomainPermissionExcludeMutation } from "../../../../lib/query/admin/domain-permissions/excludes";
|
||||
import { useTextInput } from "../../../../lib/form";
|
||||
import { formDomainValidator } from "../../../../lib/util/formvalidators";
|
||||
import MutationButton from "../../../../components/form/mutation-button";
|
||||
import { TextArea, TextInput } from "../../../../components/form/inputs";
|
||||
import { useLocation } from "wouter";
|
||||
import { DomainPermissionExcludeDocsLink, DomainPermissionExcludeHelpText } from "./common";
|
||||
|
||||
export default function DomainPermissionExcludeNew() {
|
||||
const [ _location, setLocation ] = useLocation();
|
||||
|
||||
const form = {
|
||||
domain: useTextInput("domain", {
|
||||
validator: formDomainValidator,
|
||||
}),
|
||||
private_comment: useTextInput("private_comment"),
|
||||
};
|
||||
|
||||
const [formSubmit, result] = useFormSubmit(
|
||||
form,
|
||||
useCreateDomainPermissionExcludeMutation(),
|
||||
{
|
||||
changedOnly: false,
|
||||
onFinish: (res) => {
|
||||
if (res.data) {
|
||||
// Creation successful,
|
||||
// redirect to excludes overview.
|
||||
setLocation(`/excludes/search`);
|
||||
}
|
||||
},
|
||||
});
|
||||
|
||||
return (
|
||||
<form
|
||||
onSubmit={formSubmit}
|
||||
// Prevent password managers
|
||||
// trying to fill in fields.
|
||||
autoComplete="off"
|
||||
>
|
||||
<div className="form-section-docs">
|
||||
<h2>New Domain Permission Exclude</h2>
|
||||
<p><DomainPermissionExcludeHelpText /></p>
|
||||
<DomainPermissionExcludeDocsLink />
|
||||
</div>
|
||||
|
||||
<TextInput
|
||||
field={form.domain}
|
||||
label={`Domain (without "https://" prefix)`}
|
||||
placeholder="example.org"
|
||||
autoCapitalize="none"
|
||||
spellCheck="false"
|
||||
/>
|
||||
|
||||
<TextArea
|
||||
field={form.private_comment}
|
||||
label={"Private comment"}
|
||||
placeholder="Created an exclude for this domain because we should manage it manually."
|
||||
autoCapitalize="sentences"
|
||||
rows={3}
|
||||
/>
|
||||
|
||||
<MutationButton
|
||||
label="Save"
|
||||
result={result}
|
||||
disabled={!form.domain.value || !form.domain.valid}
|
||||
/>
|
||||
</form>
|
||||
);
|
||||
}
|
||||
|
|
@ -30,6 +30,7 @@ import type { MappedDomainPerms } from "../../../lib/types/domain-permission";
|
|||
import { NoArg } from "../../../lib/types/query";
|
||||
import { PermType } from "../../../lib/types/perm";
|
||||
import { useBaseUrl } from "../../../lib/navigation/util";
|
||||
import { useCapitalize } from "../../../lib/util";
|
||||
|
||||
export default function DomainPermissionsOverview() {
|
||||
const baseUrl = useBaseUrl();
|
||||
|
|
@ -42,9 +43,7 @@ export default function DomainPermissionsOverview() {
|
|||
const permType = params.permType.slice(0, -1) as PermType;
|
||||
|
||||
// Uppercase first letter of given permType.
|
||||
const permTypeUpper = useMemo(() => {
|
||||
return permType.charAt(0).toUpperCase() + permType.slice(1);
|
||||
}, [permType]);
|
||||
const permTypeUpper = useCapitalize(permType);
|
||||
|
||||
// Fetch / wait for desired perms to load.
|
||||
const { data: blocks, isLoading: isLoadingBlocks } = useDomainBlocksQuery(NoArg, { skip: permType !== "block" });
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue