From: Janicki Artur Date: Wed, 11 Jul 2018 13:15:49 +0000 (+0200) Subject: Merge branch 'master' into 13764-icons-colors-unification-refactoring X-Git-Tag: 1.2.0~43^2 X-Git-Url: https://git.arvados.org/arvados-workbench2.git/commitdiff_plain/e6039bec0497aa7e1391958e5c4f84bbaeef653e?hp=ab08a8e2200157caaf73828b8459a641fff0d1e9 Merge branch 'master' into 13764-icons-colors-unification-refactoring refs #13764 Arvados-DCO-1.1-Signed-off-by: Janicki Artur --- diff --git a/.env b/.env index 13aaad50..a523865a 100644 --- a/.env +++ b/.env @@ -2,4 +2,5 @@ # # SPDX-License-Identifier: AGPL-3.0 +REACT_APP_ARVADOS_CONFIG_URL=/config.json REACT_APP_ARVADOS_API_HOST=https://qr1hi.arvadosapi.com \ No newline at end of file diff --git a/README.md b/README.md index 864a54fa..998d4246 100644 --- a/README.md +++ b/README.md @@ -26,12 +26,22 @@ yarn install yarn build -### Configuration +### Build time configuration You can customize project global variables using env variables. Default values are placed in the `.env` file. Example: ``` -REACT_APP_ARVADOS_API_HOST=localhost:8000 yarn start +REACT_APP_ARVADOS_CONFIG_URL=config.json yarn build +``` + +### Run time configuration +The app will fetch runtime configuration when starting. By default it will try to fetch `/config.json`. You can customize this url using build time configuration. + +Currently this configuration schema is supported: +``` +{ + "API_HOST": "string" +} ``` ### Licensing diff --git a/src/common/api/server-api.ts b/src/common/api/server-api.ts index 330ce657..5beecd48 100644 --- a/src/common/api/server-api.ts +++ b/src/common/api/server-api.ts @@ -18,3 +18,7 @@ export function setServerApiAuthorizationHeader(token: string) { export function removeServerApiAuthorizationHeader() { delete serverApi.defaults.headers.common.Authorization; } + +export const setBaseUrl = (url: string) => { + serverApi.defaults.baseURL = url + "/arvados/v1"; +}; diff --git a/src/common/config.ts b/src/common/config.ts new file mode 100644 index 00000000..4b4a52a3 --- /dev/null +++ b/src/common/config.ts @@ -0,0 +1,23 @@ +// Copyright (C) The Arvados Authors. All rights reserved. +// +// SPDX-License-Identifier: AGPL-3.0 + +import Axios from "../../node_modules/axios"; + +export const CONFIG_URL = process.env.REACT_APP_ARVADOS_CONFIG_URL || "/config.json"; + +export interface Config { + API_HOST: string; +} + +const defaultConfig: Config = { + API_HOST: process.env.REACT_APP_ARVADOS_API_HOST || "" +}; + +export const fetchConfig = () => { + return Axios + .get(CONFIG_URL + "?nocache=" + (new Date()).getTime()) + .then(response => response.data) + .catch(() => Promise.resolve(defaultConfig)); +}; + diff --git a/src/components/attribute/attribute.tsx b/src/components/attribute/attribute.tsx index 319b241d..4fb1d110 100644 --- a/src/components/attribute/attribute.tsx +++ b/src/components/attribute/attribute.tsx @@ -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; diff --git a/src/components/breadcrumbs/breadcrumbs.tsx b/src/components/breadcrumbs/breadcrumbs.tsx index 4868e137..cfcfd407 100644 --- a/src/components/breadcrumbs/breadcrumbs.tsx +++ b/src/components/breadcrumbs/breadcrumbs.tsx @@ -38,9 +38,7 @@ const Breadcrumbs: React.SFC> = ({ class - { - !isLastItem && - } + {!isLastItem && } ); }) @@ -51,7 +49,6 @@ const Breadcrumbs: React.SFC> = ({ class type CssRules = "item" | "currentItem" | "label"; const styles: StyleRulesCallback = theme => { - const { unit } = theme.spacing; return { item: { opacity: 0.6 diff --git a/src/components/context-menu/context-menu.test.tsx b/src/components/context-menu/context-menu.test.tsx index e4e2397d..86011a3c 100644 --- a/src/components/context-menu/context-menu.test.tsx +++ b/src/components/context-menu/context-menu.test.tsx @@ -7,29 +7,30 @@ import { mount, configure, shallow } from "enzyme"; import * as Adapter from "enzyme-adapter-react-16"; import ContextMenu from "./context-menu"; import { ListItem } from "@material-ui/core"; +import { IconTypes } from "../icon/icon"; configure({ adapter: new Adapter() }); describe("", () => { - const actions = [[{ - icon: "", + const items = [[{ + icon: IconTypes.ANNOUNCEMENT, name: "Action 1.1" }, { - icon: "", + icon: IconTypes.ANNOUNCEMENT, name: "Action 1.2" },], [{ - icon: "", + icon: IconTypes.ANNOUNCEMENT, name: "Action 2.1" }]]; - it("calls onActionClick with clicked action", () => { - const onActionClick = jest.fn(); + it("calls onItemClick with clicked action", () => { + const onItemClick = jest.fn(); const contextMenu = mount(); + onItemClick={onItemClick} + items={items} />); contextMenu.find(ListItem).at(2).simulate("click"); - expect(onActionClick).toHaveBeenCalledWith(actions[1][0]); + expect(onItemClick).toHaveBeenCalledWith(items[1][0]); }); }); \ No newline at end of file diff --git a/src/components/context-menu/context-menu.tsx b/src/components/context-menu/context-menu.tsx index c892ba26..a7b83bcf 100644 --- a/src/components/context-menu/context-menu.tsx +++ b/src/components/context-menu/context-menu.tsx @@ -4,25 +4,25 @@ import * as React from "react"; import { Popover, List, ListItem, ListItemIcon, ListItemText, Divider } from "@material-ui/core"; import { DefaultTransformOrigin } from "../popover/helpers"; +import IconBase, { IconTypes } from "../icon/icon"; -export interface ContextMenuAction { +export interface ContextMenuItem { name: string; - icon: string; - openCreateDialog?: boolean; + icon: IconTypes; } -export type ContextMenuActionGroup = ContextMenuAction[]; +export type ContextMenuItemGroup = ContextMenuItem[]; -export interface ContextMenuProps { +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> { +export default class ContextMenu extends React.PureComponent { render() { - const { anchorEl, actions, onClose, onActionClick } = this.props; + const { anchorEl, items, onClose, onItemClick } = this.props; return extends React.PureComponent - {actions.map((group, groupIndex) => + {items.map((group, groupIndex) => - {group.map((action, actionIndex) => + {group.map((item, actionIndex) => onActionClick(action)}> + onClick={() => onItemClick(item)}> - + - {action.name} + {item.name} )} - {groupIndex < actions.length - 1 && } + {groupIndex < items.length - 1 && } )} ; diff --git a/src/components/data-explorer/data-explorer.tsx b/src/components/data-explorer/data-explorer.tsx index e851ca99..1073ddd8 100644 --- a/src/components/data-explorer/data-explorer.tsx +++ b/src/components/data-explorer/data-explorer.tsx @@ -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 { diff --git a/src/components/popover/helpers.ts b/src/components/popover/helpers.ts index 13f74a68..f2be98cf 100644 --- a/src/components/popover/helpers.ts +++ b/src/components/popover/helpers.ts @@ -4,13 +4,13 @@ import { PopoverOrigin } from "@material-ui/core/Popover"; -export const mockAnchorFromMouseEvent = (event: React.MouseEvent) => { +export const createAnchorAt = (position: {x: number, y: number}) => { const el = document.createElement('div'); const clientRect = { - left: event.clientX, - right: event.clientX, - top: event.clientY, - bottom: event.clientY, + left: position.x, + right: position.x, + top: position.y, + bottom: position.y, width: 0, height: 0 }; diff --git a/src/index.tsx b/src/index.tsx index a06b4851..10224967 100644 --- a/src/index.tsx +++ b/src/index.tsx @@ -17,39 +17,36 @@ import { authService } from "./services/services"; import { getProjectList } from "./store/project/project-action"; import { MuiThemeProvider } from '@material-ui/core/styles'; import { CustomTheme } from './common/custom-theme'; -import CommonResourceService from './common/api/common-resource-service'; -import { CollectionResource } from './models/collection'; -import { serverApi } from './common/api/server-api'; -import { ProcessResource } from './models/process'; - -const history = createBrowserHistory(); - -const store = configureStore(history); - -store.dispatch(authActions.INIT()); -store.dispatch(getProjectList(authService.getUuid())); - -// const service = new CommonResourceService(serverApi, "collections"); -// service.create({ ownerUuid: "qr1hi-j7d0g-u55bcc7fa5w7v4p", name: "Collection 1 short title"}); -// service.create({ ownerUuid: "qr1hi-j7d0g-u55bcc7fa5w7v4p", name: "Collection 2 long long long title"}); - -// const processService = new CommonResourceService(serverApi, "container_requests"); -// processService.create({ ownerUuid: "qr1hi-j7d0g-u55bcc7fa5w7v4p", name: "Process 1 short title"}); -// processService.create({ ownerUuid: "qr1hi-j7d0g-u55bcc7fa5w7v4p", name: "Process 2 long long long title" }); - -const App = () => - - - -
- - -
-
-
-
; - -ReactDOM.render( - , - document.getElementById('root') as HTMLElement -); +import { fetchConfig } from './common/config'; +import { setBaseUrl } from './common/api/server-api'; + +fetchConfig() + .then(config => { + + setBaseUrl(config.API_HOST); + + const history = createBrowserHistory(); + const store = configureStore(history); + + store.dispatch(authActions.INIT()); + store.dispatch(getProjectList(authService.getUuid())); + + const App = () => + + + +
+ + +
+
+
+
; + + ReactDOM.render( + , + document.getElementById('root') as HTMLElement + ); + }); + + diff --git a/src/services/auth-service/auth-service.ts b/src/services/auth-service/auth-service.ts index e953a75d..5b21a616 100644 --- a/src/services/auth-service/auth-service.ts +++ b/src/services/auth-service/auth-service.ts @@ -2,8 +2,9 @@ // // SPDX-License-Identifier: AGPL-3.0 -import { API_HOST, serverApi } from "../../common/api/server-api"; +import { API_HOST } from "../../common/api/server-api"; import { User } from "../../models/user"; +import { AxiosInstance } from "../../../node_modules/axios"; export const API_TOKEN_KEY = 'apiToken'; export const USER_EMAIL_KEY = 'userEmail'; @@ -23,6 +24,8 @@ export interface UserDetailsResponse { export default class AuthService { + constructor(protected serverApi: AxiosInstance) { } + public saveApiToken(token: string) { localStorage.setItem(API_TOKEN_KEY, token); } @@ -82,7 +85,7 @@ export default class AuthService { } public getUserDetails = (): Promise => { - return serverApi + return this.serverApi .get('/users/current') .then(resp => ({ email: resp.data.email, diff --git a/src/services/project-service/project-service.test.ts b/src/services/project-service/project-service.test.ts index 68df2450..76da3d86 100644 --- a/src/services/project-service/project-service.test.ts +++ b/src/services/project-service/project-service.test.ts @@ -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"; diff --git a/src/services/services.ts b/src/services/services.ts index 143e97bd..88f6ffae 100644 --- a/src/services/services.ts +++ b/src/services/services.ts @@ -7,6 +7,6 @@ import GroupsService from "./groups-service/groups-service"; import { serverApi } from "../common/api/server-api"; import ProjectService from "./project-service/project-service"; -export const authService = new AuthService(); +export const authService = new AuthService(serverApi); export const groupsService = new GroupsService(serverApi); export const projectService = new ProjectService(serverApi); diff --git a/src/store/context-menu/context-menu-actions.ts b/src/store/context-menu/context-menu-actions.ts index 89d65244..8630f9c7 100644 --- a/src/store/context-menu/context-menu-actions.ts +++ b/src/store/context-menu/context-menu-actions.ts @@ -2,14 +2,16 @@ // // SPDX-License-Identifier: AGPL-3.0 -// import { default as unionize, ofType, UnionOf } from "unionize"; +import { default as unionize, ofType, UnionOf } from "unionize"; +import { ContextMenuPosition, ContextMenuResource } from "./context-menu-reducer"; -// const actions = unionize({ -// OPEN_CONTEXT_MENU: ofType<{position: {x: number, y: number}}>() -// }, { -// tag: 'type', -// value: 'payload' -// }); +const actions = unionize({ + OPEN_CONTEXT_MENU: ofType<{ position: ContextMenuPosition, resource: ContextMenuResource }>(), + CLOSE_CONTEXT_MENU: ofType<{}>() +}, { + tag: 'type', + value: 'payload' + }); -// export type ContextMenuAction = UnionOf; -// export default actions; \ No newline at end of file +export type ContextMenuAction = UnionOf; +export default actions; \ No newline at end of file diff --git a/src/store/context-menu/context-menu-reducer.ts b/src/store/context-menu/context-menu-reducer.ts index 9a825a5f..147f0943 100644 --- a/src/store/context-menu/context-menu-reducer.ts +++ b/src/store/context-menu/context-menu-reducer.ts @@ -2,31 +2,33 @@ // // SPDX-License-Identifier: AGPL-3.0 -// import actions, { DetailsPanelAction } from "./details-panel-action"; -// import { Resource, ResourceKind } from "../../models/resource"; +import { ResourceKind } from "../../models/resource"; +import actions, { ContextMenuAction } from "./context-menu-actions"; -// export interface ContextMenuState { -// position: { -// x: number; -// y: number; -// }, -// resource: { -// uuid: string; -// kind: ResourceKind. -// } -// } +export interface ContextMenuState { + position: ContextMenuPosition; + resource?: ContextMenuResource; +} -// const initialState = { -// item: null, -// isOpened: false -// }; +export interface ContextMenuPosition { + x: number; + y: number; +} -// const reducer = (state: DetailsPanelState = initialState, action: DetailsPanelAction) => -// actions.match(action, { -// default: () => state, -// LOAD_DETAILS: () => state, -// LOAD_DETAILS_SUCCESS: ({ item }) => ({ ...state, item }), -// TOGGLE_DETAILS_PANEL: () => ({ ...state, isOpened: !state.isOpened }) -// }); +export interface ContextMenuResource { + uuid: string; + kind: string; +} -// export default reducer; +const initialState = { + position: { x: 0, y: 0 } +}; + +const reducer = (state: ContextMenuState = initialState, action: ContextMenuAction) => + actions.match(action, { + default: () => state, + OPEN_CONTEXT_MENU: ({resource, position}) => ({ resource, position }), + CLOSE_CONTEXT_MENU: () => ({ position: state.position }) + }); + +export default reducer; diff --git a/src/store/project-panel/project-panel-middleware.ts b/src/store/project-panel/project-panel-middleware.ts index e72b6c1b..80fb7fa3 100644 --- a/src/store/project-panel/project-panel-middleware.ts +++ b/src/store/project-panel/project-panel-middleware.ts @@ -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"; diff --git a/src/store/store.ts b/src/store/store.ts index 36f92034..d87c8031 100644 --- a/src/store/store.ts +++ b/src/store/store.ts @@ -11,8 +11,9 @@ 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'; const composeEnhancers = (process.env.NODE_ENV === 'development' && @@ -26,6 +27,7 @@ export interface RootState { dataExplorer: DataExplorerState; sidePanel: SidePanelState; detailsPanel: DetailsPanelState; + contextMenu: ContextMenuState; } const rootReducer = combineReducers({ @@ -34,7 +36,8 @@ const rootReducer = combineReducers({ router: routerReducer, dataExplorer: dataExplorerReducer, sidePanel: sidePanelReducer, - detailsPanel: detailsPanelReducer + detailsPanel: detailsPanelReducer, + contextMenu: contextMenuReducer }); diff --git a/src/views-components/context-menu/action-sets/project-action-set.ts b/src/views-components/context-menu/action-sets/project-action-set.ts new file mode 100644 index 00000000..9a1b1d5b --- /dev/null +++ b/src/views-components/context-menu/action-sets/project-action-set.ts @@ -0,0 +1,19 @@ +// Copyright (C) The Arvados Authors. All rights reserved. +// +// SPDX-License-Identifier: AGPL-3.0 + +import { ContextMenuActionSet } from "../context-menu-action-set"; +import actions from "../../../store/project/project-action"; +import { IconTypes } from "../../../components/icon/icon"; + +export const projectActionSet: ContextMenuActionSet = [[{ + icon: IconTypes.FOLDER, + name: "New project", + execute: (dispatch, resource) => { + dispatch(actions.OPEN_PROJECT_CREATOR({ ownerUuid: resource.uuid })); + } +}, { + icon: IconTypes.ANNOUNCEMENT, + name: "Share", + execute: () => { return; } +}]]; \ No newline at end of file diff --git a/src/views-components/context-menu/action-sets/root-project-action-set.ts b/src/views-components/context-menu/action-sets/root-project-action-set.ts new file mode 100644 index 00000000..53642d03 --- /dev/null +++ b/src/views-components/context-menu/action-sets/root-project-action-set.ts @@ -0,0 +1,15 @@ +// Copyright (C) The Arvados Authors. All rights reserved. +// +// SPDX-License-Identifier: AGPL-3.0 + +import { ContextMenuActionSet } from "../context-menu-action-set"; +import actions from "../../../store/project/project-action"; +import { IconTypes } from "../../../components/icon/icon"; + +export const rootProjectActionSet: ContextMenuActionSet = [[{ + icon: IconTypes.FOLDER, + name: "New project", + execute: (dispatch, resource) => { + dispatch(actions.OPEN_PROJECT_CREATOR({ ownerUuid: resource.uuid })); + } +}]]; \ No newline at end of file diff --git a/src/views-components/context-menu/context-menu-action-set.ts b/src/views-components/context-menu/context-menu-action-set.ts new file mode 100644 index 00000000..089580c2 --- /dev/null +++ b/src/views-components/context-menu/context-menu-action-set.ts @@ -0,0 +1,13 @@ +// Copyright (C) The Arvados Authors. All rights reserved. +// +// SPDX-License-Identifier: AGPL-3.0 + +import { Dispatch } from "redux"; +import { ContextMenuItem } from "../../components/context-menu/context-menu"; +import { ContextMenuResource } from "../../store/context-menu/context-menu-reducer"; + +export interface ContextMenuAction extends ContextMenuItem { + execute(dispatch: Dispatch, resource: ContextMenuResource): void; +} + +export type ContextMenuActionSet = Array>; diff --git a/src/views-components/context-menu/context-menu.tsx b/src/views-components/context-menu/context-menu.tsx new file mode 100644 index 00000000..cc103c49 --- /dev/null +++ b/src/views-components/context-menu/context-menu.tsx @@ -0,0 +1,55 @@ +// Copyright (C) The Arvados Authors. All rights reserved. +// +// SPDX-License-Identifier: AGPL-3.0 + +import { connect, Dispatch, DispatchProp } from "react-redux"; +import { RootState } from "../../store/store"; +import actions from "../../store/context-menu/context-menu-actions"; +import ContextMenu, { ContextMenuProps, ContextMenuItem } from "../../components/context-menu/context-menu"; +import { createAnchorAt } from "../../components/popover/helpers"; +import { ContextMenuResource } from "../../store/context-menu/context-menu-reducer"; +import { ContextMenuActionSet, ContextMenuAction } from "./context-menu-action-set"; + +type DataProps = Pick & { resource?: ContextMenuResource }; +const mapStateToProps = (state: RootState): DataProps => { + const { position, resource } = state.contextMenu; + return { + anchorEl: resource ? createAnchorAt(position) : undefined, + items: getMenuActionSet(resource), + resource + }; +}; + +type ActionProps = Pick & { onItemClick: (item: ContextMenuItem, resource?: ContextMenuResource) => void }; +const mapDispatchToProps = (dispatch: Dispatch): ActionProps => ({ + onClose: () => { + dispatch(actions.CLOSE_CONTEXT_MENU()); + }, + onItemClick: (action: ContextMenuAction, resource?: ContextMenuResource) => { + dispatch(actions.CLOSE_CONTEXT_MENU()); + if (resource) { + action.execute(dispatch, resource); + } + } +}); + +const mergeProps = ({ resource, ...dataProps }: DataProps, actionProps: ActionProps): ContextMenuProps => ({ + ...dataProps, + ...actionProps, + onItemClick: item => { + actionProps.onItemClick(item, resource); + } +}); + +export const ContextMenuHOC = connect(mapStateToProps, mapDispatchToProps, mergeProps)(ContextMenu); + +const menuActionSets = new Map(); + +export const addMenuActionSet = (name: string, itemSet: ContextMenuActionSet) => { + menuActionSets.set(name, itemSet); +}; + +const getMenuActionSet = (resource?: ContextMenuResource): ContextMenuActionSet => { + return resource ? menuActionSets.get(resource.kind) || [] : []; +}; + diff --git a/src/views-components/context-menu/index.ts b/src/views-components/context-menu/index.ts new file mode 100644 index 00000000..6059e8f9 --- /dev/null +++ b/src/views-components/context-menu/index.ts @@ -0,0 +1,17 @@ +// Copyright (C) The Arvados Authors. All rights reserved. +// +// SPDX-License-Identifier: AGPL-3.0 + +import { ContextMenuHOC, addMenuActionSet } from "./context-menu"; +import { projectActionSet } from "./action-sets/project-action-set"; +import { rootProjectActionSet } from "./action-sets/root-project-action-set"; + +export default ContextMenuHOC; + +export enum ContextMenuKind { + RootProject = "RootProject", + Project = "Project" +} + +addMenuActionSet(ContextMenuKind.RootProject, rootProjectActionSet); +addMenuActionSet(ContextMenuKind.Project, projectActionSet); \ 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 index 701ceee1..6b69b793 100644 --- a/src/views-components/create-project-dialog/create-project-dialog.tsx +++ b/src/views-components/create-project-dialog/create-project-dialog.tsx @@ -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"; diff --git a/src/views-components/dialog-create/dialog-project-create.tsx b/src/views-components/dialog-create/dialog-project-create.tsx index ef07ea2f..89deea6f 100644 --- a/src/views-components/dialog-create/dialog-project-create.tsx +++ b/src/views-components/dialog-create/dialog-project-create.tsx @@ -87,13 +87,13 @@ class DialogProjectCreate extends React.Component) { this.setState({ name: e.target.value, }); } - handleDescriptionValue(e: any) { + handleDescriptionValue(e: React.ChangeEvent) { this.setState({ description: e.target.value, }); diff --git a/src/views-components/main-app-bar/main-app-bar.test.tsx b/src/views-components/main-app-bar/main-app-bar.test.tsx index f58b26a0..ac744b9e 100644 --- a/src/views-components/main-app-bar/main-app-bar.test.tsx +++ b/src/views-components/main-app-bar/main-app-bar.test.tsx @@ -28,6 +28,7 @@ describe("", () => { const mainAppBar = mount( ", () => { ", () => { const mainAppBar = mount( ", () => { const mainAppBar = mount( = (theme: Theme) => ({ - root: { - width: '100%', - marginTop: theme.spacing.unit * 3, - overflowX: 'auto', - }, - table: { - minWidth: 700, - }, -}); - -interface ProjectListProps { -} - -class ProjectList extends React.Component, {}> { - render() { - const {classes} = this.props; - return - - - - Name - Status - Type - Shared by - File size - Last modified - - - - - Project 1 - Complete - Project - John Doe - 1.5 GB - 9:22 PM - - -
-
; - } -} - -export default withStyles(styles)(ProjectList); diff --git a/src/views/project-panel/project-panel.tsx b/src/views/project-panel/project-panel.tsx index e34ea1ec..2fdb715f 100644 --- a/src/views/project-panel/project-panel.tsx +++ b/src/views/project-panel/project-panel.tsx @@ -37,15 +37,16 @@ type ProjectPanelProps = { class ProjectPanel extends React.Component { render() { + const { classes, currentItemId, onItemClick, onItemDoubleClick, onContextMenu, onDialogOpen } = this.props; return
-
- - -
@@ -57,10 +58,13 @@ class ProjectPanel extends React.Component { extractKey={(item: ProjectPanelItem) => item.uuid} />
; } - - componentWillReceiveProps({ match, currentItemId }: ProjectPanelProps) { + + handleNewProjectClick = () => { + this.props.onDialogOpen(this.props.currentItemId); + } + componentWillReceiveProps({ match, currentItemId, onItemRouteChange }: ProjectPanelProps) { if (match.params.id !== currentItemId) { - this.props.onItemRouteChange(match.params.id); + onItemRouteChange(match.params.id); } } } diff --git a/src/views/workbench/workbench.tsx b/src/views/workbench/workbench.tsx index 6972b2f8..c7bfc8b4 100644 --- a/src/views/workbench/workbench.tsx +++ b/src/views/workbench/workbench.tsx @@ -26,57 +26,16 @@ 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 ContextMenu, { ContextMenuKind } from "../../views-components/context-menu"; 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 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'; -const drawerWidth = 240; -const appBarHeight = 100; - -type CssRules = 'root' | 'appBar' | 'drawerPaper' | 'content' | 'contentWrapper' | 'toolbar'; - -const styles: StyleRulesCallback = (theme: ArvadosTheme) => ({ - root: { - flexGrow: 1, - zIndex: 1, - overflow: 'hidden', - position: 'relative', - display: 'flex', - width: '100vw', - height: '100vh' - }, - appBar: { - zIndex: theme.zIndex.drawer + 1, - position: "absolute", - width: "100%" - }, - drawerPaper: { - position: 'relative', - width: drawerWidth, - display: 'flex', - flexDirection: 'column', - }, - contentWrapper: { - backgroundColor: theme.palette.background.default, - display: "flex", - flexGrow: 1, - minWidth: 0, - paddingTop: appBarHeight - }, - content: { - padding: `${theme.spacing.unit}px ${theme.spacing.unit * 3}px`, - overflowY: "auto", - flexGrow: 1 - }, - toolbar: theme.mixins.toolbar -}); - interface WorkbenchDataProps { projects: Array>; currentProjectId: string; @@ -98,10 +57,6 @@ interface NavMenuItem extends MainAppBarMenuItem { } interface WorkbenchState { - contextMenu: { - anchorEl?: HTMLElement; - itemUuid?: string; - }; anchorEl: any; searchText: string; menuItems: { @@ -114,10 +69,6 @@ interface WorkbenchState { class Workbench extends React.Component { state = { - contextMenu: { - anchorEl: undefined, - itemUuid: undefined - }, isCreationDialogOpen: false, anchorEl: null, searchText: "", @@ -148,61 +99,6 @@ class Workbench extends React.Component { } }; - mainAppBarActions: MainAppBarActionProps = { - onBreadcrumbClick: ({ itemId }: NavBreadcrumb) => { - this.props.dispatch(setProjectItem(itemId, ItemMode.BOTH)); - this.props.dispatch(loadDetails(itemId, ResourceKind.Project)); - }, - onSearch: searchText => { - this.setState({ searchText }); - this.props.dispatch(push(`/search?q=${searchText}`)); - }, - onMenuItemClick: (menuItem: NavMenuItem) => menuItem.action(), - onDetailsPanelToggle: () => { - this.props.dispatch(detailsPanelActions.TOGGLE_DETAILS_PANEL()); - }, - onContextMenu: (event: React.MouseEvent, breadcrumb: NavBreadcrumb) => { - this.openContextMenu(event, breadcrumb.itemId); - } - }; - - toggleSidePanelOpen = (itemId: string) => { - this.props.dispatch(sidePanelActions.TOGGLE_SIDE_PANEL_ITEM_OPEN(itemId)); - } - - toggleSidePanelActive = (itemId: string) => { - this.props.dispatch(sidePanelActions.TOGGLE_SIDE_PANEL_ITEM_ACTIVE(itemId)); - this.props.dispatch(projectActions.RESET_PROJECT_TREE_ACTIVITY(itemId)); - this.props.dispatch(push("/")); - } - - 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 => ({ @@ -233,11 +129,11 @@ class Workbench extends React.Component { toggleOpen={this.toggleSidePanelOpen} toggleActive={this.toggleSidePanelActive} sidePanelItems={this.props.sidePanelItems} - onContextMenu={(event) => this.openContextMenu(event, authService.getUuid() || "")}> + onContextMenu={(event) => this.openContextMenu(event, authService.getUuid() || "", ContextMenuKind.RootProject)}> this.props.dispatch(setProjectItem(itemId, ItemMode.OPEN))} - onContextMenu={(event, item) => this.openContextMenu(event, item.data.uuid)} + onContextMenu={(event, item) => this.openContextMenu(event, item.data.uuid, ContextMenuKind.Project)} toggleActive={itemId => { this.props.dispatch(setProjectItem(itemId, ItemMode.ACTIVE)); this.props.dispatch(loadDetails(itemId, ResourceKind.Project)); @@ -253,11 +149,7 @@ class Workbench extends React.Component { - + ); @@ -265,7 +157,7 @@ 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)} + onContextMenu={(event, item) => this.openContextMenu(event, item.uuid, ContextMenuKind.Project)} onDialogOpen={this.handleCreationDialogOpen} onItemClick={item => { this.props.dispatch(loadDetails(item.uuid, item.kind as ResourceKind)); @@ -275,35 +167,92 @@ 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" + mainAppBarActions: MainAppBarActionProps = { + onBreadcrumbClick: ({ itemId }: NavBreadcrumb) => { + this.props.dispatch(setProjectItem(itemId, ItemMode.BOTH)); + this.props.dispatch(loadDetails(itemId, ResourceKind.Project)); + }, + onSearch: searchText => { + this.setState({ searchText }); + this.props.dispatch(push(`/search?q=${searchText}`)); + }, + onMenuItemClick: (menuItem: NavMenuItem) => menuItem.action(), + onDetailsPanelToggle: () => { + this.props.dispatch(detailsPanelActions.TOGGLE_DETAILS_PANEL()); + }, + onContextMenu: (event: React.MouseEvent, breadcrumb: NavBreadcrumb) => { + this.openContextMenu(event, breadcrumb.itemId, ContextMenuKind.Project); + } + }; + + toggleSidePanelOpen = (itemId: string) => { + this.props.dispatch(sidePanelActions.TOGGLE_SIDE_PANEL_ITEM_OPEN(itemId)); + } + + toggleSidePanelActive = (itemId: string) => { + this.props.dispatch(sidePanelActions.TOGGLE_SIDE_PANEL_ITEM_ACTIVE(itemId)); + this.props.dispatch(projectActions.RESET_PROJECT_TREE_ACTIVITY(itemId)); + this.props.dispatch(push("/")); + } + + handleCreationDialogOpen = (itemUuid: string) => { + this.props.dispatch(projectActions.OPEN_PROJECT_CREATOR({ ownerUuid: itemUuid })); + } + + openContextMenu = (event: React.MouseEvent, itemUuid: string, kind: ContextMenuKind) => { + event.preventDefault(); + this.props.dispatch( + contextMenuActions.OPEN_CONTEXT_MENU({ + position: { x: event.clientX, y: event.clientY }, + resource: { uuid: itemUuid, kind } + }) + ); + } + + } -]]; + +const drawerWidth = 240; +const appBarHeight = 100; + +type CssRules = 'root' | 'appBar' | 'drawerPaper' | 'content' | 'contentWrapper' | 'toolbar'; + +const styles: StyleRulesCallback = (theme: ArvadosTheme) => ({ + root: { + flexGrow: 1, + zIndex: 1, + overflow: 'hidden', + position: 'relative', + display: 'flex', + width: '100vw', + height: '100vh' + }, + appBar: { + zIndex: theme.zIndex.drawer + 1, + position: "absolute", + width: "100%" + }, + drawerPaper: { + position: 'relative', + width: drawerWidth, + display: 'flex', + flexDirection: 'column', + }, + contentWrapper: { + backgroundColor: theme.palette.background.default, + display: "flex", + flexGrow: 1, + minWidth: 0, + paddingTop: appBarHeight + }, + content: { + padding: `${theme.spacing.unit}px ${theme.spacing.unit * 3}px`, + overflowY: "auto", + flexGrow: 1 + }, + toolbar: theme.mixins.toolbar +}); export default connect( (state: RootState) => ({