diff --git a/frontend/src/components/cur-dir-path/dir-tool.js b/frontend/src/components/cur-dir-path/dir-tool.js index 7115224c371..e4c2822a405 100644 --- a/frontend/src/components/cur-dir-path/dir-tool.js +++ b/frontend/src/components/cur-dir-path/dir-tool.js @@ -10,6 +10,7 @@ import ViewModes from '../../components/view-modes'; import ReposSortMenu from '../../components/repos-sort-menu'; import MetadataViewToolBar from '../../metadata/components/view-toolbar'; import { PRIVATE_FILE_TYPE } from '../../constants'; +import { DIRENT_DETAIL_MODE } from '../dir-view-mode/constants'; const propTypes = { repoID: PropTypes.string.isRequired, @@ -97,10 +98,14 @@ class DirTool extends React.Component { this.props.sortItems(sortBy, sortOrder); }; + showDirentDetail = () => { + this.props.switchViewMode(DIRENT_DETAIL_MODE); + }; + render() { const menuItems = this.getMenu(); const { isDropdownMenuOpen } = this.state; - const { repoID, currentMode, currentPath, sortBy, sortOrder, viewId, switchViewMode, isCustomPermission } = this.props; + const { repoID, currentMode, currentPath, sortBy, sortOrder, viewId, isCustomPermission } = this.props; const propertiesText = TextTranslation.PROPERTIES.value; const isFileExtended = currentPath.startsWith('/' + PRIVATE_FILE_TYPE.FILE_EXTENDED_PROPERTIES + '/'); @@ -114,7 +119,7 @@ class DirTool extends React.Component { if (isFileExtended) { return (
- +
); } @@ -122,10 +127,10 @@ class DirTool extends React.Component { return (
- + {(!isCustomPermission) && -
switchViewMode('detail')}> +
} diff --git a/frontend/src/components/dir-view-mode/constants.js b/frontend/src/components/dir-view-mode/constants.js index ea05e7768f6..b3e41123ec0 100644 --- a/frontend/src/components/dir-view-mode/constants.js +++ b/frontend/src/components/dir-view-mode/constants.js @@ -1,5 +1,5 @@ -export { VIEW_TYPE } from '../../metadata/constants'; export const LIST_MODE = 'list'; export const GRID_MODE = 'grid'; +export const DIRENT_DETAIL_MODE = 'detail'; export const METADATA_MODE = 'metadata'; export const FACE_RECOGNITION_MODE = 'person_image'; diff --git a/frontend/src/components/dir-view-mode/dir-column-view.js b/frontend/src/components/dir-view-mode/dir-column-view.js index 37de95cfc0c..0559d211f24 100644 --- a/frontend/src/components/dir-view-mode/dir-column-view.js +++ b/frontend/src/components/dir-view-mode/dir-column-view.js @@ -80,6 +80,7 @@ const propTypes = { fullDirentList: PropTypes.array, onItemsScroll: PropTypes.func.isRequired, eventBus: PropTypes.object, + updateCurrentDirent: PropTypes.func.isRequired, }; class DirColumnView extends React.Component { @@ -202,6 +203,7 @@ class DirColumnView extends React.Component { viewID={this.props.viewId} deleteFilesCallback={this.props.deleteFilesCallback} renameFileCallback={this.props.renameFileCallback} + updateCurrentDirent={this.props.updateCurrentDirent} /> } {currentMode === FACE_RECOGNITION_MODE && diff --git a/frontend/src/components/dirent-detail/detail-container.js b/frontend/src/components/dirent-detail/detail-container.js index 396c3efdf9c..dd30a43bbcb 100644 --- a/frontend/src/components/dirent-detail/detail-container.js +++ b/frontend/src/components/dirent-detail/detail-container.js @@ -2,40 +2,53 @@ import React, { useEffect } from 'react'; import PropTypes from 'prop-types'; import LibDetail from './lib-details'; import DirentDetail from './dirent-details'; -import GalleryDetail from '../../metadata/components/gallery-details'; +import ViewDetails from '../../metadata/components/view-details'; import ObjectUtils from '../../metadata/utils/object-utils'; import { MetadataContext } from '../../metadata'; -import { METADATA_MODE } from '../dir-view-mode/constants'; +import { PRIVATE_FILE_TYPE } from '../../constants'; -const DetailContainer = React.memo(({ repoID, path, dirent, currentRepoInfo, repoTags, fileTags, onClose, onFileTagChanged, mode }) => { +const DetailContainer = React.memo(({ repoID, path, dirent, currentRepoInfo, repoTags, fileTags, onClose, onFileTagChanged }) => { useEffect(() => { - // init context - if (!window.sfMetadataContext) { - const context = new MetadataContext(); - window.sfMetadataContext = context; - window.sfMetadataContext.init({ repoID, repoInfo: currentRepoInfo }); - } + if (path.startsWith('/' + PRIVATE_FILE_TYPE.FILE_EXTENDED_PROPERTIES)) return; + // init context + const context = new MetadataContext(); + window.sfMetadataContext = context; + window.sfMetadataContext.init({ repoID, repoInfo: currentRepoInfo }); return () => { - if (window.sfMetadataContext && mode !== METADATA_MODE) { - window.sfMetadataContext.destroy(); - delete window['sfMetadataContext']; - } + window.sfMetadataContext.destroy(); + delete window['sfMetadataContext']; }; - }, [repoID, currentRepoInfo, mode]); + }, [repoID, currentRepoInfo, path]); - if (mode === METADATA_MODE) { - const viewID = path.split('/').pop(); - return ; + if (path.startsWith('/' + PRIVATE_FILE_TYPE.FILE_EXTENDED_PROPERTIES)) { + const viewId = path.split('/').pop(); + if (!dirent) return ( + + ); + return ( + + ); } if (path === '/' && !dirent) { return ( - + ); } - return ( { +const Header = ({ title, icon, iconSize = 32, onClose, component = {} }) => { const { closeIcon } = component; return (
- +
+ +
{title}
@@ -22,6 +24,7 @@ const Header = ({ title, icon, onClose, component = {} }) => { Header.propTypes = { title: PropTypes.string.isRequired, icon: PropTypes.string.isRequired, + iconSize: PropTypes.number, component: PropTypes.object, onClose: PropTypes.func.isRequired, }; diff --git a/frontend/src/components/dirent-detail/dirent-details/index.css b/frontend/src/components/dirent-detail/dirent-details/index.css index 9a0e83b5b6c..026a62878f0 100644 --- a/frontend/src/components/dirent-detail/dirent-details/index.css +++ b/frontend/src/components/dirent-detail/dirent-details/index.css @@ -42,3 +42,8 @@ .detail-body .sf-metadata-property-detail-tags.tags-empty { padding: 6.5px 6px;; } + +.detail-body .detail-content.detail-content-empty { + width: 100%; + height: 100%; +} diff --git a/frontend/src/metadata/components/gallery-details/index.js b/frontend/src/metadata/components/gallery-details/index.js deleted file mode 100644 index 323155b797f..00000000000 --- a/frontend/src/metadata/components/gallery-details/index.js +++ /dev/null @@ -1,133 +0,0 @@ -import React, { useEffect, useMemo, useState } from 'react'; -import PropTypes from 'prop-types'; -import Repo from '../../../models/repo'; -import toaster from '../../../components/toast'; -import Loading from '../../../components/loading'; -import DetailItem from '../../../components/dirent-detail/detail-item'; -import { Formatter } from '@seafile/sf-metadata-ui-component'; -import { Utils } from '../../../utils/utils'; -import { gettext, siteRoot, thumbnailSizeForGrid } from '../../../utils/constants'; -import { Detail, Header, Body } from '../../../components/dirent-detail/detail'; -import { CellType, PRIVATE_COLUMN_KEY } from '../../constants'; -import { useMetadata, useGallery } from '../../../metadata'; -import { seafileAPI } from '../../../utils/seafile-api'; -import FileDetails from '../../../components/dirent-detail/dirent-details/file-details'; -import { Dirent } from '../../../models'; - -const GalleryDetail = ({ currentRepoInfo, viewID, onClose }) => { - const [isLoading, setLoading] = useState(true); - const [repo, setRepo] = useState({}); - const [direntDetail, setDirentDetail] = useState(null); - const { viewsMap } = useMetadata(); - const { currentImage } = useGallery(); - - const view = useMemo(() => viewsMap[viewID], [viewID, viewsMap]); - const icon = useMemo(() => Utils.getFolderIconUrl(), []); - const creatorField = useMemo(() => ({ type: CellType.CREATOR, name: gettext('Creator') }), []); - const mtimeField = useMemo(() => ({ type: CellType.MTIME, name: gettext('Last modified time') }), []); - - useEffect(() => { - setLoading(true); - seafileAPI.getRepoInfo(currentRepoInfo.repo_id).then(res => { - const repo = new Repo(res.data); - setRepo(repo); - setLoading(false); - }).catch(error => { - let errMessage = Utils.getErrorMsg(error); - toaster.danger(errMessage); - }); - }, [currentRepoInfo.repo_id]); - - useEffect(() => { - const fetchDirentDetail = async () => { - if (currentImage) { - try { - const direntPath = Utils.joinPath(currentImage.path, currentImage.name); - const res = await seafileAPI.getFileInfo(currentRepoInfo.repo_id, direntPath); - setDirentDetail(res.data); - } catch (error) { - let errMessage = Utils.getErrorMsg(error); - toaster.danger(errMessage); - } - } else { - setDirentDetail(null); - } - }; - - fetchDirentDetail(); - }, [currentImage, currentRepoInfo.repo_id]); - - const dirent = useMemo(() => { - return currentImage ? new Dirent({ - id: currentImage.obj_id, - name: currentImage.name, - type: 'file' - }) : null; - }, [currentImage]); - - const smallIconUrl = useMemo(() => dirent ? Utils.getDirentIcon(dirent) : '', [dirent]); - const bigIconUrl = useMemo(() => dirent ? `${siteRoot}thumbnail/${currentRepoInfo.repo_id}/${thumbnailSizeForGrid}${Utils.encodePath(`${currentImage.path === '/' ? '' : currentImage.path}/${dirent.name}`)}` : '', [dirent, currentRepoInfo.repo_id, currentImage]); - - const renderGalleryInfo = () => ( - -
- - {isLoading ? ( -
- ) : ( -
- - - - - - -
- )} - - - ); - - const renderImageInfo = () => ( - -
- -
- -
- {dirent && direntDetail && ( -
- -
- )} - - - ); - - return currentImage ? renderImageInfo() : renderGalleryInfo(); -}; - -GalleryDetail.propTypes = { - currentRepoInfo: PropTypes.object.isRequired, - viewID: PropTypes.string.isRequired, - onClose: PropTypes.func.isRequired, -}; - -export default GalleryDetail; diff --git a/frontend/src/metadata/components/view-details/index.css b/frontend/src/metadata/components/view-details/index.css new file mode 100644 index 00000000000..752ea2215c6 --- /dev/null +++ b/frontend/src/metadata/components/view-details/index.css @@ -0,0 +1,3 @@ +.sf-metadata-view-detail .detail-content-empty .empty-tip { + margin: 0; +} diff --git a/frontend/src/metadata/components/view-details/index.js b/frontend/src/metadata/components/view-details/index.js new file mode 100644 index 00000000000..37a1356aa9a --- /dev/null +++ b/frontend/src/metadata/components/view-details/index.js @@ -0,0 +1,38 @@ +import React, { useMemo } from 'react'; +import PropTypes from 'prop-types'; +import { gettext, mediaUrl } from '../../../utils/constants'; +import { Detail, Header, Body } from '../../../components/dirent-detail/detail'; +import EmptyTip from '../../../components/empty-tip'; +import { useMetadata } from '../../hooks'; +import { VIEW_TYPE } from '../../constants'; + +import './index.css'; + +const ViewDetails = ({ viewId, onClose }) => { + const { viewsMap } = useMetadata(); + + const view = useMemo(() => viewsMap[viewId], [viewId, viewsMap]); + const icon = useMemo(() => { + const type = view.type; + if (type === VIEW_TYPE.GALLERY) return `${mediaUrl}favicons/gallery.png`; + if (type === VIEW_TYPE.TABLE) return `${mediaUrl}favicons/table.png`; + }, [view]); + + return ( + +
+ +
+ +
+ + + ); +}; + +ViewDetails.propTypes = { + viewId: PropTypes.string.isRequired, + onClose: PropTypes.func.isRequired, +}; + +export default ViewDetails; diff --git a/frontend/src/metadata/components/view-toolbar/gallery-view-toolbar/index.js b/frontend/src/metadata/components/view-toolbar/gallery-view-toolbar/index.js new file mode 100644 index 00000000000..79328d8634b --- /dev/null +++ b/frontend/src/metadata/components/view-toolbar/gallery-view-toolbar/index.js @@ -0,0 +1,71 @@ +import React, { useMemo } from 'react'; +import PropTypes from 'prop-types'; +import { GalleryGroupBySetter, GallerySliderSetter, FilterSetter, SortSetter } from '../../data-process-setter'; +import { PRIVATE_COLUMN_KEY } from '../../../constants'; +import { gettext } from '../../../../utils/constants'; + +const GalleryViewToolbar = ({ + readOnly, isCustomPermission, view, collaborators, + modifyFilters, modifySorts, showDetail, +}) => { + const viewType = useMemo(() => view.type, [view]); + const viewColumns = useMemo(() => { + if (!view) return []; + return view.columns; + }, [view]); + + const filterColumns = useMemo(() => { + return viewColumns.filter(c => c.key !== PRIVATE_COLUMN_KEY.FILE_TYPE); + }, [viewColumns]); + + return ( + <> +
+ + + + + {!isCustomPermission && ( +
+ +
+ )} +
+
+ + ); +}; + +GalleryViewToolbar.propTypes = { + readOnly: PropTypes.bool, + isCustomPermission: PropTypes.bool, + view: PropTypes.object.isRequired, + collaborators: PropTypes.array, + modifyFilters: PropTypes.func, + modifySorts: PropTypes.func, + showDetail: PropTypes.func, +}; + +export default GalleryViewToolbar; diff --git a/frontend/src/metadata/components/view-toolbar/index.js b/frontend/src/metadata/components/view-toolbar/index.js index 7a9b2e7713c..1404e142918 100644 --- a/frontend/src/metadata/components/view-toolbar/index.js +++ b/frontend/src/metadata/components/view-toolbar/index.js @@ -1,24 +1,15 @@ -import React, { useCallback, useEffect, useMemo, useState } from 'react'; +import React, { useCallback, useEffect, useState } from 'react'; import PropTypes from 'prop-types'; -import { GalleryGroupBySetter, GallerySliderSetter, FilterSetter, GroupbySetter, SortSetter, HideColumnSetter } from '../data-process-setter'; -import { EVENT_BUS_TYPE, PRIVATE_COLUMN_KEY, VIEW_TYPE } from '../../constants'; -import { gettext } from '../../../utils/constants'; +import { EVENT_BUS_TYPE, VIEW_TYPE } from '../../constants'; +import TableViewToolbar from './table-view-toolbar'; +import GalleryViewToolbar from './gallery-view-toolbar'; import './index.css'; -const ViewToolBar = ({ viewId, isCustomPermission, switchViewMode }) => { +const ViewToolBar = ({ viewId, isCustomPermission, showDetail }) => { const [view, setView] = useState(null); const [collaborators, setCollaborators] = useState([]); - const viewColumns = useMemo(() => { - if (!view) return []; - return view.columns; - }, [view]); - - const filterColumns = useMemo(() => { - return viewColumns.filter(c => c.key !== PRIVATE_COLUMN_KEY.FILE_TYPE); - }, [viewColumns]); - const onHeaderClick = useCallback(() => { window.sfMetadataContext.eventBus.dispatch(EVENT_BUS_TYPE.SELECT_NONE); }, []); @@ -68,73 +59,36 @@ const ViewToolBar = ({ viewId, isCustomPermission, switchViewMode }) => { if (!view) return null; const viewType = view.type; - const readOnly = !window.sfMetadataContext.canModifyView(view); + const readOnly = window.sfMetadataContext ? !window.sfMetadataContext.canModifyView(view) : true; return (
-
- {viewType === VIEW_TYPE.GALLERY && ( - <> - - - - )} - - - {viewType === VIEW_TYPE.GALLERY && !isCustomPermission && ( -
switchViewMode('detail')}> - -
- )} - {viewType !== VIEW_TYPE.GALLERY && ( - - )} - {viewType !== VIEW_TYPE.GALLERY && ( - - )} -
-
+ )}
); }; diff --git a/frontend/src/metadata/components/view-toolbar/table-view-toolbar/index.js b/frontend/src/metadata/components/view-toolbar/table-view-toolbar/index.js new file mode 100644 index 00000000000..8d3bc3c6c03 --- /dev/null +++ b/frontend/src/metadata/components/view-toolbar/table-view-toolbar/index.js @@ -0,0 +1,82 @@ +import React, { useMemo } from 'react'; +import PropTypes from 'prop-types'; +import { FilterSetter, GroupbySetter, SortSetter, HideColumnSetter } from '../../data-process-setter'; +import { PRIVATE_COLUMN_KEY } from '../../../constants'; + +const TableViewToolbar = ({ + readOnly, view, collaborators, + modifyFilters, modifySorts, modifyGroupbys, modifyHiddenColumns, modifyColumnOrder +}) => { + const viewType = useMemo(() => view.type, [view]); + const viewColumns = useMemo(() => { + if (!view) return []; + return view.columns; + }, [view]); + + const filterColumns = useMemo(() => { + return viewColumns.filter(c => c.key !== PRIVATE_COLUMN_KEY.FILE_TYPE); + }, [viewColumns]); + + return ( + <> +
+ + + + +
+
+ + ); +}; + +TableViewToolbar.propTypes = { + readOnly: PropTypes.bool, + view: PropTypes.object.isRequired, + collaborators: PropTypes.array, + modifyFilters: PropTypes.func, + modifySorts: PropTypes.func, + modifyGroupbys: PropTypes.func, + modifyHiddenColumns: PropTypes.func, + modifyColumnOrder: PropTypes.func, +}; + +export default TableViewToolbar; diff --git a/frontend/src/metadata/hooks/gallery.js b/frontend/src/metadata/hooks/gallery.js deleted file mode 100644 index 142f148ae0e..00000000000 --- a/frontend/src/metadata/hooks/gallery.js +++ /dev/null @@ -1,27 +0,0 @@ -import React, { useState, createContext, useContext, useCallback } from 'react'; - -const GalleryContext = createContext(); - -export const GalleryProvider = ({ children }) => { - const [currentImage, setCurrentImage] = useState(null); - - const updateCurrentImage = useCallback((image) => { - setCurrentImage(image); - }, []); - - return ( - - {children} - - ); -}; - -export const useGallery = () => { - const context = useContext(GalleryContext); - - if (!context) { - throw new Error('\'GalleryContext\' is null'); - } - - return context; -}; diff --git a/frontend/src/metadata/hooks/index.js b/frontend/src/metadata/hooks/index.js index 73beeb7a5a7..74b4c9ab566 100644 --- a/frontend/src/metadata/hooks/index.js +++ b/frontend/src/metadata/hooks/index.js @@ -1,4 +1,3 @@ export { MetadataProvider, useMetadata } from './metadata'; export { EnableMetadataProvider, useEnableMetadata } from './enable-metadata'; export { CollaboratorsProvider, useCollaborators } from './collaborators'; -export { GalleryProvider, useGallery } from './gallery'; diff --git a/frontend/src/metadata/hooks/metadata-view.js b/frontend/src/metadata/hooks/metadata-view.js index f5aa31e08f6..105af8eade9 100644 --- a/frontend/src/metadata/hooks/metadata-view.js +++ b/frontend/src/metadata/hooks/metadata-view.js @@ -124,6 +124,7 @@ export const MetadataViewProvider = ({ store: storeRef.current, deleteFilesCallback: params.deleteFilesCallback, renameFileCallback: params.renameFileCallback, + updateCurrentDirent: params.updateCurrentDirent, }} > {children} diff --git a/frontend/src/metadata/hooks/metadata.js b/frontend/src/metadata/hooks/metadata.js index 7c3a3d61d6d..7c572b7e8b8 100644 --- a/frontend/src/metadata/hooks/metadata.js +++ b/frontend/src/metadata/hooks/metadata.js @@ -137,11 +137,11 @@ export const MetadataProvider = ({ repoID, hideMetadataView, selectMetadataView, name: isFaceRecognitionView ? gettext('Photos - classfied by people') : gettext('File extended properties'), type: isFaceRecognitionView ? PRIVATE_FILE_TYPE.FACE_RECOGNITION : PRIVATE_FILE_TYPE.FILE_EXTENDED_PROPERTIES, isDir: () => false, - viewType: view.type, }, parentNode: {}, key: repoID, view_id: view._id, + view_type: view.type, }; selectMetadataView(node); // eslint-disable-next-line react-hooks/exhaustive-deps diff --git a/frontend/src/metadata/views/gallery/gallery-main.js b/frontend/src/metadata/views/gallery/gallery-main.js index 82a77b59071..b4a91efaa7a 100644 --- a/frontend/src/metadata/views/gallery/gallery-main.js +++ b/frontend/src/metadata/views/gallery/gallery-main.js @@ -1,4 +1,4 @@ -import React, { useState, useCallback, useMemo, useRef, useEffect } from 'react'; +import React, { useState, useCallback, useMemo, useRef } from 'react'; import classnames from 'classnames'; import PropTypes from 'prop-types'; import EmptyTip from '../../../components/empty-tip'; @@ -12,7 +12,7 @@ const GalleryMain = ({ size, gap, mode, - selectedImageIDs, + selectedImages, onImageSelect, onImageClick, onImageDoubleClick, @@ -26,6 +26,7 @@ const GalleryMain = ({ const [selectionStart, setSelectionStart] = useState(null); const imageHeight = useMemo(() => size + gap, [size, gap]); + const selectedImageIds = useMemo(() => selectedImages.map(img => img.id), [selectedImages]); const handleMouseDown = useCallback((e) => { if (e.button !== 0) return; @@ -80,28 +81,6 @@ const GalleryMain = ({ setIsSelecting(false); }, []); - const handleClickOutside = useCallback((e) => { - const images = containerRef.current.querySelectorAll('.metadata-gallery-image-item'); - let isClickInsideImage = false; - - images.forEach(img => { - if (img.contains(e.target)) { - isClickInsideImage = true; - } - }); - - if (!isClickInsideImage && containerRef.current.contains(e.target)) { - onImageSelect([]); - } - }, [onImageSelect]); - - useEffect(() => { - document.addEventListener('mousedown', handleClickOutside); - return () => { - document.removeEventListener('mousedown', handleClickOutside); - }; - }, [handleClickOutside]); - const renderDisplayGroup = useCallback((group) => { const { top: overScanTop, bottom: overScanBottom } = overScan; const { name, children, height, top, paddingTop } = group; @@ -138,6 +117,7 @@ const GalleryMain = ({
{name || gettext('Empty')}
)}
{children.slice(childrenStartIndex, childrenEndIndex + 1).map((row) => { return row.children.map((img) => { - const isSelected = selectedImageIDs.includes(img.id); + const isSelected = selectedImageIds.includes(img.id); return (
); - }, [overScan, columns, size, imageHeight, mode, selectedImageIDs, onImageClick, onImageDoubleClick, onImageRightClick]); + }, [overScan, columns, size, imageHeight, mode, selectedImageIds, onImageClick, onImageDoubleClick, onImageRightClick]); if (!Array.isArray(groups) || groups.length === 0) { return ; @@ -213,7 +192,7 @@ GalleryMain.propTypes = { size: PropTypes.number.isRequired, gap: PropTypes.number.isRequired, mode: PropTypes.string, - selectedImageIDs: PropTypes.array.isRequired, + selectedImages: PropTypes.array.isRequired, onImageSelect: PropTypes.func.isRequired, onImageClick: PropTypes.func.isRequired, onImageDoubleClick: PropTypes.func.isRequired, diff --git a/frontend/src/metadata/views/gallery/index.css b/frontend/src/metadata/views/gallery/index.css index f259d2ca56e..abefa0b1908 100644 --- a/frontend/src/metadata/views/gallery/index.css +++ b/frontend/src/metadata/views/gallery/index.css @@ -1,5 +1,5 @@ .sf-metadata-gallery-container { - height: calc(100vh - 100px); + height: 100%; padding: 0 16px; position: relative; display: flex; diff --git a/frontend/src/metadata/views/gallery/index.js b/frontend/src/metadata/views/gallery/index.js index eddfa5ae5e7..75f6ea6d1a1 100644 --- a/frontend/src/metadata/views/gallery/index.js +++ b/frontend/src/metadata/views/gallery/index.js @@ -9,13 +9,15 @@ import ImageDialog from '../../../components/dialog/image-dialog'; import ZipDownloadDialog from '../../../components/dialog/zip-download-dialog'; import ModalPortal from '../../../components/modal-portal'; import { useMetadataView } from '../../hooks/metadata-view'; +import { useCollaborators } from '../../hooks'; import { Utils } from '../../../utils/utils'; import { getDateDisplayString, getFileNameFromRecord, getParentDirFromRecord } from '../../utils/cell'; import { siteRoot, fileServerRoot, useGoFileserver, gettext, thumbnailSizeForGrid, thumbnailSizeForOriginal } from '../../../utils/constants'; import { EVENT_BUS_TYPE, PER_LOAD_NUMBER, PRIVATE_COLUMN_KEY, GALLERY_DATE_MODE, DATE_TAG_HEIGHT, GALLERY_IMAGE_GAP } from '../../constants'; -import { useGallery } from '../../hooks'; +import { getRowById } from '../../utils/table'; import './index.css'; +import { getEventClassName } from '../../utils/common'; const Gallery = () => { const [isFirstLoading, setFirstLoading] = useState(true); @@ -32,15 +34,14 @@ const Gallery = () => { const containerRef = useRef(null); const renderMoreTimer = useRef(null); - const { updateCurrentImage } = useGallery(); - const { metadata, store } = useMetadataView(); - const repoID = store.repoId; + const { metadata, store, updateCurrentDirent } = useMetadataView(); + const { getCollaborator } = useCollaborators(); + const repoID = window.sfMetadataContext.getSetting('repoID'); - useEffect(() => updateCurrentImage(selectedImages[0]), [selectedImages, updateCurrentImage]); - - const selectedImageIDs = useMemo(() => { - return selectedImages.map(image => image.id); - }, [selectedImages]); + useEffect(() => { + updateCurrentDirent(); + // eslint-disable-next-line react-hooks/exhaustive-deps + }, []); // Number of images per row const columns = useMemo(() => { @@ -214,22 +215,48 @@ const Gallery = () => { return groups.flatMap(group => group.children.flatMap(row => row.children)); }, [groups]); + const updateSelectedImage = useCallback((image = null) => { + const imageInfo = image ? getRowById(metadata, image.id) : null; + if (!imageInfo) { + updateCurrentDirent(); + return; + } + const size = imageInfo[PRIVATE_COLUMN_KEY.SIZE]; + const lastModifierEmail = imageInfo[PRIVATE_COLUMN_KEY.FILE_MODIFIER]; + const lastModifier = getCollaborator(lastModifierEmail); + const lastModified = imageInfo[PRIVATE_COLUMN_KEY.FILE_MTIME]; + updateCurrentDirent({ + type: 'file', + name: image.name, + path: image.path, + size: (typeof size === 'number') ? Utils.bytesToSize(size) : size, + last_modified: lastModified || '', + last_modifier_name: lastModifier.name || '', + last_modifier_email: lastModifier.email || '', + last_modifier_avatar: lastModified.avatar_url || '', + modifier_contact_email: lastModifier || '', + file_tags: [] + }); + }, [metadata, getCollaborator, updateCurrentDirent]); + const handleClick = useCallback((event, image) => { if (event.metaKey || event.ctrlKey) { setSelectedImages(prev => prev.includes(image) ? prev.filter(img => img !== image) : [...prev, image] ); + updateSelectedImage(image); } else if (event.shiftKey && selectedImages.length > 0) { const lastSelected = selectedImages[selectedImages.length - 1]; const start = imageItems.indexOf(lastSelected); const end = imageItems.indexOf(image); const range = imageItems.slice(Math.min(start, end), Math.max(start, end) + 1); setSelectedImages(prev => Array.from(new Set([...prev, ...range]))); + updateSelectedImage(null); } else { - updateCurrentImage(image); setSelectedImages([image]); + updateSelectedImage(image); } - }, [imageItems, selectedImages, updateCurrentImage]); + }, [imageItems, selectedImages, updateSelectedImage]); const handleDoubleClick = useCallback((event, image) => { const index = imageItems.findIndex(item => item.id === image.id); @@ -321,8 +348,18 @@ const Gallery = () => { setIsZipDialogOpen(false); }; + const handleClickOutside = useCallback((event) => { + const className = getEventClassName(event); + const isClickInsideImage = className.includes('metadata-gallery-image-item') || className.includes('metadata-gallery-grid-image'); + + if (!isClickInsideImage && containerRef.current.contains(event.target)) { + handleImageSelection([]); + updateSelectedImage(); + } + }, [handleImageSelection, updateSelectedImage]); + return ( -
+
{!isFirstLoading && ( <> @@ -333,7 +370,7 @@ const Gallery = () => { overScan={overScan} gap={GALLERY_IMAGE_GAP} mode={mode} - selectedImageIDs={selectedImageIDs} + selectedImages={selectedImages} onImageSelect={handleImageSelection} onImageClick={handleClick} onImageDoubleClick={handleDoubleClick} 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 1a871643ce5..911ca6cd68c 100644 --- a/frontend/src/pages/lib-content-view/lib-content-view.js +++ b/frontend/src/pages/lib-content-view/lib-content-view.js @@ -21,13 +21,14 @@ import CopyMoveDirentProgressDialog from '../../components/dialog/copy-move-dire import DeleteFolderDialog from '../../components/dialog/delete-folder-dialog'; import { EVENT_BUS_TYPE } from '../../components/common/event-bus-type'; import { PRIVATE_FILE_TYPE } from '../../constants'; -import { MetadataProvider, CollaboratorsProvider, GalleryProvider } from '../../metadata/hooks'; -import { LIST_MODE, METADATA_MODE, FACE_RECOGNITION_MODE, VIEW_TYPE } from '../../components/dir-view-mode/constants'; +import { MetadataProvider, CollaboratorsProvider } from '../../metadata/hooks'; +import { LIST_MODE, METADATA_MODE, FACE_RECOGNITION_MODE, DIRENT_DETAIL_MODE } from '../../components/dir-view-mode/constants'; import CurDirPath from '../../components/cur-dir-path'; import DirTool from '../../components/cur-dir-path/dir-tool'; import DetailContainer from '../../components/dirent-detail/detail-container'; import DirColumnView from '../../components/dir-view-mode/dir-column-view'; import SelectedDirentsToolbar from '../../components/toolbar/selected-dirents-toolbar'; +import { VIEW_TYPE } from '../../metadata/constants'; import '../../css/lib-content-view.css'; @@ -83,7 +84,6 @@ class LibContentView extends React.Component { dirID: '', // for update dir list errorMsg: '', isDirentDetailShow: false, - direntDetailPanelTab: '', itemsShowLength: 100, isSessionExpired: false, isCopyMoveProgressDialogShow: false, @@ -107,7 +107,11 @@ class LibContentView extends React.Component { this.unsubscribeEventBus = null; } - updateCurrentDirent = (deletedDirent) => { + updateCurrentDirent = (dirent = null) => { + this.setState({ currentDirent: dirent }); + }; + + updateCurrentNotExistDirent = (deletedDirent) => { let { currentDirent } = this.state; if (currentDirent && deletedDirent.name === currentDirent.name) { this.setState({ currentDirent: null }); @@ -124,31 +128,16 @@ class LibContentView extends React.Component { } }; - showDirentDetail = (direntDetailPanelTab) => { - if (direntDetailPanelTab) { - this.setState({ direntDetailPanelTab: direntDetailPanelTab }, () => { - this.setState({ isDirentDetailShow: true }); - }); - } else { - this.setState({ - direntDetailPanelTab: '', - isDirentDetailShow: true - }); - } + showDirentDetail = () => { + this.setState({ isDirentDetailShow: true }); }; toggleDirentDetail = () => { - this.setState({ - direntDetailPanelTab: '', - isDirentDetailShow: !this.state.isDirentDetailShow - }); + this.setState({ isDirentDetailShow: !this.state.isDirentDetailShow }); }; closeDirentDetail = () => { - this.setState({ - isDirentDetailShow: false, - direntDetailPanelTab: '', - }); + this.setState({ isDirentDetailShow: false }); }; componentDidMount() { @@ -1020,7 +1009,7 @@ class LibContentView extends React.Component { if (mode === this.state.currentMode) { return; } - if (mode === 'detail') { + if (mode === DIRENT_DETAIL_MODE) { this.toggleDirentDetail(); return; } @@ -1134,7 +1123,7 @@ class LibContentView extends React.Component { }; onMainPanelItemDelete = (dirent) => { - this.updateCurrentDirent(dirent); + this.updateCurrentNotExistDirent(dirent); let path = Utils.joinPath(this.state.path, dirent.name); this.deleteItem(path, dirent.isDir()); }; @@ -1261,7 +1250,7 @@ class LibContentView extends React.Component { // list operations onMoveItem = (destRepo, dirent, moveToDirentPath, nodeParentPath) => { - this.updateCurrentDirent(dirent); + this.updateCurrentNotExistDirent(dirent); let repoID = this.props.repoID; // just for view list state let dirName = dirent.name; @@ -1897,7 +1886,7 @@ class LibContentView extends React.Component { } } else if (Utils.isFileMetadata(node?.object?.type)) { if (node.path !== this.state.path) { - this.showFileMetadata(node.path, node.view_id || '0000', node.object.viewType); + this.showFileMetadata(node.path, node.view_id || '0000', node.view_type || VIEW_TYPE.TABLE); } } else if (Utils.isFaceRecognition(node?.object?.type)) { if (node.path !== this.state.path) { @@ -2360,94 +2349,92 @@ class LibContentView extends React.Component {
}
- -
- {this.state.pathExist ? - - : -
{gettext('Folder does not exist.')}
- } - {this.state.isDirentDetailShow && ( - - )} -
-
+
+ {this.state.pathExist ? + + : +
{gettext('Folder does not exist.')}
+ } + {this.state.isDirentDetailShow && ( + + )} +
{canUpload && this.state.pathExist && !this.state.isViewFile && this.state.currentMode !== METADATA_MODE && (