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.",