Merge remote-tracking branch 'origin' into 13694-Data-operations-Project-creation
authorPawel Kowalczyk <pawel.kowalczyk@contractors.roche.com>
Mon, 9 Jul 2018 13:52:10 +0000 (15:52 +0200)
committerPawel Kowalczyk <pawel.kowalczyk@contractors.roche.com>
Mon, 9 Jul 2018 13:52:10 +0000 (15:52 +0200)
refs #13694

Arvados-DCO-1.1-Signed-off-by: Pawel Kowalczyk <pawel.kowalczyk@contractors.roche.com>

15 files changed:
src/components/breadcrumbs/breadcrumbs.test.tsx
src/components/breadcrumbs/breadcrumbs.tsx
src/components/context-menu/context-menu.tsx
src/components/data-explorer/data-explorer.test.tsx
src/components/data-explorer/data-explorer.tsx
src/components/data-table/data-table.test.tsx
src/components/data-table/data-table.tsx
src/components/dialog-create/dialog-project-create.tsx [new file with mode: 0644]
src/components/side-panel/side-panel.tsx
src/components/tree/tree.tsx
src/views-components/data-explorer/data-explorer.tsx
src/views-components/main-app-bar/main-app-bar.tsx
src/views-components/project-tree/project-tree.tsx
src/views/project-panel/project-panel.tsx
src/views/workbench/workbench.tsx

index b525554a2c8031d80464c5d2f070b55cfbf9b57c..ef3f8887976384922004830e2c5c6b2d8953edce 100644 (file)
@@ -24,7 +24,7 @@ describe("<Breadcrumbs />", () => {
         const items = [
             { label: 'breadcrumb 1' }
         ];
-        const breadcrumbs = mount(<Breadcrumbs items={items} onClick={onClick} />);
+        const breadcrumbs = mount(<Breadcrumbs items={items} onClick={onClick} onContextMenu={jest.fn()} />);
         expect(breadcrumbs.find(Button)).toHaveLength(1);
         expect(breadcrumbs.find(ChevronRightIcon)).toHaveLength(0);
     });
@@ -34,7 +34,7 @@ describe("<Breadcrumbs />", () => {
             { label: 'breadcrumb 1' },
             { label: 'breadcrumb 2' }
         ];
-        const breadcrumbs = mount(<Breadcrumbs items={items} onClick={onClick} />);
+        const breadcrumbs = mount(<Breadcrumbs items={items} onClick={onClick} onContextMenu={jest.fn()} />);
         expect(breadcrumbs.find(Button)).toHaveLength(2);
         expect(breadcrumbs.find(ChevronRightIcon)).toHaveLength(1);
     });
@@ -44,7 +44,7 @@ describe("<Breadcrumbs />", () => {
             { label: 'breadcrumb 1' },
             { label: 'breadcrumb 2' }
         ];
-        const breadcrumbs = mount(<Breadcrumbs items={items} onClick={onClick} />);
+        const breadcrumbs = mount(<Breadcrumbs items={items} onClick={onClick} onContextMenu={jest.fn()} />);
         breadcrumbs.find(Button).at(1).simulate('click');
         expect(onClick).toBeCalledWith(items[1]);
     });
index 41f71981e57eea29d54064f5888c78961f283f89..4868e137f9f6b11065bdd2dbfaf3d3ff4ac642da 100644 (file)
@@ -14,9 +14,10 @@ export interface Breadcrumb {
 interface BreadcrumbsProps {
     items: Breadcrumb[];
     onClick: (breadcrumb: Breadcrumb) => void;
+    onContextMenu: (event: React.MouseEvent<HTMLElement>, breadcrumb: Breadcrumb) => void;
 }
 
-const Breadcrumbs: React.SFC<BreadcrumbsProps & WithStyles<CssRules>> = ({ classes, onClick, items }) => {
+const Breadcrumbs: React.SFC<BreadcrumbsProps & WithStyles<CssRules>> = ({ classes, onClick, onContextMenu, items }) => {
     return <Grid container alignItems="center" wrap="nowrap">
         {
             items.map((item, index) => {
@@ -28,12 +29,11 @@ const Breadcrumbs: React.SFC<BreadcrumbsProps & WithStyles<CssRules>> = ({ class
                                 color="inherit"
                                 className={isLastItem ? classes.currentItem : classes.item}
                                 onClick={() => onClick(item)}
-                            >
+                                onContextMenu={event => onContextMenu(event, item)}>
                                 <Typography
                                     noWrap
                                     color="inherit"
-                                    className={classes.label}
-                                >
+                                    className={classes.label}>
                                     {item.label}
                                 </Typography>
                             </Button>
index 6ac1207b9b3fb8875a9b524bd44d21b5a4ce3fc3..c892ba2616dda6480de47d2c3767596636267917 100644 (file)
@@ -8,6 +8,7 @@ import { DefaultTransformOrigin } from "../popover/helpers";
 export interface ContextMenuAction {
     name: string;
     icon: string;
+    openCreateDialog?: boolean;
 }
 
 export type ContextMenuActionGroup = ContextMenuAction[];
index 33899c00c79aa242c902c7c4d6af7cc11c741900..97b1bec602a4330fd2c4eff29ddd8dde6be59ce6 100644 (file)
@@ -22,8 +22,6 @@ describe("<DataExplorer />", () => {
         const onContextAction = jest.fn();
         const dataExplorer = mount(<DataExplorer
             {...mockDataExplorerProps()}
-            contextActions={[]}
-            onContextAction={onContextAction}
             items={[{ key: "1", name: "item 1" }] as MockItem[]}
             columns={[{ name: "Column 1", render: jest.fn(), selected: true }]} />);
         expect(dataExplorer.find(ContextMenu).prop("actions")).toEqual([]);
@@ -54,7 +52,6 @@ describe("<DataExplorer />", () => {
             {...mockDataExplorerProps()}
             columns={columns}
             onColumnToggle={onColumnToggle}
-            contextActions={[]}
             items={[{ key: "1", name: "item 1" }] as MockItem[]} />);
         expect(dataExplorer.find(ColumnSelector).prop("columns")).toBe(columns);
         dataExplorer.find(ColumnSelector).prop("onColumnToggle")("columns");
@@ -125,7 +122,7 @@ const mockDataExplorerProps = () => ({
     onSortToggle: jest.fn(),
     onRowClick: jest.fn(),
     onColumnToggle: jest.fn(),
-    onContextAction: jest.fn(),
     onChangePage: jest.fn(),
-    onChangeRowsPerPage: jest.fn()
+    onChangeRowsPerPage: jest.fn(),
+    onContextMenu: jest.fn()
 });
\ No newline at end of file
index 09a327268b76073b3bc92f90db91e4ea9ab8e5d9..e90b1c2733d94d1a8ff51ae34841d7d3cfef1c58 100644 (file)
@@ -5,10 +5,8 @@
 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 ContextMenu, { ContextMenuActionGroup, ContextMenuAction } from "../../components/context-menu/context-menu";
 import ColumnSelector from "../../components/column-selector/column-selector";
 import DataTable, { DataColumns, DataItem } from "../../components/data-table/data-table";
-import { mockAnchorFromMouseEvent } from "../../components/popover/helpers";
 import { DataColumn } from "../../components/data-table/data-column";
 import { DataTableFilterItem } from '../../components/data-table-filters/data-table-filters';
 import SearchInput from '../search-input/search-input';
@@ -17,7 +15,6 @@ interface DataExplorerProps<T> {
     items: T[];
     itemsAvailable: number;
     columns: DataColumns<T>;
-    contextActions: ContextMenuActionGroup[];
     searchValue: string;
     rowsPerPage: number;
     rowsPerPageOptions?: number[];
@@ -25,32 +22,17 @@ interface DataExplorerProps<T> {
     onSearch: (value: string) => void;
     onRowClick: (item: T) => void;
     onColumnToggle: (column: DataColumn<T>) => void;
-    onContextAction: (action: ContextMenuAction, item: T) => void;
+    onContextMenu: (event: React.MouseEvent<HTMLElement>, item: T) => void;
     onSortToggle: (column: DataColumn<T>) => void;
     onFiltersChange: (filters: DataTableFilterItem[], column: DataColumn<T>) => void;
     onChangePage: (page: number) => void;
     onChangeRowsPerPage: (rowsPerPage: number) => void;
 }
 
-interface DataExplorerState<T> {
-    contextMenu: {
-        anchorEl?: HTMLElement;
-        item?: T;
-    };
-}
-
-class DataExplorer<T extends DataItem> extends React.Component<DataExplorerProps<T> & WithStyles<CssRules>, DataExplorerState<T>> {
-    state: DataExplorerState<T> = {
-        contextMenu: {}
-    };
+class DataExplorer<T extends DataItem> extends React.Component<DataExplorerProps<T> & WithStyles<CssRules>> {
 
     render() {
         return <Paper>
-            <ContextMenu
-                anchorEl={this.state.contextMenu.anchorEl}
-                actions={this.props.contextActions}
-                onActionClick={this.callAction}
-                onClose={this.closeContextMenu} />
             <Toolbar className={this.props.classes.toolbar}>
                 <Grid container justify="space-between" wrap="nowrap" alignItems="center">
                     <div className={this.props.classes.searchBox}>
@@ -67,7 +49,7 @@ class DataExplorer<T extends DataItem> extends React.Component<DataExplorerProps
                 columns={[...this.props.columns, this.contextMenuColumn]}
                 items={this.props.items}
                 onRowClick={(_, item: T) => this.props.onRowClick(item)}
-                onRowContextMenu={this.openContextMenu}
+                onContextMenu={this.props.onContextMenu}
                 onFiltersChange={this.props.onFiltersChange}
                 onSortToggle={this.props.onSortToggle} />
             <Toolbar>
@@ -87,29 +69,6 @@ class DataExplorer<T extends DataItem> extends React.Component<DataExplorerProps
         </Paper>;
     }
 
-    openContextMenu = (event: React.MouseEvent<HTMLElement>, item: T) => {
-        event.preventDefault();
-        event.stopPropagation();
-        this.setState({
-            contextMenu: {
-                anchorEl: mockAnchorFromMouseEvent(event),
-                item
-            }
-        });
-    }
-
-    closeContextMenu = () => {
-        this.setState({ contextMenu: {} });
-    }
-
-    callAction = (action: ContextMenuAction) => {
-        const { item } = this.state.contextMenu;
-        this.closeContextMenu();
-        if (item) {
-            this.props.onContextAction(action, item);
-        }
-    }
-
     changePage = (event: React.MouseEvent<HTMLButtonElement> | null, page: number) => {
         this.props.onChangePage(page);
     }
@@ -120,21 +79,11 @@ class DataExplorer<T extends DataItem> extends React.Component<DataExplorerProps
 
     renderContextMenuTrigger = (item: T) =>
         <Grid container justify="flex-end">
-            <IconButton onClick={event => this.openContextMenuTrigger(event, item)}>
+            <IconButton onClick={event => this.props.onContextMenu(event, item)}>
                 <MoreVertIcon />
             </IconButton>
         </Grid>
 
-    openContextMenuTrigger = (event: React.MouseEvent<HTMLElement>, item: T) => {
-        event.preventDefault();
-        this.setState({
-            contextMenu: {
-                anchorEl: event.currentTarget,
-                item
-            }
-        });
-    }
-
     contextMenuColumn = {
         name: "Actions",
         selected: true,
index 6dbccb5e7d662222119103ff6155b57ffbcb6ae8..2ee350724a341510c8dac855ac6d5ac332c53230 100644 (file)
@@ -40,7 +40,7 @@ describe("<DataTable />", () => {
             items={[{ key: "1", name: "item 1" }] as MockItem[]}
             onFiltersChange={jest.fn()}
             onRowClick={jest.fn()}
-            onRowContextMenu={jest.fn()}
+            onContextMenu={jest.fn()}
             onSortToggle={jest.fn()} />);
         expect(dataTable.find(TableHead).find(TableCell)).toHaveLength(2);
     });
@@ -58,7 +58,7 @@ describe("<DataTable />", () => {
             items={[{ key: "1", name: "item 1" }] as MockItem[]}
             onFiltersChange={jest.fn()}
             onRowClick={jest.fn()}
-            onRowContextMenu={jest.fn()}
+            onContextMenu={jest.fn()}
             onSortToggle={jest.fn()} />);
         expect(dataTable.find(TableHead).find(TableCell).text()).toBe("Column 1");
     });
@@ -77,7 +77,7 @@ describe("<DataTable />", () => {
             items={[{ key: "1", name: "item 1" }] as MockItem[]}
             onFiltersChange={jest.fn()}
             onRowClick={jest.fn()}
-            onRowContextMenu={jest.fn()}
+            onContextMenu={jest.fn()}
             onSortToggle={jest.fn()} />);
         expect(dataTable.find(TableHead).find(TableCell).text()).toBe("Column Header");
     });
@@ -96,7 +96,7 @@ describe("<DataTable />", () => {
             items={[{ key: "1", name: "item 1" }] as MockItem[]}
             onFiltersChange={jest.fn()}
             onRowClick={jest.fn()}
-            onRowContextMenu={jest.fn()}
+            onContextMenu={jest.fn()}
             onSortToggle={jest.fn()} />);
         expect(dataTable.find(TableHead).find(TableCell).key()).toBe("column-1-key");
         expect(dataTable.find(TableBody).find(TableCell).key()).toBe("column-1-key");
@@ -120,7 +120,7 @@ describe("<DataTable />", () => {
             items={[{ key: "1", name: "item 1" }] as MockItem[]}
             onFiltersChange={jest.fn()}
             onRowClick={jest.fn()}
-            onRowContextMenu={jest.fn()}
+            onContextMenu={jest.fn()}
             onSortToggle={jest.fn()} />);
         expect(dataTable.find(TableBody).find(Typography).text()).toBe("item 1");
         expect(dataTable.find(TableBody).find(Button).text()).toBe("item 1");
@@ -139,7 +139,7 @@ describe("<DataTable />", () => {
             items={[{ key: "1", name: "item 1" }] as MockItem[]}
             onFiltersChange={jest.fn()}
             onRowClick={jest.fn()}
-            onRowContextMenu={jest.fn()}
+            onContextMenu={jest.fn()}
             onSortToggle={onSortToggle} />);
         expect(dataTable.find(TableSortLabel).prop("active")).toBeTruthy();
         dataTable.find(TableSortLabel).at(0).simulate("click");
@@ -160,8 +160,8 @@ describe("<DataTable />", () => {
             items={[{ key: "1", name: "item 1" }] as MockItem[]}
             onFiltersChange={onFiltersChange}
             onRowClick={jest.fn()}
-            onRowContextMenu={jest.fn()}
-            onSortToggle={jest.fn()} />);
+            onSortToggle={jest.fn()}
+            onContextMenu={jest.fn()} />);
         expect(dataTable.find(DataTableFilters).prop("filters")).toBe(columns[0].filters);
         dataTable.find(DataTableFilters).prop("onChange")([]);
         expect(onFiltersChange).toHaveBeenCalledWith([], columns[0]);
index 5372128f316613d41c9aa7758ba8b978dc97c5dc..854b322052a8612807093f07c2eb17f835be4e6c 100644 (file)
@@ -15,7 +15,7 @@ export interface DataTableProps<T> {
     items: T[];
     columns: DataColumns<T>;
     onRowClick: (event: React.MouseEvent<HTMLTableRowElement>, item: T) => void;
-    onRowContextMenu: (event: React.MouseEvent<HTMLTableRowElement>, item: T) => void;
+    onContextMenu: (event: React.MouseEvent<HTMLElement>, item: T) => void;
     onSortToggle: (column: DataColumn<T>) => void;
     onFiltersChange: (filters: DataTableFilterItem[], column: DataColumn<T>) => void;
 }
@@ -23,7 +23,8 @@ export interface DataTableProps<T> {
 class DataTable<T extends DataItem> extends React.Component<DataTableProps<T> & WithStyles<CssRules>> {
     render() {
         const { items, classes } = this.props;
-        return <div className={classes.tableContainer}>
+        return <div
+            className={classes.tableContainer}>
             <Table>
                 <TableHead>
                     <TableRow>
@@ -40,7 +41,7 @@ class DataTable<T extends DataItem> extends React.Component<DataTableProps<T> &
     renderHeadCell = (column: DataColumn<T>, index: number) => {
         const { name, key, renderHeader, filters, sortDirection } = column;
         const { onSortToggle, onFiltersChange } = this.props;
-        return <TableCell key={key || index} style={{width: column.width, minWidth: column.width}}>
+        return <TableCell key={key || index} style={{ width: column.width, minWidth: column.width }}>
             {renderHeader ?
                 renderHeader() :
                 filters
@@ -68,12 +69,12 @@ class DataTable<T extends DataItem> extends React.Component<DataTableProps<T> &
     }
 
     renderBodyRow = (item: T, index: number) => {
-        const { onRowClick, onRowContextMenu } = this.props;
+        const { onRowClick, onContextMenu } = this.props;
         return <TableRow
             hover
             key={item.key}
             onClick={event => onRowClick && onRowClick(event, item)}
-            onContextMenu={event => onRowContextMenu && onRowContextMenu(event, item)}>
+            onContextMenu={this.handleRowContextMenu(item)}>
             {this.mapVisibleColumns((column, index) => (
                 <TableCell key={column.key || index}>
                     {column.render(item)}
@@ -86,6 +87,10 @@ class DataTable<T extends DataItem> extends React.Component<DataTableProps<T> &
         return this.props.columns.filter(column => column.selected).map(fn);
     }
 
+    handleRowContextMenu = (item: T) =>
+        (event: React.MouseEvent<HTMLElement>) =>
+            this.props.onContextMenu(event, item)
+
 }
 
 type CssRules = "tableBody" | "tableContainer" | "noItemsInfo";
diff --git a/src/components/dialog-create/dialog-project-create.tsx b/src/components/dialog-create/dialog-project-create.tsx
new file mode 100644 (file)
index 0000000..dd8c7d1
--- /dev/null
@@ -0,0 +1,69 @@
+// Copyright (C) The Arvados Authors. All rights reserved.
+//
+// SPDX-License-Identifier: AGPL-3.0
+
+import * as React from 'react';
+import TextField from '@material-ui/core/TextField';
+import Dialog from '@material-ui/core/Dialog';
+import DialogActions from '@material-ui/core/DialogActions';
+import DialogContent from '@material-ui/core/DialogContent';
+import DialogTitle from '@material-ui/core/DialogTitle';
+import { Button, StyleRulesCallback, WithStyles, withStyles } from '@material-ui/core';
+
+interface ProjectCreateProps {
+  open: boolean;
+  handleClose: () => void;
+}
+
+const DialogProjectCreate: React.SFC<ProjectCreateProps & WithStyles<CssRules>> = ({ classes, open, handleClose }) => {
+  return (
+    <Dialog
+      open={open}
+      onClose={handleClose}>
+      <div className={classes.dialog}>
+        <DialogTitle id="form-dialog-title">Create a project</DialogTitle>
+        <DialogContent className={classes.dialogContent}>
+          <TextField
+            margin="dense"
+            className={classes.textField}
+            id="name"
+            label="Project name"
+            fullWidth />
+          <TextField
+            margin="dense"
+            id="description"
+            label="Description - optional"
+            fullWidth />
+        </DialogContent>
+        <DialogActions>
+          <Button onClick={handleClose} className={classes.button} color="primary">CANCEL</Button>
+          <Button onClick={handleClose} className={classes.lastButton} color="primary" variant="raised">CREATE A PROJECT</Button>
+        </DialogActions>
+      </div>
+    </Dialog>
+  );
+};
+
+type CssRules = "button" | "lastButton" | "dialogContent" | "textField" | "dialog";
+
+const styles: StyleRulesCallback<CssRules> = theme => ({
+  button: {
+    marginLeft: theme.spacing.unit
+  },
+  lastButton: {
+    marginLeft: theme.spacing.unit,
+    marginRight: "20px",
+  },
+  dialogContent: {
+    marginTop: "20px",
+  },
+  textField: {
+    marginBottom: "32px",
+  },
+  dialog: {
+    minWidth: "550px",
+    minHeight: "320px"
+  }
+});
+
+export default withStyles(styles)(DialogProjectCreate);
\ No newline at end of file
index cc191e808df6bd4770f9f60df1888cb0e7ad1671..a7783fb256c611f99fac80803b781c9884b74f8c 100644 (file)
@@ -27,6 +27,7 @@ interface SidePanelProps {
     toggleOpen: (id: string) => void;
     toggleActive: (id: string) => void;
     sidePanelItems: SidePanelItem[];
+    onContextMenu: (event: React.MouseEvent<HTMLElement>, item: SidePanelItem) => void;
 }
 
 class SidePanel extends React.Component<SidePanelProps & WithStyles<CssRules>> {
@@ -38,7 +39,7 @@ class SidePanel extends React.Component<SidePanelProps & WithStyles<CssRules>> {
                 <List>
                     {sidePanelItems.map(it => (
                         <span key={it.name}>
-                            <ListItem button className={list} onClick={() => toggleActive(it.id)}>
+                            <ListItem button className={list} onClick={() => toggleActive(it.id)} onContextMenu={this.handleRowContextMenu(it)}>
                                 <span className={row}>
                                     {it.openAble ? <i onClick={() => toggleOpen(it.id)} className={`${it.active ? activeArrow : inactiveArrow} 
                                         ${it.open ? `fas fa-caret-down ${arrowTransition}` : `fas fa-caret-down ${arrowRotate}`}`} /> : null}
@@ -58,6 +59,11 @@ class SidePanel extends React.Component<SidePanelProps & WithStyles<CssRules>> {
             </div>
         );
     }
+
+    handleRowContextMenu = (item: SidePanelItem) =>
+        (event: React.MouseEvent<HTMLElement>) =>
+            item.openAble ? this.props.onContextMenu(event, item) : null
+
 }
 
 type CssRules = 'active' | 'listItemText' | 'row' | 'leftSidePanelContainer' | 'list' | 'icon' | 'projectIconMargin' |
index 2c19a831ecd1154bfe7de3746787a6b3d6641eb3..8de9bda5970ffe7d495f94f24b51229aacf9c1df 100644 (file)
@@ -32,25 +32,18 @@ interface TreeProps<T> {
     toggleItemOpen: (id: string, status: TreeItemStatus) => void;
     toggleItemActive: (id: string, status: TreeItemStatus) => void;
     level?: number;
+    onContextMenu: (event: React.MouseEvent<HTMLElement>, item: TreeItem<T>) => void;
 }
 
 class Tree<T> extends React.Component<TreeProps<T> & WithStyles<CssRules>, {}> {
-    renderArrow(status: TreeItemStatus, arrowClass: string, open: boolean, id: string) {
-        const { arrowTransition, arrowVisibility, arrowRotate } = this.props.classes;
-        return <i onClick={() => this.props.toggleItemOpen(id, status)}
-            className={`
-                    ${arrowClass} 
-                    ${status === TreeItemStatus.Pending ? arrowVisibility : ''} 
-                    ${open ? `fas fa-caret-down ${arrowTransition}` : `fas fa-caret-down ${arrowRotate}`}`} />;
-    }
     render(): ReactElement<any> {
         const level = this.props.level ? this.props.level : 0;
-        const { classes, render, toggleItemOpen, items, toggleItemActive } = this.props;
+        const { classes, render, toggleItemOpen, items, toggleItemActive, onContextMenu } = this.props;
         const { list, inactiveArrow, activeArrow, loader } = classes;
         return <List component="div" className={list}>
             {items && items.map((it: TreeItem<T>, idx: number) =>
                 <div key={`item/${level}/${idx}`}>
-                    <ListItem button className={list} style={{ paddingLeft: (level + 1) * 20 }} onClick={() => toggleItemActive(it.id, it.status)}>
+                    <ListItem button className={list} style={{ paddingLeft: (level + 1) * 20 }} onClick={() => toggleItemActive(it.id, it.status)} onContextMenu={this.handleRowContextMenu(it)}>
                         {it.status === TreeItemStatus.Pending ? <CircularProgress size={10} className={loader} /> : null}
                         {it.toggled && it.items && it.items.length === 0 ? null : this.renderArrow(it.status, it.active ? activeArrow : inactiveArrow, it.open, it.id)}
                         {render(it, level)}
@@ -62,11 +55,24 @@ class Tree<T> extends React.Component<TreeProps<T> & WithStyles<CssRules>, {}> {
                                 render={render}
                                 toggleItemOpen={toggleItemOpen}
                                 toggleItemActive={toggleItemActive}
-                                level={level + 1} />
+                                level={level + 1}
+                                onContextMenu={onContextMenu} />
                         </Collapse>}
                 </div>)}
         </List>;
     }
+    renderArrow(status: TreeItemStatus, arrowClass: string, open: boolean, id: string) {
+        const { arrowTransition, arrowVisibility, arrowRotate } = this.props.classes;
+        return <i onClick={() => this.props.toggleItemOpen(id, status)}
+            className={`
+                    ${arrowClass} 
+                    ${status === TreeItemStatus.Pending ? arrowVisibility : ''} 
+                    ${open ? `fas fa-caret-down ${arrowTransition}` : `fas fa-caret-down ${arrowRotate}`}`} />;
+    }
+
+    handleRowContextMenu = (item: TreeItem<T>) =>
+        (event: React.MouseEvent<HTMLElement>) =>
+            this.props.onContextMenu(event, item)
 }
 
 type CssRules = 'list' | 'activeArrow' | 'inactiveArrow' | 'arrowRotate' | 'arrowTransition' | 'loader' | 'arrowVisibility';
index f89bc65e9bd37db0a7f087bea34e27d6085f4a30..2864cefe17fd5e1e8f6de095605bb87b20dab6ee 100644 (file)
@@ -14,15 +14,14 @@ import { ContextMenuAction, ContextMenuActionGroup } from "../../components/cont
 
 interface Props {
     id: string;
-    contextActions: ContextMenuActionGroup[];
     onRowClick: (item: any) => void;
-    onContextAction: (action: ContextMenuAction, item: any) => void;
+    onContextMenu: (event: React.MouseEvent<HTMLElement>, item: any) => void;
 }
 
-const mapStateToProps = (state: RootState, { id, contextActions }: Props) =>
+const mapStateToProps = (state: RootState, { id }: Props) =>
     getDataExplorer(state.dataExplorer, id);
 
-const mapDispatchToProps = (dispatch: Dispatch, { id, contextActions, onRowClick, onContextAction }: Props) => ({
+const mapDispatchToProps = (dispatch: Dispatch, { id, onRowClick, onContextMenu }: Props) => ({
     onSearch: (searchValue: string) => {
         dispatch(actions.SET_SEARCH_VALUE({ id, searchValue }));
     },
@@ -47,11 +46,9 @@ const mapDispatchToProps = (dispatch: Dispatch, { id, contextActions, onRowClick
         dispatch(actions.SET_ROWS_PER_PAGE({ id, rowsPerPage }));
     },
 
-    contextActions,
-
     onRowClick,
 
-    onContextAction
+    onContextMenu
 });
 
 export default connect(mapStateToProps, mapDispatchToProps)(DataExplorer);
index 1230e3b7db60abe9901db66fa28a310a6737c804..d2082395125e740e1cff707293d4e7f8d081740b 100644 (file)
@@ -35,6 +35,7 @@ export interface MainAppBarActionProps {
     onSearch: (searchText: string) => void;
     onBreadcrumbClick: (breadcrumb: Breadcrumb) => void;
     onMenuItemClick: (menuItem: MainAppBarMenuItem) => void;
+    onContextMenu: (event: React.MouseEvent<HTMLElement>, breadcrumb: Breadcrumb) => void;
     onDetailsPanelToggle: () => void;
 }
 
@@ -70,7 +71,10 @@ export const MainAppBar: React.SFC<MainAppBarProps> = (props) => {
         </Toolbar>
         <Toolbar >
             {
-                props.user && <Breadcrumbs items={props.breadcrumbs} onClick={props.onBreadcrumbClick} />
+                props.user && <Breadcrumbs
+                    items={props.breadcrumbs}
+                    onClick={props.onBreadcrumbClick}
+                    onContextMenu={props.onContextMenu} />
             }
             <IconButton color="inherit" onClick={props.onDetailsPanelToggle}>
                 <InfoIcon />
index f51b65e054df7f8ab2d69a263e658f9a1fe2a7d8..511cbbbc7380198e12eec9f6f7f48bcffd99efae 100644 (file)
@@ -16,15 +16,17 @@ export interface ProjectTreeProps {
     projects: Array<TreeItem<Project>>;
     toggleOpen: (id: string, status: TreeItemStatus) => void;
     toggleActive: (id: string, status: TreeItemStatus) => void;
+    onContextMenu: (event: React.MouseEvent<HTMLElement>, item: TreeItem<Project>) => void;
 }
 
 class ProjectTree<T> extends React.Component<ProjectTreeProps & WithStyles<CssRules>> {
     render(): ReactElement<any> {
-        const { classes, projects, toggleOpen, toggleActive } = this.props;
+        const { classes, projects, toggleOpen, toggleActive, onContextMenu } = this.props;
         const { active, listItemText, row, treeContainer } = classes;
         return (
             <div className={treeContainer}>
                 <Tree items={projects}
+                    onContextMenu={onContextMenu}
                     toggleItemOpen={toggleOpen}
                     toggleItemActive={toggleActive}
                     render={(project: TreeItem<Project>) =>
index c1d66603405d9de7774c0aaca8056fd3f8e855bf..f1b82357ec524de9abe55b76add0dde639350bc6 100644 (file)
@@ -7,7 +7,6 @@ import { ProjectPanelItem } from './project-panel-item';
 import { Grid, Typography, Button, Toolbar, StyleRulesCallback, WithStyles, withStyles } from '@material-ui/core';
 import { formatDate, formatFileSize } from '../../common/formatters';
 import DataExplorer from "../../views-components/data-explorer/data-explorer";
-import { ContextMenuAction } from '../../components/context-menu/context-menu';
 import { DispatchProp, connect } from 'react-redux';
 import { DataColumns } from '../../components/data-table/data-table';
 import { RouteComponentProps } from 'react-router';
@@ -26,12 +25,15 @@ export interface ProjectPanelFilter extends DataTableFilterItem {
 type ProjectPanelProps = {
     currentItemId: string,
     onItemClick: (item: ProjectPanelItem) => void,
-    onItemRouteChange: (itemId: string) => void
+    onItemRouteChange: (itemId: string) => void,
+    onContextMenu: (event: React.MouseEvent<HTMLElement>, item: ProjectPanelItem) => void;
+    onDialogOpen: () => void;
 }
     & DispatchProp
     & WithStyles<CssRules>
     & RouteComponentProps<{ id: string }>;
-class ProjectPanel extends React.Component<ProjectPanelProps> {
+
+class ProjectPanel extends React.Component<ProjectPanelProps> {    
     render() {
         return <div>
             <div className={this.props.classes.toolbar}>
@@ -41,15 +43,14 @@ class ProjectPanel extends React.Component<ProjectPanelProps> {
                 <Button color="primary" variant="raised" className={this.props.classes.button}>
                     Run a process
                 </Button>
-                <Button color="primary" variant="raised" className={this.props.classes.button}>
-                    Create a project
+                <Button color="primary" onClick={this.props.onDialogOpen} variant="raised" className={this.props.classes.button}>
+                    New project
                 </Button>
             </div>
             <DataExplorer
                 id={PROJECT_PANEL_ID}
-                contextActions={contextMenuActions}
                 onRowClick={this.props.onItemClick}
-                onContextAction={this.executeAction} />;
+                onContextMenu={this.props.onContextMenu} />;
         </div>;
     }
 
@@ -58,11 +59,6 @@ class ProjectPanel extends React.Component<ProjectPanelProps> {
             this.props.onItemRouteChange(match.params.id);
         }
     }
-
-    executeAction = (action: ContextMenuAction, item: ProjectPanelItem) => {
-        alert(`Executing ${action.name} on ${item.name}`);
-    }
-
 }
 
 type CssRules = "toolbar" | "button";
@@ -74,7 +70,7 @@ const styles: StyleRulesCallback<CssRules> = theme => ({
     },
     button: {
         marginLeft: theme.spacing.unit
-    }
+    },
 });
 
 const renderName = (item: ProjectPanelItem) =>
@@ -218,29 +214,6 @@ export const columns: DataColumns<ProjectPanelItem, ProjectPanelFilter> = [{
     width: "150px"
 }];
 
-const contextMenuActions = [[{
-    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"
-}
-]];
 
 export default withStyles(styles)(
     connect((state: RootState) => ({ currentItemId: state.projects.currentItemId }))(
index 8cc5fc22700571a207395746390d8e778ad4e1f8..9c1336c0d074884edf590ae550b4606718fe16ac 100644 (file)
@@ -6,9 +6,8 @@ import * as React from 'react';
 import { StyleRulesCallback, WithStyles, withStyles } from '@material-ui/core/styles';
 import Drawer from '@material-ui/core/Drawer';
 import { connect, DispatchProp } from "react-redux";
-import { Route, Switch, RouteComponentProps, withRouter } from "react-router";
+import { Route, Switch, RouteComponentProps } from "react-router";
 import authActions from "../../store/auth/auth-action";
-import dataExplorerActions from "../../store/data-explorer/data-explorer-action";
 import { User } from "../../models/user";
 import { RootState } from "../../store/store";
 import MainAppBar, {
@@ -20,16 +19,17 @@ import { push } from 'react-router-redux';
 import ProjectTree from '../../views-components/project-tree/project-tree';
 import { TreeItem } from "../../components/tree/tree";
 import { Project } from "../../models/project";
-import { getTreePath, findTreeItem } from '../../store/project/project-reducer';
+import { getTreePath } from '../../store/project/project-reducer';
 import sidePanelActions from '../../store/side-panel/side-panel-action';
 import SidePanel, { SidePanelItem } from '../../components/side-panel/side-panel';
-import { ResourceKind } from "../../models/resource";
 import { ItemMode, setProjectItem } from "../../store/navigation/navigation-action";
 import projectActions from "../../store/project/project-action";
 import ProjectPanel from "../project-panel/project-panel";
-import { sidePanelData } from '../../store/side-panel/side-panel-reducer';
 import DetailsPanel from '../../views-components/details-panel/details-panel';
 import { ArvadosTheme } from '../../common/custom-theme';
+import ContextMenu, { ContextMenuAction } from '../../components/context-menu/context-menu';
+import { mockAnchorFromMouseEvent } from '../../components/popover/helpers';
+import DialogProjectCreate from '../../components/dialog-create/dialog-project-create';
 
 const drawerWidth = 240;
 const appBarHeight = 100;
@@ -93,6 +93,10 @@ interface NavMenuItem extends MainAppBarMenuItem {
 }
 
 interface WorkbenchState {
+    contextMenu: {
+        anchorEl?: HTMLElement;
+    };
+    isCreationDialogOpen: boolean;
     anchorEl: any;
     searchText: string;
     menuItems: {
@@ -105,6 +109,10 @@ interface WorkbenchState {
 
 class Workbench extends React.Component<WorkbenchProps, WorkbenchState> {
     state = {
+        contextMenu: {
+            anchorEl: undefined
+        },
+        isCreationDialogOpen: false,
         anchorEl: null,
         searchText: "",
         breadcrumbs: [],
@@ -146,6 +154,9 @@ class Workbench extends React.Component<WorkbenchProps, WorkbenchState> {
         onMenuItemClick: (menuItem: NavMenuItem) => menuItem.action(),
         onDetailsPanelToggle: () => {
             this.setState(prev => ({ isDetailsPanelOpened: !prev.isDetailsPanelOpened }));
+        },
+        onContextMenu: (event: React.MouseEvent<HTMLElement>, breadcrumb: Breadcrumb) => {
+            this.openContextMenu(event, breadcrumb);
         }
     };
 
@@ -158,6 +169,28 @@ class Workbench extends React.Component<WorkbenchProps, WorkbenchState> {
         this.props.dispatch(projectActions.RESET_PROJECT_TREE_ACTIVITY(itemId));
     }
 
+    handleCreationDialogOpen = () => {
+        this.closeContextMenu();
+        this.setState({ isCreationDialogOpen: true });
+    }
+
+    handleCreationDialogClose = () => {
+        this.setState({ isCreationDialogOpen: false });
+    }
+
+    openContextMenu = (event: React.MouseEvent<HTMLElement>, item: any) => {
+        event.preventDefault();
+        this.setState({ contextMenu: { anchorEl: mockAnchorFromMouseEvent(event) } });
+        console.log(item);
+    }
+
+    closeContextMenu = () => {
+        this.setState({ contextMenu: {} });
+    }
+
+    openCreateDialog = (item: ContextMenuAction) =>
+        item.openCreateDialog ? this.handleCreationDialogOpen() : void 0
+
     render() {
         const path = getTreePath(this.props.projects, this.props.currentProjectId);
         const breadcrumbs = path.map(item => ({
@@ -175,8 +208,7 @@ class Workbench extends React.Component<WorkbenchProps, WorkbenchState> {
                         searchText={this.state.searchText}
                         user={this.props.user}
                         menuItems={this.state.menuItems}
-                        {...this.mainAppBarActions}
-                    />
+                        {...this.mainAppBarActions} />
                 </div>
                 {user &&
                     <Drawer
@@ -188,12 +220,13 @@ class Workbench extends React.Component<WorkbenchProps, WorkbenchState> {
                         <SidePanel
                             toggleOpen={this.toggleSidePanelOpen}
                             toggleActive={this.toggleSidePanelActive}
-                            sidePanelItems={this.props.sidePanelItems}>
+                            sidePanelItems={this.props.sidePanelItems}
+                            onContextMenu={this.openContextMenu}>
                             <ProjectTree
                                 projects={this.props.projects}
                                 toggleOpen={itemId => this.props.dispatch<any>(setProjectItem(itemId, ItemMode.OPEN))}
                                 toggleActive={itemId => this.props.dispatch<any>(setProjectItem(itemId, ItemMode.ACTIVE))}
-                            />
+                                onContextMenu={this.openContextMenu} />
                         </SidePanel>
                     </Drawer>}
                 <main className={classes.contentWrapper}>
@@ -202,10 +235,16 @@ class Workbench extends React.Component<WorkbenchProps, WorkbenchState> {
                             <Route path="/projects/:id" render={this.renderProjectPanel} />
                         </Switch>
                     </div>
-                    <DetailsPanel 
-                        isOpened={this.state.isDetailsPanelOpened} 
+                    <DetailsPanel
+                        isOpened={this.state.isDetailsPanelOpened}
                         onCloseDrawer={this.mainAppBarActions.onDetailsPanelToggle} />
                 </main>
+                <ContextMenu
+                    anchorEl={this.state.contextMenu.anchorEl}
+                    actions={contextMenuActions}
+                    onActionClick={this.openCreateDialog}
+                    onClose={this.closeContextMenu} />
+                <DialogProjectCreate open={this.state.isCreationDialogOpen} handleClose={this.handleCreationDialogClose} />
             </div>
         );
     }
@@ -213,9 +252,38 @@ class Workbench extends React.Component<WorkbenchProps, WorkbenchState> {
     renderProjectPanel = (props: RouteComponentProps<{ id: string }>) => <ProjectPanel
         onItemRouteChange={itemId => this.props.dispatch<any>(setProjectItem(itemId, ItemMode.ACTIVE))}
         onItemClick={item => this.props.dispatch<any>(setProjectItem(item.uuid, ItemMode.ACTIVE))}
+        onContextMenu={this.openContextMenu}
+        onDialogOpen={this.handleCreationDialogOpen}
         {...props} />
+}
 
+const contextMenuActions = [[{
+    icon: "fas fa-plus fa-fw",
+    name: "New project",
+    openCreateDialog: true
+}, {
+    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"
 }
+]];
 
 export default connect<WorkbenchDataProps>(
     (state: RootState) => ({