mirror of
https://github.com/haiwen/seahub.git
synced 2025-09-03 16:10:26 +00:00
change search part (#5777)
* change search part handle search error massage add * change search delay time * change search check
This commit is contained in:
621
frontend/src/components/search/ai-search.js
Normal file
621
frontend/src/components/search/ai-search.js
Normal file
@@ -0,0 +1,621 @@
|
|||||||
|
import React, { Component, Fragment } from 'react';
|
||||||
|
import PropTypes from 'prop-types';
|
||||||
|
import isHotkey from 'is-hotkey';
|
||||||
|
import MediaQuery from 'react-responsive';
|
||||||
|
import { seafileAPI } from '../../utils/seafile-api';
|
||||||
|
import { gettext, siteRoot } from '../../utils/constants';
|
||||||
|
import SearchResultItem from './search-result-item';
|
||||||
|
import { Utils } from '../../utils/utils';
|
||||||
|
import { isMac } from '../../utils/extra-attributes';
|
||||||
|
import toaster from '../toast';
|
||||||
|
import Switch from '../common/switch';
|
||||||
|
import { SEARCH_DELAY_TIME } from './constant';
|
||||||
|
|
||||||
|
const INDEX_STATE = {
|
||||||
|
RUNNING: 'running',
|
||||||
|
UNCREATED: 'uncreated',
|
||||||
|
FINISHED: 'finished'
|
||||||
|
};
|
||||||
|
|
||||||
|
const PER_PAGE = 10;
|
||||||
|
const controlKey = isMac() ? '⌘' : 'Ctrl';
|
||||||
|
|
||||||
|
export default class AISearch extends Component {
|
||||||
|
|
||||||
|
static propTypes = {
|
||||||
|
repoID: PropTypes.string,
|
||||||
|
placeholder: PropTypes.string,
|
||||||
|
onSearchedClick: PropTypes.func.isRequired,
|
||||||
|
repoName: PropTypes.string,
|
||||||
|
};
|
||||||
|
|
||||||
|
constructor(props) {
|
||||||
|
super(props);
|
||||||
|
this.baseSearchPageURL = `${siteRoot}search/`;
|
||||||
|
this.state = {
|
||||||
|
width: 'default',
|
||||||
|
value: '',
|
||||||
|
resultItems: [],
|
||||||
|
highlightIndex: 0,
|
||||||
|
page: 0,
|
||||||
|
isLoading: false,
|
||||||
|
hasMore: true,
|
||||||
|
isMaskShow: false,
|
||||||
|
isResultShow: false,
|
||||||
|
isResultGetted: false,
|
||||||
|
isCloseShow: false,
|
||||||
|
isSearchInputShow: false, // for mobile
|
||||||
|
searchPageUrl: this.baseSearchPageURL,
|
||||||
|
indexState: '',
|
||||||
|
};
|
||||||
|
this.inputValue = '';
|
||||||
|
this.highlightRef = null;
|
||||||
|
this.source = null; // used to cancel request;
|
||||||
|
this.inputRef = React.createRef();
|
||||||
|
this.searchContainer = React.createRef();
|
||||||
|
this.searchResultListRef = React.createRef();
|
||||||
|
this.indexStateTimer = null;
|
||||||
|
this.timer = null;
|
||||||
|
this.isChineseInput = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
componentDidMount() {
|
||||||
|
document.addEventListener('keydown', this.onDocumentKeydown);
|
||||||
|
document.addEventListener('compositionstart', this.onCompositionStart);
|
||||||
|
document.addEventListener('compositionend', this.onCompositionEnd);
|
||||||
|
this.queryLibraryIndexState();
|
||||||
|
}
|
||||||
|
|
||||||
|
queryLibraryIndexState() {
|
||||||
|
seafileAPI.queryLibraryIndexState(this.props.repoID).then(res => {
|
||||||
|
const { state: indexState, task_id: taskId } = res.data;
|
||||||
|
this.setState({ indexState }, () => {
|
||||||
|
if (indexState === INDEX_STATE.RUNNING) {
|
||||||
|
this.queryIndexTaskStatus(taskId);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}).catch(error => {
|
||||||
|
this.setState({ indexState: INDEX_STATE.UNCREATED });
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
componentWillUnmount() {
|
||||||
|
document.removeEventListener('keydown', this.onDocumentKeydown);
|
||||||
|
document.removeEventListener('compositionstart', this.onCompositionStart);
|
||||||
|
document.removeEventListener('compositionend', this.onCompositionEnd);
|
||||||
|
this.isChineseInput = false;
|
||||||
|
if (this.timer) {
|
||||||
|
clearTimeout(this.timer);
|
||||||
|
this.timer = null;
|
||||||
|
}
|
||||||
|
if (this.indexStateTimer) {
|
||||||
|
clearInterval(this.indexStateTimer);
|
||||||
|
this.indexStateTimer = null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
onCompositionStart = () => {
|
||||||
|
this.isChineseInput = true;
|
||||||
|
if (this.timer) {
|
||||||
|
clearTimeout(this.timer);
|
||||||
|
this.timer = null;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
onCompositionEnd = () => {
|
||||||
|
this.isChineseInput = false;
|
||||||
|
// chrome:compositionstart -> onChange -> compositionend
|
||||||
|
// not chrome:compositionstart -> compositionend -> onChange
|
||||||
|
// The onChange event will setState and change input value, then setTimeout to initiate the search
|
||||||
|
if (this.timer) {
|
||||||
|
clearTimeout(this.timer);
|
||||||
|
this.timer = null;
|
||||||
|
}
|
||||||
|
this.timer = setTimeout(() => {
|
||||||
|
this.onSearch();
|
||||||
|
}, SEARCH_DELAY_TIME);
|
||||||
|
};
|
||||||
|
|
||||||
|
onDocumentKeydown = (e) => {
|
||||||
|
if (isHotkey('mod+f')(e)) {
|
||||||
|
e.preventDefault();
|
||||||
|
this.onFocusHandler();
|
||||||
|
if (this.inputRef && this.inputRef.current) {
|
||||||
|
this.inputRef.current.focus();
|
||||||
|
}
|
||||||
|
} else if (isHotkey('esc', e)) {
|
||||||
|
e.preventDefault();
|
||||||
|
this.inputRef && this.inputRef.current && this.inputRef.current.blur();
|
||||||
|
this.resetToDefault();
|
||||||
|
} else if (isHotkey('enter', e)) {
|
||||||
|
this.onEnter(e);
|
||||||
|
} else if (isHotkey('up', e)) {
|
||||||
|
this.onUp(e);
|
||||||
|
} else if (isHotkey('down', e)) {
|
||||||
|
this.onDown(e);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
onFocusHandler = () => {
|
||||||
|
this.setState({ width: '570px', isMaskShow: true, isCloseShow: true });
|
||||||
|
};
|
||||||
|
|
||||||
|
onCloseHandler = () => {
|
||||||
|
this.resetToDefault();
|
||||||
|
};
|
||||||
|
|
||||||
|
onUp = (e) => {
|
||||||
|
e.preventDefault();
|
||||||
|
e.stopPropagation();
|
||||||
|
const { highlightIndex } = this.state;
|
||||||
|
if (highlightIndex > 0) {
|
||||||
|
this.setState({ highlightIndex: highlightIndex - 1 }, () => {
|
||||||
|
if (this.highlightRef) {
|
||||||
|
const { top, height } = this.highlightRef.getBoundingClientRect();
|
||||||
|
if (top - height < 0) {
|
||||||
|
this.searchContainer.current.scrollTop -= height;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
onDown = (e) => {
|
||||||
|
e.preventDefault();
|
||||||
|
e.stopPropagation();
|
||||||
|
const { highlightIndex, resultItems } = this.state;
|
||||||
|
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) {
|
||||||
|
this.searchContainer.current.scrollTop += height;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
onEnter = (e) => {
|
||||||
|
e.preventDefault();
|
||||||
|
let item = this.state.resultItems[this.state.highlightIndex];
|
||||||
|
if (item) {
|
||||||
|
if (document.activeElement) {
|
||||||
|
document.activeElement.blur();
|
||||||
|
}
|
||||||
|
this.onItemClickHandler(item);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
onItemClickHandler = (item) => {
|
||||||
|
this.resetToDefault();
|
||||||
|
this.props.onSearchedClick(item);
|
||||||
|
};
|
||||||
|
|
||||||
|
onChangeHandler = (event) => {
|
||||||
|
const newValue = event.target.value;
|
||||||
|
this.setState({ value: newValue }, () => {
|
||||||
|
if (this.inputValue === newValue.trim()) return;
|
||||||
|
this.inputValue = newValue.trim();
|
||||||
|
if (!this.isChineseInput) {
|
||||||
|
if (this.timer) {
|
||||||
|
clearTimeout(this.timer);
|
||||||
|
this.timer = null;
|
||||||
|
}
|
||||||
|
this.timer = setTimeout(() => {
|
||||||
|
this.onSearch();
|
||||||
|
}, SEARCH_DELAY_TIME);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
onKeydownHandler = (event) => {
|
||||||
|
if (isHotkey('enter', event)) {
|
||||||
|
this.onSearch();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
onSearch = () => {
|
||||||
|
const { value } = this.state;
|
||||||
|
const { repoID } = this.props;
|
||||||
|
if (this.inputValue === '' || this.getValueLength(this.inputValue) < 3) {
|
||||||
|
this.setState({
|
||||||
|
highlightIndex: 0,
|
||||||
|
resultItems: [],
|
||||||
|
isResultShow: false,
|
||||||
|
isResultGetted: false
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const queryData = {
|
||||||
|
q: value,
|
||||||
|
search_repo: repoID ? repoID : 'all',
|
||||||
|
search_ftypes: 'all',
|
||||||
|
};
|
||||||
|
this.getSearchResult(queryData);
|
||||||
|
};
|
||||||
|
|
||||||
|
getSearchResult = (queryData) => {
|
||||||
|
if (this.source) {
|
||||||
|
this.source.cancel('prev request is cancelled');
|
||||||
|
}
|
||||||
|
this.setState({
|
||||||
|
isResultShow: true,
|
||||||
|
isResultGetted: false,
|
||||||
|
resultItems: [],
|
||||||
|
highlightIndex: 0,
|
||||||
|
});
|
||||||
|
this.source = seafileAPI.getSource();
|
||||||
|
this.sendRequest(queryData, this.source.token, 1);
|
||||||
|
};
|
||||||
|
|
||||||
|
sendRequest = (queryData, cancelToken, page) => {
|
||||||
|
this.queryData = queryData;
|
||||||
|
this.updateSearchPageURL(queryData);
|
||||||
|
queryData['per_page'] = PER_PAGE;
|
||||||
|
queryData['page'] = page;
|
||||||
|
queryData['search_filename_only'] = true;
|
||||||
|
if (this.state.indexState === INDEX_STATE.FINISHED) {
|
||||||
|
this.onCombinedSearch(queryData, cancelToken, page);
|
||||||
|
} else {
|
||||||
|
this.onNormalSearch(queryData, cancelToken, page);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
onNormalSearch = (queryData, cancelToken, 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)],
|
||||||
|
isResultGetted: true,
|
||||||
|
isLoading: false,
|
||||||
|
page: page + 1,
|
||||||
|
hasMore: res.data.has_more,
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
this.setState({
|
||||||
|
highlightIndex: 0,
|
||||||
|
resultItems: [],
|
||||||
|
isLoading: false,
|
||||||
|
isResultGetted: true,
|
||||||
|
hasMore: res.data.has_more,
|
||||||
|
});
|
||||||
|
}).catch(error => {
|
||||||
|
/* eslint-disable */
|
||||||
|
console.log(error);
|
||||||
|
this.setState({ isLoading: false });
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
onCombinedSearch = (queryData, cancelToken, page) => {
|
||||||
|
const { indexState } = this.state;
|
||||||
|
if (indexState === INDEX_STATE.UNCREATED) {
|
||||||
|
toaster.warning(gettext('Please create index first.'));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (indexState === INDEX_STATE.RUNNING) {
|
||||||
|
toaster.warning(gettext('Indexing, please try again later.'));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
let results = [];
|
||||||
|
let normalSearchQueryData = Object.assign({}, queryData, {'search_filename_only': true});
|
||||||
|
seafileAPI.searchFiles(normalSearchQueryData, cancelToken).then(res => {
|
||||||
|
if (res.data.total > 0) {
|
||||||
|
results = [...results, ...this.formatResultItems(res.data.results)];
|
||||||
|
}
|
||||||
|
seafileAPI.similaritySearchFiles(queryData, cancelToken).then(res => {
|
||||||
|
this.source = null;
|
||||||
|
if (res.data && res.data.children_list) {
|
||||||
|
results = [...results, ...this.formatSimilarityItems(res.data.children_list)];
|
||||||
|
}
|
||||||
|
|
||||||
|
let tempPathObj = {};
|
||||||
|
let searchResults = [];
|
||||||
|
results.forEach(item => {
|
||||||
|
if (!tempPathObj[item.path]) {
|
||||||
|
tempPathObj[item.path] = true;
|
||||||
|
searchResults.push(item);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
this.setState({
|
||||||
|
resultItems: searchResults,
|
||||||
|
isResultGetted: true,
|
||||||
|
isLoading: false,
|
||||||
|
page: page + 1,
|
||||||
|
hasMore: false,
|
||||||
|
});
|
||||||
|
}).catch(error => {
|
||||||
|
let errMessage = Utils.getErrorMsg(error);
|
||||||
|
toaster.danger(errMessage);
|
||||||
|
this.setState({ isLoading: false });
|
||||||
|
});
|
||||||
|
}).catch(error => {
|
||||||
|
/* eslint-disable */
|
||||||
|
console.log(error);
|
||||||
|
this.setState({ isLoading: false });
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
onResultListScroll = (e) => {
|
||||||
|
// Load less than 100 results
|
||||||
|
if (!this.state.hasMore || this.state.isLoading || this.state.resultItems.length > 100) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const listPadding = 20;
|
||||||
|
if (e.target.scrollTop + e.target.clientHeight + listPadding > this.searchResultListRef.current.clientHeight - 10) {
|
||||||
|
this.setState({isLoading: true}, () => {
|
||||||
|
this.source = seafileAPI.getSource();
|
||||||
|
this.sendRequest(this.queryData, this.source.token, this.state.page);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
updateSearchPageURL(queryData) {
|
||||||
|
let params = '';
|
||||||
|
for (let key in queryData) {
|
||||||
|
params += key + '=' + encodeURIComponent(queryData[key]) + '&';
|
||||||
|
}
|
||||||
|
this.setState({searchPageUrl: `${this.baseSearchPageURL}?${params.substring(0, params.length - 1)}`});
|
||||||
|
}
|
||||||
|
|
||||||
|
getValueLength(str) {
|
||||||
|
var i = 0, code, len = 0;
|
||||||
|
for (; i < str.length; i++) {
|
||||||
|
code = str.charCodeAt(i);
|
||||||
|
if (code == 10) { //solve enter problem
|
||||||
|
len += 2;
|
||||||
|
} else if (code < 0x007f) {
|
||||||
|
len += 1;
|
||||||
|
} else if (code >= 0x0080 && code <= 0x07ff) {
|
||||||
|
len += 2;
|
||||||
|
} else if (code >= 0x0800 && code <= 0xffff) {
|
||||||
|
len += 3;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return len;
|
||||||
|
}
|
||||||
|
|
||||||
|
formatResultItems(data) {
|
||||||
|
let items = [];
|
||||||
|
for (let i = 0; i < data.length; i++) {
|
||||||
|
items[i] = {};
|
||||||
|
items[i]['index'] = [i];
|
||||||
|
items[i]['name'] = data[i].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;
|
||||||
|
}
|
||||||
|
return items;
|
||||||
|
}
|
||||||
|
|
||||||
|
formatSimilarityItems(data) {
|
||||||
|
let items = [];
|
||||||
|
let repo_id = this.props.repoID;
|
||||||
|
for (let i = 0; i < data.length; i++) {
|
||||||
|
items[i] = {};
|
||||||
|
items[i]['index'] = [i];
|
||||||
|
items[i]['name'] = data[i].path.substring(data[i].path.lastIndexOf('/')+1);
|
||||||
|
items[i]['path'] = data[i].path;
|
||||||
|
items[i]['repo_id'] = repo_id;
|
||||||
|
items[i]['repo_name'] = this.props.repoName;
|
||||||
|
items[i]['is_dir'] = false;
|
||||||
|
items[i]['link_content'] = decodeURI(data[i].path).substring(1);
|
||||||
|
items[i]['content'] = data[i].sentence;
|
||||||
|
items[i]['thumbnail_url'] = '';
|
||||||
|
}
|
||||||
|
return items;
|
||||||
|
}
|
||||||
|
|
||||||
|
resetToDefault() {
|
||||||
|
this.inputValue = '';
|
||||||
|
this.setState({
|
||||||
|
width: '',
|
||||||
|
value: '',
|
||||||
|
isMaskShow: false,
|
||||||
|
isCloseShow: false,
|
||||||
|
isResultShow: false,
|
||||||
|
isResultGetted: false,
|
||||||
|
resultItems: [],
|
||||||
|
highlightIndex: 0,
|
||||||
|
isSearchInputShow: false,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
renderSearchResult() {
|
||||||
|
const { resultItems, highlightIndex, width } = this.state;
|
||||||
|
if (!width || width === 'default') return null;
|
||||||
|
|
||||||
|
if (!this.state.isResultShow) return null;
|
||||||
|
if (!this.state.isResultGetted || this.getValueLength(this.inputValue) < 3) {
|
||||||
|
return (
|
||||||
|
<span className="loading-icon loading-tip"></span>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
if (!resultItems.length) {
|
||||||
|
return (
|
||||||
|
<div className="search-result-none">{gettext('No results matching.')}</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const results = (
|
||||||
|
<ul className="search-result-list" ref={this.searchResultListRef}>
|
||||||
|
{resultItems.map((item, index) => {
|
||||||
|
const isHighlight = index === highlightIndex;
|
||||||
|
return (
|
||||||
|
<SearchResultItem
|
||||||
|
key={index}
|
||||||
|
item={item}
|
||||||
|
onItemClickHandler={this.onItemClickHandler}
|
||||||
|
isHighlight={isHighlight}
|
||||||
|
setRef={isHighlight ? (ref) => {this.highlightRef = ref;} : () => {}}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</ul>
|
||||||
|
);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<MediaQuery query="(min-width: 768px)">
|
||||||
|
<div className="search-result-list-container">{results}</div>
|
||||||
|
</MediaQuery>
|
||||||
|
<MediaQuery query="(max-width: 767.8px)">
|
||||||
|
{results}
|
||||||
|
</MediaQuery>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
onSearchToggle = () => {
|
||||||
|
this.setState({
|
||||||
|
isSearchInputShow: !this.state.isSearchInputShow,
|
||||||
|
isMaskShow: !this.state.isMaskShow,
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
queryIndexTaskStatus = (taskId) => {
|
||||||
|
if (!taskId) return;
|
||||||
|
this.indexStateTimer = setInterval(() => {
|
||||||
|
seafileAPI.queryIndexTaskStatus(taskId).then(res => {
|
||||||
|
const isFinished = res.data.is_finished;
|
||||||
|
if (isFinished) {
|
||||||
|
this.setState({ indexState: INDEX_STATE.FINISHED });
|
||||||
|
this.indexStateTimer && clearInterval(this.indexStateTimer);
|
||||||
|
this.indexStateTimer = null;
|
||||||
|
}
|
||||||
|
}).catch(error => {
|
||||||
|
this.indexStateTimer && clearInterval(this.indexStateTimer);
|
||||||
|
this.indexStateTimer = null;
|
||||||
|
const errorMsg = Utils.getErrorMsg(error);
|
||||||
|
toaster.danger(errorMsg);
|
||||||
|
this.setState({ indexState: INDEX_STATE.UNCREATED });
|
||||||
|
});
|
||||||
|
}, 3000);
|
||||||
|
};
|
||||||
|
|
||||||
|
onCreateIndex = () => {
|
||||||
|
this.setState({ indexState: INDEX_STATE.RUNNING });
|
||||||
|
seafileAPI.createLibraryIndex(this.props.repoID).then(res => {
|
||||||
|
const taskId = res.data.task_id;
|
||||||
|
toaster.notify(gettext('Indexing the library. Semantic search will be available within a few minutes.'))
|
||||||
|
this.queryIndexTaskStatus(taskId);
|
||||||
|
}).catch(error => {
|
||||||
|
const errorMsg = Utils.getErrorMsg(error);
|
||||||
|
toaster.danger(errorMsg);
|
||||||
|
this.setState({ indexState: INDEX_STATE.UNCREATED });
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
renderSwitch = () => {
|
||||||
|
const { indexState } = this.state;
|
||||||
|
if (indexState === INDEX_STATE.FINISHED || indexState === INDEX_STATE.RUNNING) {
|
||||||
|
return (
|
||||||
|
<Switch
|
||||||
|
checked={true}
|
||||||
|
placeholder={gettext('Turn on semantic search for this library')}
|
||||||
|
className="w-100 mt-1"
|
||||||
|
size="small"
|
||||||
|
textPosition='right'
|
||||||
|
disabled
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
} else if (indexState === '' || indexState === INDEX_STATE.UNCREATED) {
|
||||||
|
return (
|
||||||
|
<Switch
|
||||||
|
checked={false}
|
||||||
|
placeholder={gettext('Turn on semantic search for this library')}
|
||||||
|
className="w-100 mt-1"
|
||||||
|
size="small"
|
||||||
|
onChange={this.onCreateIndex}
|
||||||
|
textPosition='right'
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
render() {
|
||||||
|
let width = this.state.width !== 'default' ? this.state.width : '';
|
||||||
|
let style = {'width': width};
|
||||||
|
const { isMaskShow, isCloseShow } = this.state;
|
||||||
|
const placeholder = `${this.props.placeholder}${isMaskShow ? '' : ` (${controlKey} + f )`}`;
|
||||||
|
return (
|
||||||
|
<Fragment>
|
||||||
|
<MediaQuery query="(min-width: 768px)">
|
||||||
|
<div className="search">
|
||||||
|
<div className={`search-mask ${isMaskShow ? 'show' : 'hide'}`} onClick={this.onCloseHandler}></div>
|
||||||
|
<div className={`search-container ${isMaskShow ? 'show' : ''}`}>
|
||||||
|
<div className={`input-icon ${isMaskShow ? 'mb-1' : ''}`}>
|
||||||
|
<i className="search-icon-left input-icon-addon fas fa-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}
|
||||||
|
onKeyDown={this.onKeydownHandler}
|
||||||
|
/>
|
||||||
|
{this.state.isCloseShow &&
|
||||||
|
<button type="button" className="search-icon-right input-icon-addon fas fa-times border-0 bg-transparent mr-4" onClick={this.onCloseHandler} aria-label={gettext('Close')}></button>
|
||||||
|
}
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
className="search-result-container dropdown-search-result-container"
|
||||||
|
onScroll={this.onResultListScroll}
|
||||||
|
ref={this.searchContainer}
|
||||||
|
>
|
||||||
|
{isCloseShow && this.renderSwitch()}
|
||||||
|
{this.renderSearchResult()}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</MediaQuery>
|
||||||
|
<MediaQuery query="(max-width: 767.8px)">
|
||||||
|
<div className="search-icon-container">
|
||||||
|
<i className="search-icon fas fa-search" onClick={this.onSearchToggle}></i>
|
||||||
|
</div>
|
||||||
|
{this.state.isSearchInputShow &&
|
||||||
|
<div className="search">
|
||||||
|
<div className={`search-mask ${isMaskShow ? '' : 'hide'}`} onClick={this.onCloseHandler}></div>
|
||||||
|
<div className="search-container">
|
||||||
|
<div className="input-icon">
|
||||||
|
<i className="search-icon-left input-icon-addon fas fa-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 fas fa-times border-0 bg-transparent" onClick={this.onCloseHandler} aria-label={gettext('Close')}></button>
|
||||||
|
}
|
||||||
|
</div>
|
||||||
|
<div className="search-result-container dropdown-search-result-container" onScroll={this.onResultListScroll}>
|
||||||
|
{this.renderSearchResult()}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
</MediaQuery>
|
||||||
|
</Fragment>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
3
frontend/src/components/search/constant.js
Normal file
3
frontend/src/components/search/constant.js
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
const SEARCH_DELAY_TIME = 1000;
|
||||||
|
|
||||||
|
export { SEARCH_DELAY_TIME };
|
@@ -3,26 +3,18 @@ import PropTypes from 'prop-types';
|
|||||||
import isHotkey from 'is-hotkey';
|
import isHotkey from 'is-hotkey';
|
||||||
import MediaQuery from 'react-responsive';
|
import MediaQuery from 'react-responsive';
|
||||||
import { seafileAPI } from '../../utils/seafile-api';
|
import { seafileAPI } from '../../utils/seafile-api';
|
||||||
import { gettext, siteRoot, enableSeafileAI } from '../../utils/constants';
|
import { gettext, siteRoot } from '../../utils/constants';
|
||||||
import SearchResultItem from './search-result-item';
|
import SearchResultItem from './search-result-item';
|
||||||
import { Utils } from '../../utils/utils';
|
import { Utils } from '../../utils/utils';
|
||||||
import { isMac } from '../../utils/extra-attributes';
|
import { isMac } from '../../utils/extra-attributes';
|
||||||
import toaster from '../toast';
|
import toaster from '../toast';
|
||||||
import Switch from '../common/switch';
|
import { SEARCH_DELAY_TIME } from './constant';
|
||||||
|
|
||||||
const INDEX_STATE = {
|
|
||||||
RUNNING: 'running',
|
|
||||||
UNCREATED: 'uncreated',
|
|
||||||
FINISHED: 'finished'
|
|
||||||
};
|
|
||||||
|
|
||||||
const propTypes = {
|
const propTypes = {
|
||||||
repoID: PropTypes.string,
|
repoID: PropTypes.string,
|
||||||
placeholder: PropTypes.string,
|
placeholder: PropTypes.string,
|
||||||
onSearchedClick: PropTypes.func.isRequired,
|
onSearchedClick: PropTypes.func.isRequired,
|
||||||
isPublic: PropTypes.bool,
|
isPublic: PropTypes.bool,
|
||||||
isLibView: PropTypes.bool,
|
|
||||||
repoName: PropTypes.string,
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const PER_PAGE = 10;
|
const PER_PAGE = 10;
|
||||||
@@ -47,7 +39,6 @@ class Search extends Component {
|
|||||||
isCloseShow: false,
|
isCloseShow: false,
|
||||||
isSearchInputShow: false, // for mobile
|
isSearchInputShow: false, // for mobile
|
||||||
searchPageUrl: this.baseSearchPageURL,
|
searchPageUrl: this.baseSearchPageURL,
|
||||||
indexState: '',
|
|
||||||
};
|
};
|
||||||
this.inputValue = '';
|
this.inputValue = '';
|
||||||
this.highlightRef = null;
|
this.highlightRef = null;
|
||||||
@@ -56,7 +47,6 @@ class Search extends Component {
|
|||||||
this.searchContainer = React.createRef();
|
this.searchContainer = React.createRef();
|
||||||
this.searchResultListRef = React.createRef();
|
this.searchResultListRef = React.createRef();
|
||||||
this.timer = null;
|
this.timer = null;
|
||||||
this.indexStateTimer = null;
|
|
||||||
this.isChineseInput = false;
|
this.isChineseInput = false;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -64,35 +54,25 @@ 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);
|
||||||
if (enableSeafileAI && this.props.isLibView) {
|
|
||||||
this.queryLibraryIndexState();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
queryLibraryIndexState() {
|
|
||||||
seafileAPI.queryLibraryIndexState(this.props.repoID).then(res => {
|
|
||||||
const { state: indexState, task_id: taskId } = res.data;
|
|
||||||
this.setState({ indexState }, () => {
|
|
||||||
if (indexState === INDEX_STATE.RUNNING) {
|
|
||||||
this.queryIndexTaskStatus(taskId);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}).catch(error => {
|
|
||||||
this.setState({ indexState: INDEX_STATE.UNCREATED });
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
componentWillUnmount() {
|
componentWillUnmount() {
|
||||||
document.removeEventListener('keydown', this.onDocumentKeydown);
|
document.removeEventListener('keydown', this.onDocumentKeydown);
|
||||||
document.removeEventListener('compositionstart', this.onCompositionStart);
|
document.removeEventListener('compositionstart', this.onCompositionStart);
|
||||||
document.removeEventListener('compositionend', this.onCompositionEnd);
|
document.removeEventListener('compositionend', this.onCompositionEnd);
|
||||||
this.indexStateTimer && clearInterval(this.indexStateTimer);
|
if (this.timer) {
|
||||||
this.timer && clearTimeout(this.timer);
|
clearTimeout(this.timer);
|
||||||
|
this.timer = null;
|
||||||
|
}
|
||||||
this.isChineseInput = false;
|
this.isChineseInput = false;
|
||||||
}
|
}
|
||||||
|
|
||||||
onCompositionStart = () => {
|
onCompositionStart = () => {
|
||||||
this.isChineseInput = true;
|
this.isChineseInput = true;
|
||||||
|
if (this.timer) {
|
||||||
|
clearTimeout(this.timer);
|
||||||
|
this.timer = null;
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
onCompositionEnd = () => {
|
onCompositionEnd = () => {
|
||||||
@@ -100,9 +80,13 @@ class Search extends Component {
|
|||||||
// chrome:compositionstart -> onChange -> compositionend
|
// chrome:compositionstart -> onChange -> compositionend
|
||||||
// not chrome:compositionstart -> compositionend -> onChange
|
// not chrome:compositionstart -> compositionend -> onChange
|
||||||
// The onChange event will setState and change input value, then setTimeout to initiate the search
|
// The onChange event will setState and change input value, then setTimeout to initiate the search
|
||||||
setTimeout(() => {
|
if (this.timer) {
|
||||||
this.onSearch(!this.props.isLibView || !enableSeafileAI);
|
clearTimeout(this.timer);
|
||||||
}, 1);
|
this.timer = null;
|
||||||
|
}
|
||||||
|
this.timer = setTimeout(() => {
|
||||||
|
this.onSearch();
|
||||||
|
}, SEARCH_DELAY_TIME);
|
||||||
};
|
};
|
||||||
|
|
||||||
onDocumentKeydown = (e) => {
|
onDocumentKeydown = (e) => {
|
||||||
@@ -112,8 +96,7 @@ class Search extends Component {
|
|||||||
if (this.inputRef && this.inputRef.current) {
|
if (this.inputRef && this.inputRef.current) {
|
||||||
this.inputRef.current.focus();
|
this.inputRef.current.focus();
|
||||||
}
|
}
|
||||||
}
|
} else if (isHotkey('esc', e)) {
|
||||||
else if (isHotkey('esc', e)) {
|
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
this.inputRef && this.inputRef.current && this.inputRef.current.blur();
|
this.inputRef && this.inputRef.current && this.inputRef.current.blur();
|
||||||
this.resetToDefault();
|
this.resetToDefault();
|
||||||
@@ -189,25 +172,27 @@ class Search extends Component {
|
|||||||
if (this.inputValue === newValue.trim()) return;
|
if (this.inputValue === newValue.trim()) return;
|
||||||
this.inputValue = newValue.trim();
|
this.inputValue = newValue.trim();
|
||||||
if (!this.isChineseInput) {
|
if (!this.isChineseInput) {
|
||||||
this.onSearch(!this.props.isLibView || !enableSeafileAI);
|
if (this.timer) {
|
||||||
|
clearTimeout(this.timer);
|
||||||
|
this.timer = null;
|
||||||
|
}
|
||||||
|
this.timer = setTimeout(() => {
|
||||||
|
this.onSearch();
|
||||||
|
}, SEARCH_DELAY_TIME);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
onKeydownHandler = (event) => {
|
onKeydownHandler = (event) => {
|
||||||
if (isHotkey('enter', event)) {
|
if (isHotkey('enter', event)) {
|
||||||
if (!enableSeafileAI || !this.props.isLibView) return;
|
this.onSearch();
|
||||||
this.onSearch(true);
|
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
onSearch = (isGetSearchResult) => {
|
onSearch = () => {
|
||||||
const { value } = this.state;
|
const { value } = this.state;
|
||||||
const { repoID } = this.props;
|
const { repoID } = this.props;
|
||||||
const _this = this;
|
if (this.inputValue === '' || this.getValueLength(this.inputValue) < 3) {
|
||||||
this.timer && clearTimeout(this.timer);
|
|
||||||
|
|
||||||
if (_this.inputValue === '' || _this.getValueLength(_this.inputValue) < 3) {
|
|
||||||
this.setState({
|
this.setState({
|
||||||
highlightIndex: 0,
|
highlightIndex: 0,
|
||||||
resultItems: [],
|
resultItems: [],
|
||||||
@@ -216,14 +201,12 @@ class Search extends Component {
|
|||||||
});
|
});
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (!isGetSearchResult) return;
|
|
||||||
|
|
||||||
const queryData = {
|
const queryData = {
|
||||||
q: value,
|
q: value,
|
||||||
search_repo: repoID ? repoID : 'all',
|
search_repo: repoID ? repoID : 'all',
|
||||||
search_ftypes: 'all',
|
search_ftypes: 'all',
|
||||||
};
|
};
|
||||||
this.timer = setTimeout(_this.getSearchResult(queryData), 500);
|
this.getSearchResult(queryData);
|
||||||
};
|
};
|
||||||
|
|
||||||
getSearchResult = (queryData) => {
|
getSearchResult = (queryData) => {
|
||||||
@@ -273,124 +256,31 @@ class Search extends Component {
|
|||||||
this.updateSearchPageURL(queryData);
|
this.updateSearchPageURL(queryData);
|
||||||
queryData['per_page'] = PER_PAGE;
|
queryData['per_page'] = PER_PAGE;
|
||||||
queryData['page'] = page;
|
queryData['page'] = page;
|
||||||
if (enableSeafileAI && this.props.isLibView) {
|
seafileAPI.searchFiles(queryData, cancelToken).then(res => {
|
||||||
queryData['search_filename_only'] = true;
|
|
||||||
}
|
|
||||||
if (enableSeafileAI && this.props.isLibView && this.state.indexState === INDEX_STATE.FINISHED) {
|
|
||||||
this.onCombinedSearch(queryData, cancelToken, page);
|
|
||||||
} else {
|
|
||||||
this.onNormalSearch(queryData, cancelToken, page);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
onNormalSearch = (queryData, cancelToken, 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)],
|
|
||||||
isResultGetted: true,
|
|
||||||
isLoading: false,
|
|
||||||
page: page + 1,
|
|
||||||
hasMore: res.data.has_more,
|
|
||||||
});
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
this.setState({
|
|
||||||
highlightIndex: 0,
|
|
||||||
resultItems: [],
|
|
||||||
isLoading: false,
|
|
||||||
isResultGetted: true,
|
|
||||||
hasMore: res.data.has_more,
|
|
||||||
});
|
|
||||||
}).catch(error => {
|
|
||||||
/* eslint-disable */
|
|
||||||
console.log(error);
|
|
||||||
this.setState({ isLoading: false });
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
onSimilaritySearch = (queryData, cancelToken, page) => {
|
|
||||||
const { indexState } = this.state;
|
|
||||||
if (indexState === INDEX_STATE.UNCREATED) {
|
|
||||||
toaster.warning(gettext('Please create index first.'));
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
if (indexState === INDEX_STATE.RUNNING) {
|
|
||||||
toaster.warning(gettext('Indexing, please try again later.'));
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
seafileAPI.similaritySearchFiles(queryData, cancelToken).then(res => {
|
|
||||||
this.source = null;
|
|
||||||
if (res.data && res.data.children_list.length > 0) {
|
|
||||||
this.setState({
|
|
||||||
resultItems: [...this.state.resultItems, ...this.formatSimilarityItems(res.data.children_list)],
|
|
||||||
isResultGetted: true,
|
|
||||||
isLoading: false,
|
|
||||||
page: page + 1,
|
|
||||||
hasMore: res.data.has_more,
|
|
||||||
});
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
this.setState({
|
|
||||||
highlightIndex: 0,
|
|
||||||
resultItems: [],
|
|
||||||
isLoading: false,
|
|
||||||
isResultGetted: true,
|
|
||||||
hasMore: res.data.has_more,
|
|
||||||
});
|
|
||||||
}).catch(error => {
|
|
||||||
/* eslint-disable */
|
|
||||||
console.log(error);
|
|
||||||
this.setState({ isLoading: false });
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
onCombinedSearch = (queryData, cancelToken, page) => {
|
|
||||||
const { indexState } = this.state;
|
|
||||||
if (indexState === INDEX_STATE.UNCREATED) {
|
|
||||||
toaster.warning(gettext('Please create index first.'));
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
if (indexState === INDEX_STATE.RUNNING) {
|
|
||||||
toaster.warning(gettext('Indexing, please try again later.'));
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
let results = []
|
|
||||||
let normalSearchQueryData = Object.assign({}, queryData, {'search_filename_only': true});
|
|
||||||
seafileAPI.searchFiles(normalSearchQueryData, cancelToken).then(res => {
|
|
||||||
if (res.data.total > 0) {
|
|
||||||
results = [...results, ...this.formatResultItems(res.data.results)]
|
|
||||||
}
|
|
||||||
seafileAPI.similaritySearchFiles(queryData, cancelToken).then(res => {
|
|
||||||
this.source = null;
|
this.source = null;
|
||||||
if (res.data && res.data.children_list) {
|
if (res.data.total > 0) {
|
||||||
results = [...results, ...this.formatSimilarityItems(res.data.children_list)]
|
this.setState({
|
||||||
|
resultItems: [...this.state.resultItems, ...this.formatResultItems(res.data.results)],
|
||||||
|
isResultGetted: true,
|
||||||
|
isLoading: false,
|
||||||
|
page: page + 1,
|
||||||
|
hasMore: res.data.has_more,
|
||||||
|
});
|
||||||
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
let tempPathObj = {}
|
|
||||||
let searchResults = []
|
|
||||||
results.forEach(item => {
|
|
||||||
if (!tempPathObj[item.path]) {
|
|
||||||
tempPathObj[item.path] = true
|
|
||||||
searchResults.push(item)
|
|
||||||
}
|
|
||||||
})
|
|
||||||
this.setState({
|
this.setState({
|
||||||
resultItems: searchResults,
|
highlightIndex: 0,
|
||||||
isResultGetted: true,
|
resultItems: [],
|
||||||
isLoading: false,
|
isLoading: false,
|
||||||
page: page + 1,
|
isResultGetted: true,
|
||||||
hasMore: false,
|
hasMore: res.data.has_more,
|
||||||
});
|
});
|
||||||
})
|
}).catch(error => {
|
||||||
}).catch(error => {
|
/* eslint-disable */
|
||||||
/* eslint-disable */
|
console.log(error);
|
||||||
console.log(error);
|
this.setState({ isLoading: false });
|
||||||
this.setState({ isLoading: false });
|
});
|
||||||
});
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
onResultListScroll = (e) => {
|
onResultListScroll = (e) => {
|
||||||
@@ -449,24 +339,6 @@ class Search extends Component {
|
|||||||
return items;
|
return items;
|
||||||
}
|
}
|
||||||
|
|
||||||
formatSimilarityItems(data) {
|
|
||||||
let items = [];
|
|
||||||
let repo_id = this.props.repoID;
|
|
||||||
for (let i = 0; i < data.length; i++) {
|
|
||||||
items[i] = {};
|
|
||||||
items[i]['index'] = [i];
|
|
||||||
items[i]['name'] = data[i].path.substring(data[i].path.lastIndexOf('/')+1);
|
|
||||||
items[i]['path'] = data[i].path;
|
|
||||||
items[i]['repo_id'] = repo_id;
|
|
||||||
items[i]['repo_name'] = this.props.repoName;
|
|
||||||
items[i]['is_dir'] = false;
|
|
||||||
items[i]['link_content'] = decodeURI(data[i].path).substring(1);
|
|
||||||
items[i]['content'] = data[i].sentence;
|
|
||||||
items[i]['thumbnail_url'] = '';
|
|
||||||
}
|
|
||||||
return items;
|
|
||||||
}
|
|
||||||
|
|
||||||
resetToDefault() {
|
resetToDefault() {
|
||||||
this.inputValue = '';
|
this.inputValue = '';
|
||||||
this.setState({
|
this.setState({
|
||||||
@@ -534,71 +406,10 @@ class Search extends Component {
|
|||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
queryIndexTaskStatus = (taskId) => {
|
|
||||||
if (!taskId) return;
|
|
||||||
this.indexStateTimer = setInterval(() => {
|
|
||||||
seafileAPI.queryIndexTaskStatus(taskId).then(res => {
|
|
||||||
const isFinished = res.data.is_finished;
|
|
||||||
if (isFinished) {
|
|
||||||
this.setState({ indexState: INDEX_STATE.FINISHED });
|
|
||||||
this.indexStateTimer && clearInterval(this.indexStateTimer);
|
|
||||||
this.indexStateTimer = null;
|
|
||||||
}
|
|
||||||
}).catch(error => {
|
|
||||||
this.indexStateTimer && clearInterval(this.indexStateTimer);
|
|
||||||
this.indexStateTimer = null;
|
|
||||||
const errorMsg = Utils.getErrorMsg(error);
|
|
||||||
toaster.danger(errorMsg);
|
|
||||||
this.setState({ indexState: INDEX_STATE.UNCREATED });
|
|
||||||
});
|
|
||||||
}, 3000);
|
|
||||||
};
|
|
||||||
|
|
||||||
onCreateIndex = () => {
|
|
||||||
this.setState({ indexState: INDEX_STATE.RUNNING });
|
|
||||||
seafileAPI.createLibraryIndex(this.props.repoID).then(res => {
|
|
||||||
const taskId = res.data.task_id;
|
|
||||||
toaster.notify(gettext('Indexing the library. Semantic search will be available within a few minutes.'))
|
|
||||||
this.queryIndexTaskStatus(taskId);
|
|
||||||
}).catch(error => {
|
|
||||||
const errorMsg = Utils.getErrorMsg(error);
|
|
||||||
toaster.danger(errorMsg);
|
|
||||||
this.setState({ indexState: INDEX_STATE.UNCREATED });
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
renderSwitch = () => {
|
|
||||||
const { indexState } = this.state;
|
|
||||||
if (indexState === INDEX_STATE.FINISHED || indexState === INDEX_STATE.RUNNING) {
|
|
||||||
return (
|
|
||||||
<Switch
|
|
||||||
checked={true}
|
|
||||||
placeholder={gettext('Turn on semantic search for this library')}
|
|
||||||
className="w-100 mt-1"
|
|
||||||
size="small"
|
|
||||||
textPosition='right'
|
|
||||||
disabled
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
} else if (indexState === '' || indexState === INDEX_STATE.UNCREATED) {
|
|
||||||
return (
|
|
||||||
<Switch
|
|
||||||
checked={false}
|
|
||||||
placeholder={gettext('Turn on semantic search for this library')}
|
|
||||||
className="w-100 mt-1"
|
|
||||||
size="small"
|
|
||||||
onChange={this.onCreateIndex}
|
|
||||||
textPosition='right'
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
render() {
|
render() {
|
||||||
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 { isMaskShow, isCloseShow } = this.state;
|
const { isMaskShow } = this.state;
|
||||||
const placeholder = `${this.props.placeholder}${isMaskShow ? '' : ` (${controlKey} + f )`}`;
|
const placeholder = `${this.props.placeholder}${isMaskShow ? '' : ` (${controlKey} + f )`}`;
|
||||||
return (
|
return (
|
||||||
<Fragment>
|
<Fragment>
|
||||||
@@ -630,7 +441,6 @@ class Search extends Component {
|
|||||||
onScroll={this.onResultListScroll}
|
onScroll={this.onResultListScroll}
|
||||||
ref={this.searchContainer}
|
ref={this.searchContainer}
|
||||||
>
|
>
|
||||||
{isCloseShow && this.props.isLibView && enableSeafileAI && this.renderSwitch()}
|
|
||||||
{this.renderSearchResult()}
|
{this.renderSearchResult()}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
@@ -1,7 +1,8 @@
|
|||||||
import React from 'react';
|
import React from 'react';
|
||||||
import PropTypes from 'prop-types';
|
import PropTypes from 'prop-types';
|
||||||
import { isPro, gettext, showLogoutIcon } from '../../utils/constants';
|
import { isPro, gettext, showLogoutIcon, enableSeafileAI } from '../../utils/constants';
|
||||||
import Search from '../search/search';
|
import Search from '../search/search';
|
||||||
|
import AISearch from '../search/ai-search';
|
||||||
import SearchByName from '../search/search-by-name';
|
import SearchByName from '../search/search-by-name';
|
||||||
import Notification from '../common/notification';
|
import Notification from '../common/notification';
|
||||||
import Account from '../common/account';
|
import Account from '../common/account';
|
||||||
@@ -17,25 +18,44 @@ const propTypes = {
|
|||||||
|
|
||||||
class CommonToolbar extends React.Component {
|
class CommonToolbar extends React.Component {
|
||||||
|
|
||||||
render() {
|
renderSearch = () => {
|
||||||
const { repoID, repoName } = this.props;
|
const { repoID, repoName, isLibView, searchPlaceholder } = this.props;
|
||||||
return (
|
const placeholder = searchPlaceholder || gettext('Search files');
|
||||||
<div className="common-toolbar">
|
|
||||||
{isPro && (
|
if (isPro) {
|
||||||
|
if (enableSeafileAI && isLibView) {
|
||||||
|
return (
|
||||||
|
<AISearch
|
||||||
|
repoID={repoID}
|
||||||
|
placeholder={placeholder}
|
||||||
|
onSearchedClick={this.props.onSearchedClick}
|
||||||
|
repoName={repoName}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
return (
|
||||||
<Search
|
<Search
|
||||||
repoID={repoID}
|
repoID={repoID}
|
||||||
placeholder={this.props.searchPlaceholder || gettext('Search files')}
|
placeholder={placeholder}
|
||||||
onSearchedClick={this.props.onSearchedClick}
|
onSearchedClick={this.props.onSearchedClick}
|
||||||
isLibView={this.props.isLibView}
|
isPublic={false}
|
||||||
repoName={repoName}
|
|
||||||
/>
|
/>
|
||||||
)}
|
);
|
||||||
{this.props.isLibView && !isPro &&
|
}
|
||||||
<SearchByName
|
} else {
|
||||||
repoID={repoID}
|
if (isLibView) {
|
||||||
repoName={repoName}
|
return (
|
||||||
/>
|
<SearchByName repoID={repoID} repoName={repoName} />
|
||||||
}
|
);
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
render() {
|
||||||
|
return (
|
||||||
|
<div className="common-toolbar">
|
||||||
|
{this.renderSearch()}
|
||||||
<Notification />
|
<Notification />
|
||||||
<Account />
|
<Account />
|
||||||
{showLogoutIcon && (<Logout />)}
|
{showLogoutIcon && (<Logout />)}
|
||||||
|
Reference in New Issue
Block a user