New: Favorite folders in Manual Import

Closes #5891
pull/7359/head
Mark McDowall 4 months ago committed by Mark McDowall
parent 0f225b05c0
commit 3ddc6ac6de

@ -1,11 +1,20 @@
import AppSectionState from 'App/State/AppSectionState'; import AppSectionState from 'App/State/AppSectionState';
import RecentFolder from 'InteractiveImport/Folder/RecentFolder';
import ImportMode from 'InteractiveImport/ImportMode'; import ImportMode from 'InteractiveImport/ImportMode';
import InteractiveImport from 'InteractiveImport/InteractiveImport'; import InteractiveImport from 'InteractiveImport/InteractiveImport';
interface FavoriteFolder {
folder: string;
}
interface RecentFolder {
folder: string;
lastUsed: string;
}
interface InteractiveImportAppState extends AppSectionState<InteractiveImport> { interface InteractiveImportAppState extends AppSectionState<InteractiveImport> {
originalItems: InteractiveImport[]; originalItems: InteractiveImport[];
importMode: ImportMode; importMode: ImportMode;
favoriteFolders: FavoriteFolder[];
recentFolders: RecentFolder[]; recentFolders: RecentFolder[];
} }

@ -13,6 +13,7 @@ import {
faFileVideo as farFileVideo, faFileVideo as farFileVideo,
faFolder as farFolder, faFolder as farFolder,
faHdd as farHdd, faHdd as farHdd,
faHeart as farHeart,
faKeyboard as farKeyboard, faKeyboard as farKeyboard,
faObjectGroup as farObjectGroup, faObjectGroup as farObjectGroup,
faObjectUngroup as farObjectUngroup, faObjectUngroup as farObjectUngroup,
@ -163,6 +164,7 @@ export const FOLDER_OPEN = fasFolderOpen;
export const GROUP = farObjectGroup; export const GROUP = farObjectGroup;
export const HEALTH = fasMedkit; export const HEALTH = fasMedkit;
export const HEART = fasHeart; export const HEART = fasHeart;
export const HEART_OUTLINE = farHeart;
export const HISTORY = fasHistory; export const HISTORY = fasHistory;
export const HOUSEKEEPING = fasHome; export const HOUSEKEEPING = fasHome;
export const IGNORE = fasTimesCircle; export const IGNORE = fasTimesCircle;

@ -0,0 +1,5 @@
.actions {
composes: cell from '~Components/Table/Cells/TableRowCell.css';
width: 70px;
}

@ -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;

@ -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 (
<TableRowButton onPress={handlePress}>
<TableRowCell>{folder}</TableRowCell>
<TableRowCell className={styles.actions}>
<IconButton
title={translate('FavoriteFolderRemove')}
kind="danger"
name={icons.HEART}
onPress={handleRemoveFavoritePress}
/>
</TableRowCell>
</TableRowButton>
);
}
export default FavoriteFolderRow;

@ -1,7 +1,12 @@
.recentFoldersContainer { .foldersContainer {
margin-top: 15px; margin-top: 15px;
} }
.foldersTitle {
border-bottom: 1px solid var(--borderColor);
font-size: 21px;
}
.buttonsContainer { .buttonsContainer {
margin-top: 30px; margin-top: 30px;
} }

@ -5,7 +5,8 @@ interface CssExports {
'buttonContainer': string; 'buttonContainer': string;
'buttonIcon': string; 'buttonIcon': string;
'buttonsContainer': string; 'buttonsContainer': string;
'recentFoldersContainer': string; 'foldersContainer': string;
'foldersTitle': string;
} }
export const cssExports: CssExports; export const cssExports: CssExports;
export default cssExports; export default cssExports;

@ -1,4 +1,4 @@
import React, { useCallback, useState } from 'react'; import React, { useCallback, useMemo, useState } from 'react';
import { useDispatch, useSelector } from 'react-redux'; import { useDispatch, useSelector } from 'react-redux';
import { createSelector } from 'reselect'; import { createSelector } from 'reselect';
import AppState from 'App/State/AppState'; import AppState from 'App/State/AppState';
@ -14,14 +14,23 @@ import Table from 'Components/Table/Table';
import TableBody from 'Components/Table/TableBody'; import TableBody from 'Components/Table/TableBody';
import { icons, kinds, sizes } from 'Helpers/Props'; import { icons, kinds, sizes } from 'Helpers/Props';
import { executeCommand } from 'Store/Actions/commandActions'; import { executeCommand } from 'Store/Actions/commandActions';
import { import { addRecentFolder } from 'Store/Actions/interactiveImportActions';
addRecentFolder,
removeRecentFolder,
} from 'Store/Actions/interactiveImportActions';
import translate from 'Utilities/String/translate'; import translate from 'Utilities/String/translate';
import FavoriteFolderRow from './FavoriteFolderRow';
import RecentFolderRow from './RecentFolderRow'; import RecentFolderRow from './RecentFolderRow';
import styles from './InteractiveImportSelectFolderModalContent.css'; import styles from './InteractiveImportSelectFolderModalContent.css';
const favoriteFoldersColumns = [
{
name: 'folder',
label: () => translate('Folder'),
},
{
name: 'actions',
label: '',
},
];
const recentFoldersColumns = [ const recentFoldersColumns = [
{ {
name: 'folder', name: 'folder',
@ -49,15 +58,22 @@ function InteractiveImportSelectFolderModalContent(
const { modalTitle, onFolderSelect, onModalClose } = props; const { modalTitle, onFolderSelect, onModalClose } = props;
const [folder, setFolder] = useState(''); const [folder, setFolder] = useState('');
const dispatch = useDispatch(); const dispatch = useDispatch();
const recentFolders = useSelector( const { favoriteFolders, recentFolders } = useSelector(
createSelector( createSelector(
(state: AppState) => state.interactiveImport.recentFolders, (state: AppState) => state.interactiveImport,
(recentFolders) => { (interactiveImport) => {
return recentFolders; return {
favoriteFolders: interactiveImport.favoriteFolders,
recentFolders: interactiveImport.recentFolders,
};
} }
) )
); );
const favoriteFolderMap = useMemo(() => {
return new Map(favoriteFolders.map((f) => [f.folder, f]));
}, [favoriteFolders]);
const onPathChange = useCallback( const onPathChange = useCallback(
({ value }: { value: string }) => { ({ value }: { value: string }) => {
setFolder(value); setFolder(value);
@ -90,13 +106,6 @@ function InteractiveImportSelectFolderModalContent(
onFolderSelect(folder); onFolderSelect(folder);
}, [folder, onFolderSelect, dispatch]); }, [folder, onFolderSelect, dispatch]);
const onRemoveRecentFolderPress = useCallback(
(folderToRemove: string) => {
dispatch(removeRecentFolder({ folder: folderToRemove }));
},
[dispatch]
);
return ( return (
<ModalContent onModalClose={onModalClose}> <ModalContent onModalClose={onModalClose}>
<ModalHeader> <ModalHeader>
@ -111,8 +120,34 @@ function InteractiveImportSelectFolderModalContent(
onChange={onPathChange} onChange={onPathChange}
/> />
{favoriteFolders.length ? (
<div className={styles.foldersContainer}>
<div className={styles.foldersTitle}>
{translate('FavoriteFolders')}
</div>
<Table columns={favoriteFoldersColumns}>
<TableBody>
{favoriteFolders.map((favoriteFolder) => {
return (
<FavoriteFolderRow
key={favoriteFolder.folder}
folder={favoriteFolder.folder}
onPress={onRecentPathPress}
/>
);
})}
</TableBody>
</Table>
</div>
) : null}
{recentFolders.length ? ( {recentFolders.length ? (
<div className={styles.recentFoldersContainer}> <div className={styles.foldersContainer}>
<div className={styles.foldersTitle}>
{translate('RecentFolders')}
</div>
<Table columns={recentFoldersColumns}> <Table columns={recentFoldersColumns}>
<TableBody> <TableBody>
{recentFolders {recentFolders
@ -124,8 +159,8 @@ function InteractiveImportSelectFolderModalContent(
key={recentFolder.folder} key={recentFolder.folder}
folder={recentFolder.folder} folder={recentFolder.folder}
lastUsed={recentFolder.lastUsed} lastUsed={recentFolder.lastUsed}
isFavorite={favoriteFolderMap.has(recentFolder.folder)}
onPress={onRecentPathPress} onPress={onRecentPathPress}
onRemoveRecentFolderPress={onRemoveRecentFolderPress}
/> />
); );
})} })}

@ -1,6 +0,0 @@
interface RecentFolder {
folder: string;
lastUsed: string;
}
export default RecentFolder;

@ -1,5 +1,5 @@
.actions { .actions {
composes: cell from '~Components/Table/Cells/TableRowCell.css'; composes: cell from '~Components/Table/Cells/TableRowCell.css';
width: 40px; width: 70px;
} }

@ -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 (
<TableRowButton onPress={this.onPress}>
<TableRowCell>{folder}</TableRowCell>
<RelativeDateCell date={lastUsed} />
<TableRowCell className={styles.actions}>
<IconButton
title={translate('Remove')}
name={icons.REMOVE}
onPress={this.onRemovePress}
/>
</TableRowCell>
</TableRowButton>
);
}
}
RecentFolderRow.propTypes = {
folder: PropTypes.string.isRequired,
lastUsed: PropTypes.string.isRequired,
onPress: PropTypes.func.isRequired,
onRemoveRecentFolderPress: PropTypes.func.isRequired
};
export default RecentFolderRow;

@ -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 (
<TableRowButton onPress={handlePress}>
<TableRowCell>{folder}</TableRowCell>
<RelativeDateCell date={lastUsed} />
<TableRowCell className={styles.actions}>
<IconButton
title={
isFavorite
? translate('FavoriteFolderRemove')
: translate('FavoriteFolderAdd')
}
kind={isFavorite ? 'danger' : 'default'}
name={isFavorite ? icons.HEART : icons.HEART_OUTLINE}
onPress={handleFavoritePress}
/>
<IconButton
title={translate('Remove')}
name={icons.REMOVE}
onPress={handleRemovePress}
/>
</TableRowCell>
</TableRowButton>
);
}
export default RecentFolderRow;

@ -3,6 +3,7 @@ import { createAction } from 'redux-actions';
import { batchActions } from 'redux-batched-actions'; import { batchActions } from 'redux-batched-actions';
import { sortDirections } from 'Helpers/Props'; import { sortDirections } from 'Helpers/Props';
import { createThunk, handleThunks } from 'Store/thunks'; import { createThunk, handleThunks } from 'Store/thunks';
import sortByProp from 'Utilities/Array/sortByProp';
import createAjaxRequest from 'Utilities/createAjaxRequest'; import createAjaxRequest from 'Utilities/createAjaxRequest';
import naturalExpansion from 'Utilities/String/naturalExpansion'; import naturalExpansion from 'Utilities/String/naturalExpansion';
import { set, update, updateItem } from './baseActions'; import { set, update, updateItem } from './baseActions';
@ -30,6 +31,7 @@ export const defaultState = {
originalItems: [], originalItems: [],
sortKey: 'relativePath', sortKey: 'relativePath',
sortDirection: sortDirections.ASCENDING, sortDirection: sortDirections.ASCENDING,
favoriteFolders: [],
recentFolders: [], recentFolders: [],
importMode: 'chooseImportMode', importMode: 'chooseImportMode',
sortPredicates: { sortPredicates: {
@ -58,6 +60,7 @@ export const defaultState = {
export const persistState = [ export const persistState = [
'interactiveImport.sortKey', 'interactiveImport.sortKey',
'interactiveImport.sortDirection', 'interactiveImport.sortDirection',
'interactiveImport.favoriteFolders',
'interactiveImport.recentFolders', 'interactiveImport.recentFolders',
'interactiveImport.importMode' 'interactiveImport.importMode'
]; ];
@ -73,6 +76,8 @@ export const UPDATE_INTERACTIVE_IMPORT_ITEMS = 'interactiveImport/updateInteract
export const CLEAR_INTERACTIVE_IMPORT = 'interactiveImport/clearInteractiveImport'; export const CLEAR_INTERACTIVE_IMPORT = 'interactiveImport/clearInteractiveImport';
export const ADD_RECENT_FOLDER = 'interactiveImport/addRecentFolder'; export const ADD_RECENT_FOLDER = 'interactiveImport/addRecentFolder';
export const REMOVE_RECENT_FOLDER = 'interactiveImport/removeRecentFolder'; 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'; 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 clearInteractiveImport = createAction(CLEAR_INTERACTIVE_IMPORT);
export const addRecentFolder = createAction(ADD_RECENT_FOLDER); export const addRecentFolder = createAction(ADD_RECENT_FOLDER);
export const removeRecentFolder = createAction(REMOVE_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); export const setInteractiveImportMode = createAction(SET_INTERACTIVE_IMPORT_MODE);
// //
@ -268,9 +275,31 @@ export const reducers = createHandleActions({
return Object.assign({}, state, { recentFolders }); 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) { [CLEAR_INTERACTIVE_IMPORT]: function(state) {
const newState = { const newState = {
...defaultState, ...defaultState,
favoriteFolders: state.favoriteFolders,
recentFolders: state.recentFolders, recentFolders: state.recentFolders,
importMode: state.importMode importMode: state.importMode
}; };

@ -690,6 +690,9 @@
"FailedToLoadTranslationsFromApi": "Failed to load translations from API", "FailedToLoadTranslationsFromApi": "Failed to load translations from API",
"FailedToLoadUiSettingsFromApi": "Failed to load UI settings from API", "FailedToLoadUiSettingsFromApi": "Failed to load UI settings from API",
"False": "False", "False": "False",
"FavoriteFolderAdd": "Add Favorite Folder",
"FavoriteFolderRemove": "Remove Favorite Folder",
"FavoriteFolders": "Favorite Folders",
"FeatureRequests": "Feature Requests", "FeatureRequests": "Feature Requests",
"File": "File", "File": "File",
"FileBrowser": "File Browser", "FileBrowser": "File Browser",
@ -1621,6 +1624,7 @@
"Real": "Real", "Real": "Real",
"Reason": "Reason", "Reason": "Reason",
"RecentChanges": "Recent Changes", "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}", "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", "RecyclingBin": "Recycling Bin",
"RecyclingBinCleanup": "Recycling Bin Cleanup", "RecyclingBinCleanup": "Recycling Bin Cleanup",

Loading…
Cancel
Save