mirror of
https://github.com/haiwen/seahub.git
synced 2025-09-12 13:24:52 +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:
@@ -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 } = 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 (
|
||||
<div className="dir-tool">
|
||||
<MetadataViewToolBar viewId={viewId} />
|
||||
<MetadataViewToolBar viewId={viewId} isCustomPermission={isCustomPermission} showDetail={this.showDirentDetail} />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -124,8 +129,8 @@ class DirTool extends React.Component {
|
||||
<div className="dir-tool d-flex">
|
||||
<ViewModes currentViewMode={currentMode} switchViewMode={this.props.switchViewMode} />
|
||||
<ReposSortMenu sortOptions={sortOptions} onSelectSortOption={this.onSelectSortOption}/>
|
||||
{(!this.props.isCustomPermission) &&
|
||||
<div className="cur-view-path-btn" onClick={() => this.props.switchViewMode('detail')}>
|
||||
{(!isCustomPermission) &&
|
||||
<div className="cur-view-path-btn" onClick={this.showDirentDetail}>
|
||||
<span className="sf3-font sf3-font-info" aria-label={propertiesText} title={propertiesText}></span>
|
||||
</div>
|
||||
}
|
||||
|
@@ -1,4 +1,5 @@
|
||||
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';
|
||||
|
@@ -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 &&
|
||||
|
@@ -2,12 +2,16 @@ import React, { useEffect } from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import LibDetail from './lib-details';
|
||||
import DirentDetail from './dirent-details';
|
||||
import ViewDetails from '../../metadata/components/view-details';
|
||||
import ObjectUtils from '../../metadata/utils/object-utils';
|
||||
import { MetadataContext } from '../../metadata';
|
||||
import { PRIVATE_FILE_TYPE } from '../../constants';
|
||||
|
||||
const DetailContainer = React.memo(({ repoID, path, dirent, currentRepoInfo, repoTags, fileTags, onClose, onFileTagChanged }) => {
|
||||
|
||||
useEffect(() => {
|
||||
if (path.startsWith('/' + PRIVATE_FILE_TYPE.FILE_EXTENDED_PROPERTIES)) return;
|
||||
|
||||
// init context
|
||||
const context = new MetadataContext();
|
||||
window.sfMetadataContext = context;
|
||||
@@ -16,7 +20,13 @@ const DetailContainer = React.memo(({ repoID, path, dirent, currentRepoInfo, rep
|
||||
window.sfMetadataContext.destroy();
|
||||
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) {
|
||||
return (
|
||||
|
@@ -16,6 +16,15 @@
|
||||
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 {
|
||||
margin: 0 0.5rem 0 6px;
|
||||
line-height: 1.5rem;
|
||||
|
@@ -4,12 +4,14 @@ import Icon from '../../../icon';
|
||||
|
||||
import './index.css';
|
||||
|
||||
const Header = ({ title, icon, onClose, component = {} }) => {
|
||||
const Header = ({ title, icon, iconSize = 32, onClose, component = {} }) => {
|
||||
const { closeIcon } = component;
|
||||
return (
|
||||
<div className="detail-header">
|
||||
<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>
|
||||
</div>
|
||||
<div className="detail-control" onClick={onClose}>
|
||||
@@ -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,
|
||||
};
|
||||
|
@@ -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%;
|
||||
}
|
||||
|
3
frontend/src/metadata/components/view-details/index.css
Normal file
3
frontend/src/metadata/components/view-details/index.css
Normal file
@@ -0,0 +1,3 @@
|
||||
.sf-metadata-view-detail .detail-content-empty .empty-tip {
|
||||
margin: 0;
|
||||
}
|
39
frontend/src/metadata/components/view-details/index.js
Normal file
39
frontend/src/metadata/components/view-details/index.js
Normal 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;
|
@@ -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;
|
@@ -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 { GalleryGroupBySetter, GallerySliderSetter, FilterSetter, GroupbySetter, SortSetter, HideColumnSetter } from '../data-process-setter';
|
||||
import { EVENT_BUS_TYPE, PRIVATE_COLUMN_KEY, VIEW_TYPE } from '../../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 }) => {
|
||||
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);
|
||||
}, []);
|
||||
@@ -67,74 +59,44 @@ const ViewToolBar = ({ viewId }) => {
|
||||
if (!view) return null;
|
||||
|
||||
const viewType = view.type;
|
||||
const readOnly = !window.sfMetadataContext.canModifyView(view);
|
||||
const readOnly = window.sfMetadataContext ? !window.sfMetadataContext.canModifyView(view) : true;
|
||||
|
||||
return (
|
||||
<div
|
||||
className="sf-metadata-tool"
|
||||
onClick={onHeaderClick}
|
||||
>
|
||||
<div className="sf-metadata-tool-left-operations">
|
||||
{viewType === VIEW_TYPE.GALLERY && (
|
||||
<>
|
||||
<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"
|
||||
{viewType === VIEW_TYPE.TABLE && (
|
||||
<TableViewToolbar
|
||||
readOnly={readOnly}
|
||||
filterConjunction={view.filter_conjunction}
|
||||
basicFilters={view.basic_filters}
|
||||
filters={view.filters}
|
||||
columns={filterColumns}
|
||||
modifyFilters={modifyFilters}
|
||||
view={view}
|
||||
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}
|
||||
modifyFilters={modifyFilters}
|
||||
modifySorts={modifySorts}
|
||||
/>
|
||||
{viewType !== VIEW_TYPE.GALLERY && (
|
||||
<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}
|
||||
/>
|
||||
)}
|
||||
{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>
|
||||
{viewType === VIEW_TYPE.GALLERY && (
|
||||
<GalleryViewToolbar
|
||||
readOnly={readOnly}
|
||||
isCustomPermission={isCustomPermission}
|
||||
view={view}
|
||||
collaborators={collaborators}
|
||||
modifyFilters={modifyFilters}
|
||||
modifySorts={modifySorts}
|
||||
showDetail={showDetail}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
ViewToolBar.propTypes = {
|
||||
viewId: PropTypes.string,
|
||||
isCustomPermission: PropTypes.bool,
|
||||
switchViewMode: PropTypes.func,
|
||||
};
|
||||
|
||||
export default ViewToolBar;
|
||||
|
@@ -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;
|
@@ -124,6 +124,7 @@ export const MetadataViewProvider = ({
|
||||
store: storeRef.current,
|
||||
deleteFilesCallback: params.deleteFilesCallback,
|
||||
renameFileCallback: params.renameFileCallback,
|
||||
updateCurrentDirent: params.updateCurrentDirent,
|
||||
}}
|
||||
>
|
||||
{children}
|
||||
|
@@ -141,6 +141,7 @@ export const MetadataProvider = ({ repoID, hideMetadataView, selectMetadataView,
|
||||
parentNode: {},
|
||||
key: repoID,
|
||||
view_id: view._id,
|
||||
view_type: view.type,
|
||||
};
|
||||
selectMetadataView(node);
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
|
@@ -13,7 +13,7 @@ const GalleryMain = ({
|
||||
gap,
|
||||
mode,
|
||||
selectedImages,
|
||||
setSelectedImages,
|
||||
onImageSelect,
|
||||
onImageClick,
|
||||
onImageDoubleClick,
|
||||
onImageRightClick
|
||||
@@ -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;
|
||||
@@ -33,9 +34,7 @@ const GalleryMain = ({
|
||||
|
||||
setIsSelecting(true);
|
||||
setSelectionStart({ x: e.clientX, y: e.clientY });
|
||||
setSelectedImages([]);
|
||||
|
||||
}, [setSelectedImages]);
|
||||
}, []);
|
||||
|
||||
const handleMouseMove = useCallback((e) => {
|
||||
if (!isSelecting) return;
|
||||
@@ -70,9 +69,9 @@ const GalleryMain = ({
|
||||
});
|
||||
});
|
||||
|
||||
setSelectedImages(selected);
|
||||
onImageSelect(selected);
|
||||
});
|
||||
}, [groups, isSelecting, selectionStart, setSelectedImages]);
|
||||
}, [groups, isSelecting, selectionStart, onImageSelect]);
|
||||
|
||||
const handleMouseUp = useCallback((e) => {
|
||||
if (e.button !== 0) return;
|
||||
@@ -113,7 +112,6 @@ const GalleryMain = ({
|
||||
key={name}
|
||||
className="metadata-gallery-date-group"
|
||||
style={{ height, paddingTop }}
|
||||
|
||||
>
|
||||
{mode !== GALLERY_DATE_MODE.ALL && childrenStartIndex === 0 && (
|
||||
<div className="metadata-gallery-date-tag">{name || gettext('Empty')}</div>
|
||||
@@ -129,7 +127,7 @@ const GalleryMain = ({
|
||||
>
|
||||
{children.slice(childrenStartIndex, childrenEndIndex + 1).map((row) => {
|
||||
return row.children.map((img) => {
|
||||
const isSelected = selectedImages.includes(img);
|
||||
const isSelected = selectedImageIds.includes(img.id);
|
||||
return (
|
||||
<div
|
||||
key={img.src}
|
||||
@@ -151,7 +149,7 @@ const GalleryMain = ({
|
||||
</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) {
|
||||
return <EmptyTip text={gettext('No record')}/>;
|
||||
@@ -195,6 +193,7 @@ GalleryMain.propTypes = {
|
||||
gap: PropTypes.number.isRequired,
|
||||
mode: PropTypes.string,
|
||||
selectedImages: PropTypes.array.isRequired,
|
||||
onImageSelect: PropTypes.func.isRequired,
|
||||
onImageClick: PropTypes.func.isRequired,
|
||||
onImageDoubleClick: PropTypes.func.isRequired,
|
||||
onImageRightClick: PropTypes.func.isRequired,
|
||||
|
@@ -1,5 +1,5 @@
|
||||
.sf-metadata-gallery-container {
|
||||
height: calc(100vh - 100px);
|
||||
height: 100%;
|
||||
padding: 0 16px;
|
||||
position: relative;
|
||||
display: flex;
|
||||
|
@@ -13,6 +13,8 @@ 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 { getRowById } from '../../utils/table';
|
||||
import { getEventClassName } from '../../utils/common';
|
||||
|
||||
import './index.css';
|
||||
|
||||
@@ -31,9 +33,14 @@ const Gallery = () => {
|
||||
const containerRef = useRef(null);
|
||||
const renderMoreTimer = useRef(null);
|
||||
|
||||
const { metadata, store } = useMetadataView();
|
||||
const { metadata, store, updateCurrentDirent } = useMetadataView();
|
||||
const repoID = window.sfMetadataContext.getSetting('repoID');
|
||||
|
||||
useEffect(() => {
|
||||
updateCurrentDirent();
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, []);
|
||||
|
||||
// Number of images per row
|
||||
const columns = useMemo(() => {
|
||||
return 8 - zoomGear;
|
||||
@@ -206,21 +213,38 @@ 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;
|
||||
}
|
||||
updateCurrentDirent({
|
||||
type: 'file',
|
||||
name: image.name,
|
||||
path: image.path,
|
||||
file_tags: []
|
||||
});
|
||||
}, [metadata, 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 {
|
||||
setSelectedImages([image]);
|
||||
updateSelectedImage(image);
|
||||
}
|
||||
}, [imageItems, selectedImages]);
|
||||
}, [imageItems, selectedImages, updateSelectedImage]);
|
||||
|
||||
const handleDoubleClick = useCallback((event, image) => {
|
||||
const index = imageItems.findIndex(item => item.id === image.id);
|
||||
@@ -246,6 +270,10 @@ const Gallery = () => {
|
||||
setImageIndex((prevState) => (prevState + 1) % imageItemsLength);
|
||||
};
|
||||
|
||||
const handleImageSelection = useCallback((selectedImages) => {
|
||||
setSelectedImages(selectedImages);
|
||||
}, []);
|
||||
|
||||
const closeImagePopup = () => {
|
||||
setIsImagePopupOpen(false);
|
||||
};
|
||||
@@ -308,8 +336,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 (
|
||||
<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} >
|
||||
{!isFirstLoading && (
|
||||
<>
|
||||
@@ -321,7 +359,7 @@ const Gallery = () => {
|
||||
gap={GALLERY_IMAGE_GAP}
|
||||
mode={mode}
|
||||
selectedImages={selectedImages}
|
||||
setSelectedImages={setSelectedImages}
|
||||
onImageSelect={handleImageSelection}
|
||||
onImageClick={handleClick}
|
||||
onImageDoubleClick={handleDoubleClick}
|
||||
onImageRightClick={handleRightClick}
|
||||
|
@@ -22,12 +22,13 @@ 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 } 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 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 }, () => {
|
||||
showDirentDetail = () => {
|
||||
this.setState({ isDirentDetailShow: true });
|
||||
});
|
||||
} else {
|
||||
this.setState({
|
||||
direntDetailPanelTab: '',
|
||||
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() {
|
||||
@@ -538,14 +527,14 @@ class LibContentView extends React.Component {
|
||||
window.history.pushState({ url: url, path: filePath }, filePath, url);
|
||||
};
|
||||
|
||||
showFileMetadata = (filePath, viewId) => {
|
||||
showFileMetadata = (filePath, viewId, viewType) => {
|
||||
const repoID = this.props.repoID;
|
||||
const repoInfo = this.state.currentRepoInfo;
|
||||
this.setState({
|
||||
currentMode: METADATA_MODE,
|
||||
path: filePath,
|
||||
viewId: viewId,
|
||||
isDirentDetailShow: false
|
||||
isDirentDetailShow: viewType === VIEW_TYPE.GALLERY ? this.state.isDirentDetailShow : false,
|
||||
}, () => {
|
||||
setTimeout(() => {
|
||||
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) {
|
||||
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');
|
||||
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) {
|
||||
@@ -2428,6 +2417,7 @@ class LibContentView extends React.Component {
|
||||
getMarkDownFilePath={this.getMarkDownFilePath}
|
||||
getMarkDownFileName={this.getMarkDownFileName}
|
||||
openMarkdownFile={this.openMarkdownFile}
|
||||
updateCurrentDirent={this.updateCurrentDirent}
|
||||
/>
|
||||
:
|
||||
<div className="message err-tip">{gettext('Folder does not exist.')}</div>
|
||||
|
Reference in New Issue
Block a user