From: Michal Klobukowski Date: Tue, 10 Jul 2018 13:20:15 +0000 (+0200) Subject: Resolve merge conflicts X-Git-Url: https://git.arvados.org/arvados.git/commitdiff_plain/f5e55e7a71f2fc2390d392af752c61b4d3135cb6?hp=808b1f59d53a83c9ad456931c7e38f127b4d8342 Resolve merge conflicts Feature #13694 Arvados-DCO-1.1-Signed-off-by: Michal Klobukowski --- diff --git a/src/components/breadcrumbs/breadcrumbs.test.tsx b/src/components/breadcrumbs/breadcrumbs.test.tsx index b525554a2c..ef3f888797 100644 --- a/src/components/breadcrumbs/breadcrumbs.test.tsx +++ b/src/components/breadcrumbs/breadcrumbs.test.tsx @@ -24,7 +24,7 @@ describe("", () => { const items = [ { label: 'breadcrumb 1' } ]; - const breadcrumbs = mount(); + const breadcrumbs = mount(); expect(breadcrumbs.find(Button)).toHaveLength(1); expect(breadcrumbs.find(ChevronRightIcon)).toHaveLength(0); }); @@ -34,7 +34,7 @@ describe("", () => { { label: 'breadcrumb 1' }, { label: 'breadcrumb 2' } ]; - const breadcrumbs = mount(); + const breadcrumbs = mount(); expect(breadcrumbs.find(Button)).toHaveLength(2); expect(breadcrumbs.find(ChevronRightIcon)).toHaveLength(1); }); @@ -44,7 +44,7 @@ describe("", () => { { label: 'breadcrumb 1' }, { label: 'breadcrumb 2' } ]; - const breadcrumbs = mount(); + const breadcrumbs = mount(); breadcrumbs.find(Button).at(1).simulate('click'); expect(onClick).toBeCalledWith(items[1]); }); diff --git a/src/components/breadcrumbs/breadcrumbs.tsx b/src/components/breadcrumbs/breadcrumbs.tsx index 41f71981e5..4868e137f9 100644 --- a/src/components/breadcrumbs/breadcrumbs.tsx +++ b/src/components/breadcrumbs/breadcrumbs.tsx @@ -14,9 +14,10 @@ export interface Breadcrumb { interface BreadcrumbsProps { items: Breadcrumb[]; onClick: (breadcrumb: Breadcrumb) => void; + onContextMenu: (event: React.MouseEvent, breadcrumb: Breadcrumb) => void; } -const Breadcrumbs: React.SFC> = ({ classes, onClick, items }) => { +const Breadcrumbs: React.SFC> = ({ classes, onClick, onContextMenu, items }) => { return { items.map((item, index) => { @@ -28,12 +29,11 @@ const Breadcrumbs: React.SFC> = ({ class color="inherit" className={isLastItem ? classes.currentItem : classes.item} onClick={() => onClick(item)} - > + onContextMenu={event => onContextMenu(event, item)}> + className={classes.label}> {item.label} diff --git a/src/components/context-menu/context-menu.tsx b/src/components/context-menu/context-menu.tsx index 6ac1207b9b..c892ba2616 100644 --- a/src/components/context-menu/context-menu.tsx +++ b/src/components/context-menu/context-menu.tsx @@ -8,6 +8,7 @@ import { DefaultTransformOrigin } from "../popover/helpers"; export interface ContextMenuAction { name: string; icon: string; + openCreateDialog?: boolean; } export type ContextMenuActionGroup = ContextMenuAction[]; diff --git a/src/components/data-explorer/data-explorer.test.tsx b/src/components/data-explorer/data-explorer.test.tsx index 33899c00c7..97b1bec602 100644 --- a/src/components/data-explorer/data-explorer.test.tsx +++ b/src/components/data-explorer/data-explorer.test.tsx @@ -22,8 +22,6 @@ describe("", () => { const onContextAction = jest.fn(); const dataExplorer = mount(); expect(dataExplorer.find(ContextMenu).prop("actions")).toEqual([]); @@ -54,7 +52,6 @@ describe("", () => { {...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 diff --git a/src/components/data-explorer/data-explorer.tsx b/src/components/data-explorer/data-explorer.tsx index 9b099acb2c..9085b1f1f0 100644 --- a/src/components/data-explorer/data-explorer.tsx +++ b/src/components/data-explorer/data-explorer.tsx @@ -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 { items: T[]; itemsAvailable: number; columns: DataColumns; - contextActions: ContextMenuActionGroup[]; searchValue: string; rowsPerPage: number; rowsPerPageOptions?: number[]; @@ -26,32 +23,17 @@ interface DataExplorerProps { onRowClick: (item: T) => void; onRowDoubleClick: (item: T) => void; onColumnToggle: (column: DataColumn) => void; - onContextAction: (action: ContextMenuAction, item: T) => void; + onContextMenu: (event: React.MouseEvent, item: T) => void; onSortToggle: (column: DataColumn) => void; onFiltersChange: (filters: DataTableFilterItem[], column: DataColumn) => void; onChangePage: (page: number) => void; onChangeRowsPerPage: (rowsPerPage: number) => void; } -interface DataExplorerState { - contextMenu: { - anchorEl?: HTMLElement; - item?: T; - }; -} - -class DataExplorer extends React.Component & WithStyles, DataExplorerState> { - state: DataExplorerState = { - contextMenu: {} - }; +class DataExplorer extends React.Component & WithStyles> { render() { return -
@@ -68,8 +50,8 @@ class DataExplorer extends React.Component this.props.onRowClick(item)} + onContextMenu={this.props.onContextMenu} onRowDoubleClick={(_, item: T) => this.props.onRowDoubleClick(item)} - onRowContextMenu={this.openContextMenu} onFiltersChange={this.props.onFiltersChange} onSortToggle={this.props.onSortToggle} /> @@ -89,29 +71,6 @@ class DataExplorer extends React.Component; } - openContextMenu = (event: React.MouseEvent, 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 | null, page: number) => { this.props.onChangePage(page); } @@ -122,21 +81,11 @@ class DataExplorer extends React.Component - this.openContextMenuTrigger(event, item)}> + this.props.onContextMenu(event, item)}> - openContextMenuTrigger = (event: React.MouseEvent, item: T) => { - event.preventDefault(); - this.setState({ - contextMenu: { - anchorEl: event.currentTarget, - item - } - }); - } - contextMenuColumn = { name: "Actions", selected: true, diff --git a/src/components/data-table/data-table.test.tsx b/src/components/data-table/data-table.test.tsx index 6dbccb5e7d..2ee350724a 100644 --- a/src/components/data-table/data-table.test.tsx +++ b/src/components/data-table/data-table.test.tsx @@ -40,7 +40,7 @@ describe("", () => { 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("", () => { 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("", () => { 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("", () => { 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("", () => { 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("", () => { 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("", () => { 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]); diff --git a/src/components/data-table/data-table.tsx b/src/components/data-table/data-table.tsx index c657e11671..8f4ffc6fab 100644 --- a/src/components/data-table/data-table.tsx +++ b/src/components/data-table/data-table.tsx @@ -15,8 +15,8 @@ export interface DataTableProps { items: T[]; columns: DataColumns; onRowClick: (event: React.MouseEvent, item: T) => void; + onContextMenu: (event: React.MouseEvent, item: T) => void; onRowDoubleClick: (event: React.MouseEvent, item: T) => void; - onRowContextMenu: (event: React.MouseEvent, item: T) => void; onSortToggle: (column: DataColumn) => void; onFiltersChange: (filters: DataTableFilterItem[], column: DataColumn) => void; } @@ -24,7 +24,8 @@ export interface DataTableProps { class DataTable extends React.Component & WithStyles> { render() { const { items, classes } = this.props; - return
+ return
@@ -41,7 +42,7 @@ class DataTable extends React.Component & renderHeadCell = (column: DataColumn, index: number) => { const { name, key, renderHeader, filters, sortDirection } = column; const { onSortToggle, onFiltersChange } = this.props; - return + return {renderHeader ? renderHeader() : filters @@ -69,13 +70,13 @@ class DataTable extends React.Component & } renderBodyRow = (item: T, index: number) => { - const { onRowClick, onRowDoubleClick, onRowContextMenu } = this.props; + const { onRowClick, onRowDoubleClick, onContextMenu } = this.props; return onRowClick && onRowClick(event, item)} - onDoubleClick={event => onRowDoubleClick && onRowDoubleClick(event, item) } - onContextMenu={event => onRowContextMenu && onRowContextMenu(event, item)}> + onContextMenu={this.handleRowContextMenu(item)} + onDoubleClick={event => onRowDoubleClick && onRowDoubleClick(event, item) }> {this.mapVisibleColumns((column, index) => ( {column.render(item)} @@ -88,6 +89,10 @@ class DataTable extends React.Component & return this.props.columns.filter(column => column.selected).map(fn); } + handleRowContextMenu = (item: T) => + (event: React.MouseEvent) => + this.props.onContextMenu(event, item) + } type CssRules = "tableBody" | "tableContainer" | "noItemsInfo"; diff --git a/src/components/side-panel/side-panel.tsx b/src/components/side-panel/side-panel.tsx index cc191e808d..a7783fb256 100644 --- a/src/components/side-panel/side-panel.tsx +++ b/src/components/side-panel/side-panel.tsx @@ -27,6 +27,7 @@ interface SidePanelProps { toggleOpen: (id: string) => void; toggleActive: (id: string) => void; sidePanelItems: SidePanelItem[]; + onContextMenu: (event: React.MouseEvent, item: SidePanelItem) => void; } class SidePanel extends React.Component> { @@ -38,7 +39,7 @@ class SidePanel extends React.Component> { {sidePanelItems.map(it => ( - toggleActive(it.id)}> + toggleActive(it.id)} onContextMenu={this.handleRowContextMenu(it)}> {it.openAble ? 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> { ); } + + handleRowContextMenu = (item: SidePanelItem) => + (event: React.MouseEvent) => + item.openAble ? this.props.onContextMenu(event, item) : null + } type CssRules = 'active' | 'listItemText' | 'row' | 'leftSidePanelContainer' | 'list' | 'icon' | 'projectIconMargin' | diff --git a/src/components/tree/tree.tsx b/src/components/tree/tree.tsx index 2c19a831ec..8de9bda597 100644 --- a/src/components/tree/tree.tsx +++ b/src/components/tree/tree.tsx @@ -32,25 +32,18 @@ interface TreeProps { toggleItemOpen: (id: string, status: TreeItemStatus) => void; toggleItemActive: (id: string, status: TreeItemStatus) => void; level?: number; + onContextMenu: (event: React.MouseEvent, item: TreeItem) => void; } class Tree extends React.Component & WithStyles, {}> { - renderArrow(status: TreeItemStatus, arrowClass: string, open: boolean, id: string) { - const { arrowTransition, arrowVisibility, arrowRotate } = this.props.classes; - return this.props.toggleItemOpen(id, status)} - className={` - ${arrowClass} - ${status === TreeItemStatus.Pending ? arrowVisibility : ''} - ${open ? `fas fa-caret-down ${arrowTransition}` : `fas fa-caret-down ${arrowRotate}`}`} />; - } render(): ReactElement { 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 {items && items.map((it: TreeItem, idx: number) =>
- toggleItemActive(it.id, it.status)}> + toggleItemActive(it.id, it.status)} onContextMenu={this.handleRowContextMenu(it)}> {it.status === TreeItemStatus.Pending ? : 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 extends React.Component & WithStyles, {}> { render={render} toggleItemOpen={toggleItemOpen} toggleItemActive={toggleItemActive} - level={level + 1} /> + level={level + 1} + onContextMenu={onContextMenu} /> }
)}
; } + renderArrow(status: TreeItemStatus, arrowClass: string, open: boolean, id: string) { + const { arrowTransition, arrowVisibility, arrowRotate } = this.props.classes; + return 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) => + (event: React.MouseEvent) => + this.props.onContextMenu(event, item) } type CssRules = 'list' | 'activeArrow' | 'inactiveArrow' | 'arrowRotate' | 'arrowTransition' | 'loader' | 'arrowVisibility'; diff --git a/src/store/project/project-action.ts b/src/store/project/project-action.ts index 5169349693..c1a002f98a 100644 --- a/src/store/project/project-action.ts +++ b/src/store/project/project-action.ts @@ -8,9 +8,15 @@ import { projectService } from "../../services/services"; import { Dispatch } from "redux"; import { getResourceKind } from "../../models/resource"; import FilterBuilder from "../../common/api/filter-builder"; +import { ThunkAction } from "../../../node_modules/redux-thunk"; +import { RootState } from "../store"; const actions = unionize({ - CREATE_PROJECT: ofType(), + OPEN_PROJECT_CREATOR: ofType<{ ownerUuid: string }>(), + CLOSE_PROJECT_CREATOR: ofType<{}>(), + CREATE_PROJECT: ofType>(), + CREATE_PROJECT_SUCCESS: ofType(), + CREATE_PROJECT_ERROR: ofType(), REMOVE_PROJECT: ofType(), PROJECTS_REQUEST: ofType(), PROJECTS_SUCCESS: ofType<{ projects: Project[], parentItemId?: string }>(), @@ -38,5 +44,16 @@ export const getProjectList = (parentUuid: string = '') => (dispatch: Dispatch) }); }; +export const createProject = (project: Partial) => + (dispatch: Dispatch, getState: () => RootState) => { + const { ownerUuid } = getState().projects.creator; + const projectData = { ownerUuid, ...project }; + dispatch(actions.CREATE_PROJECT(projectData)); + return projectService + .create(projectData) + .then(project => dispatch(actions.CREATE_PROJECT_SUCCESS(project))) + .catch(() => dispatch(actions.CREATE_PROJECT_ERROR("Could not create a project"))); + }; + export type ProjectAction = UnionOf; export default actions; diff --git a/src/store/project/project-reducer.test.ts b/src/store/project/project-reducer.test.ts index c80f18c82f..0862142d48 100644 --- a/src/store/project/project-reducer.test.ts +++ b/src/store/project/project-reducer.test.ts @@ -76,7 +76,8 @@ describe('project-reducer', () => { active: true, status: 1 }], - currentItemId: "1" + currentItemId: "1", + creator: { opened: false, pending: false, ownerUuid: "" }, }; const project = { items: [{ @@ -118,7 +119,8 @@ describe('project-reducer', () => { active: false, status: 1 }], - currentItemId: "1" + currentItemId: "1", + creator: { opened: false, pending: false, ownerUuid: "" } }; const project = { items: [{ @@ -137,7 +139,8 @@ describe('project-reducer', () => { status: 1, toggled: true }], - currentItemId: "1" + currentItemId: "1", + creator: { opened: false, pending: false, ownerUuid: "" }, }; const state = projectsReducer(initialState, actions.TOGGLE_PROJECT_TREE_ITEM_ACTIVE(initialState.items[0].id)); @@ -163,7 +166,8 @@ describe('project-reducer', () => { status: 1, toggled: false, }], - currentItemId: "1" + currentItemId: "1", + creator: { opened: false, pending: false, ownerUuid: "" } }; const project = { items: [{ @@ -182,7 +186,8 @@ describe('project-reducer', () => { status: 1, toggled: true }], - currentItemId: "1" + currentItemId: "1", + creator: { opened: false, pending: false, ownerUuid: "" }, }; const state = projectsReducer(initialState, actions.TOGGLE_PROJECT_TREE_ITEM_OPEN(initialState.items[0].id)); diff --git a/src/store/project/project-reducer.ts b/src/store/project/project-reducer.ts index efef809921..8ee9e9f916 100644 --- a/src/store/project/project-reducer.ts +++ b/src/store/project/project-reducer.ts @@ -10,9 +10,16 @@ import { TreeItem, TreeItemStatus } from "../../components/tree/tree"; export type ProjectState = { items: Array>, - currentItemId: string + currentItemId: string, + creator: ProjectCreator }; +interface ProjectCreator { + opened: boolean; + pending: boolean; + ownerUuid: string; +} + export function findTreeItem(tree: Array>, itemId: string): TreeItem | undefined { let item; for (const t of tree) { @@ -40,12 +47,12 @@ export function getActiveTreeItem(tree: Array>): TreeItem | un } export function getTreePath(tree: Array>, itemId: string): Array> { - for (const item of tree){ - if(item.id === itemId){ + for (const item of tree) { + if (item.id === itemId) { return [item]; } else { const branch = getTreePath(item.items || [], itemId); - if(branch.length > 0){ + if (branch.length > 0) { return [item, ...branch]; } } @@ -85,20 +92,32 @@ function updateProjectTree(tree: Array>, projects: Project[], return items; } -const projectsReducer = (state: ProjectState = { items: [], currentItemId: "" }, action: ProjectAction) => { +const updateCreator = (state: ProjectState, creator: Partial) => ({ + ...state, + creator: { + ...state.creator, + ...creator + } +}); + +const initialState: ProjectState = { + items: [], + currentItemId: "", + creator: { + opened: false, + pending: false, + ownerUuid: "" + } +}; + + +const projectsReducer = (state: ProjectState = initialState, action: ProjectAction) => { return actions.match(action, { - CREATE_PROJECT: project => ({ - ...state, - items: state.items.concat({ - id: project.uuid, - open: false, - active: false, - status: TreeItemStatus.Loaded, - toggled: false, - items: [], - data: project - }) - }), + OPEN_PROJECT_CREATOR: ({ ownerUuid }) => updateCreator(state, { ownerUuid, opened: true, pending: false }), + CLOSE_PROJECT_CREATOR: () => updateCreator(state, { opened: false }), + CREATE_PROJECT: () => updateCreator(state, { opened: false, pending: true }), + CREATE_PROJECT_SUCCESS: () => updateCreator(state, { ownerUuid: "", pending: false }), + CREATE_PROJECT_ERROR: () => updateCreator(state, { ownerUuid: "", pending: false }), REMOVE_PROJECT: () => state, PROJECTS_REQUEST: itemId => { const items = _.cloneDeep(state.items); @@ -123,6 +142,7 @@ const projectsReducer = (state: ProjectState = { items: [], currentItemId: "" }, item.open = !item.open; } return { + ...state, items, currentItemId: itemId }; @@ -136,6 +156,7 @@ const projectsReducer = (state: ProjectState = { items: [], currentItemId: "" }, item.active = true; } return { + ...state, items, currentItemId: itemId }; @@ -144,6 +165,7 @@ const projectsReducer = (state: ProjectState = { items: [], currentItemId: "" }, const items = _.cloneDeep(state.items); resetTreeActivity(items); return { + ...state, items, currentItemId: "" }; diff --git a/src/utils/dialog-validator.tsx b/src/utils/dialog-validator.tsx new file mode 100644 index 0000000000..1d1a9214bd --- /dev/null +++ b/src/utils/dialog-validator.tsx @@ -0,0 +1,72 @@ +// Copyright (C) The Arvados Authors. All rights reserved. +// +// SPDX-License-Identifier: AGPL-3.0 + +import * as React from 'react'; +import { StyleRulesCallback, WithStyles, withStyles } from '@material-ui/core'; + +type ValidatorProps = { + value: string, + onChange: (isValid: boolean | string) => void; + render: (hasError: boolean) => React.ReactElement; + isRequired: boolean; +}; + +interface ValidatorState { + isPatternValid: boolean; + isLengthValid: boolean; +} + +const nameRegEx = /^[a-zA-Z0-9-_ ]+$/; +const maxInputLength = 60; + +class Validator extends React.Component> { + state: ValidatorState = { + isPatternValid: true, + isLengthValid: true + }; + + componentWillReceiveProps(nextProps: ValidatorProps) { + const { value } = nextProps; + + if (this.props.value !== value) { + this.setState({ + isPatternValid: value.match(nameRegEx), + isLengthValid: value.length < maxInputLength + }, () => this.onChange()); + } + } + + onChange() { + const { value, onChange, isRequired } = this.props; + const { isPatternValid, isLengthValid } = this.state; + const isValid = value && isPatternValid && isLengthValid && (isRequired || (!isRequired && value.length > 0)); + + onChange(isValid); + } + + render() { + const { classes, isRequired, value } = this.props; + const { isPatternValid, isLengthValid } = this.state; + + return ( + + {this.props.render(!(isPatternValid && isLengthValid) && (isRequired || (!isRequired && value.length > 0)))} + {!isPatternValid && (isRequired || (!isRequired && value.length > 0)) ? This field allow only alphanumeric characters, dashes, spaces and underscores.
: null} + {!isLengthValid ? This field should have max 60 characters. : null} +
+ ); + } +} + +type CssRules = "formInputError"; + +const styles: StyleRulesCallback = theme => ({ + formInputError: { + color: "#ff0000", + marginLeft: "5px", + fontSize: "11px", + } +}); + +export default withStyles(styles)(Validator); \ No newline at end of file diff --git a/src/views-components/create-project-dialog/create-project-dialog.tsx b/src/views-components/create-project-dialog/create-project-dialog.tsx new file mode 100644 index 0000000000..701ceee107 --- /dev/null +++ b/src/views-components/create-project-dialog/create-project-dialog.tsx @@ -0,0 +1,35 @@ +// Copyright (C) The Arvados Authors. All rights reserved. +// +// SPDX-License-Identifier: AGPL-3.0 + +import { connect } from "react-redux"; +import { Dispatch } from "../../../node_modules/redux"; +import { RootState } from "../../store/store"; +import DialogProjectCreate from "../dialog-create/dialog-project-create"; +import actions, { createProject, getProjectList } from "../../store/project/project-action"; +import dataExplorerActions from "../../store/data-explorer/data-explorer-action"; +import { PROJECT_PANEL_ID } from "../../views/project-panel/project-panel"; + +const mapStateToProps = (state: RootState) => ({ + open: state.projects.creator.opened +}); + +const submit = (data: { name: string, description: string }) => + (dispatch: Dispatch, getState: () => RootState) => { + const { ownerUuid } = getState().projects.creator; + dispatch(createProject(data)).then(() => { + dispatch(dataExplorerActions.REQUEST_ITEMS({ id: PROJECT_PANEL_ID })); + dispatch(getProjectList(ownerUuid)); + }); + }; + +const mapDispatchToProps = (dispatch: Dispatch) => ({ + handleClose: () => { + dispatch(actions.CLOSE_PROJECT_CREATOR()); + }, + onSubmit: (data: { name: string, description: string }) => { + dispatch(submit(data)); + } +}); + +export default connect(mapStateToProps, mapDispatchToProps)(DialogProjectCreate); diff --git a/src/views-components/data-explorer/data-explorer.tsx b/src/views-components/data-explorer/data-explorer.tsx index 5ff8c66b20..e2e145bbe3 100644 --- a/src/views-components/data-explorer/data-explorer.tsx +++ b/src/views-components/data-explorer/data-explorer.tsx @@ -14,16 +14,15 @@ import { ContextMenuAction, ContextMenuActionGroup } from "../../components/cont interface Props { id: string; - contextActions: ContextMenuActionGroup[]; onRowClick: (item: any) => void; + onContextMenu: (event: React.MouseEvent, item: any) => void; onRowDoubleClick: (item: any) => void; - onContextAction: (action: ContextMenuAction, 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, onRowDoubleClick, onContextAction }: Props) => ({ +const mapDispatchToProps = (dispatch: Dispatch, { id, onRowClick, onRowDoubleClick, onContextMenu }: Props) => ({ onSearch: (searchValue: string) => { dispatch(actions.SET_SEARCH_VALUE({ id, searchValue })); }, @@ -48,13 +47,11 @@ const mapDispatchToProps = (dispatch: Dispatch, { id, contextActions, onRowClick dispatch(actions.SET_ROWS_PER_PAGE({ id, rowsPerPage })); }, - contextActions, - onRowClick, onRowDoubleClick, - - onContextAction + + onContextMenu, }); export default connect(mapStateToProps, mapDispatchToProps)(DataExplorer); diff --git a/src/views-components/dialog-create/dialog-project-create.tsx b/src/views-components/dialog-create/dialog-project-create.tsx new file mode 100644 index 0000000000..ef07ea2f4a --- /dev/null +++ b/src/views-components/dialog-create/dialog-project-create.tsx @@ -0,0 +1,140 @@ +// 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'; + +import Validator from '../../utils/dialog-validator'; + +interface ProjectCreateProps { + open: boolean; + handleClose: () => void; + onSubmit: (data: { name: string, description: string }) => void; +} + +interface DialogState { + name: string; + description: string; + isNameValid: boolean; + isDescriptionValid: boolean; +} + +class DialogProjectCreate extends React.Component> { + state: DialogState = { + name: '', + description: '', + isNameValid: false, + isDescriptionValid: true + }; + + render() { + const { name, description } = this.state; + const { classes, open, handleClose } = this.props; + + return ( + +
+ Create a project + + this.isNameValid(e)} + isRequired={true} + render={hasError => + this.handleProjectName(e)} + label="Project name" + error={hasError} + fullWidth />} /> + this.isDescriptionValid(e)} + isRequired={false} + render={hasError => + this.handleDescriptionValue(e)} + label="Description - optional" + error={hasError} + fullWidth />} /> + + + + + +
+
+ ); + } + + handleSubmit = () => { + this.props.onSubmit({ + name: this.state.name, + description: this.state.description + }); + } + + handleProjectName(e: any) { + this.setState({ + name: e.target.value, + }); + } + + handleDescriptionValue(e: any) { + this.setState({ + description: e.target.value, + }); + } + + isNameValid(value: boolean | string) { + this.setState({ + isNameValid: value, + }); + } + + isDescriptionValid(value: boolean | string) { + this.setState({ + isDescriptionValid: value, + }); + } +} + +type CssRules = "button" | "lastButton" | "dialogContent" | "textField" | "dialog" | "dialogTitle"; + +const styles: StyleRulesCallback = theme => ({ + button: { + marginLeft: theme.spacing.unit + }, + lastButton: { + marginLeft: theme.spacing.unit, + marginRight: "20px", + }, + dialogContent: { + marginTop: "20px", + }, + dialogTitle: { + paddingBottom: "0" + }, + textField: { + marginTop: "32px", + }, + dialog: { + minWidth: "600px", + minHeight: "320px" + } +}); + +export default withStyles(styles)(DialogProjectCreate); \ No newline at end of file diff --git a/src/views-components/main-app-bar/main-app-bar.tsx b/src/views-components/main-app-bar/main-app-bar.tsx index 1230e3b7db..d208239512 100644 --- a/src/views-components/main-app-bar/main-app-bar.tsx +++ b/src/views-components/main-app-bar/main-app-bar.tsx @@ -35,6 +35,7 @@ export interface MainAppBarActionProps { onSearch: (searchText: string) => void; onBreadcrumbClick: (breadcrumb: Breadcrumb) => void; onMenuItemClick: (menuItem: MainAppBarMenuItem) => void; + onContextMenu: (event: React.MouseEvent, breadcrumb: Breadcrumb) => void; onDetailsPanelToggle: () => void; } @@ -70,7 +71,10 @@ export const MainAppBar: React.SFC = (props) => { { - props.user && + props.user && } diff --git a/src/views-components/project-tree/project-tree.tsx b/src/views-components/project-tree/project-tree.tsx index f51b65e054..511cbbbc73 100644 --- a/src/views-components/project-tree/project-tree.tsx +++ b/src/views-components/project-tree/project-tree.tsx @@ -16,15 +16,17 @@ export interface ProjectTreeProps { projects: Array>; toggleOpen: (id: string, status: TreeItemStatus) => void; toggleActive: (id: string, status: TreeItemStatus) => void; + onContextMenu: (event: React.MouseEvent, item: TreeItem) => void; } class ProjectTree extends React.Component> { render(): ReactElement { - const { classes, projects, toggleOpen, toggleActive } = this.props; + const { classes, projects, toggleOpen, toggleActive, onContextMenu } = this.props; const { active, listItemText, row, treeContainer } = classes; return (
) => diff --git a/src/views/project-panel/project-panel.tsx b/src/views/project-panel/project-panel.tsx index 0708b16370..0b1be099d0 100644 --- a/src/views/project-panel/project-panel.tsx +++ b/src/views/project-panel/project-panel.tsx @@ -4,10 +4,9 @@ import * as React from 'react'; import { ProjectPanelItem } from './project-panel-item'; -import { Grid, Typography, Button, Toolbar, StyleRulesCallback, WithStyles, withStyles } from '@material-ui/core'; +import { Grid, Typography, Button, 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,13 +25,16 @@ export interface ProjectPanelFilter extends DataTableFilterItem { type ProjectPanelProps = { currentItemId: string, onItemClick: (item: ProjectPanelItem) => void, + onContextMenu: (event: React.MouseEvent, item: ProjectPanelItem) => void; + onDialogOpen: (ownerUuid: string) => void; onItemDoubleClick: (item: ProjectPanelItem) => void, onItemRouteChange: (itemId: string) => void } & DispatchProp & WithStyles & RouteComponentProps<{ id: string }>; -class ProjectPanel extends React.Component { + +class ProjectPanel extends React.Component { render() { return
@@ -42,16 +44,15 @@ class ProjectPanel extends React.Component { -
; + onContextMenu={this.props.onContextMenu} />
; } @@ -60,11 +61,6 @@ class ProjectPanel extends React.Component { this.props.onItemRouteChange(match.params.id); } } - - executeAction = (action: ContextMenuAction, item: ProjectPanelItem) => { - alert(`Executing ${action.name} on ${item.name}`); - } - } type CssRules = "toolbar" | "button"; @@ -76,7 +72,7 @@ const styles: StyleRulesCallback = theme => ({ }, button: { marginLeft: theme.spacing.unit - } + }, }); const renderName = (item: ProjectPanelItem) => @@ -220,29 +216,6 @@ export const columns: DataColumns = [{ 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 }))( diff --git a/src/views/workbench/workbench.tsx b/src/views/workbench/workbench.tsx index 959025bfbe..6ad4d24745 100644 --- a/src/views/workbench/workbench.tsx +++ b/src/views/workbench/workbench.tsx @@ -27,6 +27,11 @@ 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, { ContextMenuAction } from '../../components/context-menu/context-menu'; +import { mockAnchorFromMouseEvent } from '../../components/popover/helpers'; +import CreateProjectDialog from "../../views-components/create-project-dialog/create-project-dialog"; +import { authService } from '../../services/services'; + import detailsPanelActions, { loadDetails } from "../../store/details-panel/details-panel-action"; import { ResourceKind } from '../../models/kinds'; @@ -92,6 +97,10 @@ interface NavMenuItem extends MainAppBarMenuItem { } interface WorkbenchState { + contextMenu: { + anchorEl?: HTMLElement; + itemUuid?: string; + }; anchorEl: any; searchText: string; menuItems: { @@ -104,6 +113,11 @@ interface WorkbenchState { class Workbench extends React.Component { state = { + contextMenu: { + anchorEl: undefined, + itemUuid: undefined + }, + isCreationDialogOpen: false, anchorEl: null, searchText: "", breadcrumbs: [], @@ -145,6 +159,9 @@ class Workbench extends React.Component { onMenuItemClick: (menuItem: NavMenuItem) => menuItem.action(), onDetailsPanelToggle: () => { this.props.dispatch(detailsPanelActions.TOGGLE_DETAILS_PANEL()); + }, + onContextMenu: (event: React.MouseEvent, breadcrumb: NavBreadcrumb) => { + this.openContextMenu(event, breadcrumb.itemId); } }; @@ -157,6 +174,33 @@ class Workbench extends React.Component { this.props.dispatch(projectActions.RESET_PROJECT_TREE_ACTIVITY(itemId)); } + handleCreationDialogOpen = (itemUuid: string) => { + this.closeContextMenu(); + this.props.dispatch(projectActions.OPEN_PROJECT_CREATOR({ ownerUuid: itemUuid })); + } + + + openContextMenu = (event: React.MouseEvent, itemUuid: string) => { + event.preventDefault(); + this.setState({ + contextMenu: { + anchorEl: mockAnchorFromMouseEvent(event), + itemUuid + } + }); + } + + closeContextMenu = () => { + this.setState({ contextMenu: {} }); + } + + openCreateDialog = (item: ContextMenuAction) => { + const { itemUuid } = this.state.contextMenu; + if (item.openCreateDialog && itemUuid) { + this.handleCreationDialogOpen(itemUuid); + } + } + render() { const path = getTreePath(this.props.projects, this.props.currentProjectId); const breadcrumbs = path.map(item => ({ @@ -174,8 +218,7 @@ class Workbench extends React.Component { searchText={this.state.searchText} user={this.props.user} menuItems={this.state.menuItems} - {...this.mainAppBarActions} - /> + {...this.mainAppBarActions} />
{user && { + sidePanelItems={this.props.sidePanelItems} + onContextMenu={(event) => this.openContextMenu(event, authService.getUuid() || "")}> this.props.dispatch(setProjectItem(itemId, ItemMode.OPEN))} + onContextMenu={(event, item) => this.openContextMenu(event, item.data.uuid)} toggleActive={itemId => { this.props.dispatch(setProjectItem(itemId, ItemMode.ACTIVE)); this.props.dispatch(loadDetails(itemId, ResourceKind.Project)); - }} - /> + }}/> }
@@ -206,12 +250,20 @@ class Workbench extends React.Component {
+ + ); } renderProjectPanel = (props: RouteComponentProps<{ id: string }>) => this.props.dispatch(setProjectItem(itemId, ItemMode.ACTIVE))} + onContextMenu={(event, item) => this.openContextMenu(event, item.uuid)} + onDialogOpen={this.handleCreationDialogOpen} onItemClick={item => { this.props.dispatch(loadDetails(item.uuid, item.kind as ResourceKind)); }} @@ -220,8 +272,35 @@ class Workbench extends React.Component { this.props.dispatch(loadDetails(item.uuid, ResourceKind.Project)); }} {...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( (state: RootState) => ({