From be84e505d06eec741c2ad1ca1b4a79dd65b41d7d Mon Sep 17 00:00:00 2001 From: Aries Date: Tue, 23 Jul 2024 23:08:53 +0800 Subject: [PATCH] 12.0 grid view support multiple selection (#6403) * Feature - multiple selection with ctrl/shift * update grid view context menu list in multiple selection mode * fix code format * optimize codes --- frontend/.eslintrc.json | 12 +- .../components/context-menu/context-menu.js | 1 - .../components/dialog/copy-dirent-dialog.js | 2 +- .../components/dialog/move-dirent-dialog.js | 2 +- frontend/src/components/dialog/op-menu.js | 2 +- .../src/components/dialog/share-to-user.js | 6 +- .../sysadmin-logs-export-excel-dialog.js | 6 +- .../sysadmin-dialog/sysadmin-share-to-user.js | 4 +- .../src/components/dialog/trash-dialog.js | 2 +- .../dir-view-mode/dir-column-view.js | 4 + .../components/dir-view-mode/dir-grid-view.js | 8 + .../dirent-grid-view/dirent-grid-item.js | 21 +-- .../dirent-grid-view/dirent-grid-view.js | 151 +++++++++++++----- .../dirent-list-view/dirent-list-view.js | 2 +- .../components/file-chooser/tree-list-item.js | 2 +- .../link-authenticated-users.js | 2 +- .../share-link-panel/link-creation.js | 4 +- .../wiki-card-view/wiki-card-item.js | 2 +- .../metadata-status-manage-dialog/index.js | 2 +- .../column-popover/data/date-data/index.js | 2 +- .../metadata-view/store/data-processor.js | 2 +- .../src/metadata/metadata-view/store/index.js | 2 +- .../metadata-view/utils/object-utils.js | 4 +- .../lib-content-view/lib-content-container.js | 4 +- .../lib-content-view/lib-content-view.js | 51 ++++-- frontend/src/pages/libraries/index.js | 2 +- .../src/pages/markdown-editor/editor-api.js | 2 +- .../pages/org-admin/org-logs-file-audit.js | 2 +- .../pages/org-admin/org-logs-file-update.js | 2 +- .../pages/org-admin/org-logs-perm-audit.js | 2 +- frontend/src/pages/search/advanced-search.js | 2 +- .../pages/sys-admin/dingtalk-departments.js | 2 +- .../sys-admin/work-weixin-departments.js | 2 +- frontend/src/pages/wiki/index.js | 2 +- frontend/src/utils/utils.js | 5 +- 35 files changed, 220 insertions(+), 103 deletions(-) diff --git a/frontend/.eslintrc.json b/frontend/.eslintrc.json index 31e4a1db66..88298fd34e 100644 --- a/frontend/.eslintrc.json +++ b/frontend/.eslintrc.json @@ -43,7 +43,17 @@ "array-bracket-spacing": ["warn", "never"], "object-curly-spacing": ["warn", "always"], "spaced-comment": "warn", - "keyword-spacing": ["warn", {"before": true}], + "space-before-blocks": ["warn", "always"], + "space-in-parens": ["warn", "never"], + "keyword-spacing": ["warn", { + "before": true, + "after": true, + "overrides": { + "if" : { + "after": true + } + } + }], "space-infix-ops": "error", "key-spacing": ["error", { "beforeColon": false }], "arrow-spacing": ["error", { "before": true, "after": true }], diff --git a/frontend/src/components/context-menu/context-menu.js b/frontend/src/components/context-menu/context-menu.js index e95120350e..777b579c91 100644 --- a/frontend/src/components/context-menu/context-menu.js +++ b/frontend/src/components/context-menu/context-menu.js @@ -82,7 +82,6 @@ class ContextMenu extends React.Component { handleShow = (e) => { if (e.detail.id !== this.props.id) return; - const { x, y } = e.detail.position; if (this.props.getMenuContainerSize) { const containerSize = this.props.getMenuContainerSize(); diff --git a/frontend/src/components/dialog/copy-dirent-dialog.js b/frontend/src/components/dialog/copy-dirent-dialog.js index 7b1c390eb1..338b5d404b 100644 --- a/frontend/src/components/dialog/copy-dirent-dialog.js +++ b/frontend/src/components/dialog/copy-dirent-dialog.js @@ -105,7 +105,7 @@ class CopyDirent extends React.Component { } // copy the dirent to it's child. eg: A/B -> A/B/C - if ( selectedPath && selectedPath.length > direntPath.length && selectedPath.indexOf(direntPath) > -1) { + if (selectedPath && selectedPath.length > direntPath.length && selectedPath.indexOf(direntPath) > -1) { message = gettext('Can not copy directory %(src)s to its subdirectory %(des)s'); message = message.replace('%(src)s', direntPath); message = message.replace('%(des)s', selectedPath); diff --git a/frontend/src/components/dialog/move-dirent-dialog.js b/frontend/src/components/dialog/move-dirent-dialog.js index 02fe8e68a3..d8d41258d7 100644 --- a/frontend/src/components/dialog/move-dirent-dialog.js +++ b/frontend/src/components/dialog/move-dirent-dialog.js @@ -119,7 +119,7 @@ class MoveDirent extends React.Component { } // copy the dirent to it's child. eg: A/B -> A/B/C - if ( selectedPath && selectedPath.length > direntPath.length && selectedPath.indexOf(direntPath) > -1) { + if (selectedPath && selectedPath.length > direntPath.length && selectedPath.indexOf(direntPath) > -1) { message = gettext('Can not move directory %(src)s to its subdirectory %(des)s'); message = message.replace('%(src)s', direntPath); message = message.replace('%(des)s', selectedPath); diff --git a/frontend/src/components/dialog/op-menu.js b/frontend/src/components/dialog/op-menu.js index 5bd60af5b7..ab4f9ca850 100644 --- a/frontend/src/components/dialog/op-menu.js +++ b/frontend/src/components/dialog/op-menu.js @@ -56,7 +56,7 @@ class OpMenu extends React.Component { aria-expanded={this.state.isItemMenuShow} /> - {operations.map((item, index ) => { + {operations.map((item, index) => { return ({translateOperations(item)}); })} diff --git a/frontend/src/components/dialog/share-to-user.js b/frontend/src/components/dialog/share-to-user.js index a616c17f65..9a4e11d39c 100644 --- a/frontend/src/components/dialog/share-to-user.js +++ b/frontend/src/components/dialog/share-to-user.js @@ -209,7 +209,7 @@ class ShareToUser extends React.Component { let users = []; let path = this.props.itemPath; let repoID = this.props.repoID; - if (this.state.selectedOption && this.state.selectedOption.length > 0 ) { + if (this.state.selectedOption && this.state.selectedOption.length > 0) { for (let i = 0; i < this.state.selectedOption.length; i ++) { users[i] = this.state.selectedOption[i].email; } @@ -284,7 +284,7 @@ class ShareToUser extends React.Component { if (this.props.isGroupOwnedRepo) { seafileAPI.deleteGroupOwnedRepoSharedUserItem(repoID, username, path).then(res => { this.setState({ - sharedItems: this.state.sharedItems.filter( item => { return item.user_info.name !== username; }) + sharedItems: this.state.sharedItems.filter(item => { return item.user_info.name !== username; }) }); }).catch(error => { let errMessage = Utils.getErrorMsg(error); @@ -293,7 +293,7 @@ class ShareToUser extends React.Component { } else { seafileAPI.deleteShareToUserItem(repoID, path, 'user', username).then(res => { this.setState({ - sharedItems: this.state.sharedItems.filter( item => { return item.user_info.name !== username; }) + sharedItems: this.state.sharedItems.filter(item => { return item.user_info.name !== username; }) }); }).catch(error => { let errMessage = Utils.getErrorMsg(error); diff --git a/frontend/src/components/dialog/sysadmin-dialog/sysadmin-logs-export-excel-dialog.js b/frontend/src/components/dialog/sysadmin-dialog/sysadmin-logs-export-excel-dialog.js index 56738c800d..7c7a6e2896 100644 --- a/frontend/src/components/dialog/sysadmin-dialog/sysadmin-logs-export-excel-dialog.js +++ b/frontend/src/components/dialog/sysadmin-dialog/sysadmin-logs-export-excel-dialog.js @@ -51,18 +51,18 @@ class LogsExportExcelDialog extends React.Component { this.props.toggle(); return systemAdminAPI.queryAsyncOperationExportExcel(task_id); }).then(res => { - if (res.data.is_finished === true){ + if (res.data.is_finished === true) { location.href = siteRoot + 'sys/log/export-excel/?task_id=' + task_id + '&log_type=' + logType; } else { this.timer = setInterval(() => { systemAdminAPI.queryAsyncOperationExportExcel(task_id).then(res => { - if (res.data.is_finished === true){ + if (res.data.is_finished === true) { this.setState({ isFinished: true }); clearInterval(this.timer); location.href = siteRoot + 'sys/log/export-excel/?task_id=' + task_id + '&log_type=' + logType; } }).catch(err => { - if (this.state.isFinished === false){ + if (this.state.isFinished === false) { clearInterval(this.timer); toaster.danger(gettext('Failed to export. Please check whether the size of table attachments exceeds the limit.')); } diff --git a/frontend/src/components/dialog/sysadmin-dialog/sysadmin-share-to-user.js b/frontend/src/components/dialog/sysadmin-dialog/sysadmin-share-to-user.js index 968f2799b4..7683d87139 100644 --- a/frontend/src/components/dialog/sysadmin-dialog/sysadmin-share-to-user.js +++ b/frontend/src/components/dialog/sysadmin-dialog/sysadmin-share-to-user.js @@ -149,7 +149,7 @@ class SysAdminShareToUser extends React.Component { shareToUser = () => { let users = []; let repoID = this.props.repoID; - if (this.state.selectedOption && this.state.selectedOption.length > 0 ) { + if (this.state.selectedOption && this.state.selectedOption.length > 0) { for (let i = 0; i < this.state.selectedOption.length; i ++) { users[i] = this.state.selectedOption[i].email; } @@ -186,7 +186,7 @@ class SysAdminShareToUser extends React.Component { let repoID = this.props.repoID; seafileAPI.sysAdminDeleteRepoSharedItem(repoID, 'user', useremail).then(res => { this.setState({ - sharedItems: this.state.sharedItems.filter( item => { return item.user_email !== useremail; }) + sharedItems: this.state.sharedItems.filter(item => { return item.user_email !== useremail; }) }); }).catch(error => { let errMessage = Utils.getErrorMsg(error); diff --git a/frontend/src/components/dialog/trash-dialog.js b/frontend/src/components/dialog/trash-dialog.js index 3eed6e640a..c6cab012a9 100644 --- a/frontend/src/components/dialog/trash-dialog.js +++ b/frontend/src/components/dialog/trash-dialog.js @@ -49,7 +49,7 @@ class TrashDialog extends React.Component { getItems2 = (page) => { repotrashAPI.getRepoFolderTrash2(this.props.repoID, page, this.state.perPage).then((res) => { const { items, total_count } = res.data; - if (!page){ + if (!page) { page = 1; } this.setState({ diff --git a/frontend/src/components/dir-view-mode/dir-column-view.js b/frontend/src/components/dir-view-mode/dir-column-view.js index 749293dc15..0f482ec6cf 100644 --- a/frontend/src/components/dir-view-mode/dir-column-view.js +++ b/frontend/src/components/dir-view-mode/dir-column-view.js @@ -253,12 +253,16 @@ class DirColumnView extends React.Component { isDirentListLoading={this.props.isDirentListLoading} direntList={this.props.direntList} fullDirentList={this.props.fullDirentList} + selectedDirentList={this.props.selectedDirentList} onAddFile={this.props.onAddFile} onItemClick={this.props.onItemClick} onItemDelete={this.props.onItemDelete} onItemMove={this.props.onItemMove} onItemCopy={this.props.onItemCopy} onItemConvert={this.props.onItemConvert} + onItemsMove={this.props.onItemsMove} + onItemsCopy={this.props.onItemsCopy} + onItemsDelete={this.props.onItemsDelete} updateDirent={this.props.updateDirent} onAddFolder={this.props.onAddFolder} showDirentDetail={this.props.showDirentDetail} diff --git a/frontend/src/components/dir-view-mode/dir-grid-view.js b/frontend/src/components/dir-view-mode/dir-grid-view.js index a06ba28beb..20b2f7807d 100644 --- a/frontend/src/components/dir-view-mode/dir-grid-view.js +++ b/frontend/src/components/dir-view-mode/dir-grid-view.js @@ -11,6 +11,7 @@ const propTypes = { usedRepoTags: PropTypes.array.isRequired, updateUsedRepoTags: PropTypes.func.isRequired, direntList: PropTypes.array.isRequired, + selectedDirentList: PropTypes.array.isRequired, onItemClick: PropTypes.func.isRequired, onGridItemClick: PropTypes.func, onAddFile: PropTypes.func.isRequired, @@ -18,6 +19,9 @@ const propTypes = { onItemMove: PropTypes.func.isRequired, onItemCopy: PropTypes.func.isRequired, onItemConvert: PropTypes.func.isRequired, + onItemsMove: PropTypes.func.isRequired, + onItemsCopy: PropTypes.func.isRequired, + onItemsDelete: PropTypes.func.isRequired, onRenameNode: PropTypes.func.isRequired, isGroupOwnedRepo: PropTypes.bool.isRequired, userPerm: PropTypes.string, @@ -68,12 +72,16 @@ class DirGridView extends React.Component { enableDirPrivateShare={this.props.enableDirPrivateShare} direntList={this.props.direntList} fullDirentList={this.props.fullDirentList} + selectedDirentList={this.props.selectedDirentList} onAddFile={this.props.onAddFile} onItemClick={this.props.onItemClick} onItemDelete={this.props.onItemDelete} onItemMove={this.props.onItemMove} onItemCopy={this.props.onItemCopy} onItemConvert={this.props.onItemConvert} + onItemsMove={this.props.onItemsMove} + onItemsCopy={this.props.onItemsCopy} + onItemsDelete={this.props.onItemsDelete} isDirentListLoading={this.props.isDirentListLoading} updateDirent={this.props.updateDirent} onRenameNode={this.props.onRenameNode} diff --git a/frontend/src/components/dirent-grid-view/dirent-grid-item.js b/frontend/src/components/dirent-grid-view/dirent-grid-item.js index 0b926f17fa..4387722e4e 100644 --- a/frontend/src/components/dirent-grid-view/dirent-grid-item.js +++ b/frontend/src/components/dirent-grid-view/dirent-grid-item.js @@ -24,7 +24,6 @@ class DirentGridItem extends React.Component { constructor(props) { super(props); this.state = { - isGridSelected: false, isGridDropTipShow: false, }; @@ -49,13 +48,6 @@ class DirentGridItem extends React.Component { } } - componentDidUpdate(prevProps) { - if (prevProps.activeDirent !== this.props.activeDirent) { - const isSelected = this.props.activeDirent && this.props.activeDirent.name === this.props.dirent.name; - this.setState({ isGridSelected: isSelected }); - } - } - onItemMove = (destRepo, dirent, selectedPath, currentPath) => { this.props.onItemMove(destRepo, dirent, selectedPath, currentPath); }; @@ -63,32 +55,31 @@ class DirentGridItem extends React.Component { onItemClick = (e) => { e.preventDefault(); e.stopPropagation(); - this.setState({ isGridSelected: false }); const { dirent, activeDirent } = this.props; if (this.clickTimeout) { clearTimeout(this.clickTimeout); this.clickTimeout = null; - this.handleSingleClick(dirent, activeDirent); + this.handleSingleClick(dirent, activeDirent, e); return; } this.clickTimeout = setTimeout(() => { this.clickTimeout = null; - this.handleSingleClick(dirent, activeDirent); + this.handleSingleClick(dirent, activeDirent, e); }, 100); // Clicks within 100 milliseconds is considered a single click. }; - handleSingleClick = (dirent, activeDirent) => { + handleSingleClick = (dirent, activeDirent, event) => { if (!this.canPreview) { return; } - if (dirent === activeDirent) { + if (dirent === activeDirent && !event.metaKey && !event.ctrlKey) { this.handleDoubleClick(dirent); } else { - this.props.onGridItemClick(this.props.dirent); + this.props.onGridItemClick(dirent, event); } }; @@ -274,7 +265,7 @@ class DirentGridItem extends React.Component { return (
  • { + onGridItemClick = (dirent, event) => { hideMenu(); - this.setState({ activeDirent: dirent }); - this.props.onGridItemClick(dirent); + if (this.state.activeDirent !== dirent) { + this.setState({ activeDirent: dirent }); + } + this.props.onGridItemClick(dirent, event); }; onMoveToggle = () => { @@ -118,7 +129,12 @@ class DirentGridView extends React.Component { onItemDelete = (currentObject, e) => { e.nativeEvent.stopImmediatePropagation(); // for document event - this.props.onItemDelete(currentObject); + if (this.props.selectedDirentList.length === 1) { + this.props.onItemDelete(currentObject); + return; + } else { + this.props.onItemsDelete(); + } }; onItemConvert = (currentObject, e, dstType) => { @@ -138,7 +154,7 @@ class DirentGridView extends React.Component { hideMenu(); switch (operation) { case 'Download': - this.onItemDownload(currentObject, event); + this.onItemsDownload(); break; case 'Share': this.onItemShare(event); @@ -223,6 +239,27 @@ class DirentGridView extends React.Component { } }; + onDirentsMenuItemClick = (operation) => { + switch (operation) { + case 'Move': + this.onMoveToggle(); + break; + case 'Copy': + this.onCopyToggle(); + break; + case 'Download': + this.onItemsDownload(); + break; + case 'Delete': + this.props.onItemsDelete(); + break; + default: + break; + } + + hideMenu(); + }; + onEditFileTagToggle = () => { this.setState({ isEditFileTagShow: !this.state.isEditFileTagShow @@ -246,19 +283,23 @@ class DirentGridView extends React.Component { }); }; - onItemDownload = (currentObject, e) => { - e.nativeEvent.stopImmediatePropagation(); - let dirent = currentObject; - let repoID = this.props.repoID; - let direntPath = this.getDirentPath(dirent); - if (dirent.type === 'dir') { - this.setState({ - isZipDialogOpen: true - }); - } else { + onItemsDownload = () => { + let { path, repoID, selectedDirentList } = this.props; + if (selectedDirentList.length === 1 && !selectedDirentList[0].isDir()) { + let direntPath = Utils.joinPath(path, selectedDirentList[0].name); let url = URLDecorator.getUrl({ type: 'download_file_url', repoID: repoID, filePath: direntPath }); location.href = url; + return; } + + let selectedDirentNames = selectedDirentList.map(dirent => { + return dirent.name; + }); + + this.setState({ + isZipDialogOpen: true, + downloadItems: selectedDirentNames + }); }; onCreateFolderToggle = () => { @@ -428,12 +469,16 @@ class DirentGridView extends React.Component { onGridContainerContextMenu = (event) => { event.preventDefault(); - // Display menu items based on the permissions of the current path - let permission = this.props.userPerm; - if (permission !== 'admin' && permission !== 'rw') { - return; - } - let id = 'dirent-grid-container-menu'; + const hasCustomPermission = (action) => { + const { isCustomPermission, customPermission } = Utils.getUserPermission(this.props.userPerm); + if (isCustomPermission) { + return customPermission.permission[action]; + } + return true; + }; + + if (!['admin', 'rw'].includes(this.props.userPerm)) return; + const { NEW_FOLDER, NEW_FILE, NEW_MARKDOWN_FILE, @@ -443,25 +488,51 @@ class DirentGridView extends React.Component { NEW_SEADOC_FILE } = TextTranslation; - const menuList = [ + let direntsContainerMenuList = [ NEW_FOLDER, NEW_FILE, 'Divider', NEW_MARKDOWN_FILE, NEW_EXCEL_FILE, NEW_POWERPOINT_FILE, NEW_WORD_FILE ]; - const { currentRepoInfo } = this.props; + const { currentRepoInfo, selectedDirentList } = this.props; + if (enableSeadoc && !currentRepoInfo.encrypted) { - menuList.push(NEW_SEADOC_FILE); + direntsContainerMenuList.push(NEW_SEADOC_FILE); + } + + if (selectedDirentList.length === 0) { + if (!hasCustomPermission('create')) return; + this.handleContextClick(event, DIRENT_GRID_CONTAINER_MENU_ID, direntsContainerMenuList); + } else if (selectedDirentList.length === 1) { + if (!this.state.activeDirent) { + let menuList = Utils.getDirentOperationList(this.isRepoOwner, currentRepoInfo, selectedDirentList[0], true); + this.handleContextClick(event, GRID_ITEM_CONTEXTMENU_ID, menuList, selectedDirentList[0]); + } else { + this.onDirentClick(null); + event.persist(); + if (!hasCustomPermission('modify')) return; + setTimeout(() => { + this.handleContextClick(event, DIRENT_GRID_CONTAINER_MENU_ID, direntsContainerMenuList); + }, 0); + } + } else { + let menuList = []; + if (!hasCustomPermission('modify') && !hasCustomPermission('copy') && !hasCustomPermission('download') && !hasCustomPermission('delete')) return; + ['move', 'copy', 'download', 'delete'].forEach(action => { + if (hasCustomPermission(action)) { + menuList.push(TextTranslation[action.toUpperCase()]); + } + }); + this.handleContextClick(event, DIRENTS_MENU_ID, menuList); } - this.handleContextClick(event, id, menuList); }; onGridItemContextMenu = (event, dirent) => { + if (this.props.selectedDirentList.length > 1) return; // Display menu items according to the current dirent permission - let id = 'grid-item-contextmenu'; - let menuList = this.getDirentItemMenuList(dirent, true); - this.handleContextClick(event, id, menuList, dirent); + const menuList = this.getDirentItemMenuList(dirent, true); + this.handleContextClick(event, GRID_ITEM_CONTEXTMENU_ID, menuList, dirent); this.props.onGridItemClick && this.props.onGridItemClick(dirent); }; @@ -505,7 +576,7 @@ class DirentGridView extends React.Component { }; render() { - let { direntList, path } = this.props; + let { direntList, selectedDirentList, path } = this.props; let dirent = this.state.activeDirent ? this.state.activeDirent : ''; let direntPath = Utils.joinPath(path, dirent.name); @@ -538,15 +609,20 @@ class DirentGridView extends React.Component { } + {this.state.isCreateFolderDialogShow && ( @@ -584,7 +661,7 @@ class DirentGridView extends React.Component { @@ -595,9 +672,9 @@ class DirentGridView extends React.Component { repoID={this.props.repoID} repoEncrypted={this.props.currentRepoInfo.encrypted} isMutipleOperation={this.state.isMutipleOperation} - onItemCopy={this.props.onItemCopy} + selectedDirentList={selectedDirentList} + onItemsCopy={this.props.onItemsCopy} onCancelCopy={this.onCopyToggle} - dirent={this.state.activeDirent} /> } {this.state.isEditFileTagShow && @@ -628,7 +705,7 @@ class DirentGridView extends React.Component { {this.state.isRenameDialogShow && ( 1 ? selectedDirentList[selectedDirentList.length - 1] : this.state.activeDirent} onRename={this.onItemRename} checkDuplicatedName={this.checkDuplicatedName} toggleCancel={this.onItemRenameToggle} diff --git a/frontend/src/components/dirent-list-view/dirent-list-view.js b/frontend/src/components/dirent-list-view/dirent-list-view.js index c9a0de81f9..71317a753e 100644 --- a/frontend/src/components/dirent-list-view/dirent-list-view.js +++ b/frontend/src/components/dirent-list-view/dirent-list-view.js @@ -131,7 +131,7 @@ class DirentListView extends React.Component { onDirentClick = (dirent) => { hideMenu(); - if (this.props.selectedDirentList.length > 0 && !this.state.activeDirent ) { + if (this.props.selectedDirentList.length > 0 && !this.state.activeDirent) { return; } this.setState({ activeDirent: dirent }); diff --git a/frontend/src/components/file-chooser/tree-list-item.js b/frontend/src/components/file-chooser/tree-list-item.js index 60aa27e9d2..dc0db29e0f 100644 --- a/frontend/src/components/file-chooser/tree-list-item.js +++ b/frontend/src/components/file-chooser/tree-list-item.js @@ -89,7 +89,7 @@ class TreeViewItem extends React.Component { const fileName = node.object.name; if (this.props.fileSuffixes && fileName && node.object.type === 'file') { - if ( fileName.indexOf('.') !== -1) { + if (fileName.indexOf('.') !== -1) { const suffix = fileName.slice(fileName.lastIndexOf('.') + 1).toLowerCase(); if (!this.props.fileSuffixes.includes(suffix)) return null; } else { diff --git a/frontend/src/components/share-link-panel/link-authenticated-users.js b/frontend/src/components/share-link-panel/link-authenticated-users.js index 64cf238c65..02a8bfa7ff 100644 --- a/frontend/src/components/share-link-panel/link-authenticated-users.js +++ b/frontend/src/components/share-link-panel/link-authenticated-users.js @@ -110,7 +110,7 @@ class LinkAuthenticatedUsers extends React.Component { addLinkAuthUsers = () => { const { linkToken, path } = this.props; const { selectedOption, authUsers } = this.state; - if (!selectedOption || !selectedOption.length ) { + if (!selectedOption || !selectedOption.length) { return false; } const users = selectedOption.map((item, index) => item.email); diff --git a/frontend/src/components/share-link-panel/link-creation.js b/frontend/src/components/share-link-panel/link-creation.js index c06a78f39d..5e095ba1a6 100644 --- a/frontend/src/components/share-link-panel/link-creation.js +++ b/frontend/src/components/share-link-panel/link-creation.js @@ -128,7 +128,7 @@ class LinkCreation extends React.Component { request = seafileAPI.batchCreateMultiShareLink(repoID, itemPath, linkAmount, autoGeneratePassword, expirationTime, permissions); } else { const { currentScope, selectedOption, inputEmails } = this.state; - if ( currentScope === 'specific_users' && selectedOption ) { + if (currentScope === 'specific_users' && selectedOption) { users = selectedOption.map((item, index) => item.email); } if (currentScope === 'specific_emails' && inputEmails) { @@ -232,7 +232,7 @@ class LinkCreation extends React.Component { } } - if (minDays === 0 && maxDays !== 0 ) { + if (minDays === 0 && maxDays !== 0) { if (expireDays > maxDays) { this.setState({ errorInfo: 'Please enter valid days' }); return false; diff --git a/frontend/src/components/wiki-card-view/wiki-card-item.js b/frontend/src/components/wiki-card-view/wiki-card-item.js index 25963c7622..e146d0401e 100644 --- a/frontend/src/components/wiki-card-view/wiki-card-item.js +++ b/frontend/src/components/wiki-card-view/wiki-card-item.js @@ -102,7 +102,7 @@ class WikiCardItem extends Component { <>
    diff --git a/frontend/src/metadata/metadata-status-manage-dialog/index.js b/frontend/src/metadata/metadata-status-manage-dialog/index.js index 19381d5a49..ad00138d29 100644 --- a/frontend/src/metadata/metadata-status-manage-dialog/index.js +++ b/frontend/src/metadata/metadata-status-manage-dialog/index.js @@ -47,7 +47,7 @@ const MetadataStatusManagementDialog = ({ value: oldValue, repoID, toggle, submi textPosition="right" className="change-metadata-status-management w-100" onChange={onValueChange} - placeholder={gettext('Enable extended properties' )} + placeholder={gettext('Enable extended properties')} />
    {gettext('After enable extended properties for files, you can add different properties to files, like collaborators, file expiring time, file description. You can also create different views for files based extended properties.')} diff --git a/frontend/src/metadata/metadata-view/components/popover/column-popover/data/date-data/index.js b/frontend/src/metadata/metadata-view/components/popover/column-popover/data/date-data/index.js index adad657162..73fab2429b 100644 --- a/frontend/src/metadata/metadata-view/components/popover/column-popover/data/date-data/index.js +++ b/frontend/src/metadata/metadata-view/components/popover/column-popover/data/date-data/index.js @@ -66,7 +66,7 @@ const DateData = ({ value, onChange }) => { textPosition="right" className="sf-metadata-date-column-data-minute w-100" onChange={onMinuteChange} - placeholder={gettext('Accurate to minute' )} /> + placeholder={gettext('Accurate to minute')} />
    ); diff --git a/frontend/src/metadata/metadata-view/store/data-processor.js b/frontend/src/metadata/metadata-view/store/data-processor.js index 860a15f5c8..8eb44d847e 100644 --- a/frontend/src/metadata/metadata-view/store/data-processor.js +++ b/frontend/src/metadata/metadata-view/store/data-processor.js @@ -100,7 +100,7 @@ class DataProcessor { table.view.groups = groups; } - static updateDataWithModifyRecords(table, relatedColumnKeyMap, ) { + static updateDataWithModifyRecords(table, relatedColumnKeyMap) { // todo } diff --git a/frontend/src/metadata/metadata-view/store/index.js b/frontend/src/metadata/metadata-view/store/index.js index c550ee318f..517be8c45a 100644 --- a/frontend/src/metadata/metadata-view/store/index.js +++ b/frontend/src/metadata/metadata-view/store/index.js @@ -172,7 +172,7 @@ class Store { this.sendNextOperation(undoRedoHandler); }; - handleUndoRedos(undoRedoHandler, operation){ + handleUndoRedos(undoRedoHandler, operation) { const { handleUndo, asyncUndoRedo } = undoRedoHandler; if (handleUndo) { if (this.redos.length > 0) { diff --git a/frontend/src/metadata/metadata-view/utils/object-utils.js b/frontend/src/metadata/metadata-view/utils/object-utils.js index fac3e1fd98..e25cd397f3 100644 --- a/frontend/src/metadata/metadata-view/utils/object-utils.js +++ b/frontend/src/metadata/metadata-view/utils/object-utils.js @@ -1,6 +1,6 @@ class ObjectUtils { - static getDataType(data){ + static getDataType(data) { let type = typeof data; if (type !== 'object') { return type; @@ -8,7 +8,7 @@ class ObjectUtils { return Object.prototype.toString.call(data).replace(/^\[object (\S+)\]$/, '$1'); } - static iterable(data){ + static iterable(data) { return ['Object', 'Array'].includes(this.getDataType(data)); } diff --git a/frontend/src/pages/lib-content-view/lib-content-container.js b/frontend/src/pages/lib-content-view/lib-content-container.js index 66d23b2073..893998b3aa 100644 --- a/frontend/src/pages/lib-content-view/lib-content-container.js +++ b/frontend/src/pages/lib-content-view/lib-content-container.js @@ -128,9 +128,9 @@ class LibContentContainer extends React.Component { this.props.closeDirentDetail(); }; - onDirentClick = (dirent) => { + onDirentClick = (dirent, event) => { this.setState({ currentDirent: dirent }); - this.props.onDirentClick(dirent); + this.props.onDirentClick(dirent, event); }; onItemSelected = (dirent) => { 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 8ee9d73036..a735924e0c 100644 --- a/frontend/src/pages/lib-content-view/lib-content-view.js +++ b/frontend/src/pages/lib-content-view/lib-content-view.js @@ -54,6 +54,7 @@ class LibContentView extends React.Component { isGroupOwnedRepo: false, userPerm: '', selectedDirentList: [], + lastSelectedIndex: null, fileTags: [], repoTags: [], usedRepoTags: [], @@ -1328,22 +1329,50 @@ class LibContentView extends React.Component { }; - onDirentClick = (dirent) => { - let direntList = this.state.direntList.map(dirent => { - dirent.isSelected = false; - return dirent; - }); - if (dirent) { + onDirentClick = (clickedDirent, event) => { + const { direntList, selectedDirentList, lastSelectedIndex } = this.state; + if (clickedDirent) { + const clickedIndex = direntList.findIndex(dirent => dirent.name === clickedDirent.name); + + let newSelectedDirentList = [...selectedDirentList]; + const isCtrlOrMetaKeyPressed = event && (event.ctrlKey || event.metaKey); + const isShiftKeyPressed = event && event.shiftKey; + + if (isCtrlOrMetaKeyPressed) { + // Ctrl (Cmd on Mac) key is pressed: Toggle selection of the clicked item + const isSelected = newSelectedDirentList.some(dirent => dirent.name === clickedDirent.name); + if (isSelected) { + newSelectedDirentList = newSelectedDirentList.filter(dirent => dirent.name !== clickedDirent.name); + } else { + newSelectedDirentList.push(clickedDirent); + } + } else if (isShiftKeyPressed && lastSelectedIndex !== null) { + // Shift key is pressed: Select all items between the last selected and the clicked item + const rangeStart = Math.min(this.state.lastSelectedIndex, clickedIndex); + const rangeEnd = Math.max(this.state.lastSelectedIndex, clickedIndex); + newSelectedDirentList = direntList.slice(rangeStart, rangeEnd + 1); + } else { + newSelectedDirentList = [clickedDirent]; + } + this.setState({ - direntList: direntList, - isDirentSelected: true, - selectedDirentList: [dirent], + direntList: direntList.map(dirent => { + dirent.isSelected = newSelectedDirentList.some(selectedDirent => selectedDirent.name === dirent.name); + return dirent; + }), + isDirentSelected: newSelectedDirentList.length > 0, + selectedDirentList: newSelectedDirentList, + lastSelectedIndex: clickedIndex, }); } else { this.setState({ - direntList: direntList, + direntList: direntList.map(dirent => { + dirent.isSelected = false; + return dirent; + }), isDirentSelected: false, selectedDirentList: [], + lastSelectedIndex: null, }); } }; @@ -1459,7 +1488,7 @@ class LibContentView extends React.Component { }; onFileUploadSuccess = (direntObject) => { - const isExist = this.state.direntList.some(item => item.name === direntObject.name && item.type === direntObject.type ); + const isExist = this.state.direntList.some(item => item.name === direntObject.name && item.type === direntObject.type); if (isExist) { const updatedDirentList = this.state.direntList.map(dirent => { diff --git a/frontend/src/pages/libraries/index.js b/frontend/src/pages/libraries/index.js index 917358d868..369340dfd8 100644 --- a/frontend/src/pages/libraries/index.js +++ b/frontend/src/pages/libraries/index.js @@ -57,7 +57,7 @@ class Libraries extends Component { let group = new Group(item); group.repos = item.repos.map(item => new Repo(item)); return group; - }).sort((a, b) => a.name.toLowerCase() < b.name.toLowerCase() ? -1 : 1 ); + }).sort((a, b) => a.name.toLowerCase() < b.name.toLowerCase() ? -1 : 1); const { allRepoList, myRepoList, sharedRepoList, publicRepoList, groupList } = this.sortRepos(repoList, groups); this.setState({ isLoading: false, diff --git a/frontend/src/pages/markdown-editor/editor-api.js b/frontend/src/pages/markdown-editor/editor-api.js index ae5f51f442..038903ac8a 100644 --- a/frontend/src/pages/markdown-editor/editor-api.js +++ b/frontend/src/pages/markdown-editor/editor-api.js @@ -101,7 +101,7 @@ class EditorApi { getFiles() { const rootPath = '/'; - return seafileAPI.listDir(repoID, rootPath, { recursive: true } ).then((response) => { + return seafileAPI.listDir(repoID, rootPath, { recursive: true }).then((response) => { var files = response.data.dirent_list.map((item) => { return { name: item.name, diff --git a/frontend/src/pages/org-admin/org-logs-file-audit.js b/frontend/src/pages/org-admin/org-logs-file-audit.js index 3cfea2e007..b7604ef670 100644 --- a/frontend/src/pages/org-admin/org-logs-file-audit.js +++ b/frontend/src/pages/org-admin/org-logs-file-audit.js @@ -239,7 +239,7 @@ class FileAuditItem extends React.Component { render() { let { fileEvent } = this.props; - if (this.props.userSelected && fileEvent.user_email !== this.props.userSelected ) { + if (this.props.userSelected && fileEvent.user_email !== this.props.userSelected) { return null; } else if (this.props.repoSelected && fileEvent.repo_name !== this.props.repoSelected) { return null; diff --git a/frontend/src/pages/org-admin/org-logs-file-update.js b/frontend/src/pages/org-admin/org-logs-file-update.js index c9ba2a8fe4..1933f3f055 100644 --- a/frontend/src/pages/org-admin/org-logs-file-update.js +++ b/frontend/src/pages/org-admin/org-logs-file-update.js @@ -265,7 +265,7 @@ class FileUpdateItem extends React.Component { render() { let { fileEvent } = this.props; - if (this.props.userSelected && fileEvent.user_email !== this.props.userSelected ) { + if (this.props.userSelected && fileEvent.user_email !== this.props.userSelected) { return null; } else if (this.props.repoSelected && fileEvent.repo_name !== this.props.repoSelected) { return null; diff --git a/frontend/src/pages/org-admin/org-logs-perm-audit.js b/frontend/src/pages/org-admin/org-logs-perm-audit.js index 3cdc20d205..d4f96acea0 100644 --- a/frontend/src/pages/org-admin/org-logs-perm-audit.js +++ b/frontend/src/pages/org-admin/org-logs-perm-audit.js @@ -222,7 +222,7 @@ class PermAuditItem extends React.Component { render() { let { permEvent } = this.props; - if (this.props.userSelected && permEvent.from_user_email !== this.props.userSelected ) { + if (this.props.userSelected && permEvent.from_user_email !== this.props.userSelected) { return null; } else { return ( diff --git a/frontend/src/pages/search/advanced-search.js b/frontend/src/pages/search/advanced-search.js index 09fa70a314..7f8a4dd92a 100644 --- a/frontend/src/pages/search/advanced-search.js +++ b/frontend/src/pages/search/advanced-search.js @@ -71,7 +71,7 @@ class AdvancedSearch extends React.Component { })} } - {(time_from && time_to ) && + {(time_from && time_to) && {gettext('Last Update')}{': '}{time_from.format('YYYY-MM-DD')}{' '}{gettext('to')}{' '}{time_to.format('YYYY-MM-DD')} } {(size_from && size_to) && diff --git a/frontend/src/pages/sys-admin/dingtalk-departments.js b/frontend/src/pages/sys-admin/dingtalk-departments.js index e71f702cb8..f4a19b3150 100644 --- a/frontend/src/pages/sys-admin/dingtalk-departments.js +++ b/frontend/src/pages/sys-admin/dingtalk-departments.js @@ -276,7 +276,7 @@ class DingtalkDepartments extends Component { for (let i = 0, len = fails.length; i < len; i++) { let fail = fails[i]; let failName = fail.type === 'department' ? fail.department_name : fail.api_user_name; - toaster.danger(failName + ' ' + fail.msg, { duration: 3 } ); + toaster.danger(failName + ' ' + fail.msg, { duration: 3 }); } }; diff --git a/frontend/src/pages/sys-admin/work-weixin-departments.js b/frontend/src/pages/sys-admin/work-weixin-departments.js index e1509f2d0e..7e131f5f8e 100644 --- a/frontend/src/pages/sys-admin/work-weixin-departments.js +++ b/frontend/src/pages/sys-admin/work-weixin-departments.js @@ -276,7 +276,7 @@ class WorkWeixinDepartments extends Component { for (let i = 0, len = fails.length; i < len; i++) { let fail = fails[i]; let failName = fail.type === 'department' ? fail.department_name : fail.api_user_name; - toaster.danger(failName + ' ' + fail.msg, { duration: 3 } ); + toaster.danger(failName + ' ' + fail.msg, { duration: 3 }); } }; diff --git a/frontend/src/pages/wiki/index.js b/frontend/src/pages/wiki/index.js index 0afdb83c4f..5590fb58ad 100644 --- a/frontend/src/pages/wiki/index.js +++ b/frontend/src/pages/wiki/index.js @@ -402,7 +402,7 @@ class Wiki extends Component { } } - if (node.path === this.state.path ) { + if (node.path === this.state.path) { return; } diff --git a/frontend/src/utils/utils.js b/frontend/src/utils/utils.js index c4a899819b..2d4c0bec46 100644 --- a/frontend/src/utils/utils.js +++ b/frontend/src/utils/utils.js @@ -682,9 +682,8 @@ export const Utils = { }, getDirentOperationList: function (isRepoOwner, currentRepoInfo, dirent, isContextmenu) { - return dirent.type == 'dir' ? - Utils.getFolderOperationList(isRepoOwner, currentRepoInfo, dirent, isContextmenu) : - Utils.getFileOperationList(isRepoOwner, currentRepoInfo, dirent, isContextmenu); + const operationListGetter = dirent.type === 'dir' ? Utils.getFolderOperationList : Utils.getFileOperationList; + return operationListGetter(isRepoOwner, currentRepoInfo, dirent, isContextmenu); }, sharePerms: function (permission) {