diff --git a/frontend/src/App/State/InteractiveImportAppState.ts b/frontend/src/App/State/InteractiveImportAppState.ts index cf86f620d..84fd9f4c1 100644 --- a/frontend/src/App/State/InteractiveImportAppState.ts +++ b/frontend/src/App/State/InteractiveImportAppState.ts @@ -1,11 +1,20 @@ import AppSectionState from 'App/State/AppSectionState'; -import RecentFolder from 'InteractiveImport/Folder/RecentFolder'; import ImportMode from 'InteractiveImport/ImportMode'; import InteractiveImport from 'InteractiveImport/InteractiveImport'; +interface FavoriteFolder { + folder: string; +} + +interface RecentFolder { + folder: string; + lastUsed: string; +} + interface InteractiveImportAppState extends AppSectionState { originalItems: InteractiveImport[]; importMode: ImportMode; + favoriteFolders: FavoriteFolder[]; recentFolders: RecentFolder[]; } diff --git a/frontend/src/Helpers/Props/icons.ts b/frontend/src/Helpers/Props/icons.ts index 3ba5c4db1..e9a361066 100644 --- a/frontend/src/Helpers/Props/icons.ts +++ b/frontend/src/Helpers/Props/icons.ts @@ -13,6 +13,7 @@ import { faFileVideo as farFileVideo, faFolder as farFolder, faHdd as farHdd, + faHeart as farHeart, faKeyboard as farKeyboard, faObjectGroup as farObjectGroup, faObjectUngroup as farObjectUngroup, @@ -163,6 +164,7 @@ export const FOLDER_OPEN = fasFolderOpen; export const GROUP = farObjectGroup; export const HEALTH = fasMedkit; export const HEART = fasHeart; +export const HEART_OUTLINE = farHeart; export const HISTORY = fasHistory; export const HOUSEKEEPING = fasHome; export const IGNORE = fasTimesCircle; diff --git a/frontend/src/InteractiveImport/Folder/FavoriteFolderRow.css b/frontend/src/InteractiveImport/Folder/FavoriteFolderRow.css new file mode 100644 index 000000000..2839ea389 --- /dev/null +++ b/frontend/src/InteractiveImport/Folder/FavoriteFolderRow.css @@ -0,0 +1,5 @@ +.actions { + composes: cell from '~Components/Table/Cells/TableRowCell.css'; + + width: 70px; +} diff --git a/frontend/src/InteractiveImport/Folder/FavoriteFolderRow.css.d.ts b/frontend/src/InteractiveImport/Folder/FavoriteFolderRow.css.d.ts new file mode 100644 index 000000000..d8ea83dc1 --- /dev/null +++ b/frontend/src/InteractiveImport/Folder/FavoriteFolderRow.css.d.ts @@ -0,0 +1,7 @@ +// This file is automatically generated. +// Please do not change this file! +interface CssExports { + 'actions': string; +} +export const cssExports: CssExports; +export default cssExports; diff --git a/frontend/src/InteractiveImport/Folder/FavoriteFolderRow.tsx b/frontend/src/InteractiveImport/Folder/FavoriteFolderRow.tsx new file mode 100644 index 000000000..e39635623 --- /dev/null +++ b/frontend/src/InteractiveImport/Folder/FavoriteFolderRow.tsx @@ -0,0 +1,48 @@ +import React, { SyntheticEvent, useCallback } from 'react'; +import { useDispatch } from 'react-redux'; +import IconButton from 'Components/Link/IconButton'; +import TableRowCell from 'Components/Table/Cells/TableRowCell'; +import TableRowButton from 'Components/Table/TableRowButton'; +import { icons } from 'Helpers/Props'; +import { removeFavoriteFolder } from 'Store/Actions/interactiveImportActions'; +import translate from 'Utilities/String/translate'; +import styles from './FavoriteFolderRow.css'; + +interface FavoriteFolderRowProps { + folder: string; + onPress: (folder: string) => unknown; +} + +function FavoriteFolderRow({ folder, onPress }: FavoriteFolderRowProps) { + const dispatch = useDispatch(); + + const handlePress = useCallback(() => { + onPress(folder); + }, [folder, onPress]); + + const handleRemoveFavoritePress = useCallback( + (e: SyntheticEvent) => { + e.stopPropagation(); + + dispatch(removeFavoriteFolder({ folder })); + }, + [folder, dispatch] + ); + + return ( + + {folder} + + + + + + ); +} + +export default FavoriteFolderRow; diff --git a/frontend/src/InteractiveImport/Folder/InteractiveImportSelectFolderModalContent.css b/frontend/src/InteractiveImport/Folder/InteractiveImportSelectFolderModalContent.css index 5f9033a18..8a7b5a541 100644 --- a/frontend/src/InteractiveImport/Folder/InteractiveImportSelectFolderModalContent.css +++ b/frontend/src/InteractiveImport/Folder/InteractiveImportSelectFolderModalContent.css @@ -1,7 +1,12 @@ -.recentFoldersContainer { +.foldersContainer { margin-top: 15px; } +.foldersTitle { + border-bottom: 1px solid var(--borderColor); + font-size: 21px; +} + .buttonsContainer { margin-top: 30px; } diff --git a/frontend/src/InteractiveImport/Folder/InteractiveImportSelectFolderModalContent.css.d.ts b/frontend/src/InteractiveImport/Folder/InteractiveImportSelectFolderModalContent.css.d.ts index 46abdcb9b..0e304b1b1 100644 --- a/frontend/src/InteractiveImport/Folder/InteractiveImportSelectFolderModalContent.css.d.ts +++ b/frontend/src/InteractiveImport/Folder/InteractiveImportSelectFolderModalContent.css.d.ts @@ -5,7 +5,8 @@ interface CssExports { 'buttonContainer': string; 'buttonIcon': string; 'buttonsContainer': string; - 'recentFoldersContainer': string; + 'foldersContainer': string; + 'foldersTitle': string; } export const cssExports: CssExports; export default cssExports; diff --git a/frontend/src/InteractiveImport/Folder/InteractiveImportSelectFolderModalContent.tsx b/frontend/src/InteractiveImport/Folder/InteractiveImportSelectFolderModalContent.tsx index 62b2da885..01b4e4bff 100644 --- a/frontend/src/InteractiveImport/Folder/InteractiveImportSelectFolderModalContent.tsx +++ b/frontend/src/InteractiveImport/Folder/InteractiveImportSelectFolderModalContent.tsx @@ -1,4 +1,4 @@ -import React, { useCallback, useState } from 'react'; +import React, { useCallback, useMemo, useState } from 'react'; import { useDispatch, useSelector } from 'react-redux'; import { createSelector } from 'reselect'; import AppState from 'App/State/AppState'; @@ -14,14 +14,23 @@ import Table from 'Components/Table/Table'; import TableBody from 'Components/Table/TableBody'; import { icons, kinds, sizes } from 'Helpers/Props'; import { executeCommand } from 'Store/Actions/commandActions'; -import { - addRecentFolder, - removeRecentFolder, -} from 'Store/Actions/interactiveImportActions'; +import { addRecentFolder } from 'Store/Actions/interactiveImportActions'; import translate from 'Utilities/String/translate'; +import FavoriteFolderRow from './FavoriteFolderRow'; import RecentFolderRow from './RecentFolderRow'; import styles from './InteractiveImportSelectFolderModalContent.css'; +const favoriteFoldersColumns = [ + { + name: 'folder', + label: () => translate('Folder'), + }, + { + name: 'actions', + label: '', + }, +]; + const recentFoldersColumns = [ { name: 'folder', @@ -49,15 +58,22 @@ function InteractiveImportSelectFolderModalContent( const { modalTitle, onFolderSelect, onModalClose } = props; const [folder, setFolder] = useState(''); const dispatch = useDispatch(); - const recentFolders = useSelector( + const { favoriteFolders, recentFolders } = useSelector( createSelector( - (state: AppState) => state.interactiveImport.recentFolders, - (recentFolders) => { - return recentFolders; + (state: AppState) => state.interactiveImport, + (interactiveImport) => { + return { + favoriteFolders: interactiveImport.favoriteFolders, + recentFolders: interactiveImport.recentFolders, + }; } ) ); + const favoriteFolderMap = useMemo(() => { + return new Map(favoriteFolders.map((f) => [f.folder, f])); + }, [favoriteFolders]); + const onPathChange = useCallback( ({ value }: { value: string }) => { setFolder(value); @@ -90,13 +106,6 @@ function InteractiveImportSelectFolderModalContent( onFolderSelect(folder); }, [folder, onFolderSelect, dispatch]); - const onRemoveRecentFolderPress = useCallback( - (folderToRemove: string) => { - dispatch(removeRecentFolder({ folder: folderToRemove })); - }, - [dispatch] - ); - return ( @@ -111,8 +120,34 @@ function InteractiveImportSelectFolderModalContent( onChange={onPathChange} /> + {favoriteFolders.length ? ( +
+
+ {translate('FavoriteFolders')} +
+ + + + {favoriteFolders.map((favoriteFolder) => { + return ( + + ); + })} + +
+
+ ) : null} + {recentFolders.length ? ( -
+
+
+ {translate('RecentFolders')} +
+ {recentFolders @@ -124,8 +159,8 @@ function InteractiveImportSelectFolderModalContent( key={recentFolder.folder} folder={recentFolder.folder} lastUsed={recentFolder.lastUsed} + isFavorite={favoriteFolderMap.has(recentFolder.folder)} onPress={onRecentPathPress} - onRemoveRecentFolderPress={onRemoveRecentFolderPress} /> ); })} diff --git a/frontend/src/InteractiveImport/Folder/RecentFolder.ts b/frontend/src/InteractiveImport/Folder/RecentFolder.ts deleted file mode 100644 index 9c6e295f6..000000000 --- a/frontend/src/InteractiveImport/Folder/RecentFolder.ts +++ /dev/null @@ -1,6 +0,0 @@ -interface RecentFolder { - folder: string; - lastUsed: string; -} - -export default RecentFolder; diff --git a/frontend/src/InteractiveImport/Folder/RecentFolderRow.css b/frontend/src/InteractiveImport/Folder/RecentFolderRow.css index 58eb9a8e4..2839ea389 100644 --- a/frontend/src/InteractiveImport/Folder/RecentFolderRow.css +++ b/frontend/src/InteractiveImport/Folder/RecentFolderRow.css @@ -1,5 +1,5 @@ .actions { composes: cell from '~Components/Table/Cells/TableRowCell.css'; - width: 40px; + width: 70px; } diff --git a/frontend/src/InteractiveImport/Folder/RecentFolderRow.js b/frontend/src/InteractiveImport/Folder/RecentFolderRow.js deleted file mode 100644 index 83c7493c4..000000000 --- a/frontend/src/InteractiveImport/Folder/RecentFolderRow.js +++ /dev/null @@ -1,65 +0,0 @@ -import PropTypes from 'prop-types'; -import React, { Component } from 'react'; -import IconButton from 'Components/Link/IconButton'; -import RelativeDateCell from 'Components/Table/Cells/RelativeDateCell'; -import TableRowCell from 'Components/Table/Cells/TableRowCell'; -import TableRowButton from 'Components/Table/TableRowButton'; -import { icons } from 'Helpers/Props'; -import translate from 'Utilities/String/translate'; -import styles from './RecentFolderRow.css'; - -class RecentFolderRow extends Component { - - // - // Listeners - - onPress = () => { - this.props.onPress(this.props.folder); - }; - - onRemovePress = (event) => { - event.stopPropagation(); - - const { - folder, - onRemoveRecentFolderPress - } = this.props; - - onRemoveRecentFolderPress(folder); - }; - - // - // Render - - render() { - const { - folder, - lastUsed - } = this.props; - - return ( - - {folder} - - - - - - - - ); - } -} - -RecentFolderRow.propTypes = { - folder: PropTypes.string.isRequired, - lastUsed: PropTypes.string.isRequired, - onPress: PropTypes.func.isRequired, - onRemoveRecentFolderPress: PropTypes.func.isRequired -}; - -export default RecentFolderRow; diff --git a/frontend/src/InteractiveImport/Folder/RecentFolderRow.tsx b/frontend/src/InteractiveImport/Folder/RecentFolderRow.tsx new file mode 100644 index 000000000..31d164e1a --- /dev/null +++ b/frontend/src/InteractiveImport/Folder/RecentFolderRow.tsx @@ -0,0 +1,85 @@ +import React, { SyntheticEvent, useCallback } from 'react'; +import { useDispatch } from 'react-redux'; +import IconButton from 'Components/Link/IconButton'; +import RelativeDateCell from 'Components/Table/Cells/RelativeDateCell'; +import TableRowCell from 'Components/Table/Cells/TableRowCell'; +import TableRowButton from 'Components/Table/TableRowButton'; +import { icons } from 'Helpers/Props'; +import { + addFavoriteFolder, + removeFavoriteFolder, + removeRecentFolder, +} from 'Store/Actions/interactiveImportActions'; +import translate from 'Utilities/String/translate'; +import styles from './RecentFolderRow.css'; + +interface RecentFolderRowProps { + folder: string; + lastUsed: string; + isFavorite: boolean; + onPress: (folder: string) => unknown; +} + +function RecentFolderRow({ + folder, + lastUsed, + isFavorite, + onPress, +}: RecentFolderRowProps) { + const dispatch = useDispatch(); + + const handlePress = useCallback(() => { + onPress(folder); + }, [folder, onPress]); + + const handleFavoritePress = useCallback( + (e: SyntheticEvent) => { + e.stopPropagation(); + + if (isFavorite) { + dispatch(removeFavoriteFolder({ folder })); + } else { + dispatch(addFavoriteFolder({ folder })); + } + }, + [folder, isFavorite, dispatch] + ); + + const handleRemovePress = useCallback( + (e: SyntheticEvent) => { + e.stopPropagation(); + + dispatch(removeRecentFolder({ folder })); + }, + [folder, dispatch] + ); + + return ( + + {folder} + + + + + + + + + + ); +} + +export default RecentFolderRow; diff --git a/frontend/src/Store/Actions/interactiveImportActions.js b/frontend/src/Store/Actions/interactiveImportActions.js index ed05ed548..ca0acc56a 100644 --- a/frontend/src/Store/Actions/interactiveImportActions.js +++ b/frontend/src/Store/Actions/interactiveImportActions.js @@ -3,6 +3,7 @@ import { createAction } from 'redux-actions'; import { batchActions } from 'redux-batched-actions'; import { sortDirections } from 'Helpers/Props'; import { createThunk, handleThunks } from 'Store/thunks'; +import sortByProp from 'Utilities/Array/sortByProp'; import createAjaxRequest from 'Utilities/createAjaxRequest'; import naturalExpansion from 'Utilities/String/naturalExpansion'; import { set, update, updateItem } from './baseActions'; @@ -30,6 +31,7 @@ export const defaultState = { originalItems: [], sortKey: 'relativePath', sortDirection: sortDirections.ASCENDING, + favoriteFolders: [], recentFolders: [], importMode: 'chooseImportMode', sortPredicates: { @@ -58,6 +60,7 @@ export const defaultState = { export const persistState = [ 'interactiveImport.sortKey', 'interactiveImport.sortDirection', + 'interactiveImport.favoriteFolders', 'interactiveImport.recentFolders', 'interactiveImport.importMode' ]; @@ -73,6 +76,8 @@ export const UPDATE_INTERACTIVE_IMPORT_ITEMS = 'interactiveImport/updateInteract export const CLEAR_INTERACTIVE_IMPORT = 'interactiveImport/clearInteractiveImport'; export const ADD_RECENT_FOLDER = 'interactiveImport/addRecentFolder'; export const REMOVE_RECENT_FOLDER = 'interactiveImport/removeRecentFolder'; +export const ADD_FAVORITE_FOLDER = 'interactiveImport/addFavoriteFolder'; +export const REMOVE_FAVORITE_FOLDER = 'interactiveImport/removeFavoriteFolder'; export const SET_INTERACTIVE_IMPORT_MODE = 'interactiveImport/setInteractiveImportMode'; // @@ -86,6 +91,8 @@ export const updateInteractiveImportItems = createAction(UPDATE_INTERACTIVE_IMPO export const clearInteractiveImport = createAction(CLEAR_INTERACTIVE_IMPORT); export const addRecentFolder = createAction(ADD_RECENT_FOLDER); export const removeRecentFolder = createAction(REMOVE_RECENT_FOLDER); +export const addFavoriteFolder = createAction(ADD_FAVORITE_FOLDER); +export const removeFavoriteFolder = createAction(REMOVE_FAVORITE_FOLDER); export const setInteractiveImportMode = createAction(SET_INTERACTIVE_IMPORT_MODE); // @@ -268,9 +275,31 @@ export const reducers = createHandleActions({ return Object.assign({}, state, { recentFolders }); }, + [ADD_FAVORITE_FOLDER]: function(state, { payload }) { + const folder = payload.folder; + const favoriteFolder = { folder }; + const favoriteFolders = [...state.favoriteFolders, favoriteFolder].sort(sortByProp('folder')); + + return Object.assign({}, state, { favoriteFolders }); + }, + + [REMOVE_FAVORITE_FOLDER]: function(state, { payload }) { + const folder = payload.folder; + const favoriteFolders = state.favoriteFolders.reduce((acc, item) => { + if (item.folder !== folder) { + acc.push(item); + } + + return acc; + }, []); + + return Object.assign({}, state, { favoriteFolders }); + }, + [CLEAR_INTERACTIVE_IMPORT]: function(state) { const newState = { ...defaultState, + favoriteFolders: state.favoriteFolders, recentFolders: state.recentFolders, importMode: state.importMode }; diff --git a/src/NzbDrone.Core/Localization/Core/en.json b/src/NzbDrone.Core/Localization/Core/en.json index a71c40cda..41969de5d 100644 --- a/src/NzbDrone.Core/Localization/Core/en.json +++ b/src/NzbDrone.Core/Localization/Core/en.json @@ -690,6 +690,9 @@ "FailedToLoadTranslationsFromApi": "Failed to load translations from API", "FailedToLoadUiSettingsFromApi": "Failed to load UI settings from API", "False": "False", + "FavoriteFolderAdd": "Add Favorite Folder", + "FavoriteFolderRemove": "Remove Favorite Folder", + "FavoriteFolders": "Favorite Folders", "FeatureRequests": "Feature Requests", "File": "File", "FileBrowser": "File Browser", @@ -1621,6 +1624,7 @@ "Real": "Real", "Reason": "Reason", "RecentChanges": "Recent Changes", + "RecentFolders": "Recent Folders", "RecycleBinUnableToWriteHealthCheckMessage": "Unable to write to configured recycling bin folder: {path}. Ensure this path exists and is writable by the user running {appName}", "RecyclingBin": "Recycling Bin", "RecyclingBinCleanup": "Recycling Bin Cleanup",