Refactor settings submit hooks, try to fix issue #1924

pull/1996/head
LASER-Yi 2 years ago
parent 1e4ffe9c60
commit 30f04feae6

@ -11,7 +11,6 @@ import { useLatestEnabledLanguages, useLatestProfiles } from ".";
import { Selector, SelectorProps } from "../components"; import { Selector, SelectorProps } from "../components";
import { useFormActions } from "../utilities/FormValues"; import { useFormActions } from "../utilities/FormValues";
import { BaseInput } from "../utilities/hooks"; import { BaseInput } from "../utilities/hooks";
import { useSubmitHookWith } from "../utilities/HooksProvider";
type LanguageSelectorProps = Omit< type LanguageSelectorProps = Omit<
MultiSelectorProps<Language.Info>, MultiSelectorProps<Language.Info>,
@ -26,10 +25,6 @@ export const LanguageSelector: FunctionComponent<
const enabled = useLatestEnabledLanguages(); const enabled = useLatestEnabledLanguages();
const { setValue } = useFormActions(); const { setValue } = useFormActions();
useSubmitHookWith(settingKey, (value: Language.Info[]) =>
value.map((v) => v.code2)
);
const wrappedOptions = useSelectorOptions(options, (value) => value.name); const wrappedOptions = useSelectorOptions(options, (value) => value.name);
return ( return (
@ -39,7 +34,9 @@ export const LanguageSelector: FunctionComponent<
value={enabled} value={enabled}
searchable searchable
onChange={(val) => { onChange={(val) => {
setValue(val, settingKey); setValue(val, settingKey, (value: Language.Info[]) =>
value.map((v) => v.code2)
);
}} }}
></MultiSelector> ></MultiSelector>
</Input.Wrapper> </Input.Wrapper>

@ -13,7 +13,6 @@ import { Column } from "react-table";
import { useLatestEnabledLanguages, useLatestProfiles } from "."; import { useLatestEnabledLanguages, useLatestProfiles } from ".";
import { languageProfileKey } from "../keys"; import { languageProfileKey } from "../keys";
import { useFormActions } from "../utilities/FormValues"; import { useFormActions } from "../utilities/FormValues";
import { useSubmitHookWith } from "../utilities/HooksProvider";
const Table: FunctionComponent = () => { const Table: FunctionComponent = () => {
const profiles = useLatestProfiles(); const profiles = useLatestProfiles();
@ -27,15 +26,13 @@ const Table: FunctionComponent = () => {
[profiles] [profiles]
); );
useSubmitHookWith(languageProfileKey, (value) => JSON.stringify(value));
const { setValue } = useFormActions(); const { setValue } = useFormActions();
const modals = useModals(); const modals = useModals();
const submitProfiles = useCallback( const submitProfiles = useCallback(
(list: Language.Profile[]) => { (list: Language.Profile[]) => {
setValue(list, languageProfileKey); setValue(list, languageProfileKey, (value) => JSON.stringify(value));
}, },
[setValue] [setValue]
); );

@ -24,14 +24,11 @@ import { Card, Check, Chips, Message, Password, Text } from "../components";
import { import {
FormContext, FormContext,
FormValues, FormValues,
runHooks,
useFormActions, useFormActions,
useStagedValues, useStagedValues,
} from "../utilities/FormValues"; } from "../utilities/FormValues";
import { useSettingValue } from "../utilities/hooks"; import { useSettingValue } from "../utilities/hooks";
import {
SubmitHooksProvider,
useSubmitHooksSource,
} from "../utilities/HooksProvider";
import { SettingsProvider, useSettings } from "../utilities/SettingsProvider"; import { SettingsProvider, useSettings } from "../utilities/SettingsProvider";
import { ProviderInfo, ProviderList } from "./list"; import { ProviderInfo, ProviderList } from "./list";
@ -48,6 +45,7 @@ export const ProviderView: FunctionComponent = () => {
const select = useCallback( const select = useCallback(
(v?: ProviderInfo) => { (v?: ProviderInfo) => {
if (settings) {
modals.openContextModal(ProviderModal, { modals.openContextModal(ProviderModal, {
payload: v ?? null, payload: v ?? null,
enabledProviders: providers ?? [], enabledProviders: providers ?? [],
@ -55,6 +53,7 @@ export const ProviderView: FunctionComponent = () => {
settings, settings,
onChange: update, onChange: update,
}); });
}
}, },
[modals, providers, settings, staged, update] [modals, providers, settings, staged, update]
); );
@ -129,11 +128,10 @@ const ProviderTool: FunctionComponent<ProviderToolProps> = ({
const form = useForm<FormValues>({ const form = useForm<FormValues>({
initialValues: { initialValues: {
settings: staged, settings: staged,
hooks: {},
}, },
}); });
const submitHooks = useSubmitHooksSource();
const deletePayload = useCallback(() => { const deletePayload = useCallback(() => {
if (payload && enabledProviders) { if (payload && enabledProviders) {
const idx = enabledProviders.findIndex((v) => v === payload.key); const idx = enabledProviders.findIndex((v) => v === payload.key);
@ -150,6 +148,7 @@ const ProviderTool: FunctionComponent<ProviderToolProps> = ({
(values: FormValues) => { (values: FormValues) => {
if (info && enabledProviders) { if (info && enabledProviders) {
const changes = { ...values.settings }; const changes = { ...values.settings };
const hooks = values.hooks;
// Add this provider if not exist // Add this provider if not exist
if (enabledProviders.find((v) => v === info.key) === undefined) { if (enabledProviders.find((v) => v === info.key) === undefined) {
@ -158,13 +157,13 @@ const ProviderTool: FunctionComponent<ProviderToolProps> = ({
} }
// Apply submit hooks // Apply submit hooks
submitHooks.invoke(changes); runHooks(hooks, changes);
onChangeRef.current(changes); onChangeRef.current(changes);
modals.closeAll(); modals.closeAll();
} }
}, },
[info, enabledProviders, submitHooks, modals] [info, enabledProviders, modals]
); );
const canSave = info !== null; const canSave = info !== null;
@ -257,7 +256,6 @@ const ProviderTool: FunctionComponent<ProviderToolProps> = ({
return ( return (
<SettingsProvider value={settings}> <SettingsProvider value={settings}>
<FormContext.Provider value={form}> <FormContext.Provider value={form}>
<SubmitHooksProvider value={submitHooks}>
<Stack> <Stack>
<Stack spacing="xs"> <Stack spacing="xs">
<Selector <Selector
@ -290,7 +288,6 @@ const ProviderTool: FunctionComponent<ProviderToolProps> = ({
</Button> </Button>
</Group> </Group>
</Stack> </Stack>
</SubmitHooksProvider>
</FormContext.Provider> </FormContext.Provider>
</SettingsProvider> </SettingsProvider>
); );

@ -9,11 +9,7 @@ import { Badge, Container, Group, LoadingOverlay } from "@mantine/core";
import { useForm } from "@mantine/form"; import { useForm } from "@mantine/form";
import { useDocumentTitle } from "@mantine/hooks"; import { useDocumentTitle } from "@mantine/hooks";
import { FunctionComponent, ReactNode, useCallback, useMemo } from "react"; import { FunctionComponent, ReactNode, useCallback, useMemo } from "react";
import { FormContext, FormValues } from "../utilities/FormValues"; import { FormContext, FormValues, runHooks } from "../utilities/FormValues";
import {
SubmitHooksProvider,
useSubmitHooksSource,
} from "../utilities/HooksProvider";
import { SettingsProvider } from "../utilities/SettingsProvider"; import { SettingsProvider } from "../utilities/SettingsProvider";
interface Props { interface Props {
@ -27,11 +23,10 @@ const Layout: FunctionComponent<Props> = (props) => {
const { data: settings, isLoading, isRefetching } = useSystemSettings(); const { data: settings, isLoading, isRefetching } = useSystemSettings();
const { mutate, isLoading: isMutating } = useSettingsMutation(); const { mutate, isLoading: isMutating } = useSettingsMutation();
const submitHooks = useSubmitHooksSource();
const form = useForm<FormValues>({ const form = useForm<FormValues>({
initialValues: { initialValues: {
settings: {}, settings: {},
hooks: {},
}, },
}); });
@ -43,16 +38,16 @@ const Layout: FunctionComponent<Props> = (props) => {
const submit = useCallback( const submit = useCallback(
(values: FormValues) => { (values: FormValues) => {
const { settings } = values; const { settings, hooks } = values;
if (Object.keys(settings).length > 0) { if (Object.keys(settings).length > 0) {
const settingsToSubmit = { ...settings }; const settingsToSubmit = { ...settings };
submitHooks.invoke(settingsToSubmit); runHooks(hooks, settingsToSubmit);
LOG("info", "submitting settings", settingsToSubmit); LOG("info", "submitting settings", settingsToSubmit);
mutate(settingsToSubmit); mutate(settingsToSubmit);
} }
}, },
[mutate, submitHooks] [mutate]
); );
const totalStagedCount = useMemo(() => { const totalStagedCount = useMemo(() => {
@ -66,15 +61,11 @@ const Layout: FunctionComponent<Props> = (props) => {
useDocumentTitle(`${name} - Bazarr (Settings)`); useDocumentTitle(`${name} - Bazarr (Settings)`);
if (settings === undefined) {
return <LoadingOverlay visible></LoadingOverlay>;
}
return ( return (
<SettingsProvider value={settings}> <SettingsProvider value={settings ?? null}>
<LoadingProvider value={isLoading || isMutating}> <LoadingProvider value={isLoading || isMutating}>
<SubmitHooksProvider value={submitHooks}> <form onSubmit={form.onSubmit(submit)} style={{ position: "relative" }}>
<form onSubmit={form.onSubmit(submit)}> <LoadingOverlay visible={settings === undefined}></LoadingOverlay>
<Toolbox> <Toolbox>
<Group> <Group>
<Toolbox.Button <Toolbox.Button
@ -83,11 +74,7 @@ const Layout: FunctionComponent<Props> = (props) => {
loading={isMutating} loading={isMutating}
disabled={totalStagedCount === 0} disabled={totalStagedCount === 0}
rightIcon={ rightIcon={
<Badge <Badge size="xs" radius="sm" hidden={totalStagedCount === 0}>
size="xs"
radius="sm"
hidden={totalStagedCount === 0}
>
{totalStagedCount} {totalStagedCount}
</Badge> </Badge>
} }
@ -102,7 +89,6 @@ const Layout: FunctionComponent<Props> = (props) => {
</Container> </Container>
</FormContext.Provider> </FormContext.Provider>
</form> </form>
</SubmitHooksProvider>
</LoadingProvider> </LoadingProvider>
</SettingsProvider> </SettingsProvider>
); );

@ -33,7 +33,7 @@ export const Number: FunctionComponent<NumberProps> = (props) => {
return ( return (
<NumberInput <NumberInput
{...rest} {...rest}
value={value ?? undefined} value={value ?? 0}
onChange={(val = 0) => { onChange={(val = 0) => {
update(val); update(val);
}} }}
@ -49,7 +49,7 @@ export const Text: FunctionComponent<TextProps> = (props) => {
return ( return (
<TextInput <TextInput
{...rest} {...rest}
value={value ?? undefined} value={value ?? ""}
onChange={(e) => { onChange={(e) => {
update(e.currentTarget.value); update(e.currentTarget.value);
}} }}
@ -65,7 +65,7 @@ export const Password: FunctionComponent<PasswordProps> = (props) => {
return ( return (
<PasswordInput <PasswordInput
{...rest} {...rest}
value={value ?? undefined} value={value ?? ""}
onChange={(e) => { onChange={(e) => {
update(e.currentTarget.value); update(e.currentTarget.value);
}} }}

@ -35,21 +35,52 @@ export function useFormActions() {
}); });
}, []); }, []);
const setValue = useCallback((v: unknown, key: string) => { const setValue = useCallback((v: unknown, key: string, hook?: HookType) => {
LOG("info", `Updating value of ${key}`, v); LOG("info", `Updating value of ${key}`, v);
formRef.current.setValues((values) => { formRef.current.setValues((values) => {
const changes = { ...values.settings, [key]: v }; const changes = { ...values.settings, [key]: v };
return { ...values, settings: changes }; const hooks = { ...values.hooks };
if (hook) {
LOG(
"info",
`Adding submit hook ${key}, will be executed before submitting`
);
hooks[key] = hook;
}
return { ...values, settings: changes, hooks };
}); });
}, []); }, []);
return { update, setValue }; return { update, setValue };
} }
// eslint-disable-next-line @typescript-eslint/no-explicit-any
type HookType = (value: any) => unknown;
export type FormKey = keyof FormValues; export type FormKey = keyof FormValues;
export type FormValues = { export type FormValues = {
// Settings that saved to the backend // Settings that saved to the backend
settings: LooseObject; settings: LooseObject;
// Settings that saved to the frontend // Settings that saved to the frontend
// storages: LooseObject; // storages: LooseObject;
// submit hooks
hooks: StrictObject<HookType>;
}; };
export function runHooks(
hooks: FormValues["hooks"],
settings: FormValues["settings"]
) {
for (const key in settings) {
if (key in hooks) {
LOG("info", "Running submit hook for", key, settings[key]);
const value = settings[key];
const fn = hooks[key];
settings[key] = fn(value);
LOG("info", "Finish submit hook", key, settings[key]);
}
}
}

@ -1,119 +0,0 @@
import { LOG } from "@/utilities/console";
import {
createContext,
FunctionComponent,
useCallback,
useContext,
useEffect,
useMemo,
useRef,
useState,
} from "react";
// eslint-disable-next-line @typescript-eslint/no-explicit-any
type HookType = (value: any) => unknown;
export type SubmitHookType = {
[key: string]: HookType;
};
export type SubmitHookModifierType = {
add: (key: string, fn: HookType) => void;
remove: (key: string) => void;
invoke: (settings: LooseObject) => void;
};
const SubmitHooksContext = createContext<SubmitHookModifierType | null>(null);
type SubmitHooksProviderProps = {
value: SubmitHookModifierType;
};
export const SubmitHooksProvider: FunctionComponent<
SubmitHooksProviderProps
> = ({ value, children }) => {
return (
<SubmitHooksContext.Provider value={value}>
{children}
</SubmitHooksContext.Provider>
);
};
export function useSubmitHooks() {
const context = useContext(SubmitHooksContext);
if (context === null) {
throw new Error(
"useSubmitHooksModifier must be used within a SubmitHooksProvider"
);
}
return context;
}
export function useSubmitHookWith(key: string, fn?: HookType) {
const fnRef = useRef(fn);
fnRef.current = fn;
const hooks = useSubmitHooks();
useEffect(() => {
const currentFn = fnRef.current;
if (currentFn) {
LOG("info", "Adding submit hook for", key);
hooks.add(key, currentFn);
}
return () => {
LOG("info", "Removing submit hook for", key);
hooks.remove(key);
};
}, [key, hooks]);
}
export function useSubmitHooksSource(): SubmitHookModifierType {
const [submitHooks, setSubmitHooks] = useState<SubmitHookType>({});
const hooksRef = useRef(submitHooks);
hooksRef.current = submitHooks;
const invokeHooks = useCallback((settings: LooseObject) => {
const hooks = hooksRef.current;
for (const key in settings) {
if (key in hooks) {
LOG("info", "Running submit hook for", key, settings[key]);
const value = settings[key];
const fn = hooks[key];
settings[key] = fn(value);
LOG("info", "Finish submit hook", key, settings[key]);
}
}
}, []);
const addHook = useCallback(
(key: string, fn: (value: unknown) => unknown) => {
setSubmitHooks((hooks) => ({ ...hooks, [key]: fn }));
},
[]
);
const removeHook = useCallback((key: string) => {
setSubmitHooks((hooks) => {
const newHooks = { ...hooks };
if (key in newHooks) {
delete newHooks[key];
}
return newHooks;
});
}, []);
return useMemo(
() => ({
add: addHook,
remove: removeHook,
invoke: invokeHooks,
}),
[addHook, invokeHooks, removeHook]
);
}

@ -5,15 +5,11 @@ const SettingsContext = createContext<Settings | null>(null);
export function useSettings() { export function useSettings() {
const context = useContext(SettingsContext); const context = useContext(SettingsContext);
if (context === null) {
throw new Error("useSettings must be used within a SettingsProvider");
}
return context; return context;
} }
type SettingsProviderProps = { type SettingsProviderProps = {
value: Settings; value: Settings | null;
}; };
export const SettingsProvider: FunctionComponent<SettingsProviderProps> = ({ export const SettingsProvider: FunctionComponent<SettingsProviderProps> = ({

@ -3,7 +3,6 @@ import { get, isNull, isUndefined, uniqBy } from "lodash";
import { useCallback, useMemo, useRef } from "react"; import { useCallback, useMemo, useRef } from "react";
import { useFormActions, useStagedValues } from "../utilities/FormValues"; import { useFormActions, useStagedValues } from "../utilities/FormValues";
import { useSettings } from "../utilities/SettingsProvider"; import { useSettings } from "../utilities/SettingsProvider";
import { useSubmitHookWith } from "./HooksProvider";
export interface BaseInput<T> { export interface BaseInput<T> {
disabled?: boolean; disabled?: boolean;
@ -31,7 +30,7 @@ export function useBaseInput<T, V>(props: T & BaseInput<V>) {
const moddedValue = const moddedValue =
(newValue && settingOptions?.onSaved?.(newValue)) ?? newValue; (newValue && settingOptions?.onSaved?.(newValue)) ?? newValue;
setValue(moddedValue, settingKey); setValue(moddedValue, settingKey, settingOptions?.onSubmit);
}, },
[settingOptions, setValue, settingKey] [settingOptions, setValue, settingKey]
); );
@ -48,12 +47,10 @@ export function useSettingValue<T>(
const optionsRef = useRef(options); const optionsRef = useRef(options);
optionsRef.current = options; optionsRef.current = options;
useSubmitHookWith(key, options?.onSubmit);
const originalValue = useMemo(() => { const originalValue = useMemo(() => {
const onLoaded = optionsRef.current?.onLoaded; const onLoaded = optionsRef.current?.onLoaded;
const defaultValue = optionsRef.current?.defaultValue; const defaultValue = optionsRef.current?.defaultValue;
if (onLoaded) { if (onLoaded && settings) {
LOG("info", `${key} is using custom loader`); LOG("info", `${key} is using custom loader`);
return onLoaded(settings); return onLoaded(settings);

Loading…
Cancel
Save