gtsocial-umbx

Unnamed repository; edit this file 'description' to name the repository.
Log | Files | Refs | README | LICENSE

commit 47daddc10c291ec67320dd2485bffc498ea44bdf
parent 0a9874329d64984cb6dd0fb13b501e266f613745
Author: f0x52 <f0x@cthu.lu>
Date:   Mon,  6 Feb 2023 09:19:56 +0100

[chore/frogend] Restructure form data default values / update from Query data (#1422)

* eslint: set console use to error to catch debug littering in CI

* remove debug logging

* some form field restructuring, fixes submitted updates not being reflected

* more form field restructuring

* remove debug logger

* simplify field updates

* fix react state set during render when submitting import file

* className instead of class

* show Select hints again
Diffstat:
Mweb/source/.eslintrc.js | 3++-
Mweb/source/package.json | 1+
Mweb/source/settings/admin/emoji/local/detail.js | 2+-
Mweb/source/settings/admin/federation/detail.js | 29+++++++++++++++++++++--------
Mweb/source/settings/admin/federation/import-export/form.jsx | 10++--------
Mweb/source/settings/admin/federation/import-export/index.jsx | 2+-
Mweb/source/settings/admin/federation/import-export/process.jsx | 2+-
Mweb/source/settings/admin/settings.js | 17++++++++++-------
Mweb/source/settings/components/form/inputs.jsx | 5++---
Mweb/source/settings/lib/form/bool.jsx | 10++++++----
Mweb/source/settings/lib/form/check-list.jsx | 12++++++------
Mweb/source/settings/lib/form/combo-box.jsx | 13++++++++-----
Mweb/source/settings/lib/form/index.js | 49+++++++++++++++++++++++++++++++++++++++++++------
Mweb/source/settings/lib/form/radio.jsx | 10++++++----
Mweb/source/settings/lib/form/submit.js | 17++++-------------
Mweb/source/settings/lib/form/text.jsx | 10++++++----
Mweb/source/settings/user/profile.js | 12++++++------
Mweb/source/settings/user/settings.js | 30++++++++++++++++++++++--------
Mweb/source/yarn.lock | 5+++++
19 files changed, 153 insertions(+), 86 deletions(-)

diff --git a/web/source/.eslintrc.js b/web/source/.eslintrc.js @@ -22,6 +22,7 @@ module.exports = { "extends": ["@joepie91/eslint-config/react"], "plugins": ["license-header"], "rules": { - "license-header/header": ["error", __dirname + "/.license-header.js"] + "license-header/header": ["error", __dirname + "/.license-header.js"], + "no-console": 'error' } }; \ No newline at end of file diff --git a/web/source/package.json b/web/source/package.json @@ -14,6 +14,7 @@ "@reduxjs/toolkit": "^1.8.6", "ariakit": "^2.0.0-next.41", "bluebird": "^3.7.2", + "get-by-dot": "^1.0.2", "is-valid-domain": "^0.1.6", "js-file-download": "^0.4.12", "langs": "^2.0.0", diff --git a/web/source/settings/admin/emoji/local/detail.js b/web/source/settings/admin/emoji/local/detail.js @@ -54,7 +54,7 @@ module.exports = function EmojiDetailRoute() { function EmojiDetailForm({ data: emoji }) { const form = { id: useValue("id", emoji.id), - category: useComboBoxInput("category", { defaultValue: emoji.category }), + category: useComboBoxInput("category", { source: emoji }), image: useFileInput("image", { withPreview: true, maxSize: 50 * 1024 // TODO: get from instance api diff --git a/web/source/settings/admin/federation/detail.js b/web/source/settings/admin/federation/detail.js @@ -19,7 +19,7 @@ "use strict"; const React = require("react"); -const { useRoute, Redirect } = require("wouter"); +const { useRoute, Redirect, useLocation } = require("wouter"); const query = require("../../lib/query"); @@ -69,12 +69,12 @@ module.exports = function InstanceDetail({ baseUrl }) { <div> <h1 className="text-cutoff"><BackButton to={baseUrl} /> Federation settings for: <span title={domain}>{domain}</span></h1> {infoContent} - <DomainBlockForm defaultDomain={domain} block={existingBlock} /> + <DomainBlockForm defaultDomain={domain} block={existingBlock} baseUrl={baseUrl} /> </div> ); }; -function DomainBlockForm({ defaultDomain, block = {} }) { +function DomainBlockForm({ defaultDomain, block = {}, baseUrl }) { const isExistingBlock = block.domain != undefined; const disabledForm = isExistingBlock @@ -85,18 +85,31 @@ function DomainBlockForm({ defaultDomain, block = {} }) { : {}; const form = { - domain: useTextInput("domain", { defaultValue: block.domain ?? defaultDomain }), - obfuscate: useBoolInput("obfuscate", { defaultValue: block.obfuscate }), - commentPrivate: useTextInput("private_comment", { defaultValue: block.private_comment }), - commentPublic: useTextInput("public_comment", { defaultValue: block.public_comment }) + domain: useTextInput("domain", { source: block, defaultValue: defaultDomain }), + obfuscate: useBoolInput("obfuscate", { source: block }), + commentPrivate: useTextInput("private_comment", { source: block }), + commentPublic: useTextInput("public_comment", { source: block }) }; const [submitForm, addResult] = useFormSubmit(form, query.useAddInstanceBlockMutation(), { changedOnly: false }); const [removeBlock, removeResult] = query.useRemoveInstanceBlockMutation({ fixedCacheKey: block.id }); + const [location, setLocation] = useLocation(); + + function verifyUrlThenSubmit(e) { + // Adding a new block happens on /settings/admin/federation/domain.com + // but if domain input changes, that doesn't match anymore and causes issues later on + // so, before submitting the form, silently change url, then submit + let correctUrl = `${baseUrl}/${form.domain.value}`; + if (location != correctUrl) { + setLocation(correctUrl); + } + return submitForm(e); + } + return ( - <form onSubmit={submitForm}> + <form onSubmit={verifyUrlThenSubmit}> <TextInput field={form.domain} label="Domain" diff --git a/web/source/settings/admin/federation/import-export/form.jsx b/web/source/settings/admin/federation/import-export/form.jsx @@ -36,13 +36,11 @@ const ExportFormatTable = require("./export-format-table"); module.exports = function ImportExportForm({ form, submitParse, parseResult }) { const [submitExport, exportResult] = useFormSubmit(form, query.useExportDomainListMutation()); - const [updateFromFile, setUpdateFromFile] = React.useState(false); - function fileChanged(e) { const reader = new FileReader(); reader.onload = function (read) { - form.domains.setter(read.target.result); - setUpdateFromFile(true); + form.domains.value = read.target.result; + submitParse(); }; reader.readAsText(e.target.files[0]); } @@ -54,10 +52,6 @@ module.exports = function ImportExportForm({ form, submitParse, parseResult }) { /* eslint-disable-next-line react-hooks/exhaustive-deps */ }, [exportResult]); - if (updateFromFile) { - setUpdateFromFile(false); - submitParse(); - } return ( <> <h1>Import / Export suspended domains</h1> diff --git a/web/source/settings/admin/federation/import-export/index.jsx b/web/source/settings/admin/federation/import-export/index.jsx @@ -40,7 +40,7 @@ module.exports = function ImportExport() { exportType: useTextInput("exportType", { defaultValue: "plain", dontReset: true }) }; - const [submitParse, parseResult] = useFormSubmit(form, query.useProcessDomainListMutation()); + const [submitParse, parseResult] = useFormSubmit(form, query.useProcessDomainListMutation(), { changedOnly: false }); const [_location, setLocation] = useLocation(); diff --git a/web/source/settings/admin/federation/import-export/process.jsx b/web/source/settings/admin/federation/import-export/process.jsx @@ -234,7 +234,7 @@ const UpdateableEntry = React.memo( return ( <> <span className="text-cutoff">{entry.domain}</span> - <i class="fa fa-long-arrow-right" aria-hidden="true"></i> + <i className="fa fa-long-arrow-right" aria-hidden="true"></i> <span>{entry.suggest}</span> <a role="button" onClick={() => updateEntry(entry.key, { domain: entry.suggest, suggest: null }) diff --git a/web/source/settings/admin/settings.js b/web/source/settings/admin/settings.js @@ -49,14 +49,17 @@ module.exports = function AdminSettings() { function AdminSettingsForm({ data: instance }) { const form = { - title: useTextInput("title", { defaultValue: instance.title }), + title: useTextInput("title", { + source: instance, + validator: (val) => val.length <= 40 ? "" : "Instance title must be 40 characters or less" + }), thumbnail: useFileInput("thumbnail", { withPreview: true }), - thumbnailDesc: useTextInput("thumbnail_description", { defaultValue: instance.thumbnail_description }), - shortDesc: useTextInput("short_description", { defaultValue: instance.short_description }), - description: useTextInput("description", { defaultValue: instance.description }), - contactUser: useTextInput("contact_username", { defaultValue: instance.contact_account?.username }), - contactEmail: useTextInput("contact_email", { defaultValue: instance.email }), - terms: useTextInput("terms", { defaultValue: instance.terms }) + thumbnailDesc: useTextInput("thumbnail_description", { source: instance }), + shortDesc: useTextInput("short_description", { source: instance }), + description: useTextInput("description", { source: instance }), + contactUser: useTextInput("contact_username", { source: instance, valueSelector: (s) => s.contact_account?.username }), + contactEmail: useTextInput("contact_email", { source: instance, valueSelector: (s) => s.email }), + terms: useTextInput("terms", { source: instance }) }; const [submitForm, result] = useFormSubmit(form, query.useUpdateInstanceMutation()); diff --git a/web/source/settings/components/form/inputs.jsx b/web/source/settings/components/form/inputs.jsx @@ -22,7 +22,6 @@ const React = require("react"); function TextInput({ label, field, ...inputProps }) { const { onChange, value, ref } = field; - console.log(field.name, field.valid, field.value); return ( <div className={`form-field text${field.valid ? "" : " invalid"}`}> @@ -93,13 +92,13 @@ function Checkbox({ label, field, ...inputProps }) { ); } -function Select({ label, field, options, ...inputProps }) { +function Select({ label, field, options, children, ...inputProps }) { const { onChange, value, ref } = field; return ( <div className="form-field select"> <label> - {label} + {label} {children} <select {...{ onChange, value, ref }} {...inputProps} diff --git a/web/source/settings/lib/form/bool.jsx b/web/source/settings/lib/form/bool.jsx @@ -20,15 +20,16 @@ const React = require("react"); -module.exports = function useBoolInput({ name, Name }, { defaultValue = false } = {}) { - const [value, setValue] = React.useState(defaultValue); +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(defaultValue); + setValue(initialValue); } // Array / Object hybrid, for easier access in different contexts @@ -45,6 +46,7 @@ module.exports = function useBoolInput({ name, Name }, { defaultValue = false } reset, value, setter: setValue, - hasChanged: () => value != defaultValue + hasChanged: () => value != initialValue, + _default }); }; \ No newline at end of file diff --git a/web/source/settings/lib/form/check-list.jsx b/web/source/settings/lib/form/check-list.jsx @@ -81,13 +81,13 @@ const { reducer, actions } = createSlice({ } }); -function initialState({ entries, uniqueKey, defaultValue }) { +function initialState({ entries, uniqueKey, initialValue }) { const selectedEntries = new Set(); return { entries: syncpipe(entries, [ (_) => _.map((entry) => { let key = entry[uniqueKey]; - let checked = entry.checked ?? defaultValue; + let checked = entry.checked ?? initialValue; if (checked) { selectedEntries.add(key); @@ -110,9 +110,9 @@ function initialState({ entries, uniqueKey, defaultValue }) { }; } -module.exports = function useCheckListInput({ name }, { entries, uniqueKey = "key", defaultValue = false }) { +module.exports = function useCheckListInput({ name }, { entries, uniqueKey = "key", initialValue = false }) { const [state, dispatch] = React.useReducer(reducer, null, - () => initialState({ entries, uniqueKey, defaultValue }) // initial state + () => initialState({ entries, uniqueKey, initialValue }) // initial state ); const toggleAllRef = React.useRef(null); @@ -132,8 +132,8 @@ module.exports = function useCheckListInput({ name }, { entries, uniqueKey = "ke }, [state.selectedEntries]); const reset = React.useCallback( - () => dispatch(actions.updateAll(defaultValue)), - [defaultValue] + () => dispatch(actions.updateAll(initialValue)), + [initialValue] ); const onChange = React.useCallback( diff --git a/web/source/settings/lib/form/combo-box.jsx b/web/source/settings/lib/form/combo-box.jsx @@ -22,17 +22,18 @@ const React = require("react"); const { useComboboxState } = require("ariakit/combobox"); -module.exports = function useComboBoxInput({ name, Name }, { defaultValue } = {}) { +const _default = ""; +module.exports = function useComboBoxInput({ name, Name }, { initialValue = _default }) { const [isNew, setIsNew] = React.useState(false); const state = useComboboxState({ - defaultValue, + defaultValue: initialValue, gutter: 0, sameWidth: true }); function reset() { - state.setValue(""); + state.setValue(initialValue); } return Object.assign([ @@ -48,9 +49,11 @@ module.exports = function useComboBoxInput({ name, Name }, { defaultValue } = {} name, state, value: state.value, - hasChanged: () => state.value != defaultValue, + setter: (val) => state.setValue(val), + hasChanged: () => state.value != initialValue, isNew, setIsNew, - reset + reset, + _default }); }; \ No newline at end of file diff --git a/web/source/settings/lib/form/index.js b/web/source/settings/lib/form/index.js @@ -18,15 +18,52 @@ "use strict"; +const React = require("react"); +const getByDot = require("get-by-dot").default; + function capitalizeFirst(str) { - return str.slice(0, 1).toUpperCase() + str.slice(1); + 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(func) { - return (name, ...args) => func({ - name, - Name: capitalizeFirst(name) - }, ...args); +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 = { diff --git a/web/source/settings/lib/form/radio.jsx b/web/source/settings/lib/form/radio.jsx @@ -20,15 +20,16 @@ const React = require("react"); -module.exports = function useRadioInput({ name, Name }, { defaultValue, options } = {}) { - const [value, setValue] = React.useState(defaultValue); +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(defaultValue); + setValue(initialValue); } // Array / Object hybrid, for easier access in different contexts @@ -46,6 +47,7 @@ module.exports = function useRadioInput({ name, Name }, { defaultValue, options value, setter: setValue, options, - hasChanged: () => value != defaultValue + hasChanged: () => value != initialValue, + _default }); }; \ No newline at end of file diff --git a/web/source/settings/lib/form/submit.js b/web/source/settings/lib/form/submit.js @@ -18,7 +18,6 @@ "use strict"; -const Promise = require("bluebird"); const React = require("react"); const syncpipe = require("syncpipe"); @@ -27,7 +26,7 @@ module.exports = function useFormSubmit(form, mutationQuery, { changedOnly = tru throw new ("useFormSubmit: mutationQuery was not an Array. Is a valid useMutation RTK Query provided?"); } const [runMutation, result] = mutationQuery; - const [usedAction, setUsedAction] = React.useState(); + const usedAction = React.useRef(null); return [ function submitForm(e) { let action; @@ -41,7 +40,7 @@ module.exports = function useFormSubmit(form, mutationQuery, { changedOnly = tru if (action == "") { action = undefined; } - setUsedAction(action); + usedAction.current = action; // transform the field definitions into an object with just their values let updatedFields = []; const mutationData = syncpipe(form, [ @@ -65,19 +64,11 @@ module.exports = function useFormSubmit(form, mutationQuery, { changedOnly = tru mutationData.action = action; - return Promise.try(() => { - return runMutation(mutationData); - }).then((res) => { - if (res.error == undefined) { - updatedFields.forEach((field) => { - field.reset(); - }); - } - }); + return runMutation(mutationData); }, { ...result, - action: usedAction + action: usedAction.current } ]; }; \ No newline at end of file diff --git a/web/source/settings/lib/form/text.jsx b/web/source/settings/lib/form/text.jsx @@ -20,15 +20,16 @@ const React = require("react"); +const _default = ""; module.exports = function useTextInput({ name, Name }, { - defaultValue = "", + initialValue = _default, dontReset = false, validator, showValidation = true, initValidation } = {}) { - const [text, setText] = React.useState(defaultValue); + const [text, setText] = React.useState(initialValue); const textRef = React.useRef(null); const [validation, setValidation] = React.useState(initValidation ?? ""); @@ -48,7 +49,7 @@ module.exports = function useTextInput({ name, Name }, { function reset() { if (!dontReset) { - setText(defaultValue); + setText(initialValue); } } @@ -81,6 +82,7 @@ module.exports = function useTextInput({ name, Name }, { setter: setText, valid, validate: () => setValidation(validator(text)), - hasChanged: () => text != defaultValue + hasChanged: () => text != initialValue, + _default }); }; \ No newline at end of file diff --git a/web/source/settings/user/profile.js b/web/source/settings/user/profile.js @@ -71,12 +71,12 @@ function UserProfileForm({ data: profile }) { const form = { avatar: useFileInput("avatar", { withPreview: true }), header: useFileInput("header", { withPreview: true }), - displayName: useTextInput("display_name", { defaultValue: profile.display_name }), - note: useTextInput("note", { defaultValue: profile.source?.note }), - customCSS: useTextInput("custom_css", { defaultValue: profile.custom_css }), - bot: useBoolInput("bot", { defaultValue: profile.bot }), - locked: useBoolInput("locked", { defaultValue: profile.locked }), - enableRSS: useBoolInput("enable_rss", { defaultValue: profile.enable_rss }), + displayName: useTextInput("display_name", { source: profile }), + note: useTextInput("note", { source: profile, valueSelector: (p) => p.source?.note }), + customCSS: useTextInput("custom_css", { source: profile }), + bot: useBoolInput("bot", { source: profile }), + locked: useBoolInput("locked", { source: profile }), + enableRSS: useBoolInput("enable_rss", { source: profile }), }; const [submitForm, result] = useFormSubmit(form, query.useUpdateCredentialsMutation()); diff --git a/web/source/settings/user/settings.js b/web/source/settings/user/settings.js @@ -49,7 +49,6 @@ module.exports = function UserSettings() { }; function UserSettingsForm({ data }) { - const { source } = data; /* form keys - string source[privacy] - bool source[sensitive] @@ -58,10 +57,10 @@ function UserSettingsForm({ data }) { */ const form = { - defaultPrivacy: useTextInput("source[privacy]", { defaultValue: source.privacy ?? "unlisted" }), - isSensitive: useBoolInput("source[sensitive]", { defaultValue: source.sensitive }), - language: useTextInput("source[language]", { defaultValue: source.language?.toUpperCase() ?? "EN" }), - format: useTextInput("source[status_format]", { defaultValue: source.status_format ?? "plain" }), + defaultPrivacy: useTextInput("source[privacy]", { source: data, defaultValue: "unlisted" }), + isSensitive: useBoolInput("source[sensitive]", { source: data }), + language: useTextInput("source[language]", { source: data, valueSelector: (s) => s.source.language?.toUpperCase() ?? "EN" }), + format: useTextInput("source[status_format]", { source: data, defaultValue: "plain" }), }; const [submitForm, result] = useFormSubmit(form, query.useUpdateCredentialsMutation()); @@ -132,9 +131,24 @@ function PasswordChange() { return ( <form className="change-password" onSubmit={submitForm}> <h1>Change password</h1> - <TextInput type="password" field={form.oldPassword} label="Current password" /> - <TextInput type="password" field={form.newPassword} label="New password" /> - <TextInput type="password" field={verifyNewPassword} label="Confirm new password" /> + <TextInput + type="password" + name="password" + field={form.oldPassword} + label="Current password" + /> + <TextInput + type="password" + name="newPassword" + field={form.newPassword} + label="New password" + /> + <TextInput + type="password" + name="confirmNewPassword" + field={verifyNewPassword} + label="Confirm new password" + /> <MutationButton label="Change password" result={result} /> </form> ); diff --git a/web/source/yarn.lock b/web/source/yarn.lock @@ -3091,6 +3091,11 @@ get-assigned-identifiers@^1.1.0, get-assigned-identifiers@^1.2.0: resolved "https://registry.yarnpkg.com/get-assigned-identifiers/-/get-assigned-identifiers-1.2.0.tgz#6dbf411de648cbaf8d9169ebb0d2d576191e2ff1" integrity sha512-mBBwmeGTrxEMO4pMaaf/uUEFHnYtwr8FTe8Y/mer4rcV/bye0qGm6pw1bGZFGStxC5O76c5ZAVBGnqHmOaJpdQ== +get-by-dot@^1.0.2: + version "1.0.2" + resolved "https://registry.yarnpkg.com/get-by-dot/-/get-by-dot-1.0.2.tgz#8ba0ef82fe3435ce57faa133e45357a9059a7081" + integrity sha512-gzOcBY84Hd7vTE5r5pXHSyPGuFAxABCfYV3Oey8Z6RxikkhJbbL9x3vu0cOn53QjZfQI1X5JZuNCVwOlvqLBwQ== + get-intrinsic@^1.0.2, get-intrinsic@^1.1.0, get-intrinsic@^1.1.1, get-intrinsic@^1.1.3: version "1.1.3" resolved "https://registry.yarnpkg.com/get-intrinsic/-/get-intrinsic-1.1.3.tgz#063c84329ad93e83893c7f4f243ef63ffa351385"