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:
parent
672b351497
commit
da5323a08f
@ -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;
|
||||
|
@ -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 />
|
||||
</>
|
||||
}
|
||||
/>
|
||||
|
@ -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;
|
@ -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;
|
||||
}
|
||||
}
|
@ -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;
|
@ -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;
|
@ -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;
|
@ -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;
|
||||
}
|
@ -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;
|
@ -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;
|
@ -0,0 +1,6 @@
|
||||
.name,
|
||||
.includeCustomFormatWhenRenaming {
|
||||
composes: cell from '~Components/Table/Cells/TableRowCell.css';
|
||||
|
||||
word-break: break-all;
|
||||
}
|
@ -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;
|
@ -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;
|
@ -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;
|
@ -13,4 +13,4 @@
|
||||
composes: button from '~Components/Link/Button.css';
|
||||
|
||||
margin-right: 10px;
|
||||
}
|
||||
}
|
||||
|
@ -13,4 +13,4 @@
|
||||
composes: button from '~Components/Link/Button.css';
|
||||
|
||||
margin-right: 10px;
|
||||
}
|
||||
}
|
||||
|
@ -13,4 +13,4 @@
|
||||
composes: button from '~Components/Link/Button.css';
|
||||
|
||||
margin-right: 10px;
|
||||
}
|
||||
}
|
||||
|
@ -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)
|
||||
}
|
||||
|
||||
};
|
||||
|
@ -96,8 +96,8 @@ export default {
|
||||
sortKey: 'name',
|
||||
sortDirection: sortDirections.ASCENDING,
|
||||
sortPredicates: {
|
||||
name: function(item) {
|
||||
return item.name.toLowerCase();
|
||||
name: ({ name }) => {
|
||||
return name.toLocaleLowerCase();
|
||||
}
|
||||
}
|
||||
},
|
||||
|
@ -101,8 +101,8 @@ export default {
|
||||
sortKey: 'name',
|
||||
sortDirection: sortDirections.ASCENDING,
|
||||
sortPredicates: {
|
||||
name: function(item) {
|
||||
return item.name.toLowerCase();
|
||||
name: ({ name }) => {
|
||||
return name.toLocaleLowerCase();
|
||||
}
|
||||
}
|
||||
},
|
||||
|
@ -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;
|
||||
|
@ -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();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -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",
|
||||
|
10
src/Radarr.Api.V3/CustomFormats/CustomFormatBulkResource.cs
Normal file
10
src/Radarr.Api.V3/CustomFormats/CustomFormatBulkResource.cs
Normal 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; }
|
||||
}
|
||||
}
|
@ -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)
|
||||
{
|
||||
|
Loading…
Reference in New Issue
Block a user