New: Ability to clone Import Lists

(cherry picked from commit 2314d0b506e30d3a965497a052bc5e54fa0beb81)

Closes #10948
This commit is contained in:
Stevie Robinson 2025-04-08 01:34:41 +02:00 committed by Bogdan
parent b99c536306
commit 15c34a61de
6 changed files with 91 additions and 8 deletions

View file

@ -4,6 +4,11 @@
width: 290px; width: 290px;
} }
.nameContainer {
display: flex;
justify-content: space-between;
}
.name { .name {
@add-mixin truncate; @add-mixin truncate;
@ -12,6 +17,12 @@
font-size: 24px; font-size: 24px;
} }
.cloneButton {
composes: button from '~Components/Link/IconButton.css';
height: 36px;
}
.enabled { .enabled {
display: flex; display: flex;
flex-wrap: wrap; flex-wrap: wrap;

View file

@ -1,9 +1,11 @@
// This file is automatically generated. // This file is automatically generated.
// Please do not change this file! // Please do not change this file!
interface CssExports { interface CssExports {
'cloneButton': string;
'enabled': string; 'enabled': string;
'list': string; 'list': string;
'name': string; 'name': string;
'nameContainer': string;
} }
export const cssExports: CssExports; export const cssExports: CssExports;
export default cssExports; export default cssExports;

View file

@ -2,9 +2,10 @@ import React, { useCallback, useState } from 'react';
import { useDispatch } from 'react-redux'; import { useDispatch } from 'react-redux';
import Card from 'Components/Card'; import Card from 'Components/Card';
import Label from 'Components/Label'; import Label from 'Components/Label';
import IconButton from 'Components/Link/IconButton';
import ConfirmModal from 'Components/Modal/ConfirmModal'; import ConfirmModal from 'Components/Modal/ConfirmModal';
import TagList from 'Components/TagList'; import TagList from 'Components/TagList';
import { kinds } from 'Helpers/Props'; import { icons, kinds } from 'Helpers/Props';
import { deleteImportList } from 'Store/Actions/settingsActions'; import { deleteImportList } from 'Store/Actions/settingsActions';
import useTags from 'Tags/useTags'; import useTags from 'Tags/useTags';
import formatShortTimeSpan from 'Utilities/Date/formatShortTimeSpan'; import formatShortTimeSpan from 'Utilities/Date/formatShortTimeSpan';
@ -19,6 +20,7 @@ interface ImportListProps {
enableAuto: boolean; enableAuto: boolean;
tags: number[]; tags: number[];
minRefreshInterval: string; minRefreshInterval: string;
onCloneImportListPress: (id: number) => void;
} }
function ImportList({ function ImportList({
@ -28,6 +30,7 @@ function ImportList({
enableAuto, enableAuto,
tags, tags,
minRefreshInterval, minRefreshInterval,
onCloneImportListPress,
}: ImportListProps) { }: ImportListProps) {
const dispatch = useDispatch(); const dispatch = useDispatch();
const tagList = useTags(); const tagList = useTags();
@ -59,13 +62,26 @@ function ImportList({
dispatch(deleteImportList({ id })); dispatch(deleteImportList({ id }));
}, [id, dispatch]); }, [id, dispatch]);
const handleCloneImportListPress = useCallback(() => {
onCloneImportListPress(id);
}, [id, onCloneImportListPress]);
return ( return (
<Card <Card
className={styles.list} className={styles.list}
overlayContent={true} overlayContent={true}
onPress={handleEditImportListPress} onPress={handleEditImportListPress}
> >
<div className={styles.name}>{name}</div> <div className={styles.nameContainer}>
<div className={styles.name}>{name}</div>
<IconButton
className={styles.cloneButton}
title={translate('CloneImportList')}
name={icons.CLONE}
onPress={handleCloneImportListPress}
/>
</div>
<div className={styles.enabled}> <div className={styles.enabled}>
{enabled ? ( {enabled ? (

View file

@ -7,7 +7,10 @@ import Icon from 'Components/Icon';
import PageSectionContent from 'Components/Page/PageSectionContent'; import PageSectionContent from 'Components/Page/PageSectionContent';
import { icons } from 'Helpers/Props'; import { icons } from 'Helpers/Props';
import { fetchRootFolders } from 'Store/Actions/rootFolderActions'; 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 createSortedSectionSelector from 'Store/Selectors/createSortedSectionSelector';
import ImportListModel from 'typings/ImportList'; import ImportListModel from 'typings/ImportList';
import sortByProp from 'Utilities/Array/sortByProp'; import sortByProp from 'Utilities/Array/sortByProp';
@ -49,6 +52,14 @@ function ImportLists() {
setIsEditImportListModalOpen(false); setIsEditImportListModalOpen(false);
}, []); }, []);
const handleCloneImportListPress = useCallback(
(id: number) => {
dispatch(cloneImportList({ id }));
setIsEditImportListModalOpen(true);
},
[dispatch]
);
useEffect(() => { useEffect(() => {
dispatch(fetchImportLists()); dispatch(fetchImportLists());
dispatch(fetchRootFolders()); dispatch(fetchRootFolders());
@ -64,7 +75,13 @@ function ImportLists() {
> >
<div className={styles.lists}> <div className={styles.lists}>
{items.map((item) => { {items.map((item) => {
return <ImportList key={item.id} {...item} />; return (
<ImportList
key={item.id}
{...item}
onCloneImportListPress={handleCloneImportListPress}
/>
);
})} })}
<Card className={styles.addList} onPress={handleAddImportListPress}> <Card className={styles.addList} onPress={handleAddImportListPress}>

View file

@ -10,7 +10,10 @@ import createTestProviderHandler, { createCancelTestProviderHandler } from 'Stor
import createSetProviderFieldValueReducer from 'Store/Actions/Creators/Reducers/createSetProviderFieldValueReducer'; import createSetProviderFieldValueReducer from 'Store/Actions/Creators/Reducers/createSetProviderFieldValueReducer';
import createSetSettingValueReducer from 'Store/Actions/Creators/Reducers/createSetSettingValueReducer'; import createSetSettingValueReducer from 'Store/Actions/Creators/Reducers/createSetSettingValueReducer';
import { createThunk } from 'Store/thunks'; import { createThunk } from 'Store/thunks';
import getSectionState from 'Utilities/State/getSectionState';
import selectProviderSchema from 'Utilities/State/selectProviderSchema'; import selectProviderSchema from 'Utilities/State/selectProviderSchema';
import updateSectionState from 'Utilities/State/updateSectionState';
import translate from 'Utilities/String/translate';
// //
// Variables // Variables
@ -31,9 +34,9 @@ export const DELETE_IMPORT_LIST = 'settings/importLists/deleteImportList';
export const TEST_IMPORT_LIST = 'settings/importLists/testImportList'; export const TEST_IMPORT_LIST = 'settings/importLists/testImportList';
export const CANCEL_TEST_IMPORT_LIST = 'settings/importLists/cancelTestImportList'; export const CANCEL_TEST_IMPORT_LIST = 'settings/importLists/cancelTestImportList';
export const TEST_ALL_IMPORT_LISTS = 'settings/importLists/testAllImportLists'; export const TEST_ALL_IMPORT_LISTS = 'settings/importLists/testAllImportLists';
export const BULK_DELETE_IMPORT_LISTS = 'settings/importLists/bulkDeleteImportLists';
export const BULK_EDIT_IMPORT_LISTS = 'settings/importLists/bulkEditImportLists'; 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 // Action Creators
@ -48,9 +51,8 @@ export const deleteImportList = createThunk(DELETE_IMPORT_LIST);
export const testImportList = createThunk(TEST_IMPORT_LIST); export const testImportList = createThunk(TEST_IMPORT_LIST);
export const cancelTestImportList = createThunk(CANCEL_TEST_IMPORT_LIST); export const cancelTestImportList = createThunk(CANCEL_TEST_IMPORT_LIST);
export const testAllImportLists = createThunk(TEST_ALL_IMPORT_LISTS); export const testAllImportLists = createThunk(TEST_ALL_IMPORT_LISTS);
export const bulkDeleteImportLists = createThunk(BULK_DELETE_IMPORT_LISTS);
export const bulkEditImportLists = createThunk(BULK_EDIT_IMPORT_LISTS); export const bulkEditImportLists = createThunk(BULK_EDIT_IMPORT_LISTS);
export const bulkDeleteImportLists = createThunk(BULK_DELETE_IMPORT_LISTS);
export const setImportListValue = createAction(SET_IMPORT_LIST_VALUE, (payload) => { export const setImportListValue = createAction(SET_IMPORT_LIST_VALUE, (payload) => {
return { return {
@ -66,6 +68,8 @@ export const setImportListFieldValue = createAction(SET_IMPORT_LIST_FIELD_VALUE,
}; };
}); });
export const cloneImportList = createAction(CLONE_IMPORT_LIST);
// //
// Details // Details
@ -128,6 +132,37 @@ export default {
return selectedSchema; 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);
} }
} }

View file

@ -222,6 +222,7 @@
"CloneAutoTag": "Clone Auto Tag", "CloneAutoTag": "Clone Auto Tag",
"CloneCondition": "Clone Condition", "CloneCondition": "Clone Condition",
"CloneCustomFormat": "Clone Custom Format", "CloneCustomFormat": "Clone Custom Format",
"CloneImportList": "Clone Import List",
"CloneIndexer": "Clone Indexer", "CloneIndexer": "Clone Indexer",
"CloneProfile": "Clone Profile", "CloneProfile": "Clone Profile",
"Close": "Close", "Close": "Close",
@ -311,6 +312,7 @@
"Default": "Default", "Default": "Default",
"DefaultCase": "Default Case", "DefaultCase": "Default Case",
"DefaultDelayProfileMovie": "This is the default profile. It applies to all movies that don't have an explicit profile.", "DefaultDelayProfileMovie": "This is the default profile. It applies to all movies that don't have an explicit profile.",
"DefaultNameCopiedImportList": "{name} - Copy",
"DefaultNameCopiedProfile": "{name} - Copy", "DefaultNameCopiedProfile": "{name} - Copy",
"DefaultNameCopiedSpecification": "{name} - Copy", "DefaultNameCopiedSpecification": "{name} - Copy",
"DefaultNotFoundMessage": "You must be lost, nothing to see here.", "DefaultNotFoundMessage": "You must be lost, nothing to see here.",