diff --git a/frontend/src/pages/Settings/Languages/components.tsx b/frontend/src/pages/Settings/Languages/components.tsx index ad27278f7..0fa3b0f57 100644 --- a/frontend/src/pages/Settings/Languages/components.tsx +++ b/frontend/src/pages/Settings/Languages/components.tsx @@ -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, @@ -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) + ); }} > diff --git a/frontend/src/pages/Settings/Languages/table.tsx b/frontend/src/pages/Settings/Languages/table.tsx index 339f3fa99..1e1c089e6 100644 --- a/frontend/src/pages/Settings/Languages/table.tsx +++ b/frontend/src/pages/Settings/Languages/table.tsx @@ -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] ); diff --git a/frontend/src/pages/Settings/Providers/components.tsx b/frontend/src/pages/Settings/Providers/components.tsx index 30fe39b15..357a3f94a 100644 --- a/frontend/src/pages/Settings/Providers/components.tsx +++ b/frontend/src/pages/Settings/Providers/components.tsx @@ -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 = ({ const form = useForm({ 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 = ({ (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 = ({ } // 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 = ({ return ( - - - - - {info?.description} - {inputs} - - - - - - - + + + + {info?.description} + {inputs} + - + + + + + + ); diff --git a/frontend/src/pages/Settings/components/Layout.tsx b/frontend/src/pages/Settings/components/Layout.tsx index f9080230a..d89e04661 100644 --- a/frontend/src/pages/Settings/components/Layout.tsx +++ b/frontend/src/pages/Settings/components/Layout.tsx @@ -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) => { const { data: settings, isLoading, isRefetching } = useSystemSettings(); const { mutate, isLoading: isMutating } = useSettingsMutation(); - const submitHooks = useSubmitHooksSource(); - const form = useForm({ initialValues: { settings: {}, + hooks: {}, }, }); @@ -43,16 +38,16 @@ const Layout: FunctionComponent = (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) => { useDocumentTitle(`${name} - Bazarr (Settings)`); - if (settings === undefined) { - return ; - } - return ( - + - -
- - - - - - - - {children} - - -
-
+
+ + + + + + + + + {children} + + +
); diff --git a/frontend/src/pages/Settings/components/forms.tsx b/frontend/src/pages/Settings/components/forms.tsx index 83eb25da6..03d2cb614 100644 --- a/frontend/src/pages/Settings/components/forms.tsx +++ b/frontend/src/pages/Settings/components/forms.tsx @@ -33,7 +33,7 @@ export const Number: FunctionComponent = (props) => { return ( { update(val); }} @@ -49,7 +49,7 @@ export const Text: FunctionComponent = (props) => { return ( { update(e.currentTarget.value); }} @@ -65,7 +65,7 @@ export const Password: FunctionComponent = (props) => { return ( { update(e.currentTarget.value); }} diff --git a/frontend/src/pages/Settings/utilities/FormValues.ts b/frontend/src/pages/Settings/utilities/FormValues.ts index d6c78f03e..63dc28f97 100644 --- a/frontend/src/pages/Settings/utilities/FormValues.ts +++ b/frontend/src/pages/Settings/utilities/FormValues.ts @@ -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; }; + +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]); + } + } +} diff --git a/frontend/src/pages/Settings/utilities/HooksProvider.tsx b/frontend/src/pages/Settings/utilities/HooksProvider.tsx deleted file mode 100644 index d77da77a2..000000000 --- a/frontend/src/pages/Settings/utilities/HooksProvider.tsx +++ /dev/null @@ -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(null); - -type SubmitHooksProviderProps = { - value: SubmitHookModifierType; -}; - -export const SubmitHooksProvider: FunctionComponent< - SubmitHooksProviderProps -> = ({ value, children }) => { - return ( - - {children} - - ); -}; - -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({}); - 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] - ); -} diff --git a/frontend/src/pages/Settings/utilities/SettingsProvider.tsx b/frontend/src/pages/Settings/utilities/SettingsProvider.tsx index bafb1e41c..16bd86131 100644 --- a/frontend/src/pages/Settings/utilities/SettingsProvider.tsx +++ b/frontend/src/pages/Settings/utilities/SettingsProvider.tsx @@ -5,15 +5,11 @@ const SettingsContext = createContext(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 = ({ diff --git a/frontend/src/pages/Settings/utilities/hooks.ts b/frontend/src/pages/Settings/utilities/hooks.ts index 02902abeb..4da28959d 100644 --- a/frontend/src/pages/Settings/utilities/hooks.ts +++ b/frontend/src/pages/Settings/utilities/hooks.ts @@ -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 { disabled?: boolean; @@ -31,7 +30,7 @@ export function useBaseInput(props: T & BaseInput) { 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( 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);