mirror of
https://github.com/haiwen/seahub.git
synced 2025-09-02 23:48:47 +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:
@@ -113,7 +113,7 @@ class DirTool extends React.Component {
|
||||
|
||||
if (isFileExtended) {
|
||||
return (
|
||||
<div className="d-flex">
|
||||
<div className="dir-tool">
|
||||
<MetadataViewToolBar viewId={viewId} />
|
||||
</div>
|
||||
);
|
||||
|
@@ -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',
|
||||
}
|
||||
];
|
||||
|
@@ -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;
|
||||
}
|
||||
|
@@ -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' } });
|
||||
};
|
||||
|
||||
|
@@ -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' });
|
||||
|
@@ -65,3 +65,7 @@
|
||||
font-size: 14px;
|
||||
margin-top: 2px;
|
||||
}
|
||||
|
||||
.metadata-tree-view .metadata-views-icon {
|
||||
fill: #666;
|
||||
}
|
||||
|
@@ -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 && (
|
||||
<Form onSubmit={handleInputSubmit} className='tree-view-inner sf-metadata-view-form'>
|
||||
<div className="left-icon">
|
||||
<div className="tree-node-icon">
|
||||
<Icon symbol="table" className="metadata-views-icon" />
|
||||
</div>
|
||||
<Icon symbol={newView.type} className="metadata-views-icon" />
|
||||
</div>
|
||||
<Input
|
||||
className='sf-metadata-view-input'
|
||||
@@ -157,7 +157,7 @@ const MetadataTreeView = ({ userPerm, currentPath }) => {
|
||||
</div>
|
||||
</div>
|
||||
{showAddViewPopover && (
|
||||
<AddView target='sf-metadata-view-popover' toggle={togglePopover} onOptionClick={handlePopoverOptionClick} />
|
||||
<AddView target='sf-metadata-view-popover' options={VIEW_OPTIONS} toggle={togglePopover} onOptionClick={handlePopoverOptionClick} />
|
||||
)}
|
||||
</>
|
||||
);
|
||||
|
@@ -151,7 +151,7 @@ const ViewItem = ({
|
||||
</div>
|
||||
<div className="left-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 className="right-icon" id={`metadata-view-dropdown-item-${view._id}`} >
|
||||
|
@@ -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,
|
||||
|
@@ -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;
|
@@ -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 }) => {
|
||||
<div ref={popoverRef}>
|
||||
<div className='sf-metadata-addview-popover-header'>{gettext('New view')}</div>
|
||||
<div className='sf-metadata-addview-popover-body'>
|
||||
<button className='dropdown-item sf-metadata-addview-popover-item' onClick={onOptionClick}>
|
||||
<div className="left-icon">
|
||||
<div className="metadata-view-icon">
|
||||
<Icon symbol="table" />
|
||||
</div>
|
||||
</div>
|
||||
<div>{gettext('Table')}</div>
|
||||
</button>
|
||||
{options.map((item, index) => {
|
||||
return (
|
||||
<button key={index} className='dropdown-item sf-metadata-addview-popover-item' onClick={() => onOptionClick(item)}>
|
||||
<div className="left-icon">
|
||||
<Icon symbol={item.type} className='metadata-view-icon' />
|
||||
</div>
|
||||
<div>{gettext(item.label)}</div>
|
||||
</button>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
</UncontrolledPopover>
|
||||
|
@@ -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;
|
||||
}
|
||||
|
@@ -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 && (<div className="d-center-middle error">{gettext(errorMsg)}</div>)}
|
||||
{!errorMsg && (
|
||||
<div className="sf-metadata-container" ref={containerRef}>
|
||||
<TableMain
|
||||
isGroupView={isGroupView}
|
||||
isLoadingMore={isLoadingMore}
|
||||
loadMore={loadMore}
|
||||
metadata={metadata}
|
||||
modifyRecord={modifyRecord}
|
||||
modifyRecords={modifyRecords}
|
||||
recordGetterById={recordGetterById}
|
||||
recordGetterByIndex={recordGetterByIndex}
|
||||
getTableContentRect={getTableContentRect}
|
||||
getAdjacentRowsIds={getAdjacentRowsIds}
|
||||
loadAll={loadAll}
|
||||
renameColumn={renameColumn}
|
||||
deleteColumn={deleteColumn}
|
||||
modifyColumnData={modifyColumnData}
|
||||
/>
|
||||
{metadata.view.type === 'table' && (
|
||||
<TableMain
|
||||
isGroupView={isGroupView}
|
||||
isLoadingMore={isLoadingMore}
|
||||
loadMore={loadMore}
|
||||
metadata={metadata}
|
||||
modifyRecord={modifyRecord}
|
||||
modifyRecords={modifyRecords}
|
||||
recordGetterById={recordGetterById}
|
||||
recordGetterByIndex={recordGetterByIndex}
|
||||
getTableContentRect={getTableContentRect}
|
||||
getAdjacentRowsIds={getAdjacentRowsIds}
|
||||
loadAll={loadAll}
|
||||
renameColumn={renameColumn}
|
||||
deleteColumn={deleteColumn}
|
||||
modifyColumnData={modifyColumnData}
|
||||
/>
|
||||
)}
|
||||
{metadata.view.type === 'image' && (<Gallery />)}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
@@ -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;
|
||||
}
|
@@ -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;
|
@@ -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}
|
||||
>
|
||||
<div className="sf-metadata-tool-left-operations">
|
||||
{view.type === 'image' && <SliderSetter />}
|
||||
<FilterSetter
|
||||
isNeedSubmit={true}
|
||||
wrapperClass="sf-metadata-view-tool-operation-btn sf-metadata-view-tool-filter"
|
||||
|
@@ -47,4 +47,6 @@ export const EVENT_BUS_TYPE = {
|
||||
SAVED: 'saved',
|
||||
ERROR: 'error',
|
||||
|
||||
// gallery
|
||||
MODIFY_GALLERY_COLUMNS: 'modify_gallery_columns',
|
||||
};
|
||||
|
@@ -4,6 +4,7 @@ class View {
|
||||
constructor(object, columns) {
|
||||
|
||||
this._id = object._id || '';
|
||||
this.type = object.type || 'table';
|
||||
|
||||
// filter
|
||||
this.filters = object.filters || [];
|
||||
|
@@ -271,6 +271,7 @@ class LibContentContainer extends React.Component {
|
||||
sortOrder={this.props.sortOrder}
|
||||
sortItems={this.props.sortItems}
|
||||
viewId={this.props.viewId}
|
||||
viewType={this.props.viewType}
|
||||
/>
|
||||
</div>
|
||||
}
|
||||
|
@@ -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'
|
||||
|
@@ -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)
|
||||
|
Reference in New Issue
Block a user