mirror of
https://github.com/haiwen/seahub.git
synced 2025-08-25 18:20:48 +00:00
Feature/search filters controller (#7739)
* add search filter controller * update custom date * optimize ui * update bg color --------- Co-authored-by: zhouwenxuan <aries@Mac.local>
This commit is contained in:
parent
8cc5815107
commit
63f51d6d2a
15
frontend/src/assets/icons/filter-circled.svg
Normal file
15
frontend/src/assets/icons/filter-circled.svg
Normal file
@ -0,0 +1,15 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<!-- Generator: Adobe Illustrator 21.0.0, SVG Export Plug-In . SVG Version: 6.00 Build 0) -->
|
||||||
|
<svg version="1.1" id="图层_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
|
||||||
|
viewBox="0 0 32 32" style="enable-background:new 0 0 32 32;" xml:space="preserve">
|
||||||
|
<style type="text/css">
|
||||||
|
.st0{fill:#999999;}
|
||||||
|
</style>
|
||||||
|
<title>filter-circled</title>
|
||||||
|
<g id="filter-circled">
|
||||||
|
<path id="形状结合" class="st0" d="M16,1c8.3,0,15,6.7,15,15s-6.7,15-15,15S1,24.3,1,16S7.7,1,16,1z M16,4C9.4,4,4,9.4,4,16
|
||||||
|
s5.4,12,12,12s12-5.4,12-12S22.6,4,16,4z M20,20c0.6,0,1,0.4,1,1s-0.4,1-1,1h-8c-0.6,0-1-0.4-1-1s0.4-1,1-1H20z M22,15
|
||||||
|
c0.6,0,1,0.4,1,1s-0.4,1-1,1H10c-0.6,0-1-0.4-1-1s0.4-1,1-1H22z M24,10c0.6,0,1,0.4,1,1s-0.4,1-1,1H8c-0.6,0-1-0.4-1-1s0.4-1,1-1
|
||||||
|
H24z"/>
|
||||||
|
</g>
|
||||||
|
</svg>
|
After Width: | Height: | Size: 846 B |
@ -1,31 +1,22 @@
|
|||||||
import React, { useCallback, useEffect, useMemo, useState } from 'react';
|
import React, { useCallback, useEffect, useMemo, useState } from 'react';
|
||||||
import { Dropdown, DropdownItem, DropdownMenu, DropdownToggle } from 'reactstrap';
|
import { Dropdown, DropdownItem, DropdownMenu, DropdownToggle } from 'reactstrap';
|
||||||
import PropTypes from 'prop-types';
|
import PropTypes from 'prop-types';
|
||||||
|
import classNames from 'classnames';
|
||||||
|
import isHotkey from 'is-hotkey';
|
||||||
import { gettext } from '../../../utils/constants';
|
import { gettext } from '../../../utils/constants';
|
||||||
import { Utils } from '../../../utils/utils';
|
import { Utils } from '../../../utils/utils';
|
||||||
import UserItem from './user-item';
|
import UserItem from './user-item';
|
||||||
import { seafileAPI } from '../../../utils/seafile-api';
|
import { seafileAPI } from '../../../utils/seafile-api';
|
||||||
import ModalPortal from '../../modal-portal';
|
import ModalPortal from '../../modal-portal';
|
||||||
import toaster from '../../toast';
|
import toaster from '../../toast';
|
||||||
|
import { SEARCH_FILTERS_KEY } from '../../../constants';
|
||||||
|
|
||||||
const FilterByCreator = ({ onSelect }) => {
|
const FilterByCreator = ({ creatorList, onSelect }) => {
|
||||||
const [isOpen, setIsOpen] = useState(false);
|
const [isOpen, setIsOpen] = useState(false);
|
||||||
const [options, setOptions] = useState([]);
|
const [options, setOptions] = useState([]);
|
||||||
const [value, setValue] = useState([]);
|
const [selectedOptions, setSelectedOptions] = useState(creatorList || []);
|
||||||
const [searchValue, setSearchValue] = useState('');
|
const [searchValue, setSearchValue] = useState('');
|
||||||
|
|
||||||
const label = useMemo(() => {
|
|
||||||
if (!value || value.length === 0) return gettext('Creator');
|
|
||||||
const label = [];
|
|
||||||
value.forEach((v) => {
|
|
||||||
const option = options.find((o) => o.key === v);
|
|
||||||
if (option) {
|
|
||||||
label.push(option.name);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
return `Creator: ${label.join(',')}`;
|
|
||||||
}, [options, value]);
|
|
||||||
|
|
||||||
const toggle = useCallback((e) => {
|
const toggle = useCallback((e) => {
|
||||||
setIsOpen(!isOpen);
|
setIsOpen(!isOpen);
|
||||||
}, [isOpen]);
|
}, [isOpen]);
|
||||||
@ -40,33 +31,43 @@ const FilterByCreator = ({ onSelect }) => {
|
|||||||
const onSelectOption = useCallback((e) => {
|
const onSelectOption = useCallback((e) => {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
e.stopPropagation();
|
e.stopPropagation();
|
||||||
const option = Utils.getEventData(e, 'toggle') ?? e.currentTarget.getAttribute('data-toggle');
|
const name = Utils.getEventData(e, 'toggle') ?? e.currentTarget.getAttribute('data-toggle');
|
||||||
let updated = [...value];
|
let updated = [...selectedOptions];
|
||||||
if (!updated.includes(option)) {
|
if (!updated.some((item) => item.name === name)) {
|
||||||
updated = [...updated, option];
|
const newOption = options.find((option) => option.name === name);
|
||||||
|
updated = [...updated, newOption];
|
||||||
} else {
|
} else {
|
||||||
updated = updated.filter((v) => v !== option);
|
updated = updated.filter((option) => option.name !== name);
|
||||||
}
|
}
|
||||||
setValue(updated);
|
setSelectedOptions(updated);
|
||||||
onSelect('creator', updated);
|
onSelect(SEARCH_FILTERS_KEY.CREATOR_LIST, updated);
|
||||||
if (displayOptions.length === 1) {
|
if (displayOptions.length === 1) {
|
||||||
setSearchValue('');
|
setSearchValue('');
|
||||||
}
|
}
|
||||||
}, [value, displayOptions, onSelect]);
|
}, [selectedOptions, displayOptions, options, onSelect]);
|
||||||
|
|
||||||
const handleCancel = useCallback((v) => {
|
const handleCancel = useCallback((e, name) => {
|
||||||
const updated = value.filter((item) => item !== v);
|
const updated = selectedOptions.filter((option) => option.name !== name);
|
||||||
setValue(updated);
|
setSelectedOptions(updated);
|
||||||
onSelect('creator', updated);
|
onSelect(SEARCH_FILTERS_KEY.CREATOR_LIST, updated);
|
||||||
}, [value, onSelect]);
|
}, [selectedOptions, onSelect]);
|
||||||
|
|
||||||
const handleInputChange = useCallback((e) => {
|
const handleInputChange = useCallback((e) => {
|
||||||
const v = e.target.value;
|
const v = e.target.value;
|
||||||
setSearchValue(v);
|
setSearchValue(v);
|
||||||
if (!value) {
|
if (!selectedOptions) {
|
||||||
setOptions([]);
|
setOptions([]);
|
||||||
}
|
}
|
||||||
}, [value]);
|
}, [selectedOptions]);
|
||||||
|
|
||||||
|
const handleInputKeyDown = useCallback((e) => {
|
||||||
|
if (isHotkey('enter')(e)) {
|
||||||
|
e.preventDefault();
|
||||||
|
e.stopPropagation();
|
||||||
|
setSearchValue('');
|
||||||
|
toggle();
|
||||||
|
}
|
||||||
|
}, [toggle]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!searchValue) return;
|
if (!searchValue) return;
|
||||||
@ -75,16 +76,9 @@ const FilterByCreator = ({ onSelect }) => {
|
|||||||
try {
|
try {
|
||||||
const res = await seafileAPI.searchUsers(searchValue);
|
const res = await seafileAPI.searchUsers(searchValue);
|
||||||
const userList = res.data.users
|
const userList = res.data.users
|
||||||
.filter(user => user.name.toLowerCase().includes(searchValue.toLowerCase()))
|
.filter(user => user.name.toLowerCase().includes(searchValue.toLowerCase()));
|
||||||
.map(user => ({
|
|
||||||
key: user.email,
|
|
||||||
value: user.email,
|
|
||||||
name: user.name,
|
|
||||||
label: <UserItem user={user} />,
|
|
||||||
}))
|
|
||||||
.filter(user => !options.some(option => option.key === user.key));
|
|
||||||
|
|
||||||
setOptions(prevOptions => [...prevOptions, ...userList]);
|
setOptions(userList);
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
toaster.danger(Utils.getErrorMsg(err));
|
toaster.danger(Utils.getErrorMsg(err));
|
||||||
}
|
}
|
||||||
@ -97,45 +91,46 @@ const FilterByCreator = ({ onSelect }) => {
|
|||||||
return (
|
return (
|
||||||
<div className="search-filter filter-by-creator-container">
|
<div className="search-filter filter-by-creator-container">
|
||||||
<Dropdown isOpen={isOpen} toggle={toggle}>
|
<Dropdown isOpen={isOpen} toggle={toggle}>
|
||||||
<DropdownToggle tag="div" className="search-filter-toggle">
|
<DropdownToggle tag="div" className={classNames('search-filter-toggle', {
|
||||||
<div className="filter-label" title={label}>{label}</div>
|
'active': isOpen && selectedOptions.length > 0,
|
||||||
|
'highlighted': selectedOptions.length > 0,
|
||||||
|
})}>
|
||||||
|
<div className="filter-label" title={gettext('Creator')}>{gettext('Creator')}</div>
|
||||||
<i className="sf3-font sf3-font-down sf3-font pl-1" />
|
<i className="sf3-font sf3-font-down sf3-font pl-1" />
|
||||||
</DropdownToggle>
|
</DropdownToggle>
|
||||||
<ModalPortal>
|
<ModalPortal>
|
||||||
<DropdownMenu className="search-filter-menu creator-dropdown-menu">
|
<DropdownMenu className="search-filter-menu filter-by-creator-menu">
|
||||||
<div className="input-container">
|
<div className="input-container">
|
||||||
{value.map((v) => {
|
{selectedOptions.map((option) => (
|
||||||
const option = options.find((o) => o.key === v);
|
|
||||||
return option && (
|
|
||||||
<UserItem
|
<UserItem
|
||||||
key={option.key}
|
key={option.name}
|
||||||
user={option}
|
user={option}
|
||||||
isCancellable={true}
|
isCancellable={true}
|
||||||
onCancel={() => handleCancel(v)}
|
onCancel={handleCancel}
|
||||||
/>
|
/>
|
||||||
);
|
))}
|
||||||
})}
|
|
||||||
<div className="search-input-wrapper">
|
<div className="search-input-wrapper">
|
||||||
<input
|
<input
|
||||||
type="text"
|
type="text"
|
||||||
placeholder={value.length ? '' : gettext('Search user')}
|
placeholder={selectedOptions.length ? '' : gettext('Search user')}
|
||||||
value={searchValue}
|
value={searchValue}
|
||||||
onChange={handleInputChange}
|
onChange={handleInputChange}
|
||||||
|
onKeyDown={handleInputKeyDown}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
{displayOptions && displayOptions.map((option) => (
|
{displayOptions && displayOptions.map((option) => (
|
||||||
<DropdownItem
|
<DropdownItem
|
||||||
key={option.key}
|
key={option.name}
|
||||||
tag="div"
|
tag="div"
|
||||||
tabIndex="-1"
|
tabIndex="-1"
|
||||||
data-toggle={option.key}
|
data-toggle={option.name}
|
||||||
onMouseDown={(e) => e.preventDefault()}
|
onMouseDown={(e) => e.preventDefault()}
|
||||||
onClick={onSelectOption}
|
onClick={onSelectOption}
|
||||||
toggle={false}
|
toggle={false}
|
||||||
>
|
>
|
||||||
{option.label}
|
{isOpen && <UserItem user={option} />}
|
||||||
{value.includes(option.key) && <i className="dropdown-item-tick sf2-icon-tick"></i>}
|
{selectedOptions.includes(option.name) && <i className="dropdown-item-tick sf2-icon-tick"></i>}
|
||||||
</DropdownItem>
|
</DropdownItem>
|
||||||
))}
|
))}
|
||||||
</DropdownMenu>
|
</DropdownMenu>
|
||||||
@ -146,6 +141,7 @@ const FilterByCreator = ({ onSelect }) => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
FilterByCreator.propTypes = {
|
FilterByCreator.propTypes = {
|
||||||
|
creatorList: PropTypes.array.isRequired,
|
||||||
onSelect: PropTypes.func.isRequired,
|
onSelect: PropTypes.func.isRequired,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -1,41 +1,27 @@
|
|||||||
import React, { useCallback, useMemo, useState } from 'react';
|
import React, { useCallback, useMemo, useState } from 'react';
|
||||||
|
import PropTypes from 'prop-types';
|
||||||
import { Dropdown, DropdownItem, DropdownMenu, DropdownToggle } from 'reactstrap';
|
import { Dropdown, DropdownItem, DropdownMenu, DropdownToggle } from 'reactstrap';
|
||||||
import dayjs from 'dayjs';
|
import dayjs from 'dayjs';
|
||||||
import { gettext } from '../../../utils/constants';
|
import { gettext } from '../../../utils/constants';
|
||||||
import { Utils } from '../../../utils/utils';
|
import { Utils } from '../../../utils/utils';
|
||||||
import Picker from '../../date-and-time-picker';
|
import Picker from '../../date-and-time-picker';
|
||||||
import ModalPortal from '../../modal-portal';
|
import ModalPortal from '../../modal-portal';
|
||||||
|
import { SEARCH_FILTERS_KEY, SEARCH_FILTER_BY_DATE_OPTION_KEY, SEARCH_FILTER_BY_DATE_TYPE_KEY } from '../../../constants';
|
||||||
const DATE_FILTER_TYPE_KEY = {
|
|
||||||
CREATE_TIME: 'create_time',
|
|
||||||
LAST_MODIFIED_TIME: 'last_modified_time',
|
|
||||||
};
|
|
||||||
|
|
||||||
const DATE_OPTION_KEY = {
|
|
||||||
TODAY: 'today',
|
|
||||||
LAST_7_DAYS: 'last_7_days',
|
|
||||||
LAST_30_DAYS: 'last_30_days',
|
|
||||||
CUSTOM: 'custom',
|
|
||||||
};
|
|
||||||
|
|
||||||
const DATE_INPUT_WIDTH = 118;
|
const DATE_INPUT_WIDTH = 118;
|
||||||
|
|
||||||
const FilterByDate = ({ onSelect }) => {
|
const FilterByDate = ({ date, onSelect }) => {
|
||||||
const [value, setValue] = useState('');
|
const [value, setValue] = useState(date.value);
|
||||||
const [isOpen, setIsOpen] = useState(false);
|
const [isOpen, setIsOpen] = useState(false);
|
||||||
const [isTypeOpen, setIsTypeOpen] = useState(false);
|
const [isTypeOpen, setIsTypeOpen] = useState(false);
|
||||||
const [isCustomDate, setIsCustomDate] = useState(false);
|
const [isCustomDate, setIsCustomDate] = useState(date.value === SEARCH_FILTER_BY_DATE_OPTION_KEY.CUSTOM);
|
||||||
const [customDate, setCustomDate] = useState({
|
const [type, setType] = useState(date.type);
|
||||||
start: null,
|
|
||||||
end: null,
|
|
||||||
});
|
|
||||||
const [type, setType] = useState(DATE_FILTER_TYPE_KEY.CREATE_TIME);
|
|
||||||
|
|
||||||
const typeLabel = useMemo(() => {
|
const typeLabel = useMemo(() => {
|
||||||
switch (type) {
|
switch (type) {
|
||||||
case DATE_FILTER_TYPE_KEY.CREATE_TIME:
|
case SEARCH_FILTER_BY_DATE_TYPE_KEY.CREATE_TIME:
|
||||||
return gettext('Create time');
|
return gettext('Create time');
|
||||||
case DATE_FILTER_TYPE_KEY.LAST_MODIFIED_TIME:
|
case SEARCH_FILTER_BY_DATE_TYPE_KEY.LAST_MODIFIED_TIME:
|
||||||
return gettext('Last modified time');
|
return gettext('Last modified time');
|
||||||
default:
|
default:
|
||||||
return gettext('Create time');
|
return gettext('Create time');
|
||||||
@ -45,10 +31,10 @@ const FilterByDate = ({ onSelect }) => {
|
|||||||
const typeOptions = useMemo(() => {
|
const typeOptions = useMemo(() => {
|
||||||
return [
|
return [
|
||||||
{
|
{
|
||||||
key: DATE_FILTER_TYPE_KEY.CREATE_TIME,
|
key: SEARCH_FILTER_BY_DATE_TYPE_KEY.CREATE_TIME,
|
||||||
label: gettext('Create time'),
|
label: gettext('Create time'),
|
||||||
}, {
|
}, {
|
||||||
key: DATE_FILTER_TYPE_KEY.LAST_MODIFIED_TIME,
|
key: SEARCH_FILTER_BY_DATE_TYPE_KEY.LAST_MODIFIED_TIME,
|
||||||
label: gettext('Last modified time'),
|
label: gettext('Last modified time'),
|
||||||
}
|
}
|
||||||
];
|
];
|
||||||
@ -61,36 +47,36 @@ const FilterByDate = ({ onSelect }) => {
|
|||||||
const prefix = `${typeLabel}: `;
|
const prefix = `${typeLabel}: `;
|
||||||
|
|
||||||
switch (value) {
|
switch (value) {
|
||||||
case DATE_OPTION_KEY.TODAY:
|
case SEARCH_FILTER_BY_DATE_OPTION_KEY.TODAY:
|
||||||
return `${prefix}${formatDate(today)}`;
|
return `${prefix}${formatDate(today)}`;
|
||||||
case DATE_OPTION_KEY.LAST_7_DAYS:
|
case SEARCH_FILTER_BY_DATE_OPTION_KEY.LAST_7_DAYS:
|
||||||
return `${prefix}${formatDate(today.subtract(6, 'day'))} - ${formatDate(today)}`;
|
return `${prefix}${formatDate(today.subtract(6, 'day'))} - ${formatDate(today)}`;
|
||||||
case DATE_OPTION_KEY.LAST_30_DAYS:
|
case SEARCH_FILTER_BY_DATE_OPTION_KEY.LAST_30_DAYS:
|
||||||
return `${prefix}${formatDate(today.subtract(29, 'day'))} - ${formatDate(today)}`;
|
return `${prefix}${formatDate(today.subtract(29, 'day'))} - ${formatDate(today)}`;
|
||||||
case DATE_OPTION_KEY.CUSTOM:
|
case SEARCH_FILTER_BY_DATE_OPTION_KEY.CUSTOM:
|
||||||
return customDate.start && customDate.end
|
return date.start && date.end
|
||||||
? `${prefix}${formatDate(customDate.start)} - ${formatDate(customDate.end)}`
|
? `${prefix}${formatDate(date.start)} - ${formatDate(date.end)}`
|
||||||
: gettext('Select date range');
|
: gettext('Select date range');
|
||||||
default:
|
default:
|
||||||
return gettext('Date');
|
return gettext('Date');
|
||||||
}
|
}
|
||||||
}, [value, customDate, typeLabel]);
|
}, [date, value, typeLabel]);
|
||||||
|
|
||||||
const options = useMemo(() => {
|
const options = useMemo(() => {
|
||||||
return [
|
return [
|
||||||
{
|
{
|
||||||
key: DATE_OPTION_KEY.TODAY,
|
key: SEARCH_FILTER_BY_DATE_OPTION_KEY.TODAY,
|
||||||
label: gettext('Today'),
|
label: gettext('Today'),
|
||||||
}, {
|
}, {
|
||||||
key: DATE_OPTION_KEY.LAST_7_DAYS,
|
key: SEARCH_FILTER_BY_DATE_OPTION_KEY.LAST_7_DAYS,
|
||||||
label: gettext('Last 7 days'),
|
label: gettext('Last 7 days'),
|
||||||
}, {
|
}, {
|
||||||
key: DATE_OPTION_KEY.LAST_30_DAYS,
|
key: SEARCH_FILTER_BY_DATE_OPTION_KEY.LAST_30_DAYS,
|
||||||
label: gettext('Last 30 days'),
|
label: gettext('Last 30 days'),
|
||||||
},
|
},
|
||||||
'Divider',
|
'Divider',
|
||||||
{
|
{
|
||||||
key: DATE_OPTION_KEY.CUSTOM,
|
key: SEARCH_FILTER_BY_DATE_OPTION_KEY.CUSTOM,
|
||||||
label: gettext('Custom time'),
|
label: gettext('Custom time'),
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
@ -100,93 +86,98 @@ const FilterByDate = ({ onSelect }) => {
|
|||||||
|
|
||||||
const toggleType = useCallback(() => setIsTypeOpen(!isTypeOpen), [isTypeOpen]);
|
const toggleType = useCallback(() => setIsTypeOpen(!isTypeOpen), [isTypeOpen]);
|
||||||
|
|
||||||
|
const onChangeType = useCallback((e) => {
|
||||||
|
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]);
|
||||||
|
|
||||||
const onClearDate = useCallback(() => {
|
const onClearDate = useCallback(() => {
|
||||||
setValue('');
|
setValue('');
|
||||||
setIsCustomDate(false);
|
setIsCustomDate(false);
|
||||||
onSelect('date', '');
|
onSelect(SEARCH_FILTERS_KEY.DATE, '');
|
||||||
}, [onSelect]);
|
}, [onSelect]);
|
||||||
|
|
||||||
const onOptionClick = useCallback((e) => {
|
const onOptionClick = useCallback((e) => {
|
||||||
const option = Utils.getEventData(e, 'toggle') ?? e.currentTarget.getAttribute('data-toggle');
|
const option = Utils.getEventData(e, 'toggle') ?? e.currentTarget.getAttribute('data-toggle');
|
||||||
|
if (option === value) return;
|
||||||
const today = dayjs().endOf('day');
|
const today = dayjs().endOf('day');
|
||||||
|
setIsCustomDate(option === SEARCH_FILTER_BY_DATE_OPTION_KEY.CUSTOM);
|
||||||
switch (option) {
|
|
||||||
case DATE_OPTION_KEY.TODAY: {
|
|
||||||
setValue(option);
|
setValue(option);
|
||||||
setIsCustomDate(false);
|
switch (option) {
|
||||||
onSelect('date', {
|
case SEARCH_FILTER_BY_DATE_OPTION_KEY.TODAY: {
|
||||||
|
onSelect(SEARCH_FILTERS_KEY.DATE, {
|
||||||
|
value: option,
|
||||||
start: dayjs().startOf('day').unix(),
|
start: dayjs().startOf('day').unix(),
|
||||||
end: today.unix()
|
end: today.unix()
|
||||||
});
|
});
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
case DATE_OPTION_KEY.LAST_7_DAYS: {
|
case SEARCH_FILTER_BY_DATE_OPTION_KEY.LAST_7_DAYS: {
|
||||||
setValue(option);
|
onSelect(SEARCH_FILTERS_KEY.DATE, {
|
||||||
setIsCustomDate(false);
|
value: option,
|
||||||
onSelect('date', {
|
|
||||||
start: dayjs().subtract(6, 'day').startOf('day').unix(),
|
start: dayjs().subtract(6, 'day').startOf('day').unix(),
|
||||||
end: today.unix()
|
end: today.unix()
|
||||||
});
|
});
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
case DATE_OPTION_KEY.LAST_30_DAYS: {
|
case SEARCH_FILTER_BY_DATE_OPTION_KEY.LAST_30_DAYS: {
|
||||||
setValue(option);
|
onSelect(SEARCH_FILTERS_KEY.DATE, {
|
||||||
setIsCustomDate(false);
|
value: option,
|
||||||
onSelect('date', {
|
|
||||||
start: dayjs().subtract(30, 'day').startOf('day').unix(),
|
start: dayjs().subtract(30, 'day').startOf('day').unix(),
|
||||||
end: today.unix()
|
end: today.unix()
|
||||||
});
|
});
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
case DATE_OPTION_KEY.CUSTOM: {
|
case SEARCH_FILTER_BY_DATE_OPTION_KEY.CUSTOM: {
|
||||||
setValue(DATE_OPTION_KEY.CUSTOM);
|
onSelect(SEARCH_FILTERS_KEY.DATE, {
|
||||||
setIsCustomDate(true);
|
value: option,
|
||||||
|
start: null,
|
||||||
|
end: null,
|
||||||
|
});
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}, [onSelect]);
|
}, [value, onSelect]);
|
||||||
|
|
||||||
const disabledStartDate = useCallback((startDate) => {
|
const disabledStartDate = useCallback((startDate) => {
|
||||||
if (!startDate) return false;
|
if (!startDate) return false;
|
||||||
const today = dayjs();
|
const today = dayjs();
|
||||||
const endValue = customDate.end;
|
const endValue = date.end;
|
||||||
|
|
||||||
if (!endValue) {
|
if (!endValue) {
|
||||||
return startDate.isAfter(today);
|
return startDate.isAfter(today);
|
||||||
}
|
}
|
||||||
return endValue.isBefore(startDate) || startDate.isAfter(today);
|
return endValue.isBefore(startDate) || startDate.isAfter(today);
|
||||||
}, [customDate]);
|
}, [date]);
|
||||||
|
|
||||||
const disabledEndDate = useCallback((endDate) => {
|
const disabledEndDate = useCallback((endDate) => {
|
||||||
if (!endDate) return false;
|
if (!endDate) return false;
|
||||||
const today = dayjs();
|
const today = dayjs();
|
||||||
const startValue = customDate.start;
|
const startValue = date.start;
|
||||||
if (!startValue) {
|
if (!startValue) {
|
||||||
return endDate.isAfter(today);
|
return endDate.isAfter(today);
|
||||||
}
|
}
|
||||||
return endDate.isBefore(startValue) || endDate.isAfter(today);
|
return endDate.isBefore(startValue) || endDate.isAfter(today);
|
||||||
}, [customDate]);
|
}, [date]);
|
||||||
|
|
||||||
const onChangeCustomDate = useCallback((date) => {
|
const onChangeCustomDate = useCallback((customDate) => {
|
||||||
const newDate = {
|
const newDate = {
|
||||||
|
...date,
|
||||||
...customDate,
|
...customDate,
|
||||||
[date.type]: date.value,
|
|
||||||
};
|
};
|
||||||
setCustomDate(newDate);
|
onSelect(SEARCH_FILTERS_KEY.DATE, newDate);
|
||||||
|
}, [date, onSelect]);
|
||||||
if (newDate.start && newDate.end) {
|
|
||||||
onSelect('date', {
|
|
||||||
start: newDate.start.unix(),
|
|
||||||
end: newDate.end.unix(),
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}, [customDate, onSelect]);
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="search-filter filter-by-date">
|
<div className="search-filter filter-by-date-container">
|
||||||
<Dropdown isOpen={isOpen} toggle={toggle}>
|
<Dropdown isOpen={isOpen} toggle={toggle}>
|
||||||
<DropdownToggle tag="div" className="search-filter-toggle">
|
<DropdownToggle tag="div" className="search-filter-toggle" onClick={toggle}>
|
||||||
<div className="filter-label" style={{ maxWidth: 200 }} title={label}>{label}</div>
|
<div className="filter-label" style={{ maxWidth: 300 }} title={label}>{label}</div>
|
||||||
<i
|
<i
|
||||||
className="sf3-font sf3-font-down sf3-font pl-1"
|
className="sf3-font sf3-font-down sf3-font pl-1"
|
||||||
onClick={(e) => {
|
onClick={(e) => {
|
||||||
@ -213,7 +204,7 @@ const FilterByDate = ({ onSelect }) => {
|
|||||||
{typeOptions.map((option) => {
|
{typeOptions.map((option) => {
|
||||||
const isSelected = option.key === type;
|
const isSelected = option.key === type;
|
||||||
return (
|
return (
|
||||||
<DropdownItem key={option.key} data-toggle={option.key} onClick={() => setType(option.key)}>
|
<DropdownItem key={option.key} data-toggle={option.key} onClick={onChangeType}>
|
||||||
{option.label}
|
{option.label}
|
||||||
{isSelected && <i className="dropdown-item-tick sf2-icon-tick"></i>}
|
{isSelected && <i className="dropdown-item-tick sf2-icon-tick"></i>}
|
||||||
</DropdownItem>
|
</DropdownItem>
|
||||||
@ -250,8 +241,8 @@ const FilterByDate = ({ onSelect }) => {
|
|||||||
<Picker
|
<Picker
|
||||||
showHourAndMinute={false}
|
showHourAndMinute={false}
|
||||||
disabledDate={disabledStartDate}
|
disabledDate={disabledStartDate}
|
||||||
value={customDate.start}
|
value={date.start}
|
||||||
onChange={(value) => onChangeCustomDate({ type: 'start', value })}
|
onChange={(value) => onChangeCustomDate({ start: value })}
|
||||||
inputWidth={DATE_INPUT_WIDTH}
|
inputWidth={DATE_INPUT_WIDTH}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
@ -260,8 +251,8 @@ const FilterByDate = ({ onSelect }) => {
|
|||||||
<Picker
|
<Picker
|
||||||
showHourAndMinute={false}
|
showHourAndMinute={false}
|
||||||
disabledDate={disabledEndDate}
|
disabledDate={disabledEndDate}
|
||||||
value={customDate.end}
|
value={date.end}
|
||||||
onChange={(value) => onChangeCustomDate({ type: 'end', value })}
|
onChange={(value) => onChangeCustomDate({ end: value })}
|
||||||
inputWidth={DATE_INPUT_WIDTH}
|
inputWidth={DATE_INPUT_WIDTH}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
@ -274,4 +265,14 @@ const FilterByDate = ({ onSelect }) => {
|
|||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
FilterByDate.propTypes = {
|
||||||
|
date: PropTypes.shape({
|
||||||
|
type: PropTypes.string,
|
||||||
|
value: PropTypes.string,
|
||||||
|
start: PropTypes.oneOfType([PropTypes.number, PropTypes.object]),
|
||||||
|
end: PropTypes.oneOfType([PropTypes.number, PropTypes.object]),
|
||||||
|
}),
|
||||||
|
onSelect: PropTypes.func.isRequired,
|
||||||
|
};
|
||||||
|
|
||||||
export default FilterByDate;
|
export default FilterByDate;
|
||||||
|
@ -1,25 +1,22 @@
|
|||||||
import React, { useCallback, useMemo, useRef, useState } from 'react';
|
import React, { useCallback, useRef, useState } from 'react';
|
||||||
import { Dropdown, DropdownMenu, DropdownToggle } from 'reactstrap';
|
import { Dropdown, DropdownMenu, DropdownToggle } from 'reactstrap';
|
||||||
|
import PropTypes from 'prop-types';
|
||||||
|
import classNames from 'classnames';
|
||||||
import { gettext } from '../../../utils/constants';
|
import { gettext } from '../../../utils/constants';
|
||||||
import ModalPortal from '../../modal-portal';
|
import ModalPortal from '../../modal-portal';
|
||||||
|
import { SEARCH_FILTERS_KEY } from '../../../constants';
|
||||||
|
|
||||||
const FilterBySuffix = ({ onSelect }) => {
|
const FilterBySuffix = ({ suffixes, onSelect }) => {
|
||||||
const [isOpen, setIsOpen] = useState(false);
|
const [isOpen, setIsOpen] = useState(false);
|
||||||
const [value, setValue] = useState('');
|
const [inputValue, setInputValue] = useState(suffixes.join(', '));
|
||||||
const inputRef = useRef(null);
|
const inputRef = useRef(null);
|
||||||
|
|
||||||
const label = useMemo(() => {
|
|
||||||
if (value) {
|
|
||||||
return `${gettext('File suffix: ')} ${value}`;
|
|
||||||
}
|
|
||||||
return gettext('File suffix');
|
|
||||||
}, [value]);
|
|
||||||
|
|
||||||
const toggle = useCallback(() => setIsOpen(!isOpen), [isOpen]);
|
const toggle = useCallback(() => setIsOpen(!isOpen), [isOpen]);
|
||||||
|
|
||||||
const handleInput = useCallback((e) => {
|
const handleInput = useCallback((e) => {
|
||||||
setValue(e.target.value);
|
setInputValue(e.target.value);
|
||||||
onSelect('suffix', e.target.value);
|
const suffixes = e.target.value.split(',').map(suffix => suffix.trim()).filter(Boolean);
|
||||||
|
onSelect(SEARCH_FILTERS_KEY.SUFFIXES, suffixes);
|
||||||
}, [onSelect]);
|
}, [onSelect]);
|
||||||
|
|
||||||
const handleKeyDown = useCallback((e) => {
|
const handleKeyDown = useCallback((e) => {
|
||||||
@ -32,18 +29,21 @@ const FilterBySuffix = ({ onSelect }) => {
|
|||||||
return (
|
return (
|
||||||
<div className="search-filter filter-by-suffix-container">
|
<div className="search-filter filter-by-suffix-container">
|
||||||
<Dropdown isOpen={isOpen} toggle={toggle}>
|
<Dropdown isOpen={isOpen} toggle={toggle}>
|
||||||
<DropdownToggle tag="div" className="search-filter-toggle">
|
<DropdownToggle tag="div" className={classNames('search-filter-toggle', {
|
||||||
<div className="filter-label" title={label}>{label}</div>
|
'active': isOpen && suffixes.length > 0,
|
||||||
{!value && <i className="sf3-font sf3-font-down sf3-font pl-1" />}
|
'highlighted': suffixes.length > 0,
|
||||||
|
})} onClick={toggle}>
|
||||||
|
<div className="filter-label" title={gettext('File suffix')}>{gettext('File suffix')}</div>
|
||||||
|
<i className="sf3-font sf3-font-down sf3-font pl-1" />
|
||||||
</DropdownToggle>
|
</DropdownToggle>
|
||||||
<ModalPortal>
|
<ModalPortal>
|
||||||
<DropdownMenu className="search-filter-menu suffix-dropdown-menu">
|
<DropdownMenu className="search-filter-menu filter-by-suffix-menu">
|
||||||
<div className="input-container">
|
<div className="input-container">
|
||||||
<input
|
<input
|
||||||
ref={inputRef}
|
ref={inputRef}
|
||||||
type="text"
|
type="text"
|
||||||
placeholder={gettext('Filter by file suffix (e.g. sdoc)')}
|
placeholder={gettext('Seperate multiple suffixes by ","(like .sdoc, .pdf)')}
|
||||||
value={value}
|
value={inputValue}
|
||||||
autoFocus
|
autoFocus
|
||||||
width={120}
|
width={120}
|
||||||
onChange={handleInput}
|
onChange={handleInput}
|
||||||
@ -57,4 +57,9 @@ const FilterBySuffix = ({ onSelect }) => {
|
|||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
FilterBySuffix.propTypes = {
|
||||||
|
suffixes: PropTypes.array.isRequired,
|
||||||
|
onSelect: PropTypes.func.isRequired,
|
||||||
|
};
|
||||||
|
|
||||||
export default FilterBySuffix;
|
export default FilterBySuffix;
|
||||||
|
@ -1,25 +1,22 @@
|
|||||||
import React, { useCallback, useMemo, useState } from 'react';
|
import React, { useCallback, useMemo, useState } from 'react';
|
||||||
import { Dropdown, DropdownItem, DropdownMenu, DropdownToggle } from 'reactstrap';
|
import { Dropdown, DropdownItem, DropdownMenu, DropdownToggle } from 'reactstrap';
|
||||||
|
import PropTypes from 'prop-types';
|
||||||
import ModalPortal from '../../../components/modal-portal';
|
import ModalPortal from '../../../components/modal-portal';
|
||||||
import { Utils } from '../../../utils/utils';
|
import { Utils } from '../../../utils/utils';
|
||||||
import { gettext } from '../../../utils/constants';
|
import { gettext } from '../../../utils/constants';
|
||||||
|
import { SEARCH_FILTERS_KEY } from '../../../constants';
|
||||||
|
|
||||||
const TEXT_FILTER_KEY = {
|
const FilterByText = ({ searchFilenameOnly, onSelect }) => {
|
||||||
SEARCH_FILENAME_AND_CONTENT: 'search_filename_and_content',
|
|
||||||
SEARCH_FILENAME_ONLY: 'search_filename_only',
|
|
||||||
};
|
|
||||||
|
|
||||||
const FilterByText = ({ onSelect }) => {
|
|
||||||
const [isOpen, setIsOpen] = useState(false);
|
const [isOpen, setIsOpen] = useState(false);
|
||||||
const [value, setValue] = useState(TEXT_FILTER_KEY.SEARCH_FILENAME_AND_CONTENT);
|
const [value, setValue] = useState(searchFilenameOnly ? SEARCH_FILTERS_KEY.SEARCH_FILENAME_ONLY : SEARCH_FILTERS_KEY.SEARCH_FILENAME_AND_CONTENT);
|
||||||
|
|
||||||
const options = useMemo(() => {
|
const options = useMemo(() => {
|
||||||
return [
|
return [
|
||||||
{
|
{
|
||||||
key: TEXT_FILTER_KEY.SEARCH_FILENAME_AND_CONTENT,
|
key: SEARCH_FILTERS_KEY.SEARCH_FILENAME_AND_CONTENT,
|
||||||
label: gettext('File name and content'),
|
label: gettext('File name and content'),
|
||||||
}, {
|
}, {
|
||||||
key: TEXT_FILTER_KEY.SEARCH_FILENAME_ONLY,
|
key: SEARCH_FILTERS_KEY.SEARCH_FILENAME_ONLY,
|
||||||
label: gettext('File name only'),
|
label: gettext('File name only'),
|
||||||
}
|
}
|
||||||
];
|
];
|
||||||
@ -29,8 +26,8 @@ const FilterByText = ({ onSelect }) => {
|
|||||||
const onOptionClick = useCallback((e) => {
|
const onOptionClick = useCallback((e) => {
|
||||||
const option = Utils.getEventData(e, 'toggle') ?? e.currentTarget.getAttribute('data-toggle');
|
const option = Utils.getEventData(e, 'toggle') ?? e.currentTarget.getAttribute('data-toggle');
|
||||||
setValue(option);
|
setValue(option);
|
||||||
const isSearchFilenameOnly = option === TEXT_FILTER_KEY.SEARCH_FILENAME_ONLY;
|
const isSearchFilenameOnly = option === SEARCH_FILTERS_KEY.SEARCH_FILENAME_ONLY;
|
||||||
onSelect('search_filename_only', isSearchFilenameOnly);
|
onSelect(SEARCH_FILTERS_KEY.SEARCH_FILENAME_ONLY, isSearchFilenameOnly);
|
||||||
}, [onSelect]);
|
}, [onSelect]);
|
||||||
|
|
||||||
const label = options.find((option) => option.key === value).label;
|
const label = options.find((option) => option.key === value).label;
|
||||||
@ -60,4 +57,9 @@ const FilterByText = ({ onSelect }) => {
|
|||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
FilterByText.propTypes = {
|
||||||
|
searchFilenameOnly: PropTypes.bool.isRequired,
|
||||||
|
onSelect: PropTypes.func.isRequired,
|
||||||
|
};
|
||||||
|
|
||||||
export default FilterByText;
|
export default FilterByText;
|
||||||
|
@ -18,23 +18,18 @@
|
|||||||
margin-right: 8px;
|
margin-right: 8px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.search-filters-container .search-filter .sf3-font-down,
|
|
||||||
.search-filter-menu .sf3-font-down {
|
|
||||||
color: #666;
|
|
||||||
}
|
|
||||||
|
|
||||||
.search-filters-container .search-filter .search-filter-toggle,
|
.search-filters-container .search-filter .search-filter-toggle,
|
||||||
.search-filter-menu .search-filter-toggle {
|
.search-filter-menu .search-filter-toggle {
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
padding: 2px 4px;
|
padding: 2px 4px;
|
||||||
|
border-radius: 3px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.search-filters-container .search-filter .search-filter-toggle:hover,
|
.search-filters-container .search-filter .search-filter-toggle:hover,
|
||||||
.search-filter-menu .search-filter-toggle:hover {
|
.search-filter-menu .search-filter-toggle:hover {
|
||||||
background-color: #efefef;
|
background-color: #efefef;
|
||||||
border-radius: 3px;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.search-filters-container .search-filter .filter-label {
|
.search-filters-container .search-filter .filter-label {
|
||||||
@ -55,10 +50,15 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
.search-filter-menu.filter-by-text-menu,
|
.search-filter-menu.filter-by-text-menu,
|
||||||
.search-filter-menu.filter-by-date-menu {
|
.search-filter-menu.filter-by-date-menu,
|
||||||
|
.search-filter-menu.filter-by-creator-menu {
|
||||||
width: 280px;
|
width: 280px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.search-filter-menu.filter-by-suffix-menu {
|
||||||
|
width: 400px;
|
||||||
|
}
|
||||||
|
|
||||||
.search-filters-container .search-filters-dropdown-item {
|
.search-filters-container .search-filters-dropdown-item {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
}
|
}
|
||||||
@ -81,7 +81,6 @@
|
|||||||
|
|
||||||
.search-filter-menu .input-container {
|
.search-filter-menu .input-container {
|
||||||
position: relative;
|
position: relative;
|
||||||
width: 240px;
|
|
||||||
border: 1px solid #eaeaea;
|
border: 1px solid #eaeaea;
|
||||||
border-radius: 4px;
|
border-radius: 4px;
|
||||||
padding: 4px 6px;
|
padding: 4px 6px;
|
||||||
@ -184,3 +183,13 @@
|
|||||||
height: 28px;
|
height: 28px;
|
||||||
font-size: 14px;
|
font-size: 14px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.search-filters-container .search-filter-toggle.active,
|
||||||
|
.search-filters-container .search-filter-toggle.active:hover,
|
||||||
|
.search-filters-container .search-filter-toggle.highlighted:hover {
|
||||||
|
background-color: rgba(255, 152, 0, 0.2);
|
||||||
|
}
|
||||||
|
|
||||||
|
.search-filters-container .search-filter-toggle.highlighted {
|
||||||
|
color: #ff9800;
|
||||||
|
}
|
||||||
|
@ -7,19 +7,20 @@ import FilterBySuffix from './filter-by-suffix';
|
|||||||
|
|
||||||
import './index.css';
|
import './index.css';
|
||||||
|
|
||||||
const SearchFilters = ({ onChange }) => {
|
const SearchFilters = ({ filters, onChange }) => {
|
||||||
return (
|
return (
|
||||||
<div className="search-filters-container">
|
<div className="search-filters-container">
|
||||||
<FilterByText onSelect={onChange} />
|
<FilterBySuffix suffixes={filters.suffixes} onSelect={onChange} />
|
||||||
<FilterByCreator onSelect={onChange} />
|
<FilterByText searchFilenameOnly={filters.search_filename_only} onSelect={onChange} />
|
||||||
<FilterByDate onSelect={onChange} />
|
<FilterByCreator creatorList={filters.creator_list} onSelect={onChange} />
|
||||||
<FilterBySuffix onSelect={onChange} />
|
<FilterByDate date={filters.date} onSelect={onChange} />
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
SearchFilters.propTypes = {
|
SearchFilters.propTypes = {
|
||||||
onChange: PropTypes.func,
|
filters: PropTypes.object.isRequired,
|
||||||
|
onChange: PropTypes.func.isRequired,
|
||||||
};
|
};
|
||||||
|
|
||||||
export default SearchFilters;
|
export default SearchFilters;
|
||||||
|
@ -1,4 +1,5 @@
|
|||||||
import React from 'react';
|
import React from 'react';
|
||||||
|
import PropTypes from 'prop-types';
|
||||||
import { mediaUrl } from '../../../utils/constants';
|
import { mediaUrl } from '../../../utils/constants';
|
||||||
import IconBtn from '../../icon-btn';
|
import IconBtn from '../../icon-btn';
|
||||||
|
|
||||||
@ -7,9 +8,15 @@ const UserItem = ({ user, isCancellable, onCancel }) => {
|
|||||||
<div className="user-item">
|
<div className="user-item">
|
||||||
<img src={user.avatar_url || `${mediaUrl}avatars/default.png`} alt={user.name} className="user-avatar" />
|
<img src={user.avatar_url || `${mediaUrl}avatars/default.png`} alt={user.name} className="user-avatar" />
|
||||||
<span className="user-name">{user.name}</span>
|
<span className="user-name">{user.name}</span>
|
||||||
{isCancellable && <IconBtn className="user-remove" onClick={(e) => onCancel(e, user)} symbol="x-01" />}
|
{isCancellable && <IconBtn className="user-remove" onClick={(e) => onCancel(e, user.name)} symbol="x-01" />}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
UserItem.propTypes = {
|
||||||
|
user: PropTypes.object.isRequired,
|
||||||
|
isCancellable: PropTypes.bool,
|
||||||
|
onCancel: PropTypes.func,
|
||||||
|
};
|
||||||
|
|
||||||
export default UserItem;
|
export default UserItem;
|
||||||
|
@ -12,9 +12,10 @@ import { Utils } from '../../utils/utils';
|
|||||||
import toaster from '../toast';
|
import toaster from '../toast';
|
||||||
import Loading from '../loading';
|
import Loading from '../loading';
|
||||||
import { SEARCH_MASK, SEARCH_CONTAINER } from '../../constants/zIndexes';
|
import { SEARCH_MASK, SEARCH_CONTAINER } from '../../constants/zIndexes';
|
||||||
import { PRIVATE_FILE_TYPE } from '../../constants';
|
import { PRIVATE_FILE_TYPE, SEARCH_FILTER_BY_DATE_OPTION_KEY, SEARCH_FILTER_BY_DATE_TYPE_KEY, SEARCH_FILTERS_SHOW_KEY } from '../../constants';
|
||||||
import SearchFilters from './search-filters';
|
import SearchFilters from './search-filters';
|
||||||
import SearchTags from './search-tags';
|
import SearchTags from './search-tags';
|
||||||
|
import IconBtn from '../icon-btn';
|
||||||
|
|
||||||
const propTypes = {
|
const propTypes = {
|
||||||
repoID: PropTypes.string,
|
repoID: PropTypes.string,
|
||||||
@ -52,11 +53,18 @@ class Search extends Component {
|
|||||||
isSearchInputShow: false, // for mobile
|
isSearchInputShow: false, // for mobile
|
||||||
searchTypesMax: 0,
|
searchTypesMax: 0,
|
||||||
highlightSearchTypesIndex: 0,
|
highlightSearchTypesIndex: 0,
|
||||||
|
isFiltersShow: true,
|
||||||
|
isFilterControllerActive: false,
|
||||||
filters: {
|
filters: {
|
||||||
search_filename_only: false,
|
search_filename_only: false,
|
||||||
creator: [],
|
creator_list: [],
|
||||||
date: null,
|
date: {
|
||||||
suffix: '',
|
type: SEARCH_FILTER_BY_DATE_TYPE_KEY.CREATE_TIME,
|
||||||
|
value: '',
|
||||||
|
start: null,
|
||||||
|
end: null,
|
||||||
|
},
|
||||||
|
suffixes: [],
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
this.highlightRef = null;
|
this.highlightRef = null;
|
||||||
@ -73,6 +81,8 @@ class Search extends Component {
|
|||||||
document.addEventListener('keydown', this.onDocumentKeydown);
|
document.addEventListener('keydown', this.onDocumentKeydown);
|
||||||
document.addEventListener('compositionstart', this.onCompositionStart);
|
document.addEventListener('compositionstart', this.onCompositionStart);
|
||||||
document.addEventListener('compositionend', this.onCompositionEnd);
|
document.addEventListener('compositionend', this.onCompositionEnd);
|
||||||
|
const isFiltersShow = localStorage.getItem(SEARCH_FILTERS_SHOW_KEY) === 'true';
|
||||||
|
this.setState({ isFiltersShow });
|
||||||
}
|
}
|
||||||
|
|
||||||
UNSAFE_componentWillReceiveProps(nextProps) {
|
UNSAFE_componentWillReceiveProps(nextProps) {
|
||||||
@ -128,7 +138,7 @@ class Search extends Component {
|
|||||||
};
|
};
|
||||||
|
|
||||||
onFocusHandler = () => {
|
onFocusHandler = () => {
|
||||||
this.setState({ width: '570px', isMaskShow: true, isCloseShow: true });
|
this.setState({ width: '570px', isMaskShow: true });
|
||||||
this.calculateHighlightType();
|
this.calculateHighlightType();
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -364,7 +374,7 @@ class Search extends Component {
|
|||||||
if (this.state.showRecent) {
|
if (this.state.showRecent) {
|
||||||
this.setState({ showRecent: false });
|
this.setState({ showRecent: false });
|
||||||
}
|
}
|
||||||
this.setState({ value: newValue });
|
this.setState({ value: newValue, isCloseShow: newValue.length > 0 });
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
const trimmedValue = newValue.trim();
|
const trimmedValue = newValue.trim();
|
||||||
const isInRepo = this.props.repoID;
|
const isInRepo = this.props.repoID;
|
||||||
@ -492,7 +502,7 @@ class Search extends Component {
|
|||||||
items[i]['link_content'] = decodeURI(data[i].fullpath).substring(1);
|
items[i]['link_content'] = decodeURI(data[i].fullpath).substring(1);
|
||||||
items[i]['content'] = data[i].content_highlight;
|
items[i]['content'] = data[i].content_highlight;
|
||||||
items[i]['thumbnail_url'] = data[i].thumbnail_url;
|
items[i]['thumbnail_url'] = data[i].thumbnail_url;
|
||||||
items[i]['last_modified'] = data[i].last_modified || '';
|
items[i]['mtime'] = data[i].mtime || '';
|
||||||
items[i]['repo_owner_email'] = data[i].repo_owner_email || '';
|
items[i]['repo_owner_email'] = data[i].repo_owner_email || '';
|
||||||
}
|
}
|
||||||
return items;
|
return items;
|
||||||
@ -510,6 +520,18 @@ class Search extends Component {
|
|||||||
highlightIndex: 0,
|
highlightIndex: 0,
|
||||||
isSearchInputShow: false,
|
isSearchInputShow: false,
|
||||||
showRecent: true,
|
showRecent: true,
|
||||||
|
isFilterControllerActive: false,
|
||||||
|
filters: {
|
||||||
|
search_filename_only: false,
|
||||||
|
creator_list: [],
|
||||||
|
date: {
|
||||||
|
type: SEARCH_FILTER_BY_DATE_TYPE_KEY.CREATE_TIME,
|
||||||
|
value: '',
|
||||||
|
start: null,
|
||||||
|
end: null,
|
||||||
|
},
|
||||||
|
suffixes: [],
|
||||||
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -521,6 +543,7 @@ class Search extends Component {
|
|||||||
resultItems: [],
|
resultItems: [],
|
||||||
highlightIndex: 0,
|
highlightIndex: 0,
|
||||||
isSearchInputShow: false,
|
isSearchInputShow: false,
|
||||||
|
isCloseShow: false,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -681,22 +704,30 @@ class Search extends Component {
|
|||||||
filterResults = (results) => {
|
filterResults = (results) => {
|
||||||
const { filters } = this.state;
|
const { filters } = this.state;
|
||||||
return results.filter(item => {
|
return results.filter(item => {
|
||||||
if (filters.creator && filters.creator.length > 0) {
|
if (filters.creator_list && filters.creator_list.length > 0) {
|
||||||
if (!filters.creator.includes(item.repo_owner_email)) {
|
if (!filters.creator_list.some(creator => creator.email === item.repo_owner_email)) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (filters.date?.start && item.last_modified < filters.date.start) {
|
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;
|
return false;
|
||||||
}
|
}
|
||||||
if (filters.date?.end && item.last_modified > filters.date.end) {
|
if (endDate && item.mtime > endDate) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (filters.suffix && filters.suffix.length > 0) {
|
if (filters.suffixes && filters.suffixes.length > 0) {
|
||||||
const suffix = item.path.includes('.') ? item.path.split('.').pop() : '';
|
const pathParts = item.path.split('.');
|
||||||
if (!suffix.toLocaleLowerCase().includes(filters.suffix.toLocaleLowerCase())) {
|
const suffix = pathParts.length > 1 ? `.${pathParts.pop()}` : '';
|
||||||
|
if (!filters.suffixes.includes(suffix)) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -748,6 +779,12 @@ class Search extends Component {
|
|||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
|
handleFiltersShow = () => {
|
||||||
|
const { isFiltersShow } = this.state;
|
||||||
|
localStorage.setItem(SEARCH_FILTERS_SHOW_KEY, !isFiltersShow);
|
||||||
|
this.setState({ isFiltersShow: !isFiltersShow });
|
||||||
|
}
|
||||||
|
|
||||||
handleFiltersChange = (key, value) => {
|
handleFiltersChange = (key, value) => {
|
||||||
const newFilters = { ...this.state.filters, [key]: value};
|
const newFilters = { ...this.state.filters, [key]: value};
|
||||||
if (newFilters.search_filename_only !== this.state.filters.search_filename_only) {
|
if (newFilters.search_filename_only !== this.state.filters.search_filename_only) {
|
||||||
@ -759,7 +796,13 @@ class Search extends Component {
|
|||||||
this.getSearchResult(newQueryData);
|
this.getSearchResult(newQueryData);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
this.setState({ filters: newFilters }, () => this.forceUpdate());
|
|
||||||
|
let isFilterControllerActive = false;
|
||||||
|
if (newFilters.creator_list.length > 0 || newFilters.date || newFilters.suffixes.length > 0) {
|
||||||
|
isFilterControllerActive = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.setState({ filters: newFilters, isFilterControllerActive }, () => this.forceUpdate());
|
||||||
}
|
}
|
||||||
|
|
||||||
handleSelectTag = (tag) => {
|
handleSelectTag = (tag) => {
|
||||||
@ -771,9 +814,8 @@ class Search extends Component {
|
|||||||
let width = this.state.width !== 'default' ? this.state.width : '';
|
let width = this.state.width !== 'default' ? this.state.width : '';
|
||||||
let style = {'width': width};
|
let style = {'width': width};
|
||||||
const { repoID, isTagEnabled, tagsData } = this.props;
|
const { repoID, isTagEnabled, tagsData } = this.props;
|
||||||
const { isMaskShow, isResultGotten } = this.state;
|
const { isMaskShow, isResultGotten, isCloseShow, isFiltersShow, isFilterControllerActive, filters } = this.state;
|
||||||
const placeholder = `${this.props.placeholder}${isMaskShow ? '' : ` (${controlKey} + k)`}`;
|
const placeholder = `${this.props.placeholder}${isMaskShow ? '' : ` (${controlKey} + k)`}`;
|
||||||
const isFiltersShow = isMaskShow && isResultGotten;
|
|
||||||
const isTagsShow = this.props.repoID && isTagEnabled && isMaskShow && isResultGotten;
|
const isTagsShow = this.props.repoID && isTagEnabled && isMaskShow && isResultGotten;
|
||||||
return (
|
return (
|
||||||
<Fragment>
|
<Fragment>
|
||||||
@ -795,17 +837,29 @@ class Search extends Component {
|
|||||||
autoComplete="off"
|
autoComplete="off"
|
||||||
ref={this.inputRef}
|
ref={this.inputRef}
|
||||||
/>
|
/>
|
||||||
{this.state.isCloseShow &&
|
{isCloseShow &&
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
className="search-icon-right input-icon-addon sf3-font sf3-font-x-01"
|
className="search-icon-right sf3-font sf3-font-x-01"
|
||||||
onClick={this.onClearSearch}
|
onClick={this.onClearSearch}
|
||||||
aria-label={gettext('Clear search')}
|
aria-label={gettext('Clear search')}
|
||||||
></button>
|
></button>
|
||||||
}
|
}
|
||||||
|
{isMaskShow && (
|
||||||
|
<IconBtn
|
||||||
|
symbol="filter-circled"
|
||||||
|
size={20}
|
||||||
|
className={classnames('search-icon-right input-icon-addon search-filter-controller', { 'active': isFilterControllerActive })}
|
||||||
|
onClick={this.handleFiltersShow}
|
||||||
|
title={isFiltersShow ? gettext('Hide advanced search') : gettext('Show advanced search')}
|
||||||
|
aria-label={isFiltersShow ? gettext('Hide advanced search') : gettext('Show advanced search')}
|
||||||
|
tabIndex={0}
|
||||||
|
id="search-filter-controller"
|
||||||
|
/>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
{isFiltersShow &&
|
{isMaskShow && isFiltersShow &&
|
||||||
<SearchFilters onChange={this.handleFiltersChange} />
|
<SearchFilters filters={filters} onChange={this.handleFiltersChange} />
|
||||||
}
|
}
|
||||||
{isTagsShow &&
|
{isTagsShow &&
|
||||||
<SearchTags repoID={repoID} tagsData={tagsData} keyword={this.state.value} onSelectTag={this.handleSelectTag} />
|
<SearchTags repoID={repoID} tagsData={tagsData} keyword={this.state.value} onSelectTag={this.handleSelectTag} />
|
||||||
|
@ -53,3 +53,25 @@ export const TREE_PANEL_STATE_KEY = 'sf_dir_view_tree_panel_open';
|
|||||||
export const TREE_PANEL_SECTION_STATE_KEY = 'sf_dir_view_tree_panel_section_state';
|
export const TREE_PANEL_SECTION_STATE_KEY = 'sf_dir_view_tree_panel_section_state';
|
||||||
|
|
||||||
export const RECENTLY_USED_LIST_KEY = 'recently_used_list';
|
export const RECENTLY_USED_LIST_KEY = 'recently_used_list';
|
||||||
|
|
||||||
|
export const SEARCH_FILTERS_KEY = {
|
||||||
|
SEARCH_FILENAME_AND_CONTENT: 'search_filename_and_content',
|
||||||
|
SEARCH_FILENAME_ONLY: 'search_filename_only',
|
||||||
|
CREATOR_LIST: 'creator_list',
|
||||||
|
DATE: 'date',
|
||||||
|
SUFFIXES: 'suffixes',
|
||||||
|
};
|
||||||
|
|
||||||
|
export const SEARCH_FILTER_BY_DATE_OPTION_KEY = {
|
||||||
|
TODAY: 'today',
|
||||||
|
LAST_7_DAYS: 'last_7_days',
|
||||||
|
LAST_30_DAYS: 'last_30_days',
|
||||||
|
CUSTOM: 'custom',
|
||||||
|
};
|
||||||
|
|
||||||
|
export const SEARCH_FILTER_BY_DATE_TYPE_KEY = {
|
||||||
|
CREATE_TIME: 'create_time',
|
||||||
|
LAST_MODIFIED_TIME: 'last_modified_time',
|
||||||
|
};
|
||||||
|
|
||||||
|
export const SEARCH_FILTERS_SHOW_KEY = 'search_filters_show';
|
||||||
|
@ -40,6 +40,19 @@
|
|||||||
padding: 12px;
|
padding: 12px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.search-container.show .input-icon .search-icon-right.sf3-font-x-01 {
|
||||||
|
position: absolute;
|
||||||
|
top: 0;
|
||||||
|
right: 28px;
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.search-container.show .input-icon .search-icon-right.search-filter-controller.active {
|
||||||
|
color: #ff9800;
|
||||||
|
}
|
||||||
|
|
||||||
.search-icon-left {
|
.search-icon-left {
|
||||||
display: flex;
|
display: flex;
|
||||||
}
|
}
|
||||||
|
Loading…
Reference in New Issue
Block a user