mirror of
https://github.com/haiwen/seahub.git
synced 2025-09-14 22:33:17 +00:00
feat: metadata gallery view (#6727)
* feat: metadata gallery view * feat: update code * feat: update code * feat: update code --------- Co-authored-by: 杨国璇 <ygx@192.168.1.4>
This commit is contained in:
@@ -1,6 +1,7 @@
|
|||||||
import axios from 'axios';
|
import axios from 'axios';
|
||||||
import cookie from 'react-cookies';
|
import cookie from 'react-cookies';
|
||||||
import { siteRoot } from '../utils/constants';
|
import { siteRoot } from '../utils/constants';
|
||||||
|
import { VIEW_TYPE_DEFAULT_BASIC_FILTER, VIEW_TYPE_DEFAULT_SORTS } from './metadata-view/_basic';
|
||||||
|
|
||||||
class MetadataManagerAPI {
|
class MetadataManagerAPI {
|
||||||
init({ server, username, password, token }) {
|
init({ server, username, password, token }) {
|
||||||
@@ -114,7 +115,14 @@ class MetadataManagerAPI {
|
|||||||
|
|
||||||
addView = (repoID, name, type = 'table') => {
|
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, 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' } });
|
return this._sendPostRequest(url, params, { headers: { 'Content-type': 'application/json' } });
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@@ -19,12 +19,27 @@ const SORT_COLUMN_OPTIONS = [
|
|||||||
CellType.RATE,
|
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 TEXT_SORTER_COLUMN_TYPES = [CellType.TEXT];
|
||||||
const NUMBER_SORTER_COLUMN_TYPES = [CellType.NUMBER, CellType.RATE];
|
const NUMBER_SORTER_COLUMN_TYPES = [CellType.NUMBER, CellType.RATE];
|
||||||
|
|
||||||
export {
|
export {
|
||||||
SORT_TYPE,
|
SORT_TYPE,
|
||||||
SORT_COLUMN_OPTIONS,
|
SORT_COLUMN_OPTIONS,
|
||||||
|
GALLERY_SORT_COLUMN_OPTIONS,
|
||||||
|
GALLERY_FIRST_SORT_COLUMN_OPTIONS,
|
||||||
TEXT_SORTER_COLUMN_TYPES,
|
TEXT_SORTER_COLUMN_TYPES,
|
||||||
NUMBER_SORTER_COLUMN_TYPES,
|
NUMBER_SORTER_COLUMN_TYPES,
|
||||||
};
|
};
|
||||||
|
@@ -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 = {
|
export const VIEW_TYPE = {
|
||||||
TABLE: 'table',
|
TABLE: 'table',
|
||||||
GALLERY: 'gallery'
|
GALLERY: 'gallery'
|
||||||
@@ -8,3 +12,23 @@ export const VIEW_TYPE_ICON = {
|
|||||||
[VIEW_TYPE.GALLERY]: 'image',
|
[VIEW_TYPE.GALLERY]: 'image',
|
||||||
'image': '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,
|
||||||
|
};
|
||||||
|
@@ -1,35 +1,32 @@
|
|||||||
import React, { useState, useCallback, useEffect } from 'react';
|
import React, { useState, useCallback } from 'react';
|
||||||
import { Button, Input } from 'reactstrap';
|
import { Button, Input } from 'reactstrap';
|
||||||
import { EVENT_BUS_TYPE } from '../../constants';
|
import { EVENT_BUS_TYPE } from '../../constants';
|
||||||
import Icon from '../../../../components/icon';
|
import Icon from '../../../../components/icon';
|
||||||
|
|
||||||
import './slider-setter.css';
|
import './slider-setter.css';
|
||||||
|
|
||||||
const SliderSetter = () => {
|
const SliderSetter = () => {
|
||||||
const [sliderValue, setSliderValue] = useState(() => {
|
const [sliderValue, setSliderValue] = useState(() => {
|
||||||
const savedValue = localStorage.getItem('sliderValue');
|
const savedValue = window.sfMetadataContext.localStorage.getItem('zoom-gear', 0);
|
||||||
return savedValue !== null ? parseInt(savedValue, 10) : 0;
|
return savedValue || 0;
|
||||||
});
|
});
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
localStorage.setItem('sliderValue', sliderValue);
|
|
||||||
}, [sliderValue]);
|
|
||||||
|
|
||||||
const handleGalleryColumnsChange = useCallback((e) => {
|
const handleGalleryColumnsChange = useCallback((e) => {
|
||||||
const adjust = parseInt(e.target.value, 10);
|
const adjust = parseInt(e.target.value, 10);
|
||||||
setSliderValue(adjust);
|
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 handleImageExpand = useCallback(() => {
|
||||||
const adjust = Math.min(sliderValue + 1, 2);
|
const adjust = Math.min(sliderValue + 1, 2);
|
||||||
setSliderValue(adjust);
|
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]);
|
}, [sliderValue]);
|
||||||
|
|
||||||
const handleImageShrink = useCallback(() => {
|
const handleImageShrink = useCallback(() => {
|
||||||
const adjust = Math.max(sliderValue - 1, -2);
|
const adjust = Math.max(sliderValue - 1, -2);
|
||||||
setSliderValue(adjust);
|
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]);
|
}, [sliderValue]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
@@ -6,7 +6,7 @@ import { getValidSorts, CommonlyUsedHotkey } from '../../_basic';
|
|||||||
import { gettext } from '../../utils';
|
import { gettext } from '../../utils';
|
||||||
import { SortPopover } from '../popover';
|
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 [isShowSetter, setShowSetter] = useState(false);
|
||||||
|
|
||||||
const sorts = useMemo(() => {
|
const sorts = useMemo(() => {
|
||||||
@@ -54,6 +54,7 @@ const SortSetter = ({ target, sorts: propsSorts, readOnly, columns, isNeedSubmit
|
|||||||
<SortPopover
|
<SortPopover
|
||||||
isNeedSubmit={isNeedSubmit}
|
isNeedSubmit={isNeedSubmit}
|
||||||
readOnly={readOnly}
|
readOnly={readOnly}
|
||||||
|
type={type}
|
||||||
target={target}
|
target={target}
|
||||||
columns={columns}
|
columns={columns}
|
||||||
sorts={sorts}
|
sorts={sorts}
|
||||||
@@ -68,10 +69,11 @@ const SortSetter = ({ target, sorts: propsSorts, readOnly, columns, isNeedSubmit
|
|||||||
|
|
||||||
|
|
||||||
const propTypes = {
|
const propTypes = {
|
||||||
|
isNeedSubmit: PropTypes.bool,
|
||||||
readOnly: PropTypes.bool,
|
readOnly: PropTypes.bool,
|
||||||
wrapperClass: PropTypes.string,
|
wrapperClass: PropTypes.string,
|
||||||
target: PropTypes.string,
|
target: PropTypes.string,
|
||||||
isNeedSubmit: PropTypes.bool,
|
type: PropTypes.string,
|
||||||
sorts: PropTypes.array,
|
sorts: PropTypes.array,
|
||||||
columns: PropTypes.array,
|
columns: PropTypes.array,
|
||||||
modifySorts: PropTypes.func,
|
modifySorts: PropTypes.func,
|
||||||
|
@@ -0,0 +1,69 @@
|
|||||||
|
import React, { useCallback, useMemo } from 'react';
|
||||||
|
import PropTypes from 'prop-types';
|
||||||
|
import { gettext } from '../../../../utils';
|
||||||
|
import { CustomizeSelect, Icon } from '@seafile/sf-metadata-ui-component';
|
||||||
|
|
||||||
|
const OPTIONS = [
|
||||||
|
{ value: 'picture', name: gettext('Only pictures') },
|
||||||
|
{ value: 'video', name: gettext('Only videos') },
|
||||||
|
{ value: 'all', name: gettext('Pictures and videos') },
|
||||||
|
];
|
||||||
|
|
||||||
|
const FileTypeFilter = ({ readOnly, value = 'picture', onChange: onChangeAPI }) => {
|
||||||
|
|
||||||
|
const options = useMemo(() => {
|
||||||
|
return OPTIONS.map(o => {
|
||||||
|
const { name } = o;
|
||||||
|
return {
|
||||||
|
value: o.value,
|
||||||
|
label: (
|
||||||
|
<div className="select-basic-filter-option">
|
||||||
|
<div className="select-basic-filter-option-name" title={name} aria-label={name}>{name}</div>
|
||||||
|
<div className="select-basic-filter-option-check-icon">
|
||||||
|
{value === o.value && (<Icon iconName="check-mark" />)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
};
|
||||||
|
});
|
||||||
|
}, [value]);
|
||||||
|
|
||||||
|
const displayValue = useMemo(() => {
|
||||||
|
const selectedOption = OPTIONS.find(o => o.value === value) || OPTIONS[2];
|
||||||
|
return {
|
||||||
|
label: (
|
||||||
|
<div>
|
||||||
|
{selectedOption.name}
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
};
|
||||||
|
}, [value]);
|
||||||
|
|
||||||
|
const onChange = useCallback((newValue) => {
|
||||||
|
if (newValue === value) return;
|
||||||
|
onChangeAPI(newValue);
|
||||||
|
}, [value, onChangeAPI]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<CustomizeSelect
|
||||||
|
readOnly={readOnly}
|
||||||
|
className="sf-metadata-basic-filters-select"
|
||||||
|
value={displayValue}
|
||||||
|
options={options}
|
||||||
|
onSelectOption={onChange}
|
||||||
|
component={{
|
||||||
|
DropDownIcon: (
|
||||||
|
<i className="sf3-font sf3-font-down"></i>
|
||||||
|
)
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
FileTypeFilter.propTypes = {
|
||||||
|
readOnly: PropTypes.bool,
|
||||||
|
value: PropTypes.string,
|
||||||
|
onChange: PropTypes.func,
|
||||||
|
};
|
||||||
|
|
||||||
|
export default FileTypeFilter;
|
@@ -55,3 +55,7 @@
|
|||||||
.sf-metadata-basic-filters-select .sf-metadata-option-group {
|
.sf-metadata-basic-filters-select .sf-metadata-option-group {
|
||||||
margin-left: 6px;
|
margin-left: 6px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.filter-group-basic .sf-metadata-filters-list {
|
||||||
|
min-height: unset;
|
||||||
|
}
|
||||||
|
@@ -4,6 +4,7 @@ import { FormGroup, Label } from 'reactstrap';
|
|||||||
import { gettext } from '../../../../utils';
|
import { gettext } from '../../../../utils';
|
||||||
import { PRIVATE_COLUMN_KEY } from '../../../../_basic';
|
import { PRIVATE_COLUMN_KEY } from '../../../../_basic';
|
||||||
import FileOrFolderFilter from './file-folder-filter';
|
import FileOrFolderFilter from './file-folder-filter';
|
||||||
|
import FileTypeFilter from './file-type-filter';
|
||||||
|
|
||||||
import './index.css';
|
import './index.css';
|
||||||
|
|
||||||
@@ -17,27 +18,43 @@ const BasicFilters = ({ readOnly, filters = [], onChange }) => {
|
|||||||
onChange(newFilters);
|
onChange(newFilters);
|
||||||
}, [filters, onChange]);
|
}, [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 (
|
return (
|
||||||
<FormGroup className="filter-group p-4">
|
<FormGroup className="filter-group-basic filter-group p-4">
|
||||||
<Label className="filter-group-name">{gettext('Basic')}</Label>
|
<Label className="filter-group-name">{gettext('Basic')}</Label>
|
||||||
<div className="filter-group-container">
|
<div className="filter-group-container">
|
||||||
{filters.map(filter => {
|
<div className="sf-metadata-filters-list">
|
||||||
|
{filters.map((filter, index) => {
|
||||||
const { column_key, filter_term } = filter;
|
const { column_key, filter_term } = filter;
|
||||||
if (column_key === PRIVATE_COLUMN_KEY.IS_DIR) {
|
if (column_key === PRIVATE_COLUMN_KEY.IS_DIR) {
|
||||||
return (
|
return (
|
||||||
<FileOrFolderFilter key={column_key} readOnly={readOnly} value={filter_term} onChange={onChangeFileOrFolderFilter} />
|
<FileOrFolderFilter key={column_key} readOnly={readOnly} value={filter_term} onChange={onChangeFileOrFolderFilter} />
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
if (column_key === PRIVATE_COLUMN_KEY.FILE_TYPE) {
|
||||||
|
return (
|
||||||
|
<FileTypeFilter key={column_key} readOnly={readOnly} value={filter_term} onChange={onChangeFileTypeFilter} />
|
||||||
|
);
|
||||||
|
}
|
||||||
return null;
|
return null;
|
||||||
})}
|
})}
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
</FormGroup>
|
</FormGroup>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
BasicFilters.propTypes = {
|
BasicFilters.propTypes = {
|
||||||
readOnly: PropTypes.bool,
|
readOnly: PropTypes.bool,
|
||||||
value: PropTypes.string,
|
filters: PropTypes.array,
|
||||||
|
columns: PropTypes.array,
|
||||||
onChange: PropTypes.func,
|
onChange: PropTypes.func,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@@ -5,8 +5,10 @@ import { Button, UncontrolledPopover } from 'reactstrap';
|
|||||||
import { CustomizeAddTool, CustomizeSelect, Icon } from '@seafile/sf-metadata-ui-component';
|
import { CustomizeAddTool, CustomizeSelect, Icon } from '@seafile/sf-metadata-ui-component';
|
||||||
import {
|
import {
|
||||||
COLUMNS_ICON_CONFIG,
|
COLUMNS_ICON_CONFIG,
|
||||||
SORT_COLUMN_OPTIONS,
|
VIEW_SORT_COLUMN_OPTIONS,
|
||||||
|
VIEW_FIRST_SORT_COLUMN_OPTIONS,
|
||||||
SORT_TYPE,
|
SORT_TYPE,
|
||||||
|
VIEW_TYPE,
|
||||||
getColumnByKey,
|
getColumnByKey,
|
||||||
} from '../../../_basic';
|
} from '../../../_basic';
|
||||||
import { execSortsOperation, getDisplaySorts, isSortsEmpty, SORT_OPERATION } from './utils';
|
import { execSortsOperation, getDisplaySorts, isSortsEmpty, SORT_OPERATION } from './utils';
|
||||||
@@ -28,8 +30,9 @@ const SORT_TYPES = [
|
|||||||
|
|
||||||
const propTypes = {
|
const propTypes = {
|
||||||
readOnly: PropTypes.bool,
|
readOnly: PropTypes.bool,
|
||||||
target: PropTypes.string.isRequired,
|
|
||||||
isNeedSubmit: PropTypes.bool,
|
isNeedSubmit: PropTypes.bool,
|
||||||
|
target: PropTypes.string.isRequired,
|
||||||
|
type: PropTypes.string,
|
||||||
sorts: PropTypes.array,
|
sorts: PropTypes.array,
|
||||||
columns: PropTypes.array.isRequired,
|
columns: PropTypes.array.isRequired,
|
||||||
onSortComponentToggle: PropTypes.func,
|
onSortComponentToggle: PropTypes.func,
|
||||||
@@ -44,8 +47,10 @@ class SortPopover extends Component {
|
|||||||
|
|
||||||
constructor(props) {
|
constructor(props) {
|
||||||
super(props);
|
super(props);
|
||||||
const { sorts, columns } = this.props;
|
const { sorts, columns, type } = this.props;
|
||||||
this.sortTypeOptions = this.createSortTypeOptions();
|
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.columnsOptions = this.createColumnsOptions(columns);
|
||||||
this.state = {
|
this.state = {
|
||||||
sorts: getDisplaySorts(sorts, columns),
|
sorts: getDisplaySorts(sorts, columns),
|
||||||
@@ -154,7 +159,7 @@ class SortPopover extends Component {
|
|||||||
};
|
};
|
||||||
|
|
||||||
createColumnsOptions = (columns = []) => {
|
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) => {
|
return sortableColumns.map((column) => {
|
||||||
const { type, name } = column;
|
const { type, name } = column;
|
||||||
return {
|
return {
|
||||||
@@ -189,7 +194,7 @@ class SortPopover extends Component {
|
|||||||
|
|
||||||
renderSortItem = (column, sort, index) => {
|
renderSortItem = (column, sort, index) => {
|
||||||
const { name, type } = column;
|
const { name, type } = column;
|
||||||
const { readOnly } = this.props;
|
const { readOnly, type: viewType } = this.props;
|
||||||
const selectedColumn = {
|
const selectedColumn = {
|
||||||
label: (
|
label: (
|
||||||
<Fragment>
|
<Fragment>
|
||||||
@@ -205,11 +210,16 @@ class SortPopover extends Component {
|
|||||||
label: <span className="select-option-name">{selectedTypeOption?.name || gettext('Up')}</span>
|
label: <span className="select-option-name">{selectedTypeOption?.name || gettext('Up')}</span>
|
||||||
};
|
};
|
||||||
|
|
||||||
|
let columnsOptions = this.columnsOptions;
|
||||||
|
if (index === 0) {
|
||||||
|
columnsOptions = columnsOptions.filter(o => this.supportFirstSortColumnOptions.includes(o.value.column.type));
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div key={'sort-item-' + index} className="sort-item">
|
<div key={'sort-item-' + index} className="sort-item">
|
||||||
{!readOnly &&
|
{!readOnly &&
|
||||||
<div className="delete-sort" onClick={(event) => this.deleteSort(event, index)}>
|
<div className="delete-sort" onClick={!(viewType === VIEW_TYPE.GALLERY && index === 0) ? () => {} : (event) => this.deleteSort(event, index)}>
|
||||||
<Icon iconName="fork-number"/>
|
{!(viewType === VIEW_TYPE.GALLERY && index === 0) && <Icon iconName="fork-number"/>}
|
||||||
</div>
|
</div>
|
||||||
}
|
}
|
||||||
<div className="condition">
|
<div className="condition">
|
||||||
@@ -218,7 +228,7 @@ class SortPopover extends Component {
|
|||||||
readOnly={readOnly}
|
readOnly={readOnly}
|
||||||
value={selectedColumn}
|
value={selectedColumn}
|
||||||
onSelectOption={(value) => this.onSelectColumn(value, index)}
|
onSelectOption={(value) => this.onSelectColumn(value, index)}
|
||||||
options={this.columnsOptions}
|
options={columnsOptions}
|
||||||
searchable={true}
|
searchable={true}
|
||||||
searchPlaceholder={gettext('Search property')}
|
searchPlaceholder={gettext('Search property')}
|
||||||
noOptionsPlaceholder={gettext('No results')}
|
noOptionsPlaceholder={gettext('No results')}
|
||||||
|
@@ -63,6 +63,7 @@ const ViewToolBar = ({ viewId }) => {
|
|||||||
|
|
||||||
if (!view) return null;
|
if (!view) return null;
|
||||||
|
|
||||||
|
const viewType = view.type;
|
||||||
const readOnly = !window.sfMetadataContext.canModifyView(view);
|
const readOnly = !window.sfMetadataContext.canModifyView(view);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@@ -91,10 +92,11 @@ const ViewToolBar = ({ viewId }) => {
|
|||||||
target="sf-metadata-sort-popover"
|
target="sf-metadata-sort-popover"
|
||||||
readOnly={readOnly}
|
readOnly={readOnly}
|
||||||
sorts={view.sorts}
|
sorts={view.sorts}
|
||||||
|
type={viewType}
|
||||||
columns={viewColumns}
|
columns={viewColumns}
|
||||||
modifySorts={modifySorts}
|
modifySorts={modifySorts}
|
||||||
/>
|
/>
|
||||||
{view.type !== VIEW_TYPE.GALLERY && (
|
{viewType !== VIEW_TYPE.GALLERY && (
|
||||||
<GroupbySetter
|
<GroupbySetter
|
||||||
isNeedSubmit={true}
|
isNeedSubmit={true}
|
||||||
wrapperClass="sf-metadata-view-tool-operation-btn sf-metadata-view-tool-groupby"
|
wrapperClass="sf-metadata-view-tool-operation-btn sf-metadata-view-tool-groupby"
|
||||||
@@ -105,7 +107,7 @@ const ViewToolBar = ({ viewId }) => {
|
|||||||
modifyGroupbys={modifyGroupbys}
|
modifyGroupbys={modifyGroupbys}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
{view.type !== VIEW_TYPE.GALLERY && (
|
{viewType !== VIEW_TYPE.GALLERY && (
|
||||||
<HideColumnSetter
|
<HideColumnSetter
|
||||||
wrapperClass="sf-metadata-view-tool-operation-btn sf-metadata-view-tool-hide-column"
|
wrapperClass="sf-metadata-view-tool-operation-btn sf-metadata-view-tool-hide-column"
|
||||||
target="sf-metadata-hide-column-popover"
|
target="sf-metadata-hide-column-popover"
|
||||||
|
@@ -1,4 +1,4 @@
|
|||||||
.metadata-gallery-container {
|
.sf-metadata-gallery-container {
|
||||||
height: calc(100vh - 100px);
|
height: calc(100vh - 100px);
|
||||||
margin: 2px;
|
margin: 2px;
|
||||||
position: relative;
|
position: relative;
|
||||||
@@ -61,3 +61,12 @@
|
|||||||
.metadata-gallery-grid-image:hover {
|
.metadata-gallery-grid-image:hover {
|
||||||
transform: scale(1.05);
|
transform: scale(1.05);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.sf-metadata-gallery-loading-more {
|
||||||
|
height: 30px;
|
||||||
|
width: 100%;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
flex-shrink: 0;
|
||||||
|
}
|
||||||
|
@@ -1,116 +1,107 @@
|
|||||||
import React, { useMemo, useState, useEffect, useRef, useCallback } from 'react';
|
import React, { useMemo, useState, useEffect, useRef, useCallback } from 'react';
|
||||||
|
import { CenteredLoading } from '@seafile/sf-metadata-ui-component';
|
||||||
import { useMetadata } from '../../../hooks';
|
import { useMetadata } from '../../../hooks';
|
||||||
import { Utils } from '../../../../../utils/utils';
|
import { Utils } from '../../../../../utils/utils';
|
||||||
import { PRIVATE_COLUMN_KEY } from '../../../_basic';
|
import { getDateDisplayString, PRIVATE_COLUMN_KEY } from '../../../_basic';
|
||||||
import { siteRoot, thumbnailSizeForGrid } from '../../../../../utils/constants';
|
import { siteRoot, thumbnailSizeForGrid } from '../../../../../utils/constants';
|
||||||
import { EVENT_BUS_TYPE } from '../../../constants';
|
import { EVENT_BUS_TYPE, PER_LOAD_NUMBER } from '../../../constants';
|
||||||
|
import Main from './main';
|
||||||
|
import toaster from '../../../../../components/toast';
|
||||||
|
|
||||||
import './index.css';
|
import './index.css';
|
||||||
|
|
||||||
const BATCH_SIZE = 100;
|
|
||||||
const CONCURRENCY_LIMIT = 3;
|
const CONCURRENCY_LIMIT = 3;
|
||||||
|
const IMAGE_GAP = 2;
|
||||||
|
|
||||||
const Gallery = () => {
|
const 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 imageRefs = useRef([]);
|
||||||
const containerRef = useRef(null);
|
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');
|
const repoID = window.sfMetadataContext.getSetting('repoID');
|
||||||
|
|
||||||
useEffect(() => {
|
// Number of images per row
|
||||||
const handleResize = () => {
|
const columns = useMemo(() => {
|
||||||
if (containerRef.current) {
|
return 8 - zoomGear;
|
||||||
setContainerWidth(containerRef.current.offsetWidth);
|
}, [zoomGear]);
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const resizeObserver = new ResizeObserver(handleResize);
|
const imageSize = useMemo(() => {
|
||||||
const currentContainer = containerRef.current;
|
return (containerWidth - columns * 2 - 2) / columns;
|
||||||
|
}, [containerWidth, columns]);
|
||||||
|
|
||||||
if (currentContainer) {
|
const groups = useMemo(() => {
|
||||||
resizeObserver.observe(currentContainer);
|
if (isFirstLoading) return [];
|
||||||
}
|
const firstSort = metadata.view.sorts[0];
|
||||||
|
let init = metadata.rows.reduce((_init, record) => {
|
||||||
return () => {
|
const fileName = record[PRIVATE_COLUMN_KEY.FILE_NAME];
|
||||||
if (currentContainer) {
|
const parentDir = record[PRIVATE_COLUMN_KEY.PARENT_DIR];
|
||||||
resizeObserver.unobserve(currentContainer);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
window.sfMetadataContext.eventBus.subscribe(EVENT_BUS_TYPE.MODIFY_GALLERY_COLUMNS, (adjust) => {
|
|
||||||
setAdjustValue(adjust);
|
|
||||||
});
|
|
||||||
}, [columns]);
|
|
||||||
|
|
||||||
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 path = Utils.encodePath(Utils.joinPath(parentDir, fileName));
|
||||||
const date = item[PRIVATE_COLUMN_KEY.FILE_CTIME].split('T')[0];
|
const date = getDateDisplayString(record[firstSort.column_key], 'YYYY-MM-DD');
|
||||||
const src = `${siteRoot}thumbnail/${repoID}/${thumbnailSizeForGrid}${path}`;
|
const img = {
|
||||||
return {
|
|
||||||
name: fileName,
|
name: fileName,
|
||||||
url: `${siteRoot}lib/${repoID}/file${path}`,
|
url: `${siteRoot}lib/${repoID}/file${path}`,
|
||||||
src: src,
|
src: `${siteRoot}thumbnail/${repoID}/${thumbnailSizeForGrid}${path}`,
|
||||||
date: date,
|
date: date,
|
||||||
};
|
};
|
||||||
|
let _group = _init.find(g => g.name === date);
|
||||||
|
if (_group) {
|
||||||
|
_group.children.push(img);
|
||||||
|
} else {
|
||||||
|
_init.push({
|
||||||
|
name: date,
|
||||||
|
children: [img],
|
||||||
});
|
});
|
||||||
}, [metadata, repoID]);
|
}
|
||||||
|
return _init;
|
||||||
|
}, []);
|
||||||
|
|
||||||
const groupedImages = useMemo(() => {
|
let _groups = [];
|
||||||
return imageItems.reduce((acc, item) => {
|
init.forEach((_init, index) => {
|
||||||
if (!acc[item.date]) {
|
const { children } = _init;
|
||||||
acc[item.date] = [];
|
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;
|
||||||
}
|
}
|
||||||
acc[item.date].push(item);
|
_groups.push({
|
||||||
return acc;
|
..._init,
|
||||||
}, {});
|
top,
|
||||||
}, [imageItems]);
|
height,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
return _groups;
|
||||||
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||||
|
}, [isFirstLoading, metadata, metadata.recordsCount, repoID, columns, imageSize]);
|
||||||
|
|
||||||
const handleScroll = useCallback(() => {
|
const loadMore = useCallback(async () => {
|
||||||
if (visibleItems >= imageItems.length) return;
|
if (isLoadingMore) return;
|
||||||
if (containerRef.current) {
|
if (!metadata.hasMore) return;
|
||||||
const { scrollTop, scrollHeight, clientHeight } = containerRef.current;
|
setLoadingMore(true);
|
||||||
if (scrollTop + clientHeight >= scrollHeight - 10) {
|
|
||||||
setVisibleItems(prev => Math.min(prev + BATCH_SIZE, imageItems.length));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}, [visibleItems, imageItems.length]);
|
|
||||||
|
|
||||||
useEffect(() => {
|
try {
|
||||||
const container = containerRef.current;
|
await store.loadMore(PER_LOAD_NUMBER);
|
||||||
if (container) {
|
setLoadingMore(false);
|
||||||
container.addEventListener('scroll', handleScroll);
|
} catch (error) {
|
||||||
return () => container.removeEventListener('scroll', handleScroll);
|
const errorMsg = Utils.getErrorMsg(error);
|
||||||
|
toaster.danger(errorMsg);
|
||||||
|
setLoadingMore(false);
|
||||||
|
return;
|
||||||
}
|
}
|
||||||
}, [handleScroll]);
|
|
||||||
|
}, [isLoadingMore, metadata, store]);
|
||||||
|
|
||||||
const loadNextImage = useCallback(() => {
|
const loadNextImage = useCallback(() => {
|
||||||
if (loadingQueue.length === 0 || concurrentLoads >= CONCURRENCY_LIMIT) return;
|
if (loadingQueue.length === 0 || concurrentLoads >= CONCURRENCY_LIMIT) return;
|
||||||
@@ -142,16 +133,65 @@ const Gallery = () => {
|
|||||||
}, [loadingQueue, concurrentLoads, loadNextImage]);
|
}, [loadingQueue, concurrentLoads, loadNextImage]);
|
||||||
|
|
||||||
useEffect(() => {
|
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 () => {
|
return () => {
|
||||||
|
container && resizeObserver.unobserve(container);
|
||||||
|
modifyGalleryZoomGearSubscribe();
|
||||||
|
|
||||||
// Cleanup image references on unmount
|
// Cleanup image references on unmount
|
||||||
imageRefs.current.forEach(img => {
|
imageRefs.current.forEach(img => {
|
||||||
img.onload = null;
|
img.onload = null;
|
||||||
img.onerror = null;
|
img.onerror = null;
|
||||||
});
|
});
|
||||||
imageRefs.current = [];
|
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) => {
|
const addToQueue = (image) => {
|
||||||
setLoadingQueue(prev => [...prev, image]);
|
setLoadingQueue(prev => [...prev, image]);
|
||||||
loadNextImage();
|
loadNextImage();
|
||||||
@@ -159,24 +199,13 @@ const Gallery = () => {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="sf-metadata-container">
|
<div className="sf-metadata-container">
|
||||||
<div ref={containerRef} className="metadata-gallery-container">
|
<div className="sf-metadata-gallery-container" ref={containerRef} onScroll={handleScroll} >
|
||||||
{Object.keys(groupedImages).map(date => (
|
{!isFirstLoading && (
|
||||||
<div key={date} className="metadata-gallery-date-group">
|
<>
|
||||||
<div className="metadata-gallery-date-tag">{date}</div>
|
<Main groups={groups} size={imageSize} onLoad={addToQueue} columns={columns} overScan={overScan} gap={IMAGE_GAP} />
|
||||||
<ul className="metadata-gallery-image-list" style={{ gridTemplateColumns: `repeat(${columns}, 1fr)` }}>
|
{isLoadingMore && (<div className="sf-metadata-gallery-loading-more"><CenteredLoading /></div>)}
|
||||||
{groupedImages[date].slice(0, visibleItems).map((img, index) => (
|
</>
|
||||||
<li key={index} tabIndex={index} className='metadata-gallery-image-item' style={{ width: imageWidth, height: imageWidth }}>
|
)}
|
||||||
<img
|
|
||||||
className="metadata-gallery-grid-image"
|
|
||||||
src={img.src}
|
|
||||||
alt={img.name}
|
|
||||||
onLoad={() => addToQueue(img)}
|
|
||||||
/>
|
|
||||||
</li>
|
|
||||||
))}
|
|
||||||
</ul>
|
|
||||||
</div>
|
|
||||||
))}
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
@@ -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 (
|
||||||
|
<div key={name} className="metadata-gallery-date-group w-100" style={{ height }}>
|
||||||
|
{childrenStartIndex === 0 && (<div className="metadata-gallery-date-tag">{name}</div>)}
|
||||||
|
<div
|
||||||
|
className="metadata-gallery-image-list"
|
||||||
|
style={{
|
||||||
|
gridTemplateColumns: `repeat(${columns}, 1fr)`,
|
||||||
|
paddingTop: ~~(childrenStartIndex / columns) * (size + gap),
|
||||||
|
paddingBottom: ~~((children.length - 1 - childrenEndIndex) / columns) * (size + gap),
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{children.slice(childrenStartIndex, childrenEndIndex).map((img) => (
|
||||||
|
<div key={img.src} tabIndex={1} className='metadata-gallery-image-item' style={{ width: size, height: size }}>
|
||||||
|
<img
|
||||||
|
className="metadata-gallery-grid-image"
|
||||||
|
src={img.src}
|
||||||
|
alt={img.name}
|
||||||
|
onLoad={() => onLoad(img)}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}, [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;
|
@@ -51,5 +51,5 @@ export const EVENT_BUS_TYPE = {
|
|||||||
ERROR: 'error',
|
ERROR: 'error',
|
||||||
|
|
||||||
// gallery
|
// gallery
|
||||||
MODIFY_GALLERY_COLUMNS: 'modify_gallery_columns',
|
MODIFY_GALLERY_ZOOM_GEAR: 'modify_gallery_zoom_gear',
|
||||||
};
|
};
|
||||||
|
@@ -17,7 +17,7 @@ export const MetadataProvider = ({
|
|||||||
...params
|
...params
|
||||||
}) => {
|
}) => {
|
||||||
const [isLoading, setLoading] = useState(true);
|
const [isLoading, setLoading] = useState(true);
|
||||||
const [metadata, setMetadata] = useState({ rows: [], columns: [] });
|
const [metadata, setMetadata] = useState({ rows: [], columns: [], view: {} });
|
||||||
const storeRef = useRef(null);
|
const storeRef = useRef(null);
|
||||||
const { collaborators } = useCollaborators();
|
const { collaborators } = useCollaborators();
|
||||||
const { showFirstView, setShowFirstView } = usePropsMetadata();
|
const { showFirstView, setShowFirstView } = usePropsMetadata();
|
||||||
|
@@ -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 {
|
class View {
|
||||||
constructor(object, columns) {
|
constructor(object, columns) {
|
||||||
@@ -14,7 +14,7 @@ class View {
|
|||||||
this.filters = object.filters || [];
|
this.filters = object.filters || [];
|
||||||
this.filter_conjunction = object.filter_conjunction || 'Or';
|
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
|
// sort
|
||||||
this.sorts = object.sorts || [];
|
this.sorts = object.sorts || [];
|
||||||
|
@@ -550,6 +550,8 @@ class LibContentView extends React.Component {
|
|||||||
content: '',
|
content: '',
|
||||||
viewId: '',
|
viewId: '',
|
||||||
isDirentDetailShow: false
|
isDirentDetailShow: false
|
||||||
|
}, () => {
|
||||||
|
this.showDir('/');
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@@ -101,7 +101,7 @@ class MetadataManage(APIView):
|
|||||||
task_id = add_init_metadata_task(params=params)
|
task_id = add_init_metadata_task(params=params)
|
||||||
metadata_view = RepoMetadataViews.objects.filter(repo_id=repo_id).first()
|
metadata_view = RepoMetadataViews.objects.filter(repo_id=repo_id).first()
|
||||||
if not metadata_view:
|
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:
|
except Exception as e:
|
||||||
logger.error(e)
|
logger.error(e)
|
||||||
return api_error(status.HTTP_500_INTERNAL_SERVER_ERROR, 'Internal Server Error')
|
return api_error(status.HTTP_500_INTERNAL_SERVER_ERROR, 'Internal Server Error')
|
||||||
@@ -576,6 +576,7 @@ class MetadataViews(APIView):
|
|||||||
# 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')
|
view_type = request.data.get('type', 'table')
|
||||||
|
view_data = request.data.get('data', {})
|
||||||
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)
|
||||||
@@ -596,7 +597,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, view_type)
|
new_view = RepoMetadataViews.objects.add_view(repo_id, view_name, view_type, view_data)
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
logger.exception(e)
|
logger.exception(e)
|
||||||
error_msg = 'Internal Server Error'
|
error_msg = 'Internal Server Error'
|
||||||
|
@@ -62,9 +62,10 @@ class RepoMetadata(models.Model):
|
|||||||
|
|
||||||
class RepoView(object):
|
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.name = name
|
||||||
self.type = type
|
self.type = type
|
||||||
|
self.view_data = view_data
|
||||||
self.view_json = {}
|
self.view_json = {}
|
||||||
|
|
||||||
self.init_view(view_ids)
|
self.init_view(view_ids)
|
||||||
@@ -81,14 +82,18 @@ class RepoView(object):
|
|||||||
"hidden_columns": [],
|
"hidden_columns": [],
|
||||||
"type": self.type,
|
"type": self.type,
|
||||||
}
|
}
|
||||||
|
self.view_json.update(self.view_data)
|
||||||
|
|
||||||
|
|
||||||
class RepoMetadataViewsManager(models.Manager):
|
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()
|
metadata_views = self.filter(repo_id=repo_id).first()
|
||||||
if not metadata_views:
|
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_json = new_view.view_json
|
||||||
view_id = view_json.get('_id')
|
view_id = view_json.get('_id')
|
||||||
view_details = {
|
view_details = {
|
||||||
@@ -103,7 +108,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, view_type, exist_view_ids)
|
new_view = RepoView(view_name, view_type, view_data, 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)
|
||||||
|
Reference in New Issue
Block a user