summaryrefslogtreecommitdiff
path: root/web/source/settings/lib
diff options
context:
space:
mode:
Diffstat (limited to 'web/source/settings/lib')
-rw-r--r--web/source/settings/lib/form/check-list.tsx124
-rw-r--r--web/source/settings/lib/form/types.ts2
-rw-r--r--web/source/settings/lib/query/admin/custom-emoji/index.ts56
3 files changed, 54 insertions, 128 deletions
diff --git a/web/source/settings/lib/form/check-list.tsx b/web/source/settings/lib/form/check-list.tsx
index c08e5022f..e44daefff 100644
--- a/web/source/settings/lib/form/check-list.tsx
+++ b/web/source/settings/lib/form/check-list.tsx
@@ -18,15 +18,12 @@
*/
import {
- useReducer,
useRef,
useEffect,
useCallback,
useMemo,
} from "react";
-import { PayloadAction, createSlice } from "@reduxjs/toolkit";
-
import type {
Checkable,
ChecklistInputHook,
@@ -34,106 +31,12 @@ import type {
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<string>,
-}
-
-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<boolean>) => {
- const selectedEntries = new Set<string>();
- 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<Array<[key: string, value: Checkable]>>) => {
- 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<string>();
- 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
- };
-}
+import {
+ useChecklistReducer,
+ actions,
+} from "../../redux/checklist";
const _default: { [k: string]: Checkable } = {};
-
export default function useCheckListInput(
/* eslint-disable no-unused-vars */
{ name, Name }: CreateHookNames,
@@ -143,12 +46,7 @@ export default function useCheckListInput(
initialValue = false,
}: HookOpts<boolean>
): ChecklistInputHook {
- const [state, dispatch] = useReducer(
- reducer,
- initialState,
- (_) => initialHookState({ entries, uniqueKey, initialValue }) // initial state
- );
-
+ const [state, dispatch] = useChecklistReducer(entries, uniqueKey, initialValue);
const toggleAllRef = useRef<any>(null);
useEffect(() => {
@@ -167,17 +65,17 @@ export default function useCheckListInput(
const reset = useCallback(
() => dispatch(actions.updateAll(initialValue)),
- [initialValue]
+ [initialValue, dispatch]
);
const onChange = useCallback(
- (key, value) => dispatch(actions.update({ key, value })),
- []
+ (key: string, value: Checkable) => dispatch(actions.update({ key, value })),
+ [dispatch]
);
const updateMultiple = useCallback(
- (entries) => dispatch(actions.updateMultiple(entries)),
- []
+ (entries: [key: string, value: Partial<Checkable>][]) => dispatch(actions.updateMultiple(entries)),
+ [dispatch]
);
return useMemo(() => {
@@ -215,5 +113,5 @@ export default function useCheckListInput(
onChange: toggleAll
}
});
- }, [state, reset, name, onChange, updateMultiple]);
+ }, [state, reset, name, onChange, updateMultiple, dispatch]);
}
diff --git a/web/source/settings/lib/form/types.ts b/web/source/settings/lib/form/types.ts
index 45db9e0b8..f9fd2cbdd 100644
--- a/web/source/settings/lib/form/types.ts
+++ b/web/source/settings/lib/form/types.ts
@@ -152,7 +152,7 @@ interface _withSomeSelected {
}
interface _withUpdateMultiple {
- updateMultiple: (_entries: any) => void;
+ updateMultiple: (entries: [key: string, value: Partial<Checkable>][]) => void;
}
export interface TextFormInputHook extends FormInputHook<string>,
diff --git a/web/source/settings/lib/query/admin/custom-emoji/index.ts b/web/source/settings/lib/query/admin/custom-emoji/index.ts
index 204fb1c1f..56684f03b 100644
--- a/web/source/settings/lib/query/admin/custom-emoji/index.ts
+++ b/web/source/settings/lib/query/admin/custom-emoji/index.ts
@@ -199,13 +199,16 @@ const extended = gtsApi.injectEndpoints({
});
if (errors.length !== 0) {
+ const errData = errors.map(e => JSON.stringify(e.data)).join(",");
return {
error: {
status: 400,
statusText: 'Bad Request',
- data: {"error":`One or more errors fetching custom emojis: ${errors}`},
+ data: {
+ error: `One or more errors fetching custom emojis: [${errData}]`
+ },
},
- };
+ };
}
// Return our ID'd
@@ -222,14 +225,18 @@ const extended = gtsApi.injectEndpoints({
patchRemoteEmojis: build.mutation({
async queryFn({ action, ...formData }, _api, _extraOpts, fetchWithBQ) {
- const data: CustomEmoji[] = [];
const errors: FetchBaseQueryError[] = [];
-
- formData.selectEmoji.forEach(async(emoji: CustomEmoji) => {
- let body = {
+ const selectedEmoji: CustomEmoji[] = formData.selectedEmoji;
+
+ // Map function to get a promise
+ // of an emoji (or null).
+ const copyEmoji = async(emoji: CustomEmoji) => {
+ let body: {
+ type: string;
+ shortcode?: string;
+ category?: string;
+ } = {
type: action,
- shortcode: "",
- category: "",
};
if (action == "copy") {
@@ -243,22 +250,43 @@ const extended = gtsApi.injectEndpoints({
method: "PATCH",
url: `/api/v1/admin/custom_emojis/${emoji.id}`,
asForm: true,
- body: body
+ body: body,
});
+
if (emojiRes.error) {
errors.push(emojiRes.error);
- } else {
- // Got it!
- data.push(emojiRes.data as CustomEmoji);
+ return null;
}
- });
+
+ // Instead of mapping to the emoji we just got in emojiRes.data,
+ // we map here to the existing emoji. The reason for this is that
+ // if we return the new emoji, it has a new ID, and the checklist
+ // component calling this function gets its state mixed up.
+ //
+ // For example, say you copy an emoji with ID "some_emoji"; the
+ // result would return an emoji with ID "some_new_emoji_id". The
+ // checklist state would then contain one emoji with ID "some_emoji",
+ // and the new copy of the emoji with ID "some_new_emoji_id", leading
+ // to weird-looking bugs where it suddenly appears as if the searched
+ // status has another blank emoji attached to it.
+ return emoji;
+ };
+
+ // Wait for all the promises to
+ // resolve and remove any nulls.
+ const data = (
+ await Promise.all(selectedEmoji.map(copyEmoji))
+ ).flatMap((emoji) => emoji || []);
if (errors.length !== 0) {
+ const errData = errors.map(e => JSON.stringify(e.data)).join(",");
return {
error: {
status: 400,
statusText: 'Bad Request',
- data: {"error":`One or more errors patching custom emojis: ${errors}`},
+ data: {
+ error: `One or more errors patching custom emojis: [${errData}]`
+ },
},
};
}