mirror of
https://github.com/morpheus65535/bazarr
synced 2025-03-15 00:18:48 +00:00
Refactor settings submit hooks, try to fix issue #1924
This commit is contained in:
parent
1e4ffe9c60
commit
30f04feae6
9 changed files with 119 additions and 237 deletions
|
@ -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…
Add table
Reference in a new issue