From 04895cf6825dab872f91bf86941b51d5b00cb39a Mon Sep 17 00:00:00 2001 From: Michal Klobukowski Date: Tue, 19 Jun 2018 13:36:06 +0200 Subject: [PATCH] Extract generic context-menu component Feature #13634 Arvados-DCO-1.1-Signed-off-by: Michal Klobukowski --- .../context-menu.test.tsx | 37 +++-- src/components/context-menu/context-menu.tsx | 52 +++++++ src/components/data-explorer/context-menu.tsx | 110 ------------- .../data-explorer/data-explorer.tsx | 146 +++++++++++------- src/views/data-explorer/data-explorer.tsx | 6 +- 5 files changed, 167 insertions(+), 184 deletions(-) rename src/components/{data-explorer => context-menu}/context-menu.test.tsx (50%) create mode 100644 src/components/context-menu/context-menu.tsx delete mode 100644 src/components/data-explorer/context-menu.tsx diff --git a/src/components/data-explorer/context-menu.test.tsx b/src/components/context-menu/context-menu.test.tsx similarity index 50% rename from src/components/data-explorer/context-menu.test.tsx rename to src/components/context-menu/context-menu.test.tsx index 892180890e..9e4a9a4758 100644 --- a/src/components/data-explorer/context-menu.test.tsx +++ b/src/components/context-menu/context-menu.test.tsx @@ -5,7 +5,7 @@ import * as React from "react"; import { mount, configure, shallow } from "enzyme"; import * as Adapter from "enzyme-adapter-react-16"; -import { ContextMenu } from "./context-menu"; +import ContextMenu from "./context-menu"; import { ListItem } from "@material-ui/core"; configure({ adapter: new Adapter() }); @@ -19,15 +19,20 @@ describe("", () => { type: "" }; - const actions = { - onAddToFavourite: jest.fn(), - onCopy: jest.fn(), - onDownload: jest.fn(), - onMoveTo: jest.fn(), - onRemove: jest.fn(), - onRename: jest.fn(), - onShare: jest.fn() - }; + const actions = [[{ + icon: "", + name: "Action 1.1", + onClick: jest.fn() + }, + { + icon: "", + name: "Action 1.2", + onClick: jest.fn() + },], [{ + icon: "", + name: "Action 2.1", + onClick: jest.fn() + }]]; it("calls provided actions with provided item", () => { const contextMenu = mount(", () => { onClose={jest.fn()} {...{ actions, item }} />); - for (let index = 0; index < Object.keys(actions).length; index++) { - contextMenu.find(ListItem).at(index).simulate("click"); - } + contextMenu.find(ListItem).at(0).simulate("click"); + contextMenu.find(ListItem).at(1).simulate("click"); + contextMenu.find(ListItem).at(2).simulate("click"); - Object.keys(actions).forEach(key => { - expect(actions[key]).toHaveBeenCalledWith(item); - }); + expect(actions[0][0].onClick).toHaveBeenCalledWith(item); + expect(actions[0][1].onClick).toHaveBeenCalledWith(item); + expect(actions[1][0].onClick).toHaveBeenCalledWith(item); }); }); \ No newline at end of file diff --git a/src/components/context-menu/context-menu.tsx b/src/components/context-menu/context-menu.tsx new file mode 100644 index 0000000000..c86c5177a3 --- /dev/null +++ b/src/components/context-menu/context-menu.tsx @@ -0,0 +1,52 @@ +// Copyright (C) The Arvados Authors. All rights reserved. +// +// SPDX-License-Identifier: AGPL-3.0 +import * as React from "react"; +import { Popover, List, ListItem, ListItemIcon, ListItemText, Divider } from "@material-ui/core"; +import { DefaultTransformOrigin } from "../popover/helpers"; + +export interface ContextMenuAction { + name: string; + icon: string; + onClick: (item: T) => void; +} + +export type ContextMenuActionGroup = Array>; + +export interface ContextMenuProps { + anchorEl?: HTMLElement; + item?: T; + onClose: () => void; + actions: Array>; +} + +export default class ContextMenu extends React.PureComponent> { + render() { + const { anchorEl, onClose, actions, item } = this.props; + return + + {actions.map((group, groupIndex) => + + {group.map((action, actionIndex) => + item && action.onClick(item)}> + + + + + {action.name} + + )} + {groupIndex < actions.length - 1 && } + )} + + ; + } +} diff --git a/src/components/data-explorer/context-menu.tsx b/src/components/data-explorer/context-menu.tsx deleted file mode 100644 index 0b0a029ade..0000000000 --- a/src/components/data-explorer/context-menu.tsx +++ /dev/null @@ -1,110 +0,0 @@ -// Copyright (C) The Arvados Authors. All rights reserved. -// -// SPDX-License-Identifier: AGPL-3.0 -import * as React from "react"; -import { Popover, List, ListItem, ListItemIcon, ListItemText, Divider } from "@material-ui/core"; -import { DefaultTransformOrigin } from "../popover/helpers"; -import { DataItem } from "./data-item"; - -export type ContextMenuAction = (item: DataItem) => void; -export interface ContextMenuActions { - onShare: ContextMenuAction; - onMoveTo: ContextMenuAction; - onAddToFavourite: ContextMenuAction; - onRename: ContextMenuAction; - onCopy: ContextMenuAction; - onDownload: ContextMenuAction; - onRemove: ContextMenuAction; -} -export interface ContextMenuProps { - anchorEl?: HTMLElement; - item?: DataItem; - onClose: () => void; - actions: ContextMenuActions; -} - -export const ContextMenu: React.SFC = ({ anchorEl, onClose, actions, item }) => - - - ; - -interface ActionsProps { - actions: ContextMenuActions; - item?: DataItem; - onClose: () => void; -} - -const Actions: React.SFC = ({ actions, item, onClose }) => - - {[{ - icon: "fas fa-users", - label: "Share", - onClick: actions.onShare - }, - { - icon: "fas fa-sign-out-alt", - label: "Move to", - onClick: actions.onMoveTo - }, - { - icon: "fas fa-star", - label: "Add to favourite", - onClick: actions.onAddToFavourite - }, - { - icon: "fas fa-edit", - label: "Rename", - onClick: actions.onRename - }, - { - icon: "fas fa-copy", - label: "Make a copy", - onClick: actions.onCopy - }, - { - icon: "fas fa-download", - label: "Download", - onClick: actions.onDownload - }].map((props, index) => - )} - < Divider /> - - ; - -interface ActionProps { - onClick: ContextMenuAction; - item?: DataItem; - icon: string; - label: string; - onClose: () => void; -} - -const Action: React.SFC = ({ onClick, onClose, item, icon, label }) => - { - if (item) { - onClick(item); - onClose(); - } - }}> - - - - - {label} - - ; - diff --git a/src/components/data-explorer/data-explorer.tsx b/src/components/data-explorer/data-explorer.tsx index 96e259f8f0..40d6ae6878 100644 --- a/src/components/data-explorer/data-explorer.tsx +++ b/src/components/data-explorer/data-explorer.tsx @@ -10,12 +10,21 @@ import MoreVertIcon from "@material-ui/icons/MoreVert"; import { formatFileSize, formatDate } from '../../common/formatters'; import { DataItem } from './data-item'; import { mockAnchorFromMouseEvent } from '../popover/helpers'; -import { ContextMenu, ContextMenuActions } from './context-menu'; - +import ContextMenu, { ContextMenuActionGroup } from '../context-menu/context-menu'; + +export interface DataExplorerContextActions { + onAddToFavourite: (dataIitem: DataItem) => void; + onCopy: (dataIitem: DataItem) => void; + onDownload: (dataIitem: DataItem) => void; + onMoveTo: (dataIitem: DataItem) => void; + onRemove: (dataIitem: DataItem) => void; + onRename: (dataIitem: DataItem) => void; + onShare: (dataIitem: DataItem) => void; +} interface DataExplorerProps { items: DataItem[]; onItemClick: (item: DataItem) => void; - contextMenuActions: ContextMenuActions; + contextActions: DataExplorerContextActions; } interface DataExplorerState { @@ -23,59 +32,82 @@ interface DataExplorerState { contextMenu: { anchorEl?: HTMLElement; item?: DataItem; + actions: Array>; }; } class DataExplorer extends React.Component { state: DataExplorerState = { - contextMenu: {}, - columns: [ - { - name: "Name", - selected: true, - render: item => this.renderName(item) - }, - { - name: "Status", - selected: true, - render: item => renderStatus(item.status) - }, - { - name: "Type", - selected: true, - render: item => renderType(item.type) - }, - { - name: "Owner", - selected: true, - render: item => renderOwner(item.owner) - }, - { - name: "File size", - selected: true, - render: item => renderFileSize(item.fileSize) - }, - { - name: "Last modified", - selected: true, - render: item => renderDate(item.lastModified) - }, - { - name: "Actions", - selected: true, - configurable: false, - renderHeader: () => null, - render: item => this.renderActions(item) + contextMenu: { + actions: [[{ + icon: "fas fa-users fa-fw", + name: "Share", + onClick: this.handleContextAction("onShare") + }, { + icon: "fas fa-sign-out-alt fa-fw", + name: "Move to", + onClick: this.handleContextAction("onMoveTo") + }, { + icon: "fas fa-star fa-fw", + name: "Add to favourite", + onClick: this.handleContextAction("onAddToFavourite") + }, { + icon: "fas fa-edit fa-fw", + name: "Rename", + onClick: this.handleContextAction("onRename") + }, { + icon: "fas fa-copy fa-fw", + name: "Make a copy", + onClick: this.handleContextAction("onCopy") + }, { + icon: "fas fa-download fa-fw", + name: "Download", + onClick: this.handleContextAction("onDownload") + }], [{ + icon: "fas fa-trash-alt fa-fw", + name: "Remove", + onClick: this.handleContextAction("onRemove") } - ] + ]] + }, + columns: [{ + name: "Name", + selected: true, + render: item => this.renderName(item) + }, { + name: "Status", + selected: true, + render: item => renderStatus(item.status) + }, { + name: "Type", + selected: true, + render: item => renderType(item.type) + }, { + name: "Owner", + selected: true, + render: item => renderOwner(item.owner) + }, { + name: "File size", + selected: true, + render: item => renderFileSize(item.fileSize) + }, { + name: "Last modified", + selected: true, + render: item => renderDate(item.lastModified) + }, { + name: "Actions", + selected: true, + configurable: false, + renderHeader: () => null, + render: item => this.renderActions(item) + }] }; render() { return + onClose={this.closeContextMenu} /> openItemMenuOnRowClick = (event: React.MouseEvent, item: DataItem) => { event.preventDefault(); - this.setState({ - contextMenu: { - anchorEl: mockAnchorFromMouseEvent(event), - item - } + this.setContextMenuState({ + anchorEl: mockAnchorFromMouseEvent(event), + item }); } openItemMenuOnActionsClick = (event: React.MouseEvent, item: DataItem) => { - this.setState({ - contextMenu: { - anchorEl: event.currentTarget, - item - } + this.setContextMenuState({ + anchorEl: event.currentTarget, + item }); } closeContextMenu = () => { - this.setState({ contextMenu: {} }); + this.setContextMenuState({}); + } + + setContextMenuState = (contextMenu: { anchorEl?: HTMLElement; item?: DataItem; }) => { + this.setState(prev => ({ contextMenu: { ...contextMenu, actions: prev.contextMenu.actions } })); + } + + handleContextAction(action: keyof DataExplorerContextActions) { + return (item: DataItem) => this.props.contextActions[action](item); } } diff --git a/src/views/data-explorer/data-explorer.tsx b/src/views/data-explorer/data-explorer.tsx index bb19a269e4..da09b695db 100644 --- a/src/views/data-explorer/data-explorer.tsx +++ b/src/views/data-explorer/data-explorer.tsx @@ -13,7 +13,7 @@ import { push } from 'react-router-redux'; import projectActions from "../../store/project/project-action"; import { DataExplorer, DataItem } from '../../components/data-explorer'; import { TreeItem } from '../../components/tree/tree'; -import { ContextMenuActions } from '../../components/data-explorer/context-menu'; +import { DataExplorerContextActions } from '../../components/data-explorer/data-explorer'; interface DataExplorerViewDataProps { projects: ProjectState; @@ -36,12 +36,12 @@ class DataExplorerView extends React.Component ); } - contextMenuActions: ContextMenuActions = { + contextActions: DataExplorerContextActions = { onAddToFavourite: console.log, onCopy: console.log, onDownload: console.log, -- 2.30.2