parent
672b351497
commit
da5323a08f
@ -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;
|
@ -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;
|
||||
|
@ -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; }
|
||||
}
|
||||
}
|
Loading…
Reference in new issue