diff options
author | 2023-02-03 12:07:40 +0100 | |
---|---|---|
committer | 2023-02-03 12:07:40 +0100 | |
commit | a59dc855d94b332ca01b4a2477ef94ee68da9fe6 (patch) | |
tree | 0f8397b591927d317a2400e6f2d7f6ef1ef527db /web/source/settings/lib/form/check-list.jsx | |
parent | [chore] Text formatting overhaul (#1406) (diff) | |
download | gotosocial-a59dc855d94b332ca01b4a2477ef94ee68da9fe6.tar.xz |
[feature/frogend] (Mastodon) domain block CSV import (#1390)
* checkbox-list styling with taller <p> element
* CSV import/export, UI/UX improvements to import-export interface
* minor styling tweaks
* csv export, clean up export type branching
* abstract domain block entry validation
* foundation for PSL check + suggestions
* Squashed commit of the following:
commit e3655ba4fbea1d55738b2f9e407d3378af26afe6
Author: f0x <f0x@cthu.lu>
Date: Tue Jan 31 15:19:10 2023 +0100
let debug depend on env (prod/debug) again
commit 79c792b832a2b59e472dcdff646bad6d71b42cc9
Author: f0x <f0x@cthu.lu>
Date: Tue Jan 31 00:34:01 2023 +0100
update checklist components
commit 4367960fe4be4e3978077af06e63a729d64e32fb
Author: f0x <f0x@cthu.lu>
Date: Mon Jan 30 23:46:20 2023 +0100
checklist performance improvements
commit 204a4c02d16ffad189a6e8a6001d5bf4ff95fc4e
Author: f0x <f0x@cthu.lu>
Date: Mon Jan 30 20:05:34 2023 +0100
checklist field: use reducer for state
* remove debug logging
* show and use domain block suggestion
* restructure import/export buttons
* updating suggestions
* suggestion overview
* restructure check-list behavior, domain import/export
Diffstat (limited to 'web/source/settings/lib/form/check-list.jsx')
-rw-r--r-- | web/source/settings/lib/form/check-list.jsx | 249 |
1 files changed, 142 insertions, 107 deletions
diff --git a/web/source/settings/lib/form/check-list.jsx b/web/source/settings/lib/form/check-list.jsx index c1233273d..b19e17a29 100644 --- a/web/source/settings/lib/form/check-list.jsx +++ b/web/source/settings/lib/form/check-list.jsx @@ -20,128 +20,163 @@ const React = require("react"); const syncpipe = require("syncpipe"); - -function createState(entries, uniqueKey, oldState, defaultValue) { - return syncpipe(entries, [ - (_) => _.map((entry) => { - let key = entry[uniqueKey]; - return [ - key, - { - ...entry, - key, - checked: oldState[key]?.checked ?? entry.checked ?? defaultValue +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); + } } - ]; - }), - (_) => Object.fromEntries(_) - ]); -} -function updateAllState(state, newValue) { - return syncpipe(state, [ - (_) => Object.values(_), - (_) => _.map((entry) => [entry.key, { - ...entry, - checked: newValue - }]), - (_) => Object.fromEntries(_) - ]); -} + state.entries[key] = { + ...state.entries[key], + ...value + }; + }); + } + } +}); -function updateState(state, key, newValue) { +function initialState({ entries, uniqueKey, defaultValue }) { + const selectedEntries = new Set(); return { - ...state, - [key]: { - ...state[key], - ...newValue - } + entries: syncpipe(entries, [ + (_) => _.map((entry) => { + let key = entry[uniqueKey]; + let checked = entry.checked ?? defaultValue; + + 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", defaultValue = false }) { - const [state, setState] = React.useState({}); + const [state, dispatch] = React.useReducer(reducer, null, + () => initialState({ entries, uniqueKey, defaultValue }) // initial state + ); - const [someSelected, setSomeSelected] = React.useState(false); - const [toggleAllState, setToggleAllState] = React.useState(0); const toggleAllRef = React.useRef(null); React.useEffect(() => { - /* - entries changed, update state, - re-using old state if available for key - */ - setState(createState(entries, uniqueKey, state, defaultValue)); - - /* eslint-disable-next-line react-hooks/exhaustive-deps */ - }, [entries]); - - React.useEffect(() => { - /* Updates (un)check all checkbox, based on shortcode checkboxes - Can be 0 (not checked), 1 (checked) or 2 (indeterminate) - */ - if (toggleAllRef.current == null) { - return; + 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; } - - let values = Object.values(state); - /* one or more boxes are checked */ - let some = values.some((v) => v.checked); - - let all = false; - if (some) { - /* there's not at least one unchecked box */ - all = !values.some((v) => v.checked == false); + // 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(defaultValue)), + [defaultValue] + ); + + 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)); } - setSomeSelected(some); - - if (some && !all) { - setToggleAllState(2); - toggleAllRef.current.indeterminate = true; - } else { - setToggleAllState(all ? 1 : 0); - toggleAllRef.current.indeterminate = false; + function selectedValues() { + return Array.from((state.selectedEntries)).map((key) => ({ + ...state.entries[key] // returned as new object, because reducer state is immutable + })); } - }, [state, toggleAllRef]); - - function toggleAll(e) { - let selectAll = e.target.checked; - if (toggleAllState == 2) { // indeterminate - selectAll = false; - } - - setState(updateAllState(state, selectAll)); - setToggleAllState(selectAll); - } - - function reset() { - setState(updateAllState(state, defaultValue)); - } - - function selectedValues() { - return syncpipe(state, [ - (_) => Object.values(_), - (_) => _.filter((entry) => entry.checked) - ]); - } - - return Object.assign([ - state, - reset, - { name } - ], { - name, - value: state, - onChange: (key, newValue) => setState(updateState(state, key, newValue)), - selectedValues, - reset, - someSelected, - toggleAll: { - ref: toggleAllRef, - value: toggleAllState, - onChange: toggleAll - } - }); + return Object.assign([ + state, + reset, + { name } + ], { + name, + value: state.entries, + onChange, + selectedValues, + reset, + someSelected: state.someChecked, + updateMultiple, + toggleAll: { + ref: toggleAllRef, + onChange: toggleAll + } + }); + }, [state, reset, name, onChange, updateMultiple]); };
\ No newline at end of file |