mirror of
https://github.com/Sonarr/Sonarr
synced 2026-05-08 13:01:10 +02:00
Merge 0f78e0c659 into bf5d48c76a
This commit is contained in:
commit
50b15dd483
20 changed files with 706 additions and 659 deletions
|
|
@ -5,7 +5,6 @@ import AppSectionState, {
|
|||
AppSectionSaveState,
|
||||
AppSectionSchemaState,
|
||||
} from 'App/State/AppSectionState';
|
||||
import AutoTagging, { AutoTaggingSpecification } from 'typings/AutoTagging';
|
||||
import CustomFormat from 'typings/CustomFormat';
|
||||
import CustomFormatSpecification from 'typings/CustomFormatSpecification';
|
||||
import DelayProfile from 'typings/DelayProfile';
|
||||
|
|
@ -18,17 +17,6 @@ type Presets<T> = T & {
|
|||
presets: T[];
|
||||
};
|
||||
|
||||
export interface AutoTaggingAppState
|
||||
extends AppSectionState<AutoTagging>,
|
||||
AppSectionDeleteState,
|
||||
AppSectionSaveState {}
|
||||
|
||||
export interface AutoTaggingSpecificationAppState
|
||||
extends AppSectionState<AutoTaggingSpecification>,
|
||||
AppSectionDeleteState,
|
||||
AppSectionSaveState,
|
||||
AppSectionSchemaState<AutoTaggingSpecification> {}
|
||||
|
||||
export interface DelayProfileAppState
|
||||
extends AppSectionListState<DelayProfile>,
|
||||
AppSectionDeleteState,
|
||||
|
|
@ -70,8 +58,6 @@ export interface ImportListOptionsSettingsAppState
|
|||
AppSectionSaveState {}
|
||||
|
||||
interface SettingsAppState {
|
||||
autoTaggings: AutoTaggingAppState;
|
||||
autoTaggingSpecifications: AutoTaggingSpecificationAppState;
|
||||
customFormats: CustomFormatAppState;
|
||||
customFormatSpecifications: CustomFormatSpecificationAppState;
|
||||
delayProfiles: DelayProfileAppState;
|
||||
|
|
|
|||
|
|
@ -7,9 +7,12 @@ import TagList from 'Components/TagList';
|
|||
import { icons, kinds } from 'Helpers/Props';
|
||||
import { Kind } from 'Helpers/Props/kinds';
|
||||
import { Tag } from 'Tags/useTags';
|
||||
import { AutoTaggingSpecification } from 'typings/AutoTagging';
|
||||
import translate from 'Utilities/String/translate';
|
||||
import EditAutoTaggingModal from './EditAutoTaggingModal';
|
||||
import {
|
||||
AutoTaggingSpecification,
|
||||
useDeleteAutoTagging,
|
||||
} from './useAutoTaggings';
|
||||
import styles from './AutoTagging.css';
|
||||
|
||||
interface AutoTaggingProps {
|
||||
|
|
@ -18,8 +21,6 @@ interface AutoTaggingProps {
|
|||
specifications: AutoTaggingSpecification[];
|
||||
tags: number[];
|
||||
tagList: Tag[];
|
||||
isDeleting: boolean;
|
||||
onConfirmDeleteAutoTagging: (id: number) => void;
|
||||
onCloneAutoTaggingPress: (id: number) => void;
|
||||
}
|
||||
|
||||
|
|
@ -29,10 +30,9 @@ export default function AutoTagging({
|
|||
tags,
|
||||
tagList,
|
||||
specifications,
|
||||
isDeleting,
|
||||
onConfirmDeleteAutoTagging,
|
||||
onCloneAutoTaggingPress,
|
||||
}: AutoTaggingProps) {
|
||||
const { deleteAutoTagging, isDeleting } = useDeleteAutoTagging(id);
|
||||
const [isEditModalOpen, setIsEditModalOpen] = useState(false);
|
||||
const [isDeleteModalOpen, setIsDeleteModalOpen] = useState(false);
|
||||
|
||||
|
|
@ -54,8 +54,8 @@ export default function AutoTagging({
|
|||
}, [setIsDeleteModalOpen]);
|
||||
|
||||
const onConfirmDelete = useCallback(() => {
|
||||
onConfirmDeleteAutoTagging(id);
|
||||
}, [id, onConfirmDeleteAutoTagging]);
|
||||
deleteAutoTagging();
|
||||
}, [deleteAutoTagging]);
|
||||
|
||||
const onClonePress = useCallback(() => {
|
||||
onCloneAutoTaggingPress(id);
|
||||
|
|
|
|||
|
|
@ -1,66 +1,42 @@
|
|||
import React, { useCallback, useEffect, useState } from 'react';
|
||||
import { useDispatch, useSelector } from 'react-redux';
|
||||
import { AutoTaggingAppState } from 'App/State/SettingsAppState';
|
||||
import React, { useCallback, useState } from 'react';
|
||||
import Card from 'Components/Card';
|
||||
import FieldSet from 'Components/FieldSet';
|
||||
import Icon from 'Components/Icon';
|
||||
import PageSectionContent from 'Components/Page/PageSectionContent';
|
||||
import { icons } from 'Helpers/Props';
|
||||
import {
|
||||
cloneAutoTagging,
|
||||
deleteAutoTagging,
|
||||
fetchAutoTaggings,
|
||||
} from 'Store/Actions/settingsActions';
|
||||
import createSortedSectionSelector from 'Store/Selectors/createSortedSectionSelector';
|
||||
import { useTagList } from 'Tags/useTags';
|
||||
import AutoTaggingModel from 'typings/AutoTagging';
|
||||
import sortByProp from 'Utilities/Array/sortByProp';
|
||||
import translate from 'Utilities/String/translate';
|
||||
import AutoTagging from './AutoTagging';
|
||||
import EditAutoTaggingModal from './EditAutoTaggingModal';
|
||||
import { useSortedAutoTaggings } from './useAutoTaggings';
|
||||
import styles from './AutoTaggings.css';
|
||||
|
||||
export default function AutoTaggings() {
|
||||
const { error, items, isDeleting, isFetching, isPopulated } = useSelector(
|
||||
createSortedSectionSelector<AutoTaggingModel, AutoTaggingAppState>(
|
||||
'settings.autoTaggings',
|
||||
sortByProp('name')
|
||||
)
|
||||
);
|
||||
const {
|
||||
data: items,
|
||||
error,
|
||||
isFetching,
|
||||
isFetched: isPopulated,
|
||||
} = useSortedAutoTaggings();
|
||||
|
||||
const tagList = useTagList();
|
||||
const dispatch = useDispatch();
|
||||
const [isEditModalOpen, setIsEditModalOpen] = useState(false);
|
||||
const [tagsFromId, setTagsFromId] = useState<number>();
|
||||
const [cloneId, setCloneId] = useState<number>();
|
||||
|
||||
const onClonePress = useCallback(
|
||||
(id: number) => {
|
||||
dispatch(cloneAutoTagging({ id }));
|
||||
|
||||
setTagsFromId(id);
|
||||
setIsEditModalOpen(true);
|
||||
},
|
||||
[dispatch, setIsEditModalOpen]
|
||||
);
|
||||
const onClonePress = useCallback((id: number) => {
|
||||
setCloneId(id);
|
||||
setIsEditModalOpen(true);
|
||||
}, []);
|
||||
|
||||
const onEditPress = useCallback(() => {
|
||||
setCloneId(undefined);
|
||||
setIsEditModalOpen(true);
|
||||
}, [setIsEditModalOpen]);
|
||||
}, []);
|
||||
|
||||
const onEditModalClose = useCallback(() => {
|
||||
setIsEditModalOpen(false);
|
||||
}, [setIsEditModalOpen]);
|
||||
|
||||
const onConfirmDelete = useCallback(
|
||||
(id: number) => {
|
||||
dispatch(deleteAutoTagging({ id }));
|
||||
},
|
||||
[dispatch]
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
dispatch(fetchAutoTaggings());
|
||||
}, [dispatch]);
|
||||
setCloneId(undefined);
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<FieldSet legend={translate('AutoTagging')}>
|
||||
|
|
@ -76,9 +52,7 @@ export default function AutoTaggings() {
|
|||
<AutoTagging
|
||||
key={item.id}
|
||||
{...item}
|
||||
isDeleting={isDeleting}
|
||||
tagList={tagList}
|
||||
onConfirmDeleteAutoTagging={onConfirmDelete}
|
||||
onCloneAutoTaggingPress={onClonePress}
|
||||
/>
|
||||
);
|
||||
|
|
@ -93,7 +67,7 @@ export default function AutoTaggings() {
|
|||
|
||||
<EditAutoTaggingModal
|
||||
isOpen={isEditModalOpen}
|
||||
tagsFromId={tagsFromId}
|
||||
cloneId={cloneId}
|
||||
onModalClose={onEditModalClose}
|
||||
/>
|
||||
</PageSectionContent>
|
||||
|
|
|
|||
|
|
@ -1,8 +1,6 @@
|
|||
import React, { useCallback } from 'react';
|
||||
import { useDispatch } from 'react-redux';
|
||||
import React from 'react';
|
||||
import Modal from 'Components/Modal/Modal';
|
||||
import { sizes } from 'Helpers/Props';
|
||||
import { clearPendingChanges } from 'Store/Actions/baseActions';
|
||||
import EditAutoTaggingModalContent, {
|
||||
EditAutoTaggingModalContentProps,
|
||||
} from './EditAutoTaggingModalContent';
|
||||
|
|
@ -17,18 +15,11 @@ export default function EditAutoTaggingModal({
|
|||
onModalClose,
|
||||
...otherProps
|
||||
}: EditAutoTaggingModalProps) {
|
||||
const dispatch = useDispatch();
|
||||
|
||||
const handleModalClose = useCallback(() => {
|
||||
dispatch(clearPendingChanges({ section: 'settings.autoTaggings' }));
|
||||
onModalClose();
|
||||
}, [dispatch, onModalClose]);
|
||||
|
||||
return (
|
||||
<Modal isOpen={isOpen} size={sizes.LARGE} onModalClose={handleModalClose}>
|
||||
<Modal isOpen={isOpen} size={sizes.LARGE} onModalClose={onModalClose}>
|
||||
<EditAutoTaggingModalContent
|
||||
{...otherProps}
|
||||
onModalClose={handleModalClose}
|
||||
onModalClose={onModalClose}
|
||||
/>
|
||||
</Modal>
|
||||
);
|
||||
|
|
|
|||
|
|
@ -1,7 +1,4 @@
|
|||
import React, { useCallback, useEffect, useRef, useState } from 'react';
|
||||
import { useDispatch, useSelector } from 'react-redux';
|
||||
import AppState from 'App/State/AppState';
|
||||
import Alert from 'Components/Alert';
|
||||
import React, { useCallback, useEffect, useState } from 'react';
|
||||
import Card from 'Components/Card';
|
||||
import FieldSet from 'Components/FieldSet';
|
||||
import Form from 'Components/Form/Form';
|
||||
|
|
@ -11,114 +8,107 @@ import FormLabel from 'Components/Form/FormLabel';
|
|||
import Icon from 'Components/Icon';
|
||||
import Button from 'Components/Link/Button';
|
||||
import SpinnerErrorButton from 'Components/Link/SpinnerErrorButton';
|
||||
import LoadingIndicator from 'Components/Loading/LoadingIndicator';
|
||||
import ModalBody from 'Components/Modal/ModalBody';
|
||||
import ModalContent from 'Components/Modal/ModalContent';
|
||||
import ModalFooter from 'Components/Modal/ModalFooter';
|
||||
import ModalHeader from 'Components/Modal/ModalHeader';
|
||||
import usePrevious from 'Helpers/Hooks/usePrevious';
|
||||
import { icons, inputTypes, kinds } from 'Helpers/Props';
|
||||
import {
|
||||
cloneAutoTaggingSpecification,
|
||||
deleteAutoTaggingSpecification,
|
||||
fetchAutoTaggingSpecifications,
|
||||
saveAutoTagging,
|
||||
setAutoTaggingValue,
|
||||
} from 'Store/Actions/settingsActions';
|
||||
import { createProviderSettingsSelectorHook } from 'Store/Selectors/createProviderSettingsSelector';
|
||||
import { InputChanged } from 'typings/inputs';
|
||||
import translate from 'Utilities/String/translate';
|
||||
import AddSpecificationModal from './Specifications/AddSpecificationModal';
|
||||
import EditSpecificationModal from './Specifications/EditSpecificationModal';
|
||||
import Specification from './Specifications/Specification';
|
||||
import {
|
||||
AutoTagging,
|
||||
AutoTaggingSpecification,
|
||||
useManageAutoTagging,
|
||||
} from './useAutoTaggings';
|
||||
import styles from './EditAutoTaggingModalContent.css';
|
||||
|
||||
export interface EditAutoTaggingModalContentProps {
|
||||
id?: number;
|
||||
tagsFromId?: number;
|
||||
cloneId?: number;
|
||||
onModalClose: () => void;
|
||||
onDeleteAutoTaggingPress?: () => void;
|
||||
}
|
||||
|
||||
export default function EditAutoTaggingModalContent({
|
||||
id,
|
||||
tagsFromId,
|
||||
cloneId,
|
||||
onModalClose,
|
||||
onDeleteAutoTaggingPress,
|
||||
}: EditAutoTaggingModalContentProps) {
|
||||
const {
|
||||
error,
|
||||
item,
|
||||
isFetching,
|
||||
isSaving,
|
||||
saveError,
|
||||
validationErrors,
|
||||
validationWarnings,
|
||||
} = useSelector(createProviderSettingsSelectorHook('autoTaggings', id));
|
||||
updateValue,
|
||||
saveAutoTagging,
|
||||
isSaving,
|
||||
saveError,
|
||||
specifications,
|
||||
saveSpecification,
|
||||
deleteSpecification,
|
||||
cloneSpecification,
|
||||
} = useManageAutoTagging(id, cloneId);
|
||||
|
||||
const { isPopulated: specificationsPopulated, items: specifications } =
|
||||
useSelector((state: AppState) => state.settings.autoTaggingSpecifications);
|
||||
|
||||
const dispatch = useDispatch();
|
||||
const [isAddSpecificationModalOpen, setIsAddSpecificationModalOpen] =
|
||||
useState(false);
|
||||
const [isEditSpecificationModalOpen, setIsEditSpecificationModalOpen] =
|
||||
useState(false);
|
||||
const [editingSpecification, setEditingSpecification] =
|
||||
useState<AutoTaggingSpecification | null>(null);
|
||||
|
||||
const handleAddSpecificationPress = useCallback(() => {
|
||||
setIsAddSpecificationModalOpen(true);
|
||||
}, [setIsAddSpecificationModalOpen]);
|
||||
}, []);
|
||||
|
||||
const handleAddSpecificationModalClose = useCallback(
|
||||
({ specificationSelected = false } = {}) => {
|
||||
(selectedSpec?: AutoTaggingSpecification) => {
|
||||
setIsAddSpecificationModalOpen(false);
|
||||
setIsEditSpecificationModalOpen(specificationSelected);
|
||||
|
||||
if (selectedSpec) {
|
||||
setEditingSpecification({ ...selectedSpec, id: 0 });
|
||||
setIsEditSpecificationModalOpen(true);
|
||||
}
|
||||
},
|
||||
[setIsAddSpecificationModalOpen]
|
||||
[]
|
||||
);
|
||||
|
||||
const handleEditSpecificationModalClose = useCallback(() => {
|
||||
const handleNewSpecificationModalClose = useCallback(() => {
|
||||
setIsEditSpecificationModalOpen(false);
|
||||
}, [setIsEditSpecificationModalOpen]);
|
||||
setEditingSpecification(null);
|
||||
}, []);
|
||||
|
||||
const handleSaveNewSpecification = useCallback(
|
||||
(spec: AutoTaggingSpecification) => {
|
||||
saveSpecification(spec);
|
||||
},
|
||||
[saveSpecification]
|
||||
);
|
||||
|
||||
const handleInputChange = useCallback(
|
||||
({ name, value }: InputChanged) => {
|
||||
// @ts-expect-error - actions are not typed
|
||||
dispatch(setAutoTaggingValue({ name, value }));
|
||||
updateValue(
|
||||
name as keyof AutoTagging,
|
||||
value as AutoTagging[keyof AutoTagging]
|
||||
);
|
||||
},
|
||||
[dispatch]
|
||||
[updateValue]
|
||||
);
|
||||
|
||||
const handleSavePress = useCallback(() => {
|
||||
dispatch(saveAutoTagging({ id }));
|
||||
}, [dispatch, id]);
|
||||
saveAutoTagging();
|
||||
}, [saveAutoTagging]);
|
||||
|
||||
const handleCloneSpecificationPress = useCallback(
|
||||
(specId: number) => {
|
||||
dispatch(cloneAutoTaggingSpecification({ id: specId }));
|
||||
},
|
||||
[dispatch]
|
||||
);
|
||||
|
||||
const handleConfirmDeleteSpecification = useCallback(
|
||||
(specId: number) => {
|
||||
dispatch(deleteAutoTaggingSpecification({ id: specId }));
|
||||
},
|
||||
[dispatch]
|
||||
);
|
||||
const wasSaving = usePrevious(isSaving);
|
||||
|
||||
useEffect(() => {
|
||||
dispatch(fetchAutoTaggingSpecifications({ id: tagsFromId || id }));
|
||||
}, [id, tagsFromId, dispatch]);
|
||||
|
||||
const isSavingRef = useRef(false);
|
||||
|
||||
useEffect(() => {
|
||||
if (isSavingRef.current && !isSaving && !saveError) {
|
||||
if (wasSaving && !isSaving && !saveError) {
|
||||
onModalClose();
|
||||
}
|
||||
|
||||
isSavingRef.current = isSaving;
|
||||
}, [isSaving, saveError, onModalClose]);
|
||||
}, [isSaving, wasSaving, saveError, onModalClose]);
|
||||
|
||||
const { name, removeTagsAutomatically, tags } = item;
|
||||
|
||||
|
|
@ -130,96 +120,82 @@ export default function EditAutoTaggingModalContent({
|
|||
|
||||
<ModalBody>
|
||||
<div>
|
||||
{isFetching ? <LoadingIndicator /> : null}
|
||||
<Form
|
||||
validationErrors={validationErrors}
|
||||
validationWarnings={validationWarnings}
|
||||
>
|
||||
<FormGroup>
|
||||
<FormLabel>{translate('Name')}</FormLabel>
|
||||
|
||||
{!isFetching && !!error ? (
|
||||
<Alert kind={kinds.DANGER}>{translate('AddAutoTagError')}</Alert>
|
||||
) : null}
|
||||
<FormInputGroup
|
||||
type={inputTypes.TEXT}
|
||||
name="name"
|
||||
{...name}
|
||||
onChange={handleInputChange}
|
||||
/>
|
||||
</FormGroup>
|
||||
|
||||
{!isFetching && !error && specificationsPopulated ? (
|
||||
<div>
|
||||
<Form
|
||||
validationErrors={validationErrors}
|
||||
validationWarnings={validationWarnings}
|
||||
<FormGroup>
|
||||
<FormLabel>{translate('RemoveTagsAutomatically')}</FormLabel>
|
||||
|
||||
<FormInputGroup
|
||||
type={inputTypes.CHECK}
|
||||
name="removeTagsAutomatically"
|
||||
helpText={translate('RemoveTagsAutomaticallyHelpText')}
|
||||
{...removeTagsAutomatically}
|
||||
onChange={handleInputChange}
|
||||
/>
|
||||
</FormGroup>
|
||||
|
||||
<FormGroup>
|
||||
<FormLabel>{translate('Tags')}</FormLabel>
|
||||
|
||||
<FormInputGroup
|
||||
type={inputTypes.TAG}
|
||||
name="tags"
|
||||
onChange={handleInputChange}
|
||||
{...tags}
|
||||
/>
|
||||
</FormGroup>
|
||||
</Form>
|
||||
|
||||
<FieldSet legend={translate('Conditions')}>
|
||||
<div className={styles.autoTaggings}>
|
||||
{specifications.map((specification) => {
|
||||
return (
|
||||
<Specification
|
||||
key={specification.id}
|
||||
{...specification}
|
||||
onSaveSpecification={saveSpecification}
|
||||
onCloneSpecificationPress={cloneSpecification}
|
||||
onConfirmDeleteSpecification={deleteSpecification}
|
||||
/>
|
||||
);
|
||||
})}
|
||||
|
||||
<Card
|
||||
className={styles.addSpecification}
|
||||
onPress={handleAddSpecificationPress}
|
||||
>
|
||||
<FormGroup>
|
||||
<FormLabel>{translate('Name')}</FormLabel>
|
||||
|
||||
<FormInputGroup
|
||||
type={inputTypes.TEXT}
|
||||
name="name"
|
||||
{...name}
|
||||
onChange={handleInputChange}
|
||||
/>
|
||||
</FormGroup>
|
||||
|
||||
<FormGroup>
|
||||
<FormLabel>{translate('RemoveTagsAutomatically')}</FormLabel>
|
||||
|
||||
<FormInputGroup
|
||||
type={inputTypes.CHECK}
|
||||
name="removeTagsAutomatically"
|
||||
helpText={translate('RemoveTagsAutomaticallyHelpText')}
|
||||
{...removeTagsAutomatically}
|
||||
onChange={handleInputChange}
|
||||
/>
|
||||
</FormGroup>
|
||||
|
||||
<FormGroup>
|
||||
<FormLabel>{translate('Tags')}</FormLabel>
|
||||
|
||||
<FormInputGroup
|
||||
type={inputTypes.TAG}
|
||||
name="tags"
|
||||
onChange={handleInputChange}
|
||||
{...tags}
|
||||
/>
|
||||
</FormGroup>
|
||||
</Form>
|
||||
|
||||
<FieldSet legend={translate('Conditions')}>
|
||||
<div className={styles.autoTaggings}>
|
||||
{specifications.map((specification) => {
|
||||
return (
|
||||
<Specification
|
||||
key={specification.id}
|
||||
{...specification}
|
||||
onCloneSpecificationPress={
|
||||
handleCloneSpecificationPress
|
||||
}
|
||||
onConfirmDeleteSpecification={
|
||||
handleConfirmDeleteSpecification
|
||||
}
|
||||
/>
|
||||
);
|
||||
})}
|
||||
|
||||
<Card
|
||||
className={styles.addSpecification}
|
||||
onPress={handleAddSpecificationPress}
|
||||
>
|
||||
<div className={styles.center}>
|
||||
<Icon name={icons.ADD} size={45} />
|
||||
</div>
|
||||
</Card>
|
||||
<div className={styles.center}>
|
||||
<Icon name={icons.ADD} size={45} />
|
||||
</div>
|
||||
</FieldSet>
|
||||
|
||||
<AddSpecificationModal
|
||||
isOpen={isAddSpecificationModalOpen}
|
||||
onModalClose={handleAddSpecificationModalClose}
|
||||
/>
|
||||
|
||||
<EditSpecificationModal
|
||||
isOpen={isEditSpecificationModalOpen}
|
||||
onModalClose={handleEditSpecificationModalClose}
|
||||
/>
|
||||
|
||||
{/* <ImportAutoTaggingModal
|
||||
isOpen={isImportAutoTaggingModalOpen}
|
||||
onModalClose={onImportAutoTaggingModalClose}
|
||||
/> */}
|
||||
</Card>
|
||||
</div>
|
||||
</FieldSet>
|
||||
|
||||
<AddSpecificationModal
|
||||
isOpen={isAddSpecificationModalOpen}
|
||||
onModalClose={handleAddSpecificationModalClose}
|
||||
/>
|
||||
|
||||
{editingSpecification ? (
|
||||
<EditSpecificationModal
|
||||
isOpen={isEditSpecificationModalOpen}
|
||||
specification={editingSpecification}
|
||||
onSave={handleSaveNewSpecification}
|
||||
onModalClose={handleNewSpecificationModalClose}
|
||||
/>
|
||||
) : null}
|
||||
</div>
|
||||
</ModalBody>
|
||||
|
|
@ -234,13 +210,6 @@ export default function EditAutoTaggingModalContent({
|
|||
{translate('Delete')}
|
||||
</Button>
|
||||
) : null}
|
||||
|
||||
{/* <Button
|
||||
className={styles.deleteButton}
|
||||
onPress={onImportPress}
|
||||
>
|
||||
Import
|
||||
</Button> */}
|
||||
</div>
|
||||
|
||||
<Button onPress={onModalClose}>{translate('Cancel')}</Button>
|
||||
|
|
|
|||
|
|
@ -4,8 +4,8 @@ import Link from 'Components/Link/Link';
|
|||
import Menu from 'Components/Menu/Menu';
|
||||
import MenuContent from 'Components/Menu/MenuContent';
|
||||
import { sizes } from 'Helpers/Props';
|
||||
import { AutoTaggingSpecification } from 'typings/AutoTagging';
|
||||
import translate from 'Utilities/String/translate';
|
||||
import { AutoTaggingSpecification } from '../useAutoTaggings';
|
||||
import AddSpecificationPresetMenuItem from './AddSpecificationPresetMenuItem';
|
||||
import styles from './AddSpecificationItem.css';
|
||||
|
||||
|
|
|
|||
|
|
@ -1,10 +1,11 @@
|
|||
import React from 'react';
|
||||
import Modal from 'Components/Modal/Modal';
|
||||
import { AutoTaggingSpecification } from '../useAutoTaggings';
|
||||
import AddSpecificationModalContent from './AddSpecificationModalContent';
|
||||
|
||||
interface AddSpecificationModalProps {
|
||||
isOpen: boolean;
|
||||
onModalClose: (options?: { specificationSelected: boolean }) => void;
|
||||
onModalClose: (selectedSpec?: AutoTaggingSpecification) => void;
|
||||
}
|
||||
|
||||
function AddSpecificationModal({
|
||||
|
|
|
|||
|
|
@ -1,6 +1,4 @@
|
|||
import React, { useCallback, useEffect } from 'react';
|
||||
import { useDispatch, useSelector } from 'react-redux';
|
||||
import AppState from 'App/State/AppState';
|
||||
import React, { useCallback } from 'react';
|
||||
import Alert from 'Components/Alert';
|
||||
import Button from 'Components/Link/Button';
|
||||
import LoadingIndicator from 'Components/Loading/LoadingIndicator';
|
||||
|
|
@ -9,59 +7,50 @@ import ModalContent from 'Components/Modal/ModalContent';
|
|||
import ModalFooter from 'Components/Modal/ModalFooter';
|
||||
import ModalHeader from 'Components/Modal/ModalHeader';
|
||||
import { kinds } from 'Helpers/Props';
|
||||
import {
|
||||
fetchAutoTaggingSpecificationSchema,
|
||||
selectAutoTaggingSpecificationSchema,
|
||||
} from 'Store/Actions/settingsActions';
|
||||
import translate from 'Utilities/String/translate';
|
||||
import {
|
||||
AutoTaggingSpecification,
|
||||
useAutoTaggingSchema,
|
||||
} from '../useAutoTaggings';
|
||||
import AddSpecificationItem from './AddSpecificationItem';
|
||||
import styles from './AddSpecificationModalContent.css';
|
||||
|
||||
interface AddSpecificationModalContentProps {
|
||||
onModalClose: (options?: { specificationSelected: boolean }) => void;
|
||||
onModalClose: (selectedSpec?: AutoTaggingSpecification) => void;
|
||||
}
|
||||
|
||||
export default function AddSpecificationModalContent({
|
||||
onModalClose,
|
||||
}: AddSpecificationModalContentProps) {
|
||||
const { isSchemaFetching, isSchemaPopulated, schemaError, schema } =
|
||||
useSelector((state: AppState) => state.settings.autoTaggingSpecifications);
|
||||
|
||||
const dispatch = useDispatch();
|
||||
const { schema, isSchemaFetching, schemaError } = useAutoTaggingSchema();
|
||||
|
||||
const onSpecificationSelect = useCallback(
|
||||
({ implementation }: { implementation: string }) => {
|
||||
dispatch(
|
||||
selectAutoTaggingSpecificationSchema({
|
||||
implementation,
|
||||
presetName: name,
|
||||
})
|
||||
);
|
||||
onModalClose({ specificationSelected: true });
|
||||
const selected = schema.find((s) => s.implementation === implementation);
|
||||
|
||||
if (selected) {
|
||||
onModalClose(selected);
|
||||
}
|
||||
},
|
||||
[dispatch, onModalClose]
|
||||
[schema, onModalClose]
|
||||
);
|
||||
|
||||
const handleModalClose = useCallback(() => {
|
||||
onModalClose();
|
||||
}, [onModalClose]);
|
||||
|
||||
useEffect(() => {
|
||||
dispatch(fetchAutoTaggingSpecificationSchema());
|
||||
}, [dispatch]);
|
||||
|
||||
return (
|
||||
<ModalContent onModalClose={onModalClose}>
|
||||
<ModalContent onModalClose={handleModalClose}>
|
||||
<ModalHeader>{translate('AddCondition')}</ModalHeader>
|
||||
|
||||
<ModalBody>
|
||||
{isSchemaFetching ? <LoadingIndicator /> : null}
|
||||
|
||||
{!isSchemaFetching && !!schemaError ? (
|
||||
{!isSchemaFetching && schemaError ? (
|
||||
<Alert kind={kinds.DANGER}>{translate('AddConditionError')}</Alert>
|
||||
) : null}
|
||||
|
||||
{isSchemaPopulated && !schemaError ? (
|
||||
{!isSchemaFetching && !schemaError && schema.length ? (
|
||||
<div>
|
||||
<Alert kind={kinds.INFO}>
|
||||
<div>{translate('SupportedAutoTaggingProperties')}</div>
|
||||
|
|
|
|||
|
|
@ -1,37 +1,35 @@
|
|||
import React, { useCallback } from 'react';
|
||||
import { useDispatch } from 'react-redux';
|
||||
import React from 'react';
|
||||
import Modal from 'Components/Modal/Modal';
|
||||
import { sizes } from 'Helpers/Props';
|
||||
import { clearPendingChanges } from 'Store/Actions/baseActions';
|
||||
import EditSpecificationModalContent, {
|
||||
EditSpecificationModalContentProps,
|
||||
} from './EditSpecificationModalContent';
|
||||
import { AutoTaggingSpecification } from '../useAutoTaggings';
|
||||
import EditSpecificationModalContent from './EditSpecificationModalContent';
|
||||
|
||||
interface EditSpecificationModalProps
|
||||
extends EditSpecificationModalContentProps {
|
||||
interface EditSpecificationModalProps {
|
||||
isOpen: boolean;
|
||||
specification: AutoTaggingSpecification | null;
|
||||
onSave: (spec: AutoTaggingSpecification) => void;
|
||||
onDeleteSpecificationPress?: () => void;
|
||||
onModalClose: () => void;
|
||||
}
|
||||
|
||||
function EditSpecificationModal({
|
||||
isOpen,
|
||||
specification,
|
||||
onSave,
|
||||
onDeleteSpecificationPress,
|
||||
onModalClose,
|
||||
...otherProps
|
||||
}: EditSpecificationModalProps) {
|
||||
const dispatch = useDispatch();
|
||||
|
||||
const onWrappedModalClose = useCallback(() => {
|
||||
dispatch(
|
||||
clearPendingChanges({ section: 'settings.autoTaggingSpecifications' })
|
||||
);
|
||||
onModalClose();
|
||||
}, [onModalClose, dispatch]);
|
||||
if (!specification) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<Modal size={sizes.MEDIUM} isOpen={isOpen} onModalClose={onModalClose}>
|
||||
<EditSpecificationModalContent
|
||||
{...otherProps}
|
||||
onModalClose={onWrappedModalClose}
|
||||
specification={specification}
|
||||
onSave={onSave}
|
||||
onDeleteSpecificationPress={onDeleteSpecificationPress}
|
||||
onModalClose={onModalClose}
|
||||
/>
|
||||
</Modal>
|
||||
);
|
||||
|
|
|
|||
|
|
@ -1,6 +1,4 @@
|
|||
import React, { useCallback } from 'react';
|
||||
import { useDispatch, useSelector } from 'react-redux';
|
||||
import { AutoTaggingSpecificationAppState } from 'App/State/SettingsAppState';
|
||||
import React, { useCallback, useMemo } from 'react';
|
||||
import Alert from 'Components/Alert';
|
||||
import Form from 'Components/Form/Form';
|
||||
import FormGroup from 'Components/Form/FormGroup';
|
||||
|
|
@ -14,80 +12,128 @@ import ModalBody from 'Components/Modal/ModalBody';
|
|||
import ModalContent from 'Components/Modal/ModalContent';
|
||||
import ModalFooter from 'Components/Modal/ModalFooter';
|
||||
import ModalHeader from 'Components/Modal/ModalHeader';
|
||||
import { usePendingChangesStore } from 'Helpers/Hooks/usePendingChangesStore';
|
||||
import { usePendingFieldsStore } from 'Helpers/Hooks/usePendingFieldsStore';
|
||||
import { inputTypes, kinds } from 'Helpers/Props';
|
||||
import { useShowAdvancedSettings } from 'Settings/advancedSettingsStore';
|
||||
import {
|
||||
clearAutoTaggingSpecificationPending,
|
||||
saveAutoTaggingSpecification,
|
||||
setAutoTaggingSpecificationFieldValue,
|
||||
setAutoTaggingSpecificationValue,
|
||||
} from 'Store/Actions/settingsActions';
|
||||
import { createProviderSettingsSelectorHook } from 'Store/Selectors/createProviderSettingsSelector';
|
||||
import { AutoTaggingSpecification } from 'typings/AutoTagging';
|
||||
import selectSettings from 'Store/Selectors/selectSettings';
|
||||
import { InputChanged } from 'typings/inputs';
|
||||
import translate from 'Utilities/String/translate';
|
||||
import { AutoTaggingSpecification } from '../useAutoTaggings';
|
||||
import styles from './EditSpecificationModalContent.css';
|
||||
|
||||
export interface EditSpecificationModalContentProps {
|
||||
id?: number;
|
||||
interface EditSpecificationModalContentProps {
|
||||
specification: AutoTaggingSpecification;
|
||||
onSave: (spec: AutoTaggingSpecification) => void;
|
||||
onDeleteSpecificationPress?: () => void;
|
||||
onModalClose: () => void;
|
||||
}
|
||||
|
||||
function EditSpecificationModalContent({
|
||||
id,
|
||||
specification,
|
||||
onSave,
|
||||
onDeleteSpecificationPress,
|
||||
onModalClose,
|
||||
}: EditSpecificationModalContentProps) {
|
||||
const advancedSettings = useShowAdvancedSettings();
|
||||
|
||||
const { item, ...otherFormProps } = useSelector(
|
||||
createProviderSettingsSelectorHook<
|
||||
AutoTaggingSpecification,
|
||||
AutoTaggingSpecificationAppState
|
||||
>('autoTaggingSpecifications', id)
|
||||
);
|
||||
const { pendingChanges, setPendingChange, clearPendingChanges } =
|
||||
usePendingChangesStore<AutoTaggingSpecification>({});
|
||||
|
||||
const dispatch = useDispatch();
|
||||
const {
|
||||
pendingFields,
|
||||
setPendingField,
|
||||
hasPendingFields,
|
||||
clearPendingFields,
|
||||
} = usePendingFieldsStore();
|
||||
|
||||
const {
|
||||
settings: item,
|
||||
validationErrors,
|
||||
validationWarnings,
|
||||
} = useMemo(() => {
|
||||
const combinedPendingChanges = hasPendingFields
|
||||
? {
|
||||
...pendingChanges,
|
||||
fields: Object.fromEntries(pendingFields),
|
||||
}
|
||||
: pendingChanges;
|
||||
|
||||
return selectSettings(specification, combinedPendingChanges);
|
||||
}, [specification, pendingChanges, pendingFields, hasPendingFields]);
|
||||
|
||||
const onInputChange = useCallback(
|
||||
({ name, value }: InputChanged) => {
|
||||
// @ts-expect-error - actions are not typed
|
||||
dispatch(setAutoTaggingSpecificationValue({ name, value }));
|
||||
setPendingChange(
|
||||
name as keyof AutoTaggingSpecification,
|
||||
value as AutoTaggingSpecification[keyof AutoTaggingSpecification]
|
||||
);
|
||||
},
|
||||
[dispatch]
|
||||
[setPendingChange]
|
||||
);
|
||||
|
||||
const onFieldChange = useCallback(
|
||||
({ name, value }: InputChanged) => {
|
||||
// @ts-expect-error - actions are not typed
|
||||
dispatch(setAutoTaggingSpecificationFieldValue({ name, value }));
|
||||
setPendingField(name, value);
|
||||
},
|
||||
[dispatch]
|
||||
[setPendingField]
|
||||
);
|
||||
|
||||
const onDeletePress = useCallback(() => {
|
||||
if (onDeleteSpecificationPress) {
|
||||
onDeleteSpecificationPress();
|
||||
}
|
||||
}, [onDeleteSpecificationPress]);
|
||||
|
||||
const onCancelPress = useCallback(() => {
|
||||
dispatch(clearAutoTaggingSpecificationPending());
|
||||
clearPendingChanges();
|
||||
clearPendingFields();
|
||||
onModalClose();
|
||||
}, [dispatch, onModalClose]);
|
||||
}, [clearPendingChanges, clearPendingFields, onModalClose]);
|
||||
|
||||
const onSavePress = useCallback(() => {
|
||||
dispatch(saveAutoTaggingSpecification({ id }));
|
||||
let updatedSpec: AutoTaggingSpecification = {
|
||||
...specification,
|
||||
...pendingChanges,
|
||||
};
|
||||
|
||||
if (hasPendingFields) {
|
||||
updatedSpec = {
|
||||
...updatedSpec,
|
||||
fields: specification.fields.map((f) =>
|
||||
pendingFields.has(f.name)
|
||||
? { ...f, value: pendingFields.get(f.name) as typeof f.value }
|
||||
: f
|
||||
),
|
||||
};
|
||||
}
|
||||
|
||||
onSave(updatedSpec);
|
||||
onModalClose();
|
||||
}, [dispatch, id, onModalClose]);
|
||||
}, [
|
||||
specification,
|
||||
pendingChanges,
|
||||
pendingFields,
|
||||
hasPendingFields,
|
||||
onSave,
|
||||
onModalClose,
|
||||
]);
|
||||
|
||||
const { implementationName, name, negate, required, fields } = item;
|
||||
|
||||
return (
|
||||
<ModalContent onModalClose={onCancelPress}>
|
||||
<ModalHeader>
|
||||
{id
|
||||
{specification.id
|
||||
? translate('EditConditionImplementation', { implementationName })
|
||||
: translate('AddConditionImplementation', { implementationName })}
|
||||
</ModalHeader>
|
||||
|
||||
<ModalBody>
|
||||
<Form {...otherFormProps}>
|
||||
<Form
|
||||
validationErrors={validationErrors}
|
||||
validationWarnings={validationWarnings}
|
||||
>
|
||||
{fields && fields.some((x) => x.label === 'Regular Expression') && (
|
||||
<Alert kind={kinds.INFO}>
|
||||
<div>
|
||||
|
|
@ -167,11 +213,11 @@ function EditSpecificationModalContent({
|
|||
</Form>
|
||||
</ModalBody>
|
||||
<ModalFooter>
|
||||
{id ? (
|
||||
{specification.id ? (
|
||||
<Button
|
||||
className={styles.deleteButton}
|
||||
kind={kinds.DANGER}
|
||||
onPress={onDeleteSpecificationPress}
|
||||
onPress={onDeletePress}
|
||||
>
|
||||
{translate('Delete')}
|
||||
</Button>
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
import React, { useCallback, useState } from 'react';
|
||||
import React, { useCallback, useMemo, useState } from 'react';
|
||||
import Card from 'Components/Card';
|
||||
import Label from 'Components/Label';
|
||||
import IconButton from 'Components/Link/IconButton';
|
||||
|
|
@ -6,6 +6,7 @@ import ConfirmModal from 'Components/Modal/ConfirmModal';
|
|||
import { icons, kinds } from 'Helpers/Props';
|
||||
import Field from 'typings/Field';
|
||||
import translate from 'Utilities/String/translate';
|
||||
import { AutoTaggingSpecification } from '../useAutoTaggings';
|
||||
import EditSpecificationModal from './EditSpecificationModal';
|
||||
import styles from './Specification.css';
|
||||
|
||||
|
|
@ -17,38 +18,55 @@ interface SpecificationProps {
|
|||
negate: boolean;
|
||||
required: boolean;
|
||||
fields: Field[];
|
||||
onSaveSpecification: (spec: AutoTaggingSpecification) => void;
|
||||
onConfirmDeleteSpecification: (specId: number) => void;
|
||||
onCloneSpecificationPress: (specId: number) => void;
|
||||
}
|
||||
|
||||
export default function Specification({
|
||||
id,
|
||||
implementation,
|
||||
implementationName,
|
||||
name,
|
||||
required,
|
||||
negate,
|
||||
required,
|
||||
fields,
|
||||
onSaveSpecification,
|
||||
onConfirmDeleteSpecification,
|
||||
onCloneSpecificationPress,
|
||||
}: SpecificationProps) {
|
||||
const [isEditModalOpen, setIsEditModalOpen] = useState(false);
|
||||
const [isDeleteModalOpen, setIsDeleteModalOpen] = useState(false);
|
||||
|
||||
const spec = useMemo<AutoTaggingSpecification>(
|
||||
() => ({
|
||||
id,
|
||||
implementation,
|
||||
implementationName,
|
||||
name,
|
||||
negate,
|
||||
required,
|
||||
fields,
|
||||
}),
|
||||
[id, implementation, implementationName, name, negate, required, fields]
|
||||
);
|
||||
|
||||
const onEditPress = useCallback(() => {
|
||||
setIsEditModalOpen(true);
|
||||
}, [setIsEditModalOpen]);
|
||||
}, []);
|
||||
|
||||
const onEditModalClose = useCallback(() => {
|
||||
setIsEditModalOpen(false);
|
||||
}, [setIsEditModalOpen]);
|
||||
}, []);
|
||||
|
||||
const onDeletePress = useCallback(() => {
|
||||
setIsEditModalOpen(false);
|
||||
setIsDeleteModalOpen(true);
|
||||
}, [setIsEditModalOpen, setIsDeleteModalOpen]);
|
||||
}, []);
|
||||
|
||||
const onDeleteModalClose = useCallback(() => {
|
||||
setIsDeleteModalOpen(false);
|
||||
}, [setIsDeleteModalOpen]);
|
||||
}, []);
|
||||
|
||||
const onConfirmDelete = useCallback(() => {
|
||||
onConfirmDeleteSpecification(id);
|
||||
|
|
@ -89,10 +107,11 @@ export default function Specification({
|
|||
</div>
|
||||
|
||||
<EditSpecificationModal
|
||||
id={id}
|
||||
isOpen={isEditModalOpen}
|
||||
onModalClose={onEditModalClose}
|
||||
specification={spec}
|
||||
onSave={onSaveSpecification}
|
||||
onDeleteSpecificationPress={onDeletePress}
|
||||
onModalClose={onEditModalClose}
|
||||
/>
|
||||
|
||||
<ConfirmModal
|
||||
|
|
|
|||
196
frontend/src/Settings/Tags/AutoTagging/useAutoTaggings.ts
Normal file
196
frontend/src/Settings/Tags/AutoTagging/useAutoTaggings.ts
Normal file
|
|
@ -0,0 +1,196 @@
|
|||
import { useCallback, useMemo } from 'react';
|
||||
import ModelBase from 'App/ModelBase';
|
||||
import { useProviderSchema } from 'Settings/useProviderSchema';
|
||||
import {
|
||||
useDeleteProvider,
|
||||
useManageProviderSettings,
|
||||
useProviderSettings,
|
||||
} from 'Settings/useProviderSettings';
|
||||
import Field from 'typings/Field';
|
||||
import { sortByProp } from 'Utilities/Array/sortByProp';
|
||||
import translate from 'Utilities/String/translate';
|
||||
|
||||
export interface AutoTaggingSpecification {
|
||||
id: number;
|
||||
name: string;
|
||||
implementation: string;
|
||||
implementationName: string;
|
||||
negate: boolean;
|
||||
required: boolean;
|
||||
fields: Field[];
|
||||
}
|
||||
|
||||
export interface AutoTagging extends ModelBase {
|
||||
name: string;
|
||||
removeTagsAutomatically: boolean;
|
||||
tags: number[];
|
||||
specifications: AutoTaggingSpecification[];
|
||||
}
|
||||
|
||||
const PATH = '/autoTagging';
|
||||
|
||||
const DEFAULT_AUTO_TAGGING: AutoTagging = {
|
||||
id: 0,
|
||||
name: '',
|
||||
removeTagsAutomatically: false,
|
||||
tags: [],
|
||||
specifications: [],
|
||||
};
|
||||
|
||||
export const useAutoTaggings = () => {
|
||||
return useProviderSettings<AutoTagging>({
|
||||
path: PATH,
|
||||
queryOptions: {
|
||||
refetchOnWindowFocus: false,
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
export const useSortedAutoTaggings = () => {
|
||||
const result = useAutoTaggings();
|
||||
|
||||
const sortedData = useMemo(
|
||||
() => [...result.data].sort(sortByProp('name')),
|
||||
[result.data]
|
||||
);
|
||||
|
||||
return { ...result, data: sortedData };
|
||||
};
|
||||
|
||||
export const useAutoTagging = (id: number | undefined) => {
|
||||
const { data } = useAutoTaggings();
|
||||
|
||||
if (id === undefined) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
return data.find((at) => at.id === id);
|
||||
};
|
||||
|
||||
export const useAutoTaggingsWithIds = (ids: number[]) => {
|
||||
const { data } = useAutoTaggings();
|
||||
|
||||
return data.filter((at) => ids.includes(at.id));
|
||||
};
|
||||
|
||||
export const useDeleteAutoTagging = (id: number) => {
|
||||
const result = useDeleteProvider<AutoTagging>(id, PATH);
|
||||
|
||||
return {
|
||||
...result,
|
||||
deleteAutoTagging: result.deleteProvider,
|
||||
};
|
||||
};
|
||||
|
||||
export const useAutoTaggingSchema = (enabled: boolean = true) => {
|
||||
return useProviderSchema<AutoTaggingSpecification>(PATH, enabled);
|
||||
};
|
||||
|
||||
function getNextSpecId(specifications: AutoTaggingSpecification[]) {
|
||||
return specifications.length > 0
|
||||
? Math.max(...specifications.map((s) => s.id)) + 1
|
||||
: 1;
|
||||
}
|
||||
|
||||
export const useManageAutoTagging = (
|
||||
id: number | undefined,
|
||||
cloneId: number | undefined
|
||||
) => {
|
||||
const cloneSource = useAutoTagging(cloneId);
|
||||
|
||||
if (cloneId && !cloneSource) {
|
||||
throw new Error(`AutoTagging with ID ${cloneId} not found`);
|
||||
}
|
||||
|
||||
const defaultProvider = useMemo<AutoTagging>(() => {
|
||||
if (cloneId && cloneSource) {
|
||||
return {
|
||||
...cloneSource,
|
||||
id: 0,
|
||||
name: translate('DefaultNameCopiedProfile', {
|
||||
name: cloneSource.name,
|
||||
}),
|
||||
};
|
||||
}
|
||||
|
||||
return DEFAULT_AUTO_TAGGING;
|
||||
}, [cloneId, cloneSource]);
|
||||
|
||||
const manage = useManageProviderSettings<AutoTagging>(
|
||||
id,
|
||||
defaultProvider,
|
||||
PATH
|
||||
);
|
||||
|
||||
const specifications = useMemo(
|
||||
() =>
|
||||
manage.item.specifications.value.map((spec, i) => ({
|
||||
...spec,
|
||||
id: spec.id ?? i + 1,
|
||||
})),
|
||||
[manage.item.specifications.value]
|
||||
);
|
||||
|
||||
const saveSpecification = useCallback(
|
||||
(spec: AutoTaggingSpecification) => {
|
||||
if (spec.id > 0 && specifications.some((s) => s.id === spec.id)) {
|
||||
manage.updateValue(
|
||||
'specifications',
|
||||
specifications.map((s) => (s.id === spec.id ? spec : s))
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
const newId = getNextSpecId(specifications);
|
||||
|
||||
manage.updateValue('specifications', [
|
||||
...specifications,
|
||||
{ ...spec, id: newId },
|
||||
]);
|
||||
},
|
||||
[specifications, manage]
|
||||
);
|
||||
|
||||
const deleteSpecification = useCallback(
|
||||
(specId: number) => {
|
||||
manage.updateValue(
|
||||
'specifications',
|
||||
specifications.filter((s) => s.id !== specId)
|
||||
);
|
||||
},
|
||||
[specifications, manage]
|
||||
);
|
||||
|
||||
const cloneSpecification = useCallback(
|
||||
(specId: number) => {
|
||||
const spec = specifications.find((s) => s.id === specId);
|
||||
|
||||
if (!spec) {
|
||||
return;
|
||||
}
|
||||
|
||||
const newId = getNextSpecId(specifications);
|
||||
|
||||
manage.updateValue('specifications', [
|
||||
...specifications,
|
||||
{
|
||||
...spec,
|
||||
id: newId,
|
||||
name: translate('DefaultNameCopiedSpecification', {
|
||||
name: spec.name,
|
||||
}),
|
||||
},
|
||||
]);
|
||||
},
|
||||
[specifications, manage]
|
||||
);
|
||||
|
||||
return {
|
||||
...manage,
|
||||
saveAutoTagging: manage.saveProvider,
|
||||
specifications,
|
||||
saveSpecification,
|
||||
deleteSpecification,
|
||||
cloneSpecification,
|
||||
};
|
||||
};
|
||||
|
|
@ -15,6 +15,7 @@ import useSeries from 'Series/useSeries';
|
|||
import { useIndexersWithIds } from 'Settings/Indexers/useIndexers';
|
||||
import { useConnectionsWithIds } from 'Settings/Notifications/useConnections';
|
||||
import { useReleaseProfilesWithIds } from 'Settings/Profiles/Release/useReleaseProfiles';
|
||||
import { useAutoTaggingsWithIds } from 'Settings/Tags/AutoTagging/useAutoTaggings';
|
||||
import translate from 'Utilities/String/translate';
|
||||
import TagDetailsDelayProfile from './TagDetailsDelayProfile';
|
||||
import styles from './TagDetailsModalContent.css';
|
||||
|
|
@ -109,12 +110,7 @@ function TagDetailsModalContent({
|
|||
)
|
||||
);
|
||||
|
||||
const autoTags = useSelector(
|
||||
createMatchingItemSelector(
|
||||
autoTagIds,
|
||||
(state: AppState) => state.settings.autoTaggings.items
|
||||
)
|
||||
);
|
||||
const autoTags = useAutoTaggingsWithIds(autoTagIds);
|
||||
|
||||
return (
|
||||
<ModalContent onModalClose={onModalClose}>
|
||||
|
|
|
|||
|
|
@ -1,194 +0,0 @@
|
|||
import { createAction } from 'redux-actions';
|
||||
import { batchActions } from 'redux-batched-actions';
|
||||
import createFetchSchemaHandler from 'Store/Actions/Creators/createFetchSchemaHandler';
|
||||
import createClearReducer from 'Store/Actions/Creators/Reducers/createClearReducer';
|
||||
import createSetProviderFieldValueReducer from 'Store/Actions/Creators/Reducers/createSetProviderFieldValueReducer';
|
||||
import createSetSettingValueReducer from 'Store/Actions/Creators/Reducers/createSetSettingValueReducer';
|
||||
import { createThunk } from 'Store/thunks';
|
||||
import getNextId from 'Utilities/State/getNextId';
|
||||
import getProviderState from 'Utilities/State/getProviderState';
|
||||
import getSectionState from 'Utilities/State/getSectionState';
|
||||
import selectProviderSchema from 'Utilities/State/selectProviderSchema';
|
||||
import updateSectionState from 'Utilities/State/updateSectionState';
|
||||
import translate from 'Utilities/String/translate';
|
||||
import { removeItem, set, update, updateItem } from '../baseActions';
|
||||
|
||||
//
|
||||
// Variables
|
||||
|
||||
const section = 'settings.autoTaggingSpecifications';
|
||||
|
||||
//
|
||||
// Actions Types
|
||||
|
||||
export const FETCH_AUTO_TAGGING_SPECIFICATIONS = 'settings/autoTaggingSpecifications/fetchAutoTaggingSpecifications';
|
||||
export const FETCH_AUTO_TAGGING_SPECIFICATION_SCHEMA = 'settings/autoTaggingSpecifications/fetchAutoTaggingSpecificationSchema';
|
||||
export const SELECT_AUTO_TAGGING_SPECIFICATION_SCHEMA = 'settings/autoTaggingSpecifications/selectAutoTaggingSpecificationSchema';
|
||||
export const SET_AUTO_TAGGING_SPECIFICATION_VALUE = 'settings/autoTaggingSpecifications/setAutoTaggingSpecificationValue';
|
||||
export const SET_AUTO_TAGGING_SPECIFICATION_FIELD_VALUE = 'settings/autoTaggingSpecifications/setAutoTaggingSpecificationFieldValue';
|
||||
export const SAVE_AUTO_TAGGING_SPECIFICATION = 'settings/autoTaggingSpecifications/saveAutoTaggingSpecification';
|
||||
export const DELETE_AUTO_TAGGING_SPECIFICATION = 'settings/autoTaggingSpecifications/deleteAutoTaggingSpecification';
|
||||
export const DELETE_ALL_AUTO_TAGGING_SPECIFICATION = 'settings/autoTaggingSpecifications/deleteAllAutoTaggingSpecification';
|
||||
export const CLONE_AUTO_TAGGING_SPECIFICATION = 'settings/autoTaggingSpecifications/cloneAutoTaggingSpecification';
|
||||
export const CLEAR_AUTO_TAGGING_SPECIFICATIONS = 'settings/autoTaggingSpecifications/clearAutoTaggingSpecifications';
|
||||
export const CLEAR_AUTO_TAGGING_SPECIFICATION_PENDING = 'settings/autoTaggingSpecifications/clearAutoTaggingSpecificationPending';
|
||||
//
|
||||
// Action Creators
|
||||
|
||||
export const fetchAutoTaggingSpecifications = createThunk(FETCH_AUTO_TAGGING_SPECIFICATIONS);
|
||||
export const fetchAutoTaggingSpecificationSchema = createThunk(FETCH_AUTO_TAGGING_SPECIFICATION_SCHEMA);
|
||||
export const selectAutoTaggingSpecificationSchema = createAction(SELECT_AUTO_TAGGING_SPECIFICATION_SCHEMA);
|
||||
|
||||
export const saveAutoTaggingSpecification = createThunk(SAVE_AUTO_TAGGING_SPECIFICATION);
|
||||
export const deleteAutoTaggingSpecification = createThunk(DELETE_AUTO_TAGGING_SPECIFICATION);
|
||||
export const deleteAllAutoTaggingSpecification = createThunk(DELETE_ALL_AUTO_TAGGING_SPECIFICATION);
|
||||
|
||||
export const setAutoTaggingSpecificationValue = createAction(SET_AUTO_TAGGING_SPECIFICATION_VALUE, (payload) => {
|
||||
return {
|
||||
section,
|
||||
...payload
|
||||
};
|
||||
});
|
||||
|
||||
export const setAutoTaggingSpecificationFieldValue = createAction(SET_AUTO_TAGGING_SPECIFICATION_FIELD_VALUE, (payload) => {
|
||||
return {
|
||||
section,
|
||||
...payload
|
||||
};
|
||||
});
|
||||
|
||||
export const cloneAutoTaggingSpecification = createAction(CLONE_AUTO_TAGGING_SPECIFICATION);
|
||||
|
||||
export const clearAutoTaggingSpecification = createAction(CLEAR_AUTO_TAGGING_SPECIFICATIONS);
|
||||
|
||||
export const clearAutoTaggingSpecificationPending = createThunk(CLEAR_AUTO_TAGGING_SPECIFICATION_PENDING);
|
||||
|
||||
//
|
||||
// Details
|
||||
|
||||
export default {
|
||||
|
||||
//
|
||||
// State
|
||||
|
||||
defaultState: {
|
||||
isPopulated: false,
|
||||
error: null,
|
||||
isSchemaFetching: false,
|
||||
isSchemaPopulated: false,
|
||||
schemaError: null,
|
||||
schema: [],
|
||||
selectedSchema: {},
|
||||
isSaving: false,
|
||||
saveError: null,
|
||||
items: [],
|
||||
pendingChanges: {}
|
||||
},
|
||||
|
||||
//
|
||||
// Action Handlers
|
||||
|
||||
actionHandlers: {
|
||||
[FETCH_AUTO_TAGGING_SPECIFICATION_SCHEMA]: createFetchSchemaHandler(section, '/autoTagging/schema'),
|
||||
|
||||
[FETCH_AUTO_TAGGING_SPECIFICATIONS]: (getState, payload, dispatch) => {
|
||||
let tags = [];
|
||||
if (payload.id) {
|
||||
const cfState = getSectionState(getState(), 'settings.autoTaggings', true);
|
||||
const cf = cfState.items[cfState.itemMap[payload.id]];
|
||||
tags = cf.specifications.map((tag, i) => {
|
||||
return {
|
||||
id: i + 1,
|
||||
...tag
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
dispatch(batchActions([
|
||||
update({ section, data: tags }),
|
||||
set({
|
||||
section,
|
||||
isPopulated: true
|
||||
})
|
||||
]));
|
||||
},
|
||||
|
||||
[SAVE_AUTO_TAGGING_SPECIFICATION]: (getState, payload, dispatch) => {
|
||||
const {
|
||||
id,
|
||||
...otherPayload
|
||||
} = payload;
|
||||
|
||||
const saveData = getProviderState({ id, ...otherPayload }, getState, section, false);
|
||||
|
||||
// we have to set id since not actually posting to server yet
|
||||
if (!saveData.id) {
|
||||
saveData.id = getNextId(getState().settings.autoTaggingSpecifications.items);
|
||||
}
|
||||
|
||||
dispatch(batchActions([
|
||||
updateItem({ section, ...saveData }),
|
||||
set({
|
||||
section,
|
||||
pendingChanges: {}
|
||||
})
|
||||
]));
|
||||
},
|
||||
|
||||
[DELETE_AUTO_TAGGING_SPECIFICATION]: (getState, payload, dispatch) => {
|
||||
const id = payload.id;
|
||||
return dispatch(removeItem({ section, id }));
|
||||
},
|
||||
|
||||
[DELETE_ALL_AUTO_TAGGING_SPECIFICATION]: (getState, payload, dispatch) => {
|
||||
return dispatch(set({
|
||||
section,
|
||||
items: []
|
||||
}));
|
||||
},
|
||||
|
||||
[CLEAR_AUTO_TAGGING_SPECIFICATION_PENDING]: (getState, payload, dispatch) => {
|
||||
return dispatch(set({
|
||||
section,
|
||||
pendingChanges: {}
|
||||
}));
|
||||
}
|
||||
},
|
||||
|
||||
//
|
||||
// Reducers
|
||||
|
||||
reducers: {
|
||||
[SET_AUTO_TAGGING_SPECIFICATION_VALUE]: createSetSettingValueReducer(section),
|
||||
[SET_AUTO_TAGGING_SPECIFICATION_FIELD_VALUE]: createSetProviderFieldValueReducer(section),
|
||||
|
||||
[SELECT_AUTO_TAGGING_SPECIFICATION_SCHEMA]: (state, { payload }) => {
|
||||
return selectProviderSchema(state, section, payload, (selectedSchema) => {
|
||||
return selectedSchema;
|
||||
});
|
||||
},
|
||||
|
||||
[CLONE_AUTO_TAGGING_SPECIFICATION]: function(state, { payload }) {
|
||||
const id = payload.id;
|
||||
const newState = getSectionState(state, section);
|
||||
const items = newState.items;
|
||||
const item = items.find((i) => i.id === id);
|
||||
const newId = getNextId(newState.items);
|
||||
const newItem = {
|
||||
...item,
|
||||
id: newId,
|
||||
name: translate('DefaultNameCopiedSpecification', { name: item.name })
|
||||
};
|
||||
newState.items = [...items, newItem];
|
||||
newState.itemMap[newId] = newState.items.length - 1;
|
||||
|
||||
return updateSectionState(state, section, newState);
|
||||
},
|
||||
|
||||
[CLEAR_AUTO_TAGGING_SPECIFICATIONS]: createClearReducer(section, {
|
||||
isPopulated: false,
|
||||
error: null,
|
||||
items: []
|
||||
})
|
||||
}
|
||||
};
|
||||
|
|
@ -1,110 +0,0 @@
|
|||
import { createAction } from 'redux-actions';
|
||||
import { set } from 'Store/Actions/baseActions';
|
||||
import createFetchHandler from 'Store/Actions/Creators/createFetchHandler';
|
||||
import createRemoveItemHandler from 'Store/Actions/Creators/createRemoveItemHandler';
|
||||
import createSaveProviderHandler from 'Store/Actions/Creators/createSaveProviderHandler';
|
||||
import createSetSettingValueReducer from 'Store/Actions/Creators/Reducers/createSetSettingValueReducer';
|
||||
import { createThunk } from 'Store/thunks';
|
||||
import getSectionState from 'Utilities/State/getSectionState';
|
||||
import updateSectionState from 'Utilities/State/updateSectionState';
|
||||
import translate from 'Utilities/String/translate';
|
||||
|
||||
//
|
||||
// Variables
|
||||
|
||||
const section = 'settings.autoTaggings';
|
||||
|
||||
//
|
||||
// Actions Types
|
||||
|
||||
export const FETCH_AUTO_TAGGINGS = 'settings/autoTaggings/fetchAutoTaggings';
|
||||
export const SAVE_AUTO_TAGGING = 'settings/autoTaggings/saveAutoTagging';
|
||||
export const DELETE_AUTO_TAGGING = 'settings/autoTaggings/deleteAutoTagging';
|
||||
export const SET_AUTO_TAGGING_VALUE = 'settings/autoTaggings/setAutoTaggingValue';
|
||||
export const CLONE_AUTO_TAGGING = 'settings/autoTaggings/cloneAutoTagging';
|
||||
|
||||
//
|
||||
// Action Creators
|
||||
|
||||
export const fetchAutoTaggings = createThunk(FETCH_AUTO_TAGGINGS);
|
||||
export const saveAutoTagging = createThunk(SAVE_AUTO_TAGGING);
|
||||
export const deleteAutoTagging = createThunk(DELETE_AUTO_TAGGING);
|
||||
|
||||
export const setAutoTaggingValue = createAction(SET_AUTO_TAGGING_VALUE, (payload) => {
|
||||
return {
|
||||
section,
|
||||
...payload
|
||||
};
|
||||
});
|
||||
|
||||
export const cloneAutoTagging = createAction(CLONE_AUTO_TAGGING);
|
||||
|
||||
//
|
||||
// Details
|
||||
|
||||
export default {
|
||||
|
||||
//
|
||||
// State
|
||||
|
||||
defaultState: {
|
||||
isSchemaFetching: false,
|
||||
isSchemaPopulated: false,
|
||||
isFetching: false,
|
||||
isPopulated: false,
|
||||
schema: {
|
||||
removeTagsAutomatically: false,
|
||||
tags: []
|
||||
},
|
||||
error: null,
|
||||
isDeleting: false,
|
||||
deleteError: null,
|
||||
isSaving: false,
|
||||
saveError: null,
|
||||
items: [],
|
||||
pendingChanges: {}
|
||||
},
|
||||
|
||||
//
|
||||
// Action Handlers
|
||||
|
||||
actionHandlers: {
|
||||
[FETCH_AUTO_TAGGINGS]: createFetchHandler(section, '/autoTagging'),
|
||||
|
||||
[DELETE_AUTO_TAGGING]: createRemoveItemHandler(section, '/autoTagging'),
|
||||
|
||||
[SAVE_AUTO_TAGGING]: (getState, payload, dispatch) => {
|
||||
// move the format tags in as a pending change
|
||||
const state = getState();
|
||||
const pendingChanges = state.settings.autoTaggings.pendingChanges;
|
||||
pendingChanges.specifications = state.settings.autoTaggingSpecifications.items;
|
||||
dispatch(set({
|
||||
section,
|
||||
pendingChanges
|
||||
}));
|
||||
|
||||
createSaveProviderHandler(section, '/autoTagging')(getState, payload, dispatch);
|
||||
}
|
||||
},
|
||||
|
||||
//
|
||||
// Reducers
|
||||
|
||||
reducers: {
|
||||
[SET_AUTO_TAGGING_VALUE]: createSetSettingValueReducer(section),
|
||||
|
||||
[CLONE_AUTO_TAGGING]: function(state, { payload }) {
|
||||
const id = payload.id;
|
||||
const newState = getSectionState(state, section);
|
||||
const item = newState.items.find((i) => i.id === id);
|
||||
const pendingChanges = { ...item, id: 0 };
|
||||
delete pendingChanges.id;
|
||||
|
||||
pendingChanges.name = translate('DefaultNameCopiedProfile', { name: pendingChanges.name });
|
||||
newState.pendingChanges = pendingChanges;
|
||||
|
||||
return updateSectionState(state, section, newState);
|
||||
}
|
||||
}
|
||||
|
||||
};
|
||||
|
|
@ -1,7 +1,5 @@
|
|||
import { handleThunks } from 'Store/thunks';
|
||||
import createHandleActions from './Creators/createHandleActions';
|
||||
import autoTaggings from './Settings/autoTaggings';
|
||||
import autoTaggingSpecifications from './Settings/autoTaggingSpecifications';
|
||||
import customFormats from './Settings/customFormats';
|
||||
import customFormatSpecifications from './Settings/customFormatSpecifications';
|
||||
import delayProfiles from './Settings/delayProfiles';
|
||||
|
|
@ -10,8 +8,6 @@ import downloadClients from './Settings/downloadClients';
|
|||
import importListOptions from './Settings/importListOptions';
|
||||
import importLists from './Settings/importLists';
|
||||
|
||||
export * from './Settings/autoTaggingSpecifications';
|
||||
export * from './Settings/autoTaggings';
|
||||
export * from './Settings/customFormatSpecifications.js';
|
||||
export * from './Settings/customFormats';
|
||||
export * from './Settings/delayProfiles';
|
||||
|
|
@ -30,8 +26,6 @@ export const section = 'settings';
|
|||
|
||||
export const defaultState = {
|
||||
advancedSettings: false,
|
||||
autoTaggingSpecifications: autoTaggingSpecifications.defaultState,
|
||||
autoTaggings: autoTaggings.defaultState,
|
||||
customFormatSpecifications: customFormatSpecifications.defaultState,
|
||||
customFormats: customFormats.defaultState,
|
||||
delayProfiles: delayProfiles.defaultState,
|
||||
|
|
@ -49,8 +43,6 @@ export const persistState = [
|
|||
// Action Handlers
|
||||
|
||||
export const actionHandlers = handleThunks({
|
||||
...autoTaggingSpecifications.actionHandlers,
|
||||
...autoTaggings.actionHandlers,
|
||||
...customFormatSpecifications.actionHandlers,
|
||||
...customFormats.actionHandlers,
|
||||
...delayProfiles.actionHandlers,
|
||||
|
|
@ -64,8 +56,6 @@ export const actionHandlers = handleThunks({
|
|||
// Reducers
|
||||
|
||||
export const reducers = createHandleActions({
|
||||
...autoTaggingSpecifications.reducers,
|
||||
...autoTaggings.reducers,
|
||||
...customFormatSpecifications.reducers,
|
||||
...customFormats.reducers,
|
||||
...delayProfiles.reducers,
|
||||
|
|
|
|||
|
|
@ -1,21 +0,0 @@
|
|||
import ModelBase from 'App/ModelBase';
|
||||
import Field from './Field';
|
||||
|
||||
export interface AutoTaggingSpecification {
|
||||
id: number;
|
||||
name: string;
|
||||
implementation: string;
|
||||
implementationName: string;
|
||||
negate: boolean;
|
||||
required: boolean;
|
||||
fields: Field[];
|
||||
}
|
||||
|
||||
interface AutoTagging extends ModelBase {
|
||||
name: string;
|
||||
removeTagsAutomatically: boolean;
|
||||
tags: number[];
|
||||
specifications: AutoTaggingSpecification[];
|
||||
}
|
||||
|
||||
export default AutoTagging;
|
||||
114
src/Sonarr.Api.V5/AutoTagging/AutoTaggingController.cs
Normal file
114
src/Sonarr.Api.V5/AutoTagging/AutoTaggingController.cs
Normal file
|
|
@ -0,0 +1,114 @@
|
|||
using FluentValidation;
|
||||
using FluentValidation.Results;
|
||||
using Microsoft.AspNetCore.Http;
|
||||
using Microsoft.AspNetCore.Http.HttpResults;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using NzbDrone.Common.Extensions;
|
||||
using NzbDrone.Core.AutoTagging;
|
||||
using NzbDrone.Core.AutoTagging.Specifications;
|
||||
using NzbDrone.Core.Validation;
|
||||
using Sonarr.Http;
|
||||
using Sonarr.Http.REST;
|
||||
using Sonarr.Http.REST.Attributes;
|
||||
|
||||
namespace Sonarr.Api.V5.AutoTagging;
|
||||
|
||||
[V5ApiController]
|
||||
public class AutoTaggingController : RestController<AutoTaggingResource>
|
||||
{
|
||||
private readonly IAutoTaggingService _autoTaggingService;
|
||||
private readonly List<IAutoTaggingSpecification> _specifications;
|
||||
|
||||
public AutoTaggingController(IAutoTaggingService autoTaggingService,
|
||||
List<IAutoTaggingSpecification> specifications)
|
||||
{
|
||||
_autoTaggingService = autoTaggingService;
|
||||
_specifications = specifications;
|
||||
|
||||
SharedValidator.RuleFor(c => c.Name).NotEmpty();
|
||||
SharedValidator.RuleFor(c => c.Name)
|
||||
.Must((v, c) => !_autoTaggingService.All().Any(f => f.Name == c && f.Id != v.Id)).WithMessage("Must be unique.");
|
||||
SharedValidator.RuleFor(c => c.Tags).NotEmpty();
|
||||
SharedValidator.RuleFor(c => c).Custom((autoTag, context) =>
|
||||
{
|
||||
if (!autoTag.Specifications.Any())
|
||||
{
|
||||
context.AddFailure("Must contain at least one Condition");
|
||||
}
|
||||
|
||||
if (autoTag.Specifications.Any(s => s.Name.IsNullOrWhiteSpace()))
|
||||
{
|
||||
context.AddFailure("Condition name(s) cannot be empty or consist of only spaces");
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
protected override AutoTaggingResource GetResourceById(int id)
|
||||
{
|
||||
return _autoTaggingService.GetById(id).ToResource();
|
||||
}
|
||||
|
||||
[HttpGet]
|
||||
[Produces("application/json")]
|
||||
public List<AutoTaggingResource> GetAll()
|
||||
{
|
||||
return _autoTaggingService.All().ToResource();
|
||||
}
|
||||
|
||||
[RestPostById]
|
||||
[Consumes("application/json")]
|
||||
public ActionResult<AutoTaggingResource> Create([FromBody] AutoTaggingResource autoTagResource)
|
||||
{
|
||||
var model = autoTagResource.ToModel(_specifications);
|
||||
|
||||
Validate(model);
|
||||
|
||||
return Created(_autoTaggingService.Insert(model).Id);
|
||||
}
|
||||
|
||||
[RestPutById]
|
||||
[Consumes("application/json")]
|
||||
public ActionResult<AutoTaggingResource> Update([FromBody] AutoTaggingResource resource)
|
||||
{
|
||||
var model = resource.ToModel(_specifications);
|
||||
|
||||
Validate(model);
|
||||
|
||||
_autoTaggingService.Update(model);
|
||||
|
||||
return Accepted(model.Id);
|
||||
}
|
||||
|
||||
[RestDeleteById]
|
||||
public NoContent DeleteAutoTagging(int id)
|
||||
{
|
||||
_autoTaggingService.Delete(id);
|
||||
|
||||
return TypedResults.NoContent();
|
||||
}
|
||||
|
||||
[HttpGet("schema")]
|
||||
[Produces("application/json")]
|
||||
public List<AutoTaggingSpecificationSchema> GetTemplates()
|
||||
{
|
||||
return _specifications.OrderBy(x => x.Order).Select(x => x.ToSchema()).ToList();
|
||||
}
|
||||
|
||||
private void Validate(AutoTag definition)
|
||||
{
|
||||
foreach (var validationResult in definition.Specifications.Select(spec => spec.Validate()))
|
||||
{
|
||||
VerifyValidationResult(validationResult);
|
||||
}
|
||||
}
|
||||
|
||||
private void VerifyValidationResult(ValidationResult validationResult)
|
||||
{
|
||||
var result = new NzbDroneValidationResult(validationResult.Errors);
|
||||
|
||||
if (!result.IsValid)
|
||||
{
|
||||
throw new ValidationException(result.Errors);
|
||||
}
|
||||
}
|
||||
}
|
||||
72
src/Sonarr.Api.V5/AutoTagging/AutoTaggingResource.cs
Normal file
72
src/Sonarr.Api.V5/AutoTagging/AutoTaggingResource.cs
Normal file
|
|
@ -0,0 +1,72 @@
|
|||
using System.Text.Json.Serialization;
|
||||
using NzbDrone.Core.AutoTagging;
|
||||
using NzbDrone.Core.AutoTagging.Specifications;
|
||||
using Sonarr.Http.ClientSchema;
|
||||
using Sonarr.Http.REST;
|
||||
|
||||
namespace Sonarr.Api.V5.AutoTagging;
|
||||
|
||||
public class AutoTaggingResource : RestResource
|
||||
{
|
||||
[JsonIgnore(Condition = JsonIgnoreCondition.Never)]
|
||||
public override int Id { get; set; }
|
||||
public string? Name { get; set; }
|
||||
public bool RemoveTagsAutomatically { get; set; }
|
||||
public HashSet<int> Tags { get; set; } = [];
|
||||
public List<AutoTaggingSpecificationSchema> Specifications { get; set; } = [];
|
||||
}
|
||||
|
||||
public static class AutoTaggingResourceMapper
|
||||
{
|
||||
public static AutoTaggingResource ToResource(this AutoTag model)
|
||||
{
|
||||
return new AutoTaggingResource
|
||||
{
|
||||
Id = model.Id,
|
||||
Name = model.Name,
|
||||
RemoveTagsAutomatically = model.RemoveTagsAutomatically,
|
||||
Tags = model.Tags,
|
||||
Specifications = model.Specifications.Select(x => x.ToSchema()).ToList()
|
||||
};
|
||||
}
|
||||
|
||||
public static List<AutoTaggingResource> ToResource(this IEnumerable<AutoTag> models)
|
||||
{
|
||||
return models.Select(m => m.ToResource()).ToList();
|
||||
}
|
||||
|
||||
public static AutoTag ToModel(this AutoTaggingResource resource, List<IAutoTaggingSpecification> specifications)
|
||||
{
|
||||
return new AutoTag
|
||||
{
|
||||
Id = resource.Id,
|
||||
Name = resource.Name,
|
||||
RemoveTagsAutomatically = resource.RemoveTagsAutomatically,
|
||||
Tags = resource.Tags,
|
||||
Specifications = resource.Specifications.Select(x => MapSpecification(x, specifications)).ToList()
|
||||
};
|
||||
}
|
||||
|
||||
private static IAutoTaggingSpecification MapSpecification(AutoTaggingSpecificationSchema resource, List<IAutoTaggingSpecification> specifications)
|
||||
{
|
||||
var matchingSpec =
|
||||
specifications.SingleOrDefault(x => x.GetType().Name == resource.Implementation);
|
||||
|
||||
if (matchingSpec is null)
|
||||
{
|
||||
throw new ArgumentException(
|
||||
$"{resource.Implementation} is not a valid specification implementation");
|
||||
}
|
||||
|
||||
var type = matchingSpec.GetType();
|
||||
|
||||
// Finding the exact current specification isn't possible given the dynamic nature of them and the possibility that multiple
|
||||
// of the same type exist within the same format. Passing in null is safe as long as there never exists a specification that
|
||||
// relies on additional privacy.
|
||||
var spec = (IAutoTaggingSpecification)SchemaBuilder.ReadFromSchema(resource.Fields, type, null);
|
||||
spec.Name = resource.Name;
|
||||
spec.Negate = resource.Negate;
|
||||
spec.Required = resource.Required;
|
||||
return spec;
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,31 @@
|
|||
using NzbDrone.Core.AutoTagging.Specifications;
|
||||
using Sonarr.Http.ClientSchema;
|
||||
using Sonarr.Http.REST;
|
||||
|
||||
namespace Sonarr.Api.V5.AutoTagging;
|
||||
|
||||
public class AutoTaggingSpecificationSchema : RestResource
|
||||
{
|
||||
public string? Name { get; set; }
|
||||
public string? Implementation { get; set; }
|
||||
public string? ImplementationName { get; set; }
|
||||
public bool Negate { get; set; }
|
||||
public bool Required { get; set; }
|
||||
public List<Field> Fields { get; set; } = [];
|
||||
}
|
||||
|
||||
public static class AutoTaggingSpecificationSchemaMapper
|
||||
{
|
||||
public static AutoTaggingSpecificationSchema ToSchema(this IAutoTaggingSpecification model)
|
||||
{
|
||||
return new AutoTaggingSpecificationSchema
|
||||
{
|
||||
Name = model.Name,
|
||||
Implementation = model.GetType().Name,
|
||||
ImplementationName = model.ImplementationName,
|
||||
Negate = model.Negate,
|
||||
Required = model.Required,
|
||||
Fields = SchemaBuilder.ToSchema(model)
|
||||
};
|
||||
}
|
||||
}
|
||||
Loading…
Reference in a new issue