Fixed: Minor improvements and translations for managing bulk indexers, lists and clients

pull/8785/head
Bogdan 2 years ago
parent 7dde88387a
commit 48b9c1e8b9

@ -86,7 +86,7 @@ class DownloadClientSettings extends Component {
/> />
<PageToolbarButton <PageToolbarButton
label="Manage Clients" label={translate('ManageClients')}
iconName={icons.MANAGE} iconName={icons.MANAGE}
onPress={this.onManageDownloadClientsPress} onPress={this.onManageDownloadClientsPress}
/> />

@ -27,9 +27,9 @@ interface ManageDownloadClientsEditModalContentProps {
const NO_CHANGE = 'noChange'; const NO_CHANGE = 'noChange';
const enableOptions = [ const enableOptions = [
{ key: NO_CHANGE, value: 'No Change', disabled: true }, { key: NO_CHANGE, value: translate('NoChange'), disabled: true },
{ key: 'enabled', value: 'Enabled' }, { key: 'enabled', value: translate('Enabled') },
{ key: 'disabled', value: 'Disabled' }, { key: 'disabled', value: translate('Disabled') },
]; ];
function ManageDownloadClientsEditModalContent( function ManageDownloadClientsEditModalContent(
@ -97,7 +97,9 @@ function ManageDownloadClientsEditModalContent(
setRemoveFailedDownloads(value); setRemoveFailedDownloads(value);
break; break;
default: default:
console.warn('EditDownloadClientsModalContent Unknown Input'); console.warn(
`EditDownloadClientsModalContent Unknown Input: '${name}'`
);
} }
}, },
[] []
@ -162,9 +164,7 @@ function ManageDownloadClientsEditModalContent(
<ModalFooter className={styles.modalFooter}> <ModalFooter className={styles.modalFooter}>
<div className={styles.selected}> <div className={styles.selected}>
{translate('{count} download clients selected', { {translate('CountDownloadClientsSelected', [selectedCount])}
count: selectedCount,
})}
</div> </div>
<div> <div>

@ -1,6 +1,7 @@
import React, { useCallback, useMemo, useState } from 'react'; import React, { useCallback, useMemo, useState } from 'react';
import { useDispatch, useSelector } from 'react-redux'; import { useDispatch, useSelector } from 'react-redux';
import { DownloadClientAppState } from 'App/State/SettingsAppState'; import { DownloadClientAppState } from 'App/State/SettingsAppState';
import Alert from 'Components/Alert';
import Button from 'Components/Link/Button'; import Button from 'Components/Link/Button';
import SpinnerButton from 'Components/Link/SpinnerButton'; import SpinnerButton from 'Components/Link/SpinnerButton';
import LoadingIndicator from 'Components/Loading/LoadingIndicator'; import LoadingIndicator from 'Components/Loading/LoadingIndicator';
@ -20,6 +21,7 @@ import {
import createClientSideCollectionSelector from 'Store/Selectors/createClientSideCollectionSelector'; import createClientSideCollectionSelector from 'Store/Selectors/createClientSideCollectionSelector';
import { SelectStateInputProps } from 'typings/props'; import { SelectStateInputProps } from 'typings/props';
import getErrorMessage from 'Utilities/Object/getErrorMessage'; import getErrorMessage from 'Utilities/Object/getErrorMessage';
import translate from 'Utilities/String/translate';
import getSelectedIds from 'Utilities/Table/getSelectedIds'; import getSelectedIds from 'Utilities/Table/getSelectedIds';
import ManageDownloadClientsEditModal from './Edit/ManageDownloadClientsEditModal'; import ManageDownloadClientsEditModal from './Edit/ManageDownloadClientsEditModal';
import ManageDownloadClientsModalRow from './ManageDownloadClientsModalRow'; import ManageDownloadClientsModalRow from './ManageDownloadClientsModalRow';
@ -33,37 +35,37 @@ type OnSelectedChangeCallback = React.ComponentProps<
const COLUMNS = [ const COLUMNS = [
{ {
name: 'name', name: 'name',
label: 'Name', label: translate('Name'),
isSortable: true, isSortable: true,
isVisible: true, isVisible: true,
}, },
{ {
name: 'implementation', name: 'implementation',
label: 'Implementation', label: translate('Implementation'),
isSortable: true, isSortable: true,
isVisible: true, isVisible: true,
}, },
{ {
name: 'enable', name: 'enable',
label: 'Enabled', label: translate('Enabled'),
isSortable: true, isSortable: true,
isVisible: true, isVisible: true,
}, },
{ {
name: 'priority', name: 'priority',
label: 'Priority', label: translate('Priority'),
isSortable: true, isSortable: true,
isVisible: true, isVisible: true,
}, },
{ {
name: 'removeCompletedDownloads', name: 'removeCompletedDownloads',
label: 'Remove Completed', label: translate('RemoveCompleted'),
isSortable: true, isSortable: true,
isVisible: true, isVisible: true,
}, },
{ {
name: 'removeFailedDownloads', name: 'removeFailedDownloads',
label: 'Remove Failed', label: translate('RemoveFailed'),
isSortable: true, isSortable: true,
isVisible: true, isVisible: true,
}, },
@ -158,17 +160,24 @@ function ManageDownloadClientsModalContent(
[items, setSelectState] [items, setSelectState]
); );
const errorMessage = getErrorMessage(error, 'Unable to load import lists.'); const errorMessage = getErrorMessage(
error,
'Unable to load download clients.'
);
const anySelected = selectedCount > 0; const anySelected = selectedCount > 0;
return ( return (
<ModalContent onModalClose={onModalClose}> <ModalContent onModalClose={onModalClose}>
<ModalHeader>Manage Import Lists</ModalHeader> <ModalHeader>{translate('ManageDownloadClients')}</ModalHeader>
<ModalBody> <ModalBody>
{isFetching ? <LoadingIndicator /> : null} {isFetching ? <LoadingIndicator /> : null}
{error ? <div>{errorMessage}</div> : null} {error ? <div>{errorMessage}</div> : null}
{isPopulated && !error && !items.length && (
<Alert kind={kinds.INFO}>{translate('NoDownloadClientsFound')}</Alert>
)}
{isPopulated && !!items.length && !isFetching && !isFetching ? ( {isPopulated && !!items.length && !isFetching && !isFetching ? (
<Table <Table
columns={COLUMNS} columns={COLUMNS}
@ -203,7 +212,7 @@ function ManageDownloadClientsModalContent(
isDisabled={!anySelected} isDisabled={!anySelected}
onPress={onDeletePress} onPress={onDeletePress}
> >
Delete {translate('Delete')}
</SpinnerButton> </SpinnerButton>
<SpinnerButton <SpinnerButton
@ -211,11 +220,11 @@ function ManageDownloadClientsModalContent(
isDisabled={!anySelected} isDisabled={!anySelected}
onPress={onEditPress} onPress={onEditPress}
> >
Edit {translate('Edit')}
</SpinnerButton> </SpinnerButton>
</div> </div>
<Button onPress={onModalClose}>Close</Button> <Button onPress={onModalClose}>{translate('Close')}</Button>
</ModalFooter> </ModalFooter>
<ManageDownloadClientsEditModal <ManageDownloadClientsEditModal
@ -228,9 +237,11 @@ function ManageDownloadClientsModalContent(
<ConfirmModal <ConfirmModal
isOpen={isDeleteModalOpen} isOpen={isDeleteModalOpen}
kind={kinds.DANGER} kind={kinds.DANGER}
title="Delete Download Clients(s)" title={translate('DeleteSelectedDownloadClients')}
message={`Are you sure you want to delete ${selectedIds.length} download clients(s)?`} message={translate('DeleteSelectedDownloadClientsMessageText', [
confirmLabel="Delete" selectedIds.length,
])}
confirmLabel={translate('Delete')}
onConfirm={onConfirmDelete} onConfirm={onConfirmDelete}
onCancel={onDeleteModalClose} onCancel={onDeleteModalClose}
/> />

@ -1,9 +1,12 @@
import React, { useCallback } from 'react'; import React, { useCallback } from 'react';
import Label from 'Components/Label';
import TableRowCell from 'Components/Table/Cells/TableRowCell'; import TableRowCell from 'Components/Table/Cells/TableRowCell';
import TableSelectCell from 'Components/Table/Cells/TableSelectCell'; import TableSelectCell from 'Components/Table/Cells/TableSelectCell';
import Column from 'Components/Table/Column'; import Column from 'Components/Table/Column';
import TableRow from 'Components/Table/TableRow'; import TableRow from 'Components/Table/TableRow';
import { kinds } from 'Helpers/Props';
import { SelectStateInputProps } from 'typings/props'; import { SelectStateInputProps } from 'typings/props';
import translate from 'Utilities/String/translate';
import styles from './ManageDownloadClientsModalRow.css'; import styles from './ManageDownloadClientsModalRow.css';
interface ManageDownloadClientsModalRowProps { interface ManageDownloadClientsModalRowProps {
@ -58,17 +61,19 @@ function ManageDownloadClientsModalRow(
</TableRowCell> </TableRowCell>
<TableRowCell className={styles.enable}> <TableRowCell className={styles.enable}>
{enable ? 'Yes' : 'No'} <Label kind={enable ? kinds.SUCCESS : kinds.DISABLED} outline={!enable}>
{enable ? translate('Yes') : translate('No')}
</Label>
</TableRowCell> </TableRowCell>
<TableRowCell className={styles.priority}>{priority}</TableRowCell> <TableRowCell className={styles.priority}>{priority}</TableRowCell>
<TableRowCell className={styles.removeCompletedDownloads}> <TableRowCell className={styles.removeCompletedDownloads}>
{removeCompletedDownloads ? 'Yes' : 'No'} {removeCompletedDownloads ? translate('Yes') : translate('No')}
</TableRowCell> </TableRowCell>
<TableRowCell className={styles.removeFailedDownloads}> <TableRowCell className={styles.removeFailedDownloads}>
{removeFailedDownloads ? 'Yes' : 'No'} {removeFailedDownloads ? translate('Yes') : translate('No')}
</TableRowCell> </TableRowCell>
</TableRow> </TableRow>
); );

@ -86,7 +86,7 @@ class ImportListSettings extends Component {
/> />
<PageToolbarButton <PageToolbarButton
label="Manage Lists" label={translate('ManageLists')}
iconName={icons.MANAGE} iconName={icons.MANAGE}
onPress={this.onManageImportListsPress} onPress={this.onManageImportListsPress}
/> />

@ -26,9 +26,9 @@ interface ManageImportListsEditModalContentProps {
const NO_CHANGE = 'noChange'; const NO_CHANGE = 'noChange';
const autoAddOptions = [ const autoAddOptions = [
{ key: NO_CHANGE, value: 'No Change', disabled: true }, { key: NO_CHANGE, value: translate('NoChange'), disabled: true },
{ key: 'enabled', value: 'Enabled' }, { key: 'enabled', value: translate('Enabled') },
{ key: 'disabled', value: 'Disabled' }, { key: 'disabled', value: translate('Disabled') },
]; ];
function ManageImportListsEditModalContent( function ManageImportListsEditModalContent(
@ -36,7 +36,7 @@ function ManageImportListsEditModalContent(
) { ) {
const { importListIds, onSavePress, onModalClose } = props; const { importListIds, onSavePress, onModalClose } = props;
const [enableAuto, setenableAuto] = useState(NO_CHANGE); const [enableAuto, setEnableAuto] = useState(NO_CHANGE);
const [qualityProfileId, setQualityProfileId] = useState<string | number>( const [qualityProfileId, setQualityProfileId] = useState<string | number>(
NO_CHANGE NO_CHANGE
); );
@ -72,7 +72,7 @@ function ManageImportListsEditModalContent(
({ name, value }: { name: string; value: string }) => { ({ name, value }: { name: string; value: string }) => {
switch (name) { switch (name) {
case 'enableAuto': case 'enableAuto':
setenableAuto(value); setEnableAuto(value);
break; break;
case 'qualityProfileId': case 'qualityProfileId':
setQualityProfileId(value); setQualityProfileId(value);
@ -81,7 +81,7 @@ function ManageImportListsEditModalContent(
setRootFolderPath(value); setRootFolderPath(value);
break; break;
default: default:
console.warn('EditImportListModalContent Unknown Input'); console.warn(`EditImportListModalContent Unknown Input: '${name}'`);
} }
}, },
[] []
@ -136,7 +136,7 @@ function ManageImportListsEditModalContent(
<ModalFooter className={styles.modalFooter}> <ModalFooter className={styles.modalFooter}>
<div className={styles.selected}> <div className={styles.selected}>
{translate('{count} import lists selected', { count: selectedCount })} {translate('CountImportListsSelected', [selectedCount])}
</div> </div>
<div> <div>

@ -1,6 +1,7 @@
import React, { useCallback, useMemo, useState } from 'react'; import React, { useCallback, useMemo, useState } from 'react';
import { useDispatch, useSelector } from 'react-redux'; import { useDispatch, useSelector } from 'react-redux';
import { ImportListAppState } from 'App/State/SettingsAppState'; import { ImportListAppState } from 'App/State/SettingsAppState';
import Alert from 'Components/Alert';
import Button from 'Components/Link/Button'; import Button from 'Components/Link/Button';
import SpinnerButton from 'Components/Link/SpinnerButton'; import SpinnerButton from 'Components/Link/SpinnerButton';
import LoadingIndicator from 'Components/Loading/LoadingIndicator'; import LoadingIndicator from 'Components/Loading/LoadingIndicator';
@ -20,6 +21,7 @@ import {
import createClientSideCollectionSelector from 'Store/Selectors/createClientSideCollectionSelector'; import createClientSideCollectionSelector from 'Store/Selectors/createClientSideCollectionSelector';
import { SelectStateInputProps } from 'typings/props'; import { SelectStateInputProps } from 'typings/props';
import getErrorMessage from 'Utilities/Object/getErrorMessage'; import getErrorMessage from 'Utilities/Object/getErrorMessage';
import translate from 'Utilities/String/translate';
import getSelectedIds from 'Utilities/Table/getSelectedIds'; import getSelectedIds from 'Utilities/Table/getSelectedIds';
import ManageImportListsEditModal from './Edit/ManageImportListsEditModal'; import ManageImportListsEditModal from './Edit/ManageImportListsEditModal';
import ManageImportListsModalRow from './ManageImportListsModalRow'; import ManageImportListsModalRow from './ManageImportListsModalRow';
@ -34,37 +36,37 @@ type OnSelectedChangeCallback = React.ComponentProps<
const COLUMNS = [ const COLUMNS = [
{ {
name: 'name', name: 'name',
label: 'Name', label: translate('Name'),
isSortable: true, isSortable: true,
isVisible: true, isVisible: true,
}, },
{ {
name: 'implementation', name: 'implementation',
label: 'Implementation', label: translate('Implementation'),
isSortable: true, isSortable: true,
isVisible: true, isVisible: true,
}, },
{ {
name: 'qualityProfileId', name: 'qualityProfileId',
label: 'Quality Profile', label: translate('QualityProfile'),
isSortable: true, isSortable: true,
isVisible: true, isVisible: true,
}, },
{ {
name: 'rootFolderPath', name: 'rootFolderPath',
label: 'Root Folder', label: translate('RootFolder'),
isSortable: true, isSortable: true,
isVisible: true, isVisible: true,
}, },
{ {
name: 'enableAuto', name: 'enableAuto',
label: 'Auto Add', label: translate('AutomaticAdd'),
isSortable: true, isSortable: true,
isVisible: true, isVisible: true,
}, },
{ {
name: 'tags', name: 'tags',
label: 'Tags', label: translate('Tags'),
isSortable: true, isSortable: true,
isVisible: true, isVisible: true,
}, },
@ -190,12 +192,16 @@ function ManageImportListsModalContent(
return ( return (
<ModalContent onModalClose={onModalClose}> <ModalContent onModalClose={onModalClose}>
<ModalHeader>Manage Import Lists</ModalHeader> <ModalHeader>{translate('ManageImportLists')}</ModalHeader>
<ModalBody> <ModalBody>
{isFetching ? <LoadingIndicator /> : null} {isFetching ? <LoadingIndicator /> : null}
{error ? <div>{errorMessage}</div> : null} {error ? <div>{errorMessage}</div> : null}
{isPopulated && !error && !items.length && (
<Alert kind={kinds.INFO}>{translate('NoImportListsFound')}</Alert>
)}
{isPopulated && !!items.length && !isFetching && !isFetching ? ( {isPopulated && !!items.length && !isFetching && !isFetching ? (
<Table <Table
columns={COLUMNS} columns={COLUMNS}
@ -230,7 +236,7 @@ function ManageImportListsModalContent(
isDisabled={!anySelected} isDisabled={!anySelected}
onPress={onDeletePress} onPress={onDeletePress}
> >
Delete {translate('Delete')}
</SpinnerButton> </SpinnerButton>
<SpinnerButton <SpinnerButton
@ -238,7 +244,7 @@ function ManageImportListsModalContent(
isDisabled={!anySelected} isDisabled={!anySelected}
onPress={onEditPress} onPress={onEditPress}
> >
Edit {translate('Edit')}
</SpinnerButton> </SpinnerButton>
<SpinnerButton <SpinnerButton
@ -246,11 +252,11 @@ function ManageImportListsModalContent(
isDisabled={!anySelected} isDisabled={!anySelected}
onPress={onTagsPress} onPress={onTagsPress}
> >
Set Tags {translate('SetTags')}
</SpinnerButton> </SpinnerButton>
</div> </div>
<Button onPress={onModalClose}>Close</Button> <Button onPress={onModalClose}>{translate('Close')}</Button>
</ModalFooter> </ModalFooter>
<ManageImportListsEditModal <ManageImportListsEditModal
@ -270,9 +276,11 @@ function ManageImportListsModalContent(
<ConfirmModal <ConfirmModal
isOpen={isDeleteModalOpen} isOpen={isDeleteModalOpen}
kind={kinds.DANGER} kind={kinds.DANGER}
title="Delete Import List(s)" title={translate('DeleteSelectedImportLists')}
message={`Are you sure you want to delete ${selectedIds.length} import list(s)?`} message={translate('DeleteSelectedImportListsMessageText', [
confirmLabel="Delete" selectedIds.length,
])}
confirmLabel={translate('Delete')}
onConfirm={onConfirmDelete} onConfirm={onConfirmDelete}
onCancel={onDeleteModalClose} onCancel={onDeleteModalClose}
/> />

@ -17,6 +17,7 @@ import ModalHeader from 'Components/Modal/ModalHeader';
import { inputTypes, kinds, sizes } from 'Helpers/Props'; import { inputTypes, kinds, sizes } from 'Helpers/Props';
import createTagsSelector from 'Store/Selectors/createTagsSelector'; import createTagsSelector from 'Store/Selectors/createTagsSelector';
import ImportList from 'typings/ImportList'; import ImportList from 'typings/ImportList';
import translate from 'Utilities/String/translate';
import styles from './TagsModalContent.css'; import styles from './TagsModalContent.css';
interface TagsModalContentProps { interface TagsModalContentProps {
@ -36,7 +37,7 @@ function TagsModalContent(props: TagsModalContentProps) {
const [tags, setTags] = useState<number[]>([]); const [tags, setTags] = useState<number[]>([]);
const [applyTags, setApplyTags] = useState('add'); const [applyTags, setApplyTags] = useState('add');
const seriesTags = useMemo(() => { const importListsTags = useMemo(() => {
const tags = ids.reduce((acc: number[], id) => { const tags = ids.reduce((acc: number[], id) => {
const s = allImportLists.items.find((s: ImportList) => s.id === id); const s = allImportLists.items.find((s: ImportList) => s.id === id);
@ -69,19 +70,19 @@ function TagsModalContent(props: TagsModalContentProps) {
}, [tags, applyTags, onApplyTagsPress]); }, [tags, applyTags, onApplyTagsPress]);
const applyTagsOptions = [ const applyTagsOptions = [
{ key: 'add', value: 'Add' }, { key: 'add', value: translate('Add') },
{ key: 'remove', value: 'Remove' }, { key: 'remove', value: translate('Remove') },
{ key: 'replace', value: 'Replace' }, { key: 'replace', value: translate('Replace') },
]; ];
return ( return (
<ModalContent onModalClose={onModalClose}> <ModalContent onModalClose={onModalClose}>
<ModalHeader>Tags</ModalHeader> <ModalHeader>{translate('Tags')}</ModalHeader>
<ModalBody> <ModalBody>
<Form> <Form>
<FormGroup> <FormGroup>
<FormLabel>Tags</FormLabel> <FormLabel>{translate('Tags')}</FormLabel>
<FormInputGroup <FormInputGroup
type={inputTypes.TAG} type={inputTypes.TAG}
@ -92,7 +93,7 @@ function TagsModalContent(props: TagsModalContentProps) {
</FormGroup> </FormGroup>
<FormGroup> <FormGroup>
<FormLabel>Apply Tags</FormLabel> <FormLabel>{translate('ApplyTags')}</FormLabel>
<FormInputGroup <FormInputGroup
type={inputTypes.SELECT} type={inputTypes.SELECT}
@ -100,20 +101,20 @@ function TagsModalContent(props: TagsModalContentProps) {
value={applyTags} value={applyTags}
values={applyTagsOptions} values={applyTagsOptions}
helpTexts={[ helpTexts={[
'How to apply tags to the selected list', translate('ApplyTagsHelpTexts1'),
'Add: Add the tags the existing list of tags', translate('ApplyTagsHelpTexts2'),
'Remove: Remove the entered tags', translate('ApplyTagsHelpTexts3'),
'Replace: Replace the tags with the entered tags (enter no tags to clear all tags)', translate('ApplyTagsHelpTexts4'),
]} ]}
onChange={onApplyTagsChange} onChange={onApplyTagsChange}
/> />
</FormGroup> </FormGroup>
<FormGroup> <FormGroup>
<FormLabel>Result</FormLabel> <FormLabel>{translate('Result')}</FormLabel>
<div className={styles.result}> <div className={styles.result}>
{seriesTags.map((id) => { {importListsTags.map((id) => {
const tag = tagList.find((t) => t.id === id); const tag = tagList.find((t) => t.id === id);
if (!tag) { if (!tag) {
@ -127,7 +128,11 @@ function TagsModalContent(props: TagsModalContentProps) {
return ( return (
<Label <Label
key={tag.id} key={tag.id}
title={removeTag ? 'Removing tag' : 'Existing tag'} title={
removeTag
? translate('RemovingTag')
: translate('ExistingTag')
}
kind={removeTag ? kinds.INVERSE : kinds.INFO} kind={removeTag ? kinds.INVERSE : kinds.INFO}
size={sizes.LARGE} size={sizes.LARGE}
> >
@ -144,14 +149,14 @@ function TagsModalContent(props: TagsModalContentProps) {
return null; return null;
} }
if (seriesTags.indexOf(id) > -1) { if (importListsTags.indexOf(id) > -1) {
return null; return null;
} }
return ( return (
<Label <Label
key={tag.id} key={tag.id}
title={'Adding tag'} title={translate('AddingTag')}
kind={kinds.SUCCESS} kind={kinds.SUCCESS}
size={sizes.LARGE} size={sizes.LARGE}
> >
@ -165,10 +170,10 @@ function TagsModalContent(props: TagsModalContentProps) {
</ModalBody> </ModalBody>
<ModalFooter> <ModalFooter>
<Button onPress={onModalClose}>Cancel</Button> <Button onPress={onModalClose}>{translate('Cancel')}</Button>
<Button kind={kinds.PRIMARY} onPress={onApplyPress}> <Button kind={kinds.PRIMARY} onPress={onApplyPress}>
Apply {translate('Apply')}
</Button> </Button>
</ModalFooter> </ModalFooter>
</ModalContent> </ModalContent>

@ -86,7 +86,7 @@ class IndexerSettings extends Component {
/> />
<PageToolbarButton <PageToolbarButton
label="Manage Indexers" label={translate('ManageIndexers')}
iconName={icons.MANAGE} iconName={icons.MANAGE}
onPress={this.onManageIndexersPress} onPress={this.onManageIndexersPress}
/> />

@ -1,6 +1,7 @@
import React, { useCallback, useMemo, useState } from 'react'; import React, { useCallback, useMemo, useState } from 'react';
import { useDispatch, useSelector } from 'react-redux'; import { useDispatch, useSelector } from 'react-redux';
import { IndexerAppState } from 'App/State/SettingsAppState'; import { IndexerAppState } from 'App/State/SettingsAppState';
import Alert from 'Components/Alert';
import Button from 'Components/Link/Button'; import Button from 'Components/Link/Button';
import SpinnerButton from 'Components/Link/SpinnerButton'; import SpinnerButton from 'Components/Link/SpinnerButton';
import LoadingIndicator from 'Components/Loading/LoadingIndicator'; import LoadingIndicator from 'Components/Loading/LoadingIndicator';
@ -201,6 +202,10 @@ function ManageIndexersModalContent(props: ManageIndexersModalContentProps) {
{error ? <div>{errorMessage}</div> : null} {error ? <div>{errorMessage}</div> : null}
{isPopulated && !error && !items.length && (
<Alert kind={kinds.INFO}>{translate('NoIndexersFound')}</Alert>
)}
{isPopulated && !!items.length && !isFetching && !isFetching ? ( {isPopulated && !!items.length && !isFetching && !isFetching ? (
<Table <Table
columns={COLUMNS} columns={COLUMNS}
@ -275,8 +280,10 @@ function ManageIndexersModalContent(props: ManageIndexersModalContentProps) {
<ConfirmModal <ConfirmModal
isOpen={isDeleteModalOpen} isOpen={isDeleteModalOpen}
kind={kinds.DANGER} kind={kinds.DANGER}
title={translate('DeleteIndexers')} title={translate('DeleteSelectedIndexers')}
message={translate('DeleteIndexersMessageText', [selectedIds.length])} message={translate('DeleteSelectedIndexersMessageText', [
selectedIds.length,
])}
confirmLabel={translate('Delete')} confirmLabel={translate('Delete')}
onConfirm={onConfirmDelete} onConfirm={onConfirmDelete}
onCancel={onDeleteModalClose} onCancel={onDeleteModalClose}

@ -1,9 +1,11 @@
import React, { useCallback } from 'react'; import React, { useCallback } from 'react';
import Label from 'Components/Label';
import TableRowCell from 'Components/Table/Cells/TableRowCell'; import TableRowCell from 'Components/Table/Cells/TableRowCell';
import TableSelectCell from 'Components/Table/Cells/TableSelectCell'; import TableSelectCell from 'Components/Table/Cells/TableSelectCell';
import Column from 'Components/Table/Column'; import Column from 'Components/Table/Column';
import TableRow from 'Components/Table/TableRow'; import TableRow from 'Components/Table/TableRow';
import TagListConnector from 'Components/TagListConnector'; import TagListConnector from 'Components/TagListConnector';
import { kinds } from 'Helpers/Props';
import { SelectStateInputProps } from 'typings/props'; import { SelectStateInputProps } from 'typings/props';
import translate from 'Utilities/String/translate'; import translate from 'Utilities/String/translate';
import styles from './ManageIndexersModalRow.css'; import styles from './ManageIndexersModalRow.css';
@ -60,15 +62,30 @@ function ManageIndexersModalRow(props: ManageIndexersModalRowProps) {
</TableRowCell> </TableRowCell>
<TableRowCell className={styles.enableRss}> <TableRowCell className={styles.enableRss}>
{enableRss ? translate('Yes') : translate('No')} <Label
kind={enableRss ? kinds.SUCCESS : kinds.DISABLED}
outline={!enableRss}
>
{enableRss ? translate('Yes') : translate('No')}
</Label>
</TableRowCell> </TableRowCell>
<TableRowCell className={styles.enableAutomaticSearch}> <TableRowCell className={styles.enableAutomaticSearch}>
{enableAutomaticSearch ? translate('Yes') : translate('No')} <Label
kind={enableAutomaticSearch ? kinds.SUCCESS : kinds.DISABLED}
outline={!enableAutomaticSearch}
>
{enableAutomaticSearch ? translate('Yes') : translate('No')}
</Label>
</TableRowCell> </TableRowCell>
<TableRowCell className={styles.enableInteractiveSearch}> <TableRowCell className={styles.enableInteractiveSearch}>
{enableInteractiveSearch ? translate('Yes') : translate('No')} <Label
kind={enableInteractiveSearch ? kinds.SUCCESS : kinds.DISABLED}
outline={!enableInteractiveSearch}
>
{enableInteractiveSearch ? translate('Yes') : translate('No')}
</Label>
</TableRowCell> </TableRowCell>
<TableRowCell className={styles.priority}>{priority}</TableRowCell> <TableRowCell className={styles.priority}>{priority}</TableRowCell>

@ -37,7 +37,7 @@ function TagsModalContent(props: TagsModalContentProps) {
const [tags, setTags] = useState<number[]>([]); const [tags, setTags] = useState<number[]>([]);
const [applyTags, setApplyTags] = useState('add'); const [applyTags, setApplyTags] = useState('add');
const seriesTags = useMemo(() => { const indexersTags = useMemo(() => {
const tags = ids.reduce((acc: number[], id) => { const tags = ids.reduce((acc: number[], id) => {
const s = allIndexers.items.find((s: Indexer) => s.id === id); const s = allIndexers.items.find((s: Indexer) => s.id === id);
@ -101,10 +101,10 @@ function TagsModalContent(props: TagsModalContentProps) {
value={applyTags} value={applyTags}
values={applyTagsOptions} values={applyTagsOptions}
helpTexts={[ helpTexts={[
'How to apply tags to the selected indexer(s)', translate('ApplyTagsHelpTexts1'),
'Add: Add the tags the existing list of tags', translate('ApplyTagsHelpTexts2'),
'Remove: Remove the entered tags', translate('ApplyTagsHelpTexts3'),
'Replace: Replace the tags with the entered tags (enter no tags to clear all tags)', translate('ApplyTagsHelpTexts4'),
]} ]}
onChange={onApplyTagsChange} onChange={onApplyTagsChange}
/> />
@ -114,7 +114,7 @@ function TagsModalContent(props: TagsModalContentProps) {
<FormLabel>{translate('Result')}</FormLabel> <FormLabel>{translate('Result')}</FormLabel>
<div className={styles.result}> <div className={styles.result}>
{seriesTags.map((id) => { {indexersTags.map((id) => {
const tag = tagList.find((t) => t.id === id); const tag = tagList.find((t) => t.id === id);
if (!tag) { if (!tag) {
@ -149,7 +149,7 @@ function TagsModalContent(props: TagsModalContentProps) {
return null; return null;
} }
if (seriesTags.indexOf(id) > -1) { if (indexersTags.indexOf(id) > -1) {
return null; return null;
} }

@ -71,6 +71,7 @@
"AutoRedownloadFailedHelpText": "Automatically search for and attempt to download a different release", "AutoRedownloadFailedHelpText": "Automatically search for and attempt to download a different release",
"AutoUnmonitorPreviouslyDownloadedMoviesHelpText": "Movies deleted from the disk are automatically unmonitored in Radarr", "AutoUnmonitorPreviouslyDownloadedMoviesHelpText": "Movies deleted from the disk are automatically unmonitored in Radarr",
"Automatic": "Automatic", "Automatic": "Automatic",
"AutomaticAdd": "Automatic Add",
"AutomaticSearch": "Automatic Search", "AutomaticSearch": "Automatic Search",
"AvailabilityDelay": "Availability Delay", "AvailabilityDelay": "Availability Delay",
"AvailabilityDelayHelpText": "Amount of time before or after available date to search for Movie", "AvailabilityDelayHelpText": "Amount of time before or after available date to search for Movie",
@ -161,7 +162,9 @@
"CopyUsingHardlinksHelpTextWarning": "Occasionally, file locks may prevent renaming files that are being seeded. You may temporarily disable seeding and use Radarr's rename function as a work around.", "CopyUsingHardlinksHelpTextWarning": "Occasionally, file locks may prevent renaming files that are being seeded. You may temporarily disable seeding and use Radarr's rename function as a work around.",
"CouldNotConnectSignalR": "Could not connect to SignalR, UI won't update", "CouldNotConnectSignalR": "Could not connect to SignalR, UI won't update",
"CouldNotFindResults": "Couldn't find any results for '{0}'", "CouldNotFindResults": "Couldn't find any results for '{0}'",
"CountIndexersSelected": "{0} indexers selected", "CountDownloadClientsSelected": "{0} download client(s) selected",
"CountImportListsSelected": "{0} import list(s) selected",
"CountIndexersSelected": "{0} indexer(s) selected",
"CreateEmptyMovieFolders": "Create empty movie folders", "CreateEmptyMovieFolders": "Create empty movie folders",
"CreateEmptyMovieFoldersHelpText": "Create missing movie folders during disk scan", "CreateEmptyMovieFoldersHelpText": "Create missing movie folders during disk scan",
"CreateGroup": "Create group", "CreateGroup": "Create group",
@ -230,6 +233,12 @@
"DeleteRemotePathMappingMessageText": "Are you sure you want to delete this remote path mapping?", "DeleteRemotePathMappingMessageText": "Are you sure you want to delete this remote path mapping?",
"DeleteRestriction": "Delete Restriction", "DeleteRestriction": "Delete Restriction",
"DeleteRestrictionHelpText": "Are you sure you want to delete this restriction?", "DeleteRestrictionHelpText": "Are you sure you want to delete this restriction?",
"DeleteSelectedDownloadClients": "Delete Download Client(s)",
"DeleteSelectedDownloadClientsMessageText": "Are you sure you want to delete {0} selected download client(s)?",
"DeleteSelectedImportLists": "Delete Import List(s)",
"DeleteSelectedImportListsMessageText": "Are you sure you want to delete {0} selected import list(s)?",
"DeleteSelectedIndexers": "Delete Indexer(s)",
"DeleteSelectedIndexersMessageText": "Are you sure you want to delete {0} selected indexer(s)?",
"DeleteSelectedMovie": "Delete Selected Movie(s)", "DeleteSelectedMovie": "Delete Selected Movie(s)",
"DeleteSelectedMovieFiles": "Delete Selected Movie Files", "DeleteSelectedMovieFiles": "Delete Selected Movie Files",
"DeleteSelectedMovieFilesMessage": "Are you sure you want to delete the selected movie files?", "DeleteSelectedMovieFilesMessage": "Are you sure you want to delete the selected movie files?",
@ -295,6 +304,8 @@
"EditQualityProfile": "Edit Quality Profile", "EditQualityProfile": "Edit Quality Profile",
"EditRemotePathMapping": "Edit Remote Path Mapping", "EditRemotePathMapping": "Edit Remote Path Mapping",
"EditRestriction": "Edit Restriction", "EditRestriction": "Edit Restriction",
"EditSelectedDownloadClients": "Edit Selected Download Clients",
"EditSelectedImportLists": "Edit Selected Import Lists",
"EditSelectedIndexers": "Edit Selected Indexers", "EditSelectedIndexers": "Edit Selected Indexers",
"EditSelectedMovies": "Edit Selected Movies", "EditSelectedMovies": "Edit Selected Movies",
"Edition": "Edition", "Edition": "Edition",
@ -406,6 +417,7 @@
"Images": "Images", "Images": "Images",
"ImdbRating": "IMDb Rating", "ImdbRating": "IMDb Rating",
"ImdbVotes": "IMDb Votes", "ImdbVotes": "IMDb Votes",
"Implementation": "Implementation",
"Import": "Import", "Import": "Import",
"ImportCustomFormat": "Import Custom Format", "ImportCustomFormat": "Import Custom Format",
"ImportErrors": "Import Errors", "ImportErrors": "Import Errors",
@ -514,7 +526,11 @@
"LowerCase": "Lowercase", "LowerCase": "Lowercase",
"MIA": "MIA", "MIA": "MIA",
"MaintenanceRelease": "Maintenance Release: bug fixes and other improvements. See Github Commit History for more details", "MaintenanceRelease": "Maintenance Release: bug fixes and other improvements. See Github Commit History for more details",
"ManageClients": "Manage Clients",
"ManageDownloadClients": "Manage Download Clients",
"ManageImportLists": "Manage Import Lists",
"ManageIndexers": "Manage Indexers", "ManageIndexers": "Manage Indexers",
"ManageLists": "Manage Lists",
"Manual": "Manual", "Manual": "Manual",
"ManualImport": "Manual Import", "ManualImport": "Manual Import",
"ManualImportSelectLanguage": "Manual Import - Select Language", "ManualImportSelectLanguage": "Manual Import - Select Language",
@ -632,9 +648,12 @@
"NoChange": "No Change", "NoChange": "No Change",
"NoChanges": "No Changes", "NoChanges": "No Changes",
"NoCollections": "No collections found, to get started you'll want to add a new movie, or import some existing ones", "NoCollections": "No collections found, to get started you'll want to add a new movie, or import some existing ones",
"NoDownloadClientsFound": "No download clients found",
"NoEventsFound": "No events found", "NoEventsFound": "No events found",
"NoHistory": "No history", "NoHistory": "No history",
"NoHistoryBlocklist": "No history blocklist", "NoHistoryBlocklist": "No history blocklist",
"NoImportListsFound": "No import lists found",
"NoIndexersFound": "No indexers found",
"NoLeaveIt": "No, Leave It", "NoLeaveIt": "No, Leave It",
"NoLimitForAnyRuntime": "No limit for any runtime", "NoLimitForAnyRuntime": "No limit for any runtime",
"NoLinks": "No Links", "NoLinks": "No Links",

@ -6,8 +6,8 @@ namespace Radarr.Api.V3.DownloadClient
[V3ApiController] [V3ApiController]
public class DownloadClientController : ProviderControllerBase<DownloadClientResource, DownloadClientBulkResource, IDownloadClient, DownloadClientDefinition> public class DownloadClientController : ProviderControllerBase<DownloadClientResource, DownloadClientBulkResource, IDownloadClient, DownloadClientDefinition>
{ {
public static readonly DownloadClientResourceMapper ResourceMapper = new DownloadClientResourceMapper(); public static readonly DownloadClientResourceMapper ResourceMapper = new ();
public static readonly DownloadClientBulkResourceMapper BulkResourceMapper = new DownloadClientBulkResourceMapper(); public static readonly DownloadClientBulkResourceMapper BulkResourceMapper = new ();
public DownloadClientController(IDownloadClientFactory downloadClientFactory) public DownloadClientController(IDownloadClientFactory downloadClientFactory)
: base(downloadClientFactory, "downloadclient", ResourceMapper, BulkResourceMapper) : base(downloadClientFactory, "downloadclient", ResourceMapper, BulkResourceMapper)

@ -9,8 +9,8 @@ namespace Radarr.Api.V3.ImportLists
[V3ApiController] [V3ApiController]
public class ImportListController : ProviderControllerBase<ImportListResource, ImportListBulkResource, IImportList, ImportListDefinition> public class ImportListController : ProviderControllerBase<ImportListResource, ImportListBulkResource, IImportList, ImportListDefinition>
{ {
public static readonly ImportListResourceMapper ResourceMapper = new ImportListResourceMapper(); public static readonly ImportListResourceMapper ResourceMapper = new ();
public static readonly ImportListBulkResourceMapper BulkResourceMapper = new ImportListBulkResourceMapper(); public static readonly ImportListBulkResourceMapper BulkResourceMapper = new ();
public ImportListController(IImportListFactory importListFactory, ProfileExistsValidator profileExistsValidator) public ImportListController(IImportListFactory importListFactory, ProfileExistsValidator profileExistsValidator)
: base(importListFactory, "importlist", ResourceMapper, BulkResourceMapper) : base(importListFactory, "importlist", ResourceMapper, BulkResourceMapper)

@ -6,8 +6,8 @@ namespace Radarr.Api.V3.Indexers
[V3ApiController] [V3ApiController]
public class IndexerController : ProviderControllerBase<IndexerResource, IndexerBulkResource, IIndexer, IndexerDefinition> public class IndexerController : ProviderControllerBase<IndexerResource, IndexerBulkResource, IIndexer, IndexerDefinition>
{ {
public static readonly IndexerResourceMapper ResourceMapper = new IndexerResourceMapper(); public static readonly IndexerResourceMapper ResourceMapper = new ();
public static readonly IndexerBulkResourceMapper BulkResourceMapper = new IndexerBulkResourceMapper(); public static readonly IndexerBulkResourceMapper BulkResourceMapper = new ();
public IndexerController(IndexerFactory indexerFactory) public IndexerController(IndexerFactory indexerFactory)
: base(indexerFactory, "indexer", ResourceMapper, BulkResourceMapper) : base(indexerFactory, "indexer", ResourceMapper, BulkResourceMapper)

@ -1,3 +1,5 @@
using System;
using Microsoft.AspNetCore.Mvc;
using NzbDrone.Core.Extras.Metadata; using NzbDrone.Core.Extras.Metadata;
using Radarr.Http; using Radarr.Http;
@ -6,12 +8,24 @@ namespace Radarr.Api.V3.Metadata
[V3ApiController] [V3ApiController]
public class MetadataController : ProviderControllerBase<MetadataResource, MetadataBulkResource, IMetadata, MetadataDefinition> public class MetadataController : ProviderControllerBase<MetadataResource, MetadataBulkResource, IMetadata, MetadataDefinition>
{ {
public static readonly MetadataResourceMapper ResourceMapper = new MetadataResourceMapper(); public static readonly MetadataResourceMapper ResourceMapper = new ();
public static readonly MetadataBulkResourceMapper BulkResourceMapper = new MetadataBulkResourceMapper(); public static readonly MetadataBulkResourceMapper BulkResourceMapper = new ();
public MetadataController(IMetadataFactory metadataFactory) public MetadataController(IMetadataFactory metadataFactory)
: base(metadataFactory, "metadata", ResourceMapper, BulkResourceMapper) : base(metadataFactory, "metadata", ResourceMapper, BulkResourceMapper)
{ {
} }
[NonAction]
public override ActionResult<MetadataResource> UpdateProvider([FromBody] MetadataBulkResource providerResource)
{
throw new NotImplementedException();
}
[NonAction]
public override object DeleteProviders([FromBody] MetadataBulkResource resource)
{
throw new NotImplementedException();
}
} }
} }

@ -16,11 +16,4 @@ namespace Radarr.Api.V3.Movies
public bool DeleteFiles { get; set; } public bool DeleteFiles { get; set; }
public bool AddImportExclusion { get; set; } public bool AddImportExclusion { get; set; }
} }
public enum ApplyTags
{
Add,
Remove,
Replace
}
} }

@ -1,3 +1,5 @@
using System;
using Microsoft.AspNetCore.Mvc;
using NzbDrone.Core.Notifications; using NzbDrone.Core.Notifications;
using Radarr.Http; using Radarr.Http;
@ -6,12 +8,24 @@ namespace Radarr.Api.V3.Notifications
[V3ApiController] [V3ApiController]
public class NotificationController : ProviderControllerBase<NotificationResource, NotificationBulkResource, INotification, NotificationDefinition> public class NotificationController : ProviderControllerBase<NotificationResource, NotificationBulkResource, INotification, NotificationDefinition>
{ {
public static readonly NotificationResourceMapper ResourceMapper = new NotificationResourceMapper(); public static readonly NotificationResourceMapper ResourceMapper = new ();
public static readonly NotificationBulkResourceMapper BulkResourceMapper = new NotificationBulkResourceMapper(); public static readonly NotificationBulkResourceMapper BulkResourceMapper = new ();
public NotificationController(NotificationFactory notificationFactory) public NotificationController(NotificationFactory notificationFactory)
: base(notificationFactory, "notification", ResourceMapper, BulkResourceMapper) : base(notificationFactory, "notification", ResourceMapper, BulkResourceMapper)
{ {
} }
[NonAction]
public override ActionResult<NotificationResource> UpdateProvider([FromBody] NotificationBulkResource providerResource)
{
throw new NotImplementedException();
}
[NonAction]
public override object DeleteProviders([FromBody] NotificationBulkResource resource)
{
throw new NotImplementedException();
}
} }
} }

@ -1,6 +1,5 @@
using System.Collections.Generic; using System.Collections.Generic;
using NzbDrone.Core.ThingiProvider; using NzbDrone.Core.ThingiProvider;
using Radarr.Api.V3.Movies;
namespace Radarr.Api.V3 namespace Radarr.Api.V3
{ {
@ -9,6 +8,18 @@ namespace Radarr.Api.V3
public List<int> Ids { get; set; } public List<int> Ids { get; set; }
public List<int> Tags { get; set; } public List<int> Tags { get; set; }
public ApplyTags ApplyTags { get; set; } public ApplyTags ApplyTags { get; set; }
public ProviderBulkResource()
{
Ids = new List<int>();
}
}
public enum ApplyTags
{
Add,
Remove,
Replace
} }
public class ProviderBulkResourceMapper<TProviderBulkResource, TProviderDefinition> public class ProviderBulkResourceMapper<TProviderBulkResource, TProviderDefinition>

@ -6,7 +6,6 @@ using Microsoft.AspNetCore.Mvc;
using NzbDrone.Common.Serializer; using NzbDrone.Common.Serializer;
using NzbDrone.Core.ThingiProvider; using NzbDrone.Core.ThingiProvider;
using NzbDrone.Core.Validation; using NzbDrone.Core.Validation;
using Radarr.Api.V3.Movies;
using Radarr.Http.REST; using Radarr.Http.REST;
using Radarr.Http.REST.Attributes; using Radarr.Http.REST.Attributes;
@ -103,8 +102,13 @@ namespace Radarr.Api.V3
[HttpPut("bulk")] [HttpPut("bulk")]
[Consumes("application/json")] [Consumes("application/json")]
[Produces("application/json")] [Produces("application/json")]
public ActionResult<TProviderResource> UpdateProvider([FromBody] TBulkProviderResource providerResource) public virtual ActionResult<TProviderResource> UpdateProvider([FromBody] TBulkProviderResource providerResource)
{ {
if (!providerResource.Ids.Any())
{
throw new BadRequestException("ids must be provided");
}
var definitionsToUpdate = _providerFactory.Get(providerResource.Ids).ToList(); var definitionsToUpdate = _providerFactory.Get(providerResource.Ids).ToList();
foreach (var definition in definitionsToUpdate) foreach (var definition in definitionsToUpdate)
@ -157,7 +161,7 @@ namespace Radarr.Api.V3
[HttpDelete("bulk")] [HttpDelete("bulk")]
[Consumes("application/json")] [Consumes("application/json")]
public object DeleteProviders([FromBody] TBulkProviderResource resource) public virtual object DeleteProviders([FromBody] TBulkProviderResource resource)
{ {
_providerFactory.Delete(resource.Ids); _providerFactory.Delete(resource.Ids);

Loading…
Cancel
Save