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

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

@ -24,14 +24,11 @@ import { Card, Check, Chips, Message, Password, Text } from "../components";
import {
FormContext,
FormValues,
runHooks,
useFormActions,
useStagedValues,
} from "../utilities/FormValues";
import { useSettingValue } from "../utilities/hooks";
import {
SubmitHooksProvider,
useSubmitHooksSource,
} from "../utilities/HooksProvider";
import { SettingsProvider, useSettings } from "../utilities/SettingsProvider";
import { ProviderInfo, ProviderList } from "./list";
@ -48,13 +45,15 @@ export const ProviderView: FunctionComponent = () => {
const select = useCallback(
(v?: ProviderInfo) => {
modals.openContextModal(ProviderModal, {
payload: v ?? null,
enabledProviders: providers ?? [],
staged,
settings,
onChange: update,
});
if (settings) {
modals.openContextModal(ProviderModal, {
payload: v ?? null,
enabledProviders: providers ?? [],
staged,
settings,
onChange: update,
});
}
},
[modals, providers, settings, staged, update]
);
@ -129,11 +128,10 @@ const ProviderTool: FunctionComponent<ProviderToolProps> = ({
const form = useForm<FormValues>({
initialValues: {
settings: staged,
hooks: {},
},
});
const submitHooks = useSubmitHooksSource();
const deletePayload = useCallback(() => {
if (payload && enabledProviders) {
const idx = enabledProviders.findIndex((v) => v === payload.key);
@ -150,6 +148,7 @@ const ProviderTool: FunctionComponent<ProviderToolProps> = ({
(values: FormValues) => {
if (info && enabledProviders) {
const changes = { ...values.settings };
const hooks = values.hooks;
// Add this provider if not exist
if (enabledProviders.find((v) => v === info.key) === undefined) {
@ -158,13 +157,13 @@ const ProviderTool: FunctionComponent<ProviderToolProps> = ({
}
// Apply submit hooks
submitHooks.invoke(changes);
runHooks(hooks, changes);
onChangeRef.current(changes);
modals.closeAll();
}
},
[info, enabledProviders, submitHooks, modals]
[info, enabledProviders, modals]
);
const canSave = info !== null;
@ -257,40 +256,38 @@ const ProviderTool: FunctionComponent<ProviderToolProps> = ({
return (
<SettingsProvider value={settings}>
<FormContext.Provider value={form}>
<SubmitHooksProvider value={submitHooks}>
<Stack>
<Stack spacing="xs">
<Selector
searchable
placeholder="Click to Select a Provider"
itemComponent={SelectItem}
disabled={payload !== null}
{...options}
value={info}
onChange={onSelect}
></Selector>
<Message>{info?.description}</Message>
{inputs}
<div hidden={info?.message === undefined}>
<Message>{info?.message}</Message>
</div>
</Stack>
<Divider></Divider>
<Group position="right">
<Button hidden={!payload} color="red" onClick={deletePayload}>
Delete
</Button>
<Button
disabled={!canSave}
onClick={() => {
submit(form.values);
}}
>
Save
</Button>
</Group>
<Stack>
<Stack spacing="xs">
<Selector
searchable
placeholder="Click to Select a Provider"
itemComponent={SelectItem}
disabled={payload !== null}
{...options}
value={info}
onChange={onSelect}
></Selector>
<Message>{info?.description}</Message>
{inputs}
<div hidden={info?.message === undefined}>
<Message>{info?.message}</Message>
</div>
</Stack>
</SubmitHooksProvider>
<Divider></Divider>
<Group position="right">
<Button hidden={!payload} color="red" onClick={deletePayload}>
Delete
</Button>
<Button
disabled={!canSave}
onClick={() => {
submit(form.values);
}}
>
Save
</Button>
</Group>
</Stack>
</FormContext.Provider>
</SettingsProvider>
);

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

@ -33,7 +33,7 @@ export const Number: FunctionComponent<NumberProps> = (props) => {
return (
<NumberInput
{...rest}
value={value ?? undefined}
value={value ?? 0}
onChange={(val = 0) => {
update(val);
}}
@ -49,7 +49,7 @@ export const Text: FunctionComponent<TextProps> = (props) => {
return (
<TextInput
{...rest}
value={value ?? undefined}
value={value ?? ""}
onChange={(e) => {
update(e.currentTarget.value);
}}
@ -65,7 +65,7 @@ export const Password: FunctionComponent<PasswordProps> = (props) => {
return (
<PasswordInput
{...rest}
value={value ?? undefined}
value={value ?? ""}
onChange={(e) => {
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);
formRef.current.setValues((values) => {
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 };
}
// eslint-disable-next-line @typescript-eslint/no-explicit-any
type HookType = (value: any) => unknown;
export type FormKey = keyof FormValues;
export type FormValues = {
// Settings that saved to the backend
settings: LooseObject;
// Settings that saved to the frontend
// 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() {
const context = useContext(SettingsContext);
if (context === null) {
throw new Error("useSettings must be used within a SettingsProvider");
}
return context;
}
type SettingsProviderProps = {
value: Settings;
value: Settings | null;
};
export const SettingsProvider: FunctionComponent<SettingsProviderProps> = ({

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

Loading…
Cancel
Save