Merge branch '15685-file-renaming-empty-name'
authorLucas Di Pentima <lucas@di-pentima.com.ar>
Thu, 12 Nov 2020 19:32:07 +0000 (16:32 -0300)
committerLucas Di Pentima <lucas@di-pentima.com.ar>
Thu, 12 Nov 2020 19:32:07 +0000 (16:32 -0300)
Closes #15685

Arvados-DCO-1.1-Signed-off-by: Lucas Di Pentima <lucas@di-pentima.com.ar>

17 files changed:
Makefile
src/common/array-utils.ts [new file with mode: 0644]
src/common/redirect-to.ts
src/services/auth-service/auth-service.ts
src/store/breadcrumbs/breadcrumbs-actions.ts
src/store/collection-panel/collection-panel-action.ts
src/store/open-in-new-tab/open-in-new-tab.actions.ts
src/store/store.ts
src/views-components/advanced-tab-dialog/advanced-tab-dialog.tsx
src/views-components/api-token/api-token.tsx
src/views-components/context-menu/action-sets/collection-action-set.ts
src/views-components/context-menu/action-sets/collection-admin-action-set.ts
src/views-components/context-menu/action-sets/collection-resource-action-set.ts [new file with mode: 0644]
src/views-components/context-menu/action-sets/project-action-set.ts
src/views-components/context-menu/action-sets/project-admin-action-set.ts
src/views-components/context-menu/actions/helpers.ts
src/views-components/context-menu/context-menu.tsx

index 64fe9e563f4a26b7534aabc7fd424a0032b29ba2..6da3ed1062b038ece0a901794944f93cb053a6ba 100644 (file)
--- a/Makefile
+++ b/Makefile
@@ -17,7 +17,7 @@ VERSION?=$(shell ./version-at-commit.sh HEAD)
 # changes in the package. (i.e. example config files externally added
 ITERATION?=1
 
-TARGETS?="centos7 debian8 debian9 debian10 ubuntu1404 ubuntu1604 ubuntu1804"
+TARGETS?="centos7 debian8 debian10 ubuntu1404 ubuntu1604 ubuntu1804 ubuntu2004"
 
 DESCRIPTION=Arvados Workbench2 - Arvados is a free and open source platform for big data science.
 MAINTAINER=Arvados Package Maintainers <packaging@arvados.org>
diff --git a/src/common/array-utils.ts b/src/common/array-utils.ts
new file mode 100644 (file)
index 0000000..a92461c
--- /dev/null
@@ -0,0 +1,18 @@
+// Copyright (C) The Arvados Authors. All rights reserved.
+//
+// SPDX-License-Identifier: AGPL-3.0
+
+export const sortByProperty = (propName: string) => (obj1: any, obj2: any) => {
+    const prop1 = obj1[propName];
+    const prop2 = obj2[propName];
+    
+    if (prop1 > prop2) {
+        return 1;
+    }
+
+    if (prop1 < prop2) {
+        return -1;
+    }
+
+    return 0;
+};
index f5ece21be4e35e2aa563e817549cab364b29cf50..77be742f877ce62477a6e3439eb6c020e41821b0 100644 (file)
@@ -7,13 +7,15 @@ import { Config } from './config';
 const REDIRECT_TO_KEY = 'redirectTo';
 
 export const storeRedirects = () => {
-    if (window.location.href.indexOf(REDIRECT_TO_KEY) > -1) {
-        const { location: { href }, localStorage } = window;
-        const redirectUrl = href.split(`${REDIRECT_TO_KEY}=`)[1];
+    let redirectUrl;
+    const { location: { href }, localStorage } = window;
 
-        if (localStorage) {
-            localStorage.setItem(REDIRECT_TO_KEY, redirectUrl);
-        }
+    if (href.indexOf(REDIRECT_TO_KEY) > -1) {
+        redirectUrl = href.split(`${REDIRECT_TO_KEY}=`)[1];
+    }
+
+    if (localStorage && redirectUrl) {
+        localStorage.setItem(REDIRECT_TO_KEY, redirectUrl);
     }
 };
 
@@ -24,6 +26,7 @@ export const handleRedirects = (token: string, config: Config) => {
     if (localStorage && localStorage.getItem(REDIRECT_TO_KEY)) {
         const redirectUrl = localStorage.getItem(REDIRECT_TO_KEY);
         localStorage.removeItem(REDIRECT_TO_KEY);
+
         if (redirectUrl) {
             const sep = redirectUrl.indexOf("?") > -1 ? "&" : "?";
             window.location.href = `${keepWebServiceUrl}${redirectUrl}${sep}api_token=${token}`;
index 7510171106eb2761a4b0a118661ee55dc8c812b2..8d27b5bbe88991a255c56fe92db6461965133c25 100644 (file)
@@ -10,6 +10,7 @@ import { Session, SessionStatus } from "~/models/session";
 import { Config } from "~/common/config";
 import { uniqBy } from "lodash";
 
+export const TARGET_URL = 'targetURL';
 export const API_TOKEN_KEY = 'apiToken';
 export const USER_EMAIL_KEY = 'userEmail';
 export const USER_FIRST_NAME_KEY = 'userFirstName';
@@ -57,6 +58,14 @@ export class AuthService {
         }
     }
 
+    public removeTargetURL() {
+        this.getStorage().removeItem(TARGET_URL);
+    }
+
+    public getTargetURL() {
+        return this.getStorage().getItem(TARGET_URL);
+    }
+
     public removeApiToken() {
         this.getStorage().removeItem(API_TOKEN_KEY);
     }
@@ -83,11 +92,14 @@ export class AuthService {
         this.getStorage().removeItem(USER_IS_ACTIVE);
         this.getStorage().removeItem(USER_USERNAME);
         this.getStorage().removeItem(USER_PREFS);
+        this.getStorage().removeItem(TARGET_URL);
     }
 
     public login(uuidPrefix: string, homeCluster: string, loginCluster: string, remoteHosts: { [key: string]: string }) {
         const currentUrl = `${window.location.protocol}//${window.location.host}/token`;
         const homeClusterHost = remoteHosts[homeCluster];
+        const rd = new URL(window.location.href);
+        this.getStorage().setItem(TARGET_URL, rd.pathname + rd.search);
         window.location.assign(`https://${homeClusterHost}/login?${(uuidPrefix !== homeCluster && homeCluster !== loginCluster) ? "remote=" + uuidPrefix + "&" : ""}return_to=${currentUrl}`);
     }
 
index 90af2c2fb42c55cda81fad596cef9d857d76f213..8803cfba7390a7d6141c9cad92a9c70d58a8fc23 100644 (file)
@@ -43,14 +43,14 @@ const getSidePanelTreeBreadcrumbs = (uuid: string) => (treePicker: TreePicker):
 
 export const setSidePanelBreadcrumbs = (uuid: string) =>
     async (dispatch: Dispatch, getState: () => RootState, services: ServiceRepository) => {
-        const { treePicker } = getState();
+        const { treePicker, collectionPanel: { item } } = getState();
         const breadcrumbs = getSidePanelTreeBreadcrumbs(uuid)(treePicker);
         const path = getState().router.location!.pathname;
         const currentUuid = path.split('/')[2];
         const uuidKind = extractUuidKind(currentUuid);
 
         if (uuidKind === ResourceKind.COLLECTION) {
-            const collectionItem = await services.collectionService.get(currentUuid);
+            const collectionItem = item ? item : await services.collectionService.get(currentUuid);
             dispatch(setBreadcrumbs(breadcrumbs, collectionItem));
         } else if (uuidKind === ResourceKind.PROCESS) {
             const processItem = await services.containerRequestService.get(currentUuid);
@@ -72,6 +72,7 @@ export const setCategoryBreadcrumbs = (uuid: string, category: SidePanelTreeCate
         const initialBreadcrumbs: ResourceBreadcrumb[] = [
             { label: category, uuid: category }
         ];
+        const { collectionPanel: { item } } = getState();
         const path = getState().router.location!.pathname;
         const currentUuid = path.split('/')[2];
         const uuidKind = extractUuidKind(currentUuid);
@@ -81,7 +82,7 @@ export const setCategoryBreadcrumbs = (uuid: string, category: SidePanelTreeCate
                 : breadcrumbs,
             initialBreadcrumbs);
         if (uuidKind === ResourceKind.COLLECTION) {
-            const collectionItem = await services.collectionService.get(currentUuid);
+            const collectionItem = item ? item : await services.collectionService.get(currentUuid);
             dispatch(setBreadcrumbs(breadcrumbs, collectionItem));
         } else if (uuidKind === ResourceKind.PROCESS) {
             const processItem = await services.containerRequestService.get(currentUuid);
index 13943665dfe54457168a67b72ed8b9a77acc6efb..15d5ef72934f4d2fe3a26e15306e4e72ad9da864 100644 (file)
@@ -29,8 +29,9 @@ export const COLLECTION_TAG_FORM_NAME = 'collectionTagForm';
 
 export const loadCollectionPanel = (uuid: string) =>
     async (dispatch: Dispatch, getState: () => RootState, services: ServiceRepository) => {
+        const { collectionPanel: { item } } = getState();
         dispatch(collectionPanelActions.LOAD_COLLECTION({ uuid }));
-        const collection = await services.collectionService.get(uuid);
+        const collection = item ? item : await services.collectionService.get(uuid);
         dispatch(loadDetailsPanel(collection.uuid));
         dispatch(collectionPanelActions.LOAD_COLLECTION_SUCCESS({ item: collection }));
         dispatch(resourcesActions.SET_RESOURCES([collection]));
index 17ba740279993029b5e5f197c289ef2af33e625f..c6462ea139f1ed7d53a463c37164d39ff3cc3c00 100644 (file)
@@ -2,24 +2,36 @@
 //
 // SPDX-License-Identifier: AGPL-3.0
 
-import { Dispatch } from 'redux';
+import * as copy from 'copy-to-clipboard';
 import { ResourceKind } from '~/models/resource';
-import { unionize, ofType } from '~/common/unionize';
+import { getClipboardUrl } from '~/views-components/context-menu/actions/helpers';
 
-export const openInNewTabActions = unionize({
-    COPY_STORE: ofType<{}>(),
-    OPEN_COLLECTION_IN_NEW_TAB: ofType<string>(),
-    OPEN_PROJECT_IN_NEW_TAB: ofType<string>()
-});
-
-export const openInNewTabAction = (resource: any) => (dispatch: Dispatch) => {
+const getUrl = (resource: any) => {
+    let url = null;
     const { uuid, kind } = resource;
 
-    dispatch(openInNewTabActions.COPY_STORE());
-
     if (kind === ResourceKind.COLLECTION) {
-        dispatch(openInNewTabActions.OPEN_COLLECTION_IN_NEW_TAB(uuid));
-    } else if (kind === ResourceKind.PROJECT) {
-        dispatch(openInNewTabActions.OPEN_PROJECT_IN_NEW_TAB(uuid));
+        url = `/collections/${uuid}`;
+    }
+    if (kind === ResourceKind.PROJECT) {
+        url = `/projects/${uuid}`;
+    }
+
+    return url;
+};
+
+export const openInNewTabAction = (resource: any) => () => {
+    const url = getUrl(resource);
+
+    if (url) {
+        window.open(`${window.location.origin}${url}`, '_blank');
+    }
+};
+
+export const copyToClipboardAction = (resource: any) => () => {
+    const url = getUrl(resource);
+
+    if (url) {
+        copy(getClipboardUrl(url, false));
     }
 };
\ No newline at end of file
index 7beb099c1cf9c8f32ba818d064c6d2938b422b57..517368aa43badea0d26c3bd6dbb54701257e0572 100644 (file)
@@ -163,6 +163,7 @@ export function configureStore(history: History, services: ServiceRepository, co
         collectionsContentAddress,
         subprocessMiddleware,
     ];
+
     const enhancer = composeEnhancers(applyMiddleware(redirectToMiddleware, ...middlewares));
     return createStore(rootReducer, enhancer);
 }
index fdd0bd70891d7078b1e87ff950e6e540f566954d..7e90bfd29533253cf55beb4a4dab27d1aa16996c 100644 (file)
@@ -87,7 +87,7 @@ export const AdvancedTabDialog = compose(
                     {value === 4 && dialogContent(curlHeader, curlExample, classes)}
                 </DialogContent>
                 <DialogActions>
-                    <Button variant='text' color='primary' onClick={closeDialog}>
+                    <Button data-cy="close-advanced-dialog" variant='text' color='primary' onClick={closeDialog}>
                         Close
                     </Button>
                 </DialogActions>
index e11afa7bf3395b587e23c312bf331b69151c8a97..f4b50e36d0fdbb7404ee014db80ec605ce51634b 100644 (file)
@@ -11,6 +11,7 @@ import { AuthService } from "~/services/auth-service/auth-service";
 import { navigateToRootProject, navigateToLinkAccount } from "~/store/navigation/navigation-action";
 import { Config } from "~/common/config";
 import { getAccountLinkData } from "~/store/link-account-panel/link-account-panel-actions";
+import { replace } from "react-router-redux";
 
 interface ApiTokenProps {
     authService: AuthService;
@@ -25,8 +26,14 @@ export const ApiToken = connect()(
             const apiToken = getUrlParameter(search, 'api_token');
             const loadMainApp = this.props.loadMainApp;
             this.props.dispatch<any>(saveApiToken(apiToken)).finally(() => {
+                const redirectURL = this.props.authService.getTargetURL();
+
                 if (loadMainApp) {
-                    if (this.props.dispatch(getAccountLinkData())) {
+                    if (redirectURL) {
+                        this.props.authService.removeTargetURL();
+                        this.props.dispatch(replace(redirectURL));
+                    }
+                    else if (this.props.dispatch(getAccountLinkData())) {
                         this.props.dispatch(navigateToLinkAccount);
                     }
                     else {
index 7fa6f2241f5144f6c21eeefef5eb2375d737fd09..4b6b9224df0f0cb5902ef11301e02739c5371be8 100644 (file)
@@ -5,7 +5,7 @@
 import { ContextMenuActionSet } from "../context-menu-action-set";
 import { ToggleFavoriteAction } from "../actions/favorite-action";
 import { toggleFavorite } from "~/store/favorites/favorites-actions";
-import { RenameIcon, ShareIcon, MoveToIcon, CopyIcon, DetailsIcon, AdvancedIcon } from "~/components/icon/icon";
+import { RenameIcon, ShareIcon, MoveToIcon, CopyIcon, DetailsIcon, AdvancedIcon, OpenIcon, Link } from "~/components/icon/icon";
 import { openCollectionUpdateDialog } from "~/store/collections/collection-update-actions";
 import { favoritePanelActions } from "~/store/favorite-panel/favorite-panel-action";
 import { openMoveCollectionDialog } from '~/store/collections/collection-move-actions';
@@ -15,16 +15,32 @@ import { toggleCollectionTrashed } from "~/store/trash/trash-actions";
 import { openSharingDialog } from '~/store/sharing-dialog/sharing-dialog-actions';
 import { openAdvancedTabDialog } from "~/store/advanced-tab/advanced-tab";
 import { toggleDetailsPanel } from '~/store/details-panel/details-panel-action';
+import { copyToClipboardAction, openInNewTabAction } from "~/store/open-in-new-tab/open-in-new-tab.actions";
 
 export const readOnlyCollectionActionSet: ContextMenuActionSet = [[
     {
         component: ToggleFavoriteAction,
+        name: 'ToggleFavoriteAction',
         execute: (dispatch, resource) => {
             dispatch<any>(toggleFavorite(resource)).then(() => {
                 dispatch<any>(favoritePanelActions.REQUEST_ITEMS());
             });
         }
     },
+    {
+        icon: OpenIcon,
+        name: "Open in new tab",
+        execute: (dispatch, resource) => {
+            dispatch<any>(openInNewTabAction(resource));
+        }
+    },
+    {
+        icon: Link,
+        name: "Copy to clipboard",
+        execute: (dispatch, resource) => {
+            dispatch<any>(copyToClipboardAction(resource));
+        }
+    },
     {
         icon: CopyIcon,
         name: "Make a copy",
@@ -73,6 +89,7 @@ export const collectionActionSet: ContextMenuActionSet = [
         },
         {
             component: ToggleTrashAction,
+            name: 'ToggleTrashAction',
             execute: (dispatch, resource) => {
                 dispatch<any>(toggleCollectionTrashed(resource.uuid, resource.isTrashed!!));
             }
index 10a839d842592dc41a1bf3e0745550c83b105663..7b39d749397c8672da488877e40682bf2a12f784 100644 (file)
@@ -5,7 +5,7 @@
 import { ContextMenuActionSet } from "../context-menu-action-set";
 import { ToggleFavoriteAction } from "../actions/favorite-action";
 import { toggleFavorite } from "~/store/favorites/favorites-actions";
-import { RenameIcon, ShareIcon, MoveToIcon, CopyIcon, DetailsIcon, AdvancedIcon } from "~/components/icon/icon";
+import { RenameIcon, ShareIcon, MoveToIcon, CopyIcon, DetailsIcon, AdvancedIcon, OpenIcon, Link } from "~/components/icon/icon";
 import { openCollectionUpdateDialog } from "~/store/collections/collection-update-actions";
 import { favoritePanelActions } from "~/store/favorite-panel/favorite-panel-action";
 import { openMoveCollectionDialog } from '~/store/collections/collection-move-actions';
@@ -18,6 +18,7 @@ import { toggleDetailsPanel } from '~/store/details-panel/details-panel-action';
 import { TogglePublicFavoriteAction } from "~/views-components/context-menu/actions/public-favorite-action";
 import { publicFavoritePanelActions } from "~/store/public-favorites-panel/public-favorites-action";
 import { togglePublicFavorite } from "~/store/public-favorites/public-favorites-actions";
+import { copyToClipboardAction, openInNewTabAction } from "~/store/open-in-new-tab/open-in-new-tab.actions";
 
 export const collectionAdminActionSet: ContextMenuActionSet = [[
     {
@@ -27,6 +28,20 @@ export const collectionAdminActionSet: ContextMenuActionSet = [[
             dispatch<any>(openCollectionUpdateDialog(resource));
         }
     },
+    {
+        icon: OpenIcon,
+        name: "Open in new tab",
+        execute: (dispatch, resource) => {
+            dispatch<any>(openInNewTabAction(resource));
+        }
+    },
+    {
+        icon: Link,
+        name: "Copy to clipboard",
+        execute: (dispatch, resource) => {
+            dispatch<any>(copyToClipboardAction(resource));
+        }
+    },
     {
         icon: ShareIcon,
         name: "Share",
@@ -36,6 +51,7 @@ export const collectionAdminActionSet: ContextMenuActionSet = [[
     },
     {
         component: ToggleFavoriteAction,
+        name: 'ToggleFavoriteAction',
         execute: (dispatch, resource) => {
             dispatch<any>(toggleFavorite(resource)).then(() => {
                 dispatch<any>(favoritePanelActions.REQUEST_ITEMS());
@@ -44,6 +60,7 @@ export const collectionAdminActionSet: ContextMenuActionSet = [[
     },
     {
         component: TogglePublicFavoriteAction,
+        name: 'TogglePublicFavoriteAction',
         execute: (dispatch, resource) => {
             dispatch<any>(togglePublicFavorite(resource)).then(() => {
                 dispatch<any>(publicFavoritePanelActions.REQUEST_ITEMS());
@@ -79,6 +96,7 @@ export const collectionAdminActionSet: ContextMenuActionSet = [[
     },
     {
         component: ToggleTrashAction,
+        name: 'ToggleTrashAction',
         execute: (dispatch, resource) => {
             dispatch<any>(toggleCollectionTrashed(resource.uuid, resource.isTrashed!!));
         }
diff --git a/src/views-components/context-menu/action-sets/collection-resource-action-set.ts b/src/views-components/context-menu/action-sets/collection-resource-action-set.ts
new file mode 100644 (file)
index 0000000..5bd362f
--- /dev/null
@@ -0,0 +1,91 @@
+// Copyright (C) The Arvados Authors. All rights reserved.
+//
+// SPDX-License-Identifier: AGPL-3.0
+
+import { ContextMenuActionSet } from "../context-menu-action-set";
+import { ToggleFavoriteAction } from "../actions/favorite-action";
+import { ToggleTrashAction } from "~/views-components/context-menu/actions/trash-action";
+import { toggleFavorite } from "~/store/favorites/favorites-actions";
+import { RenameIcon, ShareIcon, MoveToIcon, CopyIcon, DetailsIcon, AdvancedIcon, OpenIcon } from '~/components/icon/icon';
+import { openCollectionUpdateDialog } from "~/store/collections/collection-update-actions";
+import { favoritePanelActions } from "~/store/favorite-panel/favorite-panel-action";
+import { openMoveCollectionDialog } from '~/store/collections/collection-move-actions';
+import { openCollectionCopyDialog } from '~/store/collections/collection-copy-actions';
+import { toggleCollectionTrashed } from "~/store/trash/trash-actions";
+import { openSharingDialog } from "~/store/sharing-dialog/sharing-dialog-actions";
+import { openAdvancedTabDialog } from '~/store/advanced-tab/advanced-tab';
+import { toggleDetailsPanel } from '~/store/details-panel/details-panel-action';
+import { openInNewTabAction } from "~/store/open-in-new-tab/open-in-new-tab.actions";
+
+export const collectionResourceActionSet: ContextMenuActionSet = [[
+    {
+        icon: RenameIcon,
+        name: "Edit collection",
+        execute: (dispatch, resource) => {
+            dispatch<any>(openCollectionUpdateDialog(resource));
+        }
+    },
+    {
+        icon: ShareIcon,
+        name: "Share",
+        execute: (dispatch, { uuid }) => {
+            dispatch<any>(openSharingDialog(uuid));
+        }
+    },
+    {
+        component: ToggleFavoriteAction,
+        execute: (dispatch, resource) => {
+            dispatch<any>(toggleFavorite(resource)).then(() => {
+                dispatch<any>(favoritePanelActions.REQUEST_ITEMS());
+            });
+        }
+    },
+    {
+        icon: OpenIcon,
+        name: "Open in new tab",
+        execute: (dispatch, resource) => {
+            dispatch<any>(openInNewTabAction(resource));
+        }
+    },
+    {
+        icon: MoveToIcon,
+        name: "Move to",
+        execute: (dispatch, resource) => {
+            dispatch<any>(openMoveCollectionDialog(resource));
+        }
+    },
+    {
+        icon: CopyIcon,
+        name: "Copy to project",
+        execute: (dispatch, resource) => {
+            dispatch<any>(openCollectionCopyDialog(resource));
+        }
+    },
+    {
+        icon: DetailsIcon,
+        name: "View details",
+        execute: dispatch => {
+            dispatch<any>(toggleDetailsPanel());
+        }
+    },
+    {
+        icon: AdvancedIcon,
+        name: "Advanced",
+        execute: (dispatch, resource) => {
+            dispatch<any>(openAdvancedTabDialog(resource.uuid));
+        }
+    },
+    {
+        component: ToggleTrashAction,
+        execute: (dispatch, resource) => {
+            dispatch<any>(toggleCollectionTrashed(resource.uuid, resource.isTrashed!!));
+        }
+    },
+    // {
+    //     icon: RemoveIcon,
+    //     name: "Remove",
+    //     execute: (dispatch, resource) => {
+    //         // add code
+    //     }
+    // }
+]];
index 4f92aeb8132720abccda9a374bce7b6221be4cbe..c0b925c2ce49eab712926577a6abfe169eff38dd 100644 (file)
@@ -3,7 +3,7 @@
 // SPDX-License-Identifier: AGPL-3.0
 
 import { ContextMenuActionSet } from "../context-menu-action-set";
-import { NewProjectIcon, RenameIcon, MoveToIcon, DetailsIcon, AdvancedIcon } from '~/components/icon/icon';
+import { NewProjectIcon, RenameIcon, MoveToIcon, DetailsIcon, AdvancedIcon, OpenIcon, Link } from '~/components/icon/icon';
 import { ToggleFavoriteAction } from "../actions/favorite-action";
 import { toggleFavorite } from "~/store/favorites/favorites-actions";
 import { favoritePanelActions } from "~/store/favorite-panel/favorite-panel-action";
@@ -16,16 +16,32 @@ import { ShareIcon } from '~/components/icon/icon';
 import { openSharingDialog } from "~/store/sharing-dialog/sharing-dialog-actions";
 import { openAdvancedTabDialog } from "~/store/advanced-tab/advanced-tab";
 import { toggleDetailsPanel } from '~/store/details-panel/details-panel-action';
+import { copyToClipboardAction, openInNewTabAction } from "~/store/open-in-new-tab/open-in-new-tab.actions";
 
 export const readOnlyProjectActionSet: ContextMenuActionSet = [[
     {
         component: ToggleFavoriteAction,
+        name: 'ToggleFavoriteAction',
         execute: (dispatch, resource) => {
             dispatch<any>(toggleFavorite(resource)).then(() => {
                 dispatch<any>(favoritePanelActions.REQUEST_ITEMS());
             });
         }
     },
+    {
+        icon: OpenIcon,
+        name: "Open in new tab",
+        execute: (dispatch, resource) => {
+            dispatch<any>(openInNewTabAction(resource));
+        }
+    },
+    {
+        icon: Link,
+        name: "Copy to clipboard",
+        execute: (dispatch, resource) => {
+            dispatch<any>(copyToClipboardAction(resource));
+        }
+    },
     {
         icon: DetailsIcon,
         name: "View details",
@@ -75,6 +91,7 @@ export const projectActionSet: ContextMenuActionSet = [
         },
         {
             component: ToggleTrashAction,
+            name: 'ToggleTrashAction',
             execute: (dispatch, resource) => {
                 dispatch<any>(toggleProjectTrashed(resource.uuid, resource.ownerUuid, resource.isTrashed!!));
             }
index f6185804406321a27b36149c1029f3dca3bcea53..398864dc97b7b5bc4147fd3639dfaab8d9da54d5 100644 (file)
@@ -3,7 +3,7 @@
 // SPDX-License-Identifier: AGPL-3.0
 
 import { ContextMenuActionSet } from "../context-menu-action-set";
-import { NewProjectIcon, RenameIcon, MoveToIcon, DetailsIcon, AdvancedIcon } from '~/components/icon/icon';
+import { NewProjectIcon, RenameIcon, MoveToIcon, DetailsIcon, AdvancedIcon, OpenIcon, Link } from '~/components/icon/icon';
 import { ToggleFavoriteAction } from "../actions/favorite-action";
 import { toggleFavorite } from "~/store/favorites/favorites-actions";
 import { favoritePanelActions } from "~/store/favorite-panel/favorite-panel-action";
@@ -19,6 +19,7 @@ import { toggleDetailsPanel } from '~/store/details-panel/details-panel-action';
 import { TogglePublicFavoriteAction } from "~/views-components/context-menu/actions/public-favorite-action";
 import { togglePublicFavorite } from "~/store/public-favorites/public-favorites-actions";
 import { publicFavoritePanelActions } from "~/store/public-favorites-panel/public-favorites-action";
+import { copyToClipboardAction, openInNewTabAction } from "~/store/open-in-new-tab/open-in-new-tab.actions";
 
 export const projectAdminActionSet: ContextMenuActionSet = [[
     {
@@ -28,6 +29,20 @@ export const projectAdminActionSet: ContextMenuActionSet = [[
             dispatch<any>(openProjectCreateDialog(resource.uuid));
         }
     },
+    {
+        icon: OpenIcon,
+        name: "Open in new tab",
+        execute: (dispatch, resource) => {
+            dispatch<any>(openInNewTabAction(resource));
+        }
+    },
+    {
+        icon: Link,
+        name: "Copy to clipboard",
+        execute: (dispatch, resource) => {
+            dispatch<any>(copyToClipboardAction(resource));
+        }
+    },
     {
         icon: RenameIcon,
         name: "Edit project",
@@ -44,6 +59,7 @@ export const projectAdminActionSet: ContextMenuActionSet = [[
     },
     {
         component: ToggleFavoriteAction,
+        name: 'ToggleFavoriteAction',
         execute: (dispatch, resource) => {
             dispatch<any>(toggleFavorite(resource)).then(() => {
                 dispatch<any>(favoritePanelActions.REQUEST_ITEMS());
@@ -52,6 +68,7 @@ export const projectAdminActionSet: ContextMenuActionSet = [[
     },
     {
         component: TogglePublicFavoriteAction,
+        name: 'TogglePublicFavoriteAction',
         execute: (dispatch, resource) => {
             dispatch<any>(togglePublicFavorite(resource)).then(() => {
                 dispatch<any>(publicFavoritePanelActions.REQUEST_ITEMS());
@@ -88,6 +105,7 @@ export const projectAdminActionSet: ContextMenuActionSet = [[
     },
     {
         component: ToggleTrashAction,
+        name: 'ToggleTrashAction',
         execute: (dispatch, resource) => {
             dispatch<any>(toggleProjectTrashed(resource.uuid, resource.ownerUuid, resource.isTrashed!!));
         }
index 8dfcaca0e30853d9f317f8aaa460d58dae25cbc5..0ad2dc3c56c2c2ecd25a2f397467f377e2928bef 100644 (file)
@@ -2,7 +2,7 @@
 //
 // SPDX-License-Identifier: AGPL-3.0
 
-export const sanitizeToken = (href: string, tokenAsQueryParam: boolean = true): string => {
+export const sanitizeToken = (href: string, tokenAsQueryParam = true): string => {
     const [prefix, suffix] = href.split('/t=');
     const [token1, token2, token3, ...rest] = suffix.split('/');
     const token = `${token1}/${token2}/${token3}`;
@@ -11,9 +11,9 @@ export const sanitizeToken = (href: string, tokenAsQueryParam: boolean = true):
     return `${[prefix, ...rest].join('/')}${tokenAsQueryParam ? `${sep}api_token=${token}` : ''}`;
 };
 
-export const getClipboardUrl = (href: string): string => {
+export const getClipboardUrl = (href: string, shouldSanitizeToken = true): string => {
     const { origin } = window.location;
-    const url = sanitizeToken(href, false);
+    const url = shouldSanitizeToken ? sanitizeToken(href, false) : href;
 
-    return `${origin}?redirectTo=${url}`;
+    return shouldSanitizeToken ? `${origin}?redirectTo=${url}` : `${origin}${url}`;
 };
index 43474dd1942e5bc7ea14fb5993a909e584731048..b86498a0e9687dfa7fce6e2e2306ee263569ef30 100644 (file)
@@ -10,6 +10,7 @@ import { createAnchorAt } from "~/components/popover/helpers";
 import { ContextMenuActionSet, ContextMenuAction } from "./context-menu-action-set";
 import { Dispatch } from "redux";
 import { memoize } from 'lodash';
+import { sortByProperty } from "~/common/array-utils";
 type DataProps = Pick<ContextMenuProps, "anchorEl" | "items" | "open"> & { resource?: ContextMenuResource };
 const mapStateToProps = (state: RootState): DataProps => {
     const { open, position, resource } = state.contextMenu;
@@ -53,7 +54,8 @@ export const ContextMenu = connect(mapStateToProps, mapDispatchToProps, mergePro
 const menuActionSets = new Map<string, ContextMenuActionSet>();
 
 export const addMenuActionSet = (name: string, itemSet: ContextMenuActionSet) => {
-    menuActionSets.set(name, itemSet);
+    const sorted = itemSet.map(items => items.sort(sortByProperty('name')));
+    menuActionSets.set(name, sorted);
 };
 
 const emptyActionSet: ContextMenuActionSet = [];