parent
3a9182b6a6
commit
f6ae9fd6c5
@ -0,0 +1,22 @@
|
|||||||
|
import React from 'react';
|
||||||
|
import Modal from 'Components/Modal/Modal';
|
||||||
|
import TagsModalContent from './TagsModalContent';
|
||||||
|
|
||||||
|
interface TagsModalProps {
|
||||||
|
isOpen: boolean;
|
||||||
|
ids: number[];
|
||||||
|
onApplyTagsPress: (tags: number[], applyTags: string) => void;
|
||||||
|
onModalClose: () => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
function TagsModal(props: TagsModalProps) {
|
||||||
|
const { isOpen, onModalClose, ...otherProps } = props;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Modal isOpen={isOpen} onModalClose={onModalClose}>
|
||||||
|
<TagsModalContent {...otherProps} onModalClose={onModalClose} />
|
||||||
|
</Modal>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default TagsModal;
|
@ -0,0 +1,12 @@
|
|||||||
|
.renameIcon {
|
||||||
|
margin-left: 5px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.message {
|
||||||
|
margin-top: 20px;
|
||||||
|
margin-bottom: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.result {
|
||||||
|
padding-top: 4px;
|
||||||
|
}
|
@ -0,0 +1,9 @@
|
|||||||
|
// This file is automatically generated.
|
||||||
|
// Please do not change this file!
|
||||||
|
interface CssExports {
|
||||||
|
'message': string;
|
||||||
|
'renameIcon': string;
|
||||||
|
'result': string;
|
||||||
|
}
|
||||||
|
export const cssExports: CssExports;
|
||||||
|
export default cssExports;
|
@ -0,0 +1,180 @@
|
|||||||
|
import { uniq } from 'lodash';
|
||||||
|
import React, { useCallback, useMemo, useState } from 'react';
|
||||||
|
import { useSelector } from 'react-redux';
|
||||||
|
import AppState from 'App/State/AppState';
|
||||||
|
import { DownloadClientAppState } from 'App/State/SettingsAppState';
|
||||||
|
import { Tag } from 'App/State/TagsAppState';
|
||||||
|
import Form from 'Components/Form/Form';
|
||||||
|
import FormGroup from 'Components/Form/FormGroup';
|
||||||
|
import FormInputGroup from 'Components/Form/FormInputGroup';
|
||||||
|
import FormLabel from 'Components/Form/FormLabel';
|
||||||
|
import Label from 'Components/Label';
|
||||||
|
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, kinds, sizes } from 'Helpers/Props';
|
||||||
|
import createTagsSelector from 'Store/Selectors/createTagsSelector';
|
||||||
|
import DownloadClient from 'typings/DownloadClient';
|
||||||
|
import styles from './TagsModalContent.css';
|
||||||
|
|
||||||
|
interface TagsModalContentProps {
|
||||||
|
ids: number[];
|
||||||
|
onApplyTagsPress: (tags: number[], applyTags: string) => void;
|
||||||
|
onModalClose: () => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
function TagsModalContent(props: TagsModalContentProps) {
|
||||||
|
const { ids, onModalClose, onApplyTagsPress } = props;
|
||||||
|
|
||||||
|
const allDownloadClients: DownloadClientAppState = useSelector(
|
||||||
|
(state: AppState) => state.settings.downloadClients
|
||||||
|
);
|
||||||
|
const tagList: Tag[] = useSelector(createTagsSelector());
|
||||||
|
|
||||||
|
const [tags, setTags] = useState<number[]>([]);
|
||||||
|
const [applyTags, setApplyTags] = useState('add');
|
||||||
|
|
||||||
|
const seriesTags = useMemo(() => {
|
||||||
|
const tags = ids.reduce((acc: number[], id) => {
|
||||||
|
const s = allDownloadClients.items.find(
|
||||||
|
(s: DownloadClient) => s.id === id
|
||||||
|
);
|
||||||
|
|
||||||
|
if (s) {
|
||||||
|
acc.push(...s.tags);
|
||||||
|
}
|
||||||
|
|
||||||
|
return acc;
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
return uniq(tags);
|
||||||
|
}, [ids, allDownloadClients]);
|
||||||
|
|
||||||
|
const onTagsChange = useCallback(
|
||||||
|
({ value }: { value: number[] }) => {
|
||||||
|
setTags(value);
|
||||||
|
},
|
||||||
|
[setTags]
|
||||||
|
);
|
||||||
|
|
||||||
|
const onApplyTagsChange = useCallback(
|
||||||
|
({ value }: { value: string }) => {
|
||||||
|
setApplyTags(value);
|
||||||
|
},
|
||||||
|
[setApplyTags]
|
||||||
|
);
|
||||||
|
|
||||||
|
const onApplyPress = useCallback(() => {
|
||||||
|
onApplyTagsPress(tags, applyTags);
|
||||||
|
}, [tags, applyTags, onApplyTagsPress]);
|
||||||
|
|
||||||
|
const applyTagsOptions = [
|
||||||
|
{ key: 'add', value: 'Add' },
|
||||||
|
{ key: 'remove', value: 'Remove' },
|
||||||
|
{ key: 'replace', value: 'Replace' },
|
||||||
|
];
|
||||||
|
|
||||||
|
return (
|
||||||
|
<ModalContent onModalClose={onModalClose}>
|
||||||
|
<ModalHeader>Tags</ModalHeader>
|
||||||
|
|
||||||
|
<ModalBody>
|
||||||
|
<Form>
|
||||||
|
<FormGroup>
|
||||||
|
<FormLabel>Tags</FormLabel>
|
||||||
|
|
||||||
|
<FormInputGroup
|
||||||
|
type={inputTypes.TAG}
|
||||||
|
name="tags"
|
||||||
|
value={tags}
|
||||||
|
onChange={onTagsChange}
|
||||||
|
/>
|
||||||
|
</FormGroup>
|
||||||
|
|
||||||
|
<FormGroup>
|
||||||
|
<FormLabel>Apply Tags</FormLabel>
|
||||||
|
|
||||||
|
<FormInputGroup
|
||||||
|
type={inputTypes.SELECT}
|
||||||
|
name="applyTags"
|
||||||
|
value={applyTags}
|
||||||
|
values={applyTagsOptions}
|
||||||
|
helpTexts={[
|
||||||
|
'How to apply tags to the selected download clients(s)',
|
||||||
|
'Add: Add the tags the existing list of tags',
|
||||||
|
'Remove: Remove the entered tags',
|
||||||
|
'Replace: Replace the tags with the entered tags (enter no tags to clear all tags)',
|
||||||
|
]}
|
||||||
|
onChange={onApplyTagsChange}
|
||||||
|
/>
|
||||||
|
</FormGroup>
|
||||||
|
|
||||||
|
<FormGroup>
|
||||||
|
<FormLabel>Result</FormLabel>
|
||||||
|
|
||||||
|
<div className={styles.result}>
|
||||||
|
{seriesTags.map((id) => {
|
||||||
|
const tag = tagList.find((t) => t.id === id);
|
||||||
|
|
||||||
|
if (!tag) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
const removeTag =
|
||||||
|
(applyTags === 'remove' && tags.indexOf(id) > -1) ||
|
||||||
|
(applyTags === 'replace' && tags.indexOf(id) === -1);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Label
|
||||||
|
key={tag.id}
|
||||||
|
title={removeTag ? 'Removing tag' : 'Existing tag'}
|
||||||
|
kind={removeTag ? kinds.INVERSE : kinds.INFO}
|
||||||
|
size={sizes.LARGE}
|
||||||
|
>
|
||||||
|
{tag.label}
|
||||||
|
</Label>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
|
||||||
|
{(applyTags === 'add' || applyTags === 'replace') &&
|
||||||
|
tags.map((id) => {
|
||||||
|
const tag = tagList.find((t) => t.id === id);
|
||||||
|
|
||||||
|
if (!tag) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (seriesTags.indexOf(id) > -1) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Label
|
||||||
|
key={tag.id}
|
||||||
|
title={'Adding tag'}
|
||||||
|
kind={kinds.SUCCESS}
|
||||||
|
size={sizes.LARGE}
|
||||||
|
>
|
||||||
|
{tag.label}
|
||||||
|
</Label>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</div>
|
||||||
|
</FormGroup>
|
||||||
|
</Form>
|
||||||
|
</ModalBody>
|
||||||
|
|
||||||
|
<ModalFooter>
|
||||||
|
<Button onPress={onModalClose}>Cancel</Button>
|
||||||
|
|
||||||
|
<Button kind={kinds.PRIMARY} onPress={onApplyPress}>
|
||||||
|
Apply
|
||||||
|
</Button>
|
||||||
|
</ModalFooter>
|
||||||
|
</ModalContent>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default TagsModalContent;
|
@ -0,0 +1,14 @@
|
|||||||
|
using FluentMigrator;
|
||||||
|
using NzbDrone.Core.Datastore.Migration.Framework;
|
||||||
|
|
||||||
|
namespace NzbDrone.Core.Datastore.Migration
|
||||||
|
{
|
||||||
|
[Migration(191)]
|
||||||
|
public class add_download_client_tags : NzbDroneMigrationBase
|
||||||
|
{
|
||||||
|
protected override void MainDbUpgrade()
|
||||||
|
{
|
||||||
|
Alter.Table("DownloadClients").AddColumn("Tags").AsString().Nullable();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
Loading…
Reference in new issue