diff --git a/frontend/src/components/toolbar/gallery-files-toolbar.js b/frontend/src/components/toolbar/gallery-files-toolbar.js new file mode 100644 index 0000000000..1dda3f3f98 --- /dev/null +++ b/frontend/src/components/toolbar/gallery-files-toolbar.js @@ -0,0 +1,89 @@ +import React, { useCallback, useEffect, useMemo, useRef, useState } from 'react'; +import { gettext } from '../../utils/constants'; +import { EVENT_BUS_TYPE } from '../../metadata/constants'; +import RowUtils from '../../metadata/views/table/utils/row-utils'; +import { Dirent } from '../../models'; +import { useFileOperations } from '../../hooks/file-operations'; + +const GalleryFilesToolbar = () => { + const [selectedRecordIds, setSelectedRecordIds] = useState([]); + const metadataRef = useRef([]); + const { handleDownload: handleDownloadAPI, handleCopy: handleCopyAPI } = useFileOperations(); + const eventBus = window.sfMetadataContext && window.sfMetadataContext.eventBus; + + const checkCanDeleteRow = window.sfMetadataContext.checkCanDeleteRow(); + const canDuplicateRow = window.sfMetadataContext.canDuplicateRow(); + + useEffect(() => { + const unsubscribeSelectedFileIds = eventBus && eventBus.subscribe(EVENT_BUS_TYPE.SELECT_RECORDS, (ids, metadata) => { + metadataRef.current = metadata || []; + setSelectedRecordIds(ids); + }); + + return () => { + unsubscribeSelectedFileIds && unsubscribeSelectedFileIds(); + }; + // eslint-disable-next-line react-hooks/exhaustive-deps + }, []); + + const records = useMemo(() => selectedRecordIds.map(id => RowUtils.getRecordById(id, metadataRef.current)).filter(Boolean) || [], [selectedRecordIds]); + + const unSelect = useCallback(() => { + setSelectedRecordIds([]); + eventBus && eventBus.dispatch(EVENT_BUS_TYPE.UPDATE_SELECTED_RECORD_IDS, []); + eventBus.dispatch(EVENT_BUS_TYPE.SELECT_NONE); + }, [eventBus]); + + const handleDownload = useCallback(() => { + const list = records.map(record => { + const { _parent_dir: parentDir, _name: fileName } = record || {}; + const name = parentDir === '/' ? fileName : `${parentDir}/${fileName}`; + return { name }; + }); + handleDownloadAPI('/', list); + }, [handleDownloadAPI, records]); + + const deleteRecords = useCallback(() => { + eventBus && eventBus.dispatch(EVENT_BUS_TYPE.DELETE_RECORDS, selectedRecordIds, { + success_callback: () => { + eventBus.dispatch(EVENT_BUS_TYPE.SELECT_NONE); + } + }); + }, [eventBus, selectedRecordIds]); + + const handleDuplicate = useCallback((destRepo, dirent, destPath, nodeParentPath, isByDialog) => { + eventBus && eventBus.dispatch(EVENT_BUS_TYPE.DUPLICATE_RECORD, selectedRecordIds[0], + destRepo, dirent, destPath, nodeParentPath, isByDialog); + }, [eventBus, selectedRecordIds]); + + const handleCopy = useCallback(() => { + const { _parent_dir: parentDir, _name: fileName } = records[0] || {}; + const dirent = new Dirent({ name: fileName }); + handleCopyAPI(parentDir, dirent, false, handleDuplicate); + }, [records, handleCopyAPI, handleDuplicate]); + + const length = selectedRecordIds.length; + return ( +
+ + + {length}{' '}{gettext('selected')} + + + + + {checkCanDeleteRow && + + + + } + {(canDuplicateRow && length === 1) && + + + + } +
+ ); +}; + +export default GalleryFilesToolbar; diff --git a/frontend/src/components/toolbar/metadata-path-toolbar.js b/frontend/src/components/toolbar/metadata-path-toolbar.js index 8f33b72698..dcb42cb485 100644 --- a/frontend/src/components/toolbar/metadata-path-toolbar.js +++ b/frontend/src/components/toolbar/metadata-path-toolbar.js @@ -1,21 +1,38 @@ -import React from 'react'; +import React, { useMemo } from 'react'; import PropTypes from 'prop-types'; import { TAGS_MODE } from '../dir-view-mode/constants'; import { ALL_TAGS_ID } from '../../tag/constants'; +import { useMetadata } from '../../metadata/hooks'; +import { VIEW_TYPE } from '../../metadata/constants'; import AllTagsToolbar from './all-tags-toolbar'; import TagFilesToolbar from './tag-files-toolbar'; import TableFilesToolbar from './table-files-toolbar'; +import GalleryFilesToolbar from './gallery-files-toolbar'; + +const MetadataPathToolbar = ({ repoID, repoInfo, mode, path, viewId }) => { + const { idViewMap } = useMetadata(); + const view = useMemo(() => idViewMap[viewId], [viewId, idViewMap]); + const type = view?.type; + + if (type === VIEW_TYPE.GALLERY) { + return ( + + ); + } + + if (type === VIEW_TYPE.TABLE) { + return ( + + ); + } -const MetadataPathToolbar = ({ repoID, repoInfo, mode, path }) => { if (mode === TAGS_MODE) { const isAllTagsView = path.split('/').pop() === ALL_TAGS_ID; if (isAllTagsView) return ; return ; } - return ( - - ); + }; MetadataPathToolbar.propTypes = { diff --git a/frontend/src/metadata/constants/event-bus-type.js b/frontend/src/metadata/constants/event-bus-type.js index a0f8e5eb17..18c944d2a4 100644 --- a/frontend/src/metadata/constants/event-bus-type.js +++ b/frontend/src/metadata/constants/event-bus-type.js @@ -42,6 +42,7 @@ export const EVENT_BUS_TYPE = { SELECT_RECORDS: 'select_records', TOGGLE_MOVE_DIALOG: 'toggle_move_dialog', MOVE_RECORD: 'move_record', + DUPLICATE_RECORD: 'duplicate_record', DELETE_RECORDS: 'delete_records', UPDATE_RECORD_DETAILS: 'update_record_details', UPDATE_FACE_RECOGNITION: 'update_face_recognition', diff --git a/frontend/src/metadata/constants/view/gallery.js b/frontend/src/metadata/constants/view/gallery.js index 80a7efa109..966b0a6c5b 100644 --- a/frontend/src/metadata/constants/view/gallery.js +++ b/frontend/src/metadata/constants/view/gallery.js @@ -18,3 +18,12 @@ export const GALLERY_DATE_MODE = { export const STORAGE_GALLERY_DATE_MODE_KEY = 'gallery_date_mode'; export const STORAGE_GALLERY_ZOOM_GEAR_KEY = 'gallery_zoom_gear'; + +export const GALLERY_OPERATION_KEYS = { + DOWNLOAD: 'download', + DELETE: 'delete', + DUPLICATE: 'duplicate', + REMOVE: 'remove', + SET_PEOPLE_PHOTO: 'set_people_photo', + ADD_PHOTO_TO_GROUPS: 'add_photo_to_groups' +}; diff --git a/frontend/src/metadata/hooks/metadata-view.js b/frontend/src/metadata/hooks/metadata-view.js index 7b224a0fca..b3daee561d 100644 --- a/frontend/src/metadata/hooks/metadata-view.js +++ b/frontend/src/metadata/hooks/metadata-view.js @@ -633,6 +633,7 @@ export const MetadataViewProvider = ({ const unsubscribeLocalColumnChanged = eventBus.subscribe(EVENT_BUS_TYPE.LOCAL_COLUMN_DATA_CHANGED, updateLocalColumnData); const unsubscribeUpdateSelectedRecordIds = eventBus.subscribe(EVENT_BUS_TYPE.UPDATE_SELECTED_RECORD_IDS, updateSelectedRecordIds); const unsubscribeMoveRecord = eventBus.subscribe(EVENT_BUS_TYPE.MOVE_RECORD, moveRecord); + const unsubscribeDuplicateRecord = eventBus.subscribe(EVENT_BUS_TYPE.DUPLICATE_RECORD, duplicateRecord); const unsubscribeDeleteRecords = eventBus.subscribe(EVENT_BUS_TYPE.DELETE_RECORDS, deleteRecords); const unsubscribeUpdateDetails = eventBus.subscribe(EVENT_BUS_TYPE.UPDATE_RECORD_DETAILS, updateRecordDetails); const unsubscribeUpdateFaceRecognition = eventBus.subscribe(EVENT_BUS_TYPE.UPDATE_FACE_RECOGNITION, updateFaceRecognition); @@ -661,6 +662,7 @@ export const MetadataViewProvider = ({ unsubscribeLocalColumnChanged(); unsubscribeUpdateSelectedRecordIds(); unsubscribeMoveRecord(); + unsubscribeDuplicateRecord(); unsubscribeDeleteRecords(); unsubscribeUpdateDetails(); unsubscribeUpdateFaceRecognition(); diff --git a/frontend/src/metadata/views/gallery/content.js b/frontend/src/metadata/views/gallery/content.js index bc09ebd055..2d8070787c 100644 --- a/frontend/src/metadata/views/gallery/content.js +++ b/frontend/src/metadata/views/gallery/content.js @@ -26,6 +26,7 @@ const Content = ({ const [isSelecting, setIsSelecting] = useState(false); const [selectionStart, setSelectionStart] = useState(null); + const [selectionEnd, setSelectionEnd] = useState(null); const selectedImageIds = useMemo(() => selectedImages.map(img => img.id), [selectedImages]); @@ -50,6 +51,7 @@ const Content = ({ const selectionEnd = { x: e.clientX, y: e.clientY }; const selected = []; + setSelectionEnd(selectionEnd); groups.forEach(group => { group.children.forEach((row) => { @@ -80,6 +82,7 @@ const Content = ({ e.preventDefault(); e.stopPropagation(); setIsSelecting(false); + setSelectionEnd(null); }, []); const renderDisplayGroup = useCallback((group) => { @@ -185,12 +188,30 @@ const Content = ({ ); }, [overScan, mode, columns, rowHeight, onImageClick, onImageDoubleClick, onContextMenu, size, selectedImageIds, onDateTagClick]); + const renderSelectionBox = useCallback(() => { + if (!isSelecting) return null; + if (!selectionEnd) return null; + + const containerBounds = containerRef.current.getBoundingClientRect(); + const left = Math.min(selectionStart.x, selectionEnd.x) - containerBounds.left; + const top = Math.min(selectionStart.y, selectionEnd.y) - containerBounds.top; + const width = Math.abs(selectionStart.x - selectionEnd.x); + const height = Math.abs(selectionStart.y - selectionEnd.y); + return ( +
+
+ ); + }, [isSelecting, selectionStart, selectionEnd]); + if (!Array.isArray(groups) || groups.length === 0) return (); return (
{ return renderDisplayGroup(group); })} + {renderSelectionBox()}
); }; diff --git a/frontend/src/metadata/views/gallery/context-menu/index.js b/frontend/src/metadata/views/gallery/context-menu/index.js index c361a46b66..861baba96c 100644 --- a/frontend/src/metadata/views/gallery/context-menu/index.js +++ b/frontend/src/metadata/views/gallery/context-menu/index.js @@ -6,15 +6,7 @@ import PeoplesDialog from '../../../components/dialog/peoples-dialog'; import { gettext } from '../../../../utils/constants'; import { Dirent } from '../../../../models'; import { useFileOperations } from '../../../../hooks/file-operations'; - -const CONTEXT_MENU_KEY = { - DOWNLOAD: 'download', - DELETE: 'delete', - DUPLICATE: 'duplicate', - REMOVE: 'remove', - SET_PEOPLE_PHOTO: 'set_people_photo', - ADD_PHOTO_TO_GROUPS: 'add_photo_to_groups', -}; +import { GALLERY_OPERATION_KEYS } from '../../../constants'; const GalleryContextMenu = ({ selectedImages, onDelete, onDuplicate, onRemoveImage, onAddImage, onSetPeoplePhoto }) => { const [isPeoplesDialogShow, setPeoplesDialogShow] = useState(false); @@ -28,21 +20,21 @@ const GalleryContextMenu = ({ selectedImages, onDelete, onDuplicate, onRemoveIma const canSetPeoplePhoto = window.sfMetadataContext.canSetPeoplePhoto(); const options = useMemo(() => { - let validOptions = [{ value: CONTEXT_MENU_KEY.DOWNLOAD, label: gettext('Download') }]; + let validOptions = [{ value: GALLERY_OPERATION_KEYS.DOWNLOAD, label: gettext('Download') }]; if (onDelete && checkCanDeleteRow) { - validOptions.push({ value: CONTEXT_MENU_KEY.DELETE, label: selectedImages.length > 1 ? gettext('Delete') : gettext('Delete file') }); + validOptions.push({ value: GALLERY_OPERATION_KEYS.DELETE, label: gettext('Delete') }); } if (onDuplicate && canDuplicateRow && selectedImages.length === 1) { - validOptions.push({ value: CONTEXT_MENU_KEY.DUPLICATE, label: gettext('Duplicate') }); + validOptions.push({ value: GALLERY_OPERATION_KEYS.DUPLICATE, label: gettext('Copy') }); } if (onRemoveImage && canRemovePhotoFromPeople) { - validOptions.push({ value: CONTEXT_MENU_KEY.REMOVE, label: gettext('Remove from this group') }); + validOptions.push({ value: GALLERY_OPERATION_KEYS.REMOVE, label: gettext('Remove from this group') }); } if (onAddImage && canAddPhotoToPeople) { - validOptions.push({ value: CONTEXT_MENU_KEY.ADD_PHOTO_TO_GROUPS, label: gettext('Add to groups') }); + validOptions.push({ value: GALLERY_OPERATION_KEYS.ADD_PHOTO_TO_GROUPS, label: gettext('Add to groups') }); } if (onSetPeoplePhoto && canSetPeoplePhoto) { - validOptions.push({ value: CONTEXT_MENU_KEY.SET_PEOPLE_PHOTO, label: gettext('Set as cover photo') }); + validOptions.push({ value: GALLERY_OPERATION_KEYS.SET_PEOPLE_PHOTO, label: gettext('Set as cover photo') }); } return validOptions; }, [checkCanDeleteRow, canDuplicateRow, canRemovePhotoFromPeople, canAddPhotoToPeople, selectedImages, onDuplicate, onDelete, onRemoveImage, onAddImage, canSetPeoplePhoto, onSetPeoplePhoto]); @@ -70,22 +62,22 @@ const GalleryContextMenu = ({ selectedImages, onDelete, onDuplicate, onRemoveIma const handleOptionClick = useCallback(option => { switch (option.value) { - case CONTEXT_MENU_KEY.DOWNLOAD: + case GALLERY_OPERATION_KEYS.DOWNLOAD: handleDownload(); break; - case CONTEXT_MENU_KEY.DELETE: + case GALLERY_OPERATION_KEYS.DELETE: onDelete(selectedImages); break; - case CONTEXT_MENU_KEY.DUPLICATE: + case GALLERY_OPERATION_KEYS.DUPLICATE: handleCopy(); break; - case CONTEXT_MENU_KEY.REMOVE: + case GALLERY_OPERATION_KEYS.REMOVE: onRemoveImage(selectedImages); break; - case CONTEXT_MENU_KEY.ADD_PHOTO_TO_GROUPS: + case GALLERY_OPERATION_KEYS.ADD_PHOTO_TO_GROUPS: setPeoplesDialogShow(true); break; - case CONTEXT_MENU_KEY.SET_PEOPLE_PHOTO: + case GALLERY_OPERATION_KEYS.SET_PEOPLE_PHOTO: onSetPeoplePhoto(selectedImages[0]); break; default: diff --git a/frontend/src/metadata/views/gallery/index.css b/frontend/src/metadata/views/gallery/index.css index 7065e31665..c3e66f3910 100644 --- a/frontend/src/metadata/views/gallery/index.css +++ b/frontend/src/metadata/views/gallery/index.css @@ -99,3 +99,9 @@ justify-content: center; flex-shrink: 0; } + +.selection-box { + position: absolute; + background-color: rgba(0, 120, 215, 0.3); + border: 1px solid rgba(0, 120, 215, 0.8); +} diff --git a/frontend/src/metadata/views/gallery/main.js b/frontend/src/metadata/views/gallery/main.js index 2f086ce8ad..9423db1c11 100644 --- a/frontend/src/metadata/views/gallery/main.js +++ b/frontend/src/metadata/views/gallery/main.js @@ -35,7 +35,7 @@ const Main = ({ isLoadingMore, metadata, onDelete, onLoadMore, duplicateRecord, const scrollContainer = useRef(null); const lastState = useRef({ scrollPos: 0 }); - const { repoID, updateCurrentDirent } = useMetadataView(); + const { repoID, updateCurrentDirent, updateSelectedRecordIds } = useMetadataView(); const repoInfo = window.sfMetadataContext.getSetting('repoInfo'); const canPreview = window.sfMetadataContext.canPreview(); @@ -191,12 +191,19 @@ const Main = ({ isLoadingMore, metadata, onDelete, onLoadMore, duplicateRecord, }); }, [metadata, updateCurrentDirent]); + const updateSelectedImages = useCallback((selectedImages) => { + const ids = selectedImages.map(item => item.id); + updateSelectedRecordIds(ids); + }, [updateSelectedRecordIds]); + const handleClick = useCallback((event, image) => { if (event.metaKey || event.ctrlKey) { - setSelectedImages(prev => - prev.includes(image) ? prev.filter(img => img !== image) : [...prev, image] - ); + const updatedSelectedImages = selectedImages.includes(image) + ? selectedImages.filter(img => img !== image) + : [...selectedImages, image]; + setSelectedImages(updatedSelectedImages); updateSelectedImage(image); + updateSelectedImages(updatedSelectedImages); return; } if (event.shiftKey && lastSelectedImage) { @@ -205,14 +212,18 @@ const Main = ({ isLoadingMore, metadata, onDelete, onLoadMore, duplicateRecord, const start = Math.min(lastSelectedIndex, currentIndex); const end = Math.max(lastSelectedIndex, currentIndex); const range = images.slice(start, end + 1); - setSelectedImages(prev => Array.from(new Set([...prev, ...range]))); + const updatedSelectedImages = Array.from(new Set([...selectedImages, ...range])); + setSelectedImages(updatedSelectedImages); updateSelectedImage(null); + updateSelectedImages(updatedSelectedImages); return; } - setSelectedImages([image]); + const updatedSelectedImages = [image]; + setSelectedImages(updatedSelectedImages); updateSelectedImage(image); setLastSelectedImage(image); - }, [images, updateSelectedImage, lastSelectedImage]); + updateSelectedImages(updatedSelectedImages); + }, [images, selectedImages, updateSelectedImage, lastSelectedImage, updateSelectedImages]); const handleDoubleClick = useCallback((event, image) => { event.preventDefault(); @@ -231,28 +242,40 @@ const Main = ({ isLoadingMore, metadata, onDelete, onLoadMore, duplicateRecord, const index = images.findIndex(item => item.id === image.id); if (isNaN(index) || index === -1) return; - setSelectedImages(prev => prev.length < 2 ? [image] : [...prev]); - }, [images]); + const updatedSelectedImages = selectedImages.length < 2 ? [image] : [...selectedImages]; + setSelectedImages(updatedSelectedImages); + updateSelectedImages(updatedSelectedImages); + }, [images, selectedImages, updateSelectedImages]); const moveToPrevImage = useCallback(() => { const imageItemsLength = images.length; const selectedImage = images[(imageIndex + imageItemsLength - 1) % imageItemsLength]; setImageIndex((prevState) => (prevState + imageItemsLength - 1) % imageItemsLength); - setSelectedImages([selectedImage]); + const updatedSelectedImages = [selectedImage]; + setSelectedImages(updatedSelectedImages); updateSelectedImage(selectedImage); - }, [images, imageIndex, updateSelectedImage]); + updateSelectedImages(updatedSelectedImages); + }, [images, imageIndex, updateSelectedImage, updateSelectedImages]); const moveToNextImage = useCallback(() => { const imageItemsLength = images.length; const selectedImage = images[(imageIndex + 1) % imageItemsLength]; setImageIndex((prevState) => (prevState + 1) % imageItemsLength); - setSelectedImages([selectedImage]); + const updatedSelectedImages = [selectedImage]; + setSelectedImages(updatedSelectedImages); updateSelectedImage(selectedImage); - }, [images, imageIndex, updateSelectedImage]); + updateSelectedImages(updatedSelectedImages); + }, [images, imageIndex, updateSelectedImage, updateSelectedImages]); const handleImageSelection = useCallback((selectedImages) => { setSelectedImages(selectedImages); - }, []); + updateSelectedImages(selectedImages); + }, [updateSelectedImages]); + + const selectNone = useCallback(() => { + setSelectedImages([]); + updateSelectedImages([]); + }, [updateSelectedImages]); const closeImagePopup = useCallback(() => { setIsImagePopupOpen(false); @@ -264,26 +287,29 @@ const Main = ({ isLoadingMore, metadata, onDelete, onLoadMore, duplicateRecord, success_callback: () => { updateCurrentDirent(); setSelectedImages([]); + updateSelectedImages([]); } }); - }, [onDelete, updateCurrentDirent]); + }, [onDelete, updateCurrentDirent, updateSelectedImages]); const handleRemoveSelectedImages = useCallback((selectedImages) => { if (!selectedImages.length) return; onRemoveImage && onRemoveImage(selectedImages, () => { updateCurrentDirent(); setSelectedImages([]); + updateSelectedImages([]); }); - }, [onRemoveImage, updateCurrentDirent]); + }, [onRemoveImage, updateCurrentDirent, updateSelectedImages]); const handleMakeSelectedAsCoverPhoto = useCallback((selectedImage) => { onSetPeoplePhoto(selectedImage, { success_callback: () => { updateCurrentDirent(); setSelectedImages([]); + updateSelectedImages([]); } }); - }, [onSetPeoplePhoto, updateCurrentDirent]); + }, [onSetPeoplePhoto, updateCurrentDirent, updateSelectedImages]); const handleClickOutside = useCallback((event) => { const className = getEventClassName(event); @@ -306,6 +332,7 @@ const Main = ({ isLoadingMore, metadata, onDelete, onLoadMore, duplicateRecord, if (newImageItems.length === 0) { setSelectedImages([]); + updateSelectedImages([]); setIsImagePopupOpen(false); setImageIndex(0); } else { @@ -314,9 +341,11 @@ const Main = ({ isLoadingMore, metadata, onDelete, onLoadMore, duplicateRecord, setImageIndex(newIndex); } - setSelectedImages(newSelectedImage ? [newSelectedImage] : []); + const updatedSelectedImages = newSelectedImage ? [newSelectedImage] : []; + setSelectedImages(updatedSelectedImages); updateSelectedImage(newSelectedImage); - }, [selectedImages, images, onDelete, updateSelectedImage]); + updateSelectedImages(updatedSelectedImages); + }, [selectedImages, images, onDelete, updateSelectedImage, updateSelectedImages]); const handleDateTagClick = useCallback((event, groupName) => { event.preventDefault(); @@ -348,6 +377,7 @@ const Main = ({ isLoadingMore, metadata, onDelete, onLoadMore, duplicateRecord, EVENT_BUS_TYPE.SWITCH_GALLERY_GROUP_BY, (mode) => { setSelectedImages([]); + updateSelectedImages([]); setMode(mode); lastState.current = { ...lastState.current, mode }; window.sfMetadataContext.localStorage.setItem(STORAGE_GALLERY_DATE_MODE_KEY, mode); @@ -378,9 +408,12 @@ const Main = ({ isLoadingMore, metadata, onDelete, onLoadMore, duplicateRecord, setZoomGear(zoomGear); }); + const unsubscribeSelectNone = window.sfMetadataContext.eventBus.subscribe(EVENT_BUS_TYPE.SELECT_NONE, selectNone); + return () => { container && resizeObserver.unobserve(container); modifyGalleryZoomGearSubscribe(); + unsubscribeSelectNone(); switchGalleryModeSubscribe(); }; // eslint-disable-next-line react-hooks/exhaustive-deps diff --git a/frontend/src/pages/lib-content-view/lib-content-view.js b/frontend/src/pages/lib-content-view/lib-content-view.js index 82e2d04d69..c30aead4a9 100644 --- a/frontend/src/pages/lib-content-view/lib-content-view.js +++ b/frontend/src/pages/lib-content-view/lib-content-view.js @@ -2311,7 +2311,7 @@ class LibContentView extends React.Component { render() { const { repoID } = this.props; let { currentRepoInfo, userPerm, isCopyMoveProgressDialogShow, isDeleteFolderDialogOpen, errorMsg, - path, usedRepoTags, isDirentSelected, currentMode, currentNode } = this.state; + path, usedRepoTags, isDirentSelected, currentMode, currentNode, viewId } = this.state; if (this.state.libNeedDecrypt) { return ( @@ -2417,7 +2417,7 @@ class LibContentView extends React.Component { })}> {isDirentSelected ? ( currentMode === TAGS_MODE || currentMode === METADATA_MODE ? ( - + ) : (