1
0
mirror of https://github.com/haiwen/seahub.git synced 2025-09-14 06:11:16 +00:00

feat: metadata gallery view (#6727)

* feat: metadata gallery view

* feat: update code

* feat: update code

* feat: update code

---------

Co-authored-by: 杨国璇 <ygx@192.168.1.4>
This commit is contained in:
杨国璇
2024-09-08 12:16:33 +08:00
committed by GitHub
parent 4c295bc38c
commit 4ccd0f3477
19 changed files with 412 additions and 153 deletions

View File

@@ -1,6 +1,7 @@
import axios from 'axios';
import cookie from 'react-cookies';
import { siteRoot } from '../utils/constants';
import { VIEW_TYPE_DEFAULT_BASIC_FILTER, VIEW_TYPE_DEFAULT_SORTS } from './metadata-view/_basic';
class MetadataManagerAPI {
init({ server, username, password, token }) {
@@ -114,7 +115,14 @@ class MetadataManagerAPI {
addView = (repoID, name, type = 'table') => {
const url = this.server + '/api/v2.1/repos/' + repoID + '/metadata/views/';
const params = { name, type };
let params = {
name,
type,
data: {
basic_filters: VIEW_TYPE_DEFAULT_BASIC_FILTER[type],
sorts: VIEW_TYPE_DEFAULT_SORTS[type],
}
};
return this._sendPostRequest(url, params, { headers: { 'Content-type': 'application/json' } });
};

View File

@@ -19,12 +19,27 @@ const SORT_COLUMN_OPTIONS = [
CellType.RATE,
];
const GALLERY_SORT_COLUMN_OPTIONS = [
CellType.CTIME,
CellType.MTIME,
CellType.RATE,
CellType.NUMBER,
CellType.FILE_NAME,
];
const GALLERY_FIRST_SORT_COLUMN_OPTIONS = [
CellType.CTIME,
CellType.MTIME,
];
const TEXT_SORTER_COLUMN_TYPES = [CellType.TEXT];
const NUMBER_SORTER_COLUMN_TYPES = [CellType.NUMBER, CellType.RATE];
export {
SORT_TYPE,
SORT_COLUMN_OPTIONS,
GALLERY_SORT_COLUMN_OPTIONS,
GALLERY_FIRST_SORT_COLUMN_OPTIONS,
TEXT_SORTER_COLUMN_TYPES,
NUMBER_SORTER_COLUMN_TYPES,
};

View File

@@ -1,3 +1,7 @@
import { PRIVATE_COLUMN_KEY } from './column';
import { FILTER_PREDICATE_TYPE } from './filter';
import { SORT_COLUMN_OPTIONS, GALLERY_SORT_COLUMN_OPTIONS, GALLERY_FIRST_SORT_COLUMN_OPTIONS, SORT_TYPE } from './sort';
export const VIEW_TYPE = {
TABLE: 'table',
GALLERY: 'gallery'
@@ -8,3 +12,23 @@ export const VIEW_TYPE_ICON = {
[VIEW_TYPE.GALLERY]: 'image',
'image': 'image'
};
export const VIEW_TYPE_DEFAULT_BASIC_FILTER = {
[VIEW_TYPE.TABLE]: [{ column_key: PRIVATE_COLUMN_KEY.IS_DIR, filter_predicate: FILTER_PREDICATE_TYPE.IS, filter_term: 'file' }],
[VIEW_TYPE.GALLERY]: [{ column_key: PRIVATE_COLUMN_KEY.FILE_TYPE, filter_predicate: FILTER_PREDICATE_TYPE.IS, filter_term: 'picture' }],
};
export const VIEW_TYPE_DEFAULT_SORTS = {
[VIEW_TYPE.TABLE]: [],
[VIEW_TYPE.GALLERY]: [{ column_key: PRIVATE_COLUMN_KEY.FILE_CTIME, sort_type: SORT_TYPE.DOWN }],
};
export const VIEW_SORT_COLUMN_OPTIONS = {
[VIEW_TYPE.TABLE]: SORT_COLUMN_OPTIONS,
[VIEW_TYPE.GALLERY]: GALLERY_SORT_COLUMN_OPTIONS,
};
export const VIEW_FIRST_SORT_COLUMN_OPTIONS = {
[VIEW_TYPE.TABLE]: SORT_COLUMN_OPTIONS,
[VIEW_TYPE.GALLERY]: GALLERY_FIRST_SORT_COLUMN_OPTIONS,
};

View File

@@ -1,35 +1,32 @@
import React, { useState, useCallback, useEffect } from 'react';
import React, { useState, useCallback } from 'react';
import { Button, Input } from 'reactstrap';
import { EVENT_BUS_TYPE } from '../../constants';
import Icon from '../../../../components/icon';
import './slider-setter.css';
const SliderSetter = () => {
const [sliderValue, setSliderValue] = useState(() => {
const savedValue = localStorage.getItem('sliderValue');
return savedValue !== null ? parseInt(savedValue, 10) : 0;
const savedValue = window.sfMetadataContext.localStorage.getItem('zoom-gear', 0);
return savedValue || 0;
});
useEffect(() => {
localStorage.setItem('sliderValue', sliderValue);
}, [sliderValue]);
const handleGalleryColumnsChange = useCallback((e) => {
const adjust = parseInt(e.target.value, 10);
setSliderValue(adjust);
window.sfMetadataContext.eventBus.dispatch(EVENT_BUS_TYPE.MODIFY_GALLERY_COLUMNS, adjust);
window.sfMetadataContext.eventBus.dispatch(EVENT_BUS_TYPE.MODIFY_GALLERY_ZOOM_GEAR, adjust);
}, []);
const handleImageExpand = useCallback(() => {
const adjust = Math.min(sliderValue + 1, 2);
setSliderValue(adjust);
window.sfMetadataContext.eventBus.dispatch(EVENT_BUS_TYPE.MODIFY_GALLERY_COLUMNS, adjust);
window.sfMetadataContext.eventBus.dispatch(EVENT_BUS_TYPE.MODIFY_GALLERY_ZOOM_GEAR, adjust);
}, [sliderValue]);
const handleImageShrink = useCallback(() => {
const adjust = Math.max(sliderValue - 1, -2);
setSliderValue(adjust);
window.sfMetadataContext.eventBus.dispatch(EVENT_BUS_TYPE.MODIFY_GALLERY_COLUMNS, adjust);
window.sfMetadataContext.eventBus.dispatch(EVENT_BUS_TYPE.MODIFY_GALLERY_ZOOM_GEAR, adjust);
}, [sliderValue]);
return (

View File

@@ -6,7 +6,7 @@ import { getValidSorts, CommonlyUsedHotkey } from '../../_basic';
import { gettext } from '../../utils';
import { SortPopover } from '../popover';
const SortSetter = ({ target, sorts: propsSorts, readOnly, columns, isNeedSubmit, wrapperClass, modifySorts }) => {
const SortSetter = ({ target, type, sorts: propsSorts, readOnly, columns, isNeedSubmit, wrapperClass, modifySorts }) => {
const [isShowSetter, setShowSetter] = useState(false);
const sorts = useMemo(() => {
@@ -54,6 +54,7 @@ const SortSetter = ({ target, sorts: propsSorts, readOnly, columns, isNeedSubmit
<SortPopover
isNeedSubmit={isNeedSubmit}
readOnly={readOnly}
type={type}
target={target}
columns={columns}
sorts={sorts}
@@ -68,10 +69,11 @@ const SortSetter = ({ target, sorts: propsSorts, readOnly, columns, isNeedSubmit
const propTypes = {
isNeedSubmit: PropTypes.bool,
readOnly: PropTypes.bool,
wrapperClass: PropTypes.string,
target: PropTypes.string,
isNeedSubmit: PropTypes.bool,
type: PropTypes.string,
sorts: PropTypes.array,
columns: PropTypes.array,
modifySorts: PropTypes.func,

View File

@@ -0,0 +1,69 @@
import React, { useCallback, useMemo } from 'react';
import PropTypes from 'prop-types';
import { gettext } from '../../../../utils';
import { CustomizeSelect, Icon } from '@seafile/sf-metadata-ui-component';
const OPTIONS = [
{ value: 'picture', name: gettext('Only pictures') },
{ value: 'video', name: gettext('Only videos') },
{ value: 'all', name: gettext('Pictures and videos') },
];
const FileTypeFilter = ({ readOnly, value = 'picture', onChange: onChangeAPI }) => {
const options = useMemo(() => {
return OPTIONS.map(o => {
const { name } = o;
return {
value: o.value,
label: (
<div className="select-basic-filter-option">
<div className="select-basic-filter-option-name" title={name} aria-label={name}>{name}</div>
<div className="select-basic-filter-option-check-icon">
{value === o.value && (<Icon iconName="check-mark" />)}
</div>
</div>
)
};
});
}, [value]);
const displayValue = useMemo(() => {
const selectedOption = OPTIONS.find(o => o.value === value) || OPTIONS[2];
return {
label: (
<div>
{selectedOption.name}
</div>
)
};
}, [value]);
const onChange = useCallback((newValue) => {
if (newValue === value) return;
onChangeAPI(newValue);
}, [value, onChangeAPI]);
return (
<CustomizeSelect
readOnly={readOnly}
className="sf-metadata-basic-filters-select"
value={displayValue}
options={options}
onSelectOption={onChange}
component={{
DropDownIcon: (
<i className="sf3-font sf3-font-down"></i>
)
}}
/>
);
};
FileTypeFilter.propTypes = {
readOnly: PropTypes.bool,
value: PropTypes.string,
onChange: PropTypes.func,
};
export default FileTypeFilter;

View File

@@ -55,3 +55,7 @@
.sf-metadata-basic-filters-select .sf-metadata-option-group {
margin-left: 6px;
}
.filter-group-basic .sf-metadata-filters-list {
min-height: unset;
}

View File

@@ -4,6 +4,7 @@ import { FormGroup, Label } from 'reactstrap';
import { gettext } from '../../../../utils';
import { PRIVATE_COLUMN_KEY } from '../../../../_basic';
import FileOrFolderFilter from './file-folder-filter';
import FileTypeFilter from './file-type-filter';
import './index.css';
@@ -17,27 +18,43 @@ const BasicFilters = ({ readOnly, filters = [], onChange }) => {
onChange(newFilters);
}, [filters, onChange]);
const onChangeFileTypeFilter = useCallback((newValue) => {
const filterIndex = filters.findIndex(filter => filter.column_key === PRIVATE_COLUMN_KEY.FILE_TYPE);
const filter = filters[filterIndex];
const newFilters = filters.slice(0);
newFilters[filterIndex] = { ...filter, filter_term: newValue };
onChange(newFilters);
}, [filters, onChange]);
return (
<FormGroup className="filter-group p-4">
<FormGroup className="filter-group-basic filter-group p-4">
<Label className="filter-group-name">{gettext('Basic')}</Label>
<div className="filter-group-container">
{filters.map(filter => {
<div className="sf-metadata-filters-list">
{filters.map((filter, index) => {
const { column_key, filter_term } = filter;
if (column_key === PRIVATE_COLUMN_KEY.IS_DIR) {
return (
<FileOrFolderFilter key={column_key} readOnly={readOnly} value={filter_term} onChange={onChangeFileOrFolderFilter} />
);
}
if (column_key === PRIVATE_COLUMN_KEY.FILE_TYPE) {
return (
<FileTypeFilter key={column_key} readOnly={readOnly} value={filter_term} onChange={onChangeFileTypeFilter} />
);
}
return null;
})}
</div>
</div>
</FormGroup>
);
};
BasicFilters.propTypes = {
readOnly: PropTypes.bool,
value: PropTypes.string,
filters: PropTypes.array,
columns: PropTypes.array,
onChange: PropTypes.func,
};

View File

@@ -5,8 +5,10 @@ import { Button, UncontrolledPopover } from 'reactstrap';
import { CustomizeAddTool, CustomizeSelect, Icon } from '@seafile/sf-metadata-ui-component';
import {
COLUMNS_ICON_CONFIG,
SORT_COLUMN_OPTIONS,
VIEW_SORT_COLUMN_OPTIONS,
VIEW_FIRST_SORT_COLUMN_OPTIONS,
SORT_TYPE,
VIEW_TYPE,
getColumnByKey,
} from '../../../_basic';
import { execSortsOperation, getDisplaySorts, isSortsEmpty, SORT_OPERATION } from './utils';
@@ -28,8 +30,9 @@ const SORT_TYPES = [
const propTypes = {
readOnly: PropTypes.bool,
target: PropTypes.string.isRequired,
isNeedSubmit: PropTypes.bool,
target: PropTypes.string.isRequired,
type: PropTypes.string,
sorts: PropTypes.array,
columns: PropTypes.array.isRequired,
onSortComponentToggle: PropTypes.func,
@@ -44,8 +47,10 @@ class SortPopover extends Component {
constructor(props) {
super(props);
const { sorts, columns } = this.props;
const { sorts, columns, type } = this.props;
this.sortTypeOptions = this.createSortTypeOptions();
this.supportFirstSortColumnOptions = VIEW_FIRST_SORT_COLUMN_OPTIONS[type || VIEW_TYPE.TABLE];
this.supportSortColumnOptions = VIEW_SORT_COLUMN_OPTIONS[type || VIEW_TYPE.TABLE];
this.columnsOptions = this.createColumnsOptions(columns);
this.state = {
sorts: getDisplaySorts(sorts, columns),
@@ -154,7 +159,7 @@ class SortPopover extends Component {
};
createColumnsOptions = (columns = []) => {
const sortableColumns = columns.filter(column => SORT_COLUMN_OPTIONS.includes(column.type));
const sortableColumns = columns.filter(column => this.supportSortColumnOptions.includes(column.type));
return sortableColumns.map((column) => {
const { type, name } = column;
return {
@@ -189,7 +194,7 @@ class SortPopover extends Component {
renderSortItem = (column, sort, index) => {
const { name, type } = column;
const { readOnly } = this.props;
const { readOnly, type: viewType } = this.props;
const selectedColumn = {
label: (
<Fragment>
@@ -205,11 +210,16 @@ class SortPopover extends Component {
label: <span className="select-option-name">{selectedTypeOption?.name || gettext('Up')}</span>
};
let columnsOptions = this.columnsOptions;
if (index === 0) {
columnsOptions = columnsOptions.filter(o => this.supportFirstSortColumnOptions.includes(o.value.column.type));
}
return (
<div key={'sort-item-' + index} className="sort-item">
{!readOnly &&
<div className="delete-sort" onClick={(event) => this.deleteSort(event, index)}>
<Icon iconName="fork-number"/>
<div className="delete-sort" onClick={!(viewType === VIEW_TYPE.GALLERY && index === 0) ? () => {} : (event) => this.deleteSort(event, index)}>
{!(viewType === VIEW_TYPE.GALLERY && index === 0) && <Icon iconName="fork-number"/>}
</div>
}
<div className="condition">
@@ -218,7 +228,7 @@ class SortPopover extends Component {
readOnly={readOnly}
value={selectedColumn}
onSelectOption={(value) => this.onSelectColumn(value, index)}
options={this.columnsOptions}
options={columnsOptions}
searchable={true}
searchPlaceholder={gettext('Search property')}
noOptionsPlaceholder={gettext('No results')}

View File

@@ -63,6 +63,7 @@ const ViewToolBar = ({ viewId }) => {
if (!view) return null;
const viewType = view.type;
const readOnly = !window.sfMetadataContext.canModifyView(view);
return (
@@ -91,10 +92,11 @@ const ViewToolBar = ({ viewId }) => {
target="sf-metadata-sort-popover"
readOnly={readOnly}
sorts={view.sorts}
type={viewType}
columns={viewColumns}
modifySorts={modifySorts}
/>
{view.type !== VIEW_TYPE.GALLERY && (
{viewType !== VIEW_TYPE.GALLERY && (
<GroupbySetter
isNeedSubmit={true}
wrapperClass="sf-metadata-view-tool-operation-btn sf-metadata-view-tool-groupby"
@@ -105,7 +107,7 @@ const ViewToolBar = ({ viewId }) => {
modifyGroupbys={modifyGroupbys}
/>
)}
{view.type !== VIEW_TYPE.GALLERY && (
{viewType !== VIEW_TYPE.GALLERY && (
<HideColumnSetter
wrapperClass="sf-metadata-view-tool-operation-btn sf-metadata-view-tool-hide-column"
target="sf-metadata-hide-column-popover"

View File

@@ -1,4 +1,4 @@
.metadata-gallery-container {
.sf-metadata-gallery-container {
height: calc(100vh - 100px);
margin: 2px;
position: relative;
@@ -61,3 +61,12 @@
.metadata-gallery-grid-image:hover {
transform: scale(1.05);
}
.sf-metadata-gallery-loading-more {
height: 30px;
width: 100%;
display: flex;
align-items: center;
justify-content: center;
flex-shrink: 0;
}

View File

@@ -1,116 +1,107 @@
import React, { useMemo, useState, useEffect, useRef, useCallback } from 'react';
import { CenteredLoading } from '@seafile/sf-metadata-ui-component';
import { useMetadata } from '../../../hooks';
import { Utils } from '../../../../../utils/utils';
import { PRIVATE_COLUMN_KEY } from '../../../_basic';
import { getDateDisplayString, PRIVATE_COLUMN_KEY } from '../../../_basic';
import { siteRoot, thumbnailSizeForGrid } from '../../../../../utils/constants';
import { EVENT_BUS_TYPE } from '../../../constants';
import { EVENT_BUS_TYPE, PER_LOAD_NUMBER } from '../../../constants';
import Main from './main';
import toaster from '../../../../../components/toast';
import './index.css';
const BATCH_SIZE = 100;
const CONCURRENCY_LIMIT = 3;
const IMAGE_GAP = 2;
const Gallery = () => {
const [imageWidth, setImageWidth] = useState(100);
const [columns, setColumns] = useState(8);
const [containerWidth, setContainerWidth] = useState(960);
const [adjustValue, setAdjustValue] = useState(() => {
try {
const savedValue = localStorage.getItem('sliderValue');
return savedValue !== null ? Number(savedValue) : 0;
} catch (error) {
return 0;
}
});
const [visibleItems, setVisibleItems] = useState(BATCH_SIZE);
const [loadingQueue, setLoadingQueue] = useState([]);
const [concurrentLoads, setConcurrentLoads] = useState(0);
const imageRefs = useRef([]);
const containerRef = useRef(null);
const [isFirstLoading, setFirstLoading] = useState(true);
const [isLoadingMore, setLoadingMore] = useState(false);
const [zoomGear, setZoomGear] = useState(0);
const [containerWidth, setContainerWidth] = useState(0);
const [loadingQueue, setLoadingQueue] = useState([]);
const [concurrentLoads, setConcurrentLoads] = useState(0);
const [overScan, setOverScan] = useState({ top: 0, bottom: 0 });
const renderMoreTimer = useRef(null);
const { metadata } = useMetadata();
const { metadata, store } = useMetadata();
const repoID = window.sfMetadataContext.getSetting('repoID');
useEffect(() => {
const handleResize = () => {
if (containerRef.current) {
setContainerWidth(containerRef.current.offsetWidth);
}
};
// Number of images per row
const columns = useMemo(() => {
return 8 - zoomGear;
}, [zoomGear]);
const resizeObserver = new ResizeObserver(handleResize);
const currentContainer = containerRef.current;
const imageSize = useMemo(() => {
return (containerWidth - columns * 2 - 2) / columns;
}, [containerWidth, columns]);
if (currentContainer) {
resizeObserver.observe(currentContainer);
}
return () => {
if (currentContainer) {
resizeObserver.unobserve(currentContainer);
}
};
}, []);
useEffect(() => {
window.sfMetadataContext.eventBus.subscribe(EVENT_BUS_TYPE.MODIFY_GALLERY_COLUMNS, (adjust) => {
setAdjustValue(adjust);
});
}, [columns]);
useEffect(() => {
const columns = (Utils.isDesktop() ? 8 : 4) - adjustValue;
const adjustedImageWidth = (containerWidth - columns * 2 - 2) / columns;
setColumns(columns);
setImageWidth(adjustedImageWidth);
}, [containerWidth, adjustValue]);
const imageItems = useMemo(() => {
return metadata.rows
.filter(row => Utils.imageCheck(row[PRIVATE_COLUMN_KEY.FILE_NAME]))
.map(item => {
const fileName = item[PRIVATE_COLUMN_KEY.FILE_NAME];
const parentDir = item[PRIVATE_COLUMN_KEY.PARENT_DIR];
const groups = useMemo(() => {
if (isFirstLoading) return [];
const firstSort = metadata.view.sorts[0];
let init = metadata.rows.reduce((_init, record) => {
const fileName = record[PRIVATE_COLUMN_KEY.FILE_NAME];
const parentDir = record[PRIVATE_COLUMN_KEY.PARENT_DIR];
const path = Utils.encodePath(Utils.joinPath(parentDir, fileName));
const date = item[PRIVATE_COLUMN_KEY.FILE_CTIME].split('T')[0];
const src = `${siteRoot}thumbnail/${repoID}/${thumbnailSizeForGrid}${path}`;
return {
const date = getDateDisplayString(record[firstSort.column_key], 'YYYY-MM-DD');
const img = {
name: fileName,
url: `${siteRoot}lib/${repoID}/file${path}`,
src: src,
src: `${siteRoot}thumbnail/${repoID}/${thumbnailSizeForGrid}${path}`,
date: date,
};
let _group = _init.find(g => g.name === date);
if (_group) {
_group.children.push(img);
} else {
_init.push({
name: date,
children: [img],
});
}, [metadata, repoID]);
}
return _init;
}, []);
const groupedImages = useMemo(() => {
return imageItems.reduce((acc, item) => {
if (!acc[item.date]) {
acc[item.date] = [];
let _groups = [];
init.forEach((_init, index) => {
const { children } = _init;
const childrenCount = children.length;
const value = childrenCount / columns;
const rows = childrenCount % columns ? Math.ceil(value) : ~~(value);
const height = rows * (imageSize + IMAGE_GAP);
let top = 0;
if (index > 0) {
const lastGroup = _groups[index - 1];
const { top: lastGroupTop, height: lastGroupHeight } = lastGroup;
top = lastGroupTop + lastGroupHeight;
}
acc[item.date].push(item);
return acc;
}, {});
}, [imageItems]);
_groups.push({
..._init,
top,
height,
});
});
return _groups;
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [isFirstLoading, metadata, metadata.recordsCount, repoID, columns, imageSize]);
const handleScroll = useCallback(() => {
if (visibleItems >= imageItems.length) return;
if (containerRef.current) {
const { scrollTop, scrollHeight, clientHeight } = containerRef.current;
if (scrollTop + clientHeight >= scrollHeight - 10) {
setVisibleItems(prev => Math.min(prev + BATCH_SIZE, imageItems.length));
}
}
}, [visibleItems, imageItems.length]);
const loadMore = useCallback(async () => {
if (isLoadingMore) return;
if (!metadata.hasMore) return;
setLoadingMore(true);
useEffect(() => {
const container = containerRef.current;
if (container) {
container.addEventListener('scroll', handleScroll);
return () => container.removeEventListener('scroll', handleScroll);
try {
await store.loadMore(PER_LOAD_NUMBER);
setLoadingMore(false);
} catch (error) {
const errorMsg = Utils.getErrorMsg(error);
toaster.danger(errorMsg);
setLoadingMore(false);
return;
}
}, [handleScroll]);
}, [isLoadingMore, metadata, store]);
const loadNextImage = useCallback(() => {
if (loadingQueue.length === 0 || concurrentLoads >= CONCURRENCY_LIMIT) return;
@@ -142,16 +133,65 @@ const Gallery = () => {
}, [loadingQueue, concurrentLoads, loadNextImage]);
useEffect(() => {
const gear = window.sfMetadataContext.localStorage.getItem('zoom-gear', 0) || 0;
setZoomGear(gear);
const container = containerRef.current;
if (container) {
const { offsetWidth, clientHeight } = container;
setContainerWidth(offsetWidth);
// Calculate initial overScan information
const columns = 8 - gear;
const imageSize = (offsetWidth - columns * 2 - 2) / columns;
setOverScan({ top: 0, bottom: clientHeight + (imageSize + IMAGE_GAP) * 2 });
}
setFirstLoading(false);
// resize
const handleResize = () => {
if (!containerRef.current) return;
setContainerWidth(containerRef.current.offsetWidth);
};
const resizeObserver = new ResizeObserver(handleResize);
container && resizeObserver.observe(container);
// op
const modifyGalleryZoomGearSubscribe = window.sfMetadataContext.eventBus.subscribe(EVENT_BUS_TYPE.MODIFY_GALLERY_ZOOM_GEAR, (zoomGear) => {
window.sfMetadataContext.localStorage.setItem('zoom-gear', zoomGear);
setZoomGear(zoomGear);
});
return () => {
container && resizeObserver.unobserve(container);
modifyGalleryZoomGearSubscribe();
// Cleanup image references on unmount
imageRefs.current.forEach(img => {
img.onload = null;
img.onerror = null;
});
imageRefs.current = [];
renderMoreTimer.current && clearTimeout(renderMoreTimer.current);
};
}, []);
const handleScroll = useCallback(() => {
if (!containerRef.current) return;
const { scrollTop, scrollHeight, clientHeight } = containerRef.current;
if (scrollTop + clientHeight >= scrollHeight - 10) {
loadMore();
} else {
renderMoreTimer.current && clearTimeout(renderMoreTimer.current);
renderMoreTimer.current = setTimeout(() => {
const { scrollTop, clientHeight } = containerRef.current;
const overScanTop = Math.max(0, scrollTop - (imageSize + IMAGE_GAP) * 3);
const overScanBottom = scrollTop + clientHeight + (imageSize + IMAGE_GAP) * 3;
setOverScan({ top: overScanTop, bottom: overScanBottom });
renderMoreTimer.current = null;
}, 200);
}
}, [imageSize, loadMore, renderMoreTimer]);
const addToQueue = (image) => {
setLoadingQueue(prev => [...prev, image]);
loadNextImage();
@@ -159,24 +199,13 @@ const Gallery = () => {
return (
<div className="sf-metadata-container">
<div ref={containerRef} className="metadata-gallery-container">
{Object.keys(groupedImages).map(date => (
<div key={date} className="metadata-gallery-date-group">
<div className="metadata-gallery-date-tag">{date}</div>
<ul className="metadata-gallery-image-list" style={{ gridTemplateColumns: `repeat(${columns}, 1fr)` }}>
{groupedImages[date].slice(0, visibleItems).map((img, index) => (
<li key={index} tabIndex={index} className='metadata-gallery-image-item' style={{ width: imageWidth, height: imageWidth }}>
<img
className="metadata-gallery-grid-image"
src={img.src}
alt={img.name}
onLoad={() => addToQueue(img)}
/>
</li>
))}
</ul>
</div>
))}
<div className="sf-metadata-gallery-container" ref={containerRef} onScroll={handleScroll} >
{!isFirstLoading && (
<>
<Main groups={groups} size={imageSize} onLoad={addToQueue} columns={columns} overScan={overScan} gap={IMAGE_GAP} />
{isLoadingMore && (<div className="sf-metadata-gallery-loading-more"><CenteredLoading /></div>)}
</>
)}
</div>
</div>
);

View File

@@ -0,0 +1,65 @@
import React, { useCallback } from 'react';
import PropTypes from 'prop-types';
const Main = ({ groups, overScan, columns, onLoad, size, gap }) => {
const renderDisplayGroup = useCallback((group) => {
const { top: overScanTop, bottom: overScanBottom } = overScan;
const { name, children, top, height } = group;
let childrenStartIndex = children.findIndex((r, i) => {
const rTop = ~~(i / columns) * (size + 2) + top;
return rTop >= overScanTop;
});
childrenStartIndex = Math.max(childrenStartIndex, 0);
let childrenEndIndex = children.findIndex((r, i) => {
const rTop = ~~(i / columns) * (size + gap) + top;
return rTop >= overScanBottom;
});
if (childrenEndIndex > -1 && childrenEndIndex !== 0) {
childrenEndIndex = childrenEndIndex - 1;
}
if (childrenEndIndex === -1) {
childrenEndIndex = children.length - 1;
}
return (
<div key={name} className="metadata-gallery-date-group w-100" style={{ height }}>
{childrenStartIndex === 0 && (<div className="metadata-gallery-date-tag">{name}</div>)}
<div
className="metadata-gallery-image-list"
style={{
gridTemplateColumns: `repeat(${columns}, 1fr)`,
paddingTop: ~~(childrenStartIndex / columns) * (size + gap),
paddingBottom: ~~((children.length - 1 - childrenEndIndex) / columns) * (size + gap),
}}
>
{children.slice(childrenStartIndex, childrenEndIndex).map((img) => (
<div key={img.src} tabIndex={1} className='metadata-gallery-image-item' style={{ width: size, height: size }}>
<img
className="metadata-gallery-grid-image"
src={img.src}
alt={img.name}
onLoad={() => onLoad(img)}
/>
</div>
))}
</div>
</div>
);
}, [overScan, columns, onLoad, size, gap]);
if (!Array.isArray(groups) || groups.length === 0) return null;
return groups.map((group, index) => {
return renderDisplayGroup(group, index);
});
};
Main.propTypes = {
groups: PropTypes.array,
overScan: PropTypes.object,
columns: PropTypes.number,
onLoad: PropTypes.func,
size: PropTypes.number,
};
export default Main;

View File

@@ -51,5 +51,5 @@ export const EVENT_BUS_TYPE = {
ERROR: 'error',
// gallery
MODIFY_GALLERY_COLUMNS: 'modify_gallery_columns',
MODIFY_GALLERY_ZOOM_GEAR: 'modify_gallery_zoom_gear',
};

View File

@@ -17,7 +17,7 @@ export const MetadataProvider = ({
...params
}) => {
const [isLoading, setLoading] = useState(true);
const [metadata, setMetadata] = useState({ rows: [], columns: [] });
const [metadata, setMetadata] = useState({ rows: [], columns: [], view: {} });
const storeRef = useRef(null);
const { collaborators } = useCollaborators();
const { showFirstView, setShowFirstView } = usePropsMetadata();

View File

@@ -1,4 +1,4 @@
import { getColumnByKey, VIEW_NOT_DISPLAY_COLUMN_KEYS, PRIVATE_COLUMN_KEY, FILTER_PREDICATE_TYPE, VIEW_TYPE } from '../../_basic';
import { getColumnByKey, VIEW_NOT_DISPLAY_COLUMN_KEYS, VIEW_TYPE_DEFAULT_BASIC_FILTER, VIEW_TYPE } from '../../_basic';
class View {
constructor(object, columns) {
@@ -14,7 +14,7 @@ class View {
this.filters = object.filters || [];
this.filter_conjunction = object.filter_conjunction || 'Or';
this.basic_filters = object.basic_filters && object.basic_filters.length > 0 ? object.basic_filters : [{ column_key: PRIVATE_COLUMN_KEY.IS_DIR, filter_predicate: FILTER_PREDICATE_TYPE.IS, filter_term: 'all' }];
this.basic_filters = object.basic_filters && object.basic_filters.length > 0 ? object.basic_filters : VIEW_TYPE_DEFAULT_BASIC_FILTER[this.type];
// sort
this.sorts = object.sorts || [];

View File

@@ -550,6 +550,8 @@ class LibContentView extends React.Component {
content: '',
viewId: '',
isDirentDetailShow: false
}, () => {
this.showDir('/');
});
};

View File

@@ -101,7 +101,7 @@ class MetadataManage(APIView):
task_id = add_init_metadata_task(params=params)
metadata_view = RepoMetadataViews.objects.filter(repo_id=repo_id).first()
if not metadata_view:
RepoMetadataViews.objects.add_view(repo_id, 'All files')
RepoMetadataViews.objects.add_view(repo_id, 'All files', 'table')
except Exception as e:
logger.error(e)
return api_error(status.HTTP_500_INTERNAL_SERVER_ERROR, 'Internal Server Error')
@@ -576,6 +576,7 @@ class MetadataViews(APIView):
# Add a metadata view
view_name = request.data.get('name')
view_type = request.data.get('type', 'table')
view_data = request.data.get('data', {})
if not view_name:
error_msg = 'view name is invalid.'
return api_error(status.HTTP_400_BAD_REQUEST, error_msg)
@@ -596,7 +597,7 @@ class MetadataViews(APIView):
return api_error(status.HTTP_403_FORBIDDEN, error_msg)
try:
new_view = RepoMetadataViews.objects.add_view(repo_id, view_name, view_type)
new_view = RepoMetadataViews.objects.add_view(repo_id, view_name, view_type, view_data)
except Exception as e:
logger.exception(e)
error_msg = 'Internal Server Error'

View File

@@ -62,9 +62,10 @@ class RepoMetadata(models.Model):
class RepoView(object):
def __init__(self, name, type='table', view_ids=None):
def __init__(self, name, type='table', view_data={}, view_ids=None):
self.name = name
self.type = type
self.view_data = view_data
self.view_json = {}
self.init_view(view_ids)
@@ -81,14 +82,18 @@ class RepoView(object):
"hidden_columns": [],
"type": self.type,
}
self.view_json.update(self.view_data)
class RepoMetadataViewsManager(models.Manager):
def add_view(self, repo_id, view_name, view_type='table'):
def add_view(self, repo_id, view_name, view_type='table', view_data={}):
metadata_views = self.filter(repo_id=repo_id).first()
if not metadata_views:
new_view = RepoView(view_name)
from seafevents.repo_metadata.utils import METADATA_TABLE
new_view = RepoView(view_name, view_type, {
'basic_filters': [{ 'column_key': METADATA_TABLE.columns.is_dir.key, 'filter_predicate': 'is', 'filter_term': 'file' }]
})
view_json = new_view.view_json
view_id = view_json.get('_id')
view_details = {
@@ -103,7 +108,7 @@ class RepoMetadataViewsManager(models.Manager):
view_details = json.loads(metadata_views.details)
view_name = get_no_duplicate_obj_name(view_name, metadata_views.view_names)
exist_view_ids = metadata_views.view_ids
new_view = RepoView(view_name, view_type, exist_view_ids)
new_view = RepoView(view_name, view_type, view_data, exist_view_ids)
view_json = new_view.view_json
view_id = view_json.get('_id')
view_details['views'].append(view_json)