diff --git a/frontend/src/components/dialog/copy-dirent-dialog.js b/frontend/src/components/dialog/copy-dirent-dialog.js index c53143eb46..9fbbe8c579 100644 --- a/frontend/src/components/dialog/copy-dirent-dialog.js +++ b/frontend/src/components/dialog/copy-dirent-dialog.js @@ -1,9 +1,9 @@ import React from 'react'; import PropTypes from 'prop-types'; import { Button, Modal, ModalHeader, ModalFooter, ModalBody, Alert, Row, Col } from 'reactstrap'; +import FileChooser from '../file-chooser'; import { gettext } from '../../utils/constants'; import { Utils } from '../../utils/utils'; -import FileChooser from '../file-chooser/file-chooser'; const propTypes = { path: PropTypes.string.isRequired, diff --git a/frontend/src/components/dialog/insert-file-dialog.js b/frontend/src/components/dialog/insert-file-dialog.js index bd0a961df5..22589a107d 100644 --- a/frontend/src/components/dialog/insert-file-dialog.js +++ b/frontend/src/components/dialog/insert-file-dialog.js @@ -2,7 +2,7 @@ import React from 'react'; import PropTypes from 'prop-types'; import { Button, Modal, ModalHeader, ModalBody, ModalFooter } from 'reactstrap'; import { gettext } from '../../utils/constants'; -import FileChooser from '../file-chooser/file-chooser'; +import FileChooser from '../file-chooser'; const propTypes = { repoID: PropTypes.string.isRequired, diff --git a/frontend/src/components/dialog/insert-repo-image-dialog.js b/frontend/src/components/dialog/insert-repo-image-dialog.js index d167711b11..8dc1d3f183 100644 --- a/frontend/src/components/dialog/insert-repo-image-dialog.js +++ b/frontend/src/components/dialog/insert-repo-image-dialog.js @@ -3,7 +3,7 @@ import PropTypes from 'prop-types'; import { Button, Modal, ModalHeader, ModalBody, ModalFooter } from 'reactstrap'; import { gettext } from '../../utils/constants'; import { Utils } from '../../utils/utils'; -import FileChooser from '../file-chooser/file-chooser'; +import FileChooser from '../file-chooser'; import '../../css/insert-repo-image-dialog.css'; const { siteRoot, serviceUrl } = window.app.config; diff --git a/frontend/src/components/dialog/lib-sub-folder-set-group-permission-dialog.js b/frontend/src/components/dialog/lib-sub-folder-set-group-permission-dialog.js index c8d95ad8f6..196aa0c89d 100644 --- a/frontend/src/components/dialog/lib-sub-folder-set-group-permission-dialog.js +++ b/frontend/src/components/dialog/lib-sub-folder-set-group-permission-dialog.js @@ -5,7 +5,7 @@ import { gettext, isPro, siteRoot } from '../../utils/constants'; import { seafileAPI } from '../../utils/seafile-api'; import { Utils } from '../../utils/utils'; import SharePermissionEditor from '../select-editor/share-permission-editor'; -import FileChooser from '../file-chooser/file-chooser'; +import FileChooser from '../file-chooser'; import { SeahubSelect, NoGroupMessage } from '../common/select'; import toaster from '../../components/toast'; diff --git a/frontend/src/components/dialog/lib-sub-folder-set-user-permission-dialog.js b/frontend/src/components/dialog/lib-sub-folder-set-user-permission-dialog.js index b99de046ea..ad95af290f 100644 --- a/frontend/src/components/dialog/lib-sub-folder-set-user-permission-dialog.js +++ b/frontend/src/components/dialog/lib-sub-folder-set-user-permission-dialog.js @@ -6,7 +6,7 @@ import { seafileAPI } from '../../utils/seafile-api'; import { Utils } from '../../utils/utils'; import UserSelect from '../user-select'; import SharePermissionEditor from '../select-editor/share-permission-editor'; -import FileChooser from '../file-chooser/file-chooser'; +import FileChooser from '../file-chooser'; import toaster from '../../components/toast'; class UserItem extends React.Component { diff --git a/frontend/src/components/dialog/move-dirent-dialog.js b/frontend/src/components/dialog/move-dirent-dialog.js index 844cd29a07..408e258bab 100644 --- a/frontend/src/components/dialog/move-dirent-dialog.js +++ b/frontend/src/components/dialog/move-dirent-dialog.js @@ -1,9 +1,9 @@ import React from 'react'; import PropTypes from 'prop-types'; -import { Button, Modal, ModalHeader, ModalFooter, ModalBody, Alert, Row, Col } from 'reactstrap'; +import { Modal, ModalHeader } from 'reactstrap'; +import SelectDirentBody from './select-dirent-body'; import { gettext } from '../../utils/constants'; import { Utils } from '../../utils/utils'; -import FileChooser from '../file-chooser/file-chooser'; const propTypes = { path: PropTypes.string.isRequired, @@ -14,10 +14,8 @@ const propTypes = { onItemMove: PropTypes.func, onItemsMove: PropTypes.func, onCancelMove: PropTypes.func.isRequired, - repoEncrypted: PropTypes.bool.isRequired, }; -// need dirent file Path; class MoveDirent extends React.Component { constructor(props) { @@ -26,7 +24,6 @@ class MoveDirent extends React.Component { repo: { repo_id: this.props.repoID }, selectedPath: this.props.path, errMessage: '', - mode: 'only_current_library', }; } @@ -44,7 +41,7 @@ class MoveDirent extends React.Component { let message = gettext('Invalid destination path'); if (!repo || selectedPath === '') { - this.setState({ errMessage: message }); + this.setErrMessage(message); return; } @@ -57,13 +54,13 @@ class MoveDirent extends React.Component { // move dirents to one of them. eg: A/B, A/C -> A/B if (direntPaths.some(direntPath => { return direntPath === selectedPath;})) { - this.setState({ errMessage: message }); + this.setErrMessage(message); return; } // move dirents to current path if (selectedPath && selectedPath === this.props.path && (repo.repo_id === repoID)) { - this.setState({ errMessage: message }); + this.setErrMessage(message); return; } @@ -81,7 +78,7 @@ class MoveDirent extends React.Component { message = gettext('Can not move folder %(src)s to its subfolder %(des)s'); message = message.replace('%(src)s', moveDirentPath); message = message.replace('%(des)s', selectedPath); - this.setState({ errMessage: message }); + this.setErrMessage(message); return; } @@ -96,19 +93,19 @@ class MoveDirent extends React.Component { let message = gettext('Invalid destination path'); if (!repo || (repo.repo_id === repoID && selectedPath === '')) { - this.setState({ errMessage: message }); + this.setErrMessage(message); return; } // copy the dirent to itself. eg: A/B -> A/B if (selectedPath && direntPath === selectedPath) { - this.setState({ errMessage: message }); + this.setErrMessage(message); return; } // copy the dirent to current path if (selectedPath && this.props.path === selectedPath && repo.repo_id === repoID) { - this.setState({ errMessage: message }); + this.setErrMessage(message); return; } @@ -117,7 +114,7 @@ class MoveDirent extends React.Component { message = gettext('Can not move folder %(src)s to its subfolder %(des)s'); message = message.replace('%(src)s', direntPath); message = message.replace('%(des)s', selectedPath); - this.setState({ errMessage: message }); + this.setErrMessage(message); return; } @@ -129,24 +126,16 @@ class MoveDirent extends React.Component { this.props.onCancelMove(); }; - onDirentItemClick = (repo, selectedPath) => { - this.setState({ - repo: repo, - selectedPath: selectedPath, - errMessage: '' - }); + selectRepo = (repo) => { + this.setState({ repo }); }; - onRepoItemClick = (repo) => { - this.setState({ - repo: repo, - selectedPath: '/', - errMessage: '' - }); + setSelectedPath = (selectedPath) => { + this.setState({ selectedPath }); }; - onSelectedMode = (mode) => { - this.setState({ mode: mode }); + setErrMessage = (message) => { + this.setState({ errMessage: message }); }; renderTitle = () => { @@ -161,48 +150,28 @@ class MoveDirent extends React.Component { }; render() { - const { dirent, selectedDirentList, isMultipleOperation, repoID, path } = this.props; - const { mode, errMessage } = this.state; - + const { dirent, selectedDirentList, isMultipleOperation, path, repoID } = this.props; const movedDirent = dirent || selectedDirentList[0]; const { permission } = movedDirent; const { isCustomPermission } = Utils.getUserPermission(permission); - const LibraryOption = ({ mode, label }) => ( -
this.onSelectedMode(mode)}> - {label} -
- ); - return ( {isMultipleOperation ? this.renderTitle() :
}
- - - - {!isCustomPermission && } - - - - - - {errMessage && {errMessage}} - - - - - - - +
); } diff --git a/frontend/src/components/dialog/save-shared-dir-dialog.js b/frontend/src/components/dialog/save-shared-dir-dialog.js index b5140bf922..6eb7522d14 100644 --- a/frontend/src/components/dialog/save-shared-dir-dialog.js +++ b/frontend/src/components/dialog/save-shared-dir-dialog.js @@ -2,7 +2,7 @@ import React from 'react'; import PropTypes from 'prop-types'; import { Button, Modal, ModalHeader, ModalBody, ModalFooter, Alert } from 'reactstrap'; import { gettext } from '../../utils/constants'; -import FileChooser from '../file-chooser/file-chooser'; +import FileChooser from '../file-chooser'; const propTypes = { sharedToken: PropTypes.string.isRequired, diff --git a/frontend/src/components/dialog/save-shared-file-dialog.js b/frontend/src/components/dialog/save-shared-file-dialog.js index 37f00f7623..555d917fdf 100644 --- a/frontend/src/components/dialog/save-shared-file-dialog.js +++ b/frontend/src/components/dialog/save-shared-file-dialog.js @@ -3,7 +3,7 @@ import PropTypes from 'prop-types'; import { Button, Modal, ModalHeader, ModalBody, ModalFooter, Alert } from 'reactstrap'; import { gettext } from '../../utils/constants'; import { seafileAPI } from '../../utils/seafile-api'; -import FileChooser from '../file-chooser/file-chooser'; +import FileChooser from '../file-chooser'; import { Utils } from '../../utils/utils'; const propTypes = { diff --git a/frontend/src/components/dialog/select-dirent-body.jsx b/frontend/src/components/dialog/select-dirent-body.jsx new file mode 100644 index 0000000000..eb2ad438e2 --- /dev/null +++ b/frontend/src/components/dialog/select-dirent-body.jsx @@ -0,0 +1,232 @@ +import React from 'react'; +import PropTypes from 'prop-types'; +import { Button, ModalFooter, ModalBody, Alert, Row, Col } from 'reactstrap'; +import toaster from '../toast'; +import Searcher, { SearchStatus } from '../file-chooser/searcher'; +import RepoListWrapper, { MODE_TYPE_MAP } from '../file-chooser/repo-list-wrapper'; +import { seafileAPI } from '../../utils/seafile-api'; +import { gettext, isPro } from '../../utils/constants'; +import { Utils } from '../../utils/utils'; +import { RepoInfo } from '../../models'; + +const LibraryOption = ({ mode, label, currentMode, selectedMode }) => { + return ( +
selectedMode(mode)}> + {label} +
+ ); +}; + +class SelectDirentBody extends React.Component { + + constructor(props) { + super(props); + this.state = { + mode: MODE_TYPE_MAP.ONLY_CURRENT_LIBRARY, + currentRepoInfo: null, + repoList: [], + selectedSearchedItem: null, + selectedRepo: null, + browsingPath: '', + searchStatus: SearchStatus.IDLE, + errMessage: '', + }; + } + + componentDidMount() { + this.fetchRepoInfo(); + this.fetchRepoList(); + } + + fetchRepoInfo = async () => { + try { + const res = await seafileAPI.getRepoInfo(this.props.repoID); + const repoInfo = new RepoInfo(res.data); + this.props.setSelectedPath('/'); + this.setState({ + currentRepoInfo: repoInfo, + selectedRepo: repoInfo, + }); + } catch (err) { + const errMessage = Utils.getErrorMsg(err); + toaster.danger(errMessage); + } + }; + + fetchRepoList = async () => { + try { + const res = await seafileAPI.listRepos(); + const repos = res.data.repos; + const repoList = repos.filter((repo) => repo.permission === 'rw' && repo.repo_id !== this.props.repoID); + const sortedRepoList = Utils.sortRepos(repoList, 'name', 'asc'); + const selectedRepo = sortedRepoList.find((repo) => repo.repo_id === this.props.repoID); + const path = this.props.path.substring(0, this.props.path.length - 1); + + this.props.setSelectedPath(path); + this.setState({ + repoList: sortedRepoList, + selectedRepo: selectedRepo || this.state.selectedRepo, + }); + } catch (error) { + const errMessage = Utils.getErrorMsg(error); + toaster.danger(errMessage); + } + }; + + onUpdateSearchStatus = (status) => { + this.setState({ searchStatus: status }); + }; + + onUpdateRepoList = (repoList) => { + this.setState({ repoList: repoList }); + }; + + selectSearchedItem = (item) => { + this.setState({ selectedSearchedItem: item }); + }; + + onSelectSearchedRepo = (repo) => { + this.setState({ + selectedRepo: repo, + mode: repo.repo_id === this.props.repoID ? MODE_TYPE_MAP.ONLY_CURRENT_LIBRARY : MODE_TYPE_MAP.ONLY_OTHER_LIBRARIES, + }); + }; + + selectPath = (path) => { + this.props.setSelectedPath(path); + }; + + setBrowsingPath = (path) => { + this.setState({ browsingPath: path }); + }; + + handleSubmit = () => { + if (this.props.handleSubmit) { + this.props.handleSubmit(); + } + }; + + onCancel = () => { + if (this.props.onCancel) { + this.props.onCancel(); + } + }; + + onDirentItemClick = (repo, selectedPath) => { + this.props.selectRepo(repo); + this.props.setSelectedPath(selectedPath); + this.props.setErrMessage(''); + this.setState({ selectedRepo: repo }); + }; + + onRepoItemClick = (repo) => { + this.props.selectRepo(repo); + this.props.setSelectedPath('/'); + this.props.setErrMessage(''); + this.setState({ selectedRepo: repo }); + }; + + selectedMode = (mode) => { + const { repoID, path } = this.props; + + // reset selecting status + this.props.selectRepo({ repo_id: repoID }); + this.props.setSelectedPath(path); + + this.setState({ + mode, + selectedSearchedItem: null, + searchStatus: SearchStatus.RESULTS, + }); + }; + + render() { + const { path, selectedPath, isSupportOtherLibraries, errMessage } = this.props; + const { mode, searchStatus, selectedSearchedItem, selectedRepo, repoList, currentRepoInfo, browsingPath } = this.state; + let repoListWrapperKey = 'repo-list-wrapper'; + if (selectedSearchedItem && selectedSearchedItem.repoID) { + repoListWrapperKey = `${repoListWrapperKey}-${selectedSearchedItem.repoID}`; + } + + return ( + + + {isPro && ( + + )} + + {isSupportOtherLibraries && ( + + )} + + + + + {currentRepoInfo && ( + + )} + {errMessage && {errMessage}} + + + + + + + + ); + } +} + +SelectDirentBody.propTypes = { + path: PropTypes.string, + selectedPath: PropTypes.string, + repoID: PropTypes.string, + isSupportOtherLibraries: PropTypes.bool, + onCancel: PropTypes.func, + handleSubmit: PropTypes.func, + selectRepo: PropTypes.func, + setSelectedPath: PropTypes.func, + setErrMessage: PropTypes.func, +}; + +SelectDirentBody.defaultProps = { + isSupportOtherLibraries: true, +}; + +export default SelectDirentBody; diff --git a/frontend/src/components/file-chooser/file-chooser.jsx b/frontend/src/components/file-chooser/file-chooser.jsx new file mode 100644 index 0000000000..e69de29bb2 diff --git a/frontend/src/components/file-chooser/file-chooser.js b/frontend/src/components/file-chooser/index.js similarity index 64% rename from frontend/src/components/file-chooser/file-chooser.js rename to frontend/src/components/file-chooser/index.js index ff4db6aaf3..ba47129bd2 100644 --- a/frontend/src/components/file-chooser/file-chooser.js +++ b/frontend/src/components/file-chooser/index.js @@ -1,15 +1,14 @@ import React, { Fragment } from 'react'; import PropTypes from 'prop-types'; import { Input } from 'reactstrap'; +import toaster from '../toast'; +import Loading from '../loading'; +import RepoListWrapper from './repo-list-wrapper'; +import SearchedListView from './searched-list-view'; +import RepoInfo from '../../models/repo-info'; import { seafileAPI } from '../../utils/seafile-api'; import { gettext, isPro } from '../../utils/constants'; import { Utils } from '../../utils/utils'; -import toaster from '../toast'; -import RepoInfo from '../../models/repo-info'; -import RepoListView from './repo-list-view'; -import RecentlyUsedListView from './recently-used-list-view'; -import Loading from '../loading'; -import SearchedListView from './searched-list-view'; import '../../css/file-chooser.css'; @@ -28,6 +27,10 @@ const propTypes = { ]).isRequired, fileSuffixes: PropTypes.arrayOf(PropTypes.string), currentPath: PropTypes.string, + searchResults: PropTypes.array, + selectedSearchedItem: PropTypes.object, + selectedRepo: PropTypes.object, + selectedPath: PropTypes.string, }; const defaultProps = { @@ -38,6 +41,10 @@ const defaultProps = { onRepoItemClick: () => {}, fileSuffixes: [], currentPath: '', + searchResults: [], + selectedSearchedItem: {}, + selectedRepo: null, + selectedPath: '', }; class FileChooser extends React.Component { @@ -45,7 +52,6 @@ class FileChooser extends React.Component { constructor(props) { super(props); this.state = { - hasRequest: false, isCurrentRepoShow: true, isOtherRepoShow: false, repoList: [], @@ -129,34 +135,30 @@ class FileChooser extends React.Component { } onOtherRepoToggle = async () => { - if (!this.state.hasRequest) { - try { - const res = await seafileAPI.listRepos(); - const repos = res.data.repos; - const repoList = []; - const repoIdList = []; + try { + const res = await seafileAPI.listRepos(); + const repos = res.data.repos; + const repoList = []; + const repoIdList = []; - repos.forEach(repo => { - if (repo.permission !== 'rw') return; - if (this.props.repoID && repo.repo_name === this.state.currentRepoInfo.repo_name) return; - if (repoIdList.includes(repo.repo_id)) return; + repos.forEach(repo => { + if (repo.permission !== 'rw') return; + if (this.props.repoID && repo.repo_name === this.state.currentRepoInfo.repo_name) return; + if (repoIdList.includes(repo.repo_id)) return; - repoList.push(repo); - repoIdList.push(repo.repo_id); - }); + repoList.push(repo); + repoIdList.push(repo.repo_id); + }); - const sortedRepoList = Utils.sortRepos(repoList, 'name', 'asc'); - this.setState({ - repoList: sortedRepoList, - isOtherRepoShow: !this.state.isOtherRepoShow, - selectedItemInfo: {}, - }); - } catch (error) { - const errMessage = Utils.getErrorMsg(error); - toaster.danger(errMessage); - } - } else { - this.setState({ isOtherRepoShow: !this.state.isOtherRepoShow }); + const sortedRepoList = Utils.sortRepos(repoList, 'name', 'asc'); + this.setState({ + repoList: sortedRepoList, + isOtherRepoShow: !this.state.isOtherRepoShow, + selectedItemInfo: {}, + }); + } catch (error) { + const errMessage = Utils.getErrorMsg(error); + toaster.danger(errMessage); } }; @@ -214,6 +216,7 @@ class FileChooser extends React.Component { if (this.inputValue === '') { this.setState({ + isSearching: false, isResultGot: false, }); return false; @@ -339,7 +342,7 @@ class FileChooser extends React.Component { } const { repoID } = this.props; - const { hasRequest, currentRepoInfo } = this.state; + const { currentRepoInfo } = this.state; const selectedItemInfo = { repoID: item.repo_id, @@ -406,11 +409,7 @@ class FileChooser extends React.Component { if (repoID && item.repo_id === repoID) { await fetchRepoInfo(); } else { - if (!hasRequest) { - await fetchRepoList(); - } else { - this.setState(prevState => ({ isOtherRepoShow: !prevState.isOtherRepoShow })); - } + await fetchRepoList(); } this.setState({ @@ -424,6 +423,7 @@ class FileChooser extends React.Component { this.timer = null; this.source = null; }; + onScroll = (event) => { event.stopPropagation(); }; @@ -431,122 +431,26 @@ class FileChooser extends React.Component { renderRepoListView = () => { const { mode, currentPath, isShowFile, fileSuffixes } = this.props; const { isCurrentRepoShow, isOtherRepoShow, currentRepoInfo, repoList, selectedRepo, selectedPath, selectedItemInfo } = this.state; - const recentlyUsedList = JSON.parse(localStorage.getItem('recently-used-list')) || []; - return ( -
-
- {mode === 'current_repo_and_other_repos' && ( - -
-
- - {gettext('Current Library')} -
- { - isCurrentRepoShow && currentRepoInfo && - - } -
-
-
- - {gettext('Other Libraries')} -
- { - isOtherRepoShow && - - } -
-
- )} - {mode === 'only_current_library' && ( -
- -
- )} - {mode === 'only_all_repos' && ( -
-
-
- - {gettext('Libraries')} -
- -
-
- )} - {mode === 'only_other_libraries' && ( -
- -
- )} - {mode === 'recently_used' && ( -
- -
- )} -
-
+ ); }; @@ -560,7 +464,7 @@ class FileChooser extends React.Component { return ( - {isPro && mode !== 'recently_used' && ( + {(isPro && mode !== 'recently_used') && (
{searchInfo.length !== 0 && ( diff --git a/frontend/src/components/file-chooser/recently-used-list-item.js b/frontend/src/components/file-chooser/recently-used-list-item.js index 2c79bcfde0..d73f2bebab 100644 --- a/frontend/src/components/file-chooser/recently-used-list-item.js +++ b/frontend/src/components/file-chooser/recently-used-list-item.js @@ -1,7 +1,7 @@ import React from 'react'; const RecentlyUsedListItem = ({ item, isSelected, onItemClick }) => { - const title = item.path.split('/').pop(); + const title = item.path === '/' ? item.path : item.path.split('/').pop(); const handleItemClick = () => { onItemClick(item.repo, item.path); diff --git a/frontend/src/components/file-chooser/repo-list-item.js b/frontend/src/components/file-chooser/repo-list-item.js index b1b4b69c1a..d59eebfb7f 100644 --- a/frontend/src/components/file-chooser/repo-list-item.js +++ b/frontend/src/components/file-chooser/repo-list-item.js @@ -35,6 +35,7 @@ class RepoListItem extends React.Component { hasLoaded: false, isMounted: false, }; + this.loadRepoTimer = null; } componentDidMount() { @@ -45,17 +46,16 @@ class RepoListItem extends React.Component { const { repoID, filePath } = selectedItemInfo || {}; if (repoID && repoID === repo.repo_id) { this.loadRepoDirentList(repo); - setTimeout(() => { + this.loadRepoTimer = setTimeout(() => { this.setState({ isShowChildren: true }); this.loadNodeAndParentsByPath(repoID, filePath); }, 0); return; } - // the repo is current repo and currentPath is not '/' - if (isCurrentRepo && !repoID) { + if (repo.repo_id === this.props.selectedRepo.repo_id || isCurrentRepo) { this.loadRepoDirentList(repo); - setTimeout(() => { + this.loadRepoTimer = setTimeout(() => { const repoID = repo.repo_id; if (isCurrentRepo && currentPath && currentPath != '/') { const expandNode = true; @@ -65,38 +65,20 @@ class RepoListItem extends React.Component { } } - componentDidUpdate(prevProps) { - if (prevProps.isBrowsing && !this.props.isBrowsing) { - this.setState({ - treeData: treeHelper.buildTree(), - isShowChildren: this.props.initToShowChildren, - }); - - const { isCurrentRepo, currentPath, repo, selectedItemInfo } = this.props; - const { repoID } = selectedItemInfo || {}; - - if (isCurrentRepo && !repoID) { - this.loadRepoDirentList(repo); - setTimeout(() => { - const repoID = repo.repo_id; - if (isCurrentRepo && currentPath && currentPath != '/') { - const expandNode = true; - this.loadNodeAndParentsByPath(repoID, currentPath, expandNode); - } - }, 0); - } - } - - } - componentWillUnmount() { - this.setState({ isMounted: false }); + this.clearLoadRepoTimer(); + this.setState({ isMounted: false, hasLoaded: false }); } + clearLoadRepoTimer = () => { + if (!this.loadRepoTimer) return; + clearTimeout(this.loadRepoTimer); + this.loadRepoTimer = null; + }; + loadRepoDirentList = async (repo) => { const { hasLoaded } = this.state; if (hasLoaded) return; - const repoID = repo.repo_id; try { diff --git a/frontend/src/components/file-chooser/repo-list-view.js b/frontend/src/components/file-chooser/repo-list-view.js index a41ea90562..54d8ab1370 100644 --- a/frontend/src/components/file-chooser/repo-list-view.js +++ b/frontend/src/components/file-chooser/repo-list-view.js @@ -19,6 +19,20 @@ const propTypes = { browsingPath: PropTypes.string, }; +const defaultProps = { + currentRepoInfo: null, + isShowFile: false, + repo: null, + repoList: [], + selectedRepo: null, + selectedPath: '', + fileSuffixes: [], + selectedItemInfo: null, + currentPath: '', + isBrowsing: false, + browsingPath: '', +}; + class RepoListView extends React.Component { render() { @@ -56,5 +70,6 @@ class RepoListView extends React.Component { } RepoListView.propTypes = propTypes; +RepoListView.defaultProps = defaultProps; export default RepoListView; diff --git a/frontend/src/components/file-chooser/repo-list-wrapper.jsx b/frontend/src/components/file-chooser/repo-list-wrapper.jsx new file mode 100644 index 0000000000..55f5b44cd2 --- /dev/null +++ b/frontend/src/components/file-chooser/repo-list-wrapper.jsx @@ -0,0 +1,170 @@ +import React from 'react'; +import PropTypes from 'prop-types'; +import RepoListView from './repo-list-view'; +import RecentlyUsedListView from './recently-used-list-view'; +import { gettext } from '../../utils/constants'; + +export const MODE_TYPE_MAP = { + CURRENT_AND_OTHER_REPOS: 'current_repo_and_other_repos', + ONLY_CURRENT_LIBRARY: 'only_current_library', + ONLY_ALL_REPOS: 'only_all_repos', + ONLY_OTHER_LIBRARIES: 'only_other_libraries', + RECENTLY_USED: 'recently_used', +}; + +const RepoListWrapper = (props) => { + const { + mode, isShowFile, fileSuffixes, currentPath, isBrowsing, browsingPath, isCurrentRepoShow, currentRepoInfo, selectedRepo, + selectedPath, isOtherRepoShow, selectedItemInfo, repoList, + } = props; + + const renderRecentlyUsed = () => { + const recentlyUsedList = JSON.parse(localStorage.getItem('recently-used-list')) || []; + return ( +
+ +
+ ); + }; + + const onScroll = (event) => { + event.stopPropagation(); + }; + + return ( +
+
+ {mode === MODE_TYPE_MAP.CURRENT_AND_OTHER_REPOS && ( + <> +
+
+ + {gettext('Current Library')} +
+ {(isCurrentRepoShow && currentRepoInfo) && + + } +
+
+
+ + {gettext('Other Libraries')} +
+ {isOtherRepoShow && + + } +
+ + )} + {mode === MODE_TYPE_MAP.ONLY_CURRENT_LIBRARY && ( +
+ +
+ )} + {mode === MODE_TYPE_MAP.ONLY_ALL_REPOS && ( +
+
+
+ + {gettext('Libraries')} +
+ +
+
+ )} + {mode === MODE_TYPE_MAP.ONLY_OTHER_LIBRARIES && ( +
+ +
+ )} + {mode === MODE_TYPE_MAP.RECENTLY_USED && renderRecentlyUsed()} +
+
+ ); +}; + +RepoListWrapper.propTypes = { + mode: PropTypes.string, + currentPath: PropTypes.string, + isShowFile: PropTypes.bool, + fileSuffixes: PropTypes.array, + isBrowsing: PropTypes.bool, + browsingPath: PropTypes.string, + selectedItemInfo: PropTypes.object, + currentRepoInfo: PropTypes.object, + selectedRepo: PropTypes.object, + isCurrentRepoShow: PropTypes.bool, + isOtherRepoShow: PropTypes.bool, + selectedPath: PropTypes.string, + repoList: PropTypes.array, + onCurrentRepoToggle: PropTypes.func, + onOtherRepoToggle: PropTypes.func, + handleClickRepo: PropTypes.func, + handleClickDirent: PropTypes.func, +}; + +RepoListWrapper.defaultProps = { + isShowFile: false, + fileSuffixes: [], +}; + +export default RepoListWrapper; diff --git a/frontend/src/components/file-chooser/searched-list-item.js b/frontend/src/components/file-chooser/searched-list-item.js index 30c3e9bb62..4ecfa53f8e 100644 --- a/frontend/src/components/file-chooser/searched-list-item.js +++ b/frontend/src/components/file-chooser/searched-list-item.js @@ -40,13 +40,12 @@ class SearchedListItem extends React.Component { render() { let { item, currentItem } = this.props; - let folderIconUrl = item.link_content ? Utils.getFolderIconUrl(false, 192) : Utils.getDefaultLibIconUrl(false); - let fileIconUrl = item.is_dir ? folderIconUrl : Utils.getFileIconUrl(item.name); return ( - + {item.is_dir ? : } {item.repo_name}/{item.link_content} diff --git a/frontend/src/components/file-chooser/searcher/index.css b/frontend/src/components/file-chooser/searcher/index.css new file mode 100644 index 0000000000..50f6c7b923 --- /dev/null +++ b/frontend/src/components/file-chooser/searcher/index.css @@ -0,0 +1,55 @@ +.file-chooser-searcher.search-container { + display: flex; + flex-direction: column; + position: relative; + margin-bottom: 16px; +} + +.file-chooser-searcher .search-input-container { + position: relative; +} + +.file-chooser-searcher .search-icon-left { + min-width: 2rem; +} + +.file-chooser-searcher .search-input-container .search-input { + width: 100%; + height: 2.375rem; + padding-left: 2rem; + padding-right: 1.5rem; +} + +.file-chooser-searcher .search-input-container .search-control { + position: absolute; + top: 12px; + right: 8px; + font-size: 16px; +} + +.file-chooser-search-results-popover .popover { + min-width: 400px; + padding: 1rem; +} + +.file-chooser-search-results-popover .search-results-popover-body { + width: 100%; +} + +.file-chooser-search-results-popover .search-results-popover-body .search-results-item { + display: flex; + flex-direction: column; + gap: 0.5rem; +} + +.file-chooser-search-results-popover .search-results-popover-body .searched-list-item.searched-dir td { + padding: 0; +} + +.file-chooser-search-results-popover .search-results-popover-body .searched-item-icon { + border-radius: 3px 0 0 3px; +} + +.file-chooser-search-results-popover .search-results-popover-body .searched-item-link { + border-radius: 0 3px 3px 0; +} diff --git a/frontend/src/components/file-chooser/searcher/index.js b/frontend/src/components/file-chooser/searcher/index.js new file mode 100644 index 0000000000..2e872f50e5 --- /dev/null +++ b/frontend/src/components/file-chooser/searcher/index.js @@ -0,0 +1,205 @@ +import React, { useState, useRef, useCallback } from 'react'; +import PropTypes from 'prop-types'; +import { Input, UncontrolledPopover } from 'reactstrap'; +import Loading from '../../loading'; +import toaster from '../../toast'; +import RepoInfo from '../../../models/repo-info'; +import SearchedListView from '../searched-list-view'; +import { gettext } from '../../../utils/constants'; +import { seafileAPI } from '../../../utils/seafile-api'; +import { Utils } from '../../../utils/utils'; + +import './index.css'; + +export const SearchStatus = { + IDLE: 'idle', + LOADING: 'loading', + RESULTS: 'results', + NO_RESULTS: 'no_results', + BROWSING: 'browsing', +}; + +const Searcher = ({ searchStatus, onUpdateSearchStatus, onDirentItemClick, selectSearchedItem, selectRepo, selectPath, setBrowsingPath }) => { + const [inputValue, setInputValue] = useState(''); + const [isResultsPopoverOpen, setIsResultsPopoverOpen] = useState(false); + const [searchResults, setSearchResults] = useState([]); + + const inputRef = useRef(null); + + const searchTimer = useRef(null); + const source = useRef(null); + + const onPopoverToggle = useCallback((show) => { + setIsResultsPopoverOpen(show); + }, []); + + const handleSearchInputChange = (e) => { + const newValue = e.target.value.trim(); + setInputValue(newValue); + + if (newValue.length === 0) { + onUpdateSearchStatus(SearchStatus.IDLE); + setSearchResults([]); + return; + } + + onUpdateSearchStatus(SearchStatus.LOADING); + onPopoverToggle(true); + + const queryData = { + q: newValue, + search_repo: 'all', + search_ftypes: 'all', + obj_type: 'dir', + }; + + if (searchTimer) { + clearTimeout(searchTimer.current); + } + + searchTimer.current = setTimeout(() => { + getSearchResult(queryData); + }, 500); + }; + + const getSearchResult = useCallback((queryData) => { + if (source.current) { + source.current.cancel('prev request is cancelled'); + } + + source.current = seafileAPI.getSource(); + seafileAPI.searchFiles(queryData, source.current.token).then(res => { + setSearchResults(res.data.total ? formatResultItems(res.data.results) : []); + onUpdateSearchStatus(res.data.results.length > 0 ? SearchStatus.RESULTS : SearchStatus.NO_RESULTS); + source.current = null; + }).catch(err => { + onUpdateSearchStatus(SearchStatus.NO_RESULTS); + source.current = null; + }); + }, [onUpdateSearchStatus]); + + const 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; + }; + + const onCloseSearching = useCallback(() => { + setInputValue(''); + setSearchResults([]); + onUpdateSearchStatus(SearchStatus.IDLE); + onPopoverToggle(false); + selectSearchedItem(null); + }, [onUpdateSearchStatus, selectSearchedItem, onPopoverToggle]); + + const onSearchedItemClick = (item) => { + item['type'] = item.is_dir ? 'dir' : 'file'; + let repo = new RepoInfo(item); + onDirentItemClick(repo, item.path, item); + }; + + const onSearchedItemDoubleClick = (item) => { + if (item.type !== 'dir') return; + + const selectedItemInfo = { + repoID: item.repo_id, + filePath: item.path, + }; + + selectSearchedItem(selectedItemInfo); + onPopoverToggle(false); + + seafileAPI.getRepoInfo(item.repo_id).then(res => { + const repoInfo = new RepoInfo(res.data); + const path = item.path.substring(0, item.path.length - 1); + selectRepo(repoInfo); + selectPath(path); + setBrowsingPath(item.path.substring(0, item.path.length - 1)); + }).catch(err => { + const errMessage = Utils.getErrorMsg(err); + toaster.danger(errMessage); + }); + + onUpdateSearchStatus(SearchStatus.BROWSING); + }; + + const renderSearchResults = () => { + switch (searchStatus) { + case SearchStatus.IDLE: + return null; + case SearchStatus.LOADING: + return ; + case SearchStatus.NO_RESULTS: + return ( +
{gettext('No results matching')}
+ ); + case SearchStatus.BROWSING: + case SearchStatus.RESULTS: + return ( + + ); + } + }; + + return ( +
+
+ + + {inputValue.length !== 0 && ( + + )} +
+ {searchStatus !== SearchStatus.IDLE && + onPopoverToggle(!isResultsPopoverOpen)} + target={inputRef.current} + placement='bottom-start' + hideArrow={true} + fade={false} + trigger="legacy" + > +
+ {renderSearchResults()} +
+
+ } +
+ ); +}; + +Searcher.propTypes = { + searchStatus: PropTypes.string, + onUpdateSearchStatus: PropTypes.func, + onDirentItemClick: PropTypes.func, + selectSearchedItem: PropTypes.func, + selectRepo: PropTypes.func, + selectPath: PropTypes.func, + setBrowsingPath: PropTypes.func, +}; + +export default Searcher; diff --git a/frontend/src/css/lib-content-view.css b/frontend/src/css/lib-content-view.css index 2369e2b7b0..fa35fbe50b 100644 --- a/frontend/src/css/lib-content-view.css +++ b/frontend/src/css/lib-content-view.css @@ -307,7 +307,7 @@ } .custom-modal .modal-body { - flex: 1; + height: 100%; display: flex; flex-direction: column; gap: 1rem; @@ -325,6 +325,10 @@ padding: 0.5rem; } +.custom-modal .modal-body .file-chooser-container { + height: 24rem; +} + .custom-modal .repo-list-col { max-width: 240px; padding: 1rem; diff --git a/frontend/src/pages/lib-content-view/lib-content-view.js b/frontend/src/pages/lib-content-view/lib-content-view.js index 1fce32cf9c..81a0c20f11 100644 --- a/frontend/src/pages/lib-content-view/lib-content-view.js +++ b/frontend/src/pages/lib-content-view/lib-content-view.js @@ -938,6 +938,8 @@ class LibContentView extends React.Component { this.deleteDirents(dirNames); + this.removeFromRecentlyUsed(repoID, this.state.path); + let msg = ''; if (direntPaths.length > 1) { msg = gettext('Successfully deleted {name} and {n} other items.'); @@ -965,6 +967,14 @@ class LibContentView extends React.Component { }); }; + removeFromRecentlyUsed = (repoID, path) => { + const recentlyUsed = JSON.parse(localStorage.getItem('recently-used-list')) || []; + const updatedRecentlyUsed = recentlyUsed.filter(item => + !(item.repo.repo_id === repoID && item.path === path) + ); + localStorage.setItem('recently-used-list', JSON.stringify(updatedRecentlyUsed)); + }; + onAddFolder = (dirPath) => { let repoID = this.props.repoID; seafileAPI.createDir(repoID, dirPath).then(() => { @@ -1237,6 +1247,7 @@ class LibContentView extends React.Component { this.deleteTreeNode(path); } this.deleteDirent(path); + this.removeFromRecentlyUsed(this.props.repoID, path); } // list operations