Implement better pattern for hanling actions in context menu
authorMichal Klobukowski <michal.klobukowski@contractors.roche.com>
Fri, 13 Jul 2018 14:18:05 +0000 (16:18 +0200)
committerMichal Klobukowski <michal.klobukowski@contractors.roche.com>
Fri, 13 Jul 2018 14:18:05 +0000 (16:18 +0200)
Feature #13805

Arvados-DCO-1.1-Signed-off-by: Michal Klobukowski <michal.klobukowski@contractors.roche.com>

18 files changed:
src/common/api/common-resource-service.test.ts
src/components/attribute/attribute.tsx
src/components/context-menu/context-menu.tsx
src/components/data-explorer/data-explorer.tsx
src/components/empty-state/empty-state.tsx
src/services/groups-service/groups-service.test.ts
src/services/project-service/project-service.test.ts
src/store/context-menu/context-menu-reducer.ts
src/store/project-panel/project-panel-middleware.ts
src/store/store.ts
src/views-components/context-menu/context-menu-item-set.ts [new file with mode: 0644]
src/views-components/context-menu/context-menu.tsx
src/views-components/context-menu/empty-item-set.ts [new file with mode: 0644]
src/views-components/context-menu/index.ts [new file with mode: 0644]
src/views-components/context-menu/project-item-set.ts [new file with mode: 0644]
src/views-components/context-menu/root-project-item-set.ts [new file with mode: 0644]
src/views-components/create-project-dialog/create-project-dialog.tsx
src/views/workbench/workbench.tsx

index 7093b59c555430953ed0e678ee56ff10566aa4bb..b8b1f44cc28a14d1075c3a94dc9604b62f8d7660 100644 (file)
@@ -4,7 +4,7 @@
 
 import CommonResourceService from "./common-resource-service";
 import axios from "axios";
-import MockAdapter from "axios-mock-adapter";
+import MockAdapter from "axios-mock-adapter/types";
 
 describe("CommonResourceService", () => {
     const axiosInstance = axios.create();
index ea35f5bfa1d343277a1956655fab5dce51b642a8..c0874f7d5469818123ecb266c9096ee365ce97dc 100644 (file)
@@ -5,7 +5,7 @@
 import * as React from 'react';
 import Typography from '@material-ui/core/Typography';
 import { StyleRulesCallback, WithStyles, withStyles } from '@material-ui/core/styles';
-import { ArvadosTheme } from 'src/common/custom-theme';
+import { ArvadosTheme } from '../../common/custom-theme';
 
 interface AttributeDataProps {
     label: string;
index 562618180b57e1f66906b6f1a92e63e28e520d52..49b65927a0715f6a790a04272f8b0e1a44226080 100644 (file)
@@ -5,24 +5,23 @@ import * as React from "react";
 import { Popover, List, ListItem, ListItemIcon, ListItemText, Divider } from "@material-ui/core";
 import { DefaultTransformOrigin } from "../popover/helpers";
 
-export interface ContextMenuAction {
+export interface ContextMenuItem {
     name: string;
     icon: string;
-    openCreateDialog?: boolean;
 }
 
-export type ContextMenuActionGroup = ContextMenuAction[];
+export type ContextMenuItemGroup = ContextMenuItem[];
 
 export interface ContextMenuProps {
     anchorEl?: HTMLElement;
-    actions: ContextMenuActionGroup[];
-    onActionClick: (action: ContextMenuAction) => void;
+    items: ContextMenuItemGroup[];
+    onItemClick: (action: ContextMenuItem) => void;
     onClose: () => void;
 }
 
 export default class ContextMenu extends React.PureComponent<ContextMenuProps> {
     render() {
-        const { anchorEl, actions, onClose, onActionClick } = this.props;
+        const { anchorEl, items, onClose, onItemClick } = this.props;
         return <Popover
             anchorEl={anchorEl}
             open={!!anchorEl}
@@ -31,21 +30,21 @@ export default class ContextMenu extends React.PureComponent<ContextMenuProps> {
             anchorOrigin={DefaultTransformOrigin}
             onContextMenu={this.handleContextMenu}>
             <List dense>
-                {actions.map((group, groupIndex) =>
+                {items.map((group, groupIndex) =>
                     <React.Fragment key={groupIndex}>
-                        {group.map((action, actionIndex) =>
+                        {group.map((item, actionIndex) =>
                             <ListItem
                                 button
                                 key={actionIndex}
-                                onClick={() => onActionClick(action)}>
+                                onClick={() => onItemClick(item)}>
                                 <ListItemIcon>
-                                    <i className={action.icon} />
+                                    <i className={item.icon} />
                                 </ListItemIcon>
                                 <ListItemText>
-                                    {action.name}
+                                    {item.name}
                                 </ListItemText>
                             </ListItem>)}
-                        {groupIndex < actions.length - 1 && <Divider />}
+                        {groupIndex < items.length - 1 && <Divider />}
                     </React.Fragment>)}
             </List>
         </Popover>;
index e851ca992412257e8e036b7c5371f00d9411359f..1073ddd8b596e670ed3e80e56a085669354ca1ae 100644 (file)
@@ -5,10 +5,10 @@
 import * as React from 'react';
 import { Grid, Paper, Toolbar, StyleRulesCallback, withStyles, Theme, WithStyles, TablePagination, IconButton } from '@material-ui/core';
 import MoreVertIcon from "@material-ui/icons/MoreVert";
-import ColumnSelector from "../../components/column-selector/column-selector";
-import DataTable, { DataColumns } from "../../components/data-table/data-table";
-import { DataColumn } from "../../components/data-table/data-column";
-import { DataTableFilterItem } from '../../components/data-table-filters/data-table-filters';
+import ColumnSelector from "../column-selector/column-selector";
+import DataTable, { DataColumns } from "../data-table/data-table";
+import { DataColumn } from "../data-table/data-column";
+import { DataTableFilterItem } from '../data-table-filters/data-table-filters';
 import SearchInput from '../search-input/search-input';
 
 interface DataExplorerProps<T> {
index 205053b5ce7f738a1ba2b1627c5d4fb7b86b1c4b..9ab306359e28b2eeba3feec5a3ee4754f883c3ca 100644 (file)
@@ -5,7 +5,7 @@
 import * as React from 'react';
 import Typography from '@material-ui/core/Typography';
 import { WithStyles, withStyles, StyleRulesCallback } from '@material-ui/core/styles';
-import { ArvadosTheme } from 'src/common/custom-theme';
+import { ArvadosTheme } from '../../common/custom-theme';
 import IconBase, { IconTypes } from '../icon/icon';
 
 export interface EmptyStateDataProps {
index 2562a595d02ff52b0c0af4954d10f5fb20e67e06..92d227781cc64dddc51c5577d55d31fb351691f2 100644 (file)
@@ -3,7 +3,7 @@
 // SPDX-License-Identifier: AGPL-3.0
 
 import axios from "axios";
-import MockAdapter from "axios-mock-adapter";
+import MockAdapter from "axios-mock-adapter/types";
 import GroupsService from "./groups-service";
 
 describe("GroupsService", () => {
index 68df2450a298ce971ee4483842590f2a0812fc0d..76da3d860a2cf730b0cb780fa7c150f9be410149 100644 (file)
@@ -3,7 +3,7 @@
 // SPDX-License-Identifier: AGPL-3.0
 
 import axios from "axios";
-import MockAdapter from "axios-mock-adapter";
+import MockAdapter from "axios-mock-adapter/types";
 import ProjectService from "./project-service";
 import FilterBuilder from "../../common/api/filter-builder";
 import { ProjectResource } from "../../models/project";
index 129c2af4763ed0556cdbb224046174ba5375fddb..147f094336526106182c294426597c6a385d2230 100644 (file)
@@ -17,13 +17,7 @@ export interface ContextMenuPosition {
 
 export interface ContextMenuResource {
     uuid: string;
-    kind: ContextMenuKind;
-}
-
-export enum ContextMenuKind {
-    RootProject = "RootProject",
-    Project = "Project",
-    Collection = "Collection"
+    kind: string;
 }
 
 const initialState = {
index e72b6c1b905469e9a5f6ea5e2d9bbc7457b0de23..80fb7fa3b9c23ea1d1eb7f8cc044cd40036e5d07 100644 (file)
@@ -3,11 +3,11 @@
 // SPDX-License-Identifier: AGPL-3.0
 
 import { Middleware } from "redux";
-import actions from "../../store/data-explorer/data-explorer-action";
+import actions from "../data-explorer/data-explorer-action";
 import { PROJECT_PANEL_ID, columns, ProjectPanelFilter, ProjectPanelColumnNames } from "../../views/project-panel/project-panel";
 import { groupsService } from "../../services/services";
-import { RootState } from "../../store/store";
-import { getDataExplorer, DataExplorerState } from "../../store/data-explorer/data-explorer-reducer";
+import { RootState } from "../store";
+import { getDataExplorer, DataExplorerState } from "../data-explorer/data-explorer-reducer";
 import { resourceToDataItem, ProjectPanelItem } from "../../views/project-panel/project-panel-item";
 import FilterBuilder from "../../common/api/filter-builder";
 import { DataColumns } from "../../components/data-table/data-table";
index 130028adcf87b9dd6ea80f318f06e2caa7f12f93..d87c8031fcbfeffe596271a2c324e4764c219ddb 100644 (file)
@@ -11,7 +11,7 @@ import projectsReducer, { ProjectState } from "./project/project-reducer";
 import sidePanelReducer, { SidePanelState } from './side-panel/side-panel-reducer';
 import authReducer, { AuthState } from "./auth/auth-reducer";
 import dataExplorerReducer, { DataExplorerState } from './data-explorer/data-explorer-reducer';
-import { projectPanelMiddleware } from '../store/project-panel/project-panel-middleware';
+import { projectPanelMiddleware } from './project-panel/project-panel-middleware';
 import detailsPanelReducer, { DetailsPanelState } from './details-panel/details-panel-reducer';
 import contextMenuReducer, { ContextMenuState } from './context-menu/context-menu-reducer';
 
diff --git a/src/views-components/context-menu/context-menu-item-set.ts b/src/views-components/context-menu/context-menu-item-set.ts
new file mode 100644 (file)
index 0000000..0b207ad
--- /dev/null
@@ -0,0 +1,12 @@
+// Copyright (C) The Arvados Authors. All rights reserved.
+//
+// SPDX-License-Identifier: AGPL-3.0
+
+import { Dispatch } from "redux";
+import { ContextMenuItemGroup, ContextMenuItem } from "../../components/context-menu/context-menu";
+import { ContextMenuResource } from "../../store/context-menu/context-menu-reducer";
+
+export interface ContextMenuItemSet {
+    handleItem (dispatch: Dispatch, action: ContextMenuItem, resource: ContextMenuResource): void;
+    getItems (): ContextMenuItemGroup[];
+}
index bfd833a8e6fadf31c561fc72f86455731a079d63..93cb644a681181d46e29ff65009c47218b88ec73 100644 (file)
@@ -5,33 +5,31 @@
 import { connect, Dispatch, DispatchProp } from "react-redux";
 import { RootState } from "../../store/store";
 import actions from "../../store/context-menu/context-menu-actions";
-import ContextMenu, { ContextMenuAction, ContextMenuProps } from "../../components/context-menu/context-menu";
+import ContextMenu, { ContextMenuProps, ContextMenuItem } from "../../components/context-menu/context-menu";
 import { createAnchorAt } from "../../components/popover/helpers";
-import projectActions from "../../store/project/project-action";
-import { ContextMenuResource, ContextMenuKind } from "../../store/context-menu/context-menu-reducer";
+import { ContextMenuResource } from "../../store/context-menu/context-menu-reducer";
+import { ContextMenuItemSet } from "./context-menu-item-set";
+import { emptyItemSet } from "./empty-item-set";
 
-
-type DataProps = Pick<ContextMenuProps, "anchorEl" | "actions"> & { resource?: ContextMenuResource };
+type DataProps = Pick<ContextMenuProps, "anchorEl" | "items"> & { resource?: ContextMenuResource };
 const mapStateToProps = (state: RootState): DataProps => {
     const { position, resource } = state.contextMenu;
     return {
         anchorEl: resource ? createAnchorAt(position) : undefined,
-        actions: resource ? menuActions[resource.kind] : [],
+        items: getMenuItemSet(resource).getItems(),
         resource
     };
 };
 
-type ActionProps = Pick<ContextMenuProps, "onClose"> & { onActionClick: (action: ContextMenuAction, resource?: ContextMenuResource) => void };
+type ActionProps = Pick<ContextMenuProps, "onClose"> & { onItemClick: (item: ContextMenuItem, resource?: ContextMenuResource) => void };
 const mapDispatchToProps = (dispatch: Dispatch): ActionProps => ({
     onClose: () => {
         dispatch(actions.CLOSE_CONTEXT_MENU());
     },
-    onActionClick: (action: ContextMenuAction, resource?: ContextMenuResource) => {
+    onItemClick: (item: ContextMenuItem, resource?: ContextMenuResource) => {
         dispatch(actions.CLOSE_CONTEXT_MENU());
         if (resource) {
-            if (action.name === "New project") {
-                dispatch(projectActions.OPEN_PROJECT_CREATOR({ ownerUuid: resource.uuid }));
-            }
+            getMenuItemSet(resource).handleItem(dispatch, item, resource);
         }
     }
 });
@@ -39,65 +37,20 @@ const mapDispatchToProps = (dispatch: Dispatch): ActionProps => ({
 const mergeProps = ({ resource, ...dataProps }: DataProps, actionProps: ActionProps): ContextMenuProps => ({
     ...dataProps,
     ...actionProps,
-    onActionClick: (action: ContextMenuAction) => {
-        actionProps.onActionClick(action, resource);
+    onItemClick: item => {
+        actionProps.onItemClick(item, resource);
     }
 });
 
-export default connect(mapStateToProps, mapDispatchToProps, mergeProps)(ContextMenu);
+export const ContextMenuHOC = connect(mapStateToProps, mapDispatchToProps, mergeProps)(ContextMenu);
 
-const menuActions = {
-    [ContextMenuKind.RootProject]: [[{
-        icon: "fas fa-plus fa-fw",
-        name: "New project"
-    }]],
-    [ContextMenuKind.Project]: [[{
-        icon: "fas fa-plus fa-fw",
-        name: "New project"
-    }, {
-        icon: "fas fa-users fa-fw",
-        name: "Share"
-    }, {
-        icon: "fas fa-sign-out-alt fa-fw",
-        name: "Move to"
-    }, {
-        icon: "fas fa-star fa-fw",
-        name: "Add to favourite"
-    }, {
-        icon: "fas fa-edit fa-fw",
-        name: "Rename"
-    }, {
-        icon: "fas fa-copy fa-fw",
-        name: "Make a copy"
-    }, {
-        icon: "fas fa-download fa-fw",
-        name: "Download"
-    }], [{
-        icon: "fas fa-trash-alt fa-fw",
-        name: "Remove"
-    }
-    ]],
-    [ContextMenuKind.Collection]: [[{
-        icon: "fas fa-users fa-fw",
-        name: "Share"
-    }, {
-        icon: "fas fa-sign-out-alt fa-fw",
-        name: "Move to"
-    }, {
-        icon: "fas fa-star fa-fw",
-        name: "Add to favourite"
-    }, {
-        icon: "fas fa-edit fa-fw",
-        name: "Rename"
-    }, {
-        icon: "fas fa-copy fa-fw",
-        name: "Make a copy"
-    }, {
-        icon: "fas fa-download fa-fw",
-        name: "Download"
-    }], [{
-        icon: "fas fa-trash-alt fa-fw",
-        name: "Remove"
-    }
-    ]]
+const menuItemSets = new Map<string, ContextMenuItemSet>();
+
+export const addMenuItemsSet = (name: string, itemSet: ContextMenuItemSet) => {
+    menuItemSets.set(name, itemSet);
 };
+
+const getMenuItemSet = (resource?: ContextMenuResource): ContextMenuItemSet => {
+    return resource ? menuItemSets.get(resource.kind) || emptyItemSet : emptyItemSet;
+};
+
diff --git a/src/views-components/context-menu/empty-item-set.ts b/src/views-components/context-menu/empty-item-set.ts
new file mode 100644 (file)
index 0000000..209b75c
--- /dev/null
@@ -0,0 +1,13 @@
+// Copyright (C) The Arvados Authors. All rights reserved.
+//
+// SPDX-License-Identifier: AGPL-3.0
+
+import { ContextMenuItemGroup } from "../../components/context-menu/context-menu";
+import { ContextMenuItemSet } from "./context-menu-item-set";
+
+export const emptyItemSet: ContextMenuItemSet = {
+    getItems: () => items,
+    handleItem: () => { return; }
+};
+
+const items: ContextMenuItemGroup[] = [];
\ No newline at end of file
diff --git a/src/views-components/context-menu/index.ts b/src/views-components/context-menu/index.ts
new file mode 100644 (file)
index 0000000..9293330
--- /dev/null
@@ -0,0 +1,17 @@
+// Copyright (C) The Arvados Authors. All rights reserved.
+//
+// SPDX-License-Identifier: AGPL-3.0
+
+import { ContextMenuHOC, addMenuItemsSet } from "./context-menu";
+import { projectItemSet } from "./project-item-set";
+import { rootProjectItemSet } from "./root-project-item-set";
+
+export default ContextMenuHOC;
+
+export enum ContextMenuKind {
+    RootProject = "RootProject",
+    Project = "Project"
+}
+
+addMenuItemsSet(ContextMenuKind.RootProject, rootProjectItemSet);
+addMenuItemsSet(ContextMenuKind.Project, projectItemSet);
\ No newline at end of file
diff --git a/src/views-components/context-menu/project-item-set.ts b/src/views-components/context-menu/project-item-set.ts
new file mode 100644 (file)
index 0000000..583bbaa
--- /dev/null
@@ -0,0 +1,43 @@
+// Copyright (C) The Arvados Authors. All rights reserved.
+//
+// SPDX-License-Identifier: AGPL-3.0
+
+import { ContextMenuItemGroup } from "../../components/context-menu/context-menu";
+import { ContextMenuItemSet } from "./context-menu-item-set";
+import actions from "../../store/project/project-action";
+
+export const projectItemSet: ContextMenuItemSet = {
+    getItems: () => items,
+    handleItem: (dispatch, item, resource) => {
+        if (item.name === "New project") {
+            dispatch(actions.OPEN_PROJECT_CREATOR({ ownerUuid: resource.uuid }));
+        }
+    }
+};
+
+const items: ContextMenuItemGroup[] = [[{
+    icon: "fas fa-plus fa-fw",
+    name: "New project"
+}, {
+    icon: "fas fa-users fa-fw",
+    name: "Share"
+}, {
+    icon: "fas fa-sign-out-alt fa-fw",
+    name: "Move to"
+}, {
+    icon: "fas fa-star fa-fw",
+    name: "Add to favourite"
+}, {
+    icon: "fas fa-edit fa-fw",
+    name: "Rename"
+}, {
+    icon: "fas fa-copy fa-fw",
+    name: "Make a copy"
+}, {
+    icon: "fas fa-download fa-fw",
+    name: "Download"
+}], [{
+    icon: "fas fa-trash-alt fa-fw",
+    name: "Remove"
+}
+]];
\ No newline at end of file
diff --git a/src/views-components/context-menu/root-project-item-set.ts b/src/views-components/context-menu/root-project-item-set.ts
new file mode 100644 (file)
index 0000000..ae760f0
--- /dev/null
@@ -0,0 +1,21 @@
+// Copyright (C) The Arvados Authors. All rights reserved.
+//
+// SPDX-License-Identifier: AGPL-3.0
+
+import { ContextMenuItemGroup } from "../../components/context-menu/context-menu";
+import { ContextMenuItemSet } from "./context-menu-item-set";
+import actions from "../../store/project/project-action";
+
+export const rootProjectItemSet: ContextMenuItemSet = {
+    getItems: () => items,
+    handleItem: (dispatch, item, resource) => {
+        if (item.name === "New project") {
+            dispatch(actions.OPEN_PROJECT_CREATOR({ ownerUuid: resource.uuid }));
+        }
+    }
+};
+
+const items: ContextMenuItemGroup[] = [[{
+    icon: "fas fa-plus fa-fw",
+    name: "New project"
+}]];
\ No newline at end of file
index 701ceee107d3e2fcf8d5e22a8f1861126d732ada..6b69b79301faf7fd7ac05ae958b46550f1340db0 100644 (file)
@@ -3,7 +3,7 @@
 // SPDX-License-Identifier: AGPL-3.0
 
 import { connect } from "react-redux";
-import { Dispatch } from "../../../node_modules/redux";
+import { Dispatch } from "redux";
 import { RootState } from "../../store/store";
 import DialogProjectCreate from "../dialog-create/dialog-project-create";
 import actions, { createProject, getProjectList } from "../../store/project/project-action";
index 52332e8b161b4d965fbe418a875037696c050e12..13e22d8ca76e72f6fee4a4c9f62ffaf971e03d0e 100644 (file)
@@ -26,7 +26,7 @@ import projectActions from "../../store/project/project-action";
 import ProjectPanel from "../project-panel/project-panel";
 import DetailsPanel from '../../views-components/details-panel/details-panel';
 import { ArvadosTheme } from '../../common/custom-theme';
-import ContextMenu from "../../views-components/context-menu/context-menu";
+import ContextMenu, { ContextMenuKind } from "../../views-components/context-menu";
 import CreateProjectDialog from "../../views-components/create-project-dialog/create-project-dialog";
 import { authService } from '../../services/services';
 
@@ -35,7 +35,6 @@ import contextMenuActions from "../../store/context-menu/context-menu-actions";
 import { SidePanelIdentifiers } from '../../store/side-panel/side-panel-reducer';
 import { ProjectResource } from '../../models/project';
 import { ResourceKind } from '../../models/resource';
-import { ContextMenuKind } from '../../store/context-menu/context-menu-reducer';
 
 const drawerWidth = 240;
 const appBarHeight = 100;
@@ -217,7 +216,7 @@ class Workbench extends React.Component<WorkbenchProps, WorkbenchState> {
                             <ProjectTree
                                 projects={this.props.projects}
                                 toggleOpen={itemId => this.props.dispatch<any>(setProjectItem(itemId, ItemMode.OPEN))}
-                                onContextMenu={(event, item) => this.openContextMenu(event, item.data.uuid,  ContextMenuKind.Project)}
+                                onContextMenu={(event, item) => this.openContextMenu(event, item.data.uuid, ContextMenuKind.Project)}
                                 toggleActive={itemId => {
                                     this.props.dispatch<any>(setProjectItem(itemId, ItemMode.ACTIVE));
                                     this.props.dispatch<any>(loadDetails(itemId, ResourceKind.Project));