1
0
mirror of https://github.com/Radarr/Radarr.git synced 2024-11-19 17:32:38 +01:00

New: Bulk manage custom formats

This commit is contained in:
Bogdan 2024-08-19 02:55:13 +03:00
parent 672b351497
commit da5323a08f
25 changed files with 691 additions and 24 deletions

View File

@ -6,6 +6,7 @@ import AppSectionState, {
PagedAppSectionState,
} from 'App/State/AppSectionState';
import Language from 'Language/Language';
import CustomFormat from 'typings/CustomFormat';
import DownloadClient from 'typings/DownloadClient';
import ImportList from 'typings/ImportList';
import ImportListExclusion from 'typings/ImportListExclusion';
@ -39,6 +40,11 @@ export interface QualityProfilesAppState
extends AppSectionState<QualityProfile>,
AppSectionSchemaState<QualityProfile> {}
export interface CustomFormatAppState
extends AppSectionState<CustomFormat>,
AppSectionDeleteState,
AppSectionSaveState {}
export interface ImportListOptionsSettingsAppState
extends AppSectionItemState<ImportListOptionsSettings>,
AppSectionSaveState {}
@ -57,6 +63,7 @@ export type UiSettingsAppState = AppSectionItemState<UiSettings>;
interface SettingsAppState {
advancedSettings: boolean;
customFormats: CustomFormatAppState;
downloadClients: DownloadClientAppState;
importListExclusions: ImportListExclusionsSettingsAppState;
importListOptions: ImportListOptionsSettingsAppState;

View File

@ -8,6 +8,7 @@ import ParseToolbarButton from 'Parse/ParseToolbarButton';
import SettingsToolbarConnector from 'Settings/SettingsToolbarConnector';
import translate from 'Utilities/String/translate';
import CustomFormatsConnector from './CustomFormats/CustomFormatsConnector';
import ManageCustomFormatsToolbarButton from './CustomFormats/Manage/ManageCustomFormatsToolbarButton';
function CustomFormatSettingsPage() {
return (
@ -21,6 +22,8 @@ function CustomFormatSettingsPage() {
<PageToolbarSeparator />
<ParseToolbarButton />
<ManageCustomFormatsToolbarButton />
</>
}
/>

View File

@ -0,0 +1,28 @@
import React from 'react';
import Modal from 'Components/Modal/Modal';
import ManageCustomFormatsEditModalContent from './ManageCustomFormatsEditModalContent';
interface ManageCustomFormatsEditModalProps {
isOpen: boolean;
customFormatIds: number[];
onSavePress(payload: object): void;
onModalClose(): void;
}
function ManageCustomFormatsEditModal(
props: ManageCustomFormatsEditModalProps
) {
const { isOpen, customFormatIds, onSavePress, onModalClose } = props;
return (
<Modal isOpen={isOpen} onModalClose={onModalClose}>
<ManageCustomFormatsEditModalContent
customFormatIds={customFormatIds}
onSavePress={onSavePress}
onModalClose={onModalClose}
/>
</Modal>
);
}
export default ManageCustomFormatsEditModal;

View File

@ -0,0 +1,16 @@
.modalFooter {
composes: modalFooter from '~Components/Modal/ModalFooter.css';
justify-content: space-between;
}
.selected {
font-weight: bold;
}
@media only screen and (max-width: $breakpointExtraSmall) {
.modalFooter {
flex-direction: column;
gap: 10px;
}
}

View File

@ -0,0 +1,8 @@
// This file is automatically generated.
// Please do not change this file!
interface CssExports {
'modalFooter': string;
'selected': string;
}
export const cssExports: CssExports;
export default cssExports;

View File

@ -0,0 +1,125 @@
import React, { useCallback, useState } from 'react';
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 ModalBody from 'Components/Modal/ModalBody';
import ModalContent from 'Components/Modal/ModalContent';
import ModalFooter from 'Components/Modal/ModalFooter';
import ModalHeader from 'Components/Modal/ModalHeader';
import { inputTypes } from 'Helpers/Props';
import translate from 'Utilities/String/translate';
import styles from './ManageCustomFormatsEditModalContent.css';
interface SavePayload {
includeCustomFormatWhenRenaming?: boolean;
}
interface ManageCustomFormatsEditModalContentProps {
customFormatIds: number[];
onSavePress(payload: object): void;
onModalClose(): void;
}
const NO_CHANGE = 'noChange';
const enableOptions = [
{
key: NO_CHANGE,
get value() {
return translate('NoChange');
},
isDisabled: true,
},
{
key: 'enabled',
get value() {
return translate('Enabled');
},
},
{
key: 'disabled',
get value() {
return translate('Disabled');
},
},
];
function ManageCustomFormatsEditModalContent(
props: ManageCustomFormatsEditModalContentProps
) {
const { customFormatIds, onSavePress, onModalClose } = props;
const [includeCustomFormatWhenRenaming, setIncludeCustomFormatWhenRenaming] =
useState(NO_CHANGE);
const save = useCallback(() => {
let hasChanges = false;
const payload: SavePayload = {};
if (includeCustomFormatWhenRenaming !== NO_CHANGE) {
hasChanges = true;
payload.includeCustomFormatWhenRenaming =
includeCustomFormatWhenRenaming === 'enabled';
}
if (hasChanges) {
onSavePress(payload);
}
onModalClose();
}, [includeCustomFormatWhenRenaming, onSavePress, onModalClose]);
const onInputChange = useCallback(
({ name, value }: { name: string; value: string }) => {
switch (name) {
case 'includeCustomFormatWhenRenaming':
setIncludeCustomFormatWhenRenaming(value);
break;
default:
console.warn(
`EditCustomFormatsModalContent Unknown Input: '${name}'`
);
}
},
[]
);
const selectedCount = customFormatIds.length;
return (
<ModalContent onModalClose={onModalClose}>
<ModalHeader>{translate('EditSelectedCustomFormats')}</ModalHeader>
<ModalBody>
<FormGroup>
<FormLabel>{translate('IncludeCustomFormatWhenRenaming')}</FormLabel>
<FormInputGroup
type={inputTypes.SELECT}
name="includeCustomFormatWhenRenaming"
value={includeCustomFormatWhenRenaming}
values={enableOptions}
onChange={onInputChange}
/>
</FormGroup>
</ModalBody>
<ModalFooter className={styles.modalFooter}>
<div className={styles.selected}>
{translate('CountCustomFormatsSelected', {
count: selectedCount,
})}
</div>
<div>
<Button onPress={onModalClose}>{translate('Cancel')}</Button>
<Button onPress={save}>{translate('ApplyChanges')}</Button>
</div>
</ModalFooter>
</ModalContent>
);
}
export default ManageCustomFormatsEditModalContent;

View File

@ -0,0 +1,20 @@
import React from 'react';
import Modal from 'Components/Modal/Modal';
import ManageCustomFormatsModalContent from './ManageCustomFormatsModalContent';
interface ManageCustomFormatsModalProps {
isOpen: boolean;
onModalClose(): void;
}
function ManageCustomFormatsModal(props: ManageCustomFormatsModalProps) {
const { isOpen, onModalClose } = props;
return (
<Modal isOpen={isOpen} onModalClose={onModalClose}>
<ManageCustomFormatsModalContent onModalClose={onModalClose} />
</Modal>
);
}
export default ManageCustomFormatsModal;

View File

@ -0,0 +1,16 @@
.leftButtons,
.rightButtons {
display: flex;
flex: 1 0 50%;
flex-wrap: wrap;
}
.rightButtons {
justify-content: flex-end;
}
.deleteButton {
composes: button from '~Components/Link/Button.css';
margin-right: 10px;
}

View File

@ -0,0 +1,9 @@
// This file is automatically generated.
// Please do not change this file!
interface CssExports {
'deleteButton': string;
'leftButtons': string;
'rightButtons': string;
}
export const cssExports: CssExports;
export default cssExports;

View File

@ -0,0 +1,241 @@
import React, { useCallback, useMemo, useState } from 'react';
import { useDispatch, useSelector } from 'react-redux';
import { CustomFormatAppState } from 'App/State/SettingsAppState';
import Alert from 'Components/Alert';
import Button from 'Components/Link/Button';
import SpinnerButton from 'Components/Link/SpinnerButton';
import LoadingIndicator from 'Components/Loading/LoadingIndicator';
import ConfirmModal from 'Components/Modal/ConfirmModal';
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 Table from 'Components/Table/Table';
import TableBody from 'Components/Table/TableBody';
import useSelectState from 'Helpers/Hooks/useSelectState';
import { kinds } from 'Helpers/Props';
import SortDirection from 'Helpers/Props/SortDirection';
import {
bulkDeleteCustomFormats,
bulkEditCustomFormats,
setManageCustomFormatsSort,
} from 'Store/Actions/settingsActions';
import createClientSideCollectionSelector from 'Store/Selectors/createClientSideCollectionSelector';
import { SelectStateInputProps } from 'typings/props';
import getErrorMessage from 'Utilities/Object/getErrorMessage';
import translate from 'Utilities/String/translate';
import getSelectedIds from 'Utilities/Table/getSelectedIds';
import ManageCustomFormatsEditModal from './Edit/ManageCustomFormatsEditModal';
import ManageCustomFormatsModalRow from './ManageCustomFormatsModalRow';
import styles from './ManageCustomFormatsModalContent.css';
// TODO: This feels janky to do, but not sure of a better way currently
type OnSelectedChangeCallback = React.ComponentProps<
typeof ManageCustomFormatsModalRow
>['onSelectedChange'];
const COLUMNS = [
{
name: 'name',
label: () => translate('Name'),
isSortable: true,
isVisible: true,
},
{
name: 'includeCustomFormatWhenRenaming',
label: () => translate('IncludeCustomFormatWhenRenaming'),
isSortable: true,
isVisible: true,
},
];
interface ManageCustomFormatsModalContentProps {
onModalClose(): void;
sortKey?: string;
sortDirection?: SortDirection;
}
function ManageCustomFormatsModalContent(
props: ManageCustomFormatsModalContentProps
) {
const { onModalClose } = props;
const {
isFetching,
isPopulated,
isDeleting,
isSaving,
error,
items,
sortKey,
sortDirection,
}: CustomFormatAppState = useSelector(
createClientSideCollectionSelector('settings.customFormats')
);
const dispatch = useDispatch();
const [isDeleteModalOpen, setIsDeleteModalOpen] = useState(false);
const [isEditModalOpen, setIsEditModalOpen] = useState(false);
const [selectState, setSelectState] = useSelectState();
const { allSelected, allUnselected, selectedState } = selectState;
const selectedIds: number[] = useMemo(() => {
return getSelectedIds(selectedState);
}, [selectedState]);
const selectedCount = selectedIds.length;
const onSortPress = useCallback(
(value: string) => {
dispatch(setManageCustomFormatsSort({ sortKey: value }));
},
[dispatch]
);
const onDeletePress = useCallback(() => {
setIsDeleteModalOpen(true);
}, [setIsDeleteModalOpen]);
const onDeleteModalClose = useCallback(() => {
setIsDeleteModalOpen(false);
}, [setIsDeleteModalOpen]);
const onEditPress = useCallback(() => {
setIsEditModalOpen(true);
}, [setIsEditModalOpen]);
const onEditModalClose = useCallback(() => {
setIsEditModalOpen(false);
}, [setIsEditModalOpen]);
const onConfirmDelete = useCallback(() => {
dispatch(bulkDeleteCustomFormats({ ids: selectedIds }));
setIsDeleteModalOpen(false);
}, [selectedIds, dispatch]);
const onSavePress = useCallback(
(payload: object) => {
setIsEditModalOpen(false);
dispatch(
bulkEditCustomFormats({
ids: selectedIds,
...payload,
})
);
},
[selectedIds, dispatch]
);
const onSelectAllChange = useCallback(
({ value }: SelectStateInputProps) => {
setSelectState({ type: value ? 'selectAll' : 'unselectAll', items });
},
[items, setSelectState]
);
const onSelectedChange = useCallback<OnSelectedChangeCallback>(
({ id, value, shiftKey = false }) => {
setSelectState({
type: 'toggleSelected',
items,
id,
isSelected: value,
shiftKey,
});
},
[items, setSelectState]
);
const errorMessage = getErrorMessage(error, 'Unable to load custom formats.');
const anySelected = selectedCount > 0;
return (
<ModalContent onModalClose={onModalClose}>
<ModalHeader>{translate('ManageCustomFormats')}</ModalHeader>
<ModalBody>
{isFetching ? <LoadingIndicator /> : null}
{error ? <div>{errorMessage}</div> : null}
{isPopulated && !error && !items.length && (
<Alert kind={kinds.INFO}>{translate('NoCustomFormatsFound')}</Alert>
)}
{isPopulated && !!items.length && !isFetching && !isFetching ? (
<Table
columns={COLUMNS}
horizontalScroll={true}
selectAll={true}
allSelected={allSelected}
allUnselected={allUnselected}
sortKey={sortKey}
sortDirection={sortDirection}
onSelectAllChange={onSelectAllChange}
onSortPress={onSortPress}
>
<TableBody>
{items.map((item) => {
return (
<ManageCustomFormatsModalRow
key={item.id}
isSelected={selectedState[item.id]}
{...item}
columns={COLUMNS}
onSelectedChange={onSelectedChange}
/>
);
})}
</TableBody>
</Table>
) : null}
</ModalBody>
<ModalFooter>
<div className={styles.leftButtons}>
<SpinnerButton
kind={kinds.DANGER}
isSpinning={isDeleting}
isDisabled={!anySelected}
onPress={onDeletePress}
>
{translate('Delete')}
</SpinnerButton>
<SpinnerButton
isSpinning={isSaving}
isDisabled={!anySelected}
onPress={onEditPress}
>
{translate('Edit')}
</SpinnerButton>
</div>
<Button onPress={onModalClose}>{translate('Close')}</Button>
</ModalFooter>
<ManageCustomFormatsEditModal
isOpen={isEditModalOpen}
customFormatIds={selectedIds}
onModalClose={onEditModalClose}
onSavePress={onSavePress}
/>
<ConfirmModal
isOpen={isDeleteModalOpen}
kind={kinds.DANGER}
title={translate('DeleteSelectedCustomFormats')}
message={translate('DeleteSelectedCustomFormatsMessageText', {
count: selectedIds.length,
})}
confirmLabel={translate('Delete')}
onConfirm={onConfirmDelete}
onCancel={onDeleteModalClose}
/>
</ModalContent>
);
}
export default ManageCustomFormatsModalContent;

View File

@ -0,0 +1,6 @@
.name,
.includeCustomFormatWhenRenaming {
composes: cell from '~Components/Table/Cells/TableRowCell.css';
word-break: break-all;
}

View File

@ -0,0 +1,8 @@
// This file is automatically generated.
// Please do not change this file!
interface CssExports {
'includeCustomFormatWhenRenaming': string;
'name': string;
}
export const cssExports: CssExports;
export default cssExports;

View File

@ -0,0 +1,54 @@
import React, { useCallback } from 'react';
import TableRowCell from 'Components/Table/Cells/TableRowCell';
import TableSelectCell from 'Components/Table/Cells/TableSelectCell';
import Column from 'Components/Table/Column';
import TableRow from 'Components/Table/TableRow';
import { SelectStateInputProps } from 'typings/props';
import translate from 'Utilities/String/translate';
import styles from './ManageCustomFormatsModalRow.css';
interface ManageCustomFormatsModalRowProps {
id: number;
name: string;
includeCustomFormatWhenRenaming: boolean;
columns: Column[];
isSelected?: boolean;
onSelectedChange(result: SelectStateInputProps): void;
}
function ManageCustomFormatsModalRow(props: ManageCustomFormatsModalRowProps) {
const {
id,
isSelected,
name,
includeCustomFormatWhenRenaming,
onSelectedChange,
} = props;
const onSelectedChangeWrapper = useCallback(
(result: SelectStateInputProps) => {
onSelectedChange({
...result,
});
},
[onSelectedChange]
);
return (
<TableRow>
<TableSelectCell
id={id}
isSelected={isSelected}
onSelectedChange={onSelectedChangeWrapper}
/>
<TableRowCell className={styles.name}>{name}</TableRowCell>
<TableRowCell className={styles.includeCustomFormatWhenRenaming}>
{includeCustomFormatWhenRenaming ? translate('Yes') : translate('No')}
</TableRowCell>
</TableRow>
);
}
export default ManageCustomFormatsModalRow;

View File

@ -0,0 +1,28 @@
import React from 'react';
import PageToolbarButton from 'Components/Page/Toolbar/PageToolbarButton';
import useModalOpenState from 'Helpers/Hooks/useModalOpenState';
import { icons } from 'Helpers/Props';
import translate from 'Utilities/String/translate';
import ManageCustomFormatsModal from './ManageCustomFormatsModal';
function ManageCustomFormatsToolbarButton() {
const [isManageModalOpen, openManageModal, closeManageModal] =
useModalOpenState(false);
return (
<>
<PageToolbarButton
label={translate('ManageCustomFormats')}
iconName={icons.MANAGE}
onPress={openManageModal}
/>
<ManageCustomFormatsModal
isOpen={isManageModalOpen}
onModalClose={closeManageModal}
/>
</>
);
}
export default ManageCustomFormatsToolbarButton;

View File

@ -13,4 +13,4 @@
composes: button from '~Components/Link/Button.css';
margin-right: 10px;
}
}

View File

@ -13,4 +13,4 @@
composes: button from '~Components/Link/Button.css';
margin-right: 10px;
}
}

View File

@ -13,4 +13,4 @@
composes: button from '~Components/Link/Button.css';
margin-right: 10px;
}
}

View File

@ -1,7 +1,12 @@
import { createAction } from 'redux-actions';
import { sortDirections } from 'Helpers/Props';
import createBulkEditItemHandler from 'Store/Actions/Creators/createBulkEditItemHandler';
import createBulkRemoveItemHandler from 'Store/Actions/Creators/createBulkRemoveItemHandler';
import createFetchHandler from 'Store/Actions/Creators/createFetchHandler';
import createRemoveItemHandler from 'Store/Actions/Creators/createRemoveItemHandler';
import createSaveProviderHandler from 'Store/Actions/Creators/createSaveProviderHandler';
import createSetClientSideCollectionSortReducer
from 'Store/Actions/Creators/Reducers/createSetClientSideCollectionSortReducer';
import createSetSettingValueReducer from 'Store/Actions/Creators/Reducers/createSetSettingValueReducer';
import { createThunk } from 'Store/thunks';
import getSectionState from 'Utilities/State/getSectionState';
@ -22,6 +27,9 @@ export const SAVE_CUSTOM_FORMAT = 'settings/customFormats/saveCustomFormat';
export const DELETE_CUSTOM_FORMAT = 'settings/customFormats/deleteCustomFormat';
export const SET_CUSTOM_FORMAT_VALUE = 'settings/customFormats/setCustomFormatValue';
export const CLONE_CUSTOM_FORMAT = 'settings/customFormats/cloneCustomFormat';
export const BULK_EDIT_CUSTOM_FORMATS = 'settings/downloadClients/bulkEditCustomFormats';
export const BULK_DELETE_CUSTOM_FORMATS = 'settings/downloadClients/bulkDeleteCustomFormats';
export const SET_MANAGE_CUSTOM_FORMATS_SORT = 'settings/downloadClients/setManageCustomFormatsSort';
//
// Action Creators
@ -29,6 +37,9 @@ export const CLONE_CUSTOM_FORMAT = 'settings/customFormats/cloneCustomFormat';
export const fetchCustomFormats = createThunk(FETCH_CUSTOM_FORMATS);
export const saveCustomFormat = createThunk(SAVE_CUSTOM_FORMAT);
export const deleteCustomFormat = createThunk(DELETE_CUSTOM_FORMAT);
export const bulkEditCustomFormats = createThunk(BULK_EDIT_CUSTOM_FORMATS);
export const bulkDeleteCustomFormats = createThunk(BULK_DELETE_CUSTOM_FORMATS);
export const setManageCustomFormatsSort = createAction(SET_MANAGE_CUSTOM_FORMATS_SORT);
export const setCustomFormatValue = createAction(SET_CUSTOM_FORMAT_VALUE, (payload) => {
return {
@ -48,20 +59,30 @@ export default {
// State
defaultState: {
isSchemaFetching: false,
isSchemaPopulated: false,
isFetching: false,
isPopulated: false,
error: null,
isSaving: false,
saveError: null,
isDeleting: false,
deleteError: null,
items: [],
pendingChanges: {},
isSchemaFetching: false,
isSchemaPopulated: false,
schemaError: null,
schema: {
includeCustomFormatWhenRenaming: false
},
error: null,
isDeleting: false,
deleteError: null,
isSaving: false,
saveError: null,
items: [],
pendingChanges: {}
sortKey: 'name',
sortDirection: sortDirections.ASCENDING,
sortPredicates: {
name: ({ name }) => {
return name.toLocaleLowerCase();
}
}
},
//
@ -83,7 +104,10 @@ export default {
}));
createSaveProviderHandler(section, '/customformat')(getState, payload, dispatch);
}
},
[BULK_EDIT_CUSTOM_FORMATS]: createBulkEditItemHandler(section, '/customformat/bulk'),
[BULK_DELETE_CUSTOM_FORMATS]: createBulkRemoveItemHandler(section, '/customformat/bulk')
},
//
@ -103,7 +127,9 @@ export default {
newState.pendingChanges = pendingChanges;
return updateSectionState(state, section, newState);
}
},
[SET_MANAGE_CUSTOM_FORMATS_SORT]: createSetClientSideCollectionSortReducer(section)
}
};

View File

@ -96,8 +96,8 @@ export default {
sortKey: 'name',
sortDirection: sortDirections.ASCENDING,
sortPredicates: {
name: function(item) {
return item.name.toLowerCase();
name: ({ name }) => {
return name.toLocaleLowerCase();
}
}
},

View File

@ -101,8 +101,8 @@ export default {
sortKey: 'name',
sortDirection: sortDirections.ASCENDING,
sortPredicates: {
name: function(item) {
return item.name.toLowerCase();
name: ({ name }) => {
return name.toLocaleLowerCase();
}
}
},

View File

@ -1,12 +1,14 @@
import ModelBase from 'App/ModelBase';
export interface QualityProfileFormatItem {
format: number;
name: string;
score: number;
}
interface CustomFormat {
id: number;
interface CustomFormat extends ModelBase {
name: string;
includeCustomFormatWhenRenaming: boolean;
}
export default CustomFormat;

View File

@ -9,10 +9,12 @@ namespace NzbDrone.Core.CustomFormats
public interface ICustomFormatService
{
void Update(CustomFormat customFormat);
void Update(List<CustomFormat> customFormat);
CustomFormat Insert(CustomFormat customFormat);
List<CustomFormat> All();
CustomFormat GetById(int id);
void Delete(int id);
void Delete(List<int> ids);
}
public class CustomFormatService : ICustomFormatService
@ -51,6 +53,12 @@ public void Update(CustomFormat customFormat)
_cache.Clear();
}
public void Update(List<CustomFormat> customFormat)
{
_formatRepository.UpdateMany(customFormat);
_cache.Clear();
}
public CustomFormat Insert(CustomFormat customFormat)
{
// Add to DB then insert into profiles
@ -72,5 +80,20 @@ public void Delete(int id)
_formatRepository.Delete(id);
_cache.Clear();
}
public void Delete(List<int> ids)
{
foreach (var id in ids)
{
var format = _formatRepository.Get(id);
// Remove from profiles before removing from DB
_eventAggregator.PublishEvent(new CustomFormatDeletedEvent(format));
_formatRepository.Delete(id);
}
_cache.Clear();
}
}
}

View File

@ -240,6 +240,7 @@
"CouldNotConnectSignalR": "Could not connect to SignalR, UI won't update",
"CouldNotFindResults": "Couldn't find any results for '{term}'",
"CountCollectionsSelected": "{count} collection(s) selected",
"CountCustomFormatsSelected": "{count} custom formats(s) selected",
"CountDownloadClientsSelected": "{count} download client(s) selected",
"CountImportListsSelected": "{count} import list(s) selected",
"CountIndexersSelected": "{count} indexer(s) selected",
@ -337,6 +338,8 @@
"DeleteRootFolder": "Delete Root Folder",
"DeleteRootFolderMessageText": "Are you sure you want to delete the root folder '{path}'?",
"DeleteSelected": "Delete Selected",
"DeleteSelectedCustomFormats": "Delete Custom Format(s)",
"DeleteSelectedCustomFormatsMessageText": "Are you sure you want to delete {count} selected custom format(s)?",
"DeleteSelectedDownloadClients": "Delete Download Client(s)",
"DeleteSelectedDownloadClientsMessageText": "Are you sure you want to delete {count} selected download client(s)?",
"DeleteSelectedImportListExclusionsMessageText": "Are you sure you want to delete the selected import list exclusions?",
@ -562,6 +565,7 @@
"EditReleaseProfile": "Edit Release Profile",
"EditRemotePathMapping": "Edit Remote Path Mapping",
"EditRestriction": "Edit Restriction",
"EditSelectedCustomFormats": "Edit Selected Custom Formats",
"EditSelectedDownloadClients": "Edit Selected Download Clients",
"EditSelectedImportLists": "Edit Selected Import Lists",
"EditSelectedIndexers": "Edit Selected Indexers",
@ -852,6 +856,7 @@
"MIA": "MIA",
"MaintenanceRelease": "Maintenance Release: bug fixes and other improvements. See Github Commit History for more details",
"ManageClients": "Manage Clients",
"ManageCustomFormats": "Manage Custom Formats",
"ManageDownloadClients": "Manage Download Clients",
"ManageFiles": "Manage Files",
"ManageImportLists": "Manage Import Lists",
@ -1007,6 +1012,7 @@
"NoChange": "No Change",
"NoChanges": "No Changes",
"NoCollections": "No collections found, to get started you'll want to add a new movie, or import some existing ones",
"NoCustomFormatsFound": "No custom formats found",
"NoDelay": "No Delay",
"NoDownloadClientsFound": "No download clients found",
"NoEventsFound": "No events found",

View File

@ -0,0 +1,10 @@
using System.Collections.Generic;
namespace Radarr.Api.V3.CustomFormats
{
public class CustomFormatBulkResource
{
public HashSet<int> Ids { get; set; } = new ();
public bool? IncludeCustomFormatWhenRenaming { get; set; }
}
}

View File

@ -46,6 +46,13 @@ protected override CustomFormatResource GetResourceById(int id)
return _formatService.GetById(id).ToResource(true);
}
[HttpGet]
[Produces("application/json")]
public List<CustomFormatResource> GetAll()
{
return _formatService.All().ToResource(true);
}
[RestPostById]
[Consumes("application/json")]
public ActionResult<CustomFormatResource> Create([FromBody] CustomFormatResource customFormatResource)
@ -70,11 +77,26 @@ public ActionResult<CustomFormatResource> Update([FromBody] CustomFormatResource
return Accepted(model.Id);
}
[HttpGet]
[HttpPut("bulk")]
[Consumes("application/json")]
[Produces("application/json")]
public List<CustomFormatResource> GetAll()
public virtual ActionResult<CustomFormatResource> Update([FromBody] CustomFormatBulkResource resource)
{
return _formatService.All().ToResource(true);
if (!resource.Ids.Any())
{
throw new BadRequestException("ids must be provided");
}
var customFormats = resource.Ids.Select(id => _formatService.GetById(id)).ToList();
customFormats.ForEach(existing =>
{
existing.IncludeCustomFormatWhenRenaming = resource.IncludeCustomFormatWhenRenaming ?? existing.IncludeCustomFormatWhenRenaming;
});
_formatService.Update(customFormats);
return Accepted(customFormats.ConvertAll(cf => cf.ToResource(true)));
}
[RestDeleteById]
@ -83,12 +105,21 @@ public void DeleteFormat(int id)
_formatService.Delete(id);
}
[HttpDelete("bulk")]
[Consumes("application/json")]
public virtual object DeleteFormats([FromBody] CustomFormatBulkResource resource)
{
_formatService.Delete(resource.Ids.ToList());
return new { };
}
[HttpGet("schema")]
public object GetTemplates()
{
var schema = _specifications.OrderBy(x => x.Order).Select(x => x.ToSchema()).ToList();
var presets = GetPresets();
var presets = GetPresets().ToList();
foreach (var item in schema)
{