1
0
mirror of https://github.com/Sonarr/Sonarr.git synced 2026-03-28 18:04:19 -04:00

Compare commits

..

1 Commits

Author SHA1 Message Date
Sonarr
d0be12eef1 Automated API Docs update
ignore-downstream
2026-03-23 00:28:44 +00:00
18 changed files with 572 additions and 504 deletions

View File

@@ -4,6 +4,7 @@ import AppSectionState, {
AppSectionListState,
AppSectionSaveState,
AppSectionSchemaState,
PagedAppSectionState,
} from 'App/State/AppSectionState';
import AutoTagging, { AutoTaggingSpecification } from 'typings/AutoTagging';
import CustomFormat from 'typings/CustomFormat';
@@ -11,6 +12,7 @@ import CustomFormatSpecification from 'typings/CustomFormatSpecification';
import DelayProfile from 'typings/DelayProfile';
import DownloadClient from 'typings/DownloadClient';
import ImportList from 'typings/ImportList';
import ImportListExclusion from 'typings/ImportListExclusion';
import ImportListOptionsSettings from 'typings/ImportListOptionsSettings';
import DownloadClientOptions from 'typings/Settings/DownloadClientOptions';
@@ -69,6 +71,14 @@ export interface ImportListOptionsSettingsAppState
extends AppSectionItemState<ImportListOptionsSettings>,
AppSectionSaveState {}
export interface ImportListExclusionsSettingsAppState
extends AppSectionState<ImportListExclusion>,
AppSectionSaveState,
PagedAppSectionState,
AppSectionDeleteState {
pendingChanges: Partial<ImportListExclusion>;
}
interface SettingsAppState {
autoTaggings: AutoTaggingAppState;
autoTaggingSpecifications: AutoTaggingSpecificationAppState;
@@ -77,6 +87,7 @@ interface SettingsAppState {
delayProfiles: DelayProfileAppState;
downloadClients: DownloadClientAppState;
downloadClientOptions: DownloadClientOptionsAppState;
importListExclusions: ImportListExclusionsSettingsAppState;
importListOptions: ImportListOptionsSettingsAppState;
importLists: ImportListAppState;
}

View File

@@ -7,7 +7,6 @@ interface PageStore {
cutoffUnmet: number;
events: number;
history: number;
importListExclusion: number;
missing: number;
queue: number;
}
@@ -17,7 +16,6 @@ const pageStore = create<PageStore>(() => ({
cutoffUnmet: 1,
events: 1,
history: 1,
importListExclusion: 1,
missing: 1,
queue: 1,
}));

View File

@@ -1,12 +1,12 @@
import React from 'react';
import React, { useCallback } from 'react';
import { useDispatch } from 'react-redux';
import Modal from 'Components/Modal/Modal';
import { sizes } from 'Helpers/Props';
import { clearPendingChanges } from 'Store/Actions/baseActions';
import EditImportListExclusionModalContent from './EditImportListExclusionModalContent';
interface EditImportListExclusionModalProps {
id?: number;
title?: string;
tvdbId?: number;
isOpen: boolean;
onModalClose: () => void;
onDeleteImportListExclusionPress?: () => void;
@@ -17,11 +17,22 @@ function EditImportListExclusionModal(
) {
const { isOpen, onModalClose, ...otherProps } = props;
const dispatch = useDispatch();
const handleModalClose = useCallback(() => {
dispatch(
clearPendingChanges({
section: 'settings.importListExclusions',
})
);
onModalClose();
}, [dispatch, onModalClose]);
return (
<Modal size={sizes.MEDIUM} isOpen={isOpen} onModalClose={onModalClose}>
<Modal size={sizes.MEDIUM} isOpen={isOpen} onModalClose={handleModalClose}>
<EditImportListExclusionModalContent
{...otherProps}
onModalClose={onModalClose}
onModalClose={handleModalClose}
/>
</Modal>
);

View File

@@ -1,3 +1,9 @@
.body {
composes: modalBody from '~Components/Modal/ModalBody.css';
flex: 1 1 430px;
}
.deleteButton {
composes: button from '~Components/Link/Button.css';

View File

@@ -1,6 +1,7 @@
// This file is automatically generated.
// Please do not change this file!
interface CssExports {
'body': string;
'deleteButton': string;
}
export const cssExports: CssExports;

View File

@@ -1,70 +1,115 @@
import React, { useCallback, useEffect } from 'react';
import { useDispatch, useSelector } from 'react-redux';
import { createSelector } from 'reselect';
import AppState from 'App/State/AppState';
import Alert from 'Components/Alert';
import Form from 'Components/Form/Form';
import FormGroup from 'Components/Form/FormGroup';
import FormInputGroup from 'Components/Form/FormInputGroup';
import FormLabel from 'Components/Form/FormLabel';
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 { inputTypes, kinds } from 'Helpers/Props';
import {
saveImportListExclusion,
setImportListExclusionValue,
} from 'Store/Actions/settingsActions';
import selectSettings from 'Store/Selectors/selectSettings';
import ImportListExclusion from 'typings/ImportListExclusion';
import { InputChanged } from 'typings/inputs';
import { PendingSection } from 'typings/pending';
import translate from 'Utilities/String/translate';
import { useManageImportListExclusion } from './useImportListExclusions';
import styles from './EditImportListExclusionModalContent.css';
const newImportListExclusion = {
title: '',
tvdbId: 0,
};
function createImportListExclusionSelector(id?: number) {
return createSelector(
(state: AppState) => state.settings.importListExclusions,
(importListExclusions) => {
const { isFetching, error, isSaving, saveError, pendingChanges, items } =
importListExclusions;
const mapping = id
? items.find((i) => i.id === id)!
: newImportListExclusion;
const settings = selectSettings(mapping, pendingChanges, saveError);
return {
isFetching,
error,
isSaving,
saveError,
item: settings.settings as PendingSection<ImportListExclusion>,
...settings,
};
}
);
}
interface EditImportListExclusionModalContentProps {
id?: number;
title?: string;
tvdbId?: number;
onModalClose: () => void;
onDeleteImportListExclusionPress?: () => void;
}
function EditImportListExclusionModalContent({
id,
title: existingTitle,
tvdbId: existingTvdbId,
onModalClose,
onDeleteImportListExclusionPress,
}: EditImportListExclusionModalContentProps) {
const {
item,
isSaving,
saveError,
validationErrors,
validationWarnings,
updateValue,
save,
} = useManageImportListExclusion({
id,
title: existingTitle,
tvdbId: existingTvdbId,
});
const { isFetching, isSaving, item, error, saveError, ...otherProps } =
useSelector(createImportListExclusionSelector(id));
const { title, tvdbId } = item;
const wasSaving = usePrevious(isSaving);
const dispatch = useDispatch();
const previousIsSaving = usePrevious(isSaving);
const dispatchSetImportListExclusionValue = (payload: {
name: string;
value: string | number;
}) => {
// @ts-expect-error 'setImportListExclusionValue' isn't typed yet
dispatch(setImportListExclusionValue(payload));
};
useEffect(() => {
if (wasSaving && !isSaving && !saveError) {
if (!id) {
Object.entries(newImportListExclusion).forEach(([name, value]) => {
dispatchSetImportListExclusionValue({ name, value });
});
}
// eslint-disable-next-line react-hooks/exhaustive-deps
}, []);
useEffect(() => {
if (previousIsSaving && !isSaving && !saveError) {
onModalClose();
}
}, [isSaving, wasSaving, saveError, onModalClose]);
}, [previousIsSaving, isSaving, saveError, onModalClose]);
const handleInputChange = useCallback(
({ name, value }: InputChanged) => {
updateValue(name, value);
const onSavePress = useCallback(() => {
dispatch(saveImportListExclusion({ id }));
}, [dispatch, id]);
const onInputChange = useCallback(
(change: InputChanged) => {
// @ts-expect-error 'setImportListExclusionValue' isn't typed yet
dispatch(setImportListExclusionValue(change));
},
[updateValue]
[dispatch]
);
const handleSavePress = useCallback(() => {
save();
}, [save]);
return (
<ModalContent onModalClose={onModalClose}>
<ModalHeader>
@@ -73,35 +118,42 @@ function EditImportListExclusionModalContent({
: translate('AddImportListExclusion')}
</ModalHeader>
<ModalBody>
<Form
validationErrors={validationErrors}
validationWarnings={validationWarnings}
>
<FormGroup>
<FormLabel>{translate('Title')}</FormLabel>
<ModalBody className={styles.body}>
{isFetching ? <LoadingIndicator /> : null}
<FormInputGroup
type={inputTypes.TEXT}
name="title"
helpText={translate('SeriesTitleToExcludeHelpText')}
{...title}
onChange={handleInputChange}
/>
</FormGroup>
{!isFetching && error ? (
<Alert kind={kinds.DANGER}>
{translate('AddImportListExclusionError')}
</Alert>
) : null}
<FormGroup>
<FormLabel>{translate('TvdbId')}</FormLabel>
{!isFetching && !error ? (
<Form {...otherProps}>
<FormGroup>
<FormLabel>{translate('Title')}</FormLabel>
<FormInputGroup
type={inputTypes.NUMBER}
name="tvdbId"
helpText={translate('TvdbIdExcludeHelpText')}
{...tvdbId}
onChange={handleInputChange}
/>
</FormGroup>
</Form>
<FormInputGroup
type={inputTypes.TEXT}
name="title"
helpText={translate('SeriesTitleToExcludeHelpText')}
{...title}
onChange={onInputChange}
/>
</FormGroup>
<FormGroup>
<FormLabel>{translate('TvdbId')}</FormLabel>
<FormInputGroup
type={inputTypes.NUMBER}
name="tvdbId"
helpText={translate('TvdbIdExcludeHelpText')}
{...tvdbId}
onChange={onInputChange}
/>
</FormGroup>
</Form>
) : null}
</ModalBody>
<ModalFooter>
@@ -120,7 +172,7 @@ function EditImportListExclusionModalContent({
<SpinnerErrorButton
isSpinning={isSaving}
error={saveError}
onPress={handleSavePress}
onPress={onSavePress}
>
{translate('Save')}
</SpinnerErrorButton>

View File

@@ -1,4 +1,5 @@
import React, { useCallback } from 'react';
import { useDispatch } from 'react-redux';
import { useSelect } from 'App/Select/SelectContext';
import IconButton from 'Components/Link/IconButton';
import ConfirmModal from 'Components/Modal/ConfirmModal';
@@ -7,30 +8,23 @@ import TableSelectCell from 'Components/Table/Cells/TableSelectCell';
import TableRow from 'Components/Table/TableRow';
import useModalOpenState from 'Helpers/Hooks/useModalOpenState';
import { icons, kinds } from 'Helpers/Props';
import { deleteImportListExclusion } from 'Store/Actions/Settings/importListExclusions';
import ImportListExclusion from 'typings/ImportListExclusion';
import { SelectStateInputProps } from 'typings/props';
import translate from 'Utilities/String/translate';
import EditImportListExclusionModal from './EditImportListExclusionModal';
import {
ImportListExclusion,
useDeleteImportListExclusion,
} from './useImportListExclusions';
import styles from './ImportListExclusionRow.css';
interface ImportListExclusionRowProps extends ImportListExclusion {
onModalClose: () => void;
}
type ImportListExclusionRowProps = ImportListExclusion;
function ImportListExclusionRow({
id,
tvdbId,
title,
onModalClose,
}: ImportListExclusionRowProps) {
const { toggleSelected, useIsSelected } = useSelect<ImportListExclusion>();
const isSelected = useIsSelected(id);
const { deleteImportListExclusion } = useDeleteImportListExclusion(id);
const handleSelectedChange = useCallback(
({ id, value, shiftKey = false }: SelectStateInputProps) => {
toggleSelected({
@@ -42,17 +36,14 @@ function ImportListExclusionRow({
[toggleSelected]
);
const dispatch = useDispatch();
const [
isEditImportListExclusionModalOpen,
setEditImportListExclusionModalOpen,
setEditImportListExclusionModalClosed,
] = useModalOpenState(false);
const handleEditModalClose = useCallback(() => {
setEditImportListExclusionModalClosed();
onModalClose();
}, [setEditImportListExclusionModalClosed, onModalClose]);
const [
isDeleteImportListExclusionModalOpen,
setDeleteImportListExclusionModalOpen,
@@ -60,8 +51,8 @@ function ImportListExclusionRow({
] = useModalOpenState(false);
const handleDeletePress = useCallback(() => {
deleteImportListExclusion();
}, [deleteImportListExclusion]);
dispatch(deleteImportListExclusion({ id }));
}, [id, dispatch]);
return (
<TableRow>
@@ -83,10 +74,8 @@ function ImportListExclusionRow({
<EditImportListExclusionModal
id={id}
title={title}
tvdbId={tvdbId}
isOpen={isEditImportListExclusionModalOpen}
onModalClose={handleEditModalClose}
onModalClose={setEditImportListExclusionModalClosed}
onDeleteImportListExclusionPress={setDeleteImportListExclusionModalOpen}
/>

View File

@@ -1,5 +1,8 @@
import React, { useCallback, useEffect, useState } from 'react';
import { useDispatch, useSelector } from 'react-redux';
import { createSelector } from 'reselect';
import { SelectProvider, useSelect } from 'App/Select/SelectContext';
import AppState from 'App/State/AppState';
import FieldSet from 'Components/FieldSet';
import IconButton from 'Components/Link/IconButton';
import SpinnerButton from 'Components/Link/SpinnerButton';
@@ -11,9 +14,21 @@ import Table from 'Components/Table/Table';
import TableBody from 'Components/Table/TableBody';
import TablePager from 'Components/Table/TablePager';
import TableRow from 'Components/Table/TableRow';
import usePaging from 'Components/Table/usePaging';
import useCurrentPage from 'Helpers/Hooks/useCurrentPage';
import useModalOpenState from 'Helpers/Hooks/useModalOpenState';
import usePrevious from 'Helpers/Hooks/usePrevious';
import { icons, kinds } from 'Helpers/Props';
import { SortDirection } from 'Helpers/Props/sortDirections';
import {
bulkDeleteImportListExclusions,
clearImportListExclusions,
fetchImportListExclusions,
gotoImportListExclusionPage,
setImportListExclusionSort,
setImportListExclusionTableOption,
} from 'Store/Actions/Settings/importListExclusions';
import ImportListExclusion from 'typings/ImportListExclusion';
import { CheckInputChanged } from 'typings/inputs';
import { TableOptionsChangePayload } from 'typings/Table';
import {
@@ -22,16 +37,7 @@ import {
} from 'Utilities/pagePopulator';
import translate from 'Utilities/String/translate';
import EditImportListExclusionModal from './EditImportListExclusionModal';
import {
setImportListExclusionOption,
setImportListExclusionSort,
useImportListExclusionOptions,
} from './importListExclusionOptionsStore';
import ImportListExclusionRow from './ImportListExclusionRow';
import useImportListExclusions, {
ImportListExclusion,
useDeleteImportListExclusions,
} from './useImportListExclusions';
import styles from './ImportListExclusions.css';
const COLUMNS: Column[] = [
@@ -56,27 +62,40 @@ const COLUMNS: Column[] = [
},
];
function createImportListExclusionsSelector() {
return createSelector(
(state: AppState) => state.settings.importListExclusions,
(importListExclusions) => {
return {
...importListExclusions,
};
}
);
}
function ImportListExclusionsContent() {
const requestCurrentPage = useCurrentPage();
const {
records,
isFetching,
isPopulated,
items,
pageSize,
sortKey,
error,
sortDirection,
page,
totalPages,
totalRecords,
isFetching,
isFetched,
isLoading,
error,
page,
goToPage,
refetch,
} = useImportListExclusions();
isDeleting,
deleteError,
} = useSelector(createImportListExclusionsSelector());
const { pageSize, sortKey, sortDirection } = useImportListExclusionOptions();
const { deleteImportListExclusions, isDeleting } =
useDeleteImportListExclusions();
const dispatch = useDispatch();
const [isConfirmDeleteModalOpen, setIsConfirmDeleteModalOpen] =
useState(false);
const previousIsDeleting = usePrevious(isDeleting);
const {
allSelected,
@@ -100,49 +119,62 @@ function ImportListExclusionsContent() {
const handleDeleteSelectedPress = useCallback(() => {
setIsConfirmDeleteModalOpen(true);
}, []);
}, [setIsConfirmDeleteModalOpen]);
const handleDeleteSelectedConfirmed = useCallback(() => {
deleteImportListExclusions({ ids: getSelectedIds() });
dispatch(bulkDeleteImportListExclusions({ ids: getSelectedIds() }));
setIsConfirmDeleteModalOpen(false);
unselectAll();
}, [getSelectedIds, deleteImportListExclusions, unselectAll]);
}, [getSelectedIds, setIsConfirmDeleteModalOpen, dispatch]);
const handleConfirmDeleteModalClose = useCallback(() => {
setIsConfirmDeleteModalOpen(false);
}, []);
}, [setIsConfirmDeleteModalOpen]);
const {
handleFirstPagePress,
handlePreviousPagePress,
handleNextPagePress,
handleLastPagePress,
handlePageSelect,
} = usePaging({
page,
totalPages,
gotoPage: gotoImportListExclusionPage,
});
const handleSortPress = useCallback(
(sortKey: string, sortDirection?: SortDirection) => {
setImportListExclusionSort({ sortKey, sortDirection });
dispatch(setImportListExclusionSort({ sortKey, sortDirection }));
},
[]
[dispatch]
);
const handleTableOptionChange = useCallback(
(payload: TableOptionsChangePayload) => {
dispatch(setImportListExclusionTableOption(payload));
if (payload.pageSize) {
setImportListExclusionOption('pageSize', payload.pageSize as number);
goToPage(1);
dispatch(gotoImportListExclusionPage({ page: 1 }));
}
},
[goToPage]
[dispatch]
);
const [
isAddImportListExclusionModalOpen,
setAddImportListExclusionModalOpen,
setAddImportListExclusionModalClosed,
] = useModalOpenState(false);
useEffect(() => {
if (requestCurrentPage) {
dispatch(fetchImportListExclusions());
} else {
dispatch(gotoImportListExclusionPage({ page: 1 }));
}
const handleAddModalClose = useCallback(() => {
setAddImportListExclusionModalClosed();
refetch();
}, [setAddImportListExclusionModalClosed, refetch]);
return () => {
dispatch(clearImportListExclusions());
};
}, [requestCurrentPage, dispatch]);
useEffect(() => {
const repopulate = () => {
refetch();
dispatch(fetchImportListExclusions());
};
registerPagePopulator(repopulate);
@@ -150,14 +182,37 @@ function ImportListExclusionsContent() {
return () => {
unregisterPagePopulator(repopulate);
};
}, [refetch]);
}, [dispatch]);
useEffect(() => {
if (previousIsDeleting && !isDeleting && !deleteError) {
unselectAll();
dispatch(fetchImportListExclusions());
}
}, [
previousIsDeleting,
isDeleting,
deleteError,
items,
dispatch,
unselectAll,
]);
const [
isAddImportListExclusionModalOpen,
setAddImportListExclusionModalOpen,
setAddImportListExclusionModalClosed,
] = useModalOpenState(false);
const isFetchingForFirstTime = isFetching && !isPopulated;
return (
<FieldSet legend={translate('ImportListExclusions')}>
<PageSectionContent
errorMessage={translate('ImportListExclusionsLoadError')}
isFetching={isLoading && !isFetched}
isPopulated={isFetched}
isFetching={isFetchingForFirstTime}
isPopulated={isPopulated}
error={error}
>
<Table
@@ -174,14 +229,8 @@ function ImportListExclusionsContent() {
onSortPress={handleSortPress}
>
<TableBody>
{records.map((item) => {
return (
<ImportListExclusionRow
key={item.id}
{...item}
onModalClose={refetch}
/>
);
{items.map((item) => {
return <ImportListExclusionRow key={item.id} {...item} />;
})}
<TableRow>
@@ -211,12 +260,16 @@ function ImportListExclusionsContent() {
totalPages={totalPages}
totalRecords={totalRecords}
isFetching={isFetching}
onPageSelect={goToPage}
onFirstPagePress={handleFirstPagePress}
onPreviousPagePress={handlePreviousPagePress}
onNextPagePress={handleNextPagePress}
onLastPagePress={handleLastPagePress}
onPageSelect={handlePageSelect}
/>
<EditImportListExclusionModal
isOpen={isAddImportListExclusionModalOpen}
onModalClose={handleAddModalClose}
onModalClose={setAddImportListExclusionModalClosed}
/>
<ConfirmModal
@@ -234,10 +287,10 @@ function ImportListExclusionsContent() {
}
function ImportListExclusions() {
const { records } = useImportListExclusions();
const { items } = useSelector(createImportListExclusionsSelector());
return (
<SelectProvider<ImportListExclusion> items={records}>
<SelectProvider items={items}>
<ImportListExclusionsContent />
</SelectProvider>
);

View File

@@ -1,25 +0,0 @@
import { createOptionsStore } from 'Helpers/Hooks/useOptionsStore';
import { SortDirection } from 'Helpers/Props/sortDirections';
export interface ImportListExclusionOptions {
pageSize: number;
sortKey: string;
sortDirection: SortDirection;
}
const { useOptions, setOptions, setOption, setSort } =
createOptionsStore<ImportListExclusionOptions>(
'import_list_exclusion_options',
() => {
return {
pageSize: 20,
sortKey: 'id',
sortDirection: 'descending',
};
}
);
export const useImportListExclusionOptions = useOptions;
export const setImportListExclusionOptions = setOptions;
export const setImportListExclusionOption = setOption;
export const setImportListExclusionSort = setSort;

View File

@@ -1,163 +0,0 @@
import { keepPreviousData, useQueryClient } from '@tanstack/react-query';
import { useCallback, useMemo } from 'react';
import ModelBase from 'App/ModelBase';
import useApiMutation from 'Helpers/Hooks/useApiMutation';
import usePage from 'Helpers/Hooks/usePage';
import usePagedApiQuery from 'Helpers/Hooks/usePagedApiQuery';
import { usePendingChangesStore } from 'Helpers/Hooks/usePendingChangesStore';
import selectSettings from 'Store/Selectors/selectSettings';
import { useImportListExclusionOptions } from './importListExclusionOptionsStore';
export interface ImportListExclusion extends ModelBase {
tvdbId: number;
title: string;
}
const PATH = '/importlistexclusion';
const NEW_IMPORT_LIST_EXCLUSION = {
title: '',
tvdbId: 0,
};
interface BulkImportListExclusionData {
ids: number[];
}
const useImportListExclusions = () => {
const { page, goToPage } = usePage('importListExclusion');
const { pageSize, sortKey, sortDirection } = useImportListExclusionOptions();
const { refetch, ...query } = usePagedApiQuery<ImportListExclusion>({
path: PATH,
page,
pageSize,
sortKey,
sortDirection,
queryOptions: {
placeholderData: keepPreviousData,
},
});
return {
...query,
goToPage,
page,
refetch,
};
};
export default useImportListExclusions;
interface ManageImportListExclusionOptions {
id?: number;
title?: string;
tvdbId?: number;
}
export const useManageImportListExclusion = ({
id,
title,
tvdbId,
}: ManageImportListExclusionOptions) => {
const queryClient = useQueryClient();
const item = useMemo(() => {
return id
? { title: title ?? '', tvdbId: tvdbId ?? 0 }
: NEW_IMPORT_LIST_EXCLUSION;
}, [id, title, tvdbId]);
const { pendingChanges, setPendingChange } =
usePendingChangesStore<ImportListExclusion>({});
const {
mutate,
isPending: isSaving,
error: saveError,
} = useApiMutation<ImportListExclusion, ImportListExclusion>({
path: id ? `${PATH}/${id}` : PATH,
method: id ? 'PUT' : 'POST',
mutationOptions: {
onSuccess: () => {
queryClient.invalidateQueries({ queryKey: [PATH] });
},
},
});
const { settings, validationErrors, validationWarnings } = useMemo(() => {
return selectSettings(item, pendingChanges, saveError);
}, [item, pendingChanges, saveError]);
const updateValue = useCallback(
(name: string, value: unknown) => {
// @ts-expect-error - name is not yet typed
setPendingChange(name, value);
},
[setPendingChange]
);
const save = useCallback(() => {
const payload = {
...item,
...pendingChanges,
} as ImportListExclusion;
if (id) {
payload.id = id;
}
mutate(payload);
}, [id, item, pendingChanges, mutate]);
return {
item: settings,
isSaving,
saveError,
validationErrors,
validationWarnings,
updateValue,
save,
};
};
export const useDeleteImportListExclusion = (id: number) => {
const queryClient = useQueryClient();
const { mutate, isPending } = useApiMutation<unknown, void>({
path: `${PATH}/${id}`,
method: 'DELETE',
mutationOptions: {
onSuccess: () => {
queryClient.invalidateQueries({ queryKey: [PATH] });
},
},
});
return {
deleteImportListExclusion: mutate,
isDeleting: isPending,
};
};
export const useDeleteImportListExclusions = () => {
const queryClient = useQueryClient();
const { mutate, isPending } = useApiMutation<
unknown,
BulkImportListExclusionData
>({
path: `${PATH}/bulk`,
method: 'DELETE',
mutationOptions: {
onSuccess: () => {
queryClient.invalidateQueries({ queryKey: [PATH] });
},
},
});
return {
deleteImportListExclusions: mutate,
isDeleting: isPending,
};
};

View File

@@ -0,0 +1,110 @@
import { createAction } from 'redux-actions';
import createBulkRemoveItemHandler from 'Store/Actions/Creators/createBulkRemoveItemHandler';
import createRemoveItemHandler from 'Store/Actions/Creators/createRemoveItemHandler';
import createSaveProviderHandler from 'Store/Actions/Creators/createSaveProviderHandler';
import createServerSideCollectionHandlers from 'Store/Actions/Creators/createServerSideCollectionHandlers';
import createClearReducer from 'Store/Actions/Creators/Reducers/createClearReducer';
import createSetSettingValueReducer from 'Store/Actions/Creators/Reducers/createSetSettingValueReducer';
import createSetTableOptionReducer from 'Store/Actions/Creators/Reducers/createSetTableOptionReducer';
import { createThunk, handleThunks } from 'Store/thunks';
import serverSideCollectionHandlers from 'Utilities/State/serverSideCollectionHandlers';
//
// Variables
const section = 'settings.importListExclusions';
//
// Actions Types
export const FETCH_IMPORT_LIST_EXCLUSIONS = 'settings/importListExclusions/fetchImportListExclusions';
export const GOTO_IMPORT_LIST_EXCLUSION_PAGE = 'settings/importListExclusions/gotoImportListExclusionPage';
export const SET_IMPORT_LIST_EXCLUSION_SORT = 'settings/importListExclusions/setImportListExclusionSort';
export const SAVE_IMPORT_LIST_EXCLUSION = 'settings/importListExclusions/saveImportListExclusion';
export const DELETE_IMPORT_LIST_EXCLUSION = 'settings/importListExclusions/deleteImportListExclusion';
export const BULK_DELETE_IMPORT_LIST_EXCLUSIONS = 'settings/importListExclusions/bulkDeleteImportListExclusions';
export const CLEAR_IMPORT_LIST_EXCLUSIONS = 'settings/importListExclusions/clearImportListExclusions';
export const SET_IMPORT_LIST_EXCLUSION_TABLE_OPTION = 'settings/importListExclusions/setImportListExclusionTableOption';
export const SET_IMPORT_LIST_EXCLUSION_VALUE = 'settings/importListExclusions/setImportListExclusionValue';
//
// Action Creators
export const fetchImportListExclusions = createThunk(FETCH_IMPORT_LIST_EXCLUSIONS);
export const gotoImportListExclusionPage = createThunk(GOTO_IMPORT_LIST_EXCLUSION_PAGE);
export const setImportListExclusionSort = createThunk(SET_IMPORT_LIST_EXCLUSION_SORT);
export const saveImportListExclusion = createThunk(SAVE_IMPORT_LIST_EXCLUSION);
export const deleteImportListExclusion = createThunk(DELETE_IMPORT_LIST_EXCLUSION);
export const bulkDeleteImportListExclusions = createThunk(BULK_DELETE_IMPORT_LIST_EXCLUSIONS);
export const clearImportListExclusions = createAction(CLEAR_IMPORT_LIST_EXCLUSIONS);
export const setImportListExclusionTableOption = createAction(SET_IMPORT_LIST_EXCLUSION_TABLE_OPTION);
export const setImportListExclusionValue = createAction(SET_IMPORT_LIST_EXCLUSION_VALUE, (payload) => {
return {
section,
...payload
};
});
//
// Details
export default {
//
// State
defaultState: {
isFetching: false,
isPopulated: false,
error: null,
pageSize: 20,
items: [],
isSaving: false,
saveError: null,
isDeleting: false,
deleteError: null,
pendingChanges: {}
},
//
// Action Handlers
actionHandlers: handleThunks({
...createServerSideCollectionHandlers(
section,
'/importlistexclusion/paged',
fetchImportListExclusions,
{
[serverSideCollectionHandlers.FETCH]: FETCH_IMPORT_LIST_EXCLUSIONS,
[serverSideCollectionHandlers.EXACT_PAGE]: GOTO_IMPORT_LIST_EXCLUSION_PAGE,
[serverSideCollectionHandlers.SORT]: SET_IMPORT_LIST_EXCLUSION_SORT
}
),
[SAVE_IMPORT_LIST_EXCLUSION]: createSaveProviderHandler(section, '/importlistexclusion'),
[DELETE_IMPORT_LIST_EXCLUSION]: createRemoveItemHandler(section, '/importlistexclusion'),
[BULK_DELETE_IMPORT_LIST_EXCLUSIONS]: createBulkRemoveItemHandler(section, '/importlistexclusion/bulk')
}),
//
// Reducers
reducers: {
[SET_IMPORT_LIST_EXCLUSION_VALUE]: createSetSettingValueReducer(section),
[SET_IMPORT_LIST_EXCLUSION_TABLE_OPTION]: createSetTableOptionReducer(section),
[CLEAR_IMPORT_LIST_EXCLUSIONS]: createClearReducer(section, {
isFetching: false,
isPopulated: false,
error: null,
items: [],
isDeleting: false,
deleteError: null,
pendingChanges: {},
totalPages: 0,
totalRecords: 0
})
}
};

View File

@@ -7,6 +7,7 @@ import customFormatSpecifications from './Settings/customFormatSpecifications';
import delayProfiles from './Settings/delayProfiles';
import downloadClientOptions from './Settings/downloadClientOptions';
import downloadClients from './Settings/downloadClients';
import importListExclusions from './Settings/importListExclusions';
import importListOptions from './Settings/importListOptions';
import importLists from './Settings/importLists';
@@ -19,6 +20,7 @@ export * from './Settings/downloadClients';
export * from './Settings/downloadClientOptions';
export * from './Settings/importListOptions';
export * from './Settings/importLists';
export * from './Settings/importListExclusions';
//
// Variables
@@ -38,6 +40,7 @@ export const defaultState = {
downloadClients: downloadClients.defaultState,
downloadClientOptions: downloadClientOptions.defaultState,
importLists: importLists.defaultState,
importListExclusions: importListExclusions.defaultState,
importListOptions: importListOptions.defaultState
};
@@ -57,6 +60,7 @@ export const actionHandlers = handleThunks({
...downloadClients.actionHandlers,
...downloadClientOptions.actionHandlers,
...importLists.actionHandlers,
...importListExclusions.actionHandlers,
...importListOptions.actionHandlers
});
@@ -72,6 +76,7 @@ export const reducers = createHandleActions({
...downloadClients.reducers,
...downloadClientOptions.reducers,
...importLists.reducers,
...importListExclusions.reducers,
...importListOptions.reducers
}, defaultState, section);

View File

@@ -0,0 +1,6 @@
import ModelBase from 'App/ModelBase';
export default interface ImportListExclusion extends ModelBase {
tvdbId: number;
title: string;
}

View File

@@ -1,6 +0,0 @@
namespace Sonarr.Api.V5.ImportLists;
public class ImportListExclusionBulkResource
{
public required HashSet<int> Ids { get; set; }
}

View File

@@ -1,86 +0,0 @@
using FluentValidation;
using Microsoft.AspNetCore.Mvc;
using NzbDrone.Core.Datastore;
using NzbDrone.Core.ImportLists.Exclusions;
using Sonarr.Http;
using Sonarr.Http.Extensions;
using Sonarr.Http.REST;
using Sonarr.Http.REST.Attributes;
namespace Sonarr.Api.V5.ImportLists;
[V5ApiController]
public class ImportListExclusionController : RestController<ImportListExclusionResource>
{
private readonly IImportListExclusionService _importListExclusionService;
public ImportListExclusionController(IImportListExclusionService importListExclusionService,
ImportListExclusionExistsValidator importListExclusionExistsValidator)
{
_importListExclusionService = importListExclusionService;
SharedValidator.RuleFor(c => c.TvdbId).Cascade(CascadeMode.Stop)
.NotEmpty()
.SetValidator(importListExclusionExistsValidator);
SharedValidator.RuleFor(c => c.Title).NotEmpty();
}
protected override ImportListExclusionResource GetResourceById(int id)
{
return _importListExclusionService.Get(id).ToResource();
}
[HttpGet]
[Produces("application/json")]
public PagingResource<ImportListExclusionResource> GetImportListExclusions([FromQuery] PagingRequestResource paging)
{
var pagingResource = new PagingResource<ImportListExclusionResource>(paging);
var pageSpec = pagingResource.MapToPagingSpec<ImportListExclusionResource, ImportListExclusion>(
new HashSet<string>(StringComparer.OrdinalIgnoreCase)
{
"id",
"title",
"tvdbId"
},
"id",
SortDirection.Descending);
return pageSpec.ApplyToPage(_importListExclusionService.Paged, ImportListExclusionResourceMapper.ToResource);
}
[RestPostById]
[Consumes("application/json")]
public ActionResult<ImportListExclusionResource> AddImportListExclusion([FromBody] ImportListExclusionResource resource)
{
var importListExclusion = _importListExclusionService.Add(resource.ToModel());
return Created(importListExclusion.Id);
}
[RestPutById]
[Consumes("application/json")]
public ActionResult<ImportListExclusionResource> UpdateImportListExclusion([FromBody] ImportListExclusionResource resource)
{
_importListExclusionService.Update(resource.ToModel());
return Accepted(resource.Id);
}
[RestDeleteById]
public ActionResult DeleteImportListExclusion(int id)
{
_importListExclusionService.Delete(id);
return NoContent();
}
[HttpDelete("bulk")]
[Consumes("application/json")]
public ActionResult DeleteImportListExclusions([FromBody] ImportListExclusionBulkResource resource)
{
_importListExclusionService.Delete(resource.Ids.ToList());
return NoContent();
}
}

View File

@@ -1,31 +0,0 @@
using FluentValidation.Validators;
using NzbDrone.Core.ImportLists.Exclusions;
namespace Sonarr.Api.V5.ImportLists;
public class ImportListExclusionExistsValidator : PropertyValidator
{
private readonly IImportListExclusionService _importListExclusionService;
public ImportListExclusionExistsValidator(IImportListExclusionService importListExclusionService)
{
_importListExclusionService = importListExclusionService;
}
protected override string GetDefaultMessageTemplate() => "This exclusion has already been added.";
protected override bool IsValid(PropertyValidatorContext context)
{
if (context.PropertyValue == null)
{
return true;
}
if (context.InstanceToValidate is not ImportListExclusionResource listExclusionResource)
{
return true;
}
return !_importListExclusionService.All().Exists(v => v.TvdbId == (int)context.PropertyValue && v.Id != listExclusionResource.Id);
}
}

View File

@@ -1,38 +0,0 @@
using NzbDrone.Core.ImportLists.Exclusions;
using Sonarr.Http.REST;
namespace Sonarr.Api.V5.ImportLists;
public class ImportListExclusionResource : RestResource
{
public int TvdbId { get; set; }
public string? Title { get; set; }
}
public static class ImportListExclusionResourceMapper
{
public static ImportListExclusionResource ToResource(this ImportListExclusion model)
{
return new ImportListExclusionResource
{
Id = model.Id,
TvdbId = model.TvdbId,
Title = model.Title,
};
}
public static ImportListExclusion ToModel(this ImportListExclusionResource resource)
{
return new ImportListExclusion
{
Id = resource.Id,
TvdbId = resource.TvdbId,
Title = resource.Title
};
}
public static List<ImportListExclusionResource> ToResource(this IEnumerable<ImportListExclusion> models)
{
return models.Select(ToResource).ToList();
}
}

View File

@@ -623,12 +623,19 @@
],
"parameters": [
{
"name": "forceSave",
"name": "skipTesting",
"in": "query",
"schema": {
"type": "boolean",
"default": false
}
},
{
"name": "skipValidation",
"in": "query",
"schema": {
"$ref": "#/components/schemas/SkipValidation"
}
}
],
"requestBody": {
@@ -670,12 +677,19 @@
}
},
{
"name": "forceSave",
"name": "skipTesting",
"in": "query",
"schema": {
"type": "boolean",
"default": false
}
},
{
"name": "skipValidation",
"in": "query",
"schema": {
"$ref": "#/components/schemas/SkipValidation"
}
}
],
"requestBody": {
@@ -779,11 +793,10 @@
],
"parameters": [
{
"name": "forceTest",
"name": "skipValidation",
"in": "query",
"schema": {
"type": "boolean",
"default": false
"$ref": "#/components/schemas/SkipValidation"
}
}
],
@@ -2058,12 +2071,19 @@
],
"parameters": [
{
"name": "forceSave",
"name": "skipTesting",
"in": "query",
"schema": {
"type": "boolean",
"default": false
}
},
{
"name": "skipValidation",
"in": "query",
"schema": {
"$ref": "#/components/schemas/SkipValidation"
}
}
],
"requestBody": {
@@ -2105,12 +2125,19 @@
}
},
{
"name": "forceSave",
"name": "skipTesting",
"in": "query",
"schema": {
"type": "boolean",
"default": false
}
},
{
"name": "skipValidation",
"in": "query",
"schema": {
"$ref": "#/components/schemas/SkipValidation"
}
}
],
"requestBody": {
@@ -2261,11 +2288,10 @@
],
"parameters": [
{
"name": "forceTest",
"name": "skipValidation",
"in": "query",
"schema": {
"type": "boolean",
"default": false
"$ref": "#/components/schemas/SkipValidation"
}
}
],
@@ -2366,6 +2392,101 @@
}
}
},
"/api/v5/settings/indexer": {
"get": {
"tags": [
"IndexerSettings"
],
"responses": {
"200": {
"description": "OK",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/IndexerSettingsResource"
}
}
}
}
}
}
},
"/api/v5/settings/indexer/{id}": {
"put": {
"tags": [
"IndexerSettings"
],
"parameters": [
{
"name": "id",
"in": "path",
"required": true,
"schema": {
"type": "string"
}
}
],
"requestBody": {
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/IndexerSettingsResource"
}
}
}
},
"responses": {
"200": {
"description": "OK",
"content": {
"text/plain": {
"schema": {
"$ref": "#/components/schemas/IndexerSettingsResource"
}
},
"application/json": {
"schema": {
"$ref": "#/components/schemas/IndexerSettingsResource"
}
},
"text/json": {
"schema": {
"$ref": "#/components/schemas/IndexerSettingsResource"
}
}
}
}
}
},
"get": {
"tags": [
"IndexerSettings"
],
"parameters": [
{
"name": "id",
"in": "path",
"required": true,
"schema": {
"type": "integer",
"format": "int32"
}
}
],
"responses": {
"200": {
"description": "OK",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/IndexerSettingsResource"
}
}
}
}
}
}
},
"/api/v5/language": {
"get": {
"tags": [
@@ -2842,12 +2963,19 @@
],
"parameters": [
{
"name": "forceSave",
"name": "skipTesting",
"in": "query",
"schema": {
"type": "boolean",
"default": false
}
},
{
"name": "skipValidation",
"in": "query",
"schema": {
"$ref": "#/components/schemas/SkipValidation"
}
}
],
"requestBody": {
@@ -2889,12 +3017,19 @@
}
},
{
"name": "forceSave",
"name": "skipTesting",
"in": "query",
"schema": {
"type": "boolean",
"default": false
}
},
{
"name": "skipValidation",
"in": "query",
"schema": {
"$ref": "#/components/schemas/SkipValidation"
}
}
],
"requestBody": {
@@ -2998,11 +3133,10 @@
],
"parameters": [
{
"name": "forceTest",
"name": "skipValidation",
"in": "query",
"schema": {
"type": "boolean",
"default": false
"$ref": "#/components/schemas/SkipValidation"
}
}
],
@@ -7691,6 +7825,32 @@
},
"additionalProperties": false
},
"IndexerSettingsResource": {
"type": "object",
"properties": {
"id": {
"type": "integer",
"format": "int32"
},
"minimumAge": {
"type": "integer",
"format": "int32"
},
"retention": {
"type": "integer",
"format": "int32"
},
"maximumSize": {
"type": "integer",
"format": "int32"
},
"rssSyncInterval": {
"type": "integer",
"format": "int32"
}
},
"additionalProperties": false
},
"Language": {
"type": "object",
"properties": {
@@ -7865,6 +8025,10 @@
"type": "string",
"nullable": true
},
"relativePath": {
"type": "string",
"nullable": true
},
"seriesId": {
"type": "integer",
"format": "int32"
@@ -10283,6 +10447,14 @@
],
"type": "string"
},
"SkipValidation": {
"enum": [
"none",
"warnings",
"all"
],
"type": "string"
},
"SortDirection": {
"enum": [
"default",
@@ -10865,6 +11037,9 @@
{
"name": "IndexerFlag"
},
{
"name": "IndexerSettings"
},
{
"name": "Language"
},