1
0
mirror of https://github.com/haiwen/seahub.git synced 2025-04-27 19:05:16 +00:00

optimize filters

This commit is contained in:
zhouwenxuan 2025-04-22 18:33:06 +08:00
parent bb09dc0222
commit 92f4256c89
6 changed files with 153 additions and 141 deletions

View File

@ -11,7 +11,7 @@ import ModalPortal from '../../modal-portal';
import toaster from '../../toast';
import { SEARCH_FILTERS_KEY } from '../../../constants';
const FilterByCreator = ({ creatorList, onSelect }) => {
const FilterByCreator = ({ creatorList, onChange }) => {
const [isOpen, setIsOpen] = useState(false);
const [options, setOptions] = useState([]);
const [selectedOptions, setSelectedOptions] = useState(creatorList || []);
@ -29,7 +29,7 @@ const FilterByCreator = ({ creatorList, onSelect }) => {
});
}, [options, searchValue]);
const onSelectOption = useCallback((e) => {
const onChangeOption = useCallback((e) => {
e.preventDefault();
e.stopPropagation();
const name = Utils.getEventData(e, 'toggle') ?? e.currentTarget.getAttribute('data-toggle');
@ -41,17 +41,17 @@ const FilterByCreator = ({ creatorList, onSelect }) => {
updated = updated.filter((option) => option.name !== name);
}
setSelectedOptions(updated);
onSelect(SEARCH_FILTERS_KEY.CREATOR_LIST, updated);
onChange(SEARCH_FILTERS_KEY.CREATOR_LIST, updated);
if (displayOptions.length === 1) {
setSearchValue('');
}
}, [selectedOptions, displayOptions, options, onSelect]);
}, [selectedOptions, displayOptions, options, onChange]);
const handleCancel = useCallback((e, name) => {
const updated = selectedOptions.filter((option) => option.name !== name);
setSelectedOptions(updated);
onSelect(SEARCH_FILTERS_KEY.CREATOR_LIST, updated);
}, [selectedOptions, onSelect]);
onChange(SEARCH_FILTERS_KEY.CREATOR_LIST, updated);
}, [selectedOptions, onChange]);
const handleInputChange = useCallback((e) => {
const v = e.target.value;
@ -130,7 +130,7 @@ const FilterByCreator = ({ creatorList, onSelect }) => {
tabIndex="-1"
data-toggle={option.name}
onMouseDown={(e) => e.preventDefault()}
onClick={onSelectOption}
onClick={onChangeOption}
toggle={false}
>
{isOpen && <UserItem user={option} />}
@ -146,7 +146,7 @@ const FilterByCreator = ({ creatorList, onSelect }) => {
FilterByCreator.propTypes = {
creatorList: PropTypes.array.isRequired,
onSelect: PropTypes.func.isRequired,
onChange: PropTypes.func.isRequired,
};
export default FilterByCreator;

View File

@ -1,4 +1,4 @@
import React, { useCallback, useMemo, useState } from 'react';
import React, { useCallback, useEffect, useMemo, useState } from 'react';
import PropTypes from 'prop-types';
import { Dropdown, DropdownItem, DropdownMenu, DropdownToggle } from 'reactstrap';
import dayjs from 'dayjs';
@ -10,11 +10,15 @@ import { SEARCH_FILTERS_KEY, SEARCH_FILTER_BY_DATE_OPTION_KEY, SEARCH_FILTER_BY_
const DATE_INPUT_WIDTH = 118;
const FilterByDate = ({ date, onSelect }) => {
const FilterByDate = ({ date, onChange }) => {
const [value, setValue] = useState(date.value);
const [isOpen, setIsOpen] = useState(false);
const [isTypeOpen, setIsTypeOpen] = useState(false);
const [isCustomDate, setIsCustomDate] = useState(date.value === SEARCH_FILTER_BY_DATE_OPTION_KEY.CUSTOM);
const [time, setTime] = useState({
from: date.from,
to: date.to,
});
const [type, setType] = useState(date.type);
const typeLabel = useMemo(() => {
@ -54,13 +58,13 @@ const FilterByDate = ({ date, onSelect }) => {
case SEARCH_FILTER_BY_DATE_OPTION_KEY.LAST_30_DAYS:
return `${prefix}${formatDate(today.subtract(29, 'day'))} - ${formatDate(today)}`;
case SEARCH_FILTER_BY_DATE_OPTION_KEY.CUSTOM:
return date.start && date.end
? `${prefix}${formatDate(date.start)} - ${formatDate(date.end)}`
return time.from && time.to
? `${prefix}${formatDate(time.from)} - ${formatDate(time.to)}`
: gettext('Select date range');
default:
return gettext('Date');
}
}, [date, value, typeLabel]);
}, [time, value, typeLabel]);
const options = useMemo(() => {
return [
@ -90,93 +94,91 @@ const FilterByDate = ({ date, onSelect }) => {
const option = Utils.getEventData(e, 'toggle') ?? e.currentTarget.getAttribute('data-toggle');
if (option === type) return;
setType(option);
onSelect(SEARCH_FILTERS_KEY.DATE, {
...date,
type: option,
});
}, [type, onSelect, date]);
}, [type]);
const onClearDate = useCallback(() => {
setValue('');
setIsCustomDate(false);
onSelect(SEARCH_FILTERS_KEY.DATE, {
type: SEARCH_FILTER_BY_DATE_TYPE_KEY.CREATE_TIME,
value: '',
start: null,
end: null,
setTime({
from: null,
to: null,
});
}, [onSelect]);
setIsOpen(false);
}, []);
const onOptionClick = useCallback((e) => {
const option = Utils.getEventData(e, 'toggle') ?? e.currentTarget.getAttribute('data-toggle');
if (option === value) return;
const today = dayjs().endOf('day');
setIsCustomDate(option === SEARCH_FILTER_BY_DATE_OPTION_KEY.CUSTOM);
const isCustomOption = option === SEARCH_FILTER_BY_DATE_OPTION_KEY.CUSTOM;
setIsCustomDate(isCustomOption);
setValue(option);
setIsOpen(isCustomOption);
switch (option) {
case SEARCH_FILTER_BY_DATE_OPTION_KEY.TODAY: {
onSelect(SEARCH_FILTERS_KEY.DATE, {
value: option,
start: dayjs().startOf('day').unix(),
end: today.unix()
setTime({
from: dayjs().startOf('day').unix(),
to: today.unix()
});
break;
}
case SEARCH_FILTER_BY_DATE_OPTION_KEY.LAST_7_DAYS: {
onSelect(SEARCH_FILTERS_KEY.DATE, {
value: option,
start: dayjs().subtract(6, 'day').startOf('day').unix(),
end: today.unix()
setTime({
from: dayjs().subtract(6, 'day').startOf('day').unix(),
to: today.unix()
});
break;
}
case SEARCH_FILTER_BY_DATE_OPTION_KEY.LAST_30_DAYS: {
onSelect(SEARCH_FILTERS_KEY.DATE, {
value: option,
start: dayjs().subtract(30, 'day').startOf('day').unix(),
end: today.unix()
setTime({
from: dayjs().subtract(30, 'day').startOf('day').unix(),
to: today.unix()
});
break;
}
case SEARCH_FILTER_BY_DATE_OPTION_KEY.CUSTOM: {
onSelect(SEARCH_FILTERS_KEY.DATE, {
value: option,
start: null,
end: null,
setTime({
from: null,
to: null,
});
break;
}
}
}, [value, onSelect]);
}, [value]);
const disabledStartDate = useCallback((startDate) => {
if (!startDate) return false;
const today = dayjs();
const endValue = date.end;
const endValue = time.to;
if (!endValue) {
return startDate.isAfter(today);
}
return endValue.isBefore(startDate) || startDate.isAfter(today);
}, [date]);
}, [time]);
const disabledEndDate = useCallback((endDate) => {
if (!endDate) return false;
const today = dayjs();
const startValue = date.start;
const startValue = time.from;
if (!startValue) {
return endDate.isAfter(today);
}
return endDate.isBefore(startValue) || endDate.isAfter(today);
}, [date]);
}, [time]);
const onChangeCustomDate = useCallback((customDate) => {
const newDate = {
...date,
...customDate,
};
onSelect(SEARCH_FILTERS_KEY.DATE, newDate);
}, [date, onSelect]);
useEffect(() => {
if (!isOpen) {
if (type !== date.type || time.from !== date.from || time.to !== date.to) {
onChange(SEARCH_FILTERS_KEY.DATE, {
type,
value,
from: time.from,
to: time.to,
});
}
}
}, [isOpen, date, time, type, value, onChange]);
return (
<div className="search-filter filter-by-date-container">
@ -246,8 +248,8 @@ const FilterByDate = ({ date, onSelect }) => {
<Picker
showHourAndMinute={false}
disabledDate={disabledStartDate}
value={date.start}
onChange={(value) => onChangeCustomDate({ start: value })}
value={time.from}
onChange={(value) => setTime({ ...time, from: value })}
inputWidth={DATE_INPUT_WIDTH}
/>
</div>
@ -256,8 +258,8 @@ const FilterByDate = ({ date, onSelect }) => {
<Picker
showHourAndMinute={false}
disabledDate={disabledEndDate}
value={date.end}
onChange={(value) => onChangeCustomDate({ end: value })}
value={time.to}
onChange={(value) => setTime({ ...time, to: value })}
inputWidth={DATE_INPUT_WIDTH}
/>
</div>
@ -277,7 +279,7 @@ FilterByDate.propTypes = {
start: PropTypes.oneOfType([PropTypes.number, PropTypes.object]),
end: PropTypes.oneOfType([PropTypes.number, PropTypes.object]),
}),
onSelect: PropTypes.func.isRequired,
onChange: PropTypes.func.isRequired,
};
export default FilterByDate;

View File

@ -1,4 +1,4 @@
import React, { useCallback, useRef, useState } from 'react';
import React, { useCallback, useEffect, useRef, useState } from 'react';
import { Dropdown, DropdownMenu, DropdownToggle } from 'reactstrap';
import PropTypes from 'prop-types';
import classNames from 'classnames';
@ -6,18 +6,16 @@ import { gettext } from '../../../utils/constants';
import ModalPortal from '../../modal-portal';
import { SEARCH_FILTERS_KEY } from '../../../constants';
const FilterBySuffix = ({ suffixes, onSelect }) => {
const FilterBySuffix = ({ suffixes, onChange }) => {
const [isOpen, setIsOpen] = useState(false);
const [inputValue, setInputValue] = useState(suffixes.join(', '));
const [inputValue, setInputValue] = useState(suffixes);
const inputRef = useRef(null);
const toggle = useCallback(() => setIsOpen(!isOpen), [isOpen]);
const handleInput = useCallback((e) => {
setInputValue(e.target.value);
const suffixes = e.target.value.split(',').map(suffix => suffix.trim()).filter(Boolean);
onSelect(SEARCH_FILTERS_KEY.SUFFIXES, suffixes);
}, [onSelect]);
}, []);
const handleKeyDown = useCallback((e) => {
e.stopPropagation();
@ -26,12 +24,23 @@ const FilterBySuffix = ({ suffixes, onSelect }) => {
}
}, []);
const handleClearInput = useCallback(() => {
setInputValue('');
setIsOpen(false);
}, []);
useEffect(() => {
if (!isOpen && inputValue !== suffixes) {
onChange(SEARCH_FILTERS_KEY.SUFFIXES, inputValue.replace(/\./g, ''));
}
}, [isOpen, inputValue, suffixes, onChange]);
return (
<div className="search-filter filter-by-suffix-container">
<Dropdown isOpen={isOpen} toggle={toggle}>
<DropdownToggle tag="div" className={classNames('search-filter-toggle', {
'active': isOpen && suffixes.length > 0,
'highlighted': suffixes.length > 0,
'active': isOpen && inputValue.length > 0,
'highlighted': inputValue.length > 0,
})} onClick={toggle}>
<div className="filter-label" title={gettext('File suffix')}>{gettext('File suffix')}</div>
<i className="sf3-font sf3-font-down pl-1" />
@ -42,7 +51,7 @@ const FilterBySuffix = ({ suffixes, onSelect }) => {
ref={inputRef}
type="text"
className="form-control"
placeholder={gettext('Seperate multiple suffixes by ","(like .sdoc, .pdf)')}
placeholder={gettext('Seperate multiple suffixes by ","(like sdoc, pdf)')}
value={inputValue}
autoFocus
onChange={handleInput}
@ -52,7 +61,7 @@ const FilterBySuffix = ({ suffixes, onSelect }) => {
<button
type="button"
className="clear-icon-right sf3-font sf3-font-x-01"
onClick={() => setInputValue('')}
onClick={handleClearInput}
aria-label={gettext('Clear')}
>
</button>
@ -65,8 +74,8 @@ const FilterBySuffix = ({ suffixes, onSelect }) => {
};
FilterBySuffix.propTypes = {
suffixes: PropTypes.array.isRequired,
onSelect: PropTypes.func.isRequired,
suffixes: PropTypes.string.isRequired,
onChange: PropTypes.func.isRequired,
};
export default FilterBySuffix;

View File

@ -6,7 +6,7 @@ import { Utils } from '../../../utils/utils';
import { gettext } from '../../../utils/constants';
import { SEARCH_FILTERS_KEY } from '../../../constants';
const FilterByText = ({ searchFilenameOnly, onSelect }) => {
const FilterByText = ({ searchFilenameOnly, onChange }) => {
const [isOpen, setIsOpen] = useState(false);
const [value, setValue] = useState(searchFilenameOnly ? SEARCH_FILTERS_KEY.SEARCH_FILENAME_ONLY : SEARCH_FILTERS_KEY.SEARCH_FILENAME_AND_CONTENT);
@ -27,8 +27,8 @@ const FilterByText = ({ searchFilenameOnly, onSelect }) => {
const option = Utils.getEventData(e, 'toggle') ?? e.currentTarget.getAttribute('data-toggle');
setValue(option);
const isSearchFilenameOnly = option === SEARCH_FILTERS_KEY.SEARCH_FILENAME_ONLY;
onSelect(SEARCH_FILTERS_KEY.SEARCH_FILENAME_ONLY, isSearchFilenameOnly);
}, [onSelect]);
onChange(SEARCH_FILTERS_KEY.SEARCH_FILENAME_ONLY, isSearchFilenameOnly);
}, [onChange]);
const label = options.find((option) => option.key === value).label;
@ -59,7 +59,7 @@ const FilterByText = ({ searchFilenameOnly, onSelect }) => {
FilterByText.propTypes = {
searchFilenameOnly: PropTypes.bool.isRequired,
onSelect: PropTypes.func.isRequired,
onChange: PropTypes.func.isRequired,
};
export default FilterByText;

View File

@ -10,10 +10,10 @@ import './index.css';
const SearchFilters = ({ filters, onChange }) => {
return (
<div className="search-filters-container">
<FilterBySuffix suffixes={filters.suffixes} onSelect={onChange} />
<FilterByText searchFilenameOnly={filters.search_filename_only} onSelect={onChange} />
<FilterByCreator creatorList={filters.creator_list} onSelect={onChange} />
<FilterByDate date={filters.date} onSelect={onChange} />
<FilterBySuffix suffixes={filters.suffixes} onChange={onChange} />
<FilterByText searchFilenameOnly={filters.search_filename_only} onChange={onChange} />
<FilterByCreator creatorList={filters.creator_list} onChange={onChange} />
<FilterByDate date={filters.date} onChange={onChange} />
</div>
);
};

View File

@ -12,7 +12,7 @@ import { Utils } from '../../utils/utils';
import toaster from '../toast';
import Loading from '../loading';
import { SEARCH_MASK, SEARCH_CONTAINER } from '../../constants/zIndexes';
import { PRIVATE_FILE_TYPE, SEARCH_FILTER_BY_DATE_OPTION_KEY, SEARCH_FILTER_BY_DATE_TYPE_KEY, SEARCH_FILTERS_SHOW_KEY } from '../../constants';
import { PRIVATE_FILE_TYPE, SEARCH_FILTER_BY_DATE_OPTION_KEY, SEARCH_FILTER_BY_DATE_TYPE_KEY, SEARCH_FILTERS_KEY, SEARCH_FILTERS_SHOW_KEY } from '../../constants';
import SearchFilters from './search-filters';
import SearchTags from './search-tags';
import IconBtn from '../icon-btn';
@ -61,10 +61,10 @@ class Search extends Component {
date: {
type: SEARCH_FILTER_BY_DATE_TYPE_KEY.CREATE_TIME,
value: '',
start: null,
end: null,
from: null,
to: null,
},
suffixes: [],
suffixes: '',
},
};
this.highlightRef = null;
@ -530,7 +530,7 @@ class Search extends Component {
start: null,
end: null,
},
suffixes: [],
suffixes: '',
}
});
}
@ -558,7 +558,6 @@ class Search extends Component {
}
}
const filteredItems = this.filterResults(resultItems);
if (isLoading) {
return <Loading />;
}
@ -568,8 +567,8 @@ class Search extends Component {
else if (!isResultGotten) {
return this.renderSearchTypes(this.state.inputValue.trim());
}
else if (filteredItems.length > 0) {
return this.renderResults(filteredItems);
else if (resultItems.length > 0) {
return this.renderResults(resultItems);
}
else {
return <div className="search-result-none">{gettext('No results matching')}</div>;
@ -673,9 +672,8 @@ class Search extends Component {
q: value,
search_repo: this.props.repoID,
search_ftypes: 'all',
search_filename_only: filters.search_filename_only,
};
this.getSearchResult(queryData);
this.getSearchResult(this.buildSearchParams(queryData));
};
searchFolder = () => {
@ -685,9 +683,8 @@ class Search extends Component {
search_repo: this.props.repoID,
search_ftypes: 'all',
search_path: this.props.path,
search_filename_only: filters.search_filename_only,
};
this.getSearchResult(queryData);
this.getSearchResult(this.buildSearchParams(queryData));
};
searchAllRepos = () => {
@ -696,44 +693,8 @@ class Search extends Component {
q: value,
search_repo: 'all',
search_ftypes: 'all',
search_filename_only: filters.search_filename_only,
};
this.getSearchResult(queryData);
};
filterResults = (results) => {
const { filters } = this.state;
return results.filter(item => {
if (filters.creator_list && filters.creator_list.length > 0) {
if (!filters.creator_list.some(creator => creator.email === item.repo_owner_email)) {
return false;
}
}
let startDate = filters.date.start;
let endDate = filters.date.end;
if (filters.date.value === SEARCH_FILTER_BY_DATE_OPTION_KEY.CUSTOM) {
startDate = filters.date.start?.unix();
endDate = filters.date.end?.unix();
}
if (startDate && item.mtime < startDate) {
return false;
}
if (endDate && item.mtime > endDate) {
return false;
}
if (filters.suffixes && filters.suffixes.length > 0) {
const pathParts = item.path.split('.');
const suffix = pathParts.length > 1 ? `.${pathParts.pop()}` : '';
if (!filters.suffixes.includes(suffix)) {
return false;
}
}
return true;
});
this.getSearchResult(this.buildSearchParams(queryData));
};
renderResults = (resultItems, isVisited) => {
@ -785,24 +746,64 @@ class Search extends Component {
this.setState({ isFiltersShow: !isFiltersShow });
}
buildSearchParams = (baseParams) => {
const { filters } = this.state;
const params = { ...baseParams };
if (filters.search_filename_only) {
params.search_filename_only = filters.search_filename_only;
}
if (filters.date.value) {
const isCustom = filters.date.value === SEARCH_FILTER_BY_DATE_OPTION_KEY.CUSTOM;
params.time_from = isCustom ? filters.date.start?.unix() : filters.date.from;
params.time_to = isCustom ? filters.date.end?.unix() : filters.date.to;
}
if (filters.suffixes) {
params.input_fexts = filters.suffixes;
params.search_ftypes = 'custom';
}
if (filters.creator_list.length > 0) {
params.creator_emails = filters.creator_list.map(c => c.email).join(',');
}
return params;
};
handleFiltersChange = (key, value) => {
const newFilters = { ...this.state.filters, [key]: value};
if (newFilters.search_filename_only !== this.state.filters.search_filename_only) {
this.setState({ filters: newFilters }, () => {
const newQueryData = {
...this.queryData,
search_filename_only: newFilters.search_filename_only,
}
this.getSearchResult(newQueryData);
});
const queryUpdates = {};
if (key === SEARCH_FILTERS_KEY.SEARCH_FILENAME_ONLY) {
queryUpdates.search_filename_only = value;
}
if (key === SEARCH_FILTERS_KEY.SUFFIXES) {
queryUpdates.search_ftypes = 'custom';
queryUpdates.input_fexts = value;
if (!value) {
queryUpdates.search_ftypes = 'all';
}
}
if (key === SEARCH_FILTERS_KEY.DATE) {
const date = value;
const isCustom = date.value === SEARCH_FILTER_BY_DATE_OPTION_KEY.CUSTOM;
queryUpdates.time_from = isCustom ? value.from.unix() : value.from;
queryUpdates.time_to = isCustom ? value.to.unix() : value.to;
}
let isFilterControllerActive = false;
if (newFilters.creator_list.length > 0 || newFilters.date.value || newFilters.suffixes.length > 0) {
isFilterControllerActive = true;
}
const newQueryData = {
...this.queryData,
...queryUpdates,
};
this.setState({ filters: newFilters, isFilterControllerActive }, () => this.forceUpdate());
this.getSearchResult(newQueryData);
const newFilters = { ...this.state.filters, [key]: value };
const hasActiveFilter = newFilters.suffixes || newFilters.creator_list.length > 0 || newFilters.date.value;
this.setState({ filters: newFilters, isFilterControllerActive: hasActiveFilter });
}
handleSelectTag = (tag) => {