From b1f07f0eb20c6090eaa153b7ad2ef0142a84042a Mon Sep 17 00:00:00 2001 From: Gauthier Date: Sat, 22 Feb 2025 17:17:19 +0100 Subject: [PATCH] refactor(overriderules): move override rules out of the service modal (#1292) * refactor(overriderules): move override rules out of the service modal This PR moves override rules out of the service modal. This will make override rules more visible than inside the service modal popup. This will also avoid having a modal inside a modal (override rules modal inside of service modal) * fix: resolve typing error --- src/components/LanguageSelector/index.tsx | 3 + src/components/Selector/index.tsx | 12 + .../OverrideRule/OverrideRuleModal.tsx | 184 +++++++++- .../OverrideRule/OverrideRuleTile.tsx | 267 --------------- .../OverrideRule/OverrideRuleTiles.tsx | 318 ++++++++++++++++++ src/components/Settings/RadarrModal/index.tsx | 74 +--- src/components/Settings/SettingsServices.tsx | 82 +++-- src/components/Settings/SonarrModal/index.tsx | 74 +--- src/i18n/locale/en.json | 10 +- 9 files changed, 577 insertions(+), 447 deletions(-) delete mode 100644 src/components/Settings/OverrideRule/OverrideRuleTile.tsx create mode 100644 src/components/Settings/OverrideRule/OverrideRuleTiles.tsx diff --git a/src/components/LanguageSelector/index.tsx b/src/components/LanguageSelector/index.tsx index d7b9853c..083ecbc7 100644 --- a/src/components/LanguageSelector/index.tsx +++ b/src/components/LanguageSelector/index.tsx @@ -33,6 +33,7 @@ interface LanguageSelectorProps { setFieldValue: (property: string, value: string) => void; serverValue?: string; isUserSettings?: boolean; + isDisabled?: boolean; } const LanguageSelector = ({ @@ -40,6 +41,7 @@ const LanguageSelector = ({ setFieldValue, serverValue, isUserSettings = false, + isDisabled, }: LanguageSelectorProps) => { const intl = useIntl(); const { data: languages } = useSWR('/api/v1/languages'); @@ -96,6 +98,7 @@ const LanguageSelector = ({ options={options} isMulti + isDisabled={isDisabled} className="react-select-container" classNamePrefix="react-select" value={ diff --git a/src/components/Selector/index.tsx b/src/components/Selector/index.tsx index 6c831909..2098c935 100644 --- a/src/components/Selector/index.tsx +++ b/src/components/Selector/index.tsx @@ -52,18 +52,21 @@ type SingleVal = { type BaseSelectorMultiProps = { defaultValue?: string; isMulti: true; + isDisabled?: boolean; onChange: (value: MultiValue | null) => void; }; type BaseSelectorSingleProps = { defaultValue?: string; isMulti?: false; + isDisabled?: boolean; onChange: (value: SingleValue | null) => void; }; export const CompanySelector = ({ defaultValue, isMulti, + isDisabled, onChange, }: BaseSelectorSingleProps | BaseSelectorMultiProps) => { const intl = useIntl(); @@ -117,6 +120,7 @@ export const CompanySelector = ({ className="react-select-container" classNamePrefix="react-select" isMulti={isMulti} + isDisabled={isDisabled} defaultValue={defaultDataValue} defaultOptions cacheOptions @@ -143,6 +147,7 @@ type GenreSelectorProps = (BaseSelectorMultiProps | BaseSelectorSingleProps) & { export const GenreSelector = ({ isMulti, defaultValue, + isDisabled, onChange, type, }: GenreSelectorProps) => { @@ -203,6 +208,7 @@ export const GenreSelector = ({ defaultOptions cacheOptions isMulti={isMulti} + isDisabled={isDisabled} loadOptions={loadGenreOptions} placeholder={intl.formatMessage(messages.searchGenres)} onChange={(value) => { @@ -215,6 +221,7 @@ export const GenreSelector = ({ export const StatusSelector = ({ isMulti, + isDisabled, defaultValue, onChange, }: BaseSelectorMultiProps | BaseSelectorSingleProps) => { @@ -272,6 +279,7 @@ export const StatusSelector = ({ defaultValue={isMulti ? defaultDataValue : defaultDataValue?.[0]} defaultOptions isMulti={isMulti} + isDisabled={isDisabled} loadOptions={loadStatusOptions} placeholder={intl.formatMessage(messages.searchStatus)} onChange={(value) => { @@ -284,6 +292,7 @@ export const StatusSelector = ({ export const KeywordSelector = ({ isMulti, + isDisabled, defaultValue, onChange, }: BaseSelectorMultiProps | BaseSelectorSingleProps) => { @@ -341,6 +350,7 @@ export const KeywordSelector = ({ key={`keyword-select-${defaultDataValue}`} inputId="data" isMulti={isMulti} + isDisabled={isDisabled} className="react-select-container" classNamePrefix="react-select" noOptionsMessage={({ inputValue }) => @@ -551,6 +561,7 @@ export const WatchProviderSelector = ({ export const UserSelector = ({ isMulti, + isDisabled, defaultValue, onChange, }: BaseSelectorMultiProps | BaseSelectorSingleProps) => { @@ -613,6 +624,7 @@ export const UserSelector = ({ defaultOptions cacheOptions isMulti={isMulti} + isDisabled={isDisabled} loadOptions={loadUserOptions} placeholder={intl.formatMessage(messages.searchUsers)} onChange={(value) => { diff --git a/src/components/Settings/OverrideRule/OverrideRuleModal.tsx b/src/components/Settings/OverrideRule/OverrideRuleModal.tsx index becb1ee9..a2776bba 100644 --- a/src/components/Settings/OverrideRule/OverrideRuleModal.tsx +++ b/src/components/Settings/OverrideRule/OverrideRuleModal.tsx @@ -11,7 +11,13 @@ import globalMessages from '@app/i18n/globalMessages'; import defineMessages from '@app/utils/defineMessages'; import { Transition } from '@headlessui/react'; import type OverrideRule from '@server/entity/OverrideRule'; +import type { + DVRSettings, + RadarrSettings, + SonarrSettings, +} from '@server/lib/settings'; import { Field, Formik } from 'formik'; +import { useCallback, useEffect, useState } from 'react'; import { useIntl } from 'react-intl'; import Select from 'react-select'; import { useToasts } from 'react-toast-notifications'; @@ -20,6 +26,9 @@ const messages = defineMessages('components.Settings.OverrideRuleModal', { createrule: 'New Override Rule', editrule: 'Edit Override Rule', create: 'Create rule', + service: 'Service', + serviceDescription: 'Apply this rule to the selected service.', + selectService: 'Select service', conditions: 'Conditions', conditionsDescription: 'Specifies conditions before applying parameter changes. Each field must be validated for the rules to be applied (AND operation). A field is considered verified if any of its properties match (OR operation).', @@ -49,21 +58,88 @@ type OptionType = { interface OverrideRuleModalProps { rule: OverrideRule | null; onClose: () => void; - testResponse: DVRTestResponse; - radarrId?: number; - sonarrId?: number; + radarrServices: RadarrSettings[]; + sonarrServices: SonarrSettings[]; } const OverrideRuleModal = ({ onClose, rule, - testResponse, - radarrId, - sonarrId, + radarrServices, + sonarrServices, }: OverrideRuleModalProps) => { const intl = useIntl(); const { addToast } = useToasts(); const { currentSettings } = useSettings(); + const [isValidated, setIsValidated] = useState(rule ? true : false); + const [isTesting, setIsTesting] = useState(false); + const [testResponse, setTestResponse] = useState({ + profiles: [], + rootFolders: [], + tags: [], + }); + + const getServiceInfos = useCallback( + async ({ + hostname, + port, + apiKey, + baseUrl, + useSsl = false, + }: { + hostname: string; + port: number; + apiKey: string; + baseUrl?: string; + useSsl?: boolean; + }) => { + setIsTesting(true); + try { + const res = await fetch('/api/v1/settings/sonarr/test', { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + }, + body: JSON.stringify({ + hostname, + apiKey, + port: Number(port), + baseUrl, + useSsl, + }), + }); + if (!res.ok) throw new Error(); + const data: DVRTestResponse = await res.json(); + + setIsValidated(true); + setTestResponse(data); + } catch (e) { + setIsValidated(false); + } finally { + setIsTesting(false); + } + }, + [] + ); + + useEffect(() => { + let service: DVRSettings | null = null; + if (rule?.radarrServiceId !== null && rule?.radarrServiceId !== undefined) { + service = radarrServices[rule?.radarrServiceId] || null; + } + if (rule?.sonarrServiceId !== null && rule?.sonarrServiceId !== undefined) { + service = sonarrServices[rule?.sonarrServiceId] || null; + } + if (service) { + getServiceInfos(service); + } + }, [ + getServiceInfos, + radarrServices, + rule?.radarrServiceId, + rule?.sonarrServiceId, + sonarrServices, + ]); return (
+

+ {intl.formatMessage(messages.service)} +

+

+ {intl.formatMessage(messages.serviceDescription)} +

+
+ +
+
+ +
+ {errors.rootFolder && + touched.rootFolder && + typeof errors.rootFolder === 'string' && ( +
{errors.rootFolder}
+ )} +
+

{intl.formatMessage(messages.conditions)}

@@ -184,6 +331,7 @@ const OverrideRuleModal = ({
{ setFieldValue( @@ -207,9 +355,10 @@ const OverrideRuleModal = ({
{ setFieldValue( 'genre', @@ -237,6 +386,7 @@ const OverrideRuleModal = ({ setFieldValue={(_key, value) => { setFieldValue('language', value); }} + isDisabled={!isValidated || isTesting} />
{errors.language && @@ -255,6 +405,7 @@ const OverrideRuleModal = ({ { setFieldValue( 'keywords', @@ -282,7 +433,12 @@ const OverrideRuleModal = ({
- + @@ -310,7 +466,12 @@ const OverrideRuleModal = ({
- + @@ -343,6 +504,7 @@ const OverrideRuleModal = ({ value: tag.id, }))} isMulti + isDisabled={!isValidated || isTesting} placeholder={intl.formatMessage(messages.selecttags)} className="react-select-container" classNamePrefix="react-select" diff --git a/src/components/Settings/OverrideRule/OverrideRuleTile.tsx b/src/components/Settings/OverrideRule/OverrideRuleTile.tsx deleted file mode 100644 index c5c0451a..00000000 --- a/src/components/Settings/OverrideRule/OverrideRuleTile.tsx +++ /dev/null @@ -1,267 +0,0 @@ -import type { DVRTestResponse } from '@app/components/Settings/SettingsServices'; -import globalMessages from '@app/i18n/globalMessages'; -import defineMessages from '@app/utils/defineMessages'; -import { PencilIcon, TrashIcon } from '@heroicons/react/24/solid'; -import type { TmdbGenre } from '@server/api/themoviedb/interfaces'; -import type OverrideRule from '@server/entity/OverrideRule'; -import type { User } from '@server/entity/User'; -import type { - Language, - RadarrSettings, - SonarrSettings, -} from '@server/lib/settings'; -import type { Keyword } from '@server/models/common'; -import { useEffect, useState } from 'react'; -import { useIntl } from 'react-intl'; -import useSWR from 'swr'; - -const messages = defineMessages('components.Settings.OverrideRuleTile', { - qualityprofile: 'Quality Profile', - rootfolder: 'Root Folder', - tags: 'Tags', - users: 'Users', - genre: 'Genre', - language: 'Language', - keywords: 'Keywords', - conditions: 'Conditions', - settings: 'Settings', -}); - -interface OverrideRuleTileProps { - rules: OverrideRule[]; - setOverrideRuleModal: ({ - open, - rule, - testResponse, - }: { - open: boolean; - rule: OverrideRule | null; - testResponse: DVRTestResponse; - }) => void; - testResponse: DVRTestResponse; - radarr?: RadarrSettings | null; - sonarr?: SonarrSettings | null; - revalidate: () => void; -} - -const OverrideRuleTile = ({ - rules, - setOverrideRuleModal, - testResponse, - radarr, - sonarr, - revalidate, -}: OverrideRuleTileProps) => { - const intl = useIntl(); - const [users, setUsers] = useState(null); - const [keywords, setKeywords] = useState(null); - const { data: languages } = useSWR('/api/v1/languages'); - const { data: genres } = useSWR('/api/v1/genres/movie'); - - useEffect(() => { - (async () => { - const keywords = await Promise.all( - rules - .map((rule) => rule.keywords?.split(',')) - .flat() - .filter((keywordId) => keywordId) - .map(async (keywordId) => { - const res = await fetch(`/api/v1/keyword/${keywordId}`); - if (!res.ok) throw new Error(); - const keyword: Keyword = await res.json(); - return keyword; - }) - ); - setKeywords(keywords); - const users = await Promise.all( - rules - .map((rule) => rule.users?.split(',')) - .flat() - .filter((userId) => userId) - .map(async (userId) => { - const res = await fetch(`/api/v1/user/${userId}`); - if (!res.ok) throw new Error(); - const user: User = await res.json(); - return user; - }) - ); - setUsers(users); - })(); - }, [rules]); - - return ( - <> - {rules - .filter( - (rule) => - (rule.radarrServiceId !== null && - rule.radarrServiceId === radarr?.id) || - (rule.sonarrServiceId !== null && - rule.sonarrServiceId === sonarr?.id) - ) - .map((rule) => ( -
  • -
    -
    - - {intl.formatMessage(messages.conditions)} - - {rule.users && ( -

    - - {intl.formatMessage(messages.users)} - -

    - {rule.users.split(',').map((userId) => { - return ( - - { - users?.find((user) => user.id === Number(userId)) - ?.displayName - } - - ); - })} -
    -

    - )} - {rule.genre && ( -

    - - {intl.formatMessage(messages.genre)} - -

    - {rule.genre.split(',').map((genreId) => ( - - {genres?.find((g) => g.id === Number(genreId))?.name} - - ))} -
    -

    - )} - {rule.language && ( -

    - - {intl.formatMessage(messages.language)} - -

    - {rule.language - .split('|') - .filter((languageId) => languageId !== 'server') - .map((languageId) => { - const language = languages?.find( - (language) => language.iso_639_1 === languageId - ); - if (!language) return null; - const languageName = - intl.formatDisplayName(language.iso_639_1, { - type: 'language', - fallback: 'none', - }) ?? language.english_name; - return {languageName}; - })} -
    -

    - )} - {rule.keywords && ( -

    - - {intl.formatMessage(messages.keywords)} - -

    - {rule.keywords.split(',').map((keywordId) => { - return ( - - { - keywords?.find( - (keyword) => keyword.id === Number(keywordId) - )?.name - } - - ); - })} -
    -

    - )} - - {intl.formatMessage(messages.settings)} - - {rule.profileId && ( -

    - - {intl.formatMessage(messages.qualityprofile)} - - { - testResponse.profiles.find( - (profile) => rule.profileId === profile.id - )?.name - } -

    - )} - {rule.rootFolder && ( -

    - - {intl.formatMessage(messages.rootfolder)} - - {rule.rootFolder} -

    - )} - {rule.tags && rule.tags.length > 0 && ( -

    - - {intl.formatMessage(messages.tags)} - -

    - {rule.tags.split(',').map((tag) => ( - - { - testResponse.tags?.find((t) => t.id === Number(tag)) - ?.label - } - - ))} -
    -

    - )} -
    -
    -
    -
    -
    - -
    -
    - -
    -
    -
    -
  • - ))} - - ); -}; - -export default OverrideRuleTile; diff --git a/src/components/Settings/OverrideRule/OverrideRuleTiles.tsx b/src/components/Settings/OverrideRule/OverrideRuleTiles.tsx new file mode 100644 index 00000000..a3b9aa37 --- /dev/null +++ b/src/components/Settings/OverrideRule/OverrideRuleTiles.tsx @@ -0,0 +1,318 @@ +import type { DVRTestResponse } from '@app/components/Settings/SettingsServices'; +import globalMessages from '@app/i18n/globalMessages'; +import defineMessages from '@app/utils/defineMessages'; +import { PencilIcon, TrashIcon } from '@heroicons/react/24/solid'; +import type { TmdbGenre } from '@server/api/themoviedb/interfaces'; +import type OverrideRule from '@server/entity/OverrideRule'; +import type { User } from '@server/entity/User'; +import type { + DVRSettings, + Language, + RadarrSettings, + SonarrSettings, +} from '@server/lib/settings'; +import type { Keyword } from '@server/models/common'; +import { useCallback, useEffect, useState } from 'react'; +import { useIntl } from 'react-intl'; +import useSWR from 'swr'; + +const messages = defineMessages('components.Settings.OverrideRuleTile', { + qualityprofile: 'Quality Profile', + rootfolder: 'Root Folder', + tags: 'Tags', + users: 'Users', + genre: 'Genre', + language: 'Language', + keywords: 'Keywords', + conditions: 'Conditions', + settings: 'Settings', +}); + +interface OverrideRuleTilesProps { + rules: OverrideRule[]; + setOverrideRuleModal: ({ + open, + rule, + }: { + open: boolean; + rule: OverrideRule | null; + }) => void; + revalidate: () => void; + radarrServices: RadarrSettings[]; + sonarrServices: SonarrSettings[]; +} + +const OverrideRuleTiles = ({ + rules, + setOverrideRuleModal, + revalidate, + radarrServices, + sonarrServices, +}: OverrideRuleTilesProps) => { + const intl = useIntl(); + const [users, setUsers] = useState(null); + const [keywords, setKeywords] = useState(null); + const { data: languages } = useSWR('/api/v1/languages'); + const { data: genres } = useSWR('/api/v1/genres/movie'); + const [testResponses, setTestResponses] = useState< + (DVRTestResponse & { type: string; id: number })[] + >([]); + + const getServiceInfos = useCallback(async () => { + const results: (DVRTestResponse & { type: string; id: number })[] = []; + const services: DVRSettings[] = [...radarrServices, ...sonarrServices]; + for (const service of services) { + const { hostname, port, apiKey, baseUrl, useSsl = false } = service; + try { + const res = await fetch( + `/api/v1/settings/${ + radarrServices.includes(service as RadarrSettings) + ? 'radarr' + : 'sonarr' + }/test`, + { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + }, + body: JSON.stringify({ + hostname, + apiKey, + port: Number(port), + baseUrl, + useSsl, + }), + } + ); + if (!res.ok) throw new Error(); + const data: DVRTestResponse = await res.json(); + results.push({ + type: radarrServices.includes(service as RadarrSettings) + ? 'radarr' + : 'sonarr', + id: service.id, + ...data, + }); + } catch { + results.push({ + type: radarrServices.includes(service as RadarrSettings) + ? 'radarr' + : 'sonarr', + id: service.id, + profiles: [], + rootFolders: [], + tags: [], + }); + } + } + setTestResponses(results); + }, [radarrServices, sonarrServices]); + + useEffect(() => { + getServiceInfos(); + }, [getServiceInfos]); + + useEffect(() => { + (async () => { + const keywords = await Promise.all( + rules + .map((rule) => rule.keywords?.split(',')) + .flat() + .filter((keywordId) => keywordId) + .map(async (keywordId) => { + const res = await fetch(`/api/v1/keyword/${keywordId}`); + if (!res.ok) throw new Error(); + const keyword: Keyword = await res.json(); + return keyword; + }) + ); + setKeywords(keywords); + const users = await Promise.all( + rules + .map((rule) => rule.users?.split(',')) + .flat() + .filter((userId) => userId) + .map(async (userId) => { + const res = await fetch(`/api/v1/user/${userId}`); + if (!res.ok) throw new Error(); + const user: User = await res.json(); + return user; + }) + ); + setUsers(users); + })(); + }, [rules]); + + return ( + <> + {rules.map((rule) => ( +
  • +
    +
    + + {intl.formatMessage(messages.conditions)} + + {rule.users && ( +

    + + {intl.formatMessage(messages.users)} + +

    + {rule.users.split(',').map((userId) => { + return ( + + { + users?.find((user) => user.id === Number(userId)) + ?.displayName + } + + ); + })} +
    +

    + )} + {rule.genre && ( +

    + + {intl.formatMessage(messages.genre)} + +

    + {rule.genre.split(',').map((genreId) => ( + + {genres?.find((g) => g.id === Number(genreId))?.name} + + ))} +
    +

    + )} + {rule.language && ( +

    + + {intl.formatMessage(messages.language)} + +

    + {rule.language + .split('|') + .filter((languageId) => languageId !== 'server') + .map((languageId) => { + const language = languages?.find( + (language) => language.iso_639_1 === languageId + ); + if (!language) return null; + const languageName = + intl.formatDisplayName(language.iso_639_1, { + type: 'language', + fallback: 'none', + }) ?? language.english_name; + return {languageName}; + })} +
    +

    + )} + {rule.keywords && ( +

    + + {intl.formatMessage(messages.keywords)} + +

    + {rule.keywords.split(',').map((keywordId) => { + return ( + + { + keywords?.find( + (keyword) => keyword.id === Number(keywordId) + )?.name + } + + ); + })} +
    +

    + )} + + {intl.formatMessage(messages.settings)} + + {rule.profileId && ( +

    + + {intl.formatMessage(messages.qualityprofile)} + + {testResponses + .find( + (r) => + (r.id === rule.radarrServiceId && + r.type === 'radarr') || + (r.id === rule.sonarrServiceId && r.type === 'sonarr') + ) + ?.profiles.find((profile) => rule.profileId === profile.id) + ?.name || rule.profileId} +

    + )} + {rule.rootFolder && ( +

    + + {intl.formatMessage(messages.rootfolder)} + + {rule.rootFolder} +

    + )} + {rule.tags && rule.tags.length > 0 && ( +

    + + {intl.formatMessage(messages.tags)} + +

    + {rule.tags.split(',').map((tag) => ( + + {testResponses + .find( + (r) => + (r.id === rule.radarrServiceId && + r.type === 'radarr') || + (r.id === rule.sonarrServiceId && + r.type === 'sonarr') + ) + ?.tags?.find((t) => t.id === Number(tag))?.label || + tag} + + ))} +
    +

    + )} +
    +
    +
    +
    +
    + +
    +
    + +
    +
    +
    +
  • + ))} + + ); +}; + +export default OverrideRuleTiles; diff --git a/src/components/Settings/RadarrModal/index.tsx b/src/components/Settings/RadarrModal/index.tsx index 5067c71b..fbeb2dec 100644 --- a/src/components/Settings/RadarrModal/index.tsx +++ b/src/components/Settings/RadarrModal/index.tsx @@ -1,24 +1,15 @@ -import Button from '@app/components/Common/Button'; import Modal from '@app/components/Common/Modal'; import SensitiveInput from '@app/components/Common/SensitiveInput'; -import OverrideRuleTile from '@app/components/Settings/OverrideRule/OverrideRuleTile'; -import type { - DVRTestResponse, - RadarrTestResponse, -} from '@app/components/Settings/SettingsServices'; +import type { RadarrTestResponse } from '@app/components/Settings/SettingsServices'; import globalMessages from '@app/i18n/globalMessages'; import defineMessages from '@app/utils/defineMessages'; import { Transition } from '@headlessui/react'; -import { PlusIcon } from '@heroicons/react/24/solid'; -import type OverrideRule from '@server/entity/OverrideRule'; -import type { OverrideRuleResultsResponse } from '@server/interfaces/api/overrideRuleInterfaces'; import type { RadarrSettings } from '@server/lib/settings'; import { Field, Formik } from 'formik'; import { useCallback, useEffect, useRef, useState } from 'react'; import { useIntl } from 'react-intl'; import Select from 'react-select'; import { useToasts } from 'react-toast-notifications'; -import useSWR from 'swr'; import * as Yup from 'yup'; type OptionType = { @@ -79,36 +70,16 @@ const messages = defineMessages('components.Settings.RadarrModal', { announced: 'Announced', inCinemas: 'In Cinemas', released: 'Released', - overrideRules: 'Override Rules', - addrule: 'New Override Rule', }); interface RadarrModalProps { radarr: RadarrSettings | null; onClose: () => void; onSave: () => void; - overrideRuleModal: { open: boolean; rule: OverrideRule | null }; - setOverrideRuleModal: ({ - open, - rule, - testResponse, - }: { - open: boolean; - rule: OverrideRule | null; - testResponse: DVRTestResponse; - }) => void; } -const RadarrModal = ({ - onClose, - radarr, - onSave, - overrideRuleModal, - setOverrideRuleModal, -}: RadarrModalProps) => { +const RadarrModal = ({ onClose, radarr, onSave }: RadarrModalProps) => { const intl = useIntl(); - const { data: rules, mutate: revalidate } = - useSWR('/api/v1/overrideRule'); const initialLoad = useRef(false); const { addToast } = useToasts(); const [isValidated, setIsValidated] = useState(radarr ? true : false); @@ -235,10 +206,6 @@ const RadarrModal = ({ } }, [radarr, testConnection]); - useEffect(() => { - revalidate(); - }, [overrideRuleModal, revalidate]); - return (
    @@ -773,42 +739,6 @@ const RadarrModal = ({
    - {radarr && ( - <> -

    - {intl.formatMessage(messages.overrideRules)} -

    -
      - {rules && ( - - )} -
    • -
      - -
      -
    • -
    - - )} ); }} diff --git a/src/components/Settings/SettingsServices.tsx b/src/components/Settings/SettingsServices.tsx index 5e3871fc..fc058c0b 100644 --- a/src/components/Settings/SettingsServices.tsx +++ b/src/components/Settings/SettingsServices.tsx @@ -7,6 +7,7 @@ import LoadingSpinner from '@app/components/Common/LoadingSpinner'; import Modal from '@app/components/Common/Modal'; import PageTitle from '@app/components/Common/PageTitle'; import OverrideRuleModal from '@app/components/Settings/OverrideRule/OverrideRuleModal'; +import OverrideRuleTiles from '@app/components/Settings/OverrideRule/OverrideRuleTiles'; import RadarrModal from '@app/components/Settings/RadarrModal'; import SonarrModal from '@app/components/Settings/SonarrModal'; import globalMessages from '@app/i18n/globalMessages'; @@ -14,6 +15,7 @@ import defineMessages from '@app/utils/defineMessages'; import { Transition } from '@headlessui/react'; import { PencilIcon, PlusIcon, TrashIcon } from '@heroicons/react/24/solid'; import type OverrideRule from '@server/entity/OverrideRule'; +import type { OverrideRuleResultsResponse } from '@server/interfaces/api/overrideRuleInterfaces'; import type { RadarrSettings, SonarrSettings } from '@server/lib/settings'; import { Fragment, useState } from 'react'; import { useIntl } from 'react-intl'; @@ -43,6 +45,10 @@ const messages = defineMessages('components.Settings', { mediaTypeMovie: 'movie', mediaTypeSeries: 'series', deleteServer: 'Delete {serverType} Server', + overrideRules: 'Override Rules', + overrideRulesDescription: + 'Override rules allow you to specify properties that will be replaced if a request matches the rule.', + addrule: 'New Override Rule', }); interface ServerInstanceProps { @@ -199,6 +205,8 @@ const SettingsServices = () => { error: sonarrError, mutate: revalidateSonarr, } = useSWR('/api/v1/settings/sonarr'); + const { data: rules, mutate: revalidate } = + useSWR('/api/v1/overrideRule'); const [editRadarrModal, setEditRadarrModal] = useState<{ open: boolean; radarr: RadarrSettings | null; @@ -225,11 +233,9 @@ const SettingsServices = () => { const [overrideRuleModal, setOverrideRuleModal] = useState<{ open: boolean; rule: OverrideRule | null; - testResponse: DVRTestResponse | null; }>({ open: false, rule: null, - testResponse: null, }); const deleteServer = async () => { @@ -265,21 +271,6 @@ const SettingsServices = () => { })}

    - {overrideRuleModal.open && overrideRuleModal.testResponse && ( - - setOverrideRuleModal({ - open: false, - rule: null, - testResponse: null, - }) - } - testResponse={overrideRuleModal.testResponse} - radarrId={editRadarrModal.radarr?.id} - sonarrId={editSonarrModal.sonarr?.id} - /> - )} {editRadarrModal.open && ( { mutate('/api/v1/settings/public'); setEditRadarrModal({ open: false, radarr: null }); }} - overrideRuleModal={overrideRuleModal} - setOverrideRuleModal={setOverrideRuleModal} /> )} {editSonarrModal.open && ( @@ -308,8 +297,6 @@ const SettingsServices = () => { mutate('/api/v1/settings/public'); setEditSonarrModal({ open: false, sonarr: null }); }} - overrideRuleModal={overrideRuleModal} - setOverrideRuleModal={setOverrideRuleModal} /> )} { )}
    +
    +

    + {intl.formatMessage(messages.overrideRules)} +

    +

    + {intl.formatMessage(messages.overrideRulesDescription, { + serverType: 'Sonarr', + })} +

    +
    +
    +
      + {rules && radarrData && sonarrData && ( + + )} +
    • +
      + +
      +
    • +
    +
    + {overrideRuleModal.open && radarrData && sonarrData && ( + { + setOverrideRuleModal({ + open: false, + rule: null, + }); + revalidate(); + }} + radarrServices={radarrData} + sonarrServices={sonarrData} + /> + )} ); }; diff --git a/src/components/Settings/SonarrModal/index.tsx b/src/components/Settings/SonarrModal/index.tsx index 888dcc54..fedea2a6 100644 --- a/src/components/Settings/SonarrModal/index.tsx +++ b/src/components/Settings/SonarrModal/index.tsx @@ -1,17 +1,9 @@ -import Button from '@app/components/Common/Button'; import Modal from '@app/components/Common/Modal'; import SensitiveInput from '@app/components/Common/SensitiveInput'; -import OverrideRuleTile from '@app/components/Settings/OverrideRule/OverrideRuleTile'; -import type { - DVRTestResponse, - SonarrTestResponse, -} from '@app/components/Settings/SettingsServices'; +import type { SonarrTestResponse } from '@app/components/Settings/SettingsServices'; import globalMessages from '@app/i18n/globalMessages'; import defineMessages from '@app/utils/defineMessages'; import { Transition } from '@headlessui/react'; -import { PlusIcon } from '@heroicons/react/24/solid'; -import type OverrideRule from '@server/entity/OverrideRule'; -import type { OverrideRuleResultsResponse } from '@server/interfaces/api/overrideRuleInterfaces'; import type { SonarrSettings } from '@server/lib/settings'; import { Field, Formik } from 'formik'; import { useCallback, useEffect, useRef, useState } from 'react'; @@ -19,7 +11,6 @@ import { useIntl } from 'react-intl'; import type { OnChangeValue } from 'react-select'; import Select from 'react-select'; import { useToasts } from 'react-toast-notifications'; -import useSWR from 'swr'; import * as Yup from 'yup'; type OptionType = { @@ -85,36 +76,16 @@ const messages = defineMessages('components.Settings.SonarrModal', { animeTags: 'Anime Tags', notagoptions: 'No tags.', selecttags: 'Select tags', - overrideRules: 'Override Rules', - addrule: 'New Override Rule', }); interface SonarrModalProps { sonarr: SonarrSettings | null; onClose: () => void; onSave: () => void; - overrideRuleModal: { open: boolean; rule: OverrideRule | null }; - setOverrideRuleModal: ({ - open, - rule, - testResponse, - }: { - open: boolean; - rule: OverrideRule | null; - testResponse: DVRTestResponse; - }) => void; } -const SonarrModal = ({ - onClose, - sonarr, - onSave, - overrideRuleModal, - setOverrideRuleModal, -}: SonarrModalProps) => { +const SonarrModal = ({ onClose, sonarr, onSave }: SonarrModalProps) => { const intl = useIntl(); - const { data: rules, mutate: revalidate } = - useSWR('/api/v1/overrideRule'); const initialLoad = useRef(false); const { addToast } = useToasts(); const [isValidated, setIsValidated] = useState(sonarr ? true : false); @@ -244,10 +215,6 @@ const SonarrModal = ({ } }, [sonarr, testConnection]); - useEffect(() => { - revalidate(); - }, [overrideRuleModal, revalidate]); - return (
    @@ -1070,42 +1036,6 @@ const SonarrModal = ({
    - {sonarr && ( - <> -

    - {intl.formatMessage(messages.overrideRules)} -

    -
      - {rules && ( - - )} -
    • -
      - -
      -
    • -
    - - )} ); }} diff --git a/src/i18n/locale/en.json b/src/i18n/locale/en.json index 2b0b3681..1a18a3a4 100644 --- a/src/i18n/locale/en.json +++ b/src/i18n/locale/en.json @@ -753,7 +753,10 @@ "components.Settings.OverrideRuleModal.ruleUpdated": "Override rule updated successfully!", "components.Settings.OverrideRuleModal.selectQualityProfile": "Select quality profile", "components.Settings.OverrideRuleModal.selectRootFolder": "Select root folder", + "components.Settings.OverrideRuleModal.selectService": "Select service", "components.Settings.OverrideRuleModal.selecttags": "Select tags", + "components.Settings.OverrideRuleModal.service": "Service", + "components.Settings.OverrideRuleModal.serviceDescription": "Apply this rule to the selected service.", "components.Settings.OverrideRuleModal.settings": "Settings", "components.Settings.OverrideRuleModal.settingsDescription": "Specifies which settings will be changed when the above conditions are met.", "components.Settings.OverrideRuleModal.tags": "Tags", @@ -768,7 +771,6 @@ "components.Settings.OverrideRuleTile.tags": "Tags", "components.Settings.OverrideRuleTile.users": "Users", "components.Settings.RadarrModal.add": "Add Server", - "components.Settings.RadarrModal.addrule": "New Override Rule", "components.Settings.RadarrModal.announced": "Announced", "components.Settings.RadarrModal.apiKey": "API Key", "components.Settings.RadarrModal.baseUrl": "URL Base", @@ -787,7 +789,6 @@ "components.Settings.RadarrModal.loadingrootfolders": "Loading root folders…", "components.Settings.RadarrModal.minimumAvailability": "Minimum Availability", "components.Settings.RadarrModal.notagoptions": "No tags.", - "components.Settings.RadarrModal.overrideRules": "Override Rules", "components.Settings.RadarrModal.port": "Port", "components.Settings.RadarrModal.qualityprofile": "Quality Profile", "components.Settings.RadarrModal.released": "Released", @@ -976,7 +977,6 @@ "components.Settings.SettingsUsers.userSettingsDescription": "Configure global and default user settings.", "components.Settings.SettingsUsers.users": "Users", "components.Settings.SonarrModal.add": "Add Server", - "components.Settings.SonarrModal.addrule": "New Override Rule", "components.Settings.SonarrModal.animeSeriesType": "Anime Series Type", "components.Settings.SonarrModal.animeTags": "Anime Tags", "components.Settings.SonarrModal.animelanguageprofile": "Anime Language Profile", @@ -999,7 +999,6 @@ "components.Settings.SonarrModal.loadingprofiles": "Loading quality profiles…", "components.Settings.SonarrModal.loadingrootfolders": "Loading root folders…", "components.Settings.SonarrModal.notagoptions": "No tags.", - "components.Settings.SonarrModal.overrideRules": "Override Rules", "components.Settings.SonarrModal.port": "Port", "components.Settings.SonarrModal.qualityprofile": "Quality Profile", "components.Settings.SonarrModal.rootfolder": "Root Folder", @@ -1036,6 +1035,7 @@ "components.Settings.activeProfile": "Active Profile", "components.Settings.addradarr": "Add Radarr Server", "components.Settings.address": "Address", + "components.Settings.addrule": "New Override Rule", "components.Settings.addsonarr": "Add Sonarr Server", "components.Settings.advancedTooltip": "Incorrectly configuring this setting may result in broken functionality", "components.Settings.apiKey": "API key", @@ -1089,6 +1089,8 @@ "components.Settings.notifications": "Notifications", "components.Settings.notificationsettings": "Notification Settings", "components.Settings.notrunning": "Not Running", + "components.Settings.overrideRules": "Override Rules", + "components.Settings.overrideRulesDescription": "Override rules allow you to specify properties that will be replaced if a request matches the rule.", "components.Settings.plex": "Plex", "components.Settings.plexlibraries": "Plex Libraries", "components.Settings.plexlibrariesDescription": "The libraries Jellyseerr scans for titles. Set up and save your Plex connection settings, then click the button below if no libraries are listed.",