From 637f188ebec71fe4b0b80bbab4592d4c269d7d93 Mon Sep 17 00:00:00 2001
From: tobi <31960611+tsmethurst@users.noreply.github.com>
Date: Tue, 17 Oct 2023 12:46:06 +0200
Subject: [feature] Allow import/export/creation of domain allows via admin
panel (#2264)
* it's happening!
* aaa
* fix silly whoopsie
* it's working pa! it's working ma!
* model report parameters
* shuffle some more stuff around
* getting there
* oo hoo
* finish tidying up for now
* aaa
* fix use form submit errors
* peepee poo poo
* aaaaa
* ffff
* they see me typin', they hatin'
* boop
* aaa
* oooo
* typing typing tappa tappa
* almost done typing
* weee
* alright
* push it push it real good doo doo doo doo doo doo
* thingy no worky
* almost done
* mutation modifers not quite right
* hmm
* it works
* view blocks + allows nicely
* it works!
* typia install
* the old linterino
* linter plz
---
web/source/settings/lib/form/bool.jsx | 51 ----
web/source/settings/lib/form/bool.tsx | 60 +++++
web/source/settings/lib/form/check-list.jsx | 181 --------------
web/source/settings/lib/form/check-list.tsx | 219 +++++++++++++++++
web/source/settings/lib/form/combo-box.jsx | 58 -----
web/source/settings/lib/form/combo-box.tsx | 67 ++++++
web/source/settings/lib/form/field-array.jsx | 63 -----
web/source/settings/lib/form/field-array.tsx | 85 +++++++
web/source/settings/lib/form/file.jsx | 90 -------
web/source/settings/lib/form/file.tsx | 112 +++++++++
web/source/settings/lib/form/form-with-data.jsx | 43 ----
web/source/settings/lib/form/form-with-data.tsx | 60 +++++
web/source/settings/lib/form/get-form-mutations.js | 45 ----
web/source/settings/lib/form/get-form-mutations.ts | 47 ++++
web/source/settings/lib/form/index.js | 83 -------
web/source/settings/lib/form/index.ts | 114 +++++++++
web/source/settings/lib/form/radio.jsx | 52 ----
web/source/settings/lib/form/radio.tsx | 60 +++++
web/source/settings/lib/form/submit.js | 67 ------
web/source/settings/lib/form/submit.ts | 140 +++++++++++
web/source/settings/lib/form/text.jsx | 87 -------
web/source/settings/lib/form/text.tsx | 102 ++++++++
web/source/settings/lib/form/types.ts | 264 +++++++++++++++++++++
23 files changed, 1330 insertions(+), 820 deletions(-)
delete mode 100644 web/source/settings/lib/form/bool.jsx
create mode 100644 web/source/settings/lib/form/bool.tsx
delete mode 100644 web/source/settings/lib/form/check-list.jsx
create mode 100644 web/source/settings/lib/form/check-list.tsx
delete mode 100644 web/source/settings/lib/form/combo-box.jsx
create mode 100644 web/source/settings/lib/form/combo-box.tsx
delete mode 100644 web/source/settings/lib/form/field-array.jsx
create mode 100644 web/source/settings/lib/form/field-array.tsx
delete mode 100644 web/source/settings/lib/form/file.jsx
create mode 100644 web/source/settings/lib/form/file.tsx
delete mode 100644 web/source/settings/lib/form/form-with-data.jsx
create mode 100644 web/source/settings/lib/form/form-with-data.tsx
delete mode 100644 web/source/settings/lib/form/get-form-mutations.js
create mode 100644 web/source/settings/lib/form/get-form-mutations.ts
delete mode 100644 web/source/settings/lib/form/index.js
create mode 100644 web/source/settings/lib/form/index.ts
delete mode 100644 web/source/settings/lib/form/radio.jsx
create mode 100644 web/source/settings/lib/form/radio.tsx
delete mode 100644 web/source/settings/lib/form/submit.js
create mode 100644 web/source/settings/lib/form/submit.ts
delete mode 100644 web/source/settings/lib/form/text.jsx
create mode 100644 web/source/settings/lib/form/text.tsx
create mode 100644 web/source/settings/lib/form/types.ts
(limited to 'web/source/settings/lib/form')
diff --git a/web/source/settings/lib/form/bool.jsx b/web/source/settings/lib/form/bool.jsx
deleted file mode 100644
index 47a4bbd1b..000000000
--- a/web/source/settings/lib/form/bool.jsx
+++ /dev/null
@@ -1,51 +0,0 @@
-/*
- GoToSocial
- Copyright (C) GoToSocial Authors admin@gotosocial.org
- SPDX-License-Identifier: AGPL-3.0-or-later
-
- This program is free software: you can redistribute it and/or modify
- it under the terms of the GNU Affero General Public License as published by
- the Free Software Foundation, either version 3 of the License, or
- (at your option) any later version.
-
- This program is distributed in the hope that it will be useful,
- but WITHOUT ANY WARRANTY; without even the implied warranty of
- MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
- GNU Affero General Public License for more details.
-
- You should have received a copy of the GNU Affero General Public License
- along with this program. If not, see .
-*/
-
-const React = require("react");
-
-const _default = false;
-module.exports = function useBoolInput({ name, Name }, { initialValue = _default }) {
- const [value, setValue] = React.useState(initialValue);
-
- function onChange(e) {
- setValue(e.target.checked);
- }
-
- function reset() {
- setValue(initialValue);
- }
-
- // Array / Object hybrid, for easier access in different contexts
- return Object.assign([
- onChange,
- reset,
- {
- [name]: value,
- [`set${Name}`]: setValue
- }
- ], {
- name,
- onChange,
- reset,
- value,
- setter: setValue,
- hasChanged: () => value != initialValue,
- _default
- });
-};
\ No newline at end of file
diff --git a/web/source/settings/lib/form/bool.tsx b/web/source/settings/lib/form/bool.tsx
new file mode 100644
index 000000000..815b17bd3
--- /dev/null
+++ b/web/source/settings/lib/form/bool.tsx
@@ -0,0 +1,60 @@
+/*
+ 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 { useState } from "react";
+import type {
+ BoolFormInputHook,
+ CreateHookNames,
+ HookOpts,
+} from "./types";
+
+const _default = false;
+export default function useBoolInput(
+ { name, Name }: CreateHookNames,
+ { initialValue = _default }: HookOpts
+): BoolFormInputHook {
+ const [value, setValue] = useState(initialValue);
+
+ function onChange(e) {
+ setValue(e.target.checked);
+ }
+
+ function reset() {
+ setValue(initialValue);
+ }
+
+ // Array / Object hybrid, for easier access in different contexts
+ return Object.assign([
+ onChange,
+ reset,
+ {
+ [name]: value,
+ [`set${Name}`]: setValue
+ }
+ ], {
+ name,
+ Name: "",
+ onChange,
+ reset,
+ value,
+ setter: setValue,
+ hasChanged: () => value != initialValue,
+ _default
+ });
+}
diff --git a/web/source/settings/lib/form/check-list.jsx b/web/source/settings/lib/form/check-list.jsx
deleted file mode 100644
index 2f649dba6..000000000
--- a/web/source/settings/lib/form/check-list.jsx
+++ /dev/null
@@ -1,181 +0,0 @@
-/*
- GoToSocial
- Copyright (C) GoToSocial Authors admin@gotosocial.org
- SPDX-License-Identifier: AGPL-3.0-or-later
-
- This program is free software: you can redistribute it and/or modify
- it under the terms of the GNU Affero General Public License as published by
- the Free Software Foundation, either version 3 of the License, or
- (at your option) any later version.
-
- This program is distributed in the hope that it will be useful,
- but WITHOUT ANY WARRANTY; without even the implied warranty of
- MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
- GNU Affero General Public License for more details.
-
- You should have received a copy of the GNU Affero General Public License
- along with this program. If not, see .
-*/
-
-const React = require("react");
-const syncpipe = require("syncpipe");
-const { createSlice } = require("@reduxjs/toolkit");
-const { enableMapSet } = require("immer");
-
-enableMapSet(); // for use in reducers
-
-const { reducer, actions } = createSlice({
- name: "checklist",
- initialState: {}, // not handled by slice itself
- reducers: {
- updateAll: (state, { payload: checked }) => {
- const selectedEntries = new Set();
- return {
- entries: syncpipe(state.entries, [
- (_) => Object.values(_),
- (_) => _.map((entry) => {
- if (checked) {
- selectedEntries.add(entry.key);
- }
- return [entry.key, {
- ...entry,
- checked
- }];
- }),
- (_) => Object.fromEntries(_)
- ]),
- selectedEntries
- };
- },
- update: (state, { payload: { key, value } }) => {
- if (value.checked !== undefined) {
- if (value.checked === true) {
- state.selectedEntries.add(key);
- } else {
- state.selectedEntries.delete(key);
- }
- }
-
- state.entries[key] = {
- ...state.entries[key],
- ...value
- };
- },
- updateMultiple: (state, { payload }) => {
- payload.forEach(([key, value]) => {
- if (value.checked !== undefined) {
- if (value.checked === true) {
- state.selectedEntries.add(key);
- } else {
- state.selectedEntries.delete(key);
- }
- }
-
- state.entries[key] = {
- ...state.entries[key],
- ...value
- };
- });
- }
- }
-});
-
-function initialState({ entries, uniqueKey, initialValue }) {
- const selectedEntries = new Set();
- return {
- entries: syncpipe(entries, [
- (_) => _.map((entry) => {
- let key = entry[uniqueKey];
- let checked = entry.checked ?? initialValue;
-
- if (checked) {
- selectedEntries.add(key);
- } else {
- selectedEntries.delete(key);
- }
-
- return [
- key,
- {
- ...entry,
- key,
- checked
- }
- ];
- }),
- (_) => Object.fromEntries(_)
- ]),
- selectedEntries
- };
-}
-
-module.exports = function useCheckListInput({ name }, { entries, uniqueKey = "key", initialValue = false }) {
- const [state, dispatch] = React.useReducer(reducer, null,
- () => initialState({ entries, uniqueKey, initialValue }) // initial state
- );
-
- const toggleAllRef = React.useRef(null);
-
- React.useEffect(() => {
- if (toggleAllRef.current != null) {
- let some = state.selectedEntries.size > 0;
- let all = false;
- if (some) {
- all = state.selectedEntries.size == Object.values(state.entries).length;
- }
- toggleAllRef.current.checked = all;
- toggleAllRef.current.indeterminate = some && !all;
- }
- // only needs to update when state.selectedEntries changes, not state.entries
- // eslint-disable-next-line react-hooks/exhaustive-deps
- }, [state.selectedEntries]);
-
- const reset = React.useCallback(
- () => dispatch(actions.updateAll(initialValue)),
- [initialValue]
- );
-
- const onChange = React.useCallback(
- (key, value) => dispatch(actions.update({ key, value })),
- []
- );
-
- const updateMultiple = React.useCallback(
- (entries) => dispatch(actions.updateMultiple(entries)),
- []
- );
-
- return React.useMemo(() => {
- function toggleAll(e) {
- let checked = e.target.checked;
- if (e.target.indeterminate) {
- checked = false;
- }
- dispatch(actions.updateAll(checked));
- }
-
- function selectedValues() {
- return Array.from((state.selectedEntries)).map((key) => ({
- ...state.entries[key] // returned as new object, because reducer state is immutable
- }));
- }
-
- return Object.assign([
- state,
- reset,
- { name }
- ], {
- name,
- value: state.entries,
- onChange,
- selectedValues,
- reset,
- someSelected: state.selectedEntries.size > 0,
- updateMultiple,
- toggleAll: {
- ref: toggleAllRef,
- onChange: toggleAll
- }
- });
- }, [state, reset, name, onChange, updateMultiple]);
-};
\ No newline at end of file
diff --git a/web/source/settings/lib/form/check-list.tsx b/web/source/settings/lib/form/check-list.tsx
new file mode 100644
index 000000000..c08e5022f
--- /dev/null
+++ b/web/source/settings/lib/form/check-list.tsx
@@ -0,0 +1,219 @@
+/*
+ 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 {
+ useReducer,
+ useRef,
+ useEffect,
+ useCallback,
+ useMemo,
+} from "react";
+
+import { PayloadAction, createSlice } from "@reduxjs/toolkit";
+
+import type {
+ Checkable,
+ ChecklistInputHook,
+ CreateHookNames,
+ HookOpts,
+} from "./types";
+
+// https://immerjs.github.io/immer/installation#pick-your-immer-version
+import { enableMapSet } from "immer";
+enableMapSet();
+
+interface ChecklistState {
+ entries: { [k: string]: Checkable },
+ selectedEntries: Set,
+}
+
+const initialState: ChecklistState = {
+ entries: {},
+ selectedEntries: new Set(),
+};
+
+const { reducer, actions } = createSlice({
+ name: "checklist",
+ initialState, // not handled by slice itself
+ reducers: {
+ updateAll: (state, { payload: checked }: PayloadAction) => {
+ const selectedEntries = new Set();
+ const entries = Object.fromEntries(
+ Object.values(state.entries).map((entry) => {
+ if (checked) {
+ // Cheekily add this to selected
+ // entries while we're here.
+ selectedEntries.add(entry.key);
+ }
+
+ return [entry.key, { ...entry, checked } ];
+ })
+ );
+
+ return { entries, selectedEntries };
+ },
+ update: (state, { payload: { key, value } }: PayloadAction<{key: string, value: Checkable}>) => {
+ if (value.checked !== undefined) {
+ if (value.checked === true) {
+ state.selectedEntries.add(key);
+ } else {
+ state.selectedEntries.delete(key);
+ }
+ }
+
+ state.entries[key] = {
+ ...state.entries[key],
+ ...value
+ };
+ },
+ updateMultiple: (state, { payload }: PayloadAction>) => {
+ payload.forEach(([key, value]) => {
+ if (value.checked !== undefined) {
+ if (value.checked === true) {
+ state.selectedEntries.add(key);
+ } else {
+ state.selectedEntries.delete(key);
+ }
+ }
+
+ state.entries[key] = {
+ ...state.entries[key],
+ ...value
+ };
+ });
+ }
+ }
+});
+
+function initialHookState({
+ entries,
+ uniqueKey,
+ initialValue,
+}: {
+ entries: Checkable[],
+ uniqueKey: string,
+ initialValue: boolean,
+}): ChecklistState {
+ const selectedEntries = new Set();
+ const mappedEntries = Object.fromEntries(
+ entries.map((entry) => {
+ const key = entry[uniqueKey];
+ const checked = entry.checked ?? initialValue;
+
+ if (checked) {
+ selectedEntries.add(key);
+ } else {
+ selectedEntries.delete(key);
+ }
+
+ return [ key, { ...entry, key, checked } ];
+ })
+ );
+
+ return {
+ entries: mappedEntries,
+ selectedEntries
+ };
+}
+
+const _default: { [k: string]: Checkable } = {};
+
+export default function useCheckListInput(
+ /* eslint-disable no-unused-vars */
+ { name, Name }: CreateHookNames,
+ {
+ entries = [],
+ uniqueKey = "key",
+ initialValue = false,
+ }: HookOpts
+): ChecklistInputHook {
+ const [state, dispatch] = useReducer(
+ reducer,
+ initialState,
+ (_) => initialHookState({ entries, uniqueKey, initialValue }) // initial state
+ );
+
+ const toggleAllRef = useRef(null);
+
+ useEffect(() => {
+ if (toggleAllRef.current != null) {
+ let some = state.selectedEntries.size > 0;
+ let all = false;
+ if (some) {
+ all = state.selectedEntries.size == Object.values(state.entries).length;
+ }
+ toggleAllRef.current.checked = all;
+ toggleAllRef.current.indeterminate = some && !all;
+ }
+ // only needs to update when state.selectedEntries changes, not state.entries
+ // eslint-disable-next-line react-hooks/exhaustive-deps
+ }, [state.selectedEntries]);
+
+ const reset = useCallback(
+ () => dispatch(actions.updateAll(initialValue)),
+ [initialValue]
+ );
+
+ const onChange = useCallback(
+ (key, value) => dispatch(actions.update({ key, value })),
+ []
+ );
+
+ const updateMultiple = useCallback(
+ (entries) => dispatch(actions.updateMultiple(entries)),
+ []
+ );
+
+ return useMemo(() => {
+ function toggleAll(e) {
+ let checked = e.target.checked;
+ if (e.target.indeterminate) {
+ checked = false;
+ }
+ dispatch(actions.updateAll(checked));
+ }
+
+ function selectedValues() {
+ return Array.from((state.selectedEntries)).map((key) => ({
+ ...state.entries[key] // returned as new object, because reducer state is immutable
+ }));
+ }
+
+ return Object.assign([
+ state,
+ reset,
+ { name }
+ ], {
+ _default,
+ hasChanged: () => true,
+ name,
+ Name: "",
+ value: state.entries,
+ onChange,
+ selectedValues,
+ reset,
+ someSelected: state.selectedEntries.size > 0,
+ updateMultiple,
+ toggleAll: {
+ ref: toggleAllRef,
+ onChange: toggleAll
+ }
+ });
+ }, [state, reset, name, onChange, updateMultiple]);
+}
diff --git a/web/source/settings/lib/form/combo-box.jsx b/web/source/settings/lib/form/combo-box.jsx
deleted file mode 100644
index 985c262d8..000000000
--- a/web/source/settings/lib/form/combo-box.jsx
+++ /dev/null
@@ -1,58 +0,0 @@
-/*
- GoToSocial
- Copyright (C) GoToSocial Authors admin@gotosocial.org
- SPDX-License-Identifier: AGPL-3.0-or-later
-
- This program is free software: you can redistribute it and/or modify
- it under the terms of the GNU Affero General Public License as published by
- the Free Software Foundation, either version 3 of the License, or
- (at your option) any later version.
-
- This program is distributed in the hope that it will be useful,
- but WITHOUT ANY WARRANTY; without even the implied warranty of
- MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
- GNU Affero General Public License for more details.
-
- You should have received a copy of the GNU Affero General Public License
- along with this program. If not, see .
-*/
-
-const React = require("react");
-
-const { useComboboxState } = require("ariakit/combobox");
-
-const _default = "";
-module.exports = function useComboBoxInput({ name, Name }, { initialValue = _default }) {
- const [isNew, setIsNew] = React.useState(false);
-
- const state = useComboboxState({
- defaultValue: initialValue,
- gutter: 0,
- sameWidth: true
- });
-
- function reset() {
- state.setValue(initialValue);
- }
-
- return Object.assign([
- state,
- reset,
- {
- [name]: state.value,
- name,
- [`${name}IsNew`]: isNew,
- [`set${Name}IsNew`]: setIsNew
- }
- ], {
- name,
- state,
- value: state.value,
- setter: (val) => state.setValue(val),
- hasChanged: () => state.value != initialValue,
- isNew,
- setIsNew,
- reset,
- _default
- });
-};
\ No newline at end of file
diff --git a/web/source/settings/lib/form/combo-box.tsx b/web/source/settings/lib/form/combo-box.tsx
new file mode 100644
index 000000000..e558d298a
--- /dev/null
+++ b/web/source/settings/lib/form/combo-box.tsx
@@ -0,0 +1,67 @@
+/*
+ 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 { useState } from "react";
+
+import { useComboboxState } from "ariakit/combobox";
+import {
+ ComboboxFormInputHook,
+ CreateHookNames,
+ HookOpts,
+} from "./types";
+
+const _default = "";
+export default function useComboBoxInput(
+ { name, Name }: CreateHookNames,
+ { initialValue = _default }: HookOpts
+): ComboboxFormInputHook {
+ const [isNew, setIsNew] = useState(false);
+
+ const state = useComboboxState({
+ defaultValue: initialValue,
+ gutter: 0,
+ sameWidth: true
+ });
+
+ function reset() {
+ state.setValue(initialValue);
+ }
+
+ return Object.assign([
+ state,
+ reset,
+ {
+ [name]: state.value,
+ name,
+ [`${name}IsNew`]: isNew,
+ [`set${Name}IsNew`]: setIsNew
+ }
+ ], {
+ reset,
+ name,
+ Name: "", // Will be set by inputHook function.
+ state,
+ value: state.value,
+ setter: (val: string) => state.setValue(val),
+ hasChanged: () => state.value != initialValue,
+ isNew,
+ setIsNew,
+ _default
+ });
+}
diff --git a/web/source/settings/lib/form/field-array.jsx b/web/source/settings/lib/form/field-array.jsx
deleted file mode 100644
index f2d7bc7ce..000000000
--- a/web/source/settings/lib/form/field-array.jsx
+++ /dev/null
@@ -1,63 +0,0 @@
-/*
- GoToSocial
- Copyright (C) GoToSocial Authors admin@gotosocial.org
- SPDX-License-Identifier: AGPL-3.0-or-later
-
- This program is free software: you can redistribute it and/or modify
- it under the terms of the GNU Affero General Public License as published by
- the Free Software Foundation, either version 3 of the License, or
- (at your option) any later version.
-
- This program is distributed in the hope that it will be useful,
- but WITHOUT ANY WARRANTY; without even the implied warranty of
- MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
- GNU Affero General Public License for more details.
-
- You should have received a copy of the GNU Affero General Public License
- along with this program. If not, see .
-*/
-
-const React = require("react");
-
-const getFormMutations = require("./get-form-mutations");
-
-function parseFields(entries, length) {
- const fields = [];
-
- for (let i = 0; i < length; i++) {
- if (entries[i] != undefined) {
- fields[i] = Object.assign({}, entries[i]);
- } else {
- fields[i] = {};
- }
- }
-
- return fields;
-}
-
-module.exports = function useArrayInput({ name, _Name }, { initialValue, length = 0 }) {
- const fields = React.useRef({});
-
- const value = React.useMemo(() => parseFields(initialValue, length), [initialValue, length]);
-
- return {
- name,
- value,
- ctx: fields.current,
- maxLength: length,
- selectedValues() {
- // if any form field changed, we need to re-send everything
- const hasUpdate = Object.values(fields.current).some((fieldSet) => {
- const { updatedFields } = getFormMutations(fieldSet, { changedOnly: true });
- return updatedFields.length > 0;
- });
- if (hasUpdate) {
- return Object.values(fields.current).map((fieldSet) => {
- return getFormMutations(fieldSet, { changedOnly: false }).mutationData;
- });
- } else {
- return [];
- }
- }
- };
-};
\ No newline at end of file
diff --git a/web/source/settings/lib/form/field-array.tsx b/web/source/settings/lib/form/field-array.tsx
new file mode 100644
index 000000000..275bf2b1b
--- /dev/null
+++ b/web/source/settings/lib/form/field-array.tsx
@@ -0,0 +1,85 @@
+/*
+ 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 { useRef, useMemo } from "react";
+
+import getFormMutations from "./get-form-mutations";
+
+import type {
+ CreateHookNames,
+ HookOpts,
+ FieldArrayInputHook,
+ HookedForm,
+} from "./types";
+
+function parseFields(entries: HookedForm[], length: number): HookedForm[] {
+ const fields: HookedForm[] = [];
+
+ for (let i = 0; i < length; i++) {
+ if (entries[i] != undefined) {
+ fields[i] = Object.assign({}, entries[i]);
+ } else {
+ fields[i] = {};
+ }
+ }
+
+ return fields;
+}
+
+export default function useArrayInput(
+ { name }: CreateHookNames,
+ {
+ initialValue,
+ length = 0,
+ }: HookOpts,
+): FieldArrayInputHook {
+ const _default: HookedForm[] = Array(length);
+ const fields = useRef(_default);
+
+ const value = useMemo(
+ () => parseFields(initialValue, length),
+ [initialValue, length],
+ );
+
+ function hasUpdate() {
+ return Object.values(fields.current).some((fieldSet) => {
+ const { updatedFields } = getFormMutations(fieldSet, { changedOnly: true });
+ return updatedFields.length > 0;
+ });
+ }
+
+ return {
+ _default,
+ name,
+ Name: "",
+ value,
+ ctx: fields.current,
+ maxLength: length,
+ hasChanged: hasUpdate,
+ selectedValues() {
+ if (hasUpdate()) {
+ return Object.values(fields.current).map((fieldSet) => {
+ return getFormMutations(fieldSet, { changedOnly: false }).mutationData;
+ });
+ } else {
+ return [];
+ }
+ }
+ };
+}
diff --git a/web/source/settings/lib/form/file.jsx b/web/source/settings/lib/form/file.jsx
deleted file mode 100644
index a9e96dc97..000000000
--- a/web/source/settings/lib/form/file.jsx
+++ /dev/null
@@ -1,90 +0,0 @@
-/*
- GoToSocial
- Copyright (C) GoToSocial Authors admin@gotosocial.org
- SPDX-License-Identifier: AGPL-3.0-or-later
-
- This program is free software: you can redistribute it and/or modify
- it under the terms of the GNU Affero General Public License as published by
- the Free Software Foundation, either version 3 of the License, or
- (at your option) any later version.
-
- This program is distributed in the hope that it will be useful,
- but WITHOUT ANY WARRANTY; without even the implied warranty of
- MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
- GNU Affero General Public License for more details.
-
- You should have received a copy of the GNU Affero General Public License
- along with this program. If not, see .
-*/
-
-const React = require("react");
-const prettierBytes = require("prettier-bytes");
-
-module.exports = function useFileInput({ name, _Name }, {
- withPreview,
- maxSize,
- initialInfo = "no file selected"
-} = {}) {
- const [file, setFile] = React.useState();
- const [imageURL, setImageURL] = React.useState();
- const [info, setInfo] = React.useState();
-
- function onChange(e) {
- let file = e.target.files[0];
- setFile(file);
-
- URL.revokeObjectURL(imageURL);
-
- if (file != undefined) {
- if (withPreview) {
- setImageURL(URL.createObjectURL(file));
- }
-
- let size = prettierBytes(file.size);
- if (maxSize && file.size > maxSize) {
- size = {size};
- }
-
- setInfo(<>
- {file.name} ({size})
- >);
- } else {
- setInfo();
- }
- }
-
- function reset() {
- URL.revokeObjectURL(imageURL);
- setImageURL();
- setFile();
- setInfo();
- }
-
- const infoComponent = (
-
- {info
- ? info
- : initialInfo
- }
-
- );
-
- // Array / Object hybrid, for easier access in different contexts
- return Object.assign([
- onChange,
- reset,
- {
- [name]: file,
- [`${name}URL`]: imageURL,
- [`${name}Info`]: infoComponent,
- }
- ], {
- onChange,
- reset,
- name,
- value: file,
- previewValue: imageURL,
- hasChanged: () => file != undefined,
- infoComponent
- });
-};
\ No newline at end of file
diff --git a/web/source/settings/lib/form/file.tsx b/web/source/settings/lib/form/file.tsx
new file mode 100644
index 000000000..944d77ae1
--- /dev/null
+++ b/web/source/settings/lib/form/file.tsx
@@ -0,0 +1,112 @@
+/*
+ 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 { useState } from "react";
+import prettierBytes from "prettier-bytes";
+
+import type {
+ CreateHookNames,
+ HookOpts,
+ FileFormInputHook,
+} from "./types";
+
+const _default = undefined;
+export default function useFileInput(
+ { name }: CreateHookNames,
+ {
+ withPreview,
+ maxSize,
+ initialInfo = "no file selected"
+ }: HookOpts
+): FileFormInputHook {
+ const [file, setFile] = useState();
+ const [imageURL, setImageURL] = useState();
+ const [info, setInfo] = useState();
+
+ function onChange(e: React.ChangeEvent) {
+ const files = e.target.files;
+ if (!files) {
+ setInfo(undefined);
+ return;
+ }
+
+ let file = files[0];
+ setFile(file);
+
+ if (imageURL) {
+ URL.revokeObjectURL(imageURL);
+ }
+
+ if (withPreview) {
+ setImageURL(URL.createObjectURL(file));
+ }
+
+ let size = prettierBytes(file.size);
+ if (maxSize && file.size > maxSize) {
+ size = {size};
+ }
+
+ setInfo(
+ <>
+ {file.name} ({size})
+ >
+ );
+ }
+
+ function reset() {
+ if (imageURL) {
+ URL.revokeObjectURL(imageURL);
+ }
+ setImageURL(undefined);
+ setFile(undefined);
+ setInfo(undefined);
+ }
+
+ const infoComponent = (
+
+ {info
+ ? info
+ : initialInfo
+ }
+
+ );
+
+ // Array / Object hybrid, for easier access in different contexts
+ return Object.assign([
+ onChange,
+ reset,
+ {
+ [name]: file,
+ [`${name}URL`]: imageURL,
+ [`${name}Info`]: infoComponent,
+ }
+ ], {
+ onChange,
+ reset,
+ name,
+ Name: "", // Will be set by inputHook function.
+ value: file,
+ previewValue: imageURL,
+ hasChanged: () => file != undefined,
+ infoComponent,
+ _default,
+ });
+}
diff --git a/web/source/settings/lib/form/form-with-data.jsx b/web/source/settings/lib/form/form-with-data.jsx
deleted file mode 100644
index ef05c46c0..000000000
--- a/web/source/settings/lib/form/form-with-data.jsx
+++ /dev/null
@@ -1,43 +0,0 @@
-/*
- GoToSocial
- Copyright (C) GoToSocial Authors admin@gotosocial.org
- SPDX-License-Identifier: AGPL-3.0-or-later
-
- This program is free software: you can redistribute it and/or modify
- it under the terms of the GNU Affero General Public License as published by
- the Free Software Foundation, either version 3 of the License, or
- (at your option) any later version.
-
- This program is distributed in the hope that it will be useful,
- but WITHOUT ANY WARRANTY; without even the implied warranty of
- MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
- GNU Affero General Public License for more details.
-
- You should have received a copy of the GNU Affero General Public License
- along with this program. If not, see .
-*/
-
-const React = require("react");
-const { Error } = require("../../components/error");
-
-const Loading = require("../../components/loading");
-
-// Wrap Form component inside component that fires the RTK Query call,
-// so Form will only be rendered when data is available to generate form-fields for
-module.exports = function FormWithData({ dataQuery, DataForm, queryArg, ...formProps }) {
- const { data, isLoading, isError, error } = dataQuery(queryArg);
-
- if (isLoading) {
- return (
-
-
-
- );
- } else if (isError) {
- return (
-
- );
- } else {
- return ;
- }
-};
\ No newline at end of file
diff --git a/web/source/settings/lib/form/form-with-data.tsx b/web/source/settings/lib/form/form-with-data.tsx
new file mode 100644
index 000000000..70a162fb0
--- /dev/null
+++ b/web/source/settings/lib/form/form-with-data.tsx
@@ -0,0 +1,60 @@
+/*
+ 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 .
+*/
+
+/* eslint-disable no-unused-vars */
+
+import React from "react";
+
+import { Error } from "../../components/error";
+import Loading from "../../components/loading";
+import { NoArg } from "../types/query";
+import { FormWithDataQuery } from "./types";
+
+export interface FormWithDataProps {
+ dataQuery: FormWithDataQuery,
+ DataForm: ({ data, ...props }) => React.JSX.Element,
+ queryArg?: any,
+}
+
+/**
+ * Wrap Form component inside component that fires the RTK Query call, so Form
+ * will only be rendered when data is available to generate form-fields for.
+ */
+export default function FormWithData({ dataQuery, DataForm, queryArg, ...props }: FormWithDataProps) {
+ if (!queryArg) {
+ queryArg = NoArg;
+ }
+
+ // Trigger provided query.
+ const { data, isLoading, isError, error } = dataQuery(queryArg);
+
+ if (isLoading) {
+ return (
+
+
+
+ );
+ } else if (isError) {
+ return (
+
+ );
+ } else {
+ return ;
+ }
+}
diff --git a/web/source/settings/lib/form/get-form-mutations.js b/web/source/settings/lib/form/get-form-mutations.js
deleted file mode 100644
index b0ae6e9b0..000000000
--- a/web/source/settings/lib/form/get-form-mutations.js
+++ /dev/null
@@ -1,45 +0,0 @@
-/*
- GoToSocial
- Copyright (C) GoToSocial Authors admin@gotosocial.org
- SPDX-License-Identifier: AGPL-3.0-or-later
-
- This program is free software: you can redistribute it and/or modify
- it under the terms of the GNU Affero General Public License as published by
- the Free Software Foundation, either version 3 of the License, or
- (at your option) any later version.
-
- This program is distributed in the hope that it will be useful,
- but WITHOUT ANY WARRANTY; without even the implied warranty of
- MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
- GNU Affero General Public License for more details.
-
- You should have received a copy of the GNU Affero General Public License
- along with this program. If not, see .
-*/
-
-const syncpipe = require("syncpipe");
-
-module.exports = function getFormMutations(form, { changedOnly }) {
- let updatedFields = [];
- return {
- updatedFields,
- mutationData: syncpipe(form, [
- (_) => Object.values(_),
- (_) => _.map((field) => {
- if (field.selectedValues != undefined) {
- let selected = field.selectedValues();
- if (!changedOnly || selected.length > 0) {
- updatedFields.push(field);
- return [field.name, selected];
- }
- } else if (!changedOnly || field.hasChanged()) {
- updatedFields.push(field);
- return [field.name, field.value];
- }
- return null;
- }),
- (_) => _.filter((value) => value != null),
- (_) => Object.fromEntries(_)
- ])
- };
-};
\ No newline at end of file
diff --git a/web/source/settings/lib/form/get-form-mutations.ts b/web/source/settings/lib/form/get-form-mutations.ts
new file mode 100644
index 000000000..6e1bfa02d
--- /dev/null
+++ b/web/source/settings/lib/form/get-form-mutations.ts
@@ -0,0 +1,47 @@
+/*
+ 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 { FormInputHook, HookedForm } from "./types";
+
+export default function getFormMutations(
+ form: HookedForm,
+ { changedOnly }: { changedOnly: boolean },
+) {
+ const updatedFields: FormInputHook[] = [];
+ const mutationData: Array<[string, any]> = [];
+
+ Object.values(form).forEach((field) => {
+ if ("selectedValues" in field) {
+ // FieldArrayInputHook.
+ const selected = field.selectedValues();
+ if (!changedOnly || selected.length > 0) {
+ updatedFields.push(field);
+ mutationData.push([field.name, selected]);
+ }
+ } else if (!changedOnly || field.hasChanged()) {
+ updatedFields.push(field);
+ mutationData.push([field.name, field.value]);
+ }
+ });
+
+ return {
+ updatedFields,
+ mutationData: Object.fromEntries(mutationData),
+ };
+}
diff --git a/web/source/settings/lib/form/index.js b/web/source/settings/lib/form/index.js
deleted file mode 100644
index 99537ae7f..000000000
--- a/web/source/settings/lib/form/index.js
+++ /dev/null
@@ -1,83 +0,0 @@
-/*
- GoToSocial
- Copyright (C) GoToSocial Authors admin@gotosocial.org
- SPDX-License-Identifier: AGPL-3.0-or-later
-
- This program is free software: you can redistribute it and/or modify
- it under the terms of the GNU Affero General Public License as published by
- the Free Software Foundation, either version 3 of the License, or
- (at your option) any later version.
-
- This program is distributed in the hope that it will be useful,
- but WITHOUT ANY WARRANTY; without even the implied warranty of
- MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
- GNU Affero General Public License for more details.
-
- You should have received a copy of the GNU Affero General Public License
- along with this program. If not, see .
-*/
-
-const React = require("react");
-const getByDot = require("get-by-dot").default;
-
-function capitalizeFirst(str) {
- return str.slice(0, 1).toUpperCase + str.slice(1);
-}
-
-function selectorByKey(key) {
- if (key.includes("[")) {
- // get-by-dot does not support 'nested[deeper][key]' notation, convert to 'nested.deeper.key'
- key = key
- .replace(/\[/g, ".") // nested.deeper].key]
- .replace(/\]/g, ""); // nested.deeper.key
- }
-
- return function selector(obj) {
- if (obj == undefined) {
- return undefined;
- } else {
- return getByDot(obj, key);
- }
- };
-}
-
-function makeHook(hookFunction) {
- return function (name, opts = {}) {
- // for dynamically generating attributes like 'setName'
- const Name = React.useMemo(() => capitalizeFirst(name), [name]);
-
- const selector = React.useMemo(() => selectorByKey(name), [name]);
- const valueSelector = opts.valueSelector ?? selector;
-
- opts.initialValue = React.useMemo(() => {
- if (opts.source == undefined) {
- return opts.defaultValue;
- } else {
- return valueSelector(opts.source) ?? opts.defaultValue;
- }
- }, [opts.source, opts.defaultValue, valueSelector]);
-
- const hook = hookFunction({ name, Name }, opts);
-
- return Object.assign(hook, {
- name, Name,
- });
- };
-}
-
-module.exports = {
- useTextInput: makeHook(require("./text")),
- useFileInput: makeHook(require("./file")),
- useBoolInput: makeHook(require("./bool")),
- useRadioInput: makeHook(require("./radio")),
- useComboBoxInput: makeHook(require("./combo-box")),
- useCheckListInput: makeHook(require("./check-list")),
- useFieldArrayInput: makeHook(require("./field-array")),
- useValue: function (name, value) {
- return {
- name,
- value,
- hasChanged: () => true // always included
- };
- }
-};
\ No newline at end of file
diff --git a/web/source/settings/lib/form/index.ts b/web/source/settings/lib/form/index.ts
new file mode 100644
index 000000000..20de33eda
--- /dev/null
+++ b/web/source/settings/lib/form/index.ts
@@ -0,0 +1,114 @@
+/*
+ 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 { useMemo } from "react";
+import getByDot from "get-by-dot";
+
+import text from "./text";
+import file from "./file";
+import bool from "./bool";
+import radio from "./radio";
+import combobox from "./combo-box";
+import checklist from "./check-list";
+import fieldarray from "./field-array";
+
+import type {
+ CreateHook,
+ FormInputHook,
+ HookOpts,
+ TextFormInputHook,
+ RadioFormInputHook,
+ FileFormInputHook,
+ BoolFormInputHook,
+ ComboboxFormInputHook,
+ FieldArrayInputHook,
+ ChecklistInputHook,
+} from "./types";
+
+function capitalizeFirst(str: string) {
+ return str.slice(0, 1).toUpperCase + str.slice(1);
+}
+
+function selectorByKey(key: string) {
+ if (key.includes("[")) {
+ // get-by-dot does not support 'nested[deeper][key]' notation, convert to 'nested.deeper.key'
+ key = key
+ .replace(/\[/g, ".") // nested.deeper].key]
+ .replace(/\]/g, ""); // nested.deeper.key
+ }
+
+ return function selector(obj) {
+ if (obj == undefined) {
+ return undefined;
+ } else {
+ return getByDot(obj, key);
+ }
+ };
+}
+
+/**
+ * Memoized hook generator function. Take a createHook
+ * function and use it to return a new FormInputHook function.
+ *
+ * @param createHook
+ * @returns
+ */
+function inputHook(createHook: CreateHook): (_name: string, _opts: HookOpts) => FormInputHook {
+ return (name: string, opts?: HookOpts): FormInputHook => {
+ // for dynamically generating attributes like 'setName'
+ const Name = useMemo(() => capitalizeFirst(name), [name]);
+ const selector = useMemo(() => selectorByKey(name), [name]);
+ const valueSelector = opts?.valueSelector?? selector;
+
+ if (opts) {
+ opts.initialValue = useMemo(() => {
+ if (opts.source == undefined) {
+ return opts.defaultValue;
+ } else {
+ return valueSelector(opts.source) ?? opts.defaultValue;
+ }
+ }, [opts.source, opts.defaultValue, valueSelector]);
+ }
+
+ const hook = createHook({ name, Name }, opts ?? {});
+ return Object.assign(hook, { name, Name });
+ };
+}
+
+/**
+ * Simplest form hook type in town.
+ */
+function value(name: string, initialValue: T) {
+ return {
+ _default: initialValue,
+ name,
+ Name: "",
+ value: initialValue,
+ hasChanged: () => true, // always included
+ };
+}
+
+export const useTextInput = inputHook(text) as (_name: string, _opts?: HookOpts) => TextFormInputHook;
+export const useFileInput = inputHook(file) as (_name: string, _opts?: HookOpts) => FileFormInputHook;
+export const useBoolInput = inputHook(bool) as (_name: string, _opts?: HookOpts) => BoolFormInputHook;
+export const useRadioInput = inputHook(radio) as (_name: string, _opts?: HookOpts) => RadioFormInputHook;
+export const useComboBoxInput = inputHook(combobox) as (_name: string, _opts?: HookOpts) => ComboboxFormInputHook;
+export const useCheckListInput = inputHook(checklist) as (_name: string, _opts?: HookOpts) => ChecklistInputHook;
+export const useFieldArrayInput = inputHook(fieldarray) as (_name: string, _opts?: HookOpts) => FieldArrayInputHook;
+export const useValue = value as (_name: string, _initialValue: T) => FormInputHook;
diff --git a/web/source/settings/lib/form/radio.jsx b/web/source/settings/lib/form/radio.jsx
deleted file mode 100644
index 4bb061f4b..000000000
--- a/web/source/settings/lib/form/radio.jsx
+++ /dev/null
@@ -1,52 +0,0 @@
-/*
- GoToSocial
- Copyright (C) GoToSocial Authors admin@gotosocial.org
- SPDX-License-Identifier: AGPL-3.0-or-later
-
- This program is free software: you can redistribute it and/or modify
- it under the terms of the GNU Affero General Public License as published by
- the Free Software Foundation, either version 3 of the License, or
- (at your option) any later version.
-
- This program is distributed in the hope that it will be useful,
- but WITHOUT ANY WARRANTY; without even the implied warranty of
- MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
- GNU Affero General Public License for more details.
-
- You should have received a copy of the GNU Affero General Public License
- along with this program. If not, see .
-*/
-
-const React = require("react");
-
-const _default = "";
-module.exports = function useRadioInput({ name, Name }, { initialValue = _default, options }) {
- const [value, setValue] = React.useState(initialValue);
-
- function onChange(e) {
- setValue(e.target.value);
- }
-
- function reset() {
- setValue(initialValue);
- }
-
- // Array / Object hybrid, for easier access in different contexts
- return Object.assign([
- onChange,
- reset,
- {
- [name]: value,
- [`set${Name}`]: setValue
- }
- ], {
- name,
- onChange,
- reset,
- value,
- setter: setValue,
- options,
- hasChanged: () => value != initialValue,
- _default
- });
-};
\ No newline at end of file
diff --git a/web/source/settings/lib/form/radio.tsx b/web/source/settings/lib/form/radio.tsx
new file mode 100644
index 000000000..164abab9d
--- /dev/null
+++ b/web/source/settings/lib/form/radio.tsx
@@ -0,0 +1,60 @@
+/*
+ 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 { useState } from "react";
+import { CreateHookNames, HookOpts, RadioFormInputHook } from "./types";
+
+const _default = "";
+export default function useRadioInput(
+ { name, Name }: CreateHookNames,
+ {
+ initialValue = _default,
+ options = {},
+ }: HookOpts
+): RadioFormInputHook {
+ const [value, setValue] = useState(initialValue);
+
+ function onChange(e) {
+ setValue(e.target.value);
+ }
+
+ function reset() {
+ setValue(initialValue);
+ }
+
+ // Array / Object hybrid, for easier access in different contexts
+ return Object.assign([
+ onChange,
+ reset,
+ {
+ [name]: value,
+ [`set${Name}`]: setValue
+ }
+ ], {
+ onChange,
+ reset,
+ name,
+ Name: "",
+ value,
+ setter: setValue,
+ options,
+ hasChanged: () => value != initialValue,
+ _default
+ });
+}
diff --git a/web/source/settings/lib/form/submit.js b/web/source/settings/lib/form/submit.js
deleted file mode 100644
index ab2945812..000000000
--- a/web/source/settings/lib/form/submit.js
+++ /dev/null
@@ -1,67 +0,0 @@
-/*
- GoToSocial
- Copyright (C) GoToSocial Authors admin@gotosocial.org
- SPDX-License-Identifier: AGPL-3.0-or-later
-
- This program is free software: you can redistribute it and/or modify
- it under the terms of the GNU Affero General Public License as published by
- the Free Software Foundation, either version 3 of the License, or
- (at your option) any later version.
-
- This program is distributed in the hope that it will be useful,
- but WITHOUT ANY WARRANTY; without even the implied warranty of
- MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
- GNU Affero General Public License for more details.
-
- You should have received a copy of the GNU Affero General Public License
- along with this program. If not, see .
-*/
-
-const Promise = require("bluebird");
-const React = require("react");
-const getFormMutations = require("./get-form-mutations");
-
-module.exports = function useFormSubmit(form, mutationQuery, { changedOnly = true, onFinish } = {}) {
- if (!Array.isArray(mutationQuery)) {
- throw new ("useFormSubmit: mutationQuery was not an Array. Is a valid useMutation RTK Query provided?");
- }
- const [runMutation, result] = mutationQuery;
- const usedAction = React.useRef(null);
- return [
- function submitForm(e) {
- let action;
- if (e?.preventDefault) {
- e.preventDefault();
- action = e.nativeEvent.submitter.name;
- } else {
- action = e;
- }
-
- if (action == "") {
- action = undefined;
- }
- usedAction.current = action;
- // transform the field definitions into an object with just their values
-
- const { mutationData, updatedFields } = getFormMutations(form, { changedOnly });
-
- if (updatedFields.length == 0) {
- return;
- }
-
- mutationData.action = action;
-
- return Promise.try(() => {
- return runMutation(mutationData);
- }).then((res) => {
- if (onFinish) {
- return onFinish(res);
- }
- });
- },
- {
- ...result,
- action: usedAction.current
- }
- ];
-};
\ No newline at end of file
diff --git a/web/source/settings/lib/form/submit.ts b/web/source/settings/lib/form/submit.ts
new file mode 100644
index 000000000..d5636a587
--- /dev/null
+++ b/web/source/settings/lib/form/submit.ts
@@ -0,0 +1,140 @@
+/*
+ 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 getFormMutations from "./get-form-mutations";
+
+import { useRef } from "react";
+
+import type {
+ MutationTrigger,
+ UseMutationStateResult,
+} from "@reduxjs/toolkit/dist/query/react/buildHooks";
+
+import type {
+ FormSubmitEvent,
+ FormSubmitFunction,
+ FormSubmitResult,
+ HookedForm,
+} from "./types";
+
+interface UseFormSubmitOptions {
+ changedOnly: boolean;
+ onFinish?: ((_res: any) => void);
+}
+
+/**
+ * Parse changed values from the hooked form into a request
+ * body, and submit it using the given mutation trigger.
+ *
+ * This function basically wraps RTK Query's submit methods to
+ * work with our hooked form interface.
+ *
+ * An `onFinish` callback function can be provided, which will
+ * be executed on a **successful** run of the given MutationTrigger,
+ * with the mutation result passed into it.
+ *
+ * If `changedOnly` is false, then **all** fields of the given HookedForm
+ * will be submitted to the mutation endpoint, not just changed ones.
+ *
+ * The returned function and result can be triggered and read
+ * from just like an RTK Query mutation hook result would be.
+ *
+ * See: https://redux-toolkit.js.org/rtk-query/usage/mutations#mutation-hook-behavior
+ */
+export default function useFormSubmit(
+ form: HookedForm,
+ mutationQuery: readonly [MutationTrigger, UseMutationStateResult],
+ opts: UseFormSubmitOptions = { changedOnly: true }
+): [ FormSubmitFunction, FormSubmitResult ] {
+ if (!Array.isArray(mutationQuery)) {
+ throw "useFormSubmit: mutationQuery was not an Array. Is a valid useMutation RTK Query provided?";
+ }
+
+ const { changedOnly, onFinish } = opts;
+ const [runMutation, mutationResult] = mutationQuery;
+ const usedAction = useRef(undefined);
+
+ const submitForm = async(e: FormSubmitEvent) => {
+ let action: FormSubmitEvent;
+
+ if (typeof e === "string") {
+ if (e !== "") {
+ // String action name was provided.
+ action = e;
+ } else {
+ // Empty string action name was provided.
+ action = undefined;
+ }
+ } else if (e) {
+ // Submit event action was provided.
+ e.preventDefault();
+ if (e.nativeEvent.submitter) {
+ // We want the name of the element that was invoked to submit this form,
+ // which will be something that extends HTMLElement, though we don't know
+ // what at this point.
+ //
+ // See: https://developer.mozilla.org/en-US/docs/Web/API/SubmitEvent/submitter
+ action = (e.nativeEvent.submitter as Object as { name: string }).name;
+ } else {
+ // No submitter defined. Fall back
+ // to just use the FormSubmitEvent.
+ action = e;
+ }
+ } else {
+ // Void or null or something
+ // else was provided.
+ action = undefined;
+ }
+
+ usedAction.current = action;
+
+ // Transform the hooked form into an object.
+ const {
+ mutationData,
+ updatedFields,
+ } = getFormMutations(form, { changedOnly });
+
+ // If there were no updated fields according to
+ // the form parsing then there's nothing for us
+ // to do, since remote and desired state match.
+ if (updatedFields.length == 0) {
+ return;
+ }
+
+ mutationData.action = action;
+
+ try {
+ const res = await runMutation(mutationData);
+ if (onFinish) {
+ onFinish(res);
+ }
+ } catch (e) {
+ // eslint-disable-next-line no-console
+ console.error(`caught error running mutation: ${e}`);
+ }
+ };
+
+ return [
+ submitForm,
+ {
+ ...mutationResult,
+ action: usedAction.current
+ }
+ ];
+}
diff --git a/web/source/settings/lib/form/text.jsx b/web/source/settings/lib/form/text.jsx
deleted file mode 100644
index f9c096ac8..000000000
--- a/web/source/settings/lib/form/text.jsx
+++ /dev/null
@@ -1,87 +0,0 @@
-/*
- GoToSocial
- Copyright (C) GoToSocial Authors admin@gotosocial.org
- SPDX-License-Identifier: AGPL-3.0-or-later
-
- This program is free software: you can redistribute it and/or modify
- it under the terms of the GNU Affero General Public License as published by
- the Free Software Foundation, either version 3 of the License, or
- (at your option) any later version.
-
- This program is distributed in the hope that it will be useful,
- but WITHOUT ANY WARRANTY; without even the implied warranty of
- MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
- GNU Affero General Public License for more details.
-
- You should have received a copy of the GNU Affero General Public License
- along with this program. If not, see .
-*/
-
-const React = require("react");
-
-const _default = "";
-module.exports = function useTextInput({ name, Name }, {
- initialValue = _default,
- dontReset = false,
- validator,
- showValidation = true,
- initValidation
-} = {}) {
-
- const [text, setText] = React.useState(initialValue);
- const textRef = React.useRef(null);
-
- const [validation, setValidation] = React.useState(initValidation ?? "");
- const [_isValidating, startValidation] = React.useTransition();
- let valid = validation == "";
-
- function onChange(e) {
- let input = e.target.value;
- setText(input);
-
- if (validator) {
- startValidation(() => {
- setValidation(validator(input));
- });
- }
- }
-
- function reset() {
- if (!dontReset) {
- setText(initialValue);
- }
- }
-
- React.useEffect(() => {
- if (validator && textRef.current) {
- if (showValidation) {
- textRef.current.setCustomValidity(validation);
- } else {
- textRef.current.setCustomValidity("");
- }
- }
- }, [validation, validator, showValidation]);
-
- // Array / Object hybrid, for easier access in different contexts
- return Object.assign([
- onChange,
- reset,
- {
- [name]: text,
- [`${name}Ref`]: textRef,
- [`set${Name}`]: setText,
- [`${name}Valid`]: valid,
- }
- ], {
- onChange,
- reset,
- name,
- value: text,
- ref: textRef,
- setter: setText,
- valid,
- validate: () => setValidation(validator(text)),
- hasChanged: () => text != initialValue,
- _default
- });
-};
\ No newline at end of file
diff --git a/web/source/settings/lib/form/text.tsx b/web/source/settings/lib/form/text.tsx
new file mode 100644
index 000000000..c0b9b93c6
--- /dev/null
+++ b/web/source/settings/lib/form/text.tsx
@@ -0,0 +1,102 @@
+/*
+ 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, {
+ useState,
+ useRef,
+ useTransition,
+ useEffect,
+} from "react";
+
+import type {
+ CreateHookNames,
+ HookOpts,
+ TextFormInputHook,
+} from "./types";
+
+const _default = "";
+
+export default function useTextInput(
+ { name, Name }: CreateHookNames,
+ {
+ initialValue = _default,
+ dontReset = false,
+ validator,
+ showValidation = true,
+ initValidation
+ }: HookOpts
+): TextFormInputHook {
+ const [text, setText] = useState(initialValue);
+ const textRef = useRef(null);
+
+ const [validation, setValidation] = useState(initValidation ?? "");
+ const [_isValidating, startValidation] = useTransition();
+ const valid = validation == "";
+
+ function onChange(e: React.ChangeEvent) {
+ const input = e.target.value;
+ setText(input);
+
+ if (validator) {
+ startValidation(() => {
+ setValidation(validator(input));
+ });
+ }
+ }
+
+ function reset() {
+ if (!dontReset) {
+ setText(initialValue);
+ }
+ }
+
+ useEffect(() => {
+ if (validator && textRef.current) {
+ if (showValidation) {
+ textRef.current.setCustomValidity(validation);
+ } else {
+ textRef.current.setCustomValidity("");
+ }
+ }
+ }, [validation, validator, showValidation]);
+
+ // Array / Object hybrid, for easier access in different contexts
+ return Object.assign([
+ onChange,
+ reset,
+ {
+ [name]: text,
+ [`${name}Ref`]: textRef,
+ [`set${Name}`]: setText,
+ [`${name}Valid`]: valid,
+ }
+ ], {
+ onChange,
+ reset,
+ name,
+ Name: "", // Will be set by inputHook function.
+ value: text,
+ ref: textRef,
+ setter: setText,
+ valid,
+ validate: () => setValidation(validator ? validator(text): ""),
+ hasChanged: () => text != initialValue,
+ _default
+ });
+}
diff --git a/web/source/settings/lib/form/types.ts b/web/source/settings/lib/form/types.ts
new file mode 100644
index 000000000..45db9e0b8
--- /dev/null
+++ b/web/source/settings/lib/form/types.ts
@@ -0,0 +1,264 @@
+/*
+ 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 .
+*/
+
+/* eslint-disable no-unused-vars */
+
+import { ComboboxState } from "ariakit";
+import React from "react";
+
+import {
+ ChangeEventHandler,
+ Dispatch,
+ RefObject,
+ SetStateAction,
+ SyntheticEvent,
+} from "react";
+
+export interface CreateHookNames {
+ name: string;
+ Name: string;
+}
+
+export interface HookOpts {
+ initialValue?: T,
+ defaultValue?: T,
+
+ dontReset?: boolean,
+ validator?,
+ showValidation?: boolean,
+ initValidation?: string,
+ length?: number;
+ options?: { [_: string]: string },
+ withPreview?: boolean,
+ maxSize?,
+ initialInfo?: string;
+ valueSelector?: Function,
+ source?,
+
+ // checklist input types
+ entries?: any[];
+ uniqueKey?: string;
+}
+
+export type CreateHook = (
+ name: CreateHookNames,
+ opts: HookOpts,
+) => FormInputHook;
+
+export interface FormInputHook {
+ /**
+ * Name of this FormInputHook, as provided
+ * in the UseFormInputHook options.
+ */
+ name: string;
+
+ /**
+ * `name` with first letter capitalized.
+ */
+ Name: string;
+
+ /**
+ * Current value of this FormInputHook.
+ */
+ value?: T;
+
+ /**
+ * Default value of this FormInputHook.
+ */
+ _default: T;
+
+ /**
+ * Return true if the values of this hook is considered
+ * to have been changed from the default / initial value.
+ */
+ hasChanged: () => boolean;
+}
+
+interface _withReset {
+ reset: () => void;
+}
+
+interface _withOnChange {
+ onChange: ChangeEventHandler;
+}
+
+interface _withSetter {
+ setter: Dispatch>;
+}
+
+interface _withValidate {
+ valid: boolean;
+ validate: () => void;
+}
+
+interface _withRef {
+ ref: RefObject;
+}
+
+interface _withFile {
+ previewValue?: string;
+ infoComponent: React.JSX.Element;
+}
+
+interface _withComboboxState {
+ state: ComboboxState;
+}
+
+interface _withNew {
+ isNew: boolean;
+ setIsNew: Dispatch>;
+}
+
+interface _withSelectedValues {
+ selectedValues: () => {
+ [_: string]: any;
+ }[]
+}
+
+interface _withCtx {
+ ctx
+}
+
+interface _withMaxLength {
+ maxLength: number;
+}
+
+interface _withOptions {
+ options: { [_: string]: string };
+}
+
+interface _withToggleAll {
+ toggleAll: _withRef & _withOnChange
+}
+
+interface _withSomeSelected {
+ someSelected: boolean;
+}
+
+interface _withUpdateMultiple {
+ updateMultiple: (_entries: any) => void;
+}
+
+export interface TextFormInputHook extends FormInputHook,
+ _withSetter,
+ _withOnChange,
+ _withReset,
+ _withValidate,
+ _withRef {}
+
+export interface RadioFormInputHook extends FormInputHook,
+ _withSetter,
+ _withOnChange,
+ _withOptions,
+ _withReset {}
+
+export interface FileFormInputHook extends FormInputHook,
+ _withOnChange,
+ _withReset,
+ Partial<_withRef>,
+ _withFile {}
+
+export interface BoolFormInputHook extends FormInputHook,
+ _withSetter,
+ _withOnChange,
+ _withReset {}
+
+export interface ComboboxFormInputHook extends FormInputHook,
+ _withSetter,
+ _withComboboxState,
+ _withNew,
+ _withReset {}
+
+export interface FieldArrayInputHook extends FormInputHook,
+ _withSelectedValues,
+ _withMaxLength,
+ _withCtx {}
+
+export interface Checkable {
+ key: string;
+ checked?: boolean;
+}
+
+export interface ChecklistInputHook extends FormInputHook<{[k: string]: T}>,
+ _withReset,
+ _withToggleAll,
+ _withSelectedValues,
+ _withSomeSelected,
+ _withUpdateMultiple {
+ // Uses its own funky onChange handler.
+ onChange: (key: any, value: any) => void
+ }
+
+export type AnyFormInputHook =
+ FormInputHook |
+ TextFormInputHook |
+ RadioFormInputHook |
+ FileFormInputHook |
+ BoolFormInputHook |
+ ComboboxFormInputHook |
+ FieldArrayInputHook |
+ ChecklistInputHook;
+
+export interface HookedForm {
+ [_: string]: AnyFormInputHook
+}
+
+/**
+ * Parameters for FormSubmitFunction.
+ */
+export type FormSubmitEvent = (string | SyntheticEvent> | undefined | void)
+
+
+/**
+ * Shadows "trigger" function for useMutation, but can also
+ * be passed to onSubmit property of forms as a handler.
+ *
+ * See: https://redux-toolkit.js.org/rtk-query/usage/mutations#mutation-hook-behavior
+ */
+export type FormSubmitFunction = ((_e: FormSubmitEvent) => void)
+
+/**
+ * Shadows redux mutation hook return values.
+ *
+ * See: https://redux-toolkit.js.org/rtk-query/usage/mutations#frequently-used-mutation-hook-return-values
+ */
+export interface FormSubmitResult {
+ /**
+ * Action used to submit the form, if any.
+ */
+ action: FormSubmitEvent;
+ data: any;
+ error: any;
+ isLoading: boolean;
+ isSuccess: boolean;
+ isError: boolean;
+ reset: () => void;
+}
+
+/**
+ * Shadows redux query hook return values.
+ *
+ * See: https://redux-toolkit.js.org/rtk-query/usage/queries#frequently-used-query-hook-return-values
+ */
+export type FormWithDataQuery = (_queryArg: any) => {
+ data?: any;
+ isLoading: boolean;
+ isError: boolean;
+ error?: any;
+}
--
cgit v1.2.3