1
0
mirror of https://github.com/haiwen/seahub.git synced 2025-09-03 16:10:26 +00:00

Feature/gallery view (#6578)

* add gallery view, display images in grid layout

* remove redundant code

* clean up redundant code, improve responsive gallery

* improve gaps in gallery
This commit is contained in:
Aries
2024-08-21 17:14:57 +08:00
committed by GitHub
parent 6f5f1f0e05
commit ce3ffff54d
21 changed files with 254 additions and 46 deletions

View File

@@ -113,7 +113,7 @@ class DirTool extends React.Component {
if (isFileExtended) { if (isFileExtended) {
return ( return (
<div className="d-flex"> <div className="dir-tool">
<MetadataViewToolBar viewId={viewId} /> <MetadataViewToolBar viewId={viewId} />
</div> </div>
); );

View File

@@ -13,3 +13,16 @@ const TAG_COLORS = ['#FBD44A', '#EAA775', '#F4667C', '#DC82D2', '#9860E5', '#9F8
export const SIDE_PANEL_FOLDED_WIDTH = 71; export const SIDE_PANEL_FOLDED_WIDTH = 71;
export { KeyCodes, zIndexes, TAG_COLORS }; export { KeyCodes, zIndexes, TAG_COLORS };
export const VIEW_OPTIONS = [
{
key: 'table',
label: 'Table',
type: 'table',
},
{
key: 'gallery',
label: 'Gallery',
type: 'image',
}
];

View File

@@ -339,3 +339,10 @@ img[src=""],img:not([src]) { /* for first loading img*/
.dir-tool>div { .dir-tool>div {
margin-left: 8px; margin-left: 8px;
} }
.dir-tool {
height: 1.5rem;
display: flex;
align-items: center;
text-align: center;
}

View File

@@ -112,9 +112,9 @@ class MetadataManagerAPI {
return this.req.get(url); 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 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' } }); return this._sendPostRequest(url, params, { headers: { 'Content-type': 'application/json' } });
}; };

View File

@@ -107,8 +107,8 @@ export const MetadataProvider = ({ repoID, hideMetadataView, selectMetadataView,
// eslint-disable-next-line react-hooks/exhaustive-deps // eslint-disable-next-line react-hooks/exhaustive-deps
}, [repoID, selectMetadataView]); }, [repoID, selectMetadataView]);
const addView = useCallback((name, successCallback, failCallback) => { const addView = useCallback((name, type, successCallback, failCallback) => {
metadataAPI.addView(repoID, name).then(res => { metadataAPI.addView(repoID, name, type).then(res => {
const view = res.data.view; const view = res.data.view;
let newNavigation = navigation.slice(0); let newNavigation = navigation.slice(0);
newNavigation.push({ _id: view._id, type: 'view' }); newNavigation.push({ _id: view._id, type: 'view' });

View File

@@ -65,3 +65,7 @@
font-size: 14px; font-size: 14px;
margin-top: 2px; margin-top: 2px;
} }
.metadata-tree-view .metadata-views-icon {
fill: #666;
}

View File

@@ -1,12 +1,12 @@
import React, { useCallback, useEffect, useMemo, useState, useRef } from 'react'; import React, { useCallback, useEffect, useMemo, useState, useRef } from 'react';
import { Form, Input } from 'reactstrap';
import PropTypes from 'prop-types'; import PropTypes from 'prop-types';
import { CustomizeAddTool } from '@seafile/sf-metadata-ui-component'; import { CustomizeAddTool } from '@seafile/sf-metadata-ui-component';
import Icon from '../../components/icon';
import { gettext } from '../../utils/constants'; 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 ViewItem from './view-item';
import { useMetadata } from '../hooks'; import { useMetadata } from '../hooks';
import { Form, Input } from 'reactstrap';
import Icon from '../../components/icon';
import { AddView } from '../metadata-view/components/popover/view-popover'; import { AddView } from '../metadata-view/components/popover/view-popover';
import './index.css'; import './index.css';
@@ -27,6 +27,7 @@ const MetadataTreeView = ({ userPerm, currentPath }) => {
updateView, updateView,
moveView moveView
} = useMetadata(); } = useMetadata();
const [newView, setNewView] = useState(null);
const [showAddViewPopover, setShowAddViewPopover] = useState(false); const [showAddViewPopover, setShowAddViewPopover] = useState(false);
const [showInput, setShowInput] = useState(false); const [showInput, setShowInput] = useState(false);
@@ -71,7 +72,8 @@ const MetadataTreeView = ({ userPerm, currentPath }) => {
setInputValue(event.target.value); setInputValue(event.target.value);
}; };
const handlePopoverOptionClick = () => { const handlePopoverOptionClick = (option) => {
setNewView(option);
setShowInput(true); setShowInput(true);
setShowAddViewPopover(false); setShowAddViewPopover(false);
}; };
@@ -79,10 +81,10 @@ const MetadataTreeView = ({ userPerm, currentPath }) => {
const handleInputSubmit = useCallback((event) => { const handleInputSubmit = useCallback((event) => {
event.preventDefault(); event.preventDefault();
event.stopPropagation(); event.stopPropagation();
addView(inputValue); addView(inputValue, newView.type);
setShowInput(false); setShowInput(false);
setInputValue('Untitled'); setInputValue('Untitled');
}, [inputValue, addView]); }, [inputValue, addView, newView]);
const handleClickOutsideInput = useCallback((event) => { const handleClickOutsideInput = useCallback((event) => {
if (inputRef.current && !inputRef.current.contains(event.target)) { if (inputRef.current && !inputRef.current.contains(event.target)) {
@@ -127,9 +129,7 @@ const MetadataTreeView = ({ userPerm, currentPath }) => {
{showInput && ( {showInput && (
<Form onSubmit={handleInputSubmit} className='tree-view-inner sf-metadata-view-form'> <Form onSubmit={handleInputSubmit} className='tree-view-inner sf-metadata-view-form'>
<div className="left-icon"> <div className="left-icon">
<div className="tree-node-icon"> <Icon symbol={newView.type} className="metadata-views-icon" />
<Icon symbol="table" className="metadata-views-icon" />
</div>
</div> </div>
<Input <Input
className='sf-metadata-view-input' className='sf-metadata-view-input'
@@ -157,7 +157,7 @@ const MetadataTreeView = ({ userPerm, currentPath }) => {
</div> </div>
</div> </div>
{showAddViewPopover && ( {showAddViewPopover && (
<AddView target='sf-metadata-view-popover' toggle={togglePopover} onOptionClick={handlePopoverOptionClick} /> <AddView target='sf-metadata-view-popover' options={VIEW_OPTIONS} toggle={togglePopover} onOptionClick={handlePopoverOptionClick} />
)} )}
</> </>
); );

View File

@@ -151,7 +151,7 @@ const ViewItem = ({
</div> </div>
<div className="left-icon"> <div className="left-icon">
<div className="tree-node-icon"> <div className="tree-node-icon">
<Icon symbol="table" className="metadata-views-icon" /> <Icon symbol={view.type ? view.type : 'table'} className="metadata-views-icon" />
</div> </div>
</div> </div>
<div className="right-icon" id={`metadata-view-dropdown-item-${view._id}`} > <div className="right-icon" id={`metadata-view-dropdown-item-${view._id}`} >

View File

@@ -1,3 +1,4 @@
import SliderSetter from './slider-setter';
import FilterSetter from './filter-setter'; import FilterSetter from './filter-setter';
import SortSetter from './sort-setter'; import SortSetter from './sort-setter';
import GroupbySetter from './groupby-setter'; import GroupbySetter from './groupby-setter';
@@ -5,6 +6,7 @@ import PreHideColumnSetter from './pre-hide-column-setter';
import HideColumnSetter from './hide-column-setter'; import HideColumnSetter from './hide-column-setter';
export { export {
SliderSetter,
FilterSetter, FilterSetter,
SortSetter, SortSetter,
GroupbySetter, GroupbySetter,

View File

@@ -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 (
<>
<Button type="button" size='sm' onClick={handleImageShrink} >-</Button>
<Input
type="range"
min="-2"
max="2"
step="1"
value={sliderValue}
onChange={handleGalleryColumnsChange}
className="custom-slider ml-2 mr-2"
/>
<Button type="button" size='sm' onClick={handleImageExpand} className='mr-2' >+</Button>
</>
);
};
export default SliderSetter;

View File

@@ -6,7 +6,7 @@ import Icon from '../../../../../../components/icon';
import '../index.css'; import '../index.css';
const AddView = ({ target, toggle, onOptionClick }) => { const AddView = ({ target, options, toggle, onOptionClick }) => {
const popoverRef = useRef(null); const popoverRef = useRef(null);
const handleClickOutside = useCallback((event) => { const handleClickOutside = useCallback((event) => {
@@ -39,14 +39,16 @@ const AddView = ({ target, toggle, onOptionClick }) => {
<div ref={popoverRef}> <div ref={popoverRef}>
<div className='sf-metadata-addview-popover-header'>{gettext('New view')}</div> <div className='sf-metadata-addview-popover-header'>{gettext('New view')}</div>
<div className='sf-metadata-addview-popover-body'> <div className='sf-metadata-addview-popover-body'>
<button className='dropdown-item sf-metadata-addview-popover-item' onClick={onOptionClick}> {options.map((item, index) => {
<div className="left-icon"> return (
<div className="metadata-view-icon"> <button key={index} className='dropdown-item sf-metadata-addview-popover-item' onClick={() => onOptionClick(item)}>
<Icon symbol="table" /> <div className="left-icon">
</div> <Icon symbol={item.type} className='metadata-view-icon' />
</div> </div>
<div>{gettext('Table')}</div> <div>{gettext(item.label)}</div>
</button> </button>
);
})}
</div> </div>
</div> </div>
</UncontrolledPopover> </UncontrolledPopover>

View File

@@ -55,6 +55,7 @@
align-items: center; align-items: center;
font-size: 1rem; font-size: 1rem;
color: #666; color: #666;
fill: #666;
} }
.sf-metadata-rename-view-popover-body { .sf-metadata-rename-view-popover-body {
@@ -63,4 +64,5 @@
.dropdown-item:hover .metadata-view-icon { .dropdown-item:hover .metadata-view-icon {
color: #fff; color: #fff;
fill: #fff;
} }

View File

@@ -5,6 +5,7 @@ import { CommonlyUsedHotkey, getValidGroupbys } from '../../_basic';
import { gettext } from '../../utils'; import { gettext } from '../../utils';
import { useMetadata } from '../../hooks'; import { useMetadata } from '../../hooks';
import TableMain from './table-main'; import TableMain from './table-main';
import Gallery from './gallery';
import { Utils } from '../../../../utils/utils'; import { Utils } from '../../../../utils/utils';
import './index.css'; import './index.css';
@@ -175,22 +176,25 @@ const Container = () => {
{errorMsg && (<div className="d-center-middle error">{gettext(errorMsg)}</div>)} {errorMsg && (<div className="d-center-middle error">{gettext(errorMsg)}</div>)}
{!errorMsg && ( {!errorMsg && (
<div className="sf-metadata-container" ref={containerRef}> <div className="sf-metadata-container" ref={containerRef}>
<TableMain {metadata.view.type === 'table' && (
isGroupView={isGroupView} <TableMain
isLoadingMore={isLoadingMore} isGroupView={isGroupView}
loadMore={loadMore} isLoadingMore={isLoadingMore}
metadata={metadata} loadMore={loadMore}
modifyRecord={modifyRecord} metadata={metadata}
modifyRecords={modifyRecords} modifyRecord={modifyRecord}
recordGetterById={recordGetterById} modifyRecords={modifyRecords}
recordGetterByIndex={recordGetterByIndex} recordGetterById={recordGetterById}
getTableContentRect={getTableContentRect} recordGetterByIndex={recordGetterByIndex}
getAdjacentRowsIds={getAdjacentRowsIds} getTableContentRect={getTableContentRect}
loadAll={loadAll} getAdjacentRowsIds={getAdjacentRowsIds}
renameColumn={renameColumn} loadAll={loadAll}
deleteColumn={deleteColumn} renameColumn={renameColumn}
modifyColumnData={modifyColumnData} deleteColumn={deleteColumn}
/> modifyColumnData={modifyColumnData}
/>
)}
{metadata.view.type === 'image' && (<Gallery />)}
</div> </div>
)} )}
</div> </div>

View File

@@ -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;
}

View File

@@ -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 (<CenteredLoading />);
return (
<div ref={containerRef} className="metadata-gallery-container">
<ul className="metadata-gallery-image-list" style={{ gridTemplateColumns: `repeat(${columns}, 1fr)` }}>
{imageItems.map((img, index) => (
<li key={index} className='metadata-gallery-image-item' style={{ width: imageWidth, height: imageWidth }}>
<img
className="metadata-gallery-grid-image"
src={img.src}
alt={img.name}
/>
</li>
))}
</ul>
</div>
);
};
export default Gallery;

View File

@@ -1,6 +1,6 @@
import React, { useCallback, useEffect, useMemo, useState } from 'react'; import React, { useCallback, useEffect, useMemo, useState } from 'react';
import PropTypes from 'prop-types'; 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 { EVENT_BUS_TYPE } from '../../constants';
import './index.css'; import './index.css';
@@ -71,6 +71,7 @@ const ViewToolBar = ({ viewId }) => {
onClick={onHeaderClick} onClick={onHeaderClick}
> >
<div className="sf-metadata-tool-left-operations"> <div className="sf-metadata-tool-left-operations">
{view.type === 'image' && <SliderSetter />}
<FilterSetter <FilterSetter
isNeedSubmit={true} isNeedSubmit={true}
wrapperClass="sf-metadata-view-tool-operation-btn sf-metadata-view-tool-filter" wrapperClass="sf-metadata-view-tool-operation-btn sf-metadata-view-tool-filter"

View File

@@ -47,4 +47,6 @@ export const EVENT_BUS_TYPE = {
SAVED: 'saved', SAVED: 'saved',
ERROR: 'error', ERROR: 'error',
// gallery
MODIFY_GALLERY_COLUMNS: 'modify_gallery_columns',
}; };

View File

@@ -4,6 +4,7 @@ class View {
constructor(object, columns) { constructor(object, columns) {
this._id = object._id || ''; this._id = object._id || '';
this.type = object.type || 'table';
// filter // filter
this.filters = object.filters || []; this.filters = object.filters || [];

View File

@@ -271,6 +271,7 @@ class LibContentContainer extends React.Component {
sortOrder={this.props.sortOrder} sortOrder={this.props.sortOrder}
sortItems={this.props.sortItems} sortItems={this.props.sortItems}
viewId={this.props.viewId} viewId={this.props.viewId}
viewType={this.props.viewType}
/> />
</div> </div>
} }

View File

@@ -575,6 +575,7 @@ class MetadataViews(APIView):
def post(self, request, repo_id): def post(self, request, repo_id):
# Add a metadata view # Add a metadata view
view_name = request.data.get('name') view_name = request.data.get('name')
view_type = request.data.get('type', 'table')
if not view_name: if not view_name:
error_msg = 'view name is invalid.' error_msg = 'view name is invalid.'
return api_error(status.HTTP_400_BAD_REQUEST, error_msg) 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) return api_error(status.HTTP_403_FORBIDDEN, error_msg)
try: 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: except Exception as e:
logger.exception(e) logger.exception(e)
error_msg = 'Internal Server Error' error_msg = 'Internal Server Error'

View File

@@ -61,8 +61,9 @@ class RepoMetadata(models.Model):
class RepoView(object): class RepoView(object):
def __init__(self, name, view_ids=None): def __init__(self, name, type= 'table', view_ids=None):
self.name = name self.name = name
self.type = type
self.view_json = {} self.view_json = {}
self.init_view(view_ids) self.init_view(view_ids)
@@ -77,12 +78,13 @@ class RepoView(object):
"groupbys": [], "groupbys": [],
"filter_conjunction": "And", "filter_conjunction": "And",
"hidden_columns": [], "hidden_columns": [],
"type": self.type,
} }
class RepoMetadataViewsManager(models.Manager): 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() metadata_views = self.filter(repo_id=repo_id).first()
if not metadata_views: if not metadata_views:
new_view = RepoView(view_name) new_view = RepoView(view_name)
@@ -100,7 +102,7 @@ class RepoMetadataViewsManager(models.Manager):
view_details = json.loads(metadata_views.details) view_details = json.loads(metadata_views.details)
view_name = get_no_duplicate_obj_name(view_name, metadata_views.view_names) view_name = get_no_duplicate_obj_name(view_name, metadata_views.view_names)
exist_view_ids = metadata_views.view_ids 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_json = new_view.view_json
view_id = view_json.get('_id') view_id = view_json.get('_id')
view_details['views'].append(view_json) view_details['views'].append(view_json)