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