1
0
mirror of https://github.com/haiwen/seahub.git synced 2025-09-08 18:30:53 +00:00

Feature/gallery info side panel (#6876)

* gallery info side panel

* clean up code

* optimize code

* optimize code

* feat: optimize code

* feat: optimize code

---------

Co-authored-by: zhouwenxuan <aries@Mac.local>
Co-authored-by: 杨国璇 <ygx@Hello-word.local>
This commit is contained in:
Aries
2024-10-18 16:01:00 +08:00
committed by GitHub
parent d46423bae7
commit a9c0caff75
18 changed files with 338 additions and 117 deletions

View File

@@ -10,6 +10,7 @@ import ViewModes from '../../components/view-modes';
import ReposSortMenu from '../../components/repos-sort-menu'; import ReposSortMenu from '../../components/repos-sort-menu';
import MetadataViewToolBar from '../../metadata/components/view-toolbar'; import MetadataViewToolBar from '../../metadata/components/view-toolbar';
import { PRIVATE_FILE_TYPE } from '../../constants'; import { PRIVATE_FILE_TYPE } from '../../constants';
import { DIRENT_DETAIL_MODE } from '../dir-view-mode/constants';
const propTypes = { const propTypes = {
repoID: PropTypes.string.isRequired, repoID: PropTypes.string.isRequired,
@@ -97,10 +98,14 @@ class DirTool extends React.Component {
this.props.sortItems(sortBy, sortOrder); this.props.sortItems(sortBy, sortOrder);
}; };
showDirentDetail = () => {
this.props.switchViewMode(DIRENT_DETAIL_MODE);
};
render() { render() {
const menuItems = this.getMenu(); const menuItems = this.getMenu();
const { isDropdownMenuOpen } = this.state; const { isDropdownMenuOpen } = this.state;
const { repoID, currentMode, currentPath, sortBy, sortOrder, viewId } = this.props; const { repoID, currentMode, currentPath, sortBy, sortOrder, viewId, isCustomPermission } = this.props;
const propertiesText = TextTranslation.PROPERTIES.value; const propertiesText = TextTranslation.PROPERTIES.value;
const isFileExtended = currentPath.startsWith('/' + PRIVATE_FILE_TYPE.FILE_EXTENDED_PROPERTIES + '/'); const isFileExtended = currentPath.startsWith('/' + PRIVATE_FILE_TYPE.FILE_EXTENDED_PROPERTIES + '/');
@@ -114,7 +119,7 @@ class DirTool extends React.Component {
if (isFileExtended) { if (isFileExtended) {
return ( return (
<div className="dir-tool"> <div className="dir-tool">
<MetadataViewToolBar viewId={viewId} /> <MetadataViewToolBar viewId={viewId} isCustomPermission={isCustomPermission} showDetail={this.showDirentDetail} />
</div> </div>
); );
} }
@@ -124,8 +129,8 @@ class DirTool extends React.Component {
<div className="dir-tool d-flex"> <div className="dir-tool d-flex">
<ViewModes currentViewMode={currentMode} switchViewMode={this.props.switchViewMode} /> <ViewModes currentViewMode={currentMode} switchViewMode={this.props.switchViewMode} />
<ReposSortMenu sortOptions={sortOptions} onSelectSortOption={this.onSelectSortOption}/> <ReposSortMenu sortOptions={sortOptions} onSelectSortOption={this.onSelectSortOption}/>
{(!this.props.isCustomPermission) && {(!isCustomPermission) &&
<div className="cur-view-path-btn" onClick={() => this.props.switchViewMode('detail')}> <div className="cur-view-path-btn" onClick={this.showDirentDetail}>
<span className="sf3-font sf3-font-info" aria-label={propertiesText} title={propertiesText}></span> <span className="sf3-font sf3-font-info" aria-label={propertiesText} title={propertiesText}></span>
</div> </div>
} }

View File

@@ -1,4 +1,5 @@
export const LIST_MODE = 'list'; export const LIST_MODE = 'list';
export const GRID_MODE = 'grid'; export const GRID_MODE = 'grid';
export const DIRENT_DETAIL_MODE = 'detail';
export const METADATA_MODE = 'metadata'; export const METADATA_MODE = 'metadata';
export const FACE_RECOGNITION_MODE = 'person_image'; export const FACE_RECOGNITION_MODE = 'person_image';

View File

@@ -80,6 +80,7 @@ const propTypes = {
fullDirentList: PropTypes.array, fullDirentList: PropTypes.array,
onItemsScroll: PropTypes.func.isRequired, onItemsScroll: PropTypes.func.isRequired,
eventBus: PropTypes.object, eventBus: PropTypes.object,
updateCurrentDirent: PropTypes.func.isRequired,
}; };
class DirColumnView extends React.Component { class DirColumnView extends React.Component {
@@ -202,6 +203,7 @@ class DirColumnView extends React.Component {
viewID={this.props.viewId} viewID={this.props.viewId}
deleteFilesCallback={this.props.deleteFilesCallback} deleteFilesCallback={this.props.deleteFilesCallback}
renameFileCallback={this.props.renameFileCallback} renameFileCallback={this.props.renameFileCallback}
updateCurrentDirent={this.props.updateCurrentDirent}
/> />
} }
{currentMode === FACE_RECOGNITION_MODE && {currentMode === FACE_RECOGNITION_MODE &&

View File

@@ -2,12 +2,16 @@ import React, { useEffect } from 'react';
import PropTypes from 'prop-types'; import PropTypes from 'prop-types';
import LibDetail from './lib-details'; import LibDetail from './lib-details';
import DirentDetail from './dirent-details'; import DirentDetail from './dirent-details';
import ViewDetails from '../../metadata/components/view-details';
import ObjectUtils from '../../metadata/utils/object-utils'; import ObjectUtils from '../../metadata/utils/object-utils';
import { MetadataContext } from '../../metadata'; import { MetadataContext } from '../../metadata';
import { PRIVATE_FILE_TYPE } from '../../constants';
const DetailContainer = React.memo(({ repoID, path, dirent, currentRepoInfo, repoTags, fileTags, onClose, onFileTagChanged }) => { const DetailContainer = React.memo(({ repoID, path, dirent, currentRepoInfo, repoTags, fileTags, onClose, onFileTagChanged }) => {
useEffect(() => { useEffect(() => {
if (path.startsWith('/' + PRIVATE_FILE_TYPE.FILE_EXTENDED_PROPERTIES)) return;
// init context // init context
const context = new MetadataContext(); const context = new MetadataContext();
window.sfMetadataContext = context; window.sfMetadataContext = context;
@@ -16,7 +20,13 @@ const DetailContainer = React.memo(({ repoID, path, dirent, currentRepoInfo, rep
window.sfMetadataContext.destroy(); window.sfMetadataContext.destroy();
delete window['sfMetadataContext']; delete window['sfMetadataContext'];
}; };
}, [repoID, currentRepoInfo]); }, [repoID, currentRepoInfo, path]);
if (path.startsWith('/' + PRIVATE_FILE_TYPE.FILE_EXTENDED_PROPERTIES)) {
const viewId = path.split('/').pop();
if (!dirent) return (<ViewDetails viewId={viewId} onClose={onClose} />);
path = dirent.path;
}
if (path === '/' && !dirent) { if (path === '/' && !dirent) {
return ( return (

View File

@@ -16,6 +16,15 @@
width: 0; /* prevent strut flex layout */ width: 0; /* prevent strut flex layout */
} }
.detail-header .detail-title .detail-header-icon-container {
height: 32px;
width: 32px;
display: flex;
align-items: center;
justify-content: center;
flex-shrink: 0;
}
.detail-header .detail-title .name { .detail-header .detail-title .name {
margin: 0 0.5rem 0 6px; margin: 0 0.5rem 0 6px;
line-height: 1.5rem; line-height: 1.5rem;

View File

@@ -4,12 +4,14 @@ import Icon from '../../../icon';
import './index.css'; import './index.css';
const Header = ({ title, icon, onClose, component = {} }) => { const Header = ({ title, icon, iconSize = 32, onClose, component = {} }) => {
const { closeIcon } = component; const { closeIcon } = component;
return ( return (
<div className="detail-header"> <div className="detail-header">
<div className="detail-title dirent-title"> <div className="detail-title dirent-title">
<img src={icon} width="32" height="32" alt="" /> <div className="detail-header-icon-container">
<img src={icon} width={iconSize} height={iconSize} alt="" />
</div>
<span className="name ellipsis" title={title}>{title}</span> <span className="name ellipsis" title={title}>{title}</span>
</div> </div>
<div className="detail-control" onClick={onClose}> <div className="detail-control" onClick={onClose}>
@@ -22,6 +24,7 @@ const Header = ({ title, icon, onClose, component = {} }) => {
Header.propTypes = { Header.propTypes = {
title: PropTypes.string.isRequired, title: PropTypes.string.isRequired,
icon: PropTypes.string.isRequired, icon: PropTypes.string.isRequired,
iconSize: PropTypes.number,
component: PropTypes.object, component: PropTypes.object,
onClose: PropTypes.func.isRequired, onClose: PropTypes.func.isRequired,
}; };

View File

@@ -42,3 +42,8 @@
.detail-body .sf-metadata-property-detail-tags.tags-empty { .detail-body .sf-metadata-property-detail-tags.tags-empty {
padding: 6.5px 6px;; padding: 6.5px 6px;;
} }
.detail-body .detail-content.detail-content-empty {
width: 100%;
height: 100%;
}

View File

@@ -0,0 +1,3 @@
.sf-metadata-view-detail .detail-content-empty .empty-tip {
margin: 0;
}

View File

@@ -0,0 +1,39 @@
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`;
return `${mediaUrl}img/file/256/file.png`;
}, [view]);
return (
<Detail className="sf-metadata-view-detail">
<Header title={view.name} icon={icon} iconSize={28} onClose={onClose} />
<Body>
<div className="detail-content detail-content-empty">
<EmptyTip text={gettext('There is no information to display.')} />
</div>
</Body>
</Detail>
);
};
ViewDetails.propTypes = {
viewId: PropTypes.string.isRequired,
onClose: PropTypes.func.isRequired,
};
export default ViewDetails;

View File

@@ -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 (
<>
<div className="sf-metadata-tool-left-operations">
<GalleryGroupBySetter view={view} />
<GallerySliderSetter view={view} />
<FilterSetter
isNeedSubmit={true}
wrapperClass="sf-metadata-view-tool-operation-btn sf-metadata-view-tool-filter"
filtersClassName="sf-metadata-filters"
target="sf-metadata-filter-popover"
readOnly={readOnly}
filterConjunction={view.filter_conjunction}
basicFilters={view.basic_filters}
filters={view.filters}
columns={filterColumns}
modifyFilters={modifyFilters}
collaborators={collaborators}
viewType={viewType}
/>
<SortSetter
isNeedSubmit={true}
wrapperClass="sf-metadata-view-tool-operation-btn sf-metadata-view-tool-sort"
target="sf-metadata-sort-popover"
readOnly={readOnly}
sorts={view.sorts}
type={viewType}
columns={viewColumns}
modifySorts={modifySorts}
/>
{!isCustomPermission && (
<div className="cur-view-path-btn ml-2" onClick={showDetail}>
<span className="sf3-font sf3-font-info" aria-label={gettext('Properties')} title={gettext('Properties')}></span>
</div>
)}
</div>
<div className="sf-metadata-tool-right-operations"></div>
</>
);
};
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;

View File

@@ -1,23 +1,15 @@
import React, { useCallback, useEffect, useMemo, useState } from 'react'; import React, { useCallback, useEffect, useState } from 'react';
import PropTypes from 'prop-types'; import PropTypes from 'prop-types';
import { GalleryGroupBySetter, GallerySliderSetter, FilterSetter, GroupbySetter, SortSetter, HideColumnSetter } from '../data-process-setter'; import { EVENT_BUS_TYPE, VIEW_TYPE } from '../../constants';
import { EVENT_BUS_TYPE, PRIVATE_COLUMN_KEY, VIEW_TYPE } from '../../constants'; import TableViewToolbar from './table-view-toolbar';
import GalleryViewToolbar from './gallery-view-toolbar';
import './index.css'; import './index.css';
const ViewToolBar = ({ viewId }) => { const ViewToolBar = ({ viewId, isCustomPermission, showDetail }) => {
const [view, setView] = useState(null); const [view, setView] = useState(null);
const [collaborators, setCollaborators] = useState([]); 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(() => { const onHeaderClick = useCallback(() => {
window.sfMetadataContext.eventBus.dispatch(EVENT_BUS_TYPE.SELECT_NONE); window.sfMetadataContext.eventBus.dispatch(EVENT_BUS_TYPE.SELECT_NONE);
}, []); }, []);
@@ -67,74 +59,44 @@ const ViewToolBar = ({ viewId }) => {
if (!view) return null; if (!view) return null;
const viewType = view.type; const viewType = view.type;
const readOnly = !window.sfMetadataContext.canModifyView(view); const readOnly = window.sfMetadataContext ? !window.sfMetadataContext.canModifyView(view) : true;
return ( return (
<div <div
className="sf-metadata-tool" className="sf-metadata-tool"
onClick={onHeaderClick} onClick={onHeaderClick}
> >
<div className="sf-metadata-tool-left-operations"> {viewType === VIEW_TYPE.TABLE && (
{viewType === VIEW_TYPE.GALLERY && ( <TableViewToolbar
<>
<GalleryGroupBySetter view={view} />
<GallerySliderSetter view={view} />
</>
)}
<FilterSetter
isNeedSubmit={true}
wrapperClass="sf-metadata-view-tool-operation-btn sf-metadata-view-tool-filter"
filtersClassName="sf-metadata-filters"
target="sf-metadata-filter-popover"
readOnly={readOnly} readOnly={readOnly}
filterConjunction={view.filter_conjunction} view={view}
basicFilters={view.basic_filters}
filters={view.filters}
columns={filterColumns}
modifyFilters={modifyFilters}
collaborators={collaborators} collaborators={collaborators}
viewType={viewType} modifyFilters={modifyFilters}
/>
<SortSetter
isNeedSubmit={true}
wrapperClass="sf-metadata-view-tool-operation-btn sf-metadata-view-tool-sort"
target="sf-metadata-sort-popover"
readOnly={readOnly}
sorts={view.sorts}
type={viewType}
columns={viewColumns}
modifySorts={modifySorts} modifySorts={modifySorts}
modifyGroupbys={modifyGroupbys}
modifyHiddenColumns={modifyHiddenColumns}
modifyColumnOrder={modifyColumnOrder}
/> />
{viewType !== VIEW_TYPE.GALLERY && ( )}
<GroupbySetter {viewType === VIEW_TYPE.GALLERY && (
isNeedSubmit={true} <GalleryViewToolbar
wrapperClass="sf-metadata-view-tool-operation-btn sf-metadata-view-tool-groupby" readOnly={readOnly}
target="sf-metadata-groupby-popover" isCustomPermission={isCustomPermission}
readOnly={readOnly} view={view}
columns={viewColumns} collaborators={collaborators}
groupbys={view.groupbys} modifyFilters={modifyFilters}
modifyGroupbys={modifyGroupbys} modifySorts={modifySorts}
/> showDetail={showDetail}
)} />
{viewType !== VIEW_TYPE.GALLERY && ( )}
<HideColumnSetter
wrapperClass="sf-metadata-view-tool-operation-btn sf-metadata-view-tool-hide-column"
target="sf-metadata-hide-column-popover"
readOnly={readOnly}
columns={viewColumns.slice(1)}
hiddenColumns={view.hidden_columns || []}
modifyHiddenColumns={modifyHiddenColumns}
modifyColumnOrder={modifyColumnOrder}
/>
)}
</div>
<div className="sf-metadata-tool-right-operations"></div>
</div> </div>
); );
}; };
ViewToolBar.propTypes = { ViewToolBar.propTypes = {
viewId: PropTypes.string, viewId: PropTypes.string,
isCustomPermission: PropTypes.bool,
switchViewMode: PropTypes.func,
}; };
export default ViewToolBar; export default ViewToolBar;

View File

@@ -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 (
<>
<div className="sf-metadata-tool-left-operations">
<FilterSetter
isNeedSubmit={true}
wrapperClass="sf-metadata-view-tool-operation-btn sf-metadata-view-tool-filter"
filtersClassName="sf-metadata-filters"
target="sf-metadata-filter-popover"
readOnly={readOnly}
filterConjunction={view.filter_conjunction}
basicFilters={view.basic_filters}
filters={view.filters}
columns={filterColumns}
modifyFilters={modifyFilters}
collaborators={collaborators}
viewType={viewType}
/>
<SortSetter
isNeedSubmit={true}
wrapperClass="sf-metadata-view-tool-operation-btn sf-metadata-view-tool-sort"
target="sf-metadata-sort-popover"
readOnly={readOnly}
sorts={view.sorts}
type={viewType}
columns={viewColumns}
modifySorts={modifySorts}
/>
<GroupbySetter
isNeedSubmit={true}
wrapperClass="sf-metadata-view-tool-operation-btn sf-metadata-view-tool-groupby"
target="sf-metadata-groupby-popover"
readOnly={readOnly}
columns={viewColumns}
groupbys={view.groupbys}
modifyGroupbys={modifyGroupbys}
/>
<HideColumnSetter
wrapperClass="sf-metadata-view-tool-operation-btn sf-metadata-view-tool-hide-column"
target="sf-metadata-hide-column-popover"
readOnly={readOnly}
columns={viewColumns.slice(1)}
hiddenColumns={view.hidden_columns || []}
modifyHiddenColumns={modifyHiddenColumns}
modifyColumnOrder={modifyColumnOrder}
/>
</div>
<div className="sf-metadata-tool-right-operations"></div>
</>
);
};
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;

View File

@@ -124,6 +124,7 @@ export const MetadataViewProvider = ({
store: storeRef.current, store: storeRef.current,
deleteFilesCallback: params.deleteFilesCallback, deleteFilesCallback: params.deleteFilesCallback,
renameFileCallback: params.renameFileCallback, renameFileCallback: params.renameFileCallback,
updateCurrentDirent: params.updateCurrentDirent,
}} }}
> >
{children} {children}

View File

@@ -141,6 +141,7 @@ export const MetadataProvider = ({ repoID, hideMetadataView, selectMetadataView,
parentNode: {}, parentNode: {},
key: repoID, key: repoID,
view_id: view._id, view_id: view._id,
view_type: view.type,
}; };
selectMetadataView(node); selectMetadataView(node);
// eslint-disable-next-line react-hooks/exhaustive-deps // eslint-disable-next-line react-hooks/exhaustive-deps

View File

@@ -13,7 +13,7 @@ const GalleryMain = ({
gap, gap,
mode, mode,
selectedImages, selectedImages,
setSelectedImages, onImageSelect,
onImageClick, onImageClick,
onImageDoubleClick, onImageDoubleClick,
onImageRightClick onImageRightClick
@@ -26,6 +26,7 @@ const GalleryMain = ({
const [selectionStart, setSelectionStart] = useState(null); const [selectionStart, setSelectionStart] = useState(null);
const imageHeight = useMemo(() => size + gap, [size, gap]); const imageHeight = useMemo(() => size + gap, [size, gap]);
const selectedImageIds = useMemo(() => selectedImages.map(img => img.id), [selectedImages]);
const handleMouseDown = useCallback((e) => { const handleMouseDown = useCallback((e) => {
if (e.button !== 0) return; if (e.button !== 0) return;
@@ -33,9 +34,7 @@ const GalleryMain = ({
setIsSelecting(true); setIsSelecting(true);
setSelectionStart({ x: e.clientX, y: e.clientY }); setSelectionStart({ x: e.clientX, y: e.clientY });
setSelectedImages([]); }, []);
}, [setSelectedImages]);
const handleMouseMove = useCallback((e) => { const handleMouseMove = useCallback((e) => {
if (!isSelecting) return; if (!isSelecting) return;
@@ -59,9 +58,9 @@ const GalleryMain = ({
const rect = imgElement.getBoundingClientRect(); const rect = imgElement.getBoundingClientRect();
if ( if (
rect.left < Math.max(selectionStart.x, selectionEnd.x) && rect.left < Math.max(selectionStart.x, selectionEnd.x) &&
rect.right > Math.min(selectionStart.x, selectionEnd.x) && rect.right > Math.min(selectionStart.x, selectionEnd.x) &&
rect.top < Math.max(selectionStart.y, selectionEnd.y) && rect.top < Math.max(selectionStart.y, selectionEnd.y) &&
rect.bottom > Math.min(selectionStart.y, selectionEnd.y) rect.bottom > Math.min(selectionStart.y, selectionEnd.y)
) { ) {
selected.push(img); selected.push(img);
} }
@@ -70,9 +69,9 @@ const GalleryMain = ({
}); });
}); });
setSelectedImages(selected); onImageSelect(selected);
}); });
}, [groups, isSelecting, selectionStart, setSelectedImages]); }, [groups, isSelecting, selectionStart, onImageSelect]);
const handleMouseUp = useCallback((e) => { const handleMouseUp = useCallback((e) => {
if (e.button !== 0) return; if (e.button !== 0) return;
@@ -113,7 +112,6 @@ const GalleryMain = ({
key={name} key={name}
className="metadata-gallery-date-group" className="metadata-gallery-date-group"
style={{ height, paddingTop }} style={{ height, paddingTop }}
> >
{mode !== GALLERY_DATE_MODE.ALL && childrenStartIndex === 0 && ( {mode !== GALLERY_DATE_MODE.ALL && childrenStartIndex === 0 && (
<div className="metadata-gallery-date-tag">{name || gettext('Empty')}</div> <div className="metadata-gallery-date-tag">{name || gettext('Empty')}</div>
@@ -129,7 +127,7 @@ const GalleryMain = ({
> >
{children.slice(childrenStartIndex, childrenEndIndex + 1).map((row) => { {children.slice(childrenStartIndex, childrenEndIndex + 1).map((row) => {
return row.children.map((img) => { return row.children.map((img) => {
const isSelected = selectedImages.includes(img); const isSelected = selectedImageIds.includes(img.id);
return ( return (
<div <div
key={img.src} key={img.src}
@@ -151,7 +149,7 @@ const GalleryMain = ({
</div> </div>
</div> </div>
); );
}, [overScan, columns, size, imageHeight, mode, selectedImages, onImageClick, onImageDoubleClick, onImageRightClick]); }, [overScan, columns, size, imageHeight, mode, selectedImageIds, onImageClick, onImageDoubleClick, onImageRightClick]);
if (!Array.isArray(groups) || groups.length === 0) { if (!Array.isArray(groups) || groups.length === 0) {
return <EmptyTip text={gettext('No record')}/>; return <EmptyTip text={gettext('No record')}/>;
@@ -195,6 +193,7 @@ GalleryMain.propTypes = {
gap: PropTypes.number.isRequired, gap: PropTypes.number.isRequired,
mode: PropTypes.string, mode: PropTypes.string,
selectedImages: PropTypes.array.isRequired, selectedImages: PropTypes.array.isRequired,
onImageSelect: PropTypes.func.isRequired,
onImageClick: PropTypes.func.isRequired, onImageClick: PropTypes.func.isRequired,
onImageDoubleClick: PropTypes.func.isRequired, onImageDoubleClick: PropTypes.func.isRequired,
onImageRightClick: PropTypes.func.isRequired, onImageRightClick: PropTypes.func.isRequired,

View File

@@ -1,5 +1,5 @@
.sf-metadata-gallery-container { .sf-metadata-gallery-container {
height: calc(100vh - 100px); height: 100%;
padding: 0 16px; padding: 0 16px;
position: relative; position: relative;
display: flex; display: flex;

View File

@@ -13,6 +13,8 @@ import { Utils } from '../../../utils/utils';
import { getDateDisplayString, getFileNameFromRecord, getParentDirFromRecord } from '../../utils/cell'; import { getDateDisplayString, getFileNameFromRecord, getParentDirFromRecord } from '../../utils/cell';
import { siteRoot, fileServerRoot, useGoFileserver, gettext, thumbnailSizeForGrid, thumbnailSizeForOriginal } from '../../../utils/constants'; 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 { EVENT_BUS_TYPE, PER_LOAD_NUMBER, PRIVATE_COLUMN_KEY, GALLERY_DATE_MODE, DATE_TAG_HEIGHT, GALLERY_IMAGE_GAP } from '../../constants';
import { getRowById } from '../../utils/table';
import { getEventClassName } from '../../utils/common';
import './index.css'; import './index.css';
@@ -31,9 +33,14 @@ const Gallery = () => {
const containerRef = useRef(null); const containerRef = useRef(null);
const renderMoreTimer = useRef(null); const renderMoreTimer = useRef(null);
const { metadata, store } = useMetadataView(); const { metadata, store, updateCurrentDirent } = useMetadataView();
const repoID = window.sfMetadataContext.getSetting('repoID'); const repoID = window.sfMetadataContext.getSetting('repoID');
useEffect(() => {
updateCurrentDirent();
// eslint-disable-next-line react-hooks/exhaustive-deps
}, []);
// Number of images per row // Number of images per row
const columns = useMemo(() => { const columns = useMemo(() => {
return 8 - zoomGear; return 8 - zoomGear;
@@ -206,21 +213,38 @@ const Gallery = () => {
return groups.flatMap(group => group.children.flatMap(row => row.children)); return groups.flatMap(group => group.children.flatMap(row => row.children));
}, [groups]); }, [groups]);
const updateSelectedImage = useCallback((image = null) => {
const imageInfo = image ? getRowById(metadata, image.id) : null;
if (!imageInfo) {
updateCurrentDirent();
return;
}
updateCurrentDirent({
type: 'file',
name: image.name,
path: image.path,
file_tags: []
});
}, [metadata, updateCurrentDirent]);
const handleClick = useCallback((event, image) => { const handleClick = useCallback((event, image) => {
if (event.metaKey || event.ctrlKey) { if (event.metaKey || event.ctrlKey) {
setSelectedImages(prev => setSelectedImages(prev =>
prev.includes(image) ? prev.filter(img => img !== image) : [...prev, image] prev.includes(image) ? prev.filter(img => img !== image) : [...prev, image]
); );
updateSelectedImage(image);
} else if (event.shiftKey && selectedImages.length > 0) { } else if (event.shiftKey && selectedImages.length > 0) {
const lastSelected = selectedImages[selectedImages.length - 1]; const lastSelected = selectedImages[selectedImages.length - 1];
const start = imageItems.indexOf(lastSelected); const start = imageItems.indexOf(lastSelected);
const end = imageItems.indexOf(image); const end = imageItems.indexOf(image);
const range = imageItems.slice(Math.min(start, end), Math.max(start, end) + 1); const range = imageItems.slice(Math.min(start, end), Math.max(start, end) + 1);
setSelectedImages(prev => Array.from(new Set([...prev, ...range]))); setSelectedImages(prev => Array.from(new Set([...prev, ...range])));
updateSelectedImage(null);
} else { } else {
setSelectedImages([image]); setSelectedImages([image]);
updateSelectedImage(image);
} }
}, [imageItems, selectedImages]); }, [imageItems, selectedImages, updateSelectedImage]);
const handleDoubleClick = useCallback((event, image) => { const handleDoubleClick = useCallback((event, image) => {
const index = imageItems.findIndex(item => item.id === image.id); const index = imageItems.findIndex(item => item.id === image.id);
@@ -246,6 +270,10 @@ const Gallery = () => {
setImageIndex((prevState) => (prevState + 1) % imageItemsLength); setImageIndex((prevState) => (prevState + 1) % imageItemsLength);
}; };
const handleImageSelection = useCallback((selectedImages) => {
setSelectedImages(selectedImages);
}, []);
const closeImagePopup = () => { const closeImagePopup = () => {
setIsImagePopupOpen(false); setIsImagePopupOpen(false);
}; };
@@ -308,8 +336,18 @@ const Gallery = () => {
setIsZipDialogOpen(false); 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 ( return (
<div className="sf-metadata-container"> <div className="sf-metadata-container" onMouseDown={handleClickOutside}>
<div className={`sf-metadata-gallery-container sf-metadata-gallery-container-${mode}`} ref={containerRef} onScroll={handleScroll} > <div className={`sf-metadata-gallery-container sf-metadata-gallery-container-${mode}`} ref={containerRef} onScroll={handleScroll} >
{!isFirstLoading && ( {!isFirstLoading && (
<> <>
@@ -321,7 +359,7 @@ const Gallery = () => {
gap={GALLERY_IMAGE_GAP} gap={GALLERY_IMAGE_GAP}
mode={mode} mode={mode}
selectedImages={selectedImages} selectedImages={selectedImages}
setSelectedImages={setSelectedImages} onImageSelect={handleImageSelection}
onImageClick={handleClick} onImageClick={handleClick}
onImageDoubleClick={handleDoubleClick} onImageDoubleClick={handleDoubleClick}
onImageRightClick={handleRightClick} onImageRightClick={handleRightClick}

View File

@@ -22,12 +22,13 @@ import DeleteFolderDialog from '../../components/dialog/delete-folder-dialog';
import { EVENT_BUS_TYPE } from '../../components/common/event-bus-type'; import { EVENT_BUS_TYPE } from '../../components/common/event-bus-type';
import { PRIVATE_FILE_TYPE } from '../../constants'; import { PRIVATE_FILE_TYPE } from '../../constants';
import { MetadataProvider, CollaboratorsProvider } from '../../metadata/hooks'; import { MetadataProvider, CollaboratorsProvider } from '../../metadata/hooks';
import { LIST_MODE, METADATA_MODE, FACE_RECOGNITION_MODE } from '../../components/dir-view-mode/constants'; 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 CurDirPath from '../../components/cur-dir-path';
import DirTool from '../../components/cur-dir-path/dir-tool'; import DirTool from '../../components/cur-dir-path/dir-tool';
import DetailContainer from '../../components/dirent-detail/detail-container'; import DetailContainer from '../../components/dirent-detail/detail-container';
import DirColumnView from '../../components/dir-view-mode/dir-column-view'; import DirColumnView from '../../components/dir-view-mode/dir-column-view';
import SelectedDirentsToolbar from '../../components/toolbar/selected-dirents-toolbar'; import SelectedDirentsToolbar from '../../components/toolbar/selected-dirents-toolbar';
import { VIEW_TYPE } from '../../metadata/constants';
import '../../css/lib-content-view.css'; import '../../css/lib-content-view.css';
@@ -83,7 +84,6 @@ class LibContentView extends React.Component {
dirID: '', // for update dir list dirID: '', // for update dir list
errorMsg: '', errorMsg: '',
isDirentDetailShow: false, isDirentDetailShow: false,
direntDetailPanelTab: '',
itemsShowLength: 100, itemsShowLength: 100,
isSessionExpired: false, isSessionExpired: false,
isCopyMoveProgressDialogShow: false, isCopyMoveProgressDialogShow: false,
@@ -107,7 +107,11 @@ class LibContentView extends React.Component {
this.unsubscribeEventBus = null; this.unsubscribeEventBus = null;
} }
updateCurrentDirent = (deletedDirent) => { updateCurrentDirent = (dirent = null) => {
this.setState({ currentDirent: dirent });
};
updateCurrentNotExistDirent = (deletedDirent) => {
let { currentDirent } = this.state; let { currentDirent } = this.state;
if (currentDirent && deletedDirent.name === currentDirent.name) { if (currentDirent && deletedDirent.name === currentDirent.name) {
this.setState({ currentDirent: null }); this.setState({ currentDirent: null });
@@ -124,31 +128,16 @@ class LibContentView extends React.Component {
} }
}; };
showDirentDetail = (direntDetailPanelTab) => { showDirentDetail = () => {
if (direntDetailPanelTab) { this.setState({ isDirentDetailShow: true });
this.setState({ direntDetailPanelTab: direntDetailPanelTab }, () => {
this.setState({ isDirentDetailShow: true });
});
} else {
this.setState({
direntDetailPanelTab: '',
isDirentDetailShow: true
});
}
}; };
toggleDirentDetail = () => { toggleDirentDetail = () => {
this.setState({ this.setState({ isDirentDetailShow: !this.state.isDirentDetailShow });
direntDetailPanelTab: '',
isDirentDetailShow: !this.state.isDirentDetailShow
});
}; };
closeDirentDetail = () => { closeDirentDetail = () => {
this.setState({ this.setState({ isDirentDetailShow: false });
isDirentDetailShow: false,
direntDetailPanelTab: '',
});
}; };
componentDidMount() { componentDidMount() {
@@ -538,14 +527,14 @@ class LibContentView extends React.Component {
window.history.pushState({ url: url, path: filePath }, filePath, url); window.history.pushState({ url: url, path: filePath }, filePath, url);
}; };
showFileMetadata = (filePath, viewId) => { showFileMetadata = (filePath, viewId, viewType) => {
const repoID = this.props.repoID; const repoID = this.props.repoID;
const repoInfo = this.state.currentRepoInfo; const repoInfo = this.state.currentRepoInfo;
this.setState({ this.setState({
currentMode: METADATA_MODE, currentMode: METADATA_MODE,
path: filePath, path: filePath,
viewId: viewId, viewId: viewId,
isDirentDetailShow: false isDirentDetailShow: viewType === VIEW_TYPE.GALLERY ? this.state.isDirentDetailShow : false,
}, () => { }, () => {
setTimeout(() => { setTimeout(() => {
this.unsubscribeEventBus = window.sfMetadataContext.eventBus.subscribe(EVENT_BUS_TYPE.OPEN_MARKDOWN_DIALOG, this.openMarkDownDialog); this.unsubscribeEventBus = window.sfMetadataContext.eventBus.subscribe(EVENT_BUS_TYPE.OPEN_MARKDOWN_DIALOG, this.openMarkDownDialog);
@@ -1020,7 +1009,7 @@ class LibContentView extends React.Component {
if (mode === this.state.currentMode) { if (mode === this.state.currentMode) {
return; return;
} }
if (mode === 'detail') { if (mode === DIRENT_DETAIL_MODE) {
this.toggleDirentDetail(); this.toggleDirentDetail();
return; return;
} }
@@ -1134,7 +1123,7 @@ class LibContentView extends React.Component {
}; };
onMainPanelItemDelete = (dirent) => { onMainPanelItemDelete = (dirent) => {
this.updateCurrentDirent(dirent); this.updateCurrentNotExistDirent(dirent);
let path = Utils.joinPath(this.state.path, dirent.name); let path = Utils.joinPath(this.state.path, dirent.name);
this.deleteItem(path, dirent.isDir()); this.deleteItem(path, dirent.isDir());
}; };
@@ -1261,7 +1250,7 @@ class LibContentView extends React.Component {
// list operations // list operations
onMoveItem = (destRepo, dirent, moveToDirentPath, nodeParentPath) => { onMoveItem = (destRepo, dirent, moveToDirentPath, nodeParentPath) => {
this.updateCurrentDirent(dirent); this.updateCurrentNotExistDirent(dirent);
let repoID = this.props.repoID; let repoID = this.props.repoID;
// just for view list state // just for view list state
let dirName = dirent.name; let dirName = dirent.name;
@@ -1897,7 +1886,7 @@ class LibContentView extends React.Component {
} }
} else if (Utils.isFileMetadata(node?.object?.type)) { } else if (Utils.isFileMetadata(node?.object?.type)) {
if (node.path !== this.state.path) { if (node.path !== this.state.path) {
this.showFileMetadata(node.path, node.view_id || '0000'); this.showFileMetadata(node.path, node.view_id || '0000', node.view_type || VIEW_TYPE.TABLE);
} }
} else if (Utils.isFaceRecognition(node?.object?.type)) { } else if (Utils.isFaceRecognition(node?.object?.type)) {
if (node.path !== this.state.path) { if (node.path !== this.state.path) {
@@ -2428,6 +2417,7 @@ class LibContentView extends React.Component {
getMarkDownFilePath={this.getMarkDownFilePath} getMarkDownFilePath={this.getMarkDownFilePath}
getMarkDownFileName={this.getMarkDownFileName} getMarkDownFileName={this.getMarkDownFileName}
openMarkdownFile={this.openMarkdownFile} openMarkdownFile={this.openMarkdownFile}
updateCurrentDirent={this.updateCurrentDirent}
/> />
: :
<div className="message err-tip">{gettext('Folder does not exist.')}</div> <div className="message err-tip">{gettext('Folder does not exist.')}</div>