From d9c334d43a159ee661dc132e53993b981ef7bc72 Mon Sep 17 00:00:00 2001 From: LASER-Yi Date: Thu, 30 Jun 2022 23:35:23 +0800 Subject: [PATCH] Fix #1872, refactor the settings builder --- frontend/src/pages/Settings/General/index.tsx | 18 +- .../pages/Settings/Languages/components.tsx | 7 +- .../src/pages/Settings/Languages/index.tsx | 10 +- .../Settings/Notifications/components.tsx | 11 +- .../pages/Settings/Providers/components.tsx | 11 +- frontend/src/pages/Settings/Radarr/index.tsx | 13 +- frontend/src/pages/Settings/Sonarr/index.tsx | 13 +- .../src/pages/Settings/Subtitles/index.tsx | 38 ++-- frontend/src/pages/Settings/UI/index.tsx | 2 +- .../src/pages/Settings/components/Layout.tsx | 35 ++-- .../pages/Settings/components/collapse.tsx | 2 +- .../src/pages/Settings/components/forms.tsx | 163 +++++------------- .../src/pages/Settings/components/hooks.ts | 86 --------- .../src/pages/Settings/components/index.tsx | 3 +- .../pages/Settings/components/pathMapper.tsx | 5 +- .../pages/Settings/utilities/FormValues.ts | 3 + .../src/pages/Settings/utilities/hooks.ts | 123 +++++++++++++ .../pages/Settings/utilities/modifications.ts | 8 + frontend/src/pages/UIError.tsx | 2 +- 19 files changed, 252 insertions(+), 301 deletions(-) delete mode 100644 frontend/src/pages/Settings/components/hooks.ts create mode 100644 frontend/src/pages/Settings/utilities/hooks.ts create mode 100644 frontend/src/pages/Settings/utilities/modifications.ts diff --git a/frontend/src/pages/Settings/General/index.tsx b/frontend/src/pages/Settings/General/index.tsx index cb50f7537..175278dc4 100644 --- a/frontend/src/pages/Settings/General/index.tsx +++ b/frontend/src/pages/Settings/General/index.tsx @@ -21,6 +21,7 @@ import { Selector, Text, } from "../components"; +import { BaseUrlModification } from "../utilities/modifications"; import { branchOptions, proxyOptions, securityOptions } from "./options"; const characters = "abcdef0123456789"; @@ -33,9 +34,6 @@ const generateApiKey = () => { .join(""); }; -const baseUrlOverride = (settings: Settings) => - settings.general.base_url?.slice(1) ?? ""; - const SettingsGeneralView: FunctionComponent = () => { const [copied, setCopy] = useState(false); @@ -59,8 +57,10 @@ const SettingsGeneralView: FunctionComponent = () => { label="Base URL" icon="/" settingKey="settings-general-base_url" - override={baseUrlOverride} - beforeStaged={(v) => "/" + v} + settingOptions={{ + onLoaded: BaseUrlModification, + onSubmit: (v) => "/" + v, + }} > Reverse proxy support @@ -71,7 +71,9 @@ const SettingsGeneralView: FunctionComponent = () => { options={securityOptions} placeholder="No Authentication" settingKey="settings-auth-type" - beforeStaged={(v) => (v === null ? "None" : v)} + settingOptions={{ + onSubmit: (v) => (v === null ? "None" : v), + }} > @@ -121,7 +123,9 @@ const SettingsGeneralView: FunctionComponent = () => { settingKey="settings-proxy-type" placeholder="No Proxy" options={proxyOptions} - beforeStaged={(v) => (v === null ? "None" : v)} + settingOptions={{ + onSubmit: (v) => (v === null ? "None" : v), + }} > , @@ -41,7 +42,7 @@ export const LanguageSelector: FunctionComponent< }; export const ProfileSelector: FunctionComponent< - Omit, "beforeStaged" | "options" | "clearable"> + Omit, "settingOptions" | "options" | "clearable"> > = ({ ...props }) => { const profiles = useLatestProfiles(); @@ -58,7 +59,7 @@ export const ProfileSelector: FunctionComponent< {...props} clearable options={profileOptions} - beforeStaged={(v) => (v === null ? "" : v)} + settingOptions={{ onSubmit: (v) => (v === null ? "" : v) }} > ); }; diff --git a/frontend/src/pages/Settings/Languages/index.tsx b/frontend/src/pages/Settings/Languages/index.tsx index 2fc744588..726992916 100644 --- a/frontend/src/pages/Settings/Languages/index.tsx +++ b/frontend/src/pages/Settings/Languages/index.tsx @@ -1,15 +1,9 @@ import { useLanguageProfiles, useLanguages } from "@/apis/hooks"; import { useEnabledLanguages } from "@/utilities/languages"; import { FunctionComponent } from "react"; -import { - Check, - CollapseBox, - Layout, - Message, - Section, - useSettingValue, -} from "../components"; +import { Check, CollapseBox, Layout, Message, Section } from "../components"; import { enabledLanguageKey, languageProfileKey } from "../keys"; +import { useSettingValue } from "../utilities/hooks"; import { LanguageSelector, ProfileSelector } from "./components"; import Table from "./table"; diff --git a/frontend/src/pages/Settings/Notifications/components.tsx b/frontend/src/pages/Settings/Notifications/components.tsx index e6185dc9b..5f1f833d9 100644 --- a/frontend/src/pages/Settings/Notifications/components.tsx +++ b/frontend/src/pages/Settings/Notifications/components.tsx @@ -14,8 +14,9 @@ import { import { useForm } from "@mantine/hooks"; import { FunctionComponent, useMemo } from "react"; import { useMutation } from "react-query"; -import { Card, useLatestArray, useUpdateArray } from "../components"; +import { Card } from "../components"; import { notificationsKey } from "../keys"; +import { useSettingValue, useUpdateArray } from "../utilities/hooks"; interface Props { selections: readonly Settings.NotificationInfo[]; @@ -100,10 +101,12 @@ const NotificationModal = withModal(NotificationForm, "notification-tool", { }); export const NotificationView: FunctionComponent = () => { - const notifications = useLatestArray( + const notifications = useSettingValue( notificationsKey, - "name", - (s) => s.notifications.providers + { + onLoaded: (settings) => settings.notifications.providers, + onSubmit: (value) => value.map((v) => JSON.stringify(v)), + } ); const update = useUpdateArray( diff --git a/frontend/src/pages/Settings/Providers/components.tsx b/frontend/src/pages/Settings/Providers/components.tsx index 8fc6575ea..da3628c8b 100644 --- a/frontend/src/pages/Settings/Providers/components.tsx +++ b/frontend/src/pages/Settings/Providers/components.tsx @@ -20,21 +20,14 @@ import { useRef, useState, } from "react"; -import { - Card, - Check, - Chips, - Message, - Password, - Text, - useSettingValue, -} from "../components"; +import { Card, Check, Chips, Message, Password, Text } from "../components"; import { FormContext, FormValues, useFormActions, useStagedValues, } from "../utilities/FormValues"; +import { useSettingValue } from "../utilities/hooks"; import { SettingsProvider, useSettings } from "../utilities/SettingsProvider"; import { ProviderInfo, ProviderList } from "./list"; diff --git a/frontend/src/pages/Settings/Radarr/index.tsx b/frontend/src/pages/Settings/Radarr/index.tsx index f76cb8dba..41f186a25 100644 --- a/frontend/src/pages/Settings/Radarr/index.tsx +++ b/frontend/src/pages/Settings/Radarr/index.tsx @@ -1,5 +1,5 @@ import { Code } from "@mantine/core"; -import { FunctionComponent, useCallback } from "react"; +import { FunctionComponent } from "react"; import { Check, Chips, @@ -14,12 +14,9 @@ import { URLTestButton, } from "../components"; import { moviesEnabledKey } from "../keys"; +import { BaseUrlModification } from "../utilities/modifications"; const SettingsRadarrView: FunctionComponent = () => { - const baseUrlOverride = useCallback((settings: Settings) => { - return settings.radarr.base_url?.slice(1) ?? ""; - }, []); - return (
@@ -34,8 +31,10 @@ const SettingsRadarrView: FunctionComponent = () => { label="Base URL" icon="/" settingKey="settings-radarr-base_url" - override={baseUrlOverride} - beforeStaged={(v) => "/" + v} + settingOptions={{ + onLoaded: BaseUrlModification, + onSubmit: (v) => "/" + v, + }} > diff --git a/frontend/src/pages/Settings/Sonarr/index.tsx b/frontend/src/pages/Settings/Sonarr/index.tsx index f388dc039..2f5490392 100644 --- a/frontend/src/pages/Settings/Sonarr/index.tsx +++ b/frontend/src/pages/Settings/Sonarr/index.tsx @@ -1,5 +1,5 @@ import { Code } from "@mantine/core"; -import { FunctionComponent, useCallback } from "react"; +import { FunctionComponent } from "react"; import { Check, Chips, @@ -16,12 +16,9 @@ import { } from "../components"; import { seriesEnabledKey } from "../keys"; import { seriesTypeOptions } from "../options"; +import { BaseUrlModification } from "../utilities/modifications"; const SettingsSonarrView: FunctionComponent = () => { - const baseUrlOverride = useCallback((settings: Settings) => { - return settings.sonarr.base_url?.slice(1) ?? ""; - }, []); - return (
@@ -36,8 +33,10 @@ const SettingsSonarrView: FunctionComponent = () => { label="Base URL" icon="/" settingKey="settings-sonarr-base_url" - override={baseUrlOverride} - beforeStaged={(v) => "/" + v} + settingOptions={{ + onLoaded: BaseUrlModification, + onSubmit: (v) => "/" + v, + }} > diff --git a/frontend/src/pages/Settings/Subtitles/index.tsx b/frontend/src/pages/Settings/Subtitles/index.tsx index f06dfa1f8..adb0b56d5 100644 --- a/frontend/src/pages/Settings/Subtitles/index.tsx +++ b/frontend/src/pages/Settings/Subtitles/index.tsx @@ -11,6 +11,10 @@ import { Slider, Text, } from "../components"; +import { + SubzeroColorModification, + SubzeroModification, +} from "../utilities/modifications"; import { adaptiveSearchingDelayOption, adaptiveSearchingDeltaOption, @@ -20,18 +24,6 @@ import { hiExtensionOptions, } from "./options"; -const subzeroOverride = (key: string) => { - return (settings: Settings) => { - return settings.general.subzero_mods?.includes(key) ?? false; - }; -}; - -const subzeroColorOverride = (settings: Settings) => { - return ( - settings.general.subzero_mods?.find((v) => v.startsWith("color")) ?? "" - ); -}; - interface CommandOption { option: string; description: string; @@ -179,7 +171,7 @@ const SettingsSubtitlesView: FunctionComponent = () => { clearable placeholder="Select a provider" settingKey="settings-general-anti_captcha_provider" - beforeStaged={(v) => (v === undefined ? "None" : v)} + settingOptions={{ onSubmit: (v) => (v === undefined ? "None" : v) }} options={antiCaptchaOption} > Choose the anti-captcha provider you want to use @@ -224,7 +216,7 @@ const SettingsSubtitlesView: FunctionComponent = () => { (v === undefined ? "3w" : v)} + settingOptions={{ onSaved: (v) => (v === undefined ? "3w" : v) }} options={adaptiveSearchingDelayOption} > @@ -233,7 +225,7 @@ const SettingsSubtitlesView: FunctionComponent = () => { (v === undefined ? "1w" : v)} + settingOptions={{ onSaved: (v) => (v === undefined ? "1w" : v) }} options={adaptiveSearchingDeltaOption} > @@ -299,7 +291,7 @@ const SettingsSubtitlesView: FunctionComponent = () => { @@ -308,7 +300,7 @@ const SettingsSubtitlesView: FunctionComponent = () => { @@ -317,7 +309,7 @@ const SettingsSubtitlesView: FunctionComponent = () => { @@ -326,7 +318,7 @@ const SettingsSubtitlesView: FunctionComponent = () => { @@ -334,7 +326,9 @@ const SettingsSubtitlesView: FunctionComponent = () => { @@ -345,7 +339,7 @@ const SettingsSubtitlesView: FunctionComponent = () => { label="Color" clearable options={colorOptions} - override={subzeroColorOverride} + settingOptions={{ onLoaded: SubzeroColorModification }} settingKey="subzero-color" > @@ -355,7 +349,7 @@ const SettingsSubtitlesView: FunctionComponent = () => { diff --git a/frontend/src/pages/Settings/UI/index.tsx b/frontend/src/pages/Settings/UI/index.tsx index dadce55c8..2f03e8c18 100644 --- a/frontend/src/pages/Settings/UI/index.tsx +++ b/frontend/src/pages/Settings/UI/index.tsx @@ -13,7 +13,7 @@ const SettingsUIView: FunctionComponent = () => { options={pageSizeOptions} location="storages" settingKey={uiPageSizeKey} - override={(_) => pageSize} + settingOptions={{ onLoaded: () => pageSize }} >
diff --git a/frontend/src/pages/Settings/components/Layout.tsx b/frontend/src/pages/Settings/components/Layout.tsx index 5e6949a28..d79cd2eec 100644 --- a/frontend/src/pages/Settings/components/Layout.tsx +++ b/frontend/src/pages/Settings/components/Layout.tsx @@ -8,28 +8,27 @@ import { faSave } from "@fortawesome/free-solid-svg-icons"; import { Container, Group, LoadingOverlay } from "@mantine/core"; import { useDocumentTitle, useForm } from "@mantine/hooks"; import { FunctionComponent, ReactNode, useCallback, useMemo } from "react"; -import { - enabledLanguageKey, - languageProfileKey, - notificationsKey, -} from "../keys"; +import { enabledLanguageKey, languageProfileKey } from "../keys"; import { FormContext, FormValues } from "../utilities/FormValues"; import { SettingsProvider } from "../utilities/SettingsProvider"; -function submitHooks(settings: LooseObject) { - if (languageProfileKey in settings) { - const item = settings[languageProfileKey]; - settings[languageProfileKey] = JSON.stringify(item); - } +type SubmitHookType = { + // eslint-disable-next-line @typescript-eslint/no-explicit-any + [key: string]: (value: any) => unknown; +}; - if (enabledLanguageKey in settings) { - const item = settings[enabledLanguageKey] as Language.Info[]; - settings[enabledLanguageKey] = item.map((v) => v.code2); - } +export const submitHooks: SubmitHookType = { + [languageProfileKey]: (value) => JSON.stringify(value), + [enabledLanguageKey]: (value: Language.Info[]) => value.map((v) => v.code2), +}; - if (notificationsKey in settings) { - const item = settings[notificationsKey] as Settings.NotificationInfo[]; - settings[notificationsKey] = item.map((v) => JSON.stringify(v)); +function invokeHooks(settings: LooseObject) { + for (const key in settings) { + if (key in submitHooks) { + const value = settings[key]; + const fn = submitHooks[key]; + settings[key] = fn(value); + } } } @@ -65,7 +64,7 @@ const Layout: FunctionComponent = (props) => { if (Object.keys(settings).length > 0) { const settingsToSubmit = { ...settings }; - submitHooks(settingsToSubmit); + invokeHooks(settingsToSubmit); LOG("info", "submitting settings", settingsToSubmit); mutate(settingsToSubmit); } diff --git a/frontend/src/pages/Settings/components/collapse.tsx b/frontend/src/pages/Settings/components/collapse.tsx index 32b582496..8f224f109 100644 --- a/frontend/src/pages/Settings/components/collapse.tsx +++ b/frontend/src/pages/Settings/components/collapse.tsx @@ -1,6 +1,6 @@ import { Collapse, Stack } from "@mantine/core"; import { FunctionComponent, useMemo, useRef } from "react"; -import { useSettingValue } from "./hooks"; +import { useSettingValue } from "../utilities/hooks"; interface ContentProps { settingKey: string; diff --git a/frontend/src/pages/Settings/components/forms.tsx b/frontend/src/pages/Settings/components/forms.tsx index e0d880db9..09cb18789 100644 --- a/frontend/src/pages/Settings/components/forms.tsx +++ b/frontend/src/pages/Settings/components/forms.tsx @@ -22,38 +22,20 @@ import { TextInput, TextInputProps, } from "@mantine/core"; -import { FunctionComponent, ReactText, useCallback } from "react"; -import { useSettingValue } from "."; -import { FormKey, useFormActions } from "../utilities/FormValues"; -import { OverrideFuncType } from "./hooks"; - -export interface BaseInput { - disabled?: boolean; - settingKey: string; - location?: FormKey; - override?: OverrideFuncType; - beforeStaged?: (v: T) => unknown; -} +import { FunctionComponent, ReactText } from "react"; +import { BaseInput, useBaseInput } from "../utilities/hooks"; export type NumberProps = BaseInput & NumberInputProps; -export const Number: FunctionComponent = ({ - beforeStaged, - override, - settingKey, - location, - ...props -}) => { - const value = useSettingValue(settingKey, override); - const { setValue } = useFormActions(); +export const Number: FunctionComponent = (props) => { + const { value, update, rest } = useBaseInput(props); return ( { - const value = beforeStaged ? beforeStaged(val) : val; - setValue(value, settingKey, location); + update(val); }} > ); @@ -61,24 +43,15 @@ export const Number: FunctionComponent = ({ export type TextProps = BaseInput & TextInputProps; -export const Text: FunctionComponent = ({ - beforeStaged, - override, - settingKey, - location, - ...props -}) => { - const value = useSettingValue(settingKey, override); - const { setValue } = useFormActions(); +export const Text: FunctionComponent = (props) => { + const { value, update, rest } = useBaseInput(props); return ( { - const val = e.currentTarget.value; - const value = beforeStaged ? beforeStaged(val) : val; - setValue(value, settingKey, location); + update(e.currentTarget.value); }} > ); @@ -86,24 +59,15 @@ export const Text: FunctionComponent = ({ export type PasswordProps = BaseInput & PasswordInputProps; -export const Password: FunctionComponent = ({ - settingKey, - location, - override, - beforeStaged, - ...props -}) => { - const value = useSettingValue(settingKey, override); - const { setValue } = useFormActions(); +export const Password: FunctionComponent = (props) => { + const { value, update, rest } = useBaseInput(props); return ( { - const val = e.currentTarget.value; - const value = beforeStaged ? beforeStaged(val) : val; - setValue(value, settingKey, location); + update(e.currentTarget.value); }} > ); @@ -116,23 +80,18 @@ export interface CheckProps extends BaseInput { export const Check: FunctionComponent = ({ label, - override, - disabled, - settingKey, - location, + inline, + ...props }) => { - const value = useSettingValue(settingKey, override); - const { setValue } = useFormActions(); + const { value, update, rest } = useBaseInput(props); return ( { - const { checked } = e.currentTarget; - setValue(checked, settingKey, location); + update(e.currentTarget.checked); }} - disabled={disabled} + disabled={rest.disabled} checked={value ?? false} > ); @@ -142,20 +101,10 @@ export type SelectorProps = BaseInput & GlobalSelectorProps; export function Selector(props: SelectorProps) { - const { settingKey, location, override, beforeStaged, ...selector } = props; - - const value = useSettingValue(settingKey, override); - const { setValue } = useFormActions(); + const { value, update, rest } = useBaseInput(props); return ( - { - const result = beforeStaged && v ? beforeStaged(v) : v; - setValue(result, settingKey, location); - }} - > + ); } @@ -165,19 +114,13 @@ export type MultiSelectorProps = BaseInput & export function MultiSelector( props: MultiSelectorProps ) { - const { settingKey, location, override, beforeStaged, ...selector } = props; - - const value = useSettingValue(settingKey, override); - const { setValue } = useFormActions(); + const { value, update, rest } = useBaseInput(props); return ( { - const result = beforeStaged && v ? beforeStaged(v) : v; - setValue(result, settingKey, location); - }} + onChange={update} > ); } @@ -186,22 +129,19 @@ type SliderProps = BaseInput & Omit; export const Slider: FunctionComponent = (props) => { - const { settingKey, location, override, label, ...slider } = props; + const { value, update, rest } = useBaseInput(props); - const value = useSettingValue(settingKey, override); - const { setValue } = useFormActions(); + const { min = 0, max = 100 } = props; - const marks = useSliderMarks([(slider.min = 0), (slider.max = 100)]); + const marks = useSliderMarks([min, max]); return ( - + { - setValue(v, settingKey, location); - }} + onChange={update} value={value ?? 0} - {...slider} > ); @@ -211,47 +151,28 @@ type ChipsProp = BaseInput & Omit; export const Chips: FunctionComponent = (props) => { - const { settingKey, location, override, ...chips } = props; - - const value = useSettingValue(settingKey, override); - const { setValue } = useFormActions(); + const { value, update, rest } = useBaseInput(props); return ( - { - setValue(v, settingKey, location); - }} - {...chips} - > + ); }; type ActionProps = { - onClick?: (update: (v: unknown) => void, value?: string) => void; -} & Omit, "override" | "beforeStaged">; + onClick?: (update: (v: string) => void, value?: string) => void; +} & Omit, "modification">; export const Action: FunctionComponent< Override > = (props) => { - const { onClick, settingKey, location, ...button } = props; - - const value = useSettingValue(settingKey); - const { setValue } = useFormActions(); - - const wrappedSetValue = useCallback( - (v: unknown) => { - setValue(v, settingKey, location); - }, - [location, setValue, settingKey] - ); + const { value, update, rest } = useBaseInput(props); return ( { - onClick?.(wrappedSetValue, value ?? undefined); + props.onClick?.(update, (value as string) ?? undefined); }} - {...button} > ); }; @@ -261,17 +182,13 @@ interface FileProps extends BaseInput {} export const File: FunctionComponent> = ( props ) => { - const { settingKey, location, override, ...file } = props; - const value = useSettingValue(settingKey); - const { setValue } = useFormActions(); + const { value, update, rest } = useBaseInput(props); return ( { - setValue(p, settingKey, location); - }} - {...file} + onChange={update} > ); }; diff --git a/frontend/src/pages/Settings/components/hooks.ts b/frontend/src/pages/Settings/components/hooks.ts deleted file mode 100644 index 4793a4a62..000000000 --- a/frontend/src/pages/Settings/components/hooks.ts +++ /dev/null @@ -1,86 +0,0 @@ -import { get, uniqBy } from "lodash"; -import { useCallback, useMemo, useRef } from "react"; -import { useFormActions, useStagedValues } from "../utilities/FormValues"; -import { useSettings } from "../utilities/SettingsProvider"; - -export type OverrideFuncType = (settings: Settings) => T; - -export function useExtract( - key: string, - override?: OverrideFuncType -): Readonly> { - const settings = useSettings(); - - const overrideRef = useRef(override); - overrideRef.current = override; - - const extractValue = useMemo(() => { - if (overrideRef.current) { - return overrideRef.current(settings); - } - - const path = key.replaceAll("-", "."); - - const value = get({ settings }, path, null) as Nullable; - - return value; - }, [key, settings]); - - return extractValue; -} - -export function useSettingValue( - key: string, - override?: OverrideFuncType -): Readonly> { - const extractValue = useExtract(key, override); - const stagedValue = useStagedValues(); - if (key in stagedValue) { - return stagedValue[key] as T; - } else { - return extractValue; - } -} - -export function useLatestArray( - key: string, - compare: keyof T, - override?: OverrideFuncType -): Readonly> { - const extractValue = useExtract(key, override); - const stagedValue = useStagedValues(); - - let staged: T[] | undefined = undefined; - if (key in stagedValue) { - staged = stagedValue[key]; - } - - return useMemo(() => { - if (staged !== undefined && extractValue) { - return uniqBy([...staged, ...extractValue], compare); - } else { - return extractValue; - } - }, [extractValue, staged, compare]); -} - -export function useUpdateArray(key: string, compare: keyof T) { - const { setValue } = useFormActions(); - const stagedValue = useStagedValues(); - - const staged: T[] = useMemo(() => { - if (key in stagedValue) { - return stagedValue[key]; - } else { - return []; - } - }, [key, stagedValue]); - - return useCallback( - (v: T) => { - const newArray = uniqBy([v, ...staged], compare); - setValue(newArray, key); - }, - [staged, compare, setValue, key] - ); -} diff --git a/frontend/src/pages/Settings/components/index.tsx b/frontend/src/pages/Settings/components/index.tsx index 9df168a5d..437e6948d 100644 --- a/frontend/src/pages/Settings/components/index.tsx +++ b/frontend/src/pages/Settings/components/index.tsx @@ -1,7 +1,7 @@ import api from "@/apis/raw"; import { Button } from "@mantine/core"; import { FunctionComponent, useCallback, useState } from "react"; -import { useSettingValue } from "./hooks"; +import { useSettingValue } from "../utilities/hooks"; export const URLTestButton: FunctionComponent<{ category: "sonarr" | "radarr"; @@ -60,7 +60,6 @@ export * from "./Card"; export * from "./collapse"; export { default as CollapseBox } from "./collapse"; export * from "./forms"; -export * from "./hooks"; export * from "./Layout"; export { default as Layout } from "./Layout"; export * from "./Message"; diff --git a/frontend/src/pages/Settings/components/pathMapper.tsx b/frontend/src/pages/Settings/components/pathMapper.tsx index 6c11139c8..a7e76594b 100644 --- a/frontend/src/pages/Settings/components/pathMapper.tsx +++ b/frontend/src/pages/Settings/components/pathMapper.tsx @@ -13,7 +13,7 @@ import { seriesEnabledKey, } from "../keys"; import { useFormActions } from "../utilities/FormValues"; -import { useExtract, useSettingValue } from "./hooks"; +import { useSettingValue } from "../utilities/hooks"; import { Message } from "./Message"; type SupportType = "sonarr" | "radarr"; @@ -48,7 +48,8 @@ export const PathMappingTable: FunctionComponent = ({ type }) => { const items = useSettingValue<[string, string][]>(key); const enabledKey = getEnabledKey(type); - const enabled = useExtract(enabledKey); + const enabled = useSettingValue(enabledKey, { original: true }); + const { setValue } = useFormActions(); const updateRow = useCallback( diff --git a/frontend/src/pages/Settings/utilities/FormValues.ts b/frontend/src/pages/Settings/utilities/FormValues.ts index d2a6618ea..caf11a282 100644 --- a/frontend/src/pages/Settings/utilities/FormValues.ts +++ b/frontend/src/pages/Settings/utilities/FormValues.ts @@ -1,3 +1,4 @@ +import { LOG } from "@/utilities/console"; import { UseForm } from "@mantine/hooks/lib/use-form/use-form"; import { createContext, useCallback, useContext, useRef } from "react"; @@ -26,6 +27,7 @@ export function useFormActions() { const update = useCallback( (object: LooseObject, location: FormKey = "settings") => { + LOG("info", `Updating values in ${location}`, object); formRef.current.setValues((values) => { const changes = { ...values[location], ...object }; return { ...values, [location]: changes }; @@ -36,6 +38,7 @@ export function useFormActions() { const setValue = useCallback( (v: unknown, key: string, location: FormKey = "settings") => { + LOG("info", `Updating value of ${key} in ${location}`, v); formRef.current.setValues((values) => { const changes = { ...values[location], [key]: v }; return { ...values, [location]: changes }; diff --git a/frontend/src/pages/Settings/utilities/hooks.ts b/frontend/src/pages/Settings/utilities/hooks.ts new file mode 100644 index 000000000..09f054cc3 --- /dev/null +++ b/frontend/src/pages/Settings/utilities/hooks.ts @@ -0,0 +1,123 @@ +import { LOG } from "@/utilities/console"; +import { get, isNull, isUndefined, uniqBy } from "lodash"; +import { useCallback, useEffect, useMemo, useRef } from "react"; +import { submitHooks } from "../components"; +import { + FormKey, + useFormActions, + useStagedValues, +} from "../utilities/FormValues"; +import { useSettings } from "../utilities/SettingsProvider"; + +export interface BaseInput { + disabled?: boolean; + settingKey: string; + location?: FormKey; + settingOptions?: SettingValueOptions; +} + +export type SettingValueOptions = { + original?: boolean; + defaultValue?: T; + onLoaded?: (settings: Settings) => T; + onSaved?: (value: T) => unknown; + onSubmit?: (value: T) => unknown; +}; + +export function useBaseInput(props: T & BaseInput) { + const { settingKey, settingOptions, location, ...rest } = props; + // TODO: Opti options + const value = useSettingValue(settingKey, settingOptions); + + const { setValue } = useFormActions(); + + const update = useCallback( + (newValue: V | null) => { + const moddedValue = + (newValue && settingOptions?.onSaved?.(newValue)) ?? newValue; + + setValue(moddedValue, settingKey, location); + }, + [settingOptions, setValue, settingKey, location] + ); + + return { value, update, rest }; +} + +export function useSettingValue( + key: string, + options?: SettingValueOptions +): Readonly> { + const settings = useSettings(); + + const optionsRef = useRef(options); + + useEffect(() => { + const onSubmit = optionsRef.current?.onSubmit; + if (onSubmit) { + LOG("info", "Adding submit hook for", key); + submitHooks[key] = onSubmit; + } + + return () => { + if (key in submitHooks) { + LOG("info", "Removing submit hook for", key); + delete submitHooks[key]; + } + }; + }, [key]); + + const originalValue = useMemo(() => { + const onLoaded = optionsRef.current?.onLoaded; + const defaultValue = optionsRef.current?.defaultValue; + if (onLoaded) { + LOG("info", `${key} is using custom loader`); + + return onLoaded(settings); + } + + const path = key.replaceAll("-", "."); + + const value = get({ settings }, path, null) as Nullable; + + if (defaultValue && (isNull(value) || isUndefined(value))) { + LOG("info", `${key} is falling back to`, defaultValue); + + return defaultValue; + } + + return value; + }, [key, settings]); + + const stagedValue = useStagedValues(); + + if (key in stagedValue && optionsRef.current?.original !== true) { + return stagedValue[key] as T; + } else { + return originalValue; + } +} + +export function useUpdateArray(key: string, compare: keyof T) { + const { setValue } = useFormActions(); + const stagedValue = useStagedValues(); + + const compareRef = useRef(compare); + compareRef.current = compare; + + const staged: T[] = useMemo(() => { + if (key in stagedValue) { + return stagedValue[key]; + } else { + return []; + } + }, [key, stagedValue]); + + return useCallback( + (v: T) => { + const newArray = uniqBy([v, ...staged], compareRef.current); + setValue(newArray, key); + }, + [staged, setValue, key] + ); +} diff --git a/frontend/src/pages/Settings/utilities/modifications.ts b/frontend/src/pages/Settings/utilities/modifications.ts new file mode 100644 index 000000000..61abc4f91 --- /dev/null +++ b/frontend/src/pages/Settings/utilities/modifications.ts @@ -0,0 +1,8 @@ +export const BaseUrlModification = (settings: Settings) => + settings.general.base_url?.slice(1) ?? ""; + +export const SubzeroModification = (key: string) => (settings: Settings) => + settings.general.subzero_mods?.includes(key) ?? false; + +export const SubzeroColorModification = (settings: Settings) => + settings.general.subzero_mods?.find((v) => v.startsWith("color")) ?? ""; diff --git a/frontend/src/pages/UIError.tsx b/frontend/src/pages/UIError.tsx index 7f84ab91b..4f26d0d0c 100644 --- a/frontend/src/pages/UIError.tsx +++ b/frontend/src/pages/UIError.tsx @@ -26,7 +26,7 @@ const UIError: FunctionComponent = ({ error }) => { let callStack = error.stack ?? ""; // Remove sensitive information from the stack - callStack = callStack.replaceAll(window.location.hostname, Placeholder); + callStack = callStack.replaceAll(window.location.host, Placeholder); return callStack; }, [error.stack]);