diff --git a/frontend/src/metadata/api.js b/frontend/src/metadata/api.js index d94526e489a..484ed0fc640 100644 --- a/frontend/src/metadata/api.js +++ b/frontend/src/metadata/api.js @@ -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' } }); }; diff --git a/frontend/src/metadata/metadata-view/_basic/constants/sort.js b/frontend/src/metadata/metadata-view/_basic/constants/sort.js index 3e8fdbd0a7d..998309d5edd 100644 --- a/frontend/src/metadata/metadata-view/_basic/constants/sort.js +++ b/frontend/src/metadata/metadata-view/_basic/constants/sort.js @@ -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, }; diff --git a/frontend/src/metadata/metadata-view/_basic/constants/view.js b/frontend/src/metadata/metadata-view/_basic/constants/view.js index 62d3b23c45c..1b62fa5f710 100644 --- a/frontend/src/metadata/metadata-view/_basic/constants/view.js +++ b/frontend/src/metadata/metadata-view/_basic/constants/view.js @@ -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, +}; diff --git a/frontend/src/metadata/metadata-view/components/data-process-setter/slider-setter.js b/frontend/src/metadata/metadata-view/components/data-process-setter/slider-setter.js index 8566bc87956..260553e19c4 100644 --- a/frontend/src/metadata/metadata-view/components/data-process-setter/slider-setter.js +++ b/frontend/src/metadata/metadata-view/components/data-process-setter/slider-setter.js @@ -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 ( diff --git a/frontend/src/metadata/metadata-view/components/data-process-setter/sort-setter.js b/frontend/src/metadata/metadata-view/components/data-process-setter/sort-setter.js index a5465eb2a7c..647b3d85157 100644 --- a/frontend/src/metadata/metadata-view/components/data-process-setter/sort-setter.js +++ b/frontend/src/metadata/metadata-view/components/data-process-setter/sort-setter.js @@ -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 { + + const options = useMemo(() => { + return OPTIONS.map(o => { + const { name } = o; + return { + value: o.value, + label: ( +
+
{name}
+
+ {value === o.value && ()} +
+
+ ) + }; + }); + }, [value]); + + const displayValue = useMemo(() => { + const selectedOption = OPTIONS.find(o => o.value === value) || OPTIONS[2]; + return { + label: ( +
+ {selectedOption.name} +
+ ) + }; + }, [value]); + + const onChange = useCallback((newValue) => { + if (newValue === value) return; + onChangeAPI(newValue); + }, [value, onChangeAPI]); + + return ( + + ) + }} + /> + ); +}; + +FileTypeFilter.propTypes = { + readOnly: PropTypes.bool, + value: PropTypes.string, + onChange: PropTypes.func, +}; + +export default FileTypeFilter; diff --git a/frontend/src/metadata/metadata-view/components/popover/filter-popover/basic-filters/index.css b/frontend/src/metadata/metadata-view/components/popover/filter-popover/basic-filters/index.css index d303a36fef0..57fd3f4ef77 100644 --- a/frontend/src/metadata/metadata-view/components/popover/filter-popover/basic-filters/index.css +++ b/frontend/src/metadata/metadata-view/components/popover/filter-popover/basic-filters/index.css @@ -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; +} diff --git a/frontend/src/metadata/metadata-view/components/popover/filter-popover/basic-filters/index.js b/frontend/src/metadata/metadata-view/components/popover/filter-popover/basic-filters/index.js index 7a11d580684..2b96f4587a7 100644 --- a/frontend/src/metadata/metadata-view/components/popover/filter-popover/basic-filters/index.js +++ b/frontend/src/metadata/metadata-view/components/popover/filter-popover/basic-filters/index.js @@ -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,19 +18,34 @@ 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 ( - +
- {filters.map(filter => { - const { column_key, filter_term } = filter; - if (column_key === PRIVATE_COLUMN_KEY.IS_DIR) { - return ( - - ); - } - return null; - })} +
+ {filters.map((filter, index) => { + const { column_key, filter_term } = filter; + if (column_key === PRIVATE_COLUMN_KEY.IS_DIR) { + return ( + + ); + } + if (column_key === PRIVATE_COLUMN_KEY.FILE_TYPE) { + return ( + + ); + } + return null; + })} +
); @@ -37,7 +53,8 @@ const BasicFilters = ({ readOnly, filters = [], onChange }) => { BasicFilters.propTypes = { readOnly: PropTypes.bool, - value: PropTypes.string, + filters: PropTypes.array, + columns: PropTypes.array, onChange: PropTypes.func, }; diff --git a/frontend/src/metadata/metadata-view/components/popover/sort-popover/index.js b/frontend/src/metadata/metadata-view/components/popover/sort-popover/index.js index 52c4f55a5eb..435ea730ecd 100644 --- a/frontend/src/metadata/metadata-view/components/popover/sort-popover/index.js +++ b/frontend/src/metadata/metadata-view/components/popover/sort-popover/index.js @@ -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: ( @@ -205,11 +210,16 @@ class SortPopover extends Component { label: {selectedTypeOption?.name || gettext('Up')} }; + let columnsOptions = this.columnsOptions; + if (index === 0) { + columnsOptions = columnsOptions.filter(o => this.supportFirstSortColumnOptions.includes(o.value.column.type)); + } + return (
{!readOnly && -
this.deleteSort(event, index)}> - +
{} : (event) => this.deleteSort(event, index)}> + {!(viewType === VIEW_TYPE.GALLERY && index === 0) && }
}
@@ -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')} diff --git a/frontend/src/metadata/metadata-view/components/view-toolbar/index.js b/frontend/src/metadata/metadata-view/components/view-toolbar/index.js index fdad96e9aff..91aaaae7326 100644 --- a/frontend/src/metadata/metadata-view/components/view-toolbar/index.js +++ b/frontend/src/metadata/metadata-view/components/view-toolbar/index.js @@ -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 && ( { modifyGroupbys={modifyGroupbys} /> )} - {view.type !== VIEW_TYPE.GALLERY && ( + {viewType !== VIEW_TYPE.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 imageSize = useMemo(() => { + return (containerWidth - columns * 2 - 2) / columns; + }, [containerWidth, columns]); + + 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 = getDateDisplayString(record[firstSort.column_key], 'YYYY-MM-DD'); + const img = { + name: fileName, + url: `${siteRoot}lib/${repoID}/file${path}`, + 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], + }); } - }; - - const resizeObserver = new ResizeObserver(handleResize); - const currentContainer = containerRef.current; - - if (currentContainer) { - resizeObserver.observe(currentContainer); - } - - return () => { - if (currentContainer) { - resizeObserver.unobserve(currentContainer); + return _init; + }, []); + + 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; } - }; - }, []); - - useEffect(() => { - window.sfMetadataContext.eventBus.subscribe(EVENT_BUS_TYPE.MODIFY_GALLERY_COLUMNS, (adjust) => { - setAdjustValue(adjust); + _groups.push({ + ..._init, + top, + height, + }); }); - }, [columns]); + return _groups; + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [isFirstLoading, metadata, metadata.recordsCount, repoID, columns, imageSize]); - 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 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 { - name: fileName, - url: `${siteRoot}lib/${repoID}/file${path}`, - src: src, - date: date, - }; - }); - }, [metadata, repoID]); + const loadMore = useCallback(async () => { + if (isLoadingMore) return; + if (!metadata.hasMore) return; + setLoadingMore(true); - const groupedImages = useMemo(() => { - return imageItems.reduce((acc, item) => { - if (!acc[item.date]) { - acc[item.date] = []; - } - acc[item.date].push(item); - return acc; - }, {}); - }, [imageItems]); - - 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)); - } + try { + await store.loadMore(PER_LOAD_NUMBER); + setLoadingMore(false); + } catch (error) { + const errorMsg = Utils.getErrorMsg(error); + toaster.danger(errorMsg); + setLoadingMore(false); + return; } - }, [visibleItems, imageItems.length]); - useEffect(() => { - const container = containerRef.current; - if (container) { - container.addEventListener('scroll', handleScroll); - return () => container.removeEventListener('scroll', handleScroll); - } - }, [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 (
-
- {Object.keys(groupedImages).map(date => ( -
-
{date}
-
    - {groupedImages[date].slice(0, visibleItems).map((img, index) => ( -
  • - {img.name} addToQueue(img)} - /> -
  • - ))} -
-
- ))} +
+ {!isFirstLoading && ( + <> +
+ {isLoadingMore && (
)} + + )}
); diff --git a/frontend/src/metadata/metadata-view/components/view/gallery/main.js b/frontend/src/metadata/metadata-view/components/view/gallery/main.js new file mode 100644 index 00000000000..cba5b01559b --- /dev/null +++ b/frontend/src/metadata/metadata-view/components/view/gallery/main.js @@ -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 ( +
+ {childrenStartIndex === 0 && (
{name}
)} +
+ {children.slice(childrenStartIndex, childrenEndIndex).map((img) => ( +
+ {img.name} onLoad(img)} + /> +
+ ))} +
+
+ ); + }, [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; diff --git a/frontend/src/metadata/metadata-view/constants/event-bus-type.js b/frontend/src/metadata/metadata-view/constants/event-bus-type.js index 7df837c39f9..17adb41795d 100644 --- a/frontend/src/metadata/metadata-view/constants/event-bus-type.js +++ b/frontend/src/metadata/metadata-view/constants/event-bus-type.js @@ -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', }; diff --git a/frontend/src/metadata/metadata-view/hooks/metadata.js b/frontend/src/metadata/metadata-view/hooks/metadata.js index 528e88bff60..8c50ed2a7cf 100644 --- a/frontend/src/metadata/metadata-view/hooks/metadata.js +++ b/frontend/src/metadata/metadata-view/hooks/metadata.js @@ -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(); diff --git a/frontend/src/metadata/metadata-view/model/metadata/view.js b/frontend/src/metadata/metadata-view/model/metadata/view.js index c08aa66381d..7a8ee142344 100644 --- a/frontend/src/metadata/metadata-view/model/metadata/view.js +++ b/frontend/src/metadata/metadata-view/model/metadata/view.js @@ -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 || []; diff --git a/frontend/src/pages/lib-content-view/lib-content-view.js b/frontend/src/pages/lib-content-view/lib-content-view.js index 255a317ec17..bbd161642f8 100644 --- a/frontend/src/pages/lib-content-view/lib-content-view.js +++ b/frontend/src/pages/lib-content-view/lib-content-view.js @@ -547,6 +547,8 @@ class LibContentView extends React.Component { content: '', viewId: '', isDirentDetailShow: false + }, () => { + this.showDir('/'); }); }; diff --git a/seahub/api2/endpoints/metadata_manage.py b/seahub/api2/endpoints/metadata_manage.py index df70a2b28a6..708463fc271 100644 --- a/seahub/api2/endpoints/metadata_manage.py +++ b/seahub/api2/endpoints/metadata_manage.py @@ -101,7 +101,7 @@ def put(self, request, repo_id): 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 @@ def post(self, request, repo_id): # 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 @@ def post(self, request, repo_id): 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' diff --git a/seahub/repo_metadata/models.py b/seahub/repo_metadata/models.py index c098fcaca1c..648088be9ac 100644 --- a/seahub/repo_metadata/models.py +++ b/seahub/repo_metadata/models.py @@ -62,9 +62,10 @@ class Meta: 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 @@ def init_view(self, view_ids=None): "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 @@ def add_view(self, repo_id, view_name, view_type='table'): 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)