diff --git a/frontend/src/Settings/ImportLists/ImportLists/ImportList.css b/frontend/src/Settings/ImportLists/ImportLists/ImportList.css index 4796748f7..c811b765a 100644 --- a/frontend/src/Settings/ImportLists/ImportLists/ImportList.css +++ b/frontend/src/Settings/ImportLists/ImportLists/ImportList.css @@ -4,6 +4,11 @@ width: 290px; } +.nameContainer { + display: flex; + justify-content: space-between; +} + .name { @add-mixin truncate; @@ -12,6 +17,12 @@ font-size: 24px; } +.cloneButton { + composes: button from '~Components/Link/IconButton.css'; + + height: 36px; +} + .enabled { display: flex; flex-wrap: wrap; diff --git a/frontend/src/Settings/ImportLists/ImportLists/ImportList.css.d.ts b/frontend/src/Settings/ImportLists/ImportLists/ImportList.css.d.ts index a0f1a4158..c8fa34ffe 100644 --- a/frontend/src/Settings/ImportLists/ImportLists/ImportList.css.d.ts +++ b/frontend/src/Settings/ImportLists/ImportLists/ImportList.css.d.ts @@ -1,9 +1,11 @@ // This file is automatically generated. // Please do not change this file! interface CssExports { + 'cloneButton': string; 'enabled': string; 'list': string; 'name': string; + 'nameContainer': string; } export const cssExports: CssExports; export default cssExports; diff --git a/frontend/src/Settings/ImportLists/ImportLists/ImportList.tsx b/frontend/src/Settings/ImportLists/ImportLists/ImportList.tsx index debebdde0..6b7e38116 100644 --- a/frontend/src/Settings/ImportLists/ImportLists/ImportList.tsx +++ b/frontend/src/Settings/ImportLists/ImportLists/ImportList.tsx @@ -2,9 +2,10 @@ import React, { useCallback, useState } from 'react'; import { useDispatch } from 'react-redux'; import Card from 'Components/Card'; import Label from 'Components/Label'; +import IconButton from 'Components/Link/IconButton'; import ConfirmModal from 'Components/Modal/ConfirmModal'; import TagList from 'Components/TagList'; -import { kinds } from 'Helpers/Props'; +import { icons, kinds } from 'Helpers/Props'; import { deleteImportList } from 'Store/Actions/settingsActions'; import useTags from 'Tags/useTags'; import formatShortTimeSpan from 'Utilities/Date/formatShortTimeSpan'; @@ -18,6 +19,7 @@ interface ImportListProps { enableAutomaticAdd: boolean; tags: number[]; minRefreshInterval: string; + onCloneImportListPress: (id: number) => void; } function ImportList({ @@ -26,6 +28,7 @@ function ImportList({ enableAutomaticAdd, tags, minRefreshInterval, + onCloneImportListPress, }: ImportListProps) { const dispatch = useDispatch(); const tagList = useTags(); @@ -57,13 +60,26 @@ function ImportList({ dispatch(deleteImportList({ id })); }, [id, dispatch]); + const handleCloneImportListPress = useCallback(() => { + onCloneImportListPress(id); + }, [id, onCloneImportListPress]); + return ( -
{name}
+
+
{name}
+ + +
{enableAutomaticAdd ? ( diff --git a/frontend/src/Settings/ImportLists/ImportLists/ImportLists.tsx b/frontend/src/Settings/ImportLists/ImportLists/ImportLists.tsx index b43691ca1..28c577b11 100644 --- a/frontend/src/Settings/ImportLists/ImportLists/ImportLists.tsx +++ b/frontend/src/Settings/ImportLists/ImportLists/ImportLists.tsx @@ -7,7 +7,10 @@ import Icon from 'Components/Icon'; import PageSectionContent from 'Components/Page/PageSectionContent'; import { icons } from 'Helpers/Props'; import { fetchRootFolders } from 'Store/Actions/rootFolderActions'; -import { fetchImportLists } from 'Store/Actions/settingsActions'; +import { + cloneImportList, + fetchImportLists, +} from 'Store/Actions/settingsActions'; import createSortedSectionSelector from 'Store/Selectors/createSortedSectionSelector'; import ImportListModel from 'typings/ImportList'; import sortByProp from 'Utilities/Array/sortByProp'; @@ -49,6 +52,14 @@ function ImportLists() { setIsEditImportListModalOpen(false); }, []); + const handleCloneImportListPress = useCallback( + (id: number) => { + dispatch(cloneImportList({ id })); + setIsEditImportListModalOpen(true); + }, + [dispatch] + ); + useEffect(() => { dispatch(fetchImportLists()); dispatch(fetchRootFolders()); @@ -64,7 +75,13 @@ function ImportLists() { >
{items.map((item) => { - return ; + return ( + + ); })} diff --git a/frontend/src/Store/Actions/Settings/importLists.js b/frontend/src/Store/Actions/Settings/importLists.js index 13b5590bc..f7e4c22b8 100644 --- a/frontend/src/Store/Actions/Settings/importLists.js +++ b/frontend/src/Store/Actions/Settings/importLists.js @@ -10,7 +10,10 @@ import createTestProviderHandler, { createCancelTestProviderHandler } from 'Stor import createSetProviderFieldValueReducer from 'Store/Actions/Creators/Reducers/createSetProviderFieldValueReducer'; import createSetSettingValueReducer from 'Store/Actions/Creators/Reducers/createSetSettingValueReducer'; import { createThunk } from 'Store/thunks'; +import getSectionState from 'Utilities/State/getSectionState'; import selectProviderSchema from 'Utilities/State/selectProviderSchema'; +import updateSectionState from 'Utilities/State/updateSectionState'; +import translate from 'Utilities/String/translate'; // // Variables @@ -33,6 +36,7 @@ export const CANCEL_TEST_IMPORT_LIST = 'settings/importLists/cancelTestImportLis export const TEST_ALL_IMPORT_LISTS = 'settings/importLists/testAllImportLists'; export const BULK_EDIT_IMPORT_LISTS = 'settings/importLists/bulkEditImportLists'; export const BULK_DELETE_IMPORT_LISTS = 'settings/importLists/bulkDeleteImportLists'; +export const CLONE_IMPORT_LIST = 'settings/importLists/cloneImportList'; // // Action Creators @@ -64,6 +68,8 @@ export const setImportListFieldValue = createAction(SET_IMPORT_LIST_FIELD_VALUE, }; }); +export const cloneImportList = createAction(CLONE_IMPORT_LIST); + // // Details @@ -127,6 +133,37 @@ export default { return selectedSchema; }); + }, + + [CLONE_IMPORT_LIST]: (state, { payload }) => { + const id = payload.id; + const newState = getSectionState(state, section); + const item = newState.items.find((i) => i.id === id); + + const selectedSchema = { ...item }; + delete selectedSchema.id; + delete selectedSchema.name; + + // Use selectedSchema so `createProviderSettingsSelector` works properly + selectedSchema.fields = selectedSchema.fields.map((field) => { + const newField = { ...field }; + + if (newField.privacy === 'apiKey' || newField.privacy === 'password') { + newField.value = ''; + } + + return newField; + }); + + newState.selectedSchema = selectedSchema; + + const pendingChanges = { ...item, id: 0 }; + delete pendingChanges.id; + + pendingChanges.name = translate('DefaultNameCopiedImportList', { name: pendingChanges.name }); + newState.pendingChanges = pendingChanges; + + return updateSectionState(state, section, newState); } } diff --git a/src/NzbDrone.Core/Localization/Core/en.json b/src/NzbDrone.Core/Localization/Core/en.json index 5f71b236a..1e10a809d 100644 --- a/src/NzbDrone.Core/Localization/Core/en.json +++ b/src/NzbDrone.Core/Localization/Core/en.json @@ -228,6 +228,7 @@ "CloneAutoTag": "Clone Auto Tag", "CloneCondition": "Clone Condition", "CloneCustomFormat": "Clone Custom Format", + "CloneImportList": "Clone Import List", "CloneIndexer": "Clone Indexer", "CloneProfile": "Clone Profile", "Close": "Close", @@ -322,6 +323,7 @@ "Default": "Default", "DefaultCase": "Default Case", "DefaultDelayProfileSeries": "This is the default profile. It applies to all series that don't have an explicit profile.", + "DefaultNameCopiedImportList": "{name} - Copy", "DefaultNameCopiedProfile": "{name} - Copy", "DefaultNameCopiedSpecification": "{name} - Copy", "DefaultNotFoundMessage": "You must be lost, nothing to see here.",