diff options
Diffstat (limited to 'web/source/settings/lib')
-rw-r--r-- | web/source/settings/lib/form/index.ts | 3 | ||||
-rw-r--r-- | web/source/settings/lib/form/number.tsx | 104 | ||||
-rw-r--r-- | web/source/settings/lib/form/submit.ts | 23 | ||||
-rw-r--r-- | web/source/settings/lib/form/types.ts | 7 | ||||
-rw-r--r-- | web/source/settings/lib/query/admin/domain-permissions/subscriptions.ts | 164 | ||||
-rw-r--r-- | web/source/settings/lib/query/gts-api.ts | 3 | ||||
-rw-r--r-- | web/source/settings/lib/types/domain-permission.ts | 154 | ||||
-rw-r--r-- | web/source/settings/lib/types/permsubcontenttype.ts | 20 | ||||
-rw-r--r-- | web/source/settings/lib/util/formvalidators.ts | 19 |
9 files changed, 494 insertions, 3 deletions
diff --git a/web/source/settings/lib/form/index.ts b/web/source/settings/lib/form/index.ts index 409ef0328..878b7c79b 100644 --- a/web/source/settings/lib/form/index.ts +++ b/web/source/settings/lib/form/index.ts @@ -41,6 +41,7 @@ import type { ChecklistInputHook, FieldArrayInputHook, ArrayInputHook, + NumberFormInputHook, } from "./types"; function capitalizeFirst(str: string) { @@ -102,11 +103,11 @@ function value<T>(name: string, initialValue: T) { name, Name: "", value: initialValue, - hasChanged: () => true, // always included }; } export const useTextInput = inputHook(text) as (_name: string, _opts?: HookOpts<string>) => TextFormInputHook; +export const useNumberInput = inputHook(text) as (_name: string, _opts?: HookOpts<number>) => NumberFormInputHook; export const useFileInput = inputHook(file) as (_name: string, _opts?: HookOpts<File>) => FileFormInputHook; export const useBoolInput = inputHook(bool) as (_name: string, _opts?: HookOpts<boolean>) => BoolFormInputHook; export const useRadioInput = inputHook(radio) as (_name: string, _opts?: HookOpts<string>) => RadioFormInputHook; diff --git a/web/source/settings/lib/form/number.tsx b/web/source/settings/lib/form/number.tsx new file mode 100644 index 000000000..15acf162a --- /dev/null +++ b/web/source/settings/lib/form/number.tsx @@ -0,0 +1,104 @@ +/* + 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, { + useState, + useRef, + useTransition, + useEffect, +} from "react"; + +import type { + CreateHookNames, + HookOpts, + NumberFormInputHook, +} from "./types"; + +const _default = 0; + +export default function useNumberInput( + { name, Name }: CreateHookNames, + { + initialValue = _default, + dontReset = false, + validator, + showValidation = true, + initValidation, + nosubmit = false, + }: HookOpts<number> +): NumberFormInputHook { + const [number, setNumber] = useState(initialValue); + const numberRef = useRef<HTMLInputElement>(null); + + const [validation, setValidation] = useState(initValidation ?? ""); + const [_isValidating, startValidation] = useTransition(); + const valid = validation == ""; + + function onChange(e: React.ChangeEvent<HTMLInputElement>) { + const input = e.target.valueAsNumber; + setNumber(input); + + if (validator) { + startValidation(() => { + setValidation(validator(input)); + }); + } + } + + function reset() { + if (!dontReset) { + setNumber(initialValue); + } + } + + useEffect(() => { + if (validator && numberRef.current) { + if (showValidation) { + numberRef.current.setCustomValidity(validation); + } else { + numberRef.current.setCustomValidity(""); + } + } + }, [validation, validator, showValidation]); + + // Array / Object hybrid, for easier access in different contexts + return Object.assign([ + onChange, + reset, + { + [name]: number, + [`${name}Ref`]: numberRef, + [`set${Name}`]: setNumber, + [`${name}Valid`]: valid, + } + ], { + onChange, + reset, + name, + Name: "", // Will be set by inputHook function. + nosubmit, + value: number, + ref: numberRef, + setter: setNumber, + valid, + validate: () => setValidation(validator ? validator(number): ""), + hasChanged: () => number != initialValue, + _default + }); +} diff --git a/web/source/settings/lib/form/submit.ts b/web/source/settings/lib/form/submit.ts index aa662086f..498481deb 100644 --- a/web/source/settings/lib/form/submit.ts +++ b/web/source/settings/lib/form/submit.ts @@ -34,8 +34,24 @@ import type { } from "./types"; interface UseFormSubmitOptions { + /** + * Include only changed fields when submitting the form. + * If no fields have been changed, submit will be a noop. + */ changedOnly: boolean; + /** + * Optional function to run when the form has been sent + * and a response has been returned from the server. + */ onFinish?: ((_res: any) => void); + /** + * Can be optionally used to modify the final mutation argument from the + * gathered mutation data before it's passed into the trigger function. + * + * Useful if the mutation trigger function takes not just a simple key/value + * object but a more complicated object. + */ + customizeMutationArgs?: (_mutationData: { [k: string]: any }) => any; } /** @@ -105,7 +121,7 @@ export default function useFormSubmit( usedAction.current = action; // Transform the hooked form into an object. - const { + let { mutationData, updatedFields, } = getFormMutations(form, { changedOnly }); @@ -117,7 +133,12 @@ export default function useFormSubmit( return; } + // Final tweaks on the mutation + // argument before triggering it. mutationData.action = action; + if (opts.customizeMutationArgs) { + mutationData = opts.customizeMutationArgs(mutationData); + } try { const res = await runMutation(mutationData); diff --git a/web/source/settings/lib/form/types.ts b/web/source/settings/lib/form/types.ts index 17fbec53a..6a4162f3a 100644 --- a/web/source/settings/lib/form/types.ts +++ b/web/source/settings/lib/form/types.ts @@ -181,6 +181,13 @@ export interface TextFormInputHook extends FormInputHook<string>, _withValidate, _withRef {} +export interface NumberFormInputHook extends FormInputHook<number>, + _withSetter<number>, + _withOnChange, + _withReset, + _withValidate, + _withRef {} + export interface RadioFormInputHook extends FormInputHook<string>, _withSetter<string>, _withOnChange, diff --git a/web/source/settings/lib/query/admin/domain-permissions/subscriptions.ts b/web/source/settings/lib/query/admin/domain-permissions/subscriptions.ts new file mode 100644 index 000000000..f065aaf54 --- /dev/null +++ b/web/source/settings/lib/query/admin/domain-permissions/subscriptions.ts @@ -0,0 +1,164 @@ +/* + GoToSocial + Copyright (C) GoToSocial Authors admin@gotosocial.org + SPDX-License-Identifier: AGPL-3.0-or-later + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU Affero General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU Affero General Public License for more details. + + You should have received a copy of the GNU Affero General Public License + along with this program. If not, see <http://www.gnu.org/licenses/>. +*/ + +import { gtsApi } from "../../gts-api"; + +import type { + DomainPermSub, + DomainPermSubCreateUpdateParams, + DomainPermSubSearchParams, + DomainPermSubSearchResp, +} from "../../../types/domain-permission"; +import parse from "parse-link-header"; +import { PermType } from "../../../types/perm"; + +const extended = gtsApi.injectEndpoints({ + endpoints: (build) => ({ + searchDomainPermissionSubscriptions: build.query<DomainPermSubSearchResp, DomainPermSubSearchParams>({ + query: (form) => { + const params = new(URLSearchParams); + Object.entries(form).forEach(([k, v]) => { + if (v !== undefined) { + params.append(k, v); + } + }); + + let query = ""; + if (params.size !== 0) { + query = `?${params.toString()}`; + } + + return { + url: `/api/v1/admin/domain_permission_subscriptions${query}` + }; + }, + // Headers required for paging. + transformResponse: (apiResp: DomainPermSub[], meta) => { + const subs = apiResp; + const linksStr = meta?.response?.headers.get("Link"); + const links = parse(linksStr); + return { subs, links }; + }, + // Only provide TRANSFORMED tag id since this model is not the same + // as getDomainPermissionSubscription model (due to transformResponse). + providesTags: [{ type: "DomainPermissionSubscription", id: "TRANSFORMED" }] + }), + + getDomainPermissionSubscriptionsPreview: build.query<DomainPermSub[], PermType>({ + query: (permType) => ({ + url: `/api/v1/admin/domain_permission_subscriptions/preview?permission_type=${permType}` + }), + providesTags: (_result, _error, permType) => + // Cache by permission type. + [{ type: "DomainPermissionSubscription", id: `${permType}sByPriority` }] + }), + + getDomainPermissionSubscription: build.query<DomainPermSub, string>({ + query: (id) => ({ + url: `/api/v1/admin/domain_permission_subscriptions/${id}` + }), + providesTags: (_result, _error, id) => [ + { type: 'DomainPermissionSubscription', id } + ], + }), + + createDomainPermissionSubscription: build.mutation<DomainPermSub, DomainPermSubCreateUpdateParams>({ + query: (formData) => ({ + method: "POST", + url: `/api/v1/admin/domain_permission_subscriptions`, + asForm: true, + body: formData, + discardEmpty: true + }), + invalidatesTags: (_res, _error, formData) => + [ + // Invalidate transformed list of all perm subs. + { type: "DomainPermissionSubscription", id: "TRANSFORMED" }, + // Invalidate perm subs of this type sorted by priority. + { type: "DomainPermissionSubscription", id: `${formData.permission_type}sByPriority` } + ] + }), + + updateDomainPermissionSubscription: build.mutation<DomainPermSub, { id: string, permType: PermType, formData: DomainPermSubCreateUpdateParams }>({ + query: ({ id, formData }) => ({ + method: "PATCH", + url: `/api/v1/admin/domain_permission_subscriptions/${id}`, + asForm: true, + body: formData, + }), + invalidatesTags: (_res, _error, { id, permType }) => + [ + // Invalidate this perm sub. + { type: "DomainPermissionSubscription", id: id }, + // Invalidate transformed list of all perms subs. + { type: "DomainPermissionSubscription", id: "TRANSFORMED" }, + // Invalidate perm subs of this type sorted by priority. + { type: "DomainPermissionSubscription", id: `${permType}sByPriority` } + ], + }), + + removeDomainPermissionSubscription: build.mutation<DomainPermSub, { id: string, remove_children: boolean }>({ + query: ({ id, remove_children }) => ({ + method: "POST", + url: `/api/v1/admin/domain_permission_subscriptions/${id}/remove`, + asForm: true, + body: { remove_children: remove_children }, + }), + }) + }), +}); + +/** + * View domain permission subscriptions. + */ +const useLazySearchDomainPermissionSubscriptionsQuery = extended.useLazySearchDomainPermissionSubscriptionsQuery; + +/** + * Get domain permission subscription with the given ID. + */ +const useGetDomainPermissionSubscriptionQuery = extended.useGetDomainPermissionSubscriptionQuery; + +/** + * Create a domain permission subscription with the given parameters. + */ +const useCreateDomainPermissionSubscriptionMutation = extended.useCreateDomainPermissionSubscriptionMutation; + +/** + * View domain permission subscriptions of selected perm type, sorted by priority descending. + */ +const useGetDomainPermissionSubscriptionsPreviewQuery = extended.useGetDomainPermissionSubscriptionsPreviewQuery; + +/** + * Update domain permission subscription. + */ +const useUpdateDomainPermissionSubscriptionMutation = extended.useUpdateDomainPermissionSubscriptionMutation; + +/** + * Remove a domain permission subscription and optionally its children (harsh). + */ +const useRemoveDomainPermissionSubscriptionMutation = extended.useRemoveDomainPermissionSubscriptionMutation; + +export { + useLazySearchDomainPermissionSubscriptionsQuery, + useGetDomainPermissionSubscriptionQuery, + useCreateDomainPermissionSubscriptionMutation, + useGetDomainPermissionSubscriptionsPreviewQuery, + useUpdateDomainPermissionSubscriptionMutation, + useRemoveDomainPermissionSubscriptionMutation, +}; diff --git a/web/source/settings/lib/query/gts-api.ts b/web/source/settings/lib/query/gts-api.ts index 9543819a9..34b66913a 100644 --- a/web/source/settings/lib/query/gts-api.ts +++ b/web/source/settings/lib/query/gts-api.ts @@ -170,7 +170,8 @@ export const gtsApi = createApi({ "DefaultInteractionPolicies", "InteractionRequest", "DomainPermissionDraft", - "DomainPermissionExclude" + "DomainPermissionExclude", + "DomainPermissionSubscription" ], endpoints: (build) => ({ instanceV1: build.query<InstanceV1, void>({ diff --git a/web/source/settings/lib/types/domain-permission.ts b/web/source/settings/lib/types/domain-permission.ts index 1a0a9bd0b..c4560d79b 100644 --- a/web/source/settings/lib/types/domain-permission.ts +++ b/web/source/settings/lib/types/domain-permission.ts @@ -20,6 +20,7 @@ import typia from "typia"; import { PermType } from "./perm"; import { Links } from "parse-link-header"; +import { PermSubContentType } from "./permsubcontenttype"; export const validateDomainPerms = typia.createValidate<DomainPerm[]>(); @@ -213,3 +214,156 @@ export interface DomainPermExcludeCreateParams { */ private_comment?: string; } + +/** + * API model of one domain permission susbcription. + */ +export interface DomainPermSub { + /** + * The ID of the domain permission subscription. + */ + id: string; + /** + * The priority of the domain permission subscription. + */ + priority: number; + /** + * Time at which the subscription was created (ISO 8601 Datetime). + */ + created_at: string; + /** + * Title of this subscription, as set by admin who created or updated it. + */ + title: string; + /** + * The type of domain permission subscription (allow, block). + */ + permission_type: PermType; + /** + * If true, domain permissions arising from this subscription will be created as drafts that must be approved by a moderator to take effect. + * If false, domain permissions from this subscription will come into force immediately. + */ + as_draft: boolean; + /** + * If true, this domain permission subscription will "adopt" domain permissions + * which already exist on the instance, and which meet the following conditions: + * 1) they have no subscription ID (ie., they're "orphaned") and 2) they are present + * in the subscribed list. Such orphaned domain permissions will be given this + * subscription's subscription ID value and be managed by this subscription. + */ + adopt_orphans: boolean; + /** + * ID of the account that created this subscription. + */ + created_by: string; + /** + * URI to call in order to fetch the permissions list. + */ + uri: string; + /** + * MIME content type to use when parsing the permissions list. + */ + content_type: PermSubContentType; + /** + * (Optional) username to set for basic auth when doing a fetch of URI. + */ + fetch_username?: string; + /** + * (Optional) password to set for basic auth when doing a fetch of URI. + */ + fetch_password?: string; + /** + * Time at which the most recent fetch was attempted (ISO 8601 Datetime). + */ + fetched_at?: string; + /** + * Time of the most recent successful fetch (ISO 8601 Datetime). + */ + successfully_fetched_at?: string; + /** + * If most recent fetch attempt failed, this field will contain an error message related to the fetch attempt. + */ + error?: string; + /** + * Count of domain permission entries discovered at URI on last (successful) fetch. + */ + count: number; +} + +/** + * Parameters for GET to /api/v1/admin/domain_permission_subscriptions. + */ +export interface DomainPermSubSearchParams { + /** + * Return only block or allow subscriptions. + */ + permission_type?: PermType; + /** + * Return only items *OLDER* than the given max ID (for paging downwards). + * The item with the specified ID will not be included in the response. + */ + max_id?: string; + /** + * Return only items *NEWER* than the given since ID. + * The item with the specified ID will not be included in the response. + */ + since_id?: string; + /** + * Return only items immediately *NEWER* than the given min ID (for paging upwards). + * The item with the specified ID will not be included in the response. + */ + min_id?: string; + /** + * Number of items to return. + */ + limit?: number; +} + +export interface DomainPermSubCreateUpdateParams { + /** + * The priority of the domain permission subscription. + */ + priority?: number; + /** + * Title of this subscription, as set by admin who created or updated it. + */ + title?: string; + /** + * URI to call in order to fetch the permissions list. + */ + uri: string; + /** + * MIME content type to use when parsing the permissions list. + */ + content_type: PermSubContentType; + /** + * If true, domain permissions arising from this subscription will be created as drafts that must be approved by a moderator to take effect. + * If false, domain permissions from this subscription will come into force immediately. + */ + as_draft?: boolean; + /** + * If true, this domain permission subscription will "adopt" domain permissions + * which already exist on the instance, and which meet the following conditions: + * 1) they have no subscription ID (ie., they're "orphaned") and 2) they are present + * in the subscribed list. Such orphaned domain permissions will be given this + * subscription's subscription ID value and be managed by this subscription. + */ + adopt_orphans?: boolean; + /** + * (Optional) username to set for basic auth when doing a fetch of URI. + */ + fetch_username?: string; + /** + * (Optional) password to set for basic auth when doing a fetch of URI. + */ + fetch_password?: string; + /** + * The type of domain permission subscription to create or update (allow, block). + */ + permission_type: PermType; +} + +export interface DomainPermSubSearchResp { + subs: DomainPermSub[]; + links: Links | null; +} diff --git a/web/source/settings/lib/types/permsubcontenttype.ts b/web/source/settings/lib/types/permsubcontenttype.ts new file mode 100644 index 000000000..0468aae4d --- /dev/null +++ b/web/source/settings/lib/types/permsubcontenttype.ts @@ -0,0 +1,20 @@ +/* + GoToSocial + Copyright (C) GoToSocial Authors admin@gotosocial.org + SPDX-License-Identifier: AGPL-3.0-or-later + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU Affero General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU Affero General Public License for more details. + + You should have received a copy of the GNU Affero General Public License + along with this program. If not, see <http://www.gnu.org/licenses/>. +*/ + +export type PermSubContentType = "text/plain" | "text/csv" | "application/json"; diff --git a/web/source/settings/lib/util/formvalidators.ts b/web/source/settings/lib/util/formvalidators.ts index c509cf59d..358db616c 100644 --- a/web/source/settings/lib/util/formvalidators.ts +++ b/web/source/settings/lib/util/formvalidators.ts @@ -46,3 +46,22 @@ export function formDomainValidator(domain: string): string { return "invalid domain"; } + +export function urlValidator(urlStr: string): string { + if (urlStr.length === 0) { + return ""; + } + + let url: URL; + try { + url = new URL(urlStr); + } catch (e) { + return e.message; + } + + if (url.protocol !== "http:" && url.protocol !== "https:") { + return `invalid protocol, must be http or https`; + } + + return formDomainValidator(url.host); +} |