mirror of
https://github.com/superseriousbusiness/gotosocial.git
synced 2024-12-04 22:20:53 +01:00
a59dc855d9
* 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: commite3655ba4fb
Author: f0x <f0x@cthu.lu> Date: Tue Jan 31 15:19:10 2023 +0100 let debug depend on env (prod/debug) again commit79c792b832
Author: f0x <f0x@cthu.lu> Date: Tue Jan 31 00:34:01 2023 +0100 update checklist components commit4367960fe4
Author: f0x <f0x@cthu.lu> Date: Mon Jan 30 23:46:20 2023 +0100 checklist performance improvements commit204a4c02d1
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
182 lines
4.4 KiB
JavaScript
182 lines
4.4 KiB
JavaScript
/*
|
|
GoToSocial
|
|
Copyright (C) 2021-2023 GoToSocial Authors admin@gotosocial.org
|
|
|
|
This program is free software: you can redistribute it and/or modify
|
|
it under the terms of the GNU Affero General Public License as published by
|
|
the Free Software Foundation, either version 3 of the License, or
|
|
(at your option) any later version.
|
|
|
|
This program is distributed in the hope that it will be useful,
|
|
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
GNU Affero General Public License for more details.
|
|
|
|
You should have received a copy of the GNU Affero General Public License
|
|
along with this program. If not, see <http://www.gnu.org/licenses/>.
|
|
*/
|
|
|
|
"use strict";
|
|
|
|
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, defaultValue }) {
|
|
const selectedEntries = new Set();
|
|
return {
|
|
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, dispatch] = React.useReducer(reducer, null,
|
|
() => initialState({ entries, uniqueKey, defaultValue }) // 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(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));
|
|
}
|
|
|
|
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.someChecked,
|
|
updateMultiple,
|
|
toggleAll: {
|
|
ref: toggleAllRef,
|
|
onChange: toggleAll
|
|
}
|
|
});
|
|
}, [state, reset, name, onChange, updateMultiple]);
|
|
}; |