mirror of
https://github.com/superseriousbusiness/gotosocial
synced 2025-06-05 21:59:39 +02:00
[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: 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
This commit is contained in:
@ -20,128 +20,163 @@
|
||||
|
||||
const React = require("react");
|
||||
const syncpipe = require("syncpipe");
|
||||
const { createSlice } = require("@reduxjs/toolkit");
|
||||
const { enableMapSet } = require("immer");
|
||||
|
||||
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
|
||||
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);
|
||||
}
|
||||
];
|
||||
}),
|
||||
(_) => 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
|
||||
};
|
||||
},
|
||||
updateMultiple: (state, { payload }) => {
|
||||
payload.forEach(([key, value]) => {
|
||||
if (value.checked !== undefined) {
|
||||
if (value.checked === true) {
|
||||
state.selectedEntries.add(key);
|
||||
} else {
|
||||
state.selectedEntries.delete(key);
|
||||
}
|
||||
}
|
||||
|
||||
function updateState(state, key, newValue) {
|
||||
return {
|
||||
...state,
|
||||
[key]: {
|
||||
...state[key],
|
||||
...newValue
|
||||
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, 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));
|
||||
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]);
|
||||
|
||||
/* eslint-disable-next-line react-hooks/exhaustive-deps */
|
||||
}, [entries]);
|
||||
const reset = React.useCallback(
|
||||
() => dispatch(actions.updateAll(defaultValue)),
|
||||
[defaultValue]
|
||||
);
|
||||
|
||||
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;
|
||||
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));
|
||||
}
|
||||
|
||||
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);
|
||||
function selectedValues() {
|
||||
return Array.from((state.selectedEntries)).map((key) => ({
|
||||
...state.entries[key] // returned as new object, because reducer state is immutable
|
||||
}));
|
||||
}
|
||||
|
||||
setSomeSelected(some);
|
||||
|
||||
if (some && !all) {
|
||||
setToggleAllState(2);
|
||||
toggleAllRef.current.indeterminate = true;
|
||||
} else {
|
||||
setToggleAllState(all ? 1 : 0);
|
||||
toggleAllRef.current.indeterminate = false;
|
||||
}
|
||||
}, [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]);
|
||||
};
|
Reference in New Issue
Block a user