From 8623e01e99125b07b63a50dc0b543b31b4ca6681 Mon Sep 17 00:00:00 2001 From: Aries Date: Mon, 5 Aug 2024 10:48:16 +0800 Subject: [PATCH] Improve move and copy dialog ui components (#6467) * implement catalog view in move dialog * update move dialog ui components * improve move dialog ui * remove debug info * improve copy dialog ui * improve search view ui in move and copy dialog, refactor part of file-chooser.js and file-chooser.css * handle cases where repo_name is too long, truncate text with an ellipsis, remove search container border * handle cases where repo_name too long in search result item * update move and dialog ui details * add radius to repo list item --- .../components/dialog/copy-dirent-dialog.js | 86 +-- .../components/dialog/move-dirent-dialog.js | 88 +-- .../dirent-grid-view/dirent-grid-view.js | 1 + .../components/file-chooser/file-chooser.js | 509 ++++++++++-------- .../components/file-chooser/repo-list-item.js | 32 +- .../components/file-chooser/repo-list-view.js | 9 +- .../components/file-chooser/tree-list-item.js | 26 +- .../components/file-chooser/tree-list-view.js | 3 +- frontend/src/css/file-chooser.css | 220 ++++---- frontend/src/css/lib-content-view.css | 89 +++ frontend/src/css/search.css | 2 +- .../lib-content-view/lib-content-view.js | 27 + 12 files changed, 679 insertions(+), 413 deletions(-) diff --git a/frontend/src/components/dialog/copy-dirent-dialog.js b/frontend/src/components/dialog/copy-dirent-dialog.js index fe064ce96b..d7918d2e13 100644 --- a/frontend/src/components/dialog/copy-dirent-dialog.js +++ b/frontend/src/components/dialog/copy-dirent-dialog.js @@ -1,6 +1,6 @@ import React from 'react'; import PropTypes from 'prop-types'; -import { Button, Modal, ModalHeader, ModalFooter, ModalBody, Alert } from 'reactstrap'; +import { Button, Modal, ModalHeader, ModalFooter, ModalBody, Alert, Row, Col } from 'reactstrap'; import { gettext } from '../../utils/constants'; import { Utils } from '../../utils/utils'; import FileChooser from '../file-chooser/file-chooser'; @@ -25,17 +25,11 @@ class CopyDirent extends React.Component { this.state = { repo: { repo_id: this.props.repoID }, selectedPath: this.props.path, - errMessage: '' + errMessage: '', + mode: 'only_current_library', }; } - shouldComponentUpdate(nextProps, nextState) { - if (this.state.errMessage === nextState.errMessage) { - return false; - } - return true; - } - handleSubmit = () => { if (this.props.isMultipleOperation) { this.copyItems(); @@ -137,34 +131,64 @@ class CopyDirent extends React.Component { }); }; - render() { + onSelectedMode = (mode) => { + this.setState({ mode: mode }); + }; + + renderTitle = () => { + const { dirent, isMultipleOperation } = this.props; let title = gettext('Copy {placeholder} to'); - if (!this.props.isMultipleOperation) { - title = title.replace('{placeholder}', '' + Utils.HTMLescape(this.props.dirent.name) + ''); + + if (isMultipleOperation) { + return gettext('Copy selected item(s) to:'); } else { - title = gettext('Copy selected item(s) to:'); + return title.replace('{placeholder}', `${Utils.HTMLescape(dirent.name)}`); } - let mode = 'current_repo_and_other_repos'; - const { isMultipleOperation } = this.props; + }; + + render() { + const { dirent, selectedDirentList, isMultipleOperation, repoID, path } = this.props; + const { mode, errMessage } = this.state; + + const copiedDirent = dirent || selectedDirentList[0]; + const { permission } = copiedDirent; + const { isCustomPermission } = Utils.getUserPermission(permission); + + const LibraryOption = ({ mode, label }) => ( +
this.onSelectedMode(mode)}> + {label} +
+ ); + return ( - + - {isMultipleOperation ? title :
} + {isMultipleOperation ? this.renderTitle() :
}
- - - {this.state.errMessage && {this.state.errMessage}} - - - - - + + + + {!isCustomPermission && } + + + + + + {errMessage && {errMessage}} + + + + + + +
); } diff --git a/frontend/src/components/dialog/move-dirent-dialog.js b/frontend/src/components/dialog/move-dirent-dialog.js index c3d999676b..5c343afd58 100644 --- a/frontend/src/components/dialog/move-dirent-dialog.js +++ b/frontend/src/components/dialog/move-dirent-dialog.js @@ -1,6 +1,6 @@ import React from 'react'; import PropTypes from 'prop-types'; -import { Button, Modal, ModalHeader, ModalFooter, ModalBody, Alert } from 'reactstrap'; +import { Button, Modal, ModalHeader, ModalFooter, ModalBody, Alert, Row, Col } from 'reactstrap'; import { gettext } from '../../utils/constants'; import { Utils } from '../../utils/utils'; import FileChooser from '../file-chooser/file-chooser'; @@ -25,17 +25,11 @@ class MoveDirent extends React.Component { this.state = { repo: { repo_id: this.props.repoID }, selectedPath: this.props.path, - errMessage: '' + errMessage: '', + mode: 'only_current_library', }; } - shouldComponentUpdate(nextProps, nextState) { - if (this.state.errMessage === nextState.errMessage) { - return false; - } - return true; - } - handleSubmit = () => { if (this.props.isMultipleOperation) { this.moveItems(); @@ -151,40 +145,64 @@ class MoveDirent extends React.Component { }); }; - render() { + onSelectedMode = (mode) => { + this.setState({ mode: mode }); + }; + + renderTitle = () => { + const { dirent, isMultipleOperation } = this.props; let title = gettext('Move {placeholder} to'); - if (!this.props.isMultipleOperation) { - title = title.replace('{placeholder}', '' + Utils.HTMLescape(this.props.dirent.name) + ''); + + if (isMultipleOperation) { + return gettext('Move selected item(s) to:'); } else { - title = gettext('Move selected item(s) to:'); + return title.replace('{placeholder}', `${Utils.HTMLescape(dirent.name)}`); } - let mode = 'current_repo_and_other_repos'; - const { dirent, selectedDirentList, isMultipleOperation } = this.props; - const movedDirent = dirent ? dirent : selectedDirentList[0]; + }; + + render() { + const { dirent, selectedDirentList, isMultipleOperation, repoID, path } = this.props; + const { mode, errMessage } = this.state; + + const movedDirent = dirent || selectedDirentList[0]; const { permission } = movedDirent; const { isCustomPermission } = Utils.getUserPermission(permission); - if (isCustomPermission) { - mode = 'only_current_library'; - } + + const LibraryOption = ({ mode, label }) => ( +
this.onSelectedMode(mode)}> + {label} +
+ ); + return ( - + - {isMultipleOperation ? title :
} + {isMultipleOperation ? this.renderTitle() :
}
- - - {this.state.errMessage && {this.state.errMessage}} - - - - - + + + + {!isCustomPermission && } + + + + + + {errMessage && {errMessage}} + + + + + + +
); } diff --git a/frontend/src/components/dirent-grid-view/dirent-grid-view.js b/frontend/src/components/dirent-grid-view/dirent-grid-view.js index 216d4e2c8d..a255e19e59 100644 --- a/frontend/src/components/dirent-grid-view/dirent-grid-view.js +++ b/frontend/src/components/dirent-grid-view/dirent-grid-view.js @@ -821,6 +821,7 @@ class DirentGridView extends React.Component { repoEncrypted={this.props.currentRepoInfo.encrypted} isMultipleOperation={this.state.isMultipleOperation} selectedDirentList={selectedDirentList} + onItemMove={this.props.onItemMove} onItemsMove={this.props.onItemsMove} onCancelMove={this.onMoveToggle} dirent={this.state.activeDirent} diff --git a/frontend/src/components/file-chooser/file-chooser.js b/frontend/src/components/file-chooser/file-chooser.js index f10740e95c..7fc2dfe91a 100644 --- a/frontend/src/components/file-chooser/file-chooser.js +++ b/frontend/src/components/file-chooser/file-chooser.js @@ -18,11 +18,27 @@ const propTypes = { repoID: PropTypes.string, onDirentItemClick: PropTypes.func, onRepoItemClick: PropTypes.func, - mode: PropTypes.oneOf(['current_repo_and_other_repos', 'only_all_repos', 'only_current_library']), - fileSuffixes: PropTypes.array, + mode: PropTypes.oneOf([ + 'current_repo_and_other_repos', + 'only_all_repos', + 'only_current_library', + 'only_other_libraries', + 'recently_used' + ]).isRequired, + fileSuffixes: PropTypes.arrayOf(PropTypes.string), currentPath: PropTypes.string, }; +const defaultProps = { + isShowFile: false, + hideLibraryName: false, + repoID: '', + onDirentItemClick: () => {}, + onRepoItemClick: () => {}, + fileSuffixes: [], + currentPath: '', +}; + class FileChooser extends React.Component { constructor(props) { @@ -46,71 +62,95 @@ class FileChooser extends React.Component { this.source = null; } - componentDidMount() { - 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 - let repoInfo = new RepoInfo(res.data); + async componentDidMount() { + const { repoID } = this.props; + + const fetchRepoInfo = async (repoID) => { + try { + const res = await seafileAPI.getRepoInfo(repoID); + const repoInfo = new RepoInfo(res.data); this.setState({ currentRepoInfo: repoInfo, - selectedRepo: repoInfo + selectedRepo: repoInfo, }); - }).catch(error => { - let errMessage = Utils.getErrorMsg(error); + } catch (error) { + const errMessage = Utils.getErrorMsg(error); toaster.danger(errMessage); - }); - } 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; + } + }; + + const fetchRepoList = async () => { + try { + const res = await seafileAPI.listRepos(); + const repos = res.data.repos; + const repoList = []; + const repoIdList = []; + + repos.forEach(repo => { + if (repo.permission !== 'rw' || repoIdList.includes(repo.repo_id)) { + return; } - 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 }); - }); + repoList.push(repo); + repoIdList.push(repo.repo_id); + }); + + const sortedRepoList = Utils.sortRepos(repoList, 'name', 'asc'); + this.setState({ repoList: sortedRepoList }); + } catch (error) { + const errMessage = Utils.getErrorMsg(error); + toaster.danger(errMessage); + } + }; + + if (repoID) { + await fetchRepoInfo(repoID); + } else { + await fetchRepoList(); } } - onOtherRepoToggle = () => { - if (!this.state.hasRequest) { - let that = this; - seafileAPI.listRepos().then(res => { - // todo optimized code - let repos = res.data.repos; - let repoList = []; - let repoIdList = []; - for (let i = 0; i < repos.length; i++) { - if (repos[i].permission !== 'rw') { - continue; - } - if (that.props.repoID && (repos[i].repo_name === that.state.currentRepoInfo.repo_name)) { - 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, - isOtherRepoShow: !this.state.isOtherRepoShow, - selectedItemInfo: {} - }); + componentDidUpdate(prevProps, prevState) { + if (prevProps.mode !== this.props.mode) { + this.setState({ + isSearching: false, + isResultGot: false, + searchInfo: '', + searchResults: [], }); + if (this.props.mode === 'only_other_libraries') { + this.onOtherRepoToggle(); + } } - else { + } + + onOtherRepoToggle = async () => { + if (!this.state.hasRequest) { + 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; + + 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 }); } }; @@ -151,32 +191,22 @@ class FileChooser extends React.Component { onSearchInfoChanged = (event) => { let searchInfo = event.target.value.trim(); - - this.setState({ searchInfo: searchInfo }); - - if (this.inputValue === searchInfo) { - return false; - } - - this.inputValue = searchInfo; - - if (searchInfo.length === 0) { - this.setState({ - isSearching: false, - searchResults: [], - }); - return false; - } - if (!this.state.searchResults.length && searchInfo.length > 0) { this.setState({ isSearching: true, isResultGot: false, }); } + 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 }); + this.setState({ + isResultGot: false, + }); return false; } @@ -297,205 +327,248 @@ class FileChooser extends React.Component { } }; - onSearchedItemDoubleClick = (item) => { + onSearchedItemDoubleClick = async (item) => { if (item.type !== 'dir') { return; } - let selectedItemInfo = { + const { repoID } = this.props; + const { hasRequest, currentRepoInfo } = this.state; + + const selectedItemInfo = { repoID: item.repo_id, filePath: item.path, }; - this.setState({ - selectedItemInfo: selectedItemInfo - }); + this.setState({ selectedItemInfo }); - if (this.props.repoID && item.repo_id === this.props.repoID) { - seafileAPI.getRepoInfo(this.props.repoID).then(res => { - // need to optimized - let repoInfo = new RepoInfo(res.data); - let path = item.path.substring(0, (item.path.length - 1)); + const updateStateForRepo = (repoInfo, path) => { + this.setState({ + selectedRepo: repoInfo, + selectedPath: path, + isCurrentRepoShow: true, + }); + }; + + const handleError = (error) => { + const errMessage = Utils.getErrorMsg(error); + toaster.danger(errMessage); + }; + + const fetchRepoInfo = async () => { + try { + const res = await seafileAPI.getRepoInfo(repoID); + const repoInfo = new RepoInfo(res.data); + const path = item.path.substring(0, item.path.length - 1); + updateStateForRepo(repoInfo, path); + } catch (error) { + handleError(error); + } + }; + + const fetchRepoList = async () => { + try { + const res = await seafileAPI.listRepos(); + const repos = res.data.repos; + const repoList = []; + const repoIdList = []; + + repos.forEach(repo => { + if (repo.permission !== 'rw') return; + if (repoID && repo.repo_name === currentRepoInfo.repo_name) return; + if (repoIdList.includes(repo.repo_id)) return; + + repoList.push(repo); + repoIdList.push(repo.repo_id); + }); + + const sortedRepoList = Utils.sortRepos(repoList, 'name', 'asc'); + const selectedRepo = sortedRepoList.find(repo => repo.repo_id === item.repo_id); + const path = item.path.substring(0, item.path.length - 1); this.setState({ - selectedRepo: repoInfo, + repoList: sortedRepoList, + isOtherRepoShow: true, selectedPath: path, - isCurrentRepoShow: true, - }); - }).catch(error => { - let errMessage = Utils.getErrorMsg(error); - toaster.danger(errMessage); - }); - } else { - if (!this.state.hasRequest) { - let that = this; - seafileAPI.listRepos().then(res => { - // todo optimized code - let repos = res.data.repos; - let repoList = []; - let repoIdList = []; - for (let i = 0; i < repos.length; i++) { - if (repos[i].permission !== 'rw') { - continue; - } - if (that.props.repoID && (repos[i].repo_name === that.state.currentRepoInfo.repo_name)) { - 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'); - let repo = repoList.filter(repoItem => repoItem.repo_id === item.repo_id); - let path = item.path.substring(0, (item.path.length - 1)); - - let selectRepo = repo[0]; - this.setState({ - repoList: repoList, - isOtherRepoShow: true, - selectedPath: path, - selectedRepo: selectRepo, - }); + selectedRepo, }); + } catch (error) { + handleError(error); } - else { - this.setState({ isOtherRepoShow: !this.state.isOtherRepoShow }); + }; + + if (repoID && item.repo_id === repoID) { + await fetchRepoInfo(); + } else { + if (!hasRequest) { + await fetchRepoList(); + } else { + this.setState(prevState => ({ isOtherRepoShow: !prevState.isOtherRepoShow })); } } + this.onCloseSearching(); }; - onScroll = (event) => { event.stopPropagation(); }; renderRepoListView = () => { + const { mode, currentPath, isShowFile, fileSuffixes } = this.props; + const { isCurrentRepoShow, isOtherRepoShow, currentRepoInfo, repoList, selectedRepo, selectedPath, selectedItemInfo } = this.state; + const recentlyUsedRepos = JSON.parse(localStorage.getItem('recently-used-repos')) || []; + return ( -
- {this.props.mode === 'current_repo_and_other_repos' && ( - -
-
- - {gettext('Current Library')} +
+
+ {mode === 'current_repo_and_other_repos' && ( + +
+
+ + {gettext('Current Library')} +
+ { + isCurrentRepoShow && currentRepoInfo && + + }
- { - this.state.isCurrentRepoShow && this.state.currentRepoInfo && - - } -
-
-
- - {gettext('Other Libraries')} +
+
+ + {gettext('Other Libraries')} +
+ { + isOtherRepoShow && + + }
- { - this.state.isOtherRepoShow && - - } -
- - )} - {this.props.mode === 'only_current_library' && ( -
- {!this.props.hideLibraryName && -
- - {gettext('Current Library')} -
- } - { - this.state.isCurrentRepoShow && this.state.currentRepoInfo && + + )} + {mode === 'only_current_library' && ( +
- } -
- )} - {this.props.mode === 'only_all_repos' && ( -
-
-
- - {gettext('Libraries')} -
-
-
- )} + )} + {mode === 'only_all_repos' && ( +
+
+
+ + {gettext('Libraries')} +
+ +
+
+ )} + {mode === 'only_other_libraries' && ( +
+ +
+ )} + {mode === 'recently_used' && ( +
+ +
+ )} +
); }; render() { - if (!this.state.selectedRepo && this.props.repoID) { + const { repoID } = this.props; + const { selectedRepo, searchInfo, isSearching } = this.state; + + if (!selectedRepo && repoID) { return ''; } return ( - {isPro && ( + {isPro && this.props.mode !== 'recently_used' && (
- - {this.state.searchInfo.length !== 0 && ( + + {searchInfo.length !== 0 && ( )}
)} - {this.state.isSearching && ( + {isSearching ? (
{this.renderSearchedView()}
+ ) : ( + this.renderRepoListView() )} - {!this.state.isSearching && this.renderRepoListView()}
); } } FileChooser.propTypes = propTypes; +FileChooser.defaultProps = defaultProps; export default FileChooser; diff --git a/frontend/src/components/file-chooser/repo-list-item.js b/frontend/src/components/file-chooser/repo-list-item.js index 43c71fc209..8398f71080 100644 --- a/frontend/src/components/file-chooser/repo-list-item.js +++ b/frontend/src/components/file-chooser/repo-list-item.js @@ -31,10 +31,12 @@ class RepoListItem extends React.Component { isShowChildren: this.props.initToShowChildren, treeData: treeHelper.buildTree(), hasLoaded: false, + isMounted: false, }; } componentDidMount() { + this.setState({ isMounted: true }); const { isCurrentRepo, currentPath, repo, selectedItemInfo } = this.props; // render search result @@ -61,27 +63,31 @@ class RepoListItem extends React.Component { } } - loadRepoDirentList = (repo) => { + componentWillUnmount() { + this.setState({ isMounted: false }); + } + + loadRepoDirentList = async (repo) => { const { hasLoaded } = this.state; if (hasLoaded) return; const repoID = repo.repo_id; - seafileAPI.listDir(repoID, '/').then(res => { + try { + const res = await seafileAPI.listDir(repoID, '/'); + if (!this.state.isMounted) return; + let tree = this.state.treeData.clone(); - let direntList = []; - if (this.props.isShowFile === true) { - direntList = res.data.dirent_list; - } else { - direntList = res.data.dirent_list.filter(item => item.type === 'dir'); - } + let direntList = this.props.isShowFile ? res.data.dirent_list : res.data.dirent_list.filter(item => item.type === 'dir'); this.addResponseListToNode(direntList, tree.root); this.setState({ treeData: tree, hasLoaded: true }); - }).catch(error => { + } catch (error) { + if (!this.state.isMounted) return; + let errMessage = Utils.getErrorMsg(error); toaster.danger(errMessage); - }); + } }; addResponseListToNode = (list, node) => { @@ -201,15 +207,15 @@ class RepoListItem extends React.Component {
  • {!this.props.hideLibraryName &&
    -
    - {this.props.repo.repo_name} -
    +
    + {this.props.repo.repo_name} +
    } {this.state.isShowChildren && ( diff --git a/frontend/src/components/file-chooser/repo-list-view.js b/frontend/src/components/file-chooser/repo-list-view.js index 2ae0970c17..be8bda9f19 100644 --- a/frontend/src/components/file-chooser/repo-list-view.js +++ b/frontend/src/components/file-chooser/repo-list-view.js @@ -15,7 +15,6 @@ const propTypes = { fileSuffixes: PropTypes.array, selectedItemInfo: PropTypes.object, currentPath: PropTypes.string, - hideLibraryName: PropTypes.bool, }; class RepoListView extends React.Component { @@ -26,12 +25,9 @@ class RepoListView extends React.Component { repoList = []; repoList.push(currentRepoInfo); } - let style = {}; - if (this.props.hideLibraryName) { - style = { marginLeft: '-44px' }; - } + return ( -
      +
        {repoList.length > 0 && repoList.map((repoItem, index) => { return ( ); })} diff --git a/frontend/src/components/file-chooser/tree-list-item.js b/frontend/src/components/file-chooser/tree-list-item.js index dc0db29e0f..96ea448b4e 100644 --- a/frontend/src/components/file-chooser/tree-list-item.js +++ b/frontend/src/components/file-chooser/tree-list-item.js @@ -11,13 +11,22 @@ const propTypes = { onNodeExpanded: PropTypes.func.isRequired, filePath: PropTypes.string, fileSuffixes: PropTypes.array, + level: PropTypes.number, }; class TreeViewItem extends React.Component { constructor(props) { super(props); - let filePath = this.props.filePath ? this.props.filePath + '/' + this.props.node.object.name : this.props.node.path; + let filePath; + + if (this.props.filePath === '/') { + filePath = '/' + this.props.node.object.name; + } else if (this.props.filePath) { + filePath = this.props.filePath + '/' + this.props.node.object.name; + } else { + filePath = this.props.node.path; + } this.state = { filePath: filePath, @@ -73,6 +82,8 @@ class TreeViewItem extends React.Component { selectedRepo={this.props.selectedRepo} selectedPath={this.props.selectedPath} fileSuffixes={this.props.fileSuffixes} + filePath={this.state.filePath} + level={(this.props.level || 0) + 1} />); })}
  • @@ -97,13 +108,15 @@ class TreeViewItem extends React.Component { } } + const paddingLeft = `${this.props.level * 20}px`; return (
    -
    -
    - {node.object && node.object.name} -
    +
    { node.object.type !== 'file' && @@ -113,6 +126,9 @@ class TreeViewItem extends React.Component {
    +
    + {node.object && node.object.name} +
    {node.isExpanded && this.renderChildren()} diff --git a/frontend/src/components/file-chooser/tree-list-view.js b/frontend/src/components/file-chooser/tree-list-view.js index 916949c8d4..b3e551729a 100644 --- a/frontend/src/components/file-chooser/tree-list-view.js +++ b/frontend/src/components/file-chooser/tree-list-view.js @@ -17,7 +17,7 @@ class TreeListView extends React.Component { render() { return ( -
    +
    ); diff --git a/frontend/src/css/file-chooser.css b/frontend/src/css/file-chooser.css index e21ab1d2f5..bd1b03fb55 100644 --- a/frontend/src/css/file-chooser.css +++ b/frontend/src/css/file-chooser.css @@ -1,78 +1,97 @@ .file-chooser-container { - padding: 0.5rem; - height: 20rem; - border: 1px solid rgba(0, 40, 100, 0.12); - border-radius: 3px; - transition: border-color 0.15s ease-in-out, box-shadow 0.15s ease-in-out; - overflow: auto; - font-size: 1rem; -} - -.item-toggle{ - position: absolute; - height: 1.5rem; - width: 1.5rem; - left: 0; - top: 0; - line-height: 1.5rem !important; - text-align: center; - cursor: pointer; - color: #c0c0c0; -} - -.file-chooser-container .list-view { - margin-top: 0.25rem; -} - -.list-view-header { position: relative; - padding-left: 1.5rem; -} -.list-view-header:hover { - background-color: #FDEFB9; -} - -.list-view-header .name { - color: #eb8205; -} - -.list-view-content { - margin: 0; - padding: 0; - list-style: none; + height: 20rem; + border-radius: 3px; + font-size: 1rem; + transition: border-color 0.15s ease-in-out, box-shadow 0.15s ease-in-out; } .file-chooser-item { position: relative; - padding-left: 22px; -} - -.file-chooser-item .item-info { - height: 1.5rem; - cursor: pointer; - position: relative; - line-height: 1.625; + width: 100%; } .file-chooser-item .item-active { - background: #F3AF7D !important; + background: #f2f4f6 !important; + border-radius: 3px; +} + +.file-chooser-item .item-active::before { + content: ''; + position: absolute; + top: 2px; + left: -8px; + width: 4px; + height: 24px; + background-color: #ff8000; border-radius: 2px; - box-shadow: inset 0 0 1px #999; - color: #fff; + display: block; + z-index: 10; } - .file-chooser-item .item-info:hover { - background: #FDEFB9; - border-radius: 2px; - box-shadow: inset 0 0 1px #999; +.file-chooser-item .item-info { + position: relative; + display: flex; + align-items: center; + height: 1.75rem; + line-height: 1.625; + cursor: pointer; + transition: background-color 0.3s ease, color 0.3s ease; } -.file-chooser-item .item-info .name { - flex: 1; +.file-chooser-item .item-info:hover { + background: #f5f5f5; + border-radius: 3px; } -.file-chooser-item .item-active .icon { - color: #fff !important; +.file-chooser-item .item-info .item-left-icon { + display: flex; + align-items: center; + height: 100%; +} + +.file-chooser-item .item-info .item-text { + display: flex; + align-items: center; + height: 100%; + font-size: 16px; + line-height: 1.5; + padding-left: 0.25rem; + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; + transition: color 0.3s ease, background-color 0.3s ease; +} + +.file-chooser-search-close { + position: absolute; + top: -0.5rem; + right: -0.5rem; +} + +.file-chooser-search-container { + position: relative; + height: 20rem; + padding: 10px; + overflow: auto; +} + +.file-chooser-search-container td { + height: 40px; + padding: 0; +} + +.file-chooser-search-container td .span { + display: inline-block; + max-width: 100%; + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; + vertical-align: middle; +} + +.file-chooser-search-container .tr-highlight { + background-color: #f5f5f5; } .file-chooser-search-input { @@ -82,67 +101,64 @@ .file-chooser-search-input .search-control { position: absolute; top: 0.5rem; - right: 0.7rem; + right: 1.5rem; } .file-chooser-search-input .search-input { width: 100%; } -.file-chooser-search-container { - height: 20rem; +.file-chooser-table td { + border-bottom: 1px solid rgba(0, 0, 0, 0); +} + +.item-toggle { + width: 1.5rem; + height: 1.5rem; + line-height: 1.5rem !important; + color: #c0c0c0; + text-align: center; + cursor: pointer; +} + +.list-view-content { + margin: 0; + padding: 0; + list-style: none; +} + +.list-view-header { position: relative; - border: 1px solid #eee; - padding: 10px; + padding-left: 1.5rem; +} + +.list-view-header:hover { + background-color: #f5f5f5; +} + +.list-view-header .name { + color: #eb8205; +} + +.scroll-wrapper { + max-height: 100%; overflow: auto; } -.file-chooser-search-close { - position: absolute; - right: -0.5rem; - top: -0.5rem; -} - .searched-active { - background: #F3AF7D !important; + background: #f3af7d !important; border-radius: 2px; box-shadow: inset 0 0 1px #999; } -.searched-active td { - color: #fff; -} - .searched-active .icon { color: #fff !important; } +.searched-active td { + color: #fff; +} + .select-open-repo { - background: #FDEFB9; + background: #fdefb9; } - -.file-chooser-table td { - border-bottom: 1px solid rgba(0, 0, 0, 0); -} - -.file-chooser-item .item-info .item-text { - padding-left: 2.8rem; - font-size: 15px; - white-space: nowrap; - overflow: hidden; - text-overflow: ellipsis; - line-height: 24px; -} - -.file-chooser-item .item-info .item-left-icon { - position: absolute; - display: flex; - align-items: center; - top: 0; - left: 0; - padding-left: 1.5rem; -} - - - - diff --git a/frontend/src/css/lib-content-view.css b/frontend/src/css/lib-content-view.css index 2f721858f7..d91d5a4d5a 100644 --- a/frontend/src/css/lib-content-view.css +++ b/frontend/src/css/lib-content-view.css @@ -289,3 +289,92 @@ .dir-view-path .path-split { padding: 0 2px; } + +.custom-modal { + box-sizing: border-box; + font-size: 1rem; + max-width: 740px; +} + +.custom-modal .row { + flex: 1; + margin: 0; +} + +.custom-modal .modal-content { + min-height: 534px; + border: 0; +} + +.custom-modal .modal-body { + flex: 1; + display: flex; + flex-direction: column; + gap: 1rem; + padding: 1rem 0; + position: relative; +} + +.custom-modal .modal-body .alert-message { + position: absolute; + bottom: 0; + left: 0; + right: 0; + z-index: 10; + margin: 0; + padding: 0.5rem; +} + +.custom-modal .repo-list-col { + max-width: 240px; + padding: 1rem; + line-height: 24px; + text-overflow: ellipsis; + white-space: nowrap; +} + +.custom-modal .repo-list-item { + display: flex; + align-items: center; + position: relative; + height: 32px; + padding: 0 10px; + cursor: pointer; + transition: background-color 0.3s ease, font-weight 0.3s ease; +} + +.custom-modal .repo-list-item.active { + background-color: #f5f5f5; + font-weight: 500; + border-radius: 3px; +} + +.custom-modal .repo-list-item:hover{ + background-color: #f0f0f0; + border-radius: 3px; +} + +.custom-modal .repo-list-item.active::before { + content: ''; + display: block; + position: absolute; + top: 2px; + left: -8px; + width: 4px; + height: 24px; + background-color: #ff8000; + border-radius: 2px; + z-index: 0; +} + +.custom-modal .file-list-col { + max-width: 500px; + padding: 0; + display: flex; + flex-direction: column; +} + +.custom-modal .file-list-col .file-chooser-container, +.custom-modal .file-list-col .file-chooser-search-input { + padding: 0 1rem; +} diff --git a/frontend/src/css/search.css b/frontend/src/css/search.css index 70bdb28a1b..5db30cbeaa 100644 --- a/frontend/src/css/search.css +++ b/frontend/src/css/search.css @@ -78,7 +78,7 @@ } .search-input { - height: 1.875rem; + height: 2.375rem; width: 15rem; font-size: .875rem; } 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 26e63d9938..b4ae747f71 100644 --- a/frontend/src/pages/lib-content-view/lib-content-view.js +++ b/frontend/src/pages/lib-content-view/lib-content-view.js @@ -715,6 +715,27 @@ class LibContentView extends React.Component { }); }; + updateRecentlyUsedRepos = (destRepo) => { + const recentlyUsed = JSON.parse(localStorage.getItem('recently-used-repos')) || []; + const updatedRecentlyUsed = [destRepo, ...recentlyUsed.filter(repo => repo.repo_id !== destRepo.repo_id)]; + + const seen = new Set(); + const filteredRecentlyUsed = updatedRecentlyUsed.filter(repo => { + if (seen.has(repo.repo_id)) { + return false; + } else { + seen.add(repo.repo_id); + return true; + } + }); + + if (filteredRecentlyUsed.length > 10) { + updatedRecentlyUsed.pop(); // Limit to 10 recent directories + } + + localStorage.setItem('recently-used-repos', JSON.stringify(filteredRecentlyUsed)); + }; + // toolbar operations onMoveItems = (destRepo, destDirentPath) => { let repoID = this.props.repoID; @@ -722,6 +743,7 @@ class LibContentView extends React.Component { let dirNames = this.getSelectedDirentNames(); let direntPaths = this.getSelectedDirentPaths(); + seafileAPI.moveDir(repoID, destRepo.repo_id, destDirentPath, this.state.path, dirNames).then(res => { if (repoID !== destRepo.repo_id) { this.setState({ @@ -753,6 +775,8 @@ class LibContentView extends React.Component { toaster.success(message); } + this.updateRecentlyUsedRepos(destRepo); + }).catch((error) => { if (!error.response.data.lib_need_decrypt) { let errMessage = Utils.getErrorMsg(error); @@ -1222,6 +1246,9 @@ class LibContentView extends React.Component { message = message.replace('{name}', dirName); toaster.success(message); } + + this.updateRecentlyUsedRepos(destRepo); + }).catch((error) => { if (!error.response.data.lib_need_decrypt) { let errMessage = Utils.getErrorMsg(error);