mirror of
https://github.com/fallenbagel/jellyseerr.git
synced 2025-12-24 02:39:18 -05:00
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
This commit is contained in:
@@ -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<Language[]>('/api/v1/languages');
|
||||
@@ -96,6 +98,7 @@ const LanguageSelector = ({
|
||||
<Select<OptionType, true>
|
||||
options={options}
|
||||
isMulti
|
||||
isDisabled={isDisabled}
|
||||
className="react-select-container"
|
||||
classNamePrefix="react-select"
|
||||
value={
|
||||
|
||||
@@ -52,18 +52,21 @@ type SingleVal = {
|
||||
type BaseSelectorMultiProps = {
|
||||
defaultValue?: string;
|
||||
isMulti: true;
|
||||
isDisabled?: boolean;
|
||||
onChange: (value: MultiValue<SingleVal> | null) => void;
|
||||
};
|
||||
|
||||
type BaseSelectorSingleProps = {
|
||||
defaultValue?: string;
|
||||
isMulti?: false;
|
||||
isDisabled?: boolean;
|
||||
onChange: (value: SingleValue<SingleVal> | 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) => {
|
||||
|
||||
@@ -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<DVRTestResponse>({
|
||||
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 (
|
||||
<Transition
|
||||
@@ -79,6 +155,8 @@ const OverrideRuleModal = ({
|
||||
>
|
||||
<Formik
|
||||
initialValues={{
|
||||
radarrServiceId: rule?.radarrServiceId,
|
||||
sonarrServiceId: rule?.sonarrServiceId,
|
||||
users: rule?.users,
|
||||
genre: rule?.genre,
|
||||
language: rule?.language,
|
||||
@@ -97,8 +175,8 @@ const OverrideRuleModal = ({
|
||||
profileId: Number(values.profileId) || null,
|
||||
rootFolder: values.rootFolder || null,
|
||||
tags: values.tags || null,
|
||||
radarrServiceId: radarrId,
|
||||
sonarrServiceId: sonarrId,
|
||||
radarrServiceId: values.radarrServiceId,
|
||||
sonarrServiceId: values.sonarrServiceId,
|
||||
};
|
||||
if (!rule) {
|
||||
const res = await fetch('/api/v1/overrideRule', {
|
||||
@@ -170,6 +248,75 @@ const OverrideRuleModal = ({
|
||||
}
|
||||
>
|
||||
<div className="mb-6">
|
||||
<h3 className="text-lg font-bold leading-8 text-gray-100">
|
||||
{intl.formatMessage(messages.service)}
|
||||
</h3>
|
||||
<p className="description">
|
||||
{intl.formatMessage(messages.serviceDescription)}
|
||||
</p>
|
||||
<div className="form-row">
|
||||
<label htmlFor="service" className="text-label">
|
||||
{intl.formatMessage(messages.service)}
|
||||
</label>
|
||||
<div className="form-input-area">
|
||||
<div className="form-input-field">
|
||||
<select
|
||||
id="service"
|
||||
name="service"
|
||||
defaultValue={
|
||||
values.radarrServiceId !== null
|
||||
? `radarr-${values.radarrServiceId}`
|
||||
: `sonarr-${values.sonarrServiceId}`
|
||||
}
|
||||
onChange={(e) => {
|
||||
const id = Number(e.target.value.split('-')[1]);
|
||||
if (e.target.value.startsWith('radarr-')) {
|
||||
setFieldValue('radarrServiceId', id);
|
||||
setFieldValue('sonarrServiceId', null);
|
||||
if (radarrServices[id]) {
|
||||
getServiceInfos(radarrServices[id]);
|
||||
}
|
||||
} else if (e.target.value.startsWith('sonarr-')) {
|
||||
setFieldValue('radarrServiceId', null);
|
||||
setFieldValue('sonarrServiceId', id);
|
||||
if (sonarrServices[id]) {
|
||||
getServiceInfos(sonarrServices[id]);
|
||||
}
|
||||
} else {
|
||||
setFieldValue('radarrServiceId', null);
|
||||
setFieldValue('sonarrServiceId', null);
|
||||
setIsValidated(false);
|
||||
}
|
||||
}}
|
||||
>
|
||||
<option value="">
|
||||
{intl.formatMessage(messages.selectService)}
|
||||
</option>
|
||||
{radarrServices.map((radarr) => (
|
||||
<option
|
||||
key={`radarr-${radarr.id}`}
|
||||
value={`radarr-${radarr.id}`}
|
||||
>
|
||||
{radarr.name}
|
||||
</option>
|
||||
))}
|
||||
{sonarrServices.map((sonarr) => (
|
||||
<option
|
||||
key={`sonarr-${sonarr.id}`}
|
||||
value={`sonarr-${sonarr.id}`}
|
||||
>
|
||||
{sonarr.name}
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
</div>
|
||||
{errors.rootFolder &&
|
||||
touched.rootFolder &&
|
||||
typeof errors.rootFolder === 'string' && (
|
||||
<div className="error">{errors.rootFolder}</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
<h3 className="text-lg font-bold leading-8 text-gray-100">
|
||||
{intl.formatMessage(messages.conditions)}
|
||||
</h3>
|
||||
@@ -184,6 +331,7 @@ const OverrideRuleModal = ({
|
||||
<div className="form-input-field">
|
||||
<UserSelector
|
||||
defaultValue={values.users}
|
||||
isDisabled={!isValidated || isTesting}
|
||||
isMulti
|
||||
onChange={(users) => {
|
||||
setFieldValue(
|
||||
@@ -207,9 +355,10 @@ const OverrideRuleModal = ({
|
||||
<div className="form-input-area">
|
||||
<div className="form-input-field">
|
||||
<GenreSelector
|
||||
type={radarrId ? 'movie' : 'tv'}
|
||||
type={values.radarrServiceId ? 'movie' : 'tv'}
|
||||
defaultValue={values.genre}
|
||||
isMulti
|
||||
isDisabled={!isValidated || isTesting}
|
||||
onChange={(genres) => {
|
||||
setFieldValue(
|
||||
'genre',
|
||||
@@ -237,6 +386,7 @@ const OverrideRuleModal = ({
|
||||
setFieldValue={(_key, value) => {
|
||||
setFieldValue('language', value);
|
||||
}}
|
||||
isDisabled={!isValidated || isTesting}
|
||||
/>
|
||||
</div>
|
||||
{errors.language &&
|
||||
@@ -255,6 +405,7 @@ const OverrideRuleModal = ({
|
||||
<KeywordSelector
|
||||
defaultValue={values.keywords}
|
||||
isMulti
|
||||
isDisabled={!isValidated || isTesting}
|
||||
onChange={(value) => {
|
||||
setFieldValue(
|
||||
'keywords',
|
||||
@@ -282,7 +433,12 @@ const OverrideRuleModal = ({
|
||||
</label>
|
||||
<div className="form-input-area">
|
||||
<div className="form-input-field">
|
||||
<Field as="select" id="rootFolderRule" name="rootFolder">
|
||||
<Field
|
||||
as="select"
|
||||
id="rootFolderRule"
|
||||
name="rootFolder"
|
||||
disabled={!isValidated || isTesting}
|
||||
>
|
||||
<option value="">
|
||||
{intl.formatMessage(messages.selectRootFolder)}
|
||||
</option>
|
||||
@@ -310,7 +466,12 @@ const OverrideRuleModal = ({
|
||||
</label>
|
||||
<div className="form-input-area">
|
||||
<div className="form-input-field">
|
||||
<Field as="select" id="profileIdRule" name="profileId">
|
||||
<Field
|
||||
as="select"
|
||||
id="profileIdRule"
|
||||
name="profileId"
|
||||
disabled={!isValidated || isTesting}
|
||||
>
|
||||
<option value="">
|
||||
{intl.formatMessage(messages.selectQualityProfile)}
|
||||
</option>
|
||||
@@ -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"
|
||||
|
||||
@@ -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<User[] | null>(null);
|
||||
const [keywords, setKeywords] = useState<Keyword[] | null>(null);
|
||||
const { data: languages } = useSWR<Language[]>('/api/v1/languages');
|
||||
const { data: genres } = useSWR<TmdbGenre[]>('/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) => (
|
||||
<li className="flex h-full flex-col rounded-lg bg-gray-800 text-left shadow ring-1 ring-gray-500">
|
||||
<div className="flex w-full flex-1 items-center justify-between space-x-6 p-6">
|
||||
<div className="flex-1 truncate">
|
||||
<span className="text-lg">
|
||||
{intl.formatMessage(messages.conditions)}
|
||||
</span>
|
||||
{rule.users && (
|
||||
<p className="truncate text-sm leading-5 text-gray-300">
|
||||
<span className="mr-2 font-bold">
|
||||
{intl.formatMessage(messages.users)}
|
||||
</span>
|
||||
<div className="inline-flex gap-2">
|
||||
{rule.users.split(',').map((userId) => {
|
||||
return (
|
||||
<span>
|
||||
{
|
||||
users?.find((user) => user.id === Number(userId))
|
||||
?.displayName
|
||||
}
|
||||
</span>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</p>
|
||||
)}
|
||||
{rule.genre && (
|
||||
<p className="truncate text-sm leading-5 text-gray-300">
|
||||
<span className="mr-2 font-bold">
|
||||
{intl.formatMessage(messages.genre)}
|
||||
</span>
|
||||
<div className="inline-flex gap-2">
|
||||
{rule.genre.split(',').map((genreId) => (
|
||||
<span>
|
||||
{genres?.find((g) => g.id === Number(genreId))?.name}
|
||||
</span>
|
||||
))}
|
||||
</div>
|
||||
</p>
|
||||
)}
|
||||
{rule.language && (
|
||||
<p className="truncate text-sm leading-5 text-gray-300">
|
||||
<span className="mr-2 font-bold">
|
||||
{intl.formatMessage(messages.language)}
|
||||
</span>
|
||||
<div className="inline-flex gap-2">
|
||||
{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 <span>{languageName}</span>;
|
||||
})}
|
||||
</div>
|
||||
</p>
|
||||
)}
|
||||
{rule.keywords && (
|
||||
<p className="truncate text-sm leading-5 text-gray-300">
|
||||
<span className="mr-2 font-bold">
|
||||
{intl.formatMessage(messages.keywords)}
|
||||
</span>
|
||||
<div className="inline-flex gap-2">
|
||||
{rule.keywords.split(',').map((keywordId) => {
|
||||
return (
|
||||
<span>
|
||||
{
|
||||
keywords?.find(
|
||||
(keyword) => keyword.id === Number(keywordId)
|
||||
)?.name
|
||||
}
|
||||
</span>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</p>
|
||||
)}
|
||||
<span className="text-lg">
|
||||
{intl.formatMessage(messages.settings)}
|
||||
</span>
|
||||
{rule.profileId && (
|
||||
<p className="runcate text-sm leading-5 text-gray-300">
|
||||
<span className="mr-2 font-bold">
|
||||
{intl.formatMessage(messages.qualityprofile)}
|
||||
</span>
|
||||
{
|
||||
testResponse.profiles.find(
|
||||
(profile) => rule.profileId === profile.id
|
||||
)?.name
|
||||
}
|
||||
</p>
|
||||
)}
|
||||
{rule.rootFolder && (
|
||||
<p className="truncate text-sm leading-5 text-gray-300">
|
||||
<span className="mr-2 font-bold">
|
||||
{intl.formatMessage(messages.rootfolder)}
|
||||
</span>
|
||||
{rule.rootFolder}
|
||||
</p>
|
||||
)}
|
||||
{rule.tags && rule.tags.length > 0 && (
|
||||
<p className="truncate text-sm leading-5 text-gray-300">
|
||||
<span className="mr-2 font-bold">
|
||||
{intl.formatMessage(messages.tags)}
|
||||
</span>
|
||||
<div className="inline-flex gap-2">
|
||||
{rule.tags.split(',').map((tag) => (
|
||||
<span>
|
||||
{
|
||||
testResponse.tags?.find((t) => t.id === Number(tag))
|
||||
?.label
|
||||
}
|
||||
</span>
|
||||
))}
|
||||
</div>
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
<div className="border-t border-gray-500">
|
||||
<div className="-mt-px flex">
|
||||
<div className="flex w-0 flex-1 border-r border-gray-500">
|
||||
<button
|
||||
onClick={() =>
|
||||
setOverrideRuleModal({ open: true, rule, testResponse })
|
||||
}
|
||||
className="focus:ring-blue relative -mr-px inline-flex w-0 flex-1 items-center justify-center rounded-bl-lg border border-transparent py-4 text-sm font-medium leading-5 text-gray-200 transition duration-150 ease-in-out hover:text-white focus:z-10 focus:border-gray-500 focus:outline-none"
|
||||
>
|
||||
<PencilIcon className="mr-2 h-5 w-5" />
|
||||
<span>{intl.formatMessage(globalMessages.edit)}</span>
|
||||
</button>
|
||||
</div>
|
||||
<div className="-ml-px flex w-0 flex-1">
|
||||
<button
|
||||
onClick={async () => {
|
||||
const res = await fetch(
|
||||
`/api/v1/overrideRule/${rule.id}`,
|
||||
{
|
||||
method: 'DELETE',
|
||||
}
|
||||
);
|
||||
if (!res.ok) throw new Error();
|
||||
revalidate();
|
||||
}}
|
||||
className="focus:ring-blue relative inline-flex w-0 flex-1 items-center justify-center rounded-br-lg border border-transparent py-4 text-sm font-medium leading-5 text-gray-200 transition duration-150 ease-in-out hover:text-white focus:z-10 focus:border-gray-500 focus:outline-none"
|
||||
>
|
||||
<TrashIcon className="mr-2 h-5 w-5" />
|
||||
<span>{intl.formatMessage(globalMessages.delete)}</span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</li>
|
||||
))}
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default OverrideRuleTile;
|
||||
318
src/components/Settings/OverrideRule/OverrideRuleTiles.tsx
Normal file
318
src/components/Settings/OverrideRule/OverrideRuleTiles.tsx
Normal file
@@ -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<User[] | null>(null);
|
||||
const [keywords, setKeywords] = useState<Keyword[] | null>(null);
|
||||
const { data: languages } = useSWR<Language[]>('/api/v1/languages');
|
||||
const { data: genres } = useSWR<TmdbGenre[]>('/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) => (
|
||||
<li className="flex h-full flex-col rounded-lg bg-gray-800 text-left shadow ring-1 ring-gray-500">
|
||||
<div className="flex w-full flex-1 items-center justify-between space-x-6 p-6">
|
||||
<div className="flex-1 truncate">
|
||||
<span className="text-lg">
|
||||
{intl.formatMessage(messages.conditions)}
|
||||
</span>
|
||||
{rule.users && (
|
||||
<p className="truncate text-sm leading-5 text-gray-300">
|
||||
<span className="mr-2 font-bold">
|
||||
{intl.formatMessage(messages.users)}
|
||||
</span>
|
||||
<div className="inline-flex gap-2">
|
||||
{rule.users.split(',').map((userId) => {
|
||||
return (
|
||||
<span>
|
||||
{
|
||||
users?.find((user) => user.id === Number(userId))
|
||||
?.displayName
|
||||
}
|
||||
</span>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</p>
|
||||
)}
|
||||
{rule.genre && (
|
||||
<p className="truncate text-sm leading-5 text-gray-300">
|
||||
<span className="mr-2 font-bold">
|
||||
{intl.formatMessage(messages.genre)}
|
||||
</span>
|
||||
<div className="inline-flex gap-2">
|
||||
{rule.genre.split(',').map((genreId) => (
|
||||
<span>
|
||||
{genres?.find((g) => g.id === Number(genreId))?.name}
|
||||
</span>
|
||||
))}
|
||||
</div>
|
||||
</p>
|
||||
)}
|
||||
{rule.language && (
|
||||
<p className="truncate text-sm leading-5 text-gray-300">
|
||||
<span className="mr-2 font-bold">
|
||||
{intl.formatMessage(messages.language)}
|
||||
</span>
|
||||
<div className="inline-flex gap-2">
|
||||
{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 <span>{languageName}</span>;
|
||||
})}
|
||||
</div>
|
||||
</p>
|
||||
)}
|
||||
{rule.keywords && (
|
||||
<p className="truncate text-sm leading-5 text-gray-300">
|
||||
<span className="mr-2 font-bold">
|
||||
{intl.formatMessage(messages.keywords)}
|
||||
</span>
|
||||
<div className="inline-flex gap-2">
|
||||
{rule.keywords.split(',').map((keywordId) => {
|
||||
return (
|
||||
<span>
|
||||
{
|
||||
keywords?.find(
|
||||
(keyword) => keyword.id === Number(keywordId)
|
||||
)?.name
|
||||
}
|
||||
</span>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</p>
|
||||
)}
|
||||
<span className="text-lg">
|
||||
{intl.formatMessage(messages.settings)}
|
||||
</span>
|
||||
{rule.profileId && (
|
||||
<p className="runcate text-sm leading-5 text-gray-300">
|
||||
<span className="mr-2 font-bold">
|
||||
{intl.formatMessage(messages.qualityprofile)}
|
||||
</span>
|
||||
{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}
|
||||
</p>
|
||||
)}
|
||||
{rule.rootFolder && (
|
||||
<p className="truncate text-sm leading-5 text-gray-300">
|
||||
<span className="mr-2 font-bold">
|
||||
{intl.formatMessage(messages.rootfolder)}
|
||||
</span>
|
||||
{rule.rootFolder}
|
||||
</p>
|
||||
)}
|
||||
{rule.tags && rule.tags.length > 0 && (
|
||||
<p className="truncate text-sm leading-5 text-gray-300">
|
||||
<span className="mr-2 font-bold">
|
||||
{intl.formatMessage(messages.tags)}
|
||||
</span>
|
||||
<div className="inline-flex gap-2">
|
||||
{rule.tags.split(',').map((tag) => (
|
||||
<span>
|
||||
{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}
|
||||
</span>
|
||||
))}
|
||||
</div>
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
<div className="border-t border-gray-500">
|
||||
<div className="-mt-px flex">
|
||||
<div className="flex w-0 flex-1 border-r border-gray-500">
|
||||
<button
|
||||
onClick={() => setOverrideRuleModal({ open: true, rule })}
|
||||
className="focus:ring-blue relative -mr-px inline-flex w-0 flex-1 items-center justify-center rounded-bl-lg border border-transparent py-4 text-sm font-medium leading-5 text-gray-200 transition duration-150 ease-in-out hover:text-white focus:z-10 focus:border-gray-500 focus:outline-none"
|
||||
>
|
||||
<PencilIcon className="mr-2 h-5 w-5" />
|
||||
<span>{intl.formatMessage(globalMessages.edit)}</span>
|
||||
</button>
|
||||
</div>
|
||||
<div className="-ml-px flex w-0 flex-1">
|
||||
<button
|
||||
onClick={async () => {
|
||||
const res = await fetch(`/api/v1/overrideRule/${rule.id}`, {
|
||||
method: 'DELETE',
|
||||
});
|
||||
if (!res.ok) throw new Error();
|
||||
revalidate();
|
||||
}}
|
||||
className="focus:ring-blue relative inline-flex w-0 flex-1 items-center justify-center rounded-br-lg border border-transparent py-4 text-sm font-medium leading-5 text-gray-200 transition duration-150 ease-in-out hover:text-white focus:z-10 focus:border-gray-500 focus:outline-none"
|
||||
>
|
||||
<TrashIcon className="mr-2 h-5 w-5" />
|
||||
<span>{intl.formatMessage(globalMessages.delete)}</span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</li>
|
||||
))}
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default OverrideRuleTiles;
|
||||
@@ -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<OverrideRuleResultsResponse>('/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 (
|
||||
<Transition
|
||||
as="div"
|
||||
@@ -382,7 +349,6 @@ const RadarrModal = ({
|
||||
values.is4k ? messages.edit4kradarr : messages.editradarr
|
||||
)
|
||||
}
|
||||
backgroundClickable={!overrideRuleModal.open}
|
||||
>
|
||||
<div className="mb-6">
|
||||
<div className="form-row">
|
||||
@@ -773,42 +739,6 @@ const RadarrModal = ({
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{radarr && (
|
||||
<>
|
||||
<h3 className="mb-4 text-xl font-bold leading-8 text-gray-100">
|
||||
{intl.formatMessage(messages.overrideRules)}
|
||||
</h3>
|
||||
<ul className="grid gap-x-4 gap-y-8 sm:grid-cols-3 sm:gap-x-6 sm:gap-y-6 lg:grid-cols-2">
|
||||
{rules && (
|
||||
<OverrideRuleTile
|
||||
rules={rules}
|
||||
setOverrideRuleModal={setOverrideRuleModal}
|
||||
testResponse={testResponse}
|
||||
radarr={radarr}
|
||||
revalidate={revalidate}
|
||||
/>
|
||||
)}
|
||||
<li className="min-h-[8rem] rounded-lg border-2 border-dashed border-gray-400 shadow sm:min-h-[11rem]">
|
||||
<div className="flex h-full w-full items-center justify-center">
|
||||
<Button
|
||||
buttonType="ghost"
|
||||
onClick={() =>
|
||||
setOverrideRuleModal({
|
||||
open: true,
|
||||
rule: null,
|
||||
testResponse,
|
||||
})
|
||||
}
|
||||
disabled={!isValidated}
|
||||
>
|
||||
<PlusIcon />
|
||||
<span>{intl.formatMessage(messages.addrule)}</span>
|
||||
</Button>
|
||||
</div>
|
||||
</li>
|
||||
</ul>
|
||||
</>
|
||||
)}
|
||||
</Modal>
|
||||
);
|
||||
}}
|
||||
|
||||
@@ -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<SonarrSettings[]>('/api/v1/settings/sonarr');
|
||||
const { data: rules, mutate: revalidate } =
|
||||
useSWR<OverrideRuleResultsResponse>('/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 = () => {
|
||||
})}
|
||||
</p>
|
||||
</div>
|
||||
{overrideRuleModal.open && overrideRuleModal.testResponse && (
|
||||
<OverrideRuleModal
|
||||
rule={overrideRuleModal.rule}
|
||||
onClose={() =>
|
||||
setOverrideRuleModal({
|
||||
open: false,
|
||||
rule: null,
|
||||
testResponse: null,
|
||||
})
|
||||
}
|
||||
testResponse={overrideRuleModal.testResponse}
|
||||
radarrId={editRadarrModal.radarr?.id}
|
||||
sonarrId={editSonarrModal.sonarr?.id}
|
||||
/>
|
||||
)}
|
||||
{editRadarrModal.open && (
|
||||
<RadarrModal
|
||||
radarr={editRadarrModal.radarr}
|
||||
@@ -292,8 +283,6 @@ const SettingsServices = () => {
|
||||
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}
|
||||
/>
|
||||
)}
|
||||
<Transition
|
||||
@@ -507,6 +494,59 @@ const SettingsServices = () => {
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
<div className="mt-10 mb-6">
|
||||
<h3 className="heading">
|
||||
{intl.formatMessage(messages.overrideRules)}
|
||||
</h3>
|
||||
<p className="description">
|
||||
{intl.formatMessage(messages.overrideRulesDescription, {
|
||||
serverType: 'Sonarr',
|
||||
})}
|
||||
</p>
|
||||
</div>
|
||||
<div className="section">
|
||||
<ul className="grid max-w-6xl grid-cols-1 gap-6 lg:grid-cols-2 xl:grid-cols-3">
|
||||
{rules && radarrData && sonarrData && (
|
||||
<OverrideRuleTiles
|
||||
rules={rules}
|
||||
radarrServices={radarrData}
|
||||
sonarrServices={sonarrData}
|
||||
setOverrideRuleModal={setOverrideRuleModal}
|
||||
revalidate={revalidate}
|
||||
/>
|
||||
)}
|
||||
<li className="min-h-[8rem] rounded-lg border-2 border-dashed border-gray-400 shadow sm:min-h-[11rem]">
|
||||
<div className="flex h-full w-full items-center justify-center">
|
||||
<Button
|
||||
buttonType="ghost"
|
||||
onClick={() =>
|
||||
setOverrideRuleModal({
|
||||
open: true,
|
||||
rule: null,
|
||||
})
|
||||
}
|
||||
>
|
||||
<PlusIcon />
|
||||
<span>{intl.formatMessage(messages.addrule)}</span>
|
||||
</Button>
|
||||
</div>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
{overrideRuleModal.open && radarrData && sonarrData && (
|
||||
<OverrideRuleModal
|
||||
rule={overrideRuleModal.rule}
|
||||
onClose={() => {
|
||||
setOverrideRuleModal({
|
||||
open: false,
|
||||
rule: null,
|
||||
});
|
||||
revalidate();
|
||||
}}
|
||||
radarrServices={radarrData}
|
||||
sonarrServices={sonarrData}
|
||||
/>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -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<OverrideRuleResultsResponse>('/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 (
|
||||
<Transition
|
||||
as="div"
|
||||
@@ -415,7 +382,6 @@ const SonarrModal = ({
|
||||
values.is4k ? messages.edit4ksonarr : messages.editsonarr
|
||||
)
|
||||
}
|
||||
backgroundClickable={!overrideRuleModal.open}
|
||||
>
|
||||
<div className="mb-6">
|
||||
<div className="form-row">
|
||||
@@ -1070,42 +1036,6 @@ const SonarrModal = ({
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{sonarr && (
|
||||
<>
|
||||
<h3 className="mb-4 text-xl font-bold leading-8 text-gray-100">
|
||||
{intl.formatMessage(messages.overrideRules)}
|
||||
</h3>
|
||||
<ul className="grid gap-x-4 gap-y-8 sm:grid-cols-3 sm:gap-x-6 sm:gap-y-6 lg:grid-cols-2">
|
||||
{rules && (
|
||||
<OverrideRuleTile
|
||||
rules={rules}
|
||||
setOverrideRuleModal={setOverrideRuleModal}
|
||||
testResponse={testResponse}
|
||||
sonarr={sonarr}
|
||||
revalidate={revalidate}
|
||||
/>
|
||||
)}
|
||||
<li className="min-h-[8rem] rounded-lg border-2 border-dashed border-gray-400 shadow sm:min-h-[11rem]">
|
||||
<div className="flex h-full w-full items-center justify-center">
|
||||
<Button
|
||||
buttonType="ghost"
|
||||
onClick={() =>
|
||||
setOverrideRuleModal({
|
||||
open: true,
|
||||
rule: null,
|
||||
testResponse,
|
||||
})
|
||||
}
|
||||
disabled={!isValidated}
|
||||
>
|
||||
<PlusIcon />
|
||||
<span>{intl.formatMessage(messages.addrule)}</span>
|
||||
</Button>
|
||||
</div>
|
||||
</li>
|
||||
</ul>
|
||||
</>
|
||||
)}
|
||||
</Modal>
|
||||
);
|
||||
}}
|
||||
|
||||
@@ -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.",
|
||||
|
||||
Reference in New Issue
Block a user