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-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 (