1
0
mirror of https://github.com/haiwen/seahub.git synced 2025-08-02 07:47:32 +00:00

feat: metadata basic filter (#6633)

* feat: metadata basic filter

* feat: update code

* feat: update code

---------

Co-authored-by: 杨国璇 <ygx@192.168.124.9>
Co-authored-by: 杨国璇 <ygx@Hello-word.local>
This commit is contained in:
杨国璇 2024-08-26 16:21:23 +08:00 committed by GitHub
parent 50035219bb
commit 2ec3fc7a51
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
16 changed files with 258 additions and 58 deletions

View File

@ -10,6 +10,7 @@ export const NOT_DISPLAY_COLUMN_KEYS = [
PRIVATE_COLUMN_KEY.SUFFIX,
PRIVATE_COLUMN_KEY.FILE_DETAILS,
PRIVATE_COLUMN_KEY.LOCATION,
PRIVATE_COLUMN_KEY.IS_DIR,
];
export const VIEW_NOT_DISPLAY_COLUMN_KEYS = [

View File

@ -18,6 +18,7 @@ const FilterSetter = ({
filtersClassName,
target,
filterConjunction,
basicFilters,
modifyFilters
}) => {
const [isShowSetter, setShowSetter] = useState(false);
@ -26,12 +27,15 @@ const FilterSetter = ({
return deepCopy(getValidFilters(propsFilters || [], columns));
}, [propsFilters, columns]);
const filtersCount = useMemo(() => {
return filters.length + basicFilters.length;
}, [filters, basicFilters]);
const message = useMemo(() => {
const filtersLength = filters.length;
if (filtersLength === 1) return isNeedSubmit ? gettext('1 preset filter') : gettext('1 filter');
if (filtersLength > 1) return filtersLength + ' ' + (isNeedSubmit ? gettext('Preset filters') : gettext('Filters'));
if (filtersCount === 1) return isNeedSubmit ? gettext('1 preset filter') : gettext('1 filter');
if (filtersCount > 1) return filtersCount + ' ' + (isNeedSubmit ? gettext('Preset filters') : gettext('Filters'));
return isNeedSubmit ? gettext('Preset filter') : gettext('Filter');
}, [isNeedSubmit, filters]);
}, [isNeedSubmit, filtersCount]);
const onSetterToggle = useCallback(() => {
setShowSetter(!isShowSetter);
@ -43,13 +47,13 @@ const FilterSetter = ({
}, [onSetterToggle]);
const onChange = useCallback((update) => {
const { filters, filter_conjunction } = update || {};
const { filters, filter_conjunction, basic_filters } = update || {};
const validFilters = getValidFilters(filters, columns);
modifyFilters(validFilters, filter_conjunction);
modifyFilters(validFilters, filter_conjunction, basic_filters);
}, [columns, modifyFilters]);
if (!columns) return null;
const className = classnames(wrapperClass, { 'active': filters.length > 0 });
const className = classnames(wrapperClass, { 'active': filtersCount > 0 });
return (
<>
<IconBtn
@ -75,6 +79,7 @@ const FilterSetter = ({
collaborators={collaborators}
filterConjunction={filterConjunction}
filters={filters}
basicFilters={basicFilters}
hidePopover={onSetterToggle}
update={onChange}
isPre={isPre}
@ -97,11 +102,13 @@ FilterSetter.propTypes = {
modifyFilters: PropTypes.func,
collaborators: PropTypes.array,
isPre: PropTypes.bool,
basicFilters: PropTypes.array,
};
FilterSetter.defaultProps = {
target: 'sf-metadata-filter-popover',
isNeedSubmit: false,
basicFilters: [],
};
export default FilterSetter;

View File

@ -0,0 +1,64 @@
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: 'file', name: gettext('Only files') },
{ value: 'folder', name: gettext('Only folders') },
{ value: 'all', name: gettext('Files and folders') },
];
const FileOrFolderFilter = ({ readOnly, value = 'all', 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}
/>
);
};
FileOrFolderFilter.propTypes = {
readOnly: PropTypes.bool,
value: PropTypes.string,
onChange: PropTypes.func,
};
export default FileOrFolderFilter;

View File

@ -0,0 +1,30 @@
.sf-metadata-basic-filters-select {
width: 200px;
}
.select-basic-filter-option {
width: 100%;
display: flex;
align-items: center;
}
.select-basic-filter-option .select-basic-filter-option-name {
flex: 1;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.select-basic-filter-option .select-basic-filter-option-check-icon {
display: inline-flex;
align-items: center;
height: 20px;
width: 20px;
text-align: center;
flex-shrink: 0;
margin-left: 10px;
}
.sf-metadata-option.sf-metadata-option-active .sf-metadata-icon-check-mark {
fill: #fff;
}

View File

@ -0,0 +1,44 @@
import React, { useCallback } from 'react';
import PropTypes from 'prop-types';
import { FormGroup, Label } from 'reactstrap';
import { gettext } from '../../../../utils';
import { PRIVATE_COLUMN_KEY } from '../../../../_basic';
import FileOrFolderFilter from './file-folder-filter';
import './index.css';
const BasicFilters = ({ readOnly, filters = [], onChange }) => {
const onChangeFileOrFolderFilter = useCallback((newValue) => {
const filterIndex = filters.findIndex(filter => filter.column_key === PRIVATE_COLUMN_KEY.IS_DIR);
const filter = filters[filterIndex];
const newFilters = filters.slice(0);
newFilters[filterIndex] = { ...filter, filter_term: newValue };
onChange(newFilters);
}, [filters, onChange]);
return (
<FormGroup className="filter-group p-4">
<Label className="filter-group-name">{gettext('Basic')}</Label>
<div className="filter-group-container">
{filters.map(filter => {
const { column_key, filter_term } = filter;
if (column_key === PRIVATE_COLUMN_KEY.IS_DIR) {
return (
<FileOrFolderFilter key={column_key} readOnly={readOnly} value={filter_term} onChange={onChangeFileOrFolderFilter} />
);
}
return null;
})}
</div>
</FormGroup>
);
};
BasicFilters.propTypes = {
readOnly: PropTypes.bool,
value: PropTypes.string,
onChange: PropTypes.func,
};
export default BasicFilters;

View File

@ -2,3 +2,39 @@
max-width: none;
min-width: 300px;
}
.sf-metadata-filter-popover .sf-metadata-filters {
display: flex;
flex-direction: column;
min-width: 400px;
}
.sf-metadata-filter-popover .filter-group-name {
margin-bottom: 10px;
}
.sf-metadata-filter-popover .filter-group-advanced {
flex: 1;
display: flex;
flex-direction: column;
}
.sf-metadata-filter-popover .filter-group-advanced .filter-group-name {
padding: 0 16px;
}
.sf-metadata-filter-popover .filter-group-advanced .filter-group-container {
flex: 1;
padding: 0 16px;
}
.sf-metadata-filter-popover .add-item-btn.popover-add-tool {
height: 28px;
padding: 0 6px;
width: fit-content;
max-width: 100%;
margin-bottom: 8px;
border-radius: 3px;
font-weight: 400;
margin-left: -6px;
}

View File

@ -1,7 +1,7 @@
import React, { Component } from 'react';
import PropTypes from 'prop-types';
import isHotkey from 'is-hotkey';
import { Button, UncontrolledPopover } from 'reactstrap';
import { Button, FormGroup, Label, UncontrolledPopover } from 'reactstrap';
import { CustomizeAddTool } from '@seafile/sf-metadata-ui-component';
import {
FILTER_COLUMN_OPTIONS,
@ -11,6 +11,7 @@ import { getEventClassName, gettext } from '../../../utils';
import { getFilterByColumn } from '../../../utils/filters-utils';
import FiltersList from './widgets';
import { EVENT_BUS_TYPE } from '../../../constants';
import BasicFilters from './basic-filters';
import './index.css';
@ -32,6 +33,7 @@ class FilterPopover extends Component {
constructor(props) {
super(props);
this.state = {
basicFilters: props.basicFilters,
filters: getValidFilters(props.filters, props.columns),
filterConjunction: props.filterConjunction || 'And',
};
@ -97,7 +99,7 @@ class FilterPopover extends Component {
this.update(filters);
};
updateFilterConjunction = (conjunction) => {
modifyFilterConjunction = (conjunction) => {
if (this.props.isNeedSubmit) {
const isSubmitDisabled = false;
this.setState({ filterConjunction: conjunction, isSubmitDisabled });
@ -130,8 +132,8 @@ class FilterPopover extends Component {
};
onSubmitFilters = () => {
const { filters, filterConjunction } = this.state;
const update = { filters, filter_conjunction: filterConjunction };
const { filters, filterConjunction, basicFilters } = this.state;
const update = { filters, filter_conjunction: filterConjunction, basic_filters: basicFilters };
this.props.update(update);
this.props.hidePopover();
};
@ -140,9 +142,21 @@ class FilterPopover extends Component {
e.stopPropagation();
};
onBasicFilterChange = (value) => {
if (this.props.isNeedSubmit) {
const isSubmitDisabled = false;
this.setState({ basicFilters: value, isSubmitDisabled });
return;
}
this.setState({ basicFilters: value }, () => {
const update = { filters: this.state.filters, filter_conjunction: this.state.filterConjunction, basic_filters: value };
this.props.update(update);
});
};
render() {
const { readOnly, target, columns, placement } = this.props;
const { filters, filterConjunction } = this.state;
const { filters, filterConjunction, basicFilters } = this.state;
const canAddFilter = columns.length > 0;
return (
<UncontrolledPopover
@ -151,32 +165,38 @@ class FilterPopover extends Component {
target={target}
fade={false}
hideArrow={true}
className=" sf-metadata-filter-popover"
className="sf-metadata-filter-popover"
boundariesElement={document.body}
>
{({ scheduleUpdate }) => (
<div ref={ref => this.dtablePopoverRef = ref} onClick={this.onPopoverInsideClick} className={this.props.filtersClassName}>
<FiltersList
filterConjunction={filterConjunction}
filters={filters}
columns={columns}
emptyPlaceholder={gettext('No filters')}
updateFilter={this.updateFilter}
deleteFilter={this.deleteFilter}
updateFilterConjunction={this.updateFilterConjunction}
collaborators={this.props.collaborators}
readOnly={readOnly}
scheduleUpdate={scheduleUpdate}
isPre={this.props.isPre}
/>
{!readOnly && (
<CustomizeAddTool
className={`popover-add-tool ${canAddFilter ? '' : 'disabled'}`}
callBack={canAddFilter ? () => this.addFilter(scheduleUpdate) : () => {}}
footerName={gettext('Add filter')}
addIconClassName="popover-add-icon"
/>
)}
<BasicFilters filters={basicFilters} onChange={this.onBasicFilterChange} />
<FormGroup className="filter-group-advanced filter-group mb-0">
<Label className="filter-group-name">{gettext('Advanced')}</Label>
<div className="filter-group-container">
<FiltersList
filterConjunction={filterConjunction}
filters={filters}
columns={columns}
emptyPlaceholder={gettext('No filters')}
updateFilter={this.updateFilter}
deleteFilter={this.deleteFilter}
modifyFilterConjunction={this.modifyFilterConjunction}
collaborators={this.props.collaborators}
readOnly={readOnly}
scheduleUpdate={scheduleUpdate}
isPre={this.props.isPre}
/>
{!readOnly && (
<CustomizeAddTool
className={`popover-add-tool ${canAddFilter ? '' : 'disabled'}`}
callBack={canAddFilter ? () => this.addFilter(scheduleUpdate) : () => {}}
footerName={gettext('Add filter')}
addIconClassName="popover-add-icon"
/>
)}
</div>
</FormGroup>
{!readOnly && this.props.isNeedSubmit && (
<div className="sf-metadata-popover-footer">
<Button className='mr-2' onClick={this.onClosePopover}>{gettext('Cancel')}</Button>
@ -201,6 +221,7 @@ FilterPopover.propTypes = {
filters: PropTypes.array,
collaborators: PropTypes.array,
isPre: PropTypes.bool,
basicFilters: PropTypes.array,
hidePopover: PropTypes.func,
update: PropTypes.func,
};

View File

@ -1,12 +1,11 @@
.sf-metadata-filters-list {
min-height: 120px;
max-height: 100%;
padding: 15px;
}
.sf-metadata-filters-list.empty-filters-container {
min-height: 80px;
padding: 16px;
padding: 0;
}
.sf-metadata-filters-list.empty-filters-container .empty-filters-list {
@ -230,7 +229,7 @@
.sf-metadata-filters-list .multiple-check-icon .sf-metadata-icon-check-mark,
.sf-metadata-filters-list .collaborator-check-icon .sf-metadata-icon-check-mark {
fill: #798d99;
fill: #798d99 !important;
font-size: 12px;
}

View File

@ -2,7 +2,6 @@ import React, { Component } from 'react';
import classnames from 'classnames';
import PropTypes from 'prop-types';
import {
VIEW_NOT_DISPLAY_COLUMN_KEYS,
FILTER_COLUMN_OPTIONS,
ValidateFilter,
getColumnByKey,
@ -20,7 +19,7 @@ const propTypes = {
filterConjunction: PropTypes.string.isRequired,
updateFilter: PropTypes.func.isRequired,
deleteFilter: PropTypes.func.isRequired,
updateFilterConjunction: PropTypes.func,
modifyFilterConjunction: PropTypes.func,
emptyPlaceholder: PropTypes.string,
value: PropTypes.object,
collaborators: PropTypes.array,
@ -53,7 +52,7 @@ class FiltersList extends Component {
};
updateConjunction = (filterConjunction) => {
this.props.updateFilterConjunction(filterConjunction);
this.props.modifyFilterConjunction(filterConjunction);
};
getConjunctionOptions = () => {
@ -66,8 +65,7 @@ class FiltersList extends Component {
getFilterColumns = () => {
const { columns } = this.props;
return columns.filter(column => {
let { type, key } = column;
if (VIEW_NOT_DISPLAY_COLUMN_KEYS.includes(key)) return false;
let { type } = column;
return Object.prototype.hasOwnProperty.call(FILTER_COLUMN_OPTIONS, type);
});
};

View File

@ -100,8 +100,8 @@ const Container = () => {
return { rowIdsInOrder, upperRowIds, belowRowIds };
}, [metadata]);
const modifyFilters = useCallback((filters, filterConjunction) => {
store.modifyFilters(filterConjunction, filters);
const modifyFilters = useCallback((filters, filterConjunction, basicFilters) => {
store.modifyFilters(filterConjunction, filters, basicFilters);
}, [store]);
const modifySorts = useCallback((sorts) => {

View File

@ -9,11 +9,6 @@ const ViewToolBar = ({ viewId }) => {
const [view, setView] = useState(null);
const [collaborators, setCollaborators] = useState([]);
const availableColumns = useMemo(() => {
if (!view) return [];
return view.available_columns;
}, [view]);
const viewColumns = useMemo(() => {
if (!view) return [];
return view.columns;
@ -23,8 +18,8 @@ const ViewToolBar = ({ viewId }) => {
window.sfMetadataContext.eventBus.dispatch(EVENT_BUS_TYPE.SELECT_NONE);
}, []);
const modifyFilters = useCallback((filters, filterConjunction) => {
window.sfMetadataContext.eventBus.dispatch(EVENT_BUS_TYPE.MODIFY_FILTERS, filters, filterConjunction);
const modifyFilters = useCallback((filters, filterConjunction, basicFilters) => {
window.sfMetadataContext.eventBus.dispatch(EVENT_BUS_TYPE.MODIFY_FILTERS, filters, filterConjunction, basicFilters);
}, []);
const modifySorts = useCallback((sorts) => {
@ -83,8 +78,9 @@ const ViewToolBar = ({ viewId }) => {
target="sf-metadata-filter-popover"
readOnly={readOnly}
filterConjunction={view.filter_conjunction}
basicFilters={view.basic_filters}
filters={view.filters}
columns={availableColumns}
columns={viewColumns}
modifyFilters={modifyFilters}
collaborators={collaborators}
/>

View File

@ -1,4 +1,4 @@
import { getColumnByKey, VIEW_NOT_DISPLAY_COLUMN_KEYS } from '../../_basic';
import { getColumnByKey, VIEW_NOT_DISPLAY_COLUMN_KEYS, PRIVATE_COLUMN_KEY, FILTER_PREDICATE_TYPE } from '../../_basic';
class View {
constructor(object, columns) {
@ -10,6 +10,8 @@ class View {
this.filters = object.filters || [];
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' }];
// sort
this.sorts = object.sorts || [];

View File

@ -344,12 +344,13 @@ class Store {
this.applyOperation(operation);
}
modifyFilters(filterConjunction, filters) {
modifyFilters(filterConjunction, filters, basicFilters = []) {
const type = OPERATION_TYPE.MODIFY_FILTERS;
const operation = this.createOperation({
type,
filter_conjunction: filterConjunction,
filters,
basic_filters: basicFilters,
repo_id: this.repoId,
view_id: this.viewId,
success_callback: () => {

View File

@ -101,9 +101,10 @@ export default function apply(data, operation) {
return data;
}
case OPERATION_TYPE.MODIFY_FILTERS: {
const { filter_conjunction, filters } = operation;
const { filter_conjunction, filters, basic_filters } = operation;
data.view.filter_conjunction = filter_conjunction;
data.view.filters = filters;
data.view.basic_filters = basic_filters;
return data;
}
case OPERATION_TYPE.MODIFY_SORTS: {

View File

@ -24,7 +24,7 @@ export const OPERATION_ATTRIBUTES = {
[OPERATION_TYPE.MODIFY_RECORDS]: ['repo_id', 'row_ids', 'id_row_updates', 'id_original_row_updates', 'id_old_row_data', 'id_original_old_row_data', 'is_copy_paste', 'id_obj_id'],
[OPERATION_TYPE.RESTORE_RECORDS]: ['repo_id', 'rows_data', 'original_rows', 'link_infos', 'upper_row_ids'],
[OPERATION_TYPE.RELOAD_RECORDS]: ['repo_id', 'row_ids'],
[OPERATION_TYPE.MODIFY_FILTERS]: ['repo_id', 'view_id', 'filter_conjunction', 'filters'],
[OPERATION_TYPE.MODIFY_FILTERS]: ['repo_id', 'view_id', 'filter_conjunction', 'filters', 'basic_filters'],
[OPERATION_TYPE.MODIFY_SORTS]: ['repo_id', 'view_id', 'sorts'],
[OPERATION_TYPE.MODIFY_GROUPBYS]: ['repo_id', 'view_id', 'groupbys'],
[OPERATION_TYPE.MODIFY_HIDDEN_COLUMNS]: ['repo_id', 'view_id', 'hidden_columns'],

View File

@ -107,8 +107,8 @@ class ServerOperator {
break;
}
case OPERATION_TYPE.MODIFY_FILTERS: {
const { repo_id, view_id, filter_conjunction, filters } = operation;
window.sfMetadataContext.modifyView(repo_id, view_id, { filters, filter_conjunction }).then(res => {
const { repo_id, view_id, filter_conjunction, filters, basic_filters } = operation;
window.sfMetadataContext.modifyView(repo_id, view_id, { filters, filter_conjunction, basic_filters }).then(res => {
callback({ operation });
}).catch(error => {
callback({ error: gettext('Failed to modify filter') });