diff --git a/frontend/src/components/date-and-time-picker.js b/frontend/src/components/date-and-time-picker.js index 6e5b81ffc8..f7cb8be8e1 100644 --- a/frontend/src/components/date-and-time-picker.js +++ b/frontend/src/components/date-and-time-picker.js @@ -89,7 +89,7 @@ Picker.propTypes = { showHourAndMinute: PropTypes.bool.isRequired, disabledDate: PropTypes.func.isRequired, value: PropTypes.object, - disabled: PropTypes.func.isRequired, + disabled: PropTypes.func, inputWidth: PropTypes.number.isRequired, onChange: PropTypes.func.isRequired }; diff --git a/frontend/src/components/search/search-filters/filter-by-creator.js b/frontend/src/components/search/search-filters/filter-by-creator.js new file mode 100644 index 0000000000..a5ad134bba --- /dev/null +++ b/frontend/src/components/search/search-filters/filter-by-creator.js @@ -0,0 +1,132 @@ +import React, { useCallback, useEffect, useMemo, useState } from 'react'; +import { Dropdown, DropdownItem, DropdownMenu, DropdownToggle } from 'reactstrap'; +import { gettext } from '../../../utils/constants'; +import { Utils } from '../../../utils/utils'; +import UserItem from './user-item'; +import { seafileAPI } from '../../../utils/seafile-api'; +import ModalPortal from '../../modal-portal'; +import toaster from '../../toast'; + +const FilterByCreator = ({ repoID, onSelect }) => { + const [isOpen, setIsOpen] = useState(false); + const [options, setOptions] = useState([]); + const [value, setValue] = 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) => { + setIsOpen(!isOpen); + }, [isOpen]); + + const displayOptions = useMemo(() => { + if (!searchValue) return options; + return options.filter((option) => { + return option.name.toLowerCase().includes(searchValue.toLowerCase()); + }); + }, [options, searchValue]); + + const onSelectOption = useCallback((e) => { + e.preventDefault(); + e.stopPropagation(); + const option = Utils.getEventData(e, 'toggle') ?? e.currentTarget.getAttribute('data-toggle'); + let updated = [...value]; + if (!updated.includes(option)) { + updated = [...updated, option]; + } else { + updated = updated.filter((v) => v !== option); + } + setValue(updated); + onSelect('creator', updated); + setSearchValue(''); + }, [value, onSelect]); + + const handleCancel = useCallback((v) => { + const updated = value.filter((item) => item !== v); + setValue(updated); + onSelect('creator', updated); + }, [value, onSelect]); + + useEffect(() => { + const getUsers = async () => { + try { + const res = await seafileAPI.listRepoRelatedUsers(repoID); + const users = res.data.user_list; + const options = users.map((user) => { + return { + key: user.email, + value: user.email, + name: user.name, + label: , + }; + }); + setOptions(options); + } catch (err) { + toaster.danger(Utils.getErrorMsg(err)); + } + }; + getUsers(); + }, [repoID]); + + return ( +
+ + +
{label}
+ +
+ + +
+ {value.map((v) => { + const option = options.find((o) => o.key === v); + return option && ( + handleCancel(v)} + /> + ); + })} +
+ setSearchValue(e.target.value)} + /> +
+
+ {displayOptions.map((option) => ( + e.preventDefault()} + onClick={onSelectOption} + toggle={false} + > + {option.label} + {value.includes(option.key) && } + + ))} +
+
+
+
+ ); +}; + +export default FilterByCreator; diff --git a/frontend/src/components/search/search-filters/filter-by-date.js b/frontend/src/components/search/search-filters/filter-by-date.js new file mode 100644 index 0000000000..c671d9c1d8 --- /dev/null +++ b/frontend/src/components/search/search-filters/filter-by-date.js @@ -0,0 +1,277 @@ +import React, { useCallback, useMemo, useState } from 'react'; +import { Dropdown, DropdownItem, DropdownMenu, DropdownToggle } from 'reactstrap'; +import dayjs from 'dayjs'; +import { gettext } from '../../../utils/constants'; +import { Utils } from '../../../utils/utils'; +import Picker from '../../date-and-time-picker'; +import ModalPortal from '../../modal-portal'; + +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 FilterByDate = ({ onSelect }) => { + const [value, setValue] = useState(''); + const [isOpen, setIsOpen] = useState(false); + const [isTypeOpen, setIsTypeOpen] = useState(false); + const [isCustomDate, setIsCustomDate] = useState(false); + const [customDate, setCustomDate] = useState({ + start: null, + end: null, + }); + const [type, setType] = useState(DATE_FILTER_TYPE_KEY.CREATE_TIME); + + const typeLabel = useMemo(() => { + switch (type) { + case DATE_FILTER_TYPE_KEY.CREATE_TIME: + return gettext('Create time'); + case DATE_FILTER_TYPE_KEY.LAST_MODIFIED_TIME: + return gettext('Last modified time'); + default: + return gettext('Create time'); + } + }, [type]); + + const typeOptions = useMemo(() => { + return [ + { + key: DATE_FILTER_TYPE_KEY.CREATE_TIME, + label: gettext('Create time'), + }, { + key: DATE_FILTER_TYPE_KEY.LAST_MODIFIED_TIME, + label: gettext('Last modified time'), + } + ]; + }, []); + + const label = useMemo(() => { + if (!value || value.length === 0) return gettext('Date'); + const formatDate = (date) => dayjs(date).format('YYYY-MM-DD'); + const today = dayjs(); + const prefix = `${typeLabel}: `; + + switch (value) { + case DATE_OPTION_KEY.TODAY: + return `${prefix}${formatDate(today)}`; + case DATE_OPTION_KEY.LAST_7_DAYS: + return `${prefix}${formatDate(today.subtract(6, 'day'))} - ${formatDate(today)}`; + case DATE_OPTION_KEY.LAST_30_DAYS: + return `${prefix}${formatDate(today.subtract(29, 'day'))} - ${formatDate(today)}`; + case DATE_OPTION_KEY.CUSTOM: + return customDate.start && customDate.end + ? `${prefix}${formatDate(customDate.start)} - ${formatDate(customDate.end)}` + : gettext('Select date range'); + default: + return gettext('Date'); + } + }, [value, customDate, typeLabel]); + + const options = useMemo(() => { + return [ + { + key: DATE_OPTION_KEY.TODAY, + label: gettext('Today'), + }, { + key: DATE_OPTION_KEY.LAST_7_DAYS, + label: gettext('Last 7 days'), + }, { + key: DATE_OPTION_KEY.LAST_30_DAYS, + label: gettext('Last 30 days'), + }, + 'Divider', + { + key: DATE_OPTION_KEY.CUSTOM, + label: gettext('Custom time'), + }, + ]; + }, []); + + const toggle = useCallback(() => setIsOpen(!isOpen), [isOpen]); + + const toggleType = useCallback(() => setIsTypeOpen(!isTypeOpen), [isTypeOpen]); + + const onClearDate = useCallback(() => { + setValue(''); + setIsCustomDate(false); + onSelect('date', ''); + }, [onSelect]); + + const onOptionClick = useCallback((e) => { + const option = Utils.getEventData(e, 'toggle') ?? e.currentTarget.getAttribute('data-toggle'); + const today = dayjs().endOf('day'); + + switch (option) { + case DATE_OPTION_KEY.TODAY: { + setValue(option); + setIsCustomDate(false); + onSelect('date', { + start: dayjs().startOf('day').unix(), + end: today.unix() + }); + break; + } + case DATE_OPTION_KEY.LAST_7_DAYS: { + setValue(option); + setIsCustomDate(false); + onSelect('date', { + start: dayjs().subtract(6, 'day').startOf('day').unix(), + end: today.unix() + }); + break; + } + case DATE_OPTION_KEY.LAST_30_DAYS: { + setValue(option); + setIsCustomDate(false); + onSelect('date', { + start: dayjs().subtract(30, 'day').startOf('day').unix(), + end: today.unix() + }); + break; + } + case DATE_OPTION_KEY.CUSTOM: { + setValue(DATE_OPTION_KEY.CUSTOM); + setIsCustomDate(true); + break; + } + } + }, [onSelect]); + + const disabledStartDate = useCallback((startDate) => { + if (!startDate) return false; + const today = dayjs(); + const endValue = customDate.end; + + if (!endValue) { + return startDate.isAfter(today); + } + return endValue.isBefore(startDate) || startDate.isAfter(today); + }, [customDate]); + + const disabledEndDate = useCallback((endDate) => { + if (!endDate) return false; + const today = dayjs(); + const startValue = customDate.start; + if (!startValue) { + return endDate.isAfter(today); + } + return endDate.isBefore(startValue) || endDate.isAfter(today); + }, [customDate]); + + const onChangeCustomDate = useCallback((date) => { + const newDate = { + ...customDate, + [date.type]: date.value, + }; + setCustomDate(newDate); + + if (newDate.start && newDate.end) { + onSelect('date', { + start: newDate.start.unix(), + end: newDate.end.unix(), + }); + } + }, [customDate, onSelect]); + + return ( +
+ + +
{label}
+ { + e.stopPropagation(); + toggle(); + }} + /> +
+ + +
+ + +
{typeLabel}
+ { + e.stopPropagation(); + toggleType(); + }} + /> +
+ + {typeOptions.map((option) => { + const isSelected = option.key === type; + return ( + setType(option.key)}> + {option.label} + {isSelected && } + + ); + })} + +
+
+ +
+
+ {options.map((option, i) => { + const isSelected = option.key === value; + if (option === 'Divider') return
; + return ( + e.preventDefault()} + onClick={onOptionClick} + toggle={false} + > + {option.label} + {isSelected && } + + ); + })} + {isCustomDate && ( +
+
+
{gettext('Start date')}
+ onChangeCustomDate({ type: 'start', value })} + inputWidth={DATE_INPUT_WIDTH} + /> +
+
+
{gettext('End date')}
+ onChangeCustomDate({ type: 'end', value })} + inputWidth={DATE_INPUT_WIDTH} + /> +
+
+ )} +
+
+
+
+ ); +}; + +export default FilterByDate; diff --git a/frontend/src/components/search/search-filters/filter-by-suffix.js b/frontend/src/components/search/search-filters/filter-by-suffix.js new file mode 100644 index 0000000000..c3f1015beb --- /dev/null +++ b/frontend/src/components/search/search-filters/filter-by-suffix.js @@ -0,0 +1,60 @@ +import React, { useCallback, useMemo, useRef, useState } from 'react'; +import { Dropdown, DropdownMenu, DropdownToggle } from 'reactstrap'; +import { gettext } from '../../../utils/constants'; +import ModalPortal from '../../modal-portal'; + +const FilterBySuffix = ({ onSelect }) => { + const [isOpen, setIsOpen] = useState(false); + const [value, setValue] = useState(''); + 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 handleInput = useCallback((e) => { + setValue(e.target.value); + onSelect('suffix', e.target.value); + }, [onSelect]); + + const handleKeyDown = useCallback((e) => { + e.stopPropagation(); + if (e.key === 'Enter') { + setIsOpen(false); + } + }, []); + + return ( +
+ + +
{label}
+ {!value && } +
+ + +
+ +
+
+
+
+
+ ); +}; + +export default FilterBySuffix; diff --git a/frontend/src/components/search/search-filters/filter-by-text.js b/frontend/src/components/search/search-filters/filter-by-text.js new file mode 100644 index 0000000000..52a6a2a395 --- /dev/null +++ b/frontend/src/components/search/search-filters/filter-by-text.js @@ -0,0 +1,63 @@ +import React, { useCallback, useMemo, useState } from 'react'; +import { Dropdown, DropdownItem, DropdownMenu, DropdownToggle } from 'reactstrap'; +import ModalPortal from '../../../components/modal-portal'; +import { Utils } from '../../../utils/utils'; +import { gettext } from '../../../utils/constants'; + +const TEXT_FILTER_KEY = { + SEARCH_FILENAME_AND_CONTENT: 'search_filename_and_content', + SEARCH_FILENAME_ONLY: 'search_filename_only', +}; + +const FilterByText = ({ onSelect }) => { + const [isOpen, setIsOpen] = useState(false); + const [value, setValue] = useState(TEXT_FILTER_KEY.SEARCH_FILENAME_AND_CONTENT); + + const options = useMemo(() => { + return [ + { + key: TEXT_FILTER_KEY.SEARCH_FILENAME_AND_CONTENT, + label: gettext('Search filename and content'), + }, { + key: TEXT_FILTER_KEY.SEARCH_FILENAME_ONLY, + label: gettext('Search filename only'), + } + ]; + }, []); + const toggle = useCallback(() => setIsOpen(!isOpen), [isOpen]); + + const onOptionClick = useCallback((e) => { + const option = Utils.getEventData(e, 'toggle') ?? e.currentTarget.getAttribute('data-toggle'); + setValue(option); + const isSearchFilenameOnly = option === TEXT_FILTER_KEY.SEARCH_FILENAME_ONLY; + onSelect('search_filename_only', isSearchFilenameOnly); + }, [onSelect]); + + const label = options.find((option) => option.key === value).label; + + return ( +
+ + +
{label}
+ +
+ + + {options.map((option) => { + const isSelected = option.key === value; + return ( + + {option.label} + {isSelected && } + + ); + })} + + +
+
+ ); +}; + +export default FilterByText; diff --git a/frontend/src/components/search/search-filters/index.css b/frontend/src/components/search/search-filters/index.css new file mode 100644 index 0000000000..935eabe737 --- /dev/null +++ b/frontend/src/components/search/search-filters/index.css @@ -0,0 +1,185 @@ +.search-filters-container { + min-height: 32px; + position: relative; + display: flex; + justify-content: flex-start; + align-items: flex-start; + margin-top: 4px; + padding: 0 16px; + overflow: auto hidden; + scrollbar-color: #C1C1C1 rgba(0, 0, 0, 0); +} + +.search-filters-container .search-filter { + width: fit-content; + display: flex; + align-items: center; + 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-filter-menu .search-filter-toggle { + display: flex; + align-items: center; + cursor: pointer; + padding: 2px 4px; +} + +.search-filters-container .search-filter .search-filter-toggle:hover, +.search-filter-menu .search-filter-toggle:hover { + background-color: #efefef; + border-radius: 3px; +} + +.search-filters-container .search-filter .filter-label { + width: fit-content; + display: inline-block; + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; +} + +.search-filters-container .search-filter.filter-by-suffix-container .filter-label, +.search-filters-container .search-filter.filter-by-creator-container .filter-label { + max-width: 120px; +} + +.search-filters-container .dropdown-menu { + max-height: 400px; +} + +.search-filter-menu.filter-by-text-menu, +.search-filter-menu.filter-by-date-menu { + width: 280px; +} + +.search-filters-container .search-filters-dropdown-item { + width: 100%; +} + +.search-filter-menu { + z-index: 1050; +} + +.search-filter-menu .dropdown-item { + position: relative; + display: flex; + align-items: center; +} + +.search-filter-menu .dropdown-item .dropdown-item-tick { + width: 1rem; + left: auto; + right: 16px; +} + +.search-filter-menu .input-container { + position: relative; + width: 240px; + border: 1px solid #eaeaea; + border-radius: 4px; + padding: 4px 6px; + margin: 8px 16px; + min-height: 28px; + display: flex; + flex-wrap: wrap; + gap: 4px; +} + +.search-filter-menu .input-container .search-input-wrapper { + display: flex; + align-items: center; + border: none; + padding: 0px; + width: auto; + background: transparent; + font-size: inherit; + line-height: 20px; + flex: 1 1 60px; + min-width: 60px; + border-radius: 4px; +} + +.search-filter-menu .input-container input { + border: none; + outline: none; + width: 100%; + display: block; + resize: none; + padding: 0px; + height: 20px; +} + +.search-filter-menu .user-item { + height: 20px; + position: relative; + display: inline-flex; + align-items: center; + justify-content: flex-start; +} + +.search-filter-menu .user-item .user-avatar { + width: 16px; + height: 16px; + margin-right: 4px; + border-radius: 50%; +} + +.search-filter-menu .input-container .user-item { + background-color: #eaeaea; + border-radius: 10px; + padding: 0 4px 0 2px; + margin-right: 2px; + font-size: 13px; +} + +.filter-by-date-menu .filter-by-date-menu-toolbar { + display: flex; + align-items: center; + justify-content: space-between; + padding: 4px 12px; +} + +.filter-by-date-menu .filter-by-date-menu-toolbar .filter-by-date-type-toggle { + display: flex; + align-items: center; +} + +.filter-by-date-menu .filter-by-date-menu-toolbar .delete-btn { + display: flex; + align-items: center; + justify-content: center; + margin-right: 4px; + cursor: pointer; +} + +.filter-by-date-menu .filter-by-date-custom-date-container { + width: 100%; + display: flex; + align-items: center; + justify-content: space-between; + padding: 4px 16px; + gap: 8px; +} + +.filter-by-date-menu .filter-by-date-custom-date-container .custom-date-container { + display: flex; + flex-direction: column; + justify-content: flex-start; + align-items: flex-start; + font-size: 14px; +} + +.filter-by-date-menu .filter-by-date-custom-date-container .custom-date-container .custom-date-label { + margin-bottom: 6px; +} + +.filter-by-date-menu .filter-by-date-custom-date-container .custom-date-container .form-control { + height: 28px; + font-size: 14px; +} diff --git a/frontend/src/components/search/search-filters/index.js b/frontend/src/components/search/search-filters/index.js new file mode 100644 index 0000000000..9cd54aee7b --- /dev/null +++ b/frontend/src/components/search/search-filters/index.js @@ -0,0 +1,29 @@ +import React from 'react'; +import PropTypes from 'prop-types'; +import FilterByText from './filter-by-text'; +import FilterByCreator from './filter-by-creator'; +import FilterByDate from './filter-by-date'; +import FilterBySuffix from './filter-by-suffix'; + +import './index.css'; + +const SCROLLABLE_CONTAINER_HEIGHT = 44; + +const SearchFilters = ({ repoID, onChange, hasFileSearch }) => { + return ( +
+ {hasFileSearch && } + {hasFileSearch && } + {hasFileSearch && } + +
+ ); +}; + +SearchFilters.propTypes = { + repoID: PropTypes.string, + onChange: PropTypes.func, + hasFileSearch: PropTypes.bool, +}; + +export default SearchFilters; diff --git a/frontend/src/components/search/search-filters/user-item.js b/frontend/src/components/search/search-filters/user-item.js new file mode 100644 index 0000000000..961bad49e4 --- /dev/null +++ b/frontend/src/components/search/search-filters/user-item.js @@ -0,0 +1,15 @@ +import React from 'react'; +import { mediaUrl } from '../../../utils/constants'; +import IconBtn from '../../icon-btn'; + +const UserItem = ({ user, isCancellable, onCancel }) => { + return ( +
+ {user.name} + {user.name} + {isCancellable && onCancel(e, user)} symbol="x-01" />} +
+ ); +}; + +export default UserItem; diff --git a/frontend/src/components/search/search.js b/frontend/src/components/search/search.js index 7a3ad5e234..bc06a31573 100644 --- a/frontend/src/components/search/search.js +++ b/frontend/src/components/search/search.js @@ -13,6 +13,7 @@ import toaster from '../toast'; import Loading from '../loading'; import { SEARCH_MASK, SEARCH_CONTAINER } from '../../constants/zIndexes'; import { PRIVATE_FILE_TYPE } from '../../constants'; +import SearchFilters from './search-filters'; const propTypes = { repoID: PropTypes.string, @@ -21,6 +22,7 @@ const propTypes = { onSearchedClick: PropTypes.func.isRequired, isPublic: PropTypes.bool, isViewFile: PropTypes.bool, + hasFileSearch: PropTypes.bool, }; const PER_PAGE = 20; @@ -49,6 +51,12 @@ class Search extends Component { isSearchInputShow: false, // for mobile searchTypesMax: 0, highlightSearchTypesIndex: 0, + filters: { + search_filename_only: false, + creator: [], + date: null, + suffix: '', + }, }; this.highlightRef = null; this.source = null; // used to cancel request; @@ -415,7 +423,7 @@ class Search extends Component { this.queryData = queryData; if (isPublic) { - seafileAPI.searchFilesInPublishedRepo(queryData.search_repo, queryData.q, page, PER_PAGE).then(res => { + seafileAPI.searchFilesInPublishedRepo(queryData.search_repo, queryData.q, page, PER_PAGE, queryData.search_filename_only).then(res => { this.source = null; if (res.data.total > 0) { this.setState({ @@ -483,6 +491,8 @@ class Search extends Component { items[i]['link_content'] = decodeURI(data[i].fullpath).substring(1); items[i]['content'] = data[i].content_highlight; items[i]['thumbnail_url'] = data[i].thumbnail_url; + items[i]['last_modified'] = data[i].last_modified || ''; + items[i]['repo_owner_email'] = data[i].repo_owner_email || ''; } return items; } @@ -524,6 +534,7 @@ class Search extends Component { } } + const filteredItems = this.filterResults(resultItems); if (isLoading) { return ; } @@ -533,8 +544,8 @@ class Search extends Component { else if (!isResultGotten) { return this.renderSearchTypes(this.state.inputValue.trim()); } - else if (resultItems.length > 0) { - return this.renderResults(resultItems); + else if (filteredItems.length > 0) { + return this.renderResults(filteredItems); } else { return
{gettext('No results matching')}
; @@ -632,36 +643,66 @@ class Search extends Component { } searchRepo = () => { - const { value } = this.state; + const { value, filters } = this.state; const queryData = { q: value, search_repo: this.props.repoID, search_ftypes: 'all', + search_filename_only: filters.search_filename_only, }; this.getSearchResult(queryData); }; searchFolder = () => { - const { value } = this.state; + const { value, filters } = this.state; const queryData = { q: value, search_repo: this.props.repoID, search_ftypes: 'all', search_path: this.props.path, + search_filename_only: filters.search_filename_only, }; this.getSearchResult(queryData); }; searchAllRepos = () => { - const { value } = this.state; + const { value, filters } = this.state; const queryData = { 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 && filters.creator.length > 0) { + if (!filters.creator.includes(item.repo_owner_email)) { + return false; + } + } + + if (filters.date?.start && item.last_modified < filters.date.start) { + return false; + } + if (filters.date?.end && item.last_modified > filters.date.end) { + return false; + } + + if (filters.suffix && filters.suffix.length > 0) { + const suffix = item.path.includes('.') ? item.path.split('.').pop() : ''; + if (!suffix.toLocaleLowerCase().includes(filters.suffix.toLocaleLowerCase())) { + return false; + } + } + + return true; + }); + }; + renderResults = (resultItems, isVisited) => { const { highlightIndex } = this.state; @@ -704,11 +745,26 @@ class Search extends Component { }); }; + 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); + }); + } + this.setState({ filters: newFilters }, () => this.forceUpdate()); + } + render() { let width = this.state.width !== 'default' ? this.state.width : ''; let style = {'width': width}; const { isMaskShow } = this.state; const placeholder = `${this.props.placeholder}${isMaskShow ? '' : ` (${controlKey} + k)`}`; + const isFiltersShow = this.props.repoID && isMaskShow; return ( @@ -738,6 +794,13 @@ class Search extends Component { > } + {isFiltersShow && + + }
{ + this.setState({ hasFileSearch: res.data.has_file_search }); + }); } componentWillUnmount() { @@ -59,7 +64,7 @@ class CommonToolbar extends React.Component { }; renderSearch = () => { - const { repoID, repoName, isLibView, path, isViewFile } = this.state; + const { repoID, repoName, isLibView, path, isViewFile, hasFileSearch } = this.state; const { searchPlaceholder } = this.props; const placeholder = searchPlaceholder || gettext('Search files'); @@ -72,6 +77,7 @@ class CommonToolbar extends React.Component { isViewFile={isViewFile} isPublic={false} path={path} + hasFileSearch={hasFileSearch} /> ); } else { diff --git a/frontend/src/css/search.css b/frontend/src/css/search.css index 27e19608f2..b1a11bf30f 100644 --- a/frontend/src/css/search.css +++ b/frontend/src/css/search.css @@ -22,9 +22,13 @@ box-shadow: 0 3px 8px 0 rgba(116, 129, 141, 0.1); background-color: #fff; cursor: default; - overflow: hidden; width: 600px; - padding: 1rem 0rem 0rem 1rem; + padding: 16px 0; +} + +.search-container .input-icon { + position: relative; + margin: 0 16px; } .search-container .input-icon svg.input-icon-addon { @@ -47,7 +51,7 @@ min-width: 20px; color: #666; height: 20px; - margin: 9px 25px 9px 0; + margin: 9px; border: 0; background-color: transparent; padding: 0; @@ -87,6 +91,7 @@ border-radius: 0 0 3px 3px; box-shadow: 0 3px 8px 0 rgba(116, 129, 141, 0.1); top: 60px; + padding: 0 16px; } .dropdown-search-result-container { diff --git a/frontend/src/utils/seafile-api.js b/frontend/src/utils/seafile-api.js index 6602a2f7d1..d5088617b0 100644 --- a/frontend/src/utils/seafile-api.js +++ b/frontend/src/utils/seafile-api.js @@ -815,13 +815,19 @@ class SeafileAPI { return source; } - searchFilesInPublishedRepo(repoID, q, page, perPage) { + getSearchInfo() { + const url = this.server + '/api2/search-info/'; + return this.req.get(url); + } + + searchFilesInPublishedRepo(repoID, q, page, perPage, searchFilenameOnly) { const url = this.server + '/api/v2.1/published-repo-search/'; let params = { repo_id: repoID, q: q, page: page, - per_page: perPage + per_page: perPage, + search_filename_only: searchFilenameOnly || false, }; return this.req.get(url, { params: params }); } diff --git a/seahub/api2/urls.py b/seahub/api2/urls.py index 51c118490d..38a7d45f06 100644 --- a/seahub/api2/urls.py +++ b/seahub/api2/urls.py @@ -30,6 +30,7 @@ urlpatterns = [ re_path(r'^accounts/(?P\S+@[a-zA-Z0-9._-]+\.[a-zA-Z0-9._-]+)/$', Account.as_view(), name="api2-account"), path('account/info/', AccountInfo.as_view()), path('regdevice/', RegDevice.as_view(), name="regdevice"), + path('search-info/', SearchInfoView.as_view(), name="search-info"), path('search/', Search.as_view(), name='api_search'), path('items-search/', ItemsSearch.as_view(), name='api-items-search'), path('search-user/', SearchUser.as_view(), name='search-user'), diff --git a/seahub/api2/views.py b/seahub/api2/views.py index 8263ffabbc..194e66ea7e 100644 --- a/seahub/api2/views.py +++ b/seahub/api2/views.py @@ -438,6 +438,15 @@ class RegDevice(APIView): return Response("success") +class SearchInfoView(APIView): + @json_response + def get(self, request, format=None): + return { + 'has_file_search': HAS_FILE_SEARCH, + 'has_file_seasearch': HAS_FILE_SEASEARCH + } + + class Search(APIView): """ Search all the repos """