From b723b6436e3b81859cad27e95d1194dcb7ae8c67 Mon Sep 17 00:00:00 2001 From: Aries Date: Fri, 18 Oct 2024 10:54:58 +0800 Subject: [PATCH] add context menu for libraries list (#6909) * add context menu for libraries list * support context menu on grid mode * optimize code --------- Co-authored-by: zhouwenxuan --- .../src/components/context-menu/actions.js | 24 +++ .../shared-repo-list-item.js | 11 +- .../shared-repo-list-view.js | 82 +++++++--- frontend/src/pages/groups/group-item.js | 1 + .../src/pages/my-libs/mylib-repo-list-item.js | 8 +- .../src/pages/my-libs/mylib-repo-list-view.js | 36 ++++- frontend/src/pages/shared-libs/shared-libs.js | 82 ++++++++-- frontend/src/pages/shared-with-all/index.js | 1 + frontend/src/utils/text-translation.js | 57 +++++++ frontend/src/utils/utils.js | 150 +++++++++++++++++- 10 files changed, 419 insertions(+), 33 deletions(-) diff --git a/frontend/src/components/context-menu/actions.js b/frontend/src/components/context-menu/actions.js index 4fbca00514..132f8d35a9 100644 --- a/frontend/src/components/context-menu/actions.js +++ b/frontend/src/components/context-menu/actions.js @@ -31,3 +31,27 @@ export function showMenu(opts = {}, target) { export function hideMenu(opts = {}, target) { dispatchGlobalEvent(MENU_HIDE, assign({}, opts, { type: MENU_HIDE }), target); } + +export function handleContextClick(event, id, menuList, currentObject = null) { + event.preventDefault(); + event.stopPropagation(); + + let x = event.clientX || (event.touches && event.touches[0].pageX); + let y = event.clientY || (event.touches && event.touches[0].pageY); + + hideMenu(); + + let showMenuConfig = { + id: id, + position: { x, y }, + target: event.target, + currentObject: currentObject, + menuList: menuList, + }; + + if (menuList.length === 0) { + return; + } + + showMenu(showMenuConfig); +} diff --git a/frontend/src/components/shared-repo-list-view/shared-repo-list-item.js b/frontend/src/components/shared-repo-list-view/shared-repo-list-item.js index ada6dc00fc..2200d7941c 100644 --- a/frontend/src/components/shared-repo-list-view/shared-repo-list-item.js +++ b/frontend/src/components/shared-repo-list-view/shared-repo-list-item.js @@ -32,7 +32,8 @@ const propTypes = { onItemUnshare: PropTypes.func.isRequired, onItemRename: PropTypes.func, onItemDelete: PropTypes.func, - onMonitorRepo: PropTypes.func + onMonitorRepo: PropTypes.func, + onContextMenu: PropTypes.func.isRequired, }; class SharedRepoListItem extends React.Component { @@ -157,7 +158,7 @@ class SharedRepoListItem extends React.Component { }; onMenuItemClick = (e) => { - let operation = e.target.dataset.toggle; + let operation = e.target.dataset.toggle || e.target.dataset.operation; switch (operation) { case 'Rename': this.onItemRenameToggle(); @@ -616,6 +617,10 @@ class SharedRepoListItem extends React.Component { } }; + handleContextMenu = (e) => { + this.props.onContextMenu(e, this.props.repo); + }; + renderPCUI = () => { const { isStarred } = this.state; let { iconUrl, iconTitle, libPath } = this.getRepoComputeParams(); @@ -627,6 +632,7 @@ class SharedRepoListItem extends React.Component { onMouseOver={this.onMouseOver} onMouseLeave={this.onMouseLeave} onFocus={this.onMouseEnter} + onContextMenu={this.handleContextMenu} >
{iconTitle} diff --git a/frontend/src/components/shared-repo-list-view/shared-repo-list-view.js b/frontend/src/components/shared-repo-list-view/shared-repo-list-view.js index 566e05dc76..e25946bd30 100644 --- a/frontend/src/components/shared-repo-list-view/shared-repo-list-view.js +++ b/frontend/src/components/shared-repo-list-view/shared-repo-list-view.js @@ -7,6 +7,8 @@ import toaster from '../toast'; import LibsMobileThead from '../libs-mobile-thead'; import Loading from '../loading'; import { LIST_MODE } from '../dir-view-mode/constants'; +import ContextMenu from '../context-menu/context-menu'; +import { hideMenu, handleContextClick } from '../context-menu/actions'; const propTypes = { currentViewMode: PropTypes.string, @@ -33,6 +35,7 @@ class SharedRepoListView extends React.Component { this.state = { isItemFreezed: false, }; + this.repoItems = []; } sortByName = (e) => { @@ -86,13 +89,41 @@ class SharedRepoListView extends React.Component { this.props.onItemRename(repo, newName); }; + setRepoItemRef = (index) => item => { + this.repoItems[index] = item; + }; + + getRepoIndex = (repo) => { + return this.props.repoList.findIndex(item => { + return item.repo_id === repo.repo_id; + }); + }; + + onMenuItemClick = (operation, currentObject, event) => { + const index = this.getRepoIndex(currentObject); + if (this.repoItems[index]) { + this.repoItems[index].onMenuItemClick(event); + } + hideMenu(); + }; + + onContextMenu = (event, repo) => { + event.preventDefault(); + const { libraryType, currentGroup } = this.props; + const isPublic = libraryType === 'public'; + const id = isPublic ? 'shared-repo-item-menu' : `shared-repo-item-menu-${currentGroup.id}`; + const menuList = Utils.getSharedRepoOperationList(repo, currentGroup, isPublic); + handleContextClick(event, id, menuList, repo); + }; + renderRepoListView = () => { const { currentViewMode = LIST_MODE } = this.props; return ( - {this.props.repoList.map(repo => { + {this.props.repoList.map((repo, index) => { return ( ); })} @@ -113,31 +145,43 @@ class SharedRepoListView extends React.Component { }; renderPCUI = () => { - const { theadHidden = false, currentViewMode = LIST_MODE } = this.props; + const { theadHidden = false, currentViewMode = LIST_MODE, currentGroup, libraryType } = this.props; const { sortByName, sortByTime, sortBySize, sortIcon } = this.getSortMetaData(); - return currentViewMode == LIST_MODE ? ( - - - - - - - - - - - - - - {this.renderRepoListView()} - -
{gettext('Library Type')}{gettext('Name')} {sortByName && sortIcon}{gettext('Actions')}{gettext('Size')} {sortBySize && sortIcon}{gettext('Last Update')} {sortByTime && sortIcon}{gettext('Owner')}
+ const content = currentViewMode == LIST_MODE ? ( + <> + + + + + + + + + + + + + + {this.renderRepoListView()} + +
{gettext('Library Type')}{gettext('Name')} {sortByName && sortIcon}{gettext('Actions')}{gettext('Size')} {sortBySize && sortIcon}{gettext('Last Update')} {sortByTime && sortIcon}{gettext('Owner')}
+ ) : (
{this.renderRepoListView()}
); + + return ( + <> + {content} + ; + + ); }; renderMobileUI = () => { diff --git a/frontend/src/pages/groups/group-item.js b/frontend/src/pages/groups/group-item.js index cae1d3e600..2c8fa1bad2 100644 --- a/frontend/src/pages/groups/group-item.js +++ b/frontend/src/pages/groups/group-item.js @@ -129,6 +129,7 @@ class GroupItem extends React.Component { {group.repos.length === 0 ? emptyTip : { + this.props.onContextMenu(event, this.props.repo); + }; + renderPCUI = () => { const { isStarred } = this.state; const { repo, currentViewMode = LIST_MODE } = this.props; @@ -311,7 +316,7 @@ class MylibRepoListItem extends React.Component { let iconTitle = Utils.getLibIconTitle(repo); let repoURL = `${siteRoot}library/${repo.repo_id}/${Utils.encodePath(repo.repo_name)}/`; return currentViewMode == LIST_MODE ? ( - +
{iconTitle} diff --git a/frontend/src/pages/my-libs/mylib-repo-list-view.js b/frontend/src/pages/my-libs/mylib-repo-list-view.js index 2dde33cade..ec6e80d243 100644 --- a/frontend/src/pages/my-libs/mylib-repo-list-view.js +++ b/frontend/src/pages/my-libs/mylib-repo-list-view.js @@ -5,6 +5,9 @@ import { gettext, storages } from '../../utils/constants'; import MylibRepoListItem from './mylib-repo-list-item'; import LibsMobileThead from '../../components/libs-mobile-thead'; import { LIST_MODE } from '../../components/dir-view-mode/constants'; +import ContextMenu from '../../components/context-menu/context-menu'; +import { Utils } from '../../utils/utils'; +import { hideMenu, handleContextClick } from '../../components/context-menu/actions'; const propTypes = { sortBy: PropTypes.string.isRequired, @@ -26,6 +29,7 @@ class MylibRepoListView extends React.Component { this.state = { isItemFreezed: false, }; + this.repoItems = []; } onFreezedItem = () => { @@ -57,12 +61,37 @@ class MylibRepoListView extends React.Component { this.props.sortRepoList(sortBy, sortOrder); }; + onContextMenu = (event, repo) => { + event.preventDefault(); + const id = 'mylib-repo-item-menu'; + const menuList = Utils.getRepoOperationList(repo); + handleContextClick(event, id, menuList, repo); + }; + + setRepoItemRef = (index) => item => { + this.repoItems[index] = item; + }; + + getRepoIndex = (repo) => { + return this.props.repoList.findIndex(item => { + return item.repo_id === repo.repo_id; + }); + }; + + onMenuItemClick = (operation, currentObject) => { + const index = this.getRepoIndex(currentObject); + this.repoItems[index].onMenuItemClick(operation); + + hideMenu(); + }; + renderRepoListView = () => { return ( - {this.props.repoList.map(item => { + {this.props.repoList.map((item, index) => { return ( ); })} @@ -130,6 +160,10 @@ class MylibRepoListView extends React.Component { {this.renderMobileUI()} + ); } diff --git a/frontend/src/pages/shared-libs/shared-libs.js b/frontend/src/pages/shared-libs/shared-libs.js index e992dcd245..05be4d7fa3 100644 --- a/frontend/src/pages/shared-libs/shared-libs.js +++ b/frontend/src/pages/shared-libs/shared-libs.js @@ -17,6 +17,8 @@ import ShareDialog from '../../components/dialog/share-dialog'; import SortOptionsDialog from '../../components/dialog/sort-options'; import RepoMonitoredIcon from '../../components/repo-monitored-icon'; import { GRID_MODE, LIST_MODE } from '../../components/dir-view-mode/constants'; +import ContextMenu from '../../components/context-menu/context-menu'; +import { hideMenu, handleContextClick } from '../../components/context-menu/actions'; class Content extends Component { @@ -25,6 +27,7 @@ class Content extends Component { this.state = { isItemFreezed: false }; + this.libItems = []; } freezeItem = (freezed) => { @@ -54,6 +57,30 @@ class Content extends Component { this.props.sortItems(sortBy, sortOrder); }; + onContextMenu = (event, repo) => { + event.preventDefault(); + const id = 'shared-libs-item-menu'; + const menuList = Utils.getSharedLibsOperationList(repo); + handleContextClick(event, id, menuList, repo); + }; + + setLibItemRef = (index) => item => { + this.libItems[index] = item; + }; + + getLibIndex = (lib) => { + return this.props.items.findIndex(item => { + return item.repo_id === lib.repo_id; + }); + }; + + onMenuItemClick = (operation, currentObject, event) => { + const index = this.getLibIndex(currentObject); + this.libItems[index].onMenuItemClick(operation, event); + + hideMenu(); + }; + render() { const { loading, errorMsg, items, sortBy, sortOrder, theadHidden, inAllLibs, currentViewMode } = this.props; @@ -95,6 +122,7 @@ class Content extends Component { <> {items.map((item, index) => { return ; })} ); const content = currentViewMode == LIST_MODE ? ( - - {isDesktop ? desktopThead : } - - {itemsContent} - -
+ <> + + {isDesktop ? desktopThead : } + + {itemsContent} + +
+ ) : (
{itemsContent}
); - return items.length ? content : emptyTip; + return items.length ? ( + <> + {content} + + + ) : emptyTip; } } } @@ -265,6 +304,29 @@ class Item extends Component { }); }; + handleContextMenu = (event) => { + this.props.onContextMenu(event, this.props.data); + }; + + onMenuItemClick = (operation, event) => { + switch (operation) { + case 'Share': + this.share(event); + break; + case 'Unshare': + this.leaveShare(event); + break; + case 'Watch File Changes': + this.watchFileChanges(); + break; + case 'Unwatch File Changes': + this.unwatchFileChanges(); + break; + default: + break; + } + }; + render() { if (this.state.unshared) { return null; @@ -288,7 +350,7 @@ class Item extends Component { return ( {currentViewMode == LIST_MODE ? ( - +
{data.icon_title} @@ -459,7 +522,8 @@ Item.propTypes = { data: PropTypes.object.isRequired, isItemFreezed: PropTypes.bool.isRequired, freezeItem: PropTypes.func.isRequired, - onMonitorRepo: PropTypes.func.isRequired + onMonitorRepo: PropTypes.func.isRequired, + onContextMenu: PropTypes.func.isRequired, }; class SharedLibraries extends Component { diff --git a/frontend/src/pages/shared-with-all/index.js b/frontend/src/pages/shared-with-all/index.js index 57dc4589b2..f91c2f2799 100644 --- a/frontend/src/pages/shared-with-all/index.js +++ b/frontend/src/pages/shared-with-all/index.js @@ -138,6 +138,7 @@ class PublicSharedView extends React.Component { {(!this.state.isLoading && this.state.repoList.length === 0) && emptyTip} {(!this.state.isLoading && this.state.repoList.length > 0) && !(op === DIVIDER && arr[i + 1] === DIVIDER)); + }, + + getAdvancedOperations: function () { + const operations = []; + const { API_TOKEN, LABEL_CURRENT_STATE, OLD_FILES_AUTO_DELETE } = TextTranslation; + + operations.push(API_TOKEN); + + if (enableRepoSnapshotLabel) { + operations.push(LABEL_CURRENT_STATE); + } + + if (enableRepoAutoDel) { + operations.push(OLD_FILES_AUTO_DELETE); + } + + return operations; + }, + + getSharedLibsOperationList: function (lib) { + const { SHARE, UNSHARE, WATCH_FILE_CHANGES, UNWATCH_FILE_CHANGES } = TextTranslation; + const operations = []; + + if (isPro && lib.is_admin) { + operations.push(SHARE); + } + operations.push(UNSHARE); + + const monitorOp = lib.monitored ? UNWATCH_FILE_CHANGES : WATCH_FILE_CHANGES; + operations.push(monitorOp); + + return operations; + }, + + getPublicSharedRepoOperationList: function (repo) { + const { UNSHARE } = TextTranslation; + const operations = []; + const isRepoOwner = repo.owner_email === username; + + if (isSystemStaff || isRepoOwner) { + operations.push(UNSHARE); + } + + return operations; + }, + + getSharedRepoOperationList: function (repo, currentGroup, isPublic) { + const operations = []; + const { SHARE, UNSHARE, DELETE, RENAME, FOLDER_PERMISSION, SHARE_ADMIN, UNWATCH_FILE_CHANGES, WATCH_FILE_CHANGES, HISTORY_SETTING, ADVANCED, CHANGE_PASSWORD, RESET_PASSWORD } = TextTranslation; + + const isStaff = currentGroup && currentGroup.admins && currentGroup.admins.indexOf(username) > -1; + const isRepoOwner = repo.owner_email === username; + const isAdmin = repo.is_admin; + const DIVIDER = 'Divider'; + + if (isPublic) { + if (isSystemStaff || isRepoOwner) { + operations.push(UNSHARE); + } + return operations; + } + + if (isPro) { + if (repo.owner_email.indexOf('@seafile_group') !== -1) { + // is group admin + if (isStaff) { + if (repo.owner_email === `${currentGroup.id}@seafile_group`) { + operations.push(SHARE, DELETE, RENAME); + if (folderPermEnabled) { + operations.push(FOLDER_PERMISSION); + } + operations.push(SHARE_ADMIN, DIVIDER); + if (repo.encrypted) { + operations.push(CHANGE_PASSWORD); + } + if (repo.encrypted && enableResetEncryptedRepoPassword && isEmailConfigured) { + operations.push(RESET_PASSWORD); + } + if (repo.permission === 'r' || repo.permission === 'rw') { + const monitorOp = repo.monitored ? UNWATCH_FILE_CHANGES : WATCH_FILE_CHANGES; + operations.push(monitorOp); + } + operations.push(DIVIDER, HISTORY_SETTING); + if (Utils.isDesktop()) { + const subOpList = Utils.getAdvancedOperations(); + operations.push({ ...ADVANCED, subOpList }); + } + return operations; + } else { + operations.push(UNSHARE); + } + } + } else { + if (isRepoOwner || isAdmin) { + operations.push(SHARE); + } + if (isStaff || isRepoOwner || isAdmin) { + operations.push(UNSHARE); + } + } + if (repo.permission === 'r' || repo.permission === 'rw') { + const monitorOp = repo.monitored ? UNWATCH_FILE_CHANGES : WATCH_FILE_CHANGES; + operations.push(monitorOp); + } + } else { + if (isRepoOwner) { + operations.push(SHARE); + } + if (isStaff || isRepoOwner) { + operations.push(UNSHARE); + } + } + + return operations; + }, + sharePerms: function (permission) { var title; switch (permission) {