1
0
mirror of https://github.com/haiwen/seahub.git synced 2025-09-27 15:54:39 +00:00
Files
seahub/frontend/src/components/search/search.js
llj 67c6fbd5b7 Hover title (#7988)
* ['share' dialog] 'Share Link' panel: display full link when hover on the 'cut' link in the link list

* [user side panel] 'Files' sub nav items: added '.ellipsis' and display full text when hover on them

* ['library view' page] added title for the 'open/close the panel' icon

* ['library view' page] folder/file items: added title to the 'Star/Unstar' icon

* ['library view' page] improved the path bar(display full text when hover on the path items, and etc.)

* ['Properties' popover] added titles for the 'settings' icon & the 'close' icon

* [the top-right search] added title for the 'clear' icon

* [dialogs] added title for the 'search' & 'close' icons in dialogs/popovers
2025-06-27 21:02:25 +08:00

1007 lines
34 KiB
JavaScript

import React, { Component, Fragment } from 'react';
import axios from 'axios';
import PropTypes from 'prop-types';
import isHotkey from 'is-hotkey';
import classnames from 'classnames';
import MediaQuery from 'react-responsive';
import { seafileAPI } from '../../utils/seafile-api';
import searchAPI from '../../utils/search-api';
import { gettext } from '../../utils/constants';
import SearchResultItem from './search-result-item';
import SearchResultLibrary from './search-result-library';
import { debounce, 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_KEY, SEARCH_FILTERS_SHOW_KEY } from '../../constants';
import SearchFilters from './search-filters';
import SearchTags from './search-tags';
import IconBtn from '../icon-btn';
import SearchedItemDetails from './details';
import { CollaboratorsProvider } from '../../metadata';
const propTypes = {
repoID: PropTypes.string,
path: PropTypes.string,
placeholder: PropTypes.string,
onSearchedClick: PropTypes.func.isRequired,
isPublic: PropTypes.bool,
isViewFile: PropTypes.bool,
onSelectTag: PropTypes.func,
};
const PER_PAGE = 20;
const controlKey = Utils.isMac() ? '⌘' : 'Ctrl';
const isEnter = isHotkey('enter');
const isUp = isHotkey('up');
const isDown = isHotkey('down');
class Search extends Component {
constructor(props) {
super(props);
this.state = {
width: 'default',
value: '',
inputValue: '',
resultItems: [],
highlightIndex: 0,
page: 0,
isLoading: false,
isMaskShow: false,
showRecent: true,
isResultGotten: false,
isCloseShow: false,
isSearchInputShow: false, // for mobile
searchTypesMax: 0,
highlightSearchTypesIndex: 0,
isFiltersShow: false,
isFilterControllerActive: false,
filters: {
search_filename_only: false,
creator_list: [],
date: {
type: SEARCH_FILTER_BY_DATE_TYPE_KEY.CREATE_TIME,
value: '',
from: null,
to: null,
},
suffixes: '',
},
visitedItems: [],
};
this.highlightRef = null;
this.source = null; // used to cancel request;
this.inputRef = React.createRef();
this.searchContainer = React.createRef();
this.searchResultListRef = React.createRef();
this.isChineseInput = false;
this.searchResultListContainerRef = React.createRef();
this.calculateStoreKey(props);
this.timer = null;
}
componentDidMount() {
document.addEventListener('keydown', this.onDocumentKeydown);
document.addEventListener('compositionstart', this.onCompositionStart);
document.addEventListener('compositionend', this.onCompositionEnd);
const isFiltersShow = localStorage.getItem(SEARCH_FILTERS_SHOW_KEY) === 'true';
const visitedItems = JSON.parse(localStorage.getItem(this.storeKey)) || [];
this.setState({ isFiltersShow, visitedItems });
}
UNSAFE_componentWillReceiveProps(nextProps) {
this.calculateStoreKey(nextProps);
this.isChineseInput = false;
}
componentDidUpdate(prevProps, prevState) {
if (this.state.isMaskShow && !prevState.isMaskShow) {
const visitedItems = JSON.parse(localStorage.getItem(this.storeKey)) || [];
if (visitedItems !== prevState.visitedItems) {
this.setState({ visitedItems });
}
}
}
componentWillUnmount() {
document.removeEventListener('keydown', this.onDocumentKeydown);
document.removeEventListener('compositionstart', this.onCompositionStart);
document.removeEventListener('compositionend', this.onCompositionEnd);
this.isChineseInput = false;
}
calculateStoreKey = (props) => {
const { repoID } = props;
let storeKey = 'sfVisitedSearchItems';
if (repoID) {
storeKey += repoID;
}
this.storeKey = storeKey;
};
onCompositionStart = () => {
this.isChineseInput = true;
};
onCompositionEnd = () => {
this.isChineseInput = false;
};
onDocumentKeydown = (e) => {
if (isHotkey('mod+k')(e)) {
e.preventDefault();
this.onFocusHandler();
if (this.inputRef && this.inputRef.current) {
this.inputRef.current.focus();
}
}
if (this.state.isMaskShow) {
if (isHotkey('esc', e)) {
e.preventDefault();
this.inputRef && this.inputRef.current && this.inputRef.current.blur();
this.resetToDefault();
} else if (isEnter(e)) {
this.onEnter(e);
} else if (isUp(e)) {
this.onUp(e);
} else if (isDown(e)) {
this.onDown(e);
}
}
};
onFocusHandler = () => {
this.setState({ width: '100%', isMaskShow: true });
this.calculateHighlightType();
};
onCloseHandler = () => {
this.resetToDefault();
};
onUp = (e) => {
e.preventDefault();
e.stopPropagation();
const { highlightIndex, resultItems, isResultGotten } = this.state;
// 01 init search, display and highlight recent search results
if (this.state.showRecent) {
if (highlightIndex > 0) {
this.setState({ highlightIndex: highlightIndex - 1 }, () => {
if (this.highlightRef) {
const { top, height } = this.highlightRef.getBoundingClientRect();
if (top - height < 0) {
this.searchResultListContainerRef.current.scrollTop -= height;
}
}
});
}
return;
}
// 02 global search, display and highlight searched repos
if (!this.props.repoID && resultItems.length > 0 && !isResultGotten) {
let highlightSearchTypesIndex = this.state.highlightSearchTypesIndex - 1;
if (highlightSearchTypesIndex < 0) {
highlightSearchTypesIndex = resultItems.length;
}
this.setState({ highlightSearchTypesIndex }, () => {
if (this.highlightRef) {
const { top, height } = this.highlightRef.getBoundingClientRect();
if (top - height < 0) {
this.searchResultListContainerRef.current.scrollTop -= height;
}
}
});
return;
}
// 03 Internal repo search, highlight search types
if (!isResultGotten) {
let highlightSearchTypesIndex = this.state.highlightSearchTypesIndex - 1;
if (highlightSearchTypesIndex < 0) {
highlightSearchTypesIndex = this.state.searchTypesMax;
}
this.setState({ highlightSearchTypesIndex });
return;
}
// 04 When there are search results, highlight searched items
if (highlightIndex > 0) {
this.setState({ highlightIndex: highlightIndex - 1 }, () => {
if (this.highlightRef) {
const { top, height } = this.highlightRef.getBoundingClientRect();
if (top - height < 0) {
this.searchResultListContainerRef.current.scrollTop -= height;
}
}
});
}
};
onDown = (e) => {
e.preventDefault();
e.stopPropagation();
const { highlightIndex, resultItems, isResultGotten } = this.state;
// 01 init search, display and highlight recent search results
if (this.state.showRecent) {
const visitedItems = JSON.parse(localStorage.getItem(this.storeKey)) || [];
if (highlightIndex < visitedItems.length - 1) {
this.setState({ highlightIndex: highlightIndex + 1 }, () => {
if (this.highlightRef) {
const { top, height } = this.highlightRef.getBoundingClientRect();
const outerHeight = 300;
if (top > outerHeight) {
const newScrollTop = this.searchResultListContainerRef.current.scrollTop + height;
this.searchResultListContainerRef.current.scrollTop = newScrollTop;
}
}
});
}
return;
}
// 02 global search, display and highlight searched repos
if (!this.props.repoID && resultItems.length > 0 && !isResultGotten) {
let highlightSearchTypesIndex = this.state.highlightSearchTypesIndex + 1;
if (highlightSearchTypesIndex > resultItems.length) {
highlightSearchTypesIndex = 0;
}
this.setState({ highlightSearchTypesIndex }, () => {
if (this.highlightRef) {
const { top, height } = this.highlightRef.getBoundingClientRect();
const outerHeight = 300;
if (top > outerHeight) {
const newScrollTop = this.searchResultListContainerRef.current.scrollTop + height;
this.searchResultListContainerRef.current.scrollTop = newScrollTop;
}
}
});
return;
}
// 03 Internal repo search, highlight search types
if (!this.state.isResultGotten) {
let highlightSearchTypesIndex = this.state.highlightSearchTypesIndex + 1;
if (highlightSearchTypesIndex > this.state.searchTypesMax) {
highlightSearchTypesIndex = 0;
}
this.setState({ highlightSearchTypesIndex });
return;
}
// 04 When there are search results, highlight searched items
if (highlightIndex < resultItems.length - 1) {
this.setState({ highlightIndex: highlightIndex + 1 }, () => {
if (this.highlightRef) {
const { top, height } = this.highlightRef.getBoundingClientRect();
const outerHeight = 300;
if (top > outerHeight) {
const newScrollTop = this.searchResultListContainerRef.current.scrollTop + height;
this.searchResultListContainerRef.current.scrollTop = newScrollTop;
}
}
});
}
};
onEnter = (e) => {
e.preventDefault();
if (this.state.showRecent) {
const visitedItems = JSON.parse(localStorage.getItem(this.storeKey)) || [];
const item = visitedItems[this.state.highlightIndex];
if (item) {
if (document.activeElement) {
document.activeElement.blur();
}
this.onItemClickHandler(item);
}
return;
}
// global searching, searched repos needs to support enter
const { highlightSearchTypesIndex, resultItems, isResultGotten } = this.state;
if (!this.props.repoID && resultItems.length > 0 && !isResultGotten) {
if (highlightSearchTypesIndex === 0) {
this.searchAllRepos();
} else {
let item = this.state.resultItems[highlightSearchTypesIndex - 1];
if (item) {
if (document.activeElement) {
document.activeElement.blur();
}
this.onItemClickHandler(item);
}
return;
}
}
if (!this.state.isResultGotten) {
let highlightDom = document.querySelector('.search-types-highlight');
if (highlightDom) {
if (highlightDom.classList.contains('search-types-folder')) {
this.searchFolder();
}
else if (highlightDom.classList.contains('search-types-repo')) {
this.searchRepo();
}
else if (highlightDom.classList.contains('search-types-repos')) {
this.searchAllRepos();
}
return;
}
}
let item = this.state.resultItems[this.state.highlightIndex];
if (item) {
if (document.activeElement) {
document.activeElement.blur();
}
this.onItemClickHandler(item);
}
};
onItemClickHandler = (item) => {
if (item.is_dir === true) {
this.resetToDefault();
}
this.keepVisitedItem(item);
this.props.onSearchedClick(item);
};
calculateHighlightType = () => {
let searchTypesMax = 0;
const { repoID, path, isViewFile } = this.props;
if (repoID) {
searchTypesMax++;
}
if (path && path !== '/' && !isViewFile) {
searchTypesMax++;
}
this.setState({
searchTypesMax,
highlightSearchTypesIndex: 0,
});
};
keepVisitedItem = (targetItem) => {
let targetIndex;
const { repo_id: targetRepoID, path: targetPath } = targetItem;
const storeKey = 'sfVisitedSearchItems' + targetRepoID;
const items = JSON.parse(localStorage.getItem(storeKey)) || [];
for (let i = 0, len = items.length; i < len; i++) {
const { repo_id, path } = items[i];
if (repo_id == targetRepoID && path == targetPath) {
targetIndex = i;
break;
}
}
if (targetIndex != undefined) {
items.splice(targetIndex, 1);
}
items.unshift(targetItem);
if (items.length > 50) { // keep 50 items at most
items.pop();
}
localStorage.setItem(storeKey, JSON.stringify(items));
};
onChangeHandler = (event) => {
const newValue = event.target.value;
if (this.state.showRecent) {
this.setState({ showRecent: false });
}
this.setState({ value: newValue, isCloseShow: newValue.length > 0 });
setTimeout(() => {
const trimmedValue = newValue.trim();
const isInRepo = this.props.repoID;
if (this.isChineseInput === false && this.state.inputValue !== newValue) {
this.setState({
inputValue: newValue,
isLoading: false,
highlightIndex: 0,
isResultGotten: false,
}, () => {
if (!isInRepo && trimmedValue !== '') {
this.getRepoSearchResult(newValue);
}
});
}
}, 1);
};
handleError = (e) => {
if (!axios.isCancel(e)) {
let errMessage = Utils.getErrorMsg(e);
toaster.danger(errMessage);
}
this.setState({ isLoading: false });
};
getRepoSearchResult = (query_str) => {
if (this.source) {
this.source.cancel('prev request is cancelled');
}
this.source = seafileAPI.getSource();
const query_type = 'library';
let results = [];
searchAPI.searchItems(query_str, query_type, this.source.token).then(res => {
results = [...results, ...this.formatResultItems(res.data.results)];
this.setState({
resultItems: results,
isLoading: false,
});
}).catch(error => {
this.handleError(error);
});
};
getSearchResult = (queryData) => {
if (this.source) {
this.source.cancel('prev request is cancelled');
}
this.setState({
isLoading: true,
isResultGotten: false,
resultItems: [],
highlightIndex: 0,
});
this.source = seafileAPI.getSource();
this.sendRequest(queryData, this.source.token, 1);
};
sendRequest = (queryData, cancelToken, page) => {
let isPublic = this.props.isPublic;
this.queryData = queryData;
if (isPublic) {
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({
resultItems: [...this.state.resultItems, ...this.formatResultItems(res.data.results)],
isResultGotten: true,
page: page + 1,
isLoading: false,
});
} else {
this.setState({
highlightIndex: 0,
resultItems: [],
isLoading: false,
isResultGotten: true,
});
}
}).catch(error => {
this.handleError(error);
});
} else {
this.onNormalSearch(queryData, cancelToken, page);
}
};
onNormalSearch = (queryData, cancelToken, page) => {
queryData['per_page'] = PER_PAGE;
queryData['page'] = page;
seafileAPI.searchFiles(queryData, cancelToken).then(res => {
this.source = null;
if (res.data.total > 0) {
this.setState({
resultItems: [...this.state.resultItems, ...this.formatResultItems(res.data.results)],
isResultGotten: true,
isLoading: false,
page: page + 1,
});
return;
}
this.setState({
highlightIndex: 0,
resultItems: [],
isLoading: false,
isResultGotten: true,
});
}).catch(error => {
this.handleError(error);
});
};
formatResultItems(data) {
let items = [];
for (let i = 0; i < data.length; i++) {
items[i] = {};
let name = data[i].is_dir ? data[i].name : data[i].fullpath.split('/').pop();
items[i]['index'] = [i];
items[i]['name'] = name;
items[i]['path'] = data[i].fullpath;
items[i]['repo_id'] = data[i].repo_id;
items[i]['repo_name'] = data[i].repo_name;
items[i]['is_dir'] = data[i].is_dir;
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]['mtime'] = data[i].mtime || '';
items[i]['repo_owner_email'] = data[i].repo_owner_email || '';
}
return items;
}
resetToDefault() {
this.setState({
width: '',
value: '',
inputValue: '',
isMaskShow: false,
isCloseShow: false,
isResultGotten: false,
resultItems: [],
highlightIndex: 0,
isSearchInputShow: false,
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: '',
}
});
}
onClearSearch = () => {
this.setState({
value: '',
inputValue: '',
isResultGotten: false,
resultItems: [],
highlightIndex: 0,
isSearchInputShow: false,
isCloseShow: false,
});
};
renderSearchResult() {
const { resultItems, width, showRecent, isResultGotten, isLoading, visitedItems } = this.state;
if (!width || width === 'default') return null;
if (showRecent) {
if (visitedItems.length > 0) {
return this.renderResults(visitedItems, true);
}
}
const filteredItems = this.filterByCreator(resultItems);
if (isLoading) {
return <Loading />;
}
else if (this.state.inputValue.trim().length === 0) {
return <div className="search-result-none">{gettext('Type characters to start search')}</div>;
}
else if (!isResultGotten) {
return this.renderSearchTypes(this.state.inputValue.trim());
}
else if (filteredItems.length > 0) {
return this.renderResults(filteredItems);
}
else {
return (
<div className="search-result-none">
<h4 className="search-results-title text-start">{gettext('Files')}</h4>
{gettext('No results matching')}
</div>
);
}
}
renderSearchTypes = (inputValue) => {
const highlightIndex = this.state.highlightSearchTypesIndex;
const { resultItems } = this.state;
if (!this.props.repoID) {
return (
<div className="search-result-list-container" ref={this.searchResultListContainerRef}>
<div className="search-types">
<div
className={classnames('search-types-repos', { 'search-types-highlight': highlightIndex === 0 })}
onClick={this.searchAllRepos}
tabIndex={0}
>
<i className="search-icon-left input-icon-addon sf3-font sf3-font-search"></i>
{inputValue}
<span className="search-types-text">{gettext('in all libraries')}</span>
<i className="sf3-font sf3-font-enter"></i>
</div>
</div>
{resultItems.length > 0 && (
<div className="library-result-container">
<hr className="library-result-divider" />
<div className="library-result-header">{gettext('Libraries')}</div>
<ul
className="library-result-list"
ref={this.searchResultListRef}
>
{resultItems.map((item, index) => {
return (
<SearchResultLibrary
key={index}
item={item}
onClick={this.onItemClickHandler}
isHighlight={highlightIndex === index + 1}
setRef={highlightIndex === index + 1 ? (ref) => {this.highlightRef = ref;} : () => {}}
/>
);
})}
</ul>
</div>
)}
</div>
);
}
if (this.props.repoID) {
const { path } = this.props;
const isMetadataView = path && path.startsWith('/' + PRIVATE_FILE_TYPE.FILE_EXTENDED_PROPERTIES);
const isTagView = path && path.startsWith('/' + PRIVATE_FILE_TYPE.TAGS_PROPERTIES);
if (path && path !== '/' && !this.props.isViewFile && !isMetadataView && !isTagView) {
return (
<div className="search-types">
<div className={`search-types-repo ${highlightIndex === 0 ? 'search-types-highlight' : ''}`} onClick={this.searchRepo} tabIndex={0}>
<i className="search-icon-left input-icon-addon sf3-font sf3-font-search"></i>
{inputValue}
<span className="search-types-text">{gettext('in this library')}</span>
{highlightIndex === 0 && <i className="sf3-font sf3-font-enter"></i>}
</div>
<div className={`search-types-folder ${highlightIndex === 1 ? 'search-types-highlight' : ''}`} onClick={this.searchFolder} tabIndex={0}>
<i className="search-icon-left input-icon-addon sf3-font sf3-font-search"></i>
{inputValue}
<span className="search-types-text">{gettext('in this folder')}</span>
{highlightIndex === 1 && <i className="sf3-font sf3-font-enter"></i>}
</div>
<div className={`search-types-repos ${highlightIndex === 2 ? 'search-types-highlight' : ''}`} onClick={this.searchAllRepos} tabIndex={0}>
<i className="search-icon-left input-icon-addon sf3-font sf3-font-search"></i>
{inputValue}
<span className="search-types-text">{gettext('in all libraries')}</span>
{highlightIndex === 2 && <i className="sf3-font sf3-font-enter"></i>}
</div>
</div>
);
} else {
return (
<div className="search-types">
<div className={`search-types-repo ${highlightIndex === 0 ? 'search-types-highlight' : ''}`} onClick={this.searchRepo} tabIndex={0}>
<i className="search-icon-left input-icon-addon sf3-font sf3-font-search"></i>
{inputValue}
<span className="search-types-text">{gettext('in this library')}</span>
{highlightIndex === 0 && <i className="sf3-font sf3-font-enter"></i>}
</div>
<div className={`search-types-repos ${highlightIndex === 1 ? 'search-types-highlight' : ''}`} onClick={this.searchAllRepos} tabIndex={0}>
<i className="search-icon-left input-icon-addon sf3-font sf3-font-search"></i>
{inputValue}
<span className="search-types-text">{gettext('in all libraries')}</span>
{highlightIndex === 1 && <i className="sf3-font sf3-font-enter"></i>}
</div>
</div>
);
}
}
};
searchRepo = () => {
const { value } = this.state;
const queryData = {
q: value,
search_repo: this.props.repoID,
search_ftypes: 'all',
};
this.getSearchResult(this.buildSearchParams(queryData));
};
searchFolder = () => {
const { value } = this.state;
const queryData = {
q: value,
search_repo: this.props.repoID,
search_ftypes: 'all',
search_path: this.props.path,
};
this.getSearchResult(this.buildSearchParams(queryData));
};
searchAllRepos = () => {
const { value } = this.state;
const queryData = {
q: value,
search_repo: 'all',
search_ftypes: 'all',
};
this.getSearchResult(this.buildSearchParams(queryData));
};
renderDetails = (results) => {
const { highlightIndex } = this.state;
const item = results[highlightIndex];
if (!item) return null;
const repoID = item.repo_id;
const dirent = {
name: item.name,
type: item.is_dir ? 'dir' : 'file',
isLib: item.path === '/',
file_tags: [],
path: item.path
};
return (
<CollaboratorsProvider repoID={repoID}>
<SearchedItemDetails repoID={repoID} path={item.path} dirent={dirent} />
</CollaboratorsProvider>
);
};
debounceHighlight = debounce((index) => {
this.setState({ highlightIndex: index });
}, 200);
deleteItem = (item) => {
const { visitedItems } = this.state;
const update = visitedItems.filter(i => i.path !== item.path || i.repo_id !== item.repo_id);
this.setState({ visitedItems: update });
localStorage.setItem(this.storeKey, JSON.stringify(update));
};
renderResults = (resultItems, isVisited) => {
const { highlightIndex } = this.state;
const results = (
<>
{isVisited ? (
<h4 className="visited-search-results-title">{gettext('Search results visited recently')}</h4>
) : (
<h4 className="search-results-title">{gettext('Files')}</h4>
)}
<ul className="search-result-list" ref={this.searchResultListRef}>
{resultItems.map((item, index) => {
const isHighlight = index === highlightIndex;
return (
<SearchResultItem
key={index}
idx={index}
item={item}
onItemClickHandler={this.onItemClickHandler}
isHighlight={isHighlight}
setRef={isHighlight ? (ref) => {this.highlightRef = ref;} : () => {}}
onHighlightIndex={this.debounceHighlight}
timer={this.timer}
onSetTimer={(timer) => {this.timer = timer;}}
onDeleteItem={this.deleteItem}
/>
);
})}
</ul>
</>
);
return (
<>
<MediaQuery query="(min-width: 768px)">
<div className="search-result-sidepanel-wrapper d-flex">
<div className="search-result-list-container" ref={this.searchResultListContainerRef}>{results}</div>
<div className="search-result-container-sidepanel d-flex flex-column flex-grow-1">
{this.renderDetails(resultItems)}
</div>
</div>
</MediaQuery>
<MediaQuery query="(max-width: 767.8px)">
{results}
</MediaQuery>
</>
);
};
onSearchToggle = () => {
this.setState({
isSearchInputShow: !this.state.isSearchInputShow,
isMaskShow: !this.state.isMaskShow,
});
};
handleFiltersShow = () => {
const { isFiltersShow } = this.state;
localStorage.setItem(SEARCH_FILTERS_SHOW_KEY, !isFiltersShow);
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 };
const hasActiveFilter = newFilters.suffixes || newFilters.creator_list.length > 0 || newFilters.date.value;
this.setState({ filters: newFilters, isFilterControllerActive: hasActiveFilter });
// build query data
if (!this.state.value || !this.state.isResultGotten) return;
const queryUpdates = {};
if (key === SEARCH_FILTERS_KEY.CREATOR_LIST) return;
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;
}
const newQueryData = {
...this.queryData,
...queryUpdates,
};
this.getSearchResult(newQueryData);
};
filterByCreator = (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;
}
}
return true;
});
};
handleSelectTag = (tag) => {
this.props.onSelectTag(tag);
this.resetToDefault();
};
render() {
let width = this.state.width !== 'default' ? this.state.width : '';
let style = { 'width': width };
const { repoID, isTagEnabled, tagsData } = this.props;
const { isMaskShow, isResultGotten, isCloseShow, isFiltersShow, isFilterControllerActive, filters } = this.state;
const placeholder = `${this.props.placeholder}${isMaskShow ? '' : ` (${controlKey} + k)`}`;
const isTagsShow = this.props.repoID && isTagEnabled && isMaskShow && isResultGotten;
return (
<Fragment>
<MediaQuery query="(min-width: 768px)">
<div className="search">
<div className={`search-mask ${isMaskShow ? 'show' : 'hide'}`} onClick={this.onCloseHandler} style={isMaskShow ? { zIndex: SEARCH_MASK } : {}}></div>
<div className={`search-container ${isMaskShow ? 'show' : ''}`} style={isMaskShow ? { zIndex: SEARCH_CONTAINER } : {}}>
<div className={`input-icon ${isMaskShow ? 'mb-1' : ''}`}>
<i className="search-icon-left input-icon-addon sf3-font sf3-font-search"></i>
<input
type="text"
className="form-control search-input"
name="query"
placeholder={placeholder}
style={style}
value={this.state.value}
onFocus={this.onFocusHandler}
onChange={this.onChangeHandler}
autoComplete="off"
ref={this.inputRef}
/>
{isCloseShow &&
<button
type="button"
className="search-icon-right sf3-font sf3-font-x-01"
onClick={this.onClearSearch}
aria-label={gettext('Clear search')}
title={gettext('Clear search')}
>
</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>
{isMaskShow && isFiltersShow &&
<SearchFilters filters={filters} onChange={this.handleFiltersChange} />
}
{isTagsShow &&
<SearchTags repoID={repoID} tagsData={tagsData} keyword={this.state.value} onSelectTag={this.handleSelectTag} />
}
<div
className="search-result-container dropdown-search-result-container"
ref={this.searchContainer}
>
{this.renderSearchResult()}
</div>
</div>
</div>
</MediaQuery>
<MediaQuery query="(max-width: 767.8px)">
<div className="search-icon-container">
<i className="search-icon sf3-font sf3-font-search" onClick={this.onSearchToggle}></i>
</div>
{this.state.isSearchInputShow &&
<div className="search">
<div className={`search-mask ${isMaskShow ? '' : 'hide'}`} onClick={this.onCloseHandler} style={{ zIndex: SEARCH_MASK }}></div>
<div className="search-container" style={{ zIndex: SEARCH_CONTAINER }}>
<div className="input-icon">
<i className="search-icon-left input-icon-addon sf3-font sf3-font-search"></i>
<input
type="text"
className="form-control search-input"
name="query"
placeholder={placeholder}
style={style}
value={this.state.value}
onFocus={this.onFocusHandler}
onChange={this.onChangeHandler}
autoComplete="off"
/>
{this.state.isCloseShow &&
<button
type="button"
className="search-icon-right input-icon-addon sf3-font sf3-font-x-01"
onClick={this.onClearSearch}
aria-label={gettext('Clear search')}
title={gettext('Clear search')}
>
</button>
}
</div>
<div className="search-result-container dropdown-search-result-container">
{this.renderSearchResult()}
</div>
</div>
</div>
}
</MediaQuery>
</Fragment>
);
}
}
Search.propTypes = propTypes;
export default Search;