From ea1490caaa077e4ad40d40167bb4b46169fe8c7c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E6=9D=A8=E9=A1=BA=E5=BC=BA?= Date: Sat, 9 Mar 2019 12:06:11 +0800 Subject: [PATCH] repair file chooser bug (#3060) --- .../components/file-chooser/file-chooser.js | 326 +++++++++++++++--- .../file-chooser/searched-list-item.js | 52 +++ .../file-chooser/searched-list-view.js | 45 +++ frontend/src/components/search/search.js | 4 +- frontend/src/css/file-chooser.css | 32 ++ 5 files changed, 411 insertions(+), 48 deletions(-) create mode 100644 frontend/src/components/file-chooser/searched-list-item.js create mode 100644 frontend/src/components/file-chooser/searched-list-view.js diff --git a/frontend/src/components/file-chooser/file-chooser.js b/frontend/src/components/file-chooser/file-chooser.js index b74db4a7e0..c03c4e041a 100644 --- a/frontend/src/components/file-chooser/file-chooser.js +++ b/frontend/src/components/file-chooser/file-chooser.js @@ -1,10 +1,13 @@ -import React from 'react'; +import React, { Fragment } from 'react'; import PropTypes from 'prop-types'; -import RepoListView from './repo-list-view'; +import { Input } from 'reactstrap'; import { seafileAPI } from '../../utils/seafile-api'; -import { gettext } from '../../utils/constants'; +import { gettext, isPro } from '../../utils/constants'; import { Utils } from '../../utils/utils'; import RepoInfo from '../../models/repo-info'; +import RepoListView from './repo-list-view'; +import Loading from '../loading'; +import SearchedListView from './searched-list-view'; import '../../css/file-chooser.css'; @@ -28,11 +31,18 @@ class FileChooser extends React.Component { currentRepoInfo: null, selectedRepo: null, selectedPath: '', + isSearching: false, + isResultGot: false, + searchInfo: '', + searchResults: [], }; + this.inputValue = ''; + this.timer = null; + this.source = null; } componentDidMount() { - if (this.props.repoID) { + if (this.props.repoID) { // current_repo_and_other_repos, only_current_library let repoID = this.props.repoID; seafileAPI.getRepoInfo(repoID).then(res => { // need to optimized @@ -43,6 +53,24 @@ class FileChooser extends React.Component { }); this.props.onRepoItemClick(repoInfo); }); + } else { // only_all_repos + seafileAPI.listRepos().then(res => { + let repos = res.data.repos; + let repoList = []; + let repoIdList = []; + for(let i = 0; i < repos.length; i++) { + if (repos[i].permission !== 'rw') { + continue; + } + if (repoIdList.indexOf(repos[i].repo_id) > -1) { + continue; + } + repoList.push(repos[i]); + repoIdList.push(repos[i].repo_id); + } + repoList = Utils.sortRepos(repoList, 'name', 'asc'); + this.setState({repoList: repoList}); + }); } } @@ -101,57 +129,263 @@ class FileChooser extends React.Component { }); } - render() { - if (!this.state.selectedRepo) { - return ''; + onCloseSearching = () => { + this.setState({ + isSearching: false, + isResultGot: false, + searchInfo: '', + searchResults: [], + }); + this.inputValue = ''; + this.timer = null; + this.source = null; + } + + onSearchInfoChanged = (event) => { + let searchInfo = event.target.value.trim(); + if (!this.state.searchResults.length && searchInfo.length > 0) { + this.setState({ + isSearching: true, + isResultGot: false, + }); } - const mode = this.props.mode; - let libName = mode === 'current_repo_and_other_repos' ? gettext('Other Libraries') : gettext('Libraries'); + this.setState({searchInfo: searchInfo}); + if (this.inputValue === searchInfo) { + return false; + } + this.inputValue = searchInfo; + + if (this.inputValue === '' || this.getValueLength(this.inputValue) < 3) { + this.setState({ + isResultGot: false, + }); + return false; + } + + let repoID = this.props.repoID; + let isShowFile = this.props.isShowFile; + let mode = this.props.mode; + let searchRepo = mode === 'only_current_library' ? repoID : 'all'; + + let queryData = { + q: searchInfo, + search_repo: searchRepo, + search_ftypes: 'all', + obj_type: isShowFile ? 'file' : 'dir', + }; + + if (this.timer) { + clearTimeout(this.timer); + } + + this.timer = setTimeout(this.getSearchResult(queryData), 500); + } + + getSearchResult = (queryData) => { + if (this.source) { + this.cancelRequest(); + } + + this.setState({isResultGot: false}); + + this.source = seafileAPI.getSource(); + this.sendRequest(queryData, this.source.token); + } + + sendRequest = (queryData, cancelToken) => { + seafileAPI.searchFiles(queryData, cancelToken).then(res => { + if (!res.data.total) { + this.setState({ + searchResults: [], + isResultGot: true + }); + this.source = null; + return; + } + + let items = this.formatResultItems(res.data.results); + this.setState({ + searchResults: items, + isResultGot: true + }); + this.source = null; + }).catch(res => { + /* eslint-disable */ + console.log(res); + /* eslint-enable */ + }); + } + + cancelRequest = () => { + this.source.cancel('prev request is cancelled'); + } + + 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 = []; + let length = data.length > 10 ? 10 : data.length; + for (let i = 0; i < 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; + } + return items; + } + + onSearchedItemClick = (item) => { + item['type'] = item.is_dir ? 'dir' : 'file'; + let repo = new RepoInfo(item); + this.props.onDirentItemClick(repo, item.path, item); + } + + renderSearchedView = () => { + if (!this.state.isResultGot || this.getValueLength(this.inputValue) < 3) { + return (); + } + + if (this.state.isResultGot && this.state.searchResults.length === 0) { + return (
{gettext('No results matching.')}
); + } + + if (this.state.isResultGot && this.state.searchResults.length > 0) { + return (); + } + } + + renderRepoListView = () => { return (
- {(mode === 'current_repo_and_other_repos' || mode === 'only_current_library') && -
-
- - {gettext('Current Library')} + {this.props.mode === 'current_repo_and_other_repos' && ( + +
+
+ + {gettext('Current Library')} +
+ { + this.state.isCurrentRepoShow && this.state.currentRepoInfo && + + }
- { - this.state.isCurrentRepoShow && this.state.currentRepoInfo && - - } -
- } - {mode !== 'only_current_library' && -
-
- - {libName} +
+
+ + {gettext('Other Libraries')} +
+ { + this.state.isOtherRepoShow && + + } +
+ + )} + {this.props.mode === 'only_current_library' && ( +
+
+
+ + {gettext('Current Library')} +
+ { + this.state.isCurrentRepoShow && this.state.currentRepoInfo && + + }
- { - this.state.isOtherRepoShow && - - }
- } + )} + {this.props.mode === 'only_all_repos' && ( +
+
+
+ + {gettext('Libraries')} +
+ +
+
+ )}
); } + + render() { + if (!this.state.selectedRepo && this.props.repoID) { + return ''; + } + + return ( + + {isPro && ( +
+ + {this.state.searchInfo.length !== 0 && ( + + )} +
+ )} + {this.state.isSearching && ( +
+ {this.renderSearchedView()} +
+ )} + {!this.state.isSearching && this.renderRepoListView()} +
+ ); + } } FileChooser.propTypes = propTypes; diff --git a/frontend/src/components/file-chooser/searched-list-item.js b/frontend/src/components/file-chooser/searched-list-item.js new file mode 100644 index 0000000000..ced15e70c7 --- /dev/null +++ b/frontend/src/components/file-chooser/searched-list-item.js @@ -0,0 +1,52 @@ +import React from 'react'; +import PropTypes from 'prop-types'; +import { Utils } from '../../utils/utils'; + +const propTypes = { + currentItem: PropTypes.object, + onItemClick: PropTypes.func.isRequired, +}; + +class SearchedListItem extends React.Component { + + constructor(props) { + super(props); + this.state = { + highlight: false, + }; + } + + onMouseEnter = () => { + this.setState({highlight: true}); + } + + onMouseLeave = () => { + this.setState({highlight: false}); + } + + onClick = () => { + let item = this.props.item; + this.props.onItemClick(item); + } + + render() { + let { item, currentItem } = this.props; + let fileIconUrl = item.is_dir ? Utils.getFolderIconUrl(false, 24) : Utils.getFileIconUrl(item.name, 24); + let trClass = this.state.highlight ? 'tr-hightlight' : ''; + if (currentItem) { + if (item.repo_id === currentItem.repo_id && item.path === currentItem.path) { + trClass = 'searched-active'; + } + } + return ( + + + {item.repo_name}/{item.link_content} + + ); + } +} + +SearchedListItem.propTypes = propTypes; + +export default SearchedListItem; diff --git a/frontend/src/components/file-chooser/searched-list-view.js b/frontend/src/components/file-chooser/searched-list-view.js new file mode 100644 index 0000000000..f57d186c83 --- /dev/null +++ b/frontend/src/components/file-chooser/searched-list-view.js @@ -0,0 +1,45 @@ +import React from 'react'; +import PropTypes from 'prop-types'; +import SearchedListItem from './searched-list-item'; + +const propTypes = { + searchResults: PropTypes.array.isRequired, + onItemClick: PropTypes.func.isRequired, +}; + +class SearchedListView extends React.Component { + + constructor(props) { + super(props); + this.state = { + currentItem: null, + }; + } + + onItemClick = (item) => { + this.setState({currentItem: item}); + this.props.onItemClick(item); + } + + render() { + return ( + + + + + + + + + {this.props.searchResults.map((item, index) => { + return (); + })} + +
+ ); + } +} + +SearchedListView.propTypes = propTypes; + +export default SearchedListView; diff --git a/frontend/src/components/search/search.js b/frontend/src/components/search/search.js index 59c1e0eebe..fa6e80b014 100644 --- a/frontend/src/components/search/search.js +++ b/frontend/src/components/search/search.js @@ -1,7 +1,7 @@ import React, { Component, Fragment } from 'react'; import PropTypes from 'prop-types'; import MediaQuery from 'react-responsive'; -import { siteRoot } from '../../utils/constants'; +import { gettext, siteRoot } from '../../utils/constants'; import SearchResultItem from './search-result-item'; import editorUtilities from '../../utils/editor-utilties'; import More from '../more'; @@ -195,7 +195,7 @@ class Search extends Component { } if (!this.state.resultItems.length) { return ( -
No results matching.
+
{gettext('No results matching.')}
); } let isShowMore = this.state.resultItems.length >= 5 ? true : false; diff --git a/frontend/src/css/file-chooser.css b/frontend/src/css/file-chooser.css index aca8b6788b..b9e71e99f4 100644 --- a/frontend/src/css/file-chooser.css +++ b/frontend/src/css/file-chooser.css @@ -74,5 +74,37 @@ flex: 1; } +.file-chooser-search-input { + position: relative; +} + +.file-chooser-search-input .search-control { + position: absolute; + top: 0.7rem; + right: 0.7rem; +} + +.file-chooser-search-container { + height: 20rem; + position: relative; + border: 1px solid #eee; + padding: 10px; + overflow: auto; +} + +.file-chooser-search-close { + position: absolute; + right: -0.5rem; + top: -0.5rem; +} + +.searched-active { + background: #beebff !important; + border-radius: 2px; + box-shadow: inset 0 0 1px #999; +} + + +