diff --git a/frontend/src/components/cur-dir-path/dir-tool.js b/frontend/src/components/cur-dir-path/dir-tool.js index fd45d0713c..2628da7fcd 100644 --- a/frontend/src/components/cur-dir-path/dir-tool.js +++ b/frontend/src/components/cur-dir-path/dir-tool.js @@ -113,7 +113,7 @@ class DirTool extends React.Component { if (isFileExtended) { return ( -
+
); diff --git a/frontend/src/constants/index.js b/frontend/src/constants/index.js index bed27bfc56..202eae2101 100644 --- a/frontend/src/constants/index.js +++ b/frontend/src/constants/index.js @@ -13,3 +13,16 @@ const TAG_COLORS = ['#FBD44A', '#EAA775', '#F4667C', '#DC82D2', '#9860E5', '#9F8 export const SIDE_PANEL_FOLDED_WIDTH = 71; export { KeyCodes, zIndexes, TAG_COLORS }; + +export const VIEW_OPTIONS = [ + { + key: 'table', + label: 'Table', + type: 'table', + }, + { + key: 'gallery', + label: 'Gallery', + type: 'image', + } +]; diff --git a/frontend/src/css/layout.css b/frontend/src/css/layout.css index 4f41d60990..478755c3d4 100644 --- a/frontend/src/css/layout.css +++ b/frontend/src/css/layout.css @@ -339,3 +339,10 @@ img[src=""],img:not([src]) { /* for first loading img*/ .dir-tool>div { margin-left: 8px; } + +.dir-tool { + height: 1.5rem; + display: flex; + align-items: center; + text-align: center; +} diff --git a/frontend/src/metadata/api.js b/frontend/src/metadata/api.js index 846c2f5051..fa7f4e1cd9 100644 --- a/frontend/src/metadata/api.js +++ b/frontend/src/metadata/api.js @@ -112,9 +112,9 @@ class MetadataManagerAPI { return this.req.get(url); }; - addView = (repoID, name) => { + addView = (repoID, name, type = 'table') => { const url = this.server + '/api/v2.1/repos/' + repoID + '/metadata/views/'; - const params = { name }; + const params = { name, type }; return this._sendPostRequest(url, params, { headers: { 'Content-type': 'application/json' } }); }; diff --git a/frontend/src/metadata/hooks/metadata.js b/frontend/src/metadata/hooks/metadata.js index 2bdc0bcb4e..5cd51e0ab2 100644 --- a/frontend/src/metadata/hooks/metadata.js +++ b/frontend/src/metadata/hooks/metadata.js @@ -107,8 +107,8 @@ export const MetadataProvider = ({ repoID, hideMetadataView, selectMetadataView, // eslint-disable-next-line react-hooks/exhaustive-deps }, [repoID, selectMetadataView]); - const addView = useCallback((name, successCallback, failCallback) => { - metadataAPI.addView(repoID, name).then(res => { + const addView = useCallback((name, type, successCallback, failCallback) => { + metadataAPI.addView(repoID, name, type).then(res => { const view = res.data.view; let newNavigation = navigation.slice(0); newNavigation.push({ _id: view._id, type: 'view' }); diff --git a/frontend/src/metadata/metadata-tree-view/index.css b/frontend/src/metadata/metadata-tree-view/index.css index 094a6010a5..8fa06bed82 100644 --- a/frontend/src/metadata/metadata-tree-view/index.css +++ b/frontend/src/metadata/metadata-tree-view/index.css @@ -65,3 +65,7 @@ font-size: 14px; margin-top: 2px; } + +.metadata-tree-view .metadata-views-icon { + fill: #666; +} diff --git a/frontend/src/metadata/metadata-tree-view/index.js b/frontend/src/metadata/metadata-tree-view/index.js index 8cd555931b..c5797e02d8 100644 --- a/frontend/src/metadata/metadata-tree-view/index.js +++ b/frontend/src/metadata/metadata-tree-view/index.js @@ -1,12 +1,12 @@ import React, { useCallback, useEffect, useMemo, useState, useRef } from 'react'; +import { Form, Input } from 'reactstrap'; import PropTypes from 'prop-types'; import { CustomizeAddTool } from '@seafile/sf-metadata-ui-component'; +import Icon from '../../components/icon'; import { gettext } from '../../utils/constants'; -import { PRIVATE_FILE_TYPE } from '../../constants'; +import { PRIVATE_FILE_TYPE, VIEW_OPTIONS } from '../../constants'; import ViewItem from './view-item'; import { useMetadata } from '../hooks'; -import { Form, Input } from 'reactstrap'; -import Icon from '../../components/icon'; import { AddView } from '../metadata-view/components/popover/view-popover'; import './index.css'; @@ -27,6 +27,7 @@ const MetadataTreeView = ({ userPerm, currentPath }) => { updateView, moveView } = useMetadata(); + const [newView, setNewView] = useState(null); const [showAddViewPopover, setShowAddViewPopover] = useState(false); const [showInput, setShowInput] = useState(false); @@ -71,7 +72,8 @@ const MetadataTreeView = ({ userPerm, currentPath }) => { setInputValue(event.target.value); }; - const handlePopoverOptionClick = () => { + const handlePopoverOptionClick = (option) => { + setNewView(option); setShowInput(true); setShowAddViewPopover(false); }; @@ -79,10 +81,10 @@ const MetadataTreeView = ({ userPerm, currentPath }) => { const handleInputSubmit = useCallback((event) => { event.preventDefault(); event.stopPropagation(); - addView(inputValue); + addView(inputValue, newView.type); setShowInput(false); setInputValue('Untitled'); - }, [inputValue, addView]); + }, [inputValue, addView, newView]); const handleClickOutsideInput = useCallback((event) => { if (inputRef.current && !inputRef.current.contains(event.target)) { @@ -127,9 +129,7 @@ const MetadataTreeView = ({ userPerm, currentPath }) => { {showInput && (
-
- -
+
{
{showAddViewPopover && ( - + )} ); diff --git a/frontend/src/metadata/metadata-tree-view/view-item/index.js b/frontend/src/metadata/metadata-tree-view/view-item/index.js index bcca778b52..14d6bfd29a 100644 --- a/frontend/src/metadata/metadata-tree-view/view-item/index.js +++ b/frontend/src/metadata/metadata-tree-view/view-item/index.js @@ -151,7 +151,7 @@ const ViewItem = ({
- +
diff --git a/frontend/src/metadata/metadata-view/components/data-process-setter/index.js b/frontend/src/metadata/metadata-view/components/data-process-setter/index.js index 89cbfa66d3..0b7c69ff31 100644 --- a/frontend/src/metadata/metadata-view/components/data-process-setter/index.js +++ b/frontend/src/metadata/metadata-view/components/data-process-setter/index.js @@ -1,3 +1,4 @@ +import SliderSetter from './slider-setter'; import FilterSetter from './filter-setter'; import SortSetter from './sort-setter'; import GroupbySetter from './groupby-setter'; @@ -5,6 +6,7 @@ import PreHideColumnSetter from './pre-hide-column-setter'; import HideColumnSetter from './hide-column-setter'; export { + SliderSetter, FilterSetter, SortSetter, GroupbySetter, diff --git a/frontend/src/metadata/metadata-view/components/data-process-setter/slider-setter.jsx b/frontend/src/metadata/metadata-view/components/data-process-setter/slider-setter.jsx new file mode 100644 index 0000000000..3a6b2f545f --- /dev/null +++ b/frontend/src/metadata/metadata-view/components/data-process-setter/slider-setter.jsx @@ -0,0 +1,43 @@ +import React, { useState, useCallback } from 'react'; +import { Button, Input } from 'reactstrap'; +import { EVENT_BUS_TYPE } from '../../constants'; + +const SliderSetter = () => { + const [sliderValue, setSliderValue] = useState(0); + + const handleGalleryColumnsChange = useCallback((e) => { + const adjust = parseInt(e.target.value, 10); + setSliderValue(adjust); + window.sfMetadataContext.eventBus.dispatch(EVENT_BUS_TYPE.MODIFY_GALLERY_COLUMNS, adjust); + }, []); + + const handleImageExpand = useCallback(() => { + const adjust = Math.min(sliderValue + 1, 2); + setSliderValue(adjust); + window.sfMetadataContext.eventBus.dispatch(EVENT_BUS_TYPE.MODIFY_GALLERY_COLUMNS, 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); + }, [sliderValue]); + + return ( + <> + + + + + ); +}; + +export default SliderSetter; diff --git a/frontend/src/metadata/metadata-view/components/popover/view-popover/add-view/index.js b/frontend/src/metadata/metadata-view/components/popover/view-popover/add-view/index.js index 79405bfcd0..d985aece81 100644 --- a/frontend/src/metadata/metadata-view/components/popover/view-popover/add-view/index.js +++ b/frontend/src/metadata/metadata-view/components/popover/view-popover/add-view/index.js @@ -6,7 +6,7 @@ import Icon from '../../../../../../components/icon'; import '../index.css'; -const AddView = ({ target, toggle, onOptionClick }) => { +const AddView = ({ target, options, toggle, onOptionClick }) => { const popoverRef = useRef(null); const handleClickOutside = useCallback((event) => { @@ -39,14 +39,16 @@ const AddView = ({ target, toggle, onOptionClick }) => {
{gettext('New view')}
- + {options.map((item, index) => { + return ( + + ); + })}
diff --git a/frontend/src/metadata/metadata-view/components/popover/view-popover/index.css b/frontend/src/metadata/metadata-view/components/popover/view-popover/index.css index 1060197670..8316c503ed 100644 --- a/frontend/src/metadata/metadata-view/components/popover/view-popover/index.css +++ b/frontend/src/metadata/metadata-view/components/popover/view-popover/index.css @@ -55,6 +55,7 @@ align-items: center; font-size: 1rem; color: #666; + fill: #666; } .sf-metadata-rename-view-popover-body { @@ -63,4 +64,5 @@ .dropdown-item:hover .metadata-view-icon { color: #fff; + fill: #fff; } diff --git a/frontend/src/metadata/metadata-view/components/table/container.js b/frontend/src/metadata/metadata-view/components/table/container.js index 98f97abb9f..9fc49213fc 100644 --- a/frontend/src/metadata/metadata-view/components/table/container.js +++ b/frontend/src/metadata/metadata-view/components/table/container.js @@ -5,6 +5,7 @@ import { CommonlyUsedHotkey, getValidGroupbys } from '../../_basic'; import { gettext } from '../../utils'; import { useMetadata } from '../../hooks'; import TableMain from './table-main'; +import Gallery from './gallery'; import { Utils } from '../../../../utils/utils'; import './index.css'; @@ -175,22 +176,25 @@ const Container = () => { {errorMsg && (
{gettext(errorMsg)}
)} {!errorMsg && (
- + {metadata.view.type === 'table' && ( + + )} + {metadata.view.type === 'image' && ()}
)}
diff --git a/frontend/src/metadata/metadata-view/components/table/gallery/index.css b/frontend/src/metadata/metadata-view/components/table/gallery/index.css new file mode 100644 index 0000000000..d0ae34afb8 --- /dev/null +++ b/frontend/src/metadata/metadata-view/components/table/gallery/index.css @@ -0,0 +1,31 @@ +.metadata-gallery-container { + height: calc(100vh - 100px); + position: relative; + display: flex; + justify-content: center; + align-items: center; + overflow-y: auto; +} + +.metadata-gallery-image-list { + position: absolute; + top: 2px; + left: 2px; + display: grid; + gap: 2px; + list-style: none; + padding: 0; + margin: 0; +} + +.metadata-gallery-image-item { + display: flex; + justify-content: center; + align-items: center; +} + +.metadata-gallery-grid-image { + width: 100%; + height: 100%; + object-fit: cover; +} diff --git a/frontend/src/metadata/metadata-view/components/table/gallery/index.js b/frontend/src/metadata/metadata-view/components/table/gallery/index.js new file mode 100644 index 0000000000..5eb15445e3 --- /dev/null +++ b/frontend/src/metadata/metadata-view/components/table/gallery/index.js @@ -0,0 +1,92 @@ +import React, { useMemo, useState, useEffect, useRef } 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 { siteRoot } from '../../../../../utils/constants'; +import { EVENT_BUS_TYPE } from '../../../constants'; + +import './index.css'; + +const Gallery = () => { + const [imageWidth, setImageWidth] = useState(100); + const [columns, setColumns] = useState(8); + const [containerWidth, setContainerWidth] = useState(960); + const [adjustValue, setAdjustValue] = useState(0); + const { isLoading, metadata } = useMetadata(); + const containerRef = useRef(null); + const repoID = window.sfMetadataContext.getSetting('repoID'); + + useEffect(() => { + const handleResize = () => { + if (containerRef.current) { + setContainerWidth(containerRef.current.offsetWidth); + } + }; + + const resizeObserver = new ResizeObserver(handleResize); + const currentContainer = containerRef.current; + + 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]); + + 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}repo/${repoID}/raw${path}`; + + return { + name: fileName, + url: `${siteRoot}lib/${repoID}/file${path}`, + src: src, + date: date, + }; + }); + }, [metadata, repoID]); + + useEffect(() => { + const columns = (Utils.isDesktop() ? 8 : 4) - adjustValue; + const adjustedImageWidth = (containerWidth - columns * 2 - 2) / columns; + setColumns(columns); + setImageWidth(adjustedImageWidth); + }, [containerWidth, adjustValue]); + + if (isLoading) return (); + return ( +
+
    + {imageItems.map((img, index) => ( +
  • + {img.name} +
  • + ))} +
+
+ ); +}; + +export default Gallery; 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 b3430c6d8d..06ed020859 100644 --- a/frontend/src/metadata/metadata-view/components/view-toolbar/index.js +++ b/frontend/src/metadata/metadata-view/components/view-toolbar/index.js @@ -1,6 +1,6 @@ import React, { useCallback, useEffect, useMemo, useState } from 'react'; import PropTypes from 'prop-types'; -import { FilterSetter, GroupbySetter, SortSetter, HideColumnSetter } from '../data-process-setter'; +import { SliderSetter, FilterSetter, GroupbySetter, SortSetter, HideColumnSetter } from '../data-process-setter'; import { EVENT_BUS_TYPE } from '../../constants'; import './index.css'; @@ -71,6 +71,7 @@ const ViewToolBar = ({ viewId }) => { onClick={onHeaderClick} >
+ {view.type === 'image' && }
} diff --git a/seahub/api2/endpoints/metadata_manage.py b/seahub/api2/endpoints/metadata_manage.py index 5751c98adc..9e42436aad 100644 --- a/seahub/api2/endpoints/metadata_manage.py +++ b/seahub/api2/endpoints/metadata_manage.py @@ -575,6 +575,7 @@ class MetadataViews(APIView): def post(self, request, repo_id): # Add a metadata view view_name = request.data.get('name') + view_type = request.data.get('type', 'table') if not view_name: error_msg = 'view name is invalid.' return api_error(status.HTTP_400_BAD_REQUEST, error_msg) @@ -595,7 +596,7 @@ class MetadataViews(APIView): return api_error(status.HTTP_403_FORBIDDEN, error_msg) try: - new_view = RepoMetadataViews.objects.add_view(repo_id, view_name) + new_view = RepoMetadataViews.objects.add_view(repo_id, view_name, view_type) 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 565a401e03..516dee046c 100644 --- a/seahub/repo_metadata/models.py +++ b/seahub/repo_metadata/models.py @@ -61,8 +61,9 @@ class RepoMetadata(models.Model): class RepoView(object): - def __init__(self, name, view_ids=None): + def __init__(self, name, type= 'table', view_ids=None): self.name = name + self.type = type self.view_json = {} self.init_view(view_ids) @@ -77,12 +78,13 @@ class RepoView(object): "groupbys": [], "filter_conjunction": "And", "hidden_columns": [], + "type": self.type, } class RepoMetadataViewsManager(models.Manager): - def add_view(self, repo_id, view_name): + def add_view(self, repo_id, view_name, view_type): metadata_views = self.filter(repo_id=repo_id).first() if not metadata_views: new_view = RepoView(view_name) @@ -100,7 +102,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, exist_view_ids) + new_view = RepoView(view_name, view_type, exist_view_ids) view_json = new_view.view_json view_id = view_json.get('_id') view_details['views'].append(view_json)