From 540750a7749cb71ea0a8fde4b7a3689eeaa1c3dd Mon Sep 17 00:00:00 2001 From: Michal Klobukowski Date: Wed, 22 Aug 2018 23:12:36 +0200 Subject: [PATCH] Extract major components from workbench Feature #14102 Arvados-DCO-1.1-Signed-off-by: Michal Klobukowski --- .../collection-panel-action.ts | 20 +- .../updater/collection-updater-action.ts | 18 +- .../context-menu/context-menu-actions.ts | 20 +- .../details-panel/details-panel-action.ts | 41 +--- .../details-panel/details-panel-reducer.ts | 7 +- .../favorite-panel/favorite-panel-action.ts | 2 + .../favorite-panel-middleware-service.ts | 7 +- src/store/navigation/navigation-action.ts | 52 ++++- .../project-panel-middleware-service.ts | 7 +- src/store/project/project-action.ts | 22 +- .../action-sets/project-action-set.ts | 11 +- .../action-sets/root-project-action-set.ts | 8 +- .../data-explorer/renderers.tsx | 46 ++++- .../details-panel/details-panel.tsx | 22 +- .../dialog-create/dialog-project-create.tsx | 3 +- .../navigation-panel/navigation-panel.tsx | 111 ++++++++++ .../collection-panel/collection-panel.tsx | 156 +++++++------- .../favorite-panel/favorite-panel-item.ts | 29 --- src/views/favorite-panel/favorite-panel.tsx | 68 ++++--- src/views/project-panel/project-panel-item.ts | 31 --- src/views/project-panel/project-panel.tsx | 99 ++++++--- src/views/workbench/workbench.tsx | 190 ++---------------- 22 files changed, 498 insertions(+), 472 deletions(-) create mode 100644 src/views-components/navigation-panel/navigation-panel.tsx delete mode 100644 src/views/favorite-panel/favorite-panel-item.ts delete mode 100644 src/views/project-panel/project-panel-item.ts diff --git a/src/store/collection-panel/collection-panel-action.ts b/src/store/collection-panel/collection-panel-action.ts index 06d4d276..d8ad6d0a 100644 --- a/src/store/collection-panel/collection-panel-action.ts +++ b/src/store/collection-panel/collection-panel-action.ts @@ -2,16 +2,17 @@ // // SPDX-License-Identifier: AGPL-3.0 -import { unionize, ofType, UnionOf } from "unionize"; import { Dispatch } from "redux"; import { loadCollectionFiles } from "./collection-panel-files/collection-panel-files-actions"; -import { CollectionResource } from "~/models/collection"; +import { CollectionResource } from '~/models/collection'; import { collectionPanelFilesAction } from "./collection-panel-files/collection-panel-files-actions"; import { createTree } from "~/models/tree"; import { RootState } from "../store"; import { ServiceRepository } from "~/services/services"; import { TagResource, TagProperty } from "~/models/tag"; import { snackbarActions } from "../snackbar/snackbar-actions"; +import { resourcesActions } from "~/store/resources/resources-actions"; +import { unionize, ofType, UnionOf } from '~/common/unionize'; export const collectionPanelActions = unionize({ LOAD_COLLECTION: ofType<{ uuid: string }>(), @@ -22,22 +23,20 @@ export const collectionPanelActions = unionize({ CREATE_COLLECTION_TAG_SUCCESS: ofType<{ tag: TagResource }>(), DELETE_COLLECTION_TAG: ofType<{ uuid: string }>(), DELETE_COLLECTION_TAG_SUCCESS: ofType<{ uuid: string }>() -}, { tag: 'type', value: 'payload' }); +}); export type CollectionPanelAction = UnionOf; export const COLLECTION_TAG_FORM_NAME = 'collectionTagForm'; export const loadCollection = (uuid: string) => - (dispatch: Dispatch, getState: () => RootState, services: ServiceRepository) => { + async (dispatch: Dispatch, getState: () => RootState, services: ServiceRepository) => { dispatch(collectionPanelActions.LOAD_COLLECTION({ uuid })); dispatch(collectionPanelFilesAction.SET_COLLECTION_FILES({ files: createTree() })); - return services.collectionService - .get(uuid) - .then(item => { - dispatch(collectionPanelActions.LOAD_COLLECTION_SUCCESS({ item })); - dispatch(loadCollectionFiles(uuid)); - }); + const collection = await services.collectionService.get(uuid); + dispatch(resourcesActions.SET_RESOURCES([collection])); + dispatch(loadCollectionFiles(collection.uuid)); + dispatch(loadCollectionTags(collection.uuid)); }; export const loadCollectionTags = (uuid: string) => @@ -50,7 +49,6 @@ export const loadCollectionTags = (uuid: string) => }); }; - export const createCollectionTag = (data: TagProperty) => (dispatch: Dispatch, getState: () => RootState, services: ServiceRepository) => { dispatch(collectionPanelActions.CREATE_COLLECTION_TAG({ data })); diff --git a/src/store/collections/updater/collection-updater-action.ts b/src/store/collections/updater/collection-updater-action.ts index 2f520d4a..1ca1a83c 100644 --- a/src/store/collections/updater/collection-updater-action.ts +++ b/src/store/collections/updater/collection-updater-action.ts @@ -2,27 +2,22 @@ // // SPDX-License-Identifier: AGPL-3.0 -import { default as unionize, ofType, UnionOf } from "unionize"; import { Dispatch } from "redux"; - +import { unionize, ofType, UnionOf } from '~/common/unionize'; import { RootState } from "../../store"; import { ServiceRepository } from "~/services/services"; import { CollectionResource } from '~/models/collection'; import { initialize } from 'redux-form'; import { collectionPanelActions } from "../../collection-panel/collection-panel-action"; import { ContextMenuResource } from "../../context-menu/context-menu-reducer"; -import { updateDetails } from "~/store/details-panel/details-panel-action"; +import { resourcesActions } from "~/store/resources/resources-actions"; export const collectionUpdaterActions = unionize({ OPEN_COLLECTION_UPDATER: ofType<{ uuid: string }>(), CLOSE_COLLECTION_UPDATER: ofType<{}>(), UPDATE_COLLECTION_SUCCESS: ofType<{}>(), -}, { - tag: 'type', - value: 'payload' }); - export const COLLECTION_FORM_NAME = 'collectionEditDialog'; export const openUpdater = (item: ContextMenuResource) => @@ -39,11 +34,10 @@ export const updateCollection = (collection: Partial) => return services.collectionService .update(uuid, collection) .then(collection => { - dispatch(collectionPanelActions.LOAD_COLLECTION_SUCCESS({ item: collection as CollectionResource })); - dispatch(collectionUpdaterActions.UPDATE_COLLECTION_SUCCESS()); - dispatch(updateDetails(collection)); - } - ); + dispatch(collectionPanelActions.LOAD_COLLECTION_SUCCESS({ item: collection as CollectionResource })); + dispatch(collectionUpdaterActions.UPDATE_COLLECTION_SUCCESS()); + dispatch(resourcesActions.SET_RESOURCES([collection])); + }); }; export type CollectionUpdaterAction = UnionOf; diff --git a/src/store/context-menu/context-menu-actions.ts b/src/store/context-menu/context-menu-actions.ts index 8e5eb1e7..b517503d 100644 --- a/src/store/context-menu/context-menu-actions.ts +++ b/src/store/context-menu/context-menu-actions.ts @@ -2,15 +2,25 @@ // // SPDX-License-Identifier: AGPL-3.0 -import { default as unionize, ofType, UnionOf } from "unionize"; +import { unionize, ofType, UnionOf } from '~/common/unionize'; import { ContextMenuPosition, ContextMenuResource } from "./context-menu-reducer"; +import { ContextMenuKind } from '~/views-components/context-menu/context-menu'; +import { Dispatch } from 'redux'; export const contextMenuActions = unionize({ OPEN_CONTEXT_MENU: ofType<{ position: ContextMenuPosition, resource: ContextMenuResource }>(), CLOSE_CONTEXT_MENU: ofType<{}>() -}, { - tag: 'type', - value: 'payload' - }); +}); export type ContextMenuAction = UnionOf; + +export const openContextMenu = (event: React.MouseEvent, resource: { name: string; uuid: string; description?: string; kind: ContextMenuKind; }) => + (dispatch: Dispatch) => { + event.preventDefault(); + dispatch( + contextMenuActions.OPEN_CONTEXT_MENU({ + position: { x: event.clientX, y: event.clientY }, + resource + }) + ); + }; \ No newline at end of file diff --git a/src/store/details-panel/details-panel-action.ts b/src/store/details-panel/details-panel-action.ts index b8021fb6..2724a3e3 100644 --- a/src/store/details-panel/details-panel-action.ts +++ b/src/store/details-panel/details-panel-action.ts @@ -2,48 +2,17 @@ // // SPDX-License-Identifier: AGPL-3.0 -import { unionize, ofType, UnionOf } from "unionize"; -import { Dispatch } from "redux"; -import { Resource, ResourceKind } from "~/models/resource"; -import { RootState } from "../store"; -import { ServiceRepository } from "~/services/services"; +import { unionize, ofType, UnionOf } from '~/common/unionize'; export const detailsPanelActions = unionize({ TOGGLE_DETAILS_PANEL: ofType<{}>(), - LOAD_DETAILS: ofType<{ uuid: string, kind: ResourceKind }>(), - LOAD_DETAILS_SUCCESS: ofType<{ item: Resource }>(), - UPDATE_DETAILS: ofType<{ item: Resource }>() -}, { tag: 'type', value: 'payload' }); + LOAD_DETAILS_PANEL: ofType() +}); export type DetailsPanelAction = UnionOf; -export const loadDetails = (uuid: string, kind: ResourceKind) => - async (dispatch: Dispatch, getState: () => RootState, services: ServiceRepository) => { - dispatch(detailsPanelActions.LOAD_DETAILS({ uuid, kind })); - const item = await getService(services, kind).get(uuid); - dispatch(detailsPanelActions.LOAD_DETAILS_SUCCESS({ item })); - }; - -export const updateDetails = (item: Resource) => - async (dispatch: Dispatch, getState: () => RootState) => { - const currentItem = getState().detailsPanel.item; - if (currentItem && (currentItem.uuid === item.uuid)) { - dispatch(detailsPanelActions.UPDATE_DETAILS({ item })); - dispatch(detailsPanelActions.LOAD_DETAILS_SUCCESS({ item })); - } - }; - - -const getService = (services: ServiceRepository, kind: ResourceKind) => { - switch (kind) { - case ResourceKind.PROJECT: - return services.projectService; - case ResourceKind.COLLECTION: - return services.collectionService; - default: - return services.projectService; - } -}; +export const loadDetailsPanel = (uuid: string) => detailsPanelActions.LOAD_DETAILS_PANEL(uuid); + diff --git a/src/store/details-panel/details-panel-reducer.ts b/src/store/details-panel/details-panel-reducer.ts index f22add3d..091b2fa2 100644 --- a/src/store/details-panel/details-panel-reducer.ts +++ b/src/store/details-panel/details-panel-reducer.ts @@ -3,21 +3,20 @@ // SPDX-License-Identifier: AGPL-3.0 import { detailsPanelActions, DetailsPanelAction } from "./details-panel-action"; -import { Resource } from "~/models/resource"; export interface DetailsPanelState { - item: Resource | null; + resourceUuid: string; isOpened: boolean; } const initialState = { - item: null, + resourceUuid: '', isOpened: false }; export const detailsPanelReducer = (state: DetailsPanelState = initialState, action: DetailsPanelAction) => detailsPanelActions.match(action, { default: () => state, - LOAD_DETAILS_SUCCESS: ({ item }) => ({ ...state, item }), + LOAD_DETAILS_PANEL: resourceUuid => ({ ...state, resourceUuid }), TOGGLE_DETAILS_PANEL: () => ({ ...state, isOpened: !state.isOpened }) }); diff --git a/src/store/favorite-panel/favorite-panel-action.ts b/src/store/favorite-panel/favorite-panel-action.ts index aa1ec8d0..067d5cee 100644 --- a/src/store/favorite-panel/favorite-panel-action.ts +++ b/src/store/favorite-panel/favorite-panel-action.ts @@ -6,3 +6,5 @@ import { bindDataExplorerActions } from "../data-explorer/data-explorer-action"; export const FAVORITE_PANEL_ID = "favoritePanel"; export const favoritePanelActions = bindDataExplorerActions(FAVORITE_PANEL_ID); + +export const loadFavoritePanel = () => favoritePanelActions.REQUEST_ITEMS(); diff --git a/src/store/favorite-panel/favorite-panel-middleware-service.ts b/src/store/favorite-panel/favorite-panel-middleware-service.ts index 1c2f0622..e4be32d8 100644 --- a/src/store/favorite-panel/favorite-panel-middleware-service.ts +++ b/src/store/favorite-panel/favorite-panel-middleware-service.ts @@ -6,7 +6,6 @@ import { DataExplorerMiddlewareService } from "../data-explorer/data-explorer-mi import { FavoritePanelColumnNames, FavoritePanelFilter } from "~/views/favorite-panel/favorite-panel"; import { RootState } from "../store"; import { DataColumns } from "~/components/data-table/data-table"; -import { FavoritePanelItem, resourceToDataItem } from "~/views/favorite-panel/favorite-panel-item"; import { ServiceRepository } from "~/services/services"; import { SortDirection } from "~/components/data-table/data-column"; import { FilterBuilder } from "~/common/api/filter-builder"; @@ -16,6 +15,7 @@ import { Dispatch, MiddlewareAPI } from "redux"; import { OrderBuilder, OrderDirection } from "~/common/api/order-builder"; import { LinkResource } from "~/models/link"; import { GroupContentsResource, GroupContentsResourcePrefix } from "~/services/groups-service/groups-service"; +import { resourcesActions } from "~/store/resources/resources-actions"; export class FavoritePanelMiddlewareService extends DataExplorerMiddlewareService { constructor(private services: ServiceRepository, id: string) { @@ -24,7 +24,7 @@ export class FavoritePanelMiddlewareService extends DataExplorerMiddlewareServic requestItems(api: MiddlewareAPI) { const dataExplorer = api.getState().dataExplorer[this.getId()]; - const columns = dataExplorer.columns as DataColumns; + const columns = dataExplorer.columns as DataColumns; const sortColumn = dataExplorer.columns.find(c => c.sortDirection !== SortDirection.NONE); const typeFilters = this.getColumnFilters(columns, FavoritePanelColumnNames.TYPE); @@ -55,8 +55,9 @@ export class FavoritePanelMiddlewareService extends DataExplorerMiddlewareServic .getFilters() }) .then(response => { + api.dispatch(resourcesActions.SET_RESOURCES(response.items)); api.dispatch(favoritePanelActions.SET_ITEMS({ - items: response.items.map(resourceToDataItem), + items: response.items.map(resource => resource.uuid), itemsAvailable: response.itemsAvailable, page: Math.floor(response.offset / response.limit), rowsPerPage: response.limit diff --git a/src/store/navigation/navigation-action.ts b/src/store/navigation/navigation-action.ts index 50ec93d2..d440f190 100644 --- a/src/store/navigation/navigation-action.ts +++ b/src/store/navigation/navigation-action.ts @@ -8,7 +8,7 @@ import { push } from "react-router-redux"; import { TreeItemStatus } from "~/components/tree/tree"; import { findTreeItem } from "../project/project-reducer"; import { RootState } from "../store"; -import { ResourceKind } from "~/models/resource"; +import { ResourceKind, Resource } from '~/models/resource'; import { projectPanelActions } from "../project-panel/project-panel-action"; import { getCollectionUrl } from "~/models/collection"; import { getProjectUrl, ProjectResource } from "~/models/project"; @@ -17,6 +17,12 @@ import { ServiceRepository } from "~/services/services"; import { sidePanelActions } from "../side-panel/side-panel-action"; import { SidePanelId } from "../side-panel/side-panel-reducer"; import { getUuidObjectType, ObjectTypes } from "~/models/object-types"; +import { getResource } from '~/store/resources/resources'; +import { loadDetailsPanel } from '~/store/details-panel/details-panel-action'; +import { loadCollection } from '~/store/collection-panel/collection-panel-action'; +import { GroupContentsResource } from "~/services/groups-service/groups-service"; +import { snackbarActions } from '../snackbar/snackbar-actions'; +import { resourceLabel } from "~/common/labels"; export const getResourceUrl = (resourceKind: ResourceKind, resourceUuid: string): string => { switch (resourceKind) { @@ -98,3 +104,47 @@ const loadBranch = async (uuids: string[], dispatch: Dispatch): Promise => return loadBranch(rest, dispatch); } }; + +export const navigateToResource = (uuid: string) => + (dispatch: Dispatch, getState: () => RootState) => { + const resource = getResource(uuid)(getState().resources); + resource + ? dispatch(getResourceNavigationAction(resource)) + : dispatch(resourceIsNotLoaded(uuid)); + }; + +const getResourceNavigationAction = (resource: Resource) => { + switch (resource.kind) { + case ResourceKind.COLLECTION: + return navigateToCollection(resource); + case ResourceKind.PROJECT: + return navigateToProject(resource); + default: + return cannotNavigateToResource(resource); + } +}; + +export const navigateToProject = ({ uuid }: Resource) => + (dispatch: Dispatch) => { + dispatch(setProjectItem(uuid, ItemMode.BOTH)); + dispatch(loadDetailsPanel(uuid)); + }; + +export const navigateToCollection = ({ uuid }: Resource) => + (dispatch: Dispatch) => { + dispatch(loadCollection(uuid)); + dispatch(push(getCollectionUrl(uuid))); + }; + +export const cannotNavigateToResource = ({ kind, uuid }: Resource) => + snackbarActions.OPEN_SNACKBAR({ + message: `${resourceLabel(kind)} identified by ${uuid} cannot be opened.`, + hideDuration: 3000 + }); + + +export const resourceIsNotLoaded = (uuid: string) => + snackbarActions.OPEN_SNACKBAR({ + message: `Resource identified by ${uuid} is not loaded.`, + hideDuration: 3000 + }); diff --git a/src/store/project-panel/project-panel-middleware-service.ts b/src/store/project-panel/project-panel-middleware-service.ts index 663add3e..0196ed42 100644 --- a/src/store/project-panel/project-panel-middleware-service.ts +++ b/src/store/project-panel/project-panel-middleware-service.ts @@ -7,7 +7,6 @@ import { ProjectPanelColumnNames, ProjectPanelFilter } from "~/views/project-pan import { RootState } from "../store"; import { DataColumns } from "~/components/data-table/data-table"; import { ServiceRepository } from "~/services/services"; -import { ProjectPanelItem, resourceToDataItem } from "~/views/project-panel/project-panel-item"; import { SortDirection } from "~/components/data-table/data-column"; import { OrderBuilder, OrderDirection } from "~/common/api/order-builder"; import { FilterBuilder } from "~/common/api/filter-builder"; @@ -16,6 +15,7 @@ import { checkPresenceInFavorites } from "../favorites/favorites-actions"; import { projectPanelActions } from "./project-panel-action"; import { Dispatch, MiddlewareAPI } from "redux"; import { ProjectResource } from "~/models/project"; +import { resourcesActions } from "~/store/resources/resources-actions"; export class ProjectPanelMiddlewareService extends DataExplorerMiddlewareService { constructor(private services: ServiceRepository, id: string) { @@ -25,7 +25,7 @@ export class ProjectPanelMiddlewareService extends DataExplorerMiddlewareService requestItems(api: MiddlewareAPI) { const state = api.getState(); const dataExplorer = state.dataExplorer[this.getId()]; - const columns = dataExplorer.columns as DataColumns; + const columns = dataExplorer.columns as DataColumns; const typeFilters = this.getColumnFilters(columns, ProjectPanelColumnNames.TYPE); const statusFilters = this.getColumnFilters(columns, ProjectPanelColumnNames.STATUS); const sortColumn = dataExplorer.columns.find(c => c.sortDirection !== SortDirection.NONE); @@ -58,8 +58,9 @@ export class ProjectPanelMiddlewareService extends DataExplorerMiddlewareService .getFilters() }) .then(response => { + api.dispatch(resourcesActions.SET_RESOURCES(response.items)); api.dispatch(projectPanelActions.SET_ITEMS({ - items: response.items.map(resourceToDataItem), + items: response.items.map(resource => resource.uuid), itemsAvailable: response.itemsAvailable, page: Math.floor(response.offset / response.limit), rowsPerPage: response.limit diff --git a/src/store/project/project-action.ts b/src/store/project/project-action.ts index 20176589..da58ed28 100644 --- a/src/store/project/project-action.ts +++ b/src/store/project/project-action.ts @@ -1,8 +1,8 @@ // Copyright (C) The Arvados Authors. All rights reserved. // // SPDX-License-Identifier: AGPL-3.0 -import { default as unionize, ofType, UnionOf } from "unionize"; +import { unionize, ofType, UnionOf } from '~/common/unionize'; import { ProjectResource } from "~/models/project"; import { Dispatch } from "redux"; import { FilterBuilder } from "~/common/api/filter-builder"; @@ -10,14 +10,15 @@ import { RootState } from "../store"; import { checkPresenceInFavorites } from "../favorites/favorites-actions"; import { ServiceRepository } from "~/services/services"; import { projectPanelActions } from "~/store/project-panel/project-panel-action"; -import { updateDetails } from "~/store/details-panel/details-panel-action"; +import { resourcesActions } from "~/store/resources/resources-actions"; +import { reset } from 'redux-form'; export const projectActions = unionize({ OPEN_PROJECT_CREATOR: ofType<{ ownerUuid: string }>(), CLOSE_PROJECT_CREATOR: ofType<{}>(), CREATE_PROJECT: ofType>(), CREATE_PROJECT_SUCCESS: ofType(), - OPEN_PROJECT_UPDATER: ofType<{ uuid: string}>(), + OPEN_PROJECT_UPDATER: ofType<{ uuid: string }>(), CLOSE_PROJECT_UPDATER: ofType<{}>(), UPDATE_PROJECT_SUCCESS: ofType(), REMOVE_PROJECT: ofType(), @@ -26,14 +27,11 @@ export const projectActions = unionize({ TOGGLE_PROJECT_TREE_ITEM_OPEN: ofType(), TOGGLE_PROJECT_TREE_ITEM_ACTIVE: ofType(), RESET_PROJECT_TREE_ACTIVITY: ofType() -}, { - tag: 'type', - value: 'payload' }); export const PROJECT_FORM_NAME = 'projectEditDialog'; -export const getProjectList = (parentUuid: string = '') => +export const getProjectList = (parentUuid: string = '') => (dispatch: Dispatch, getState: () => RootState, services: ServiceRepository) => { dispatch(projectActions.PROJECTS_REQUEST(parentUuid)); return services.projectService.list({ @@ -66,8 +64,16 @@ export const updateProject = (project: Partial) => dispatch(projectActions.UPDATE_PROJECT_SUCCESS(project)); dispatch(projectPanelActions.REQUEST_ITEMS()); dispatch(getProjectList(project.ownerUuid)); - dispatch(updateDetails(project)); + dispatch(resourcesActions.SET_RESOURCES([project])); }); }; +export const PROJECT_CREATE_DIALOG = "projectCreateDialog"; + +export const openProjectCreator = (ownerUuid: string) => + (dispatch: Dispatch) => { + dispatch(reset(PROJECT_CREATE_DIALOG)); + dispatch(projectActions.OPEN_PROJECT_CREATOR({ ownerUuid })); + }; + export type ProjectAction = UnionOf; 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 index 01e6004e..7f83fb24 100644 --- a/src/views-components/context-menu/action-sets/project-action-set.ts +++ b/src/views-components/context-menu/action-sets/project-action-set.ts @@ -2,15 +2,13 @@ // // SPDX-License-Identifier: AGPL-3.0 -import { reset, initialize } from "redux-form"; - +import { initialize } from "redux-form"; import { ContextMenuActionSet } from "../context-menu-action-set"; -import { projectActions, PROJECT_FORM_NAME } from "~/store/project/project-action"; +import { projectActions, PROJECT_FORM_NAME, openProjectCreator } from '~/store/project/project-action'; import { NewProjectIcon, RenameIcon, CopyIcon, MoveToIcon } from "~/components/icon/icon"; import { ToggleFavoriteAction } from "../actions/favorite-action"; import { toggleFavorite } from "~/store/favorites/favorites-actions"; import { favoritePanelActions } from "~/store/favorite-panel/favorite-panel-action"; -import { PROJECT_CREATE_DIALOG } from "../../dialog-create/dialog-project-create"; import { openMoveProjectDialog } from '~/store/move-project-dialog/move-project-dialog'; import { openProjectCopyDialog } from "~/views-components/project-copy-dialog/project-copy-dialog"; @@ -18,10 +16,7 @@ export const projectActionSet: ContextMenuActionSet = [[ { icon: NewProjectIcon, name: "New project", - execute: (dispatch, resource) => { - dispatch(reset(PROJECT_CREATE_DIALOG)); - dispatch(projectActions.OPEN_PROJECT_CREATOR({ ownerUuid: resource.uuid })); - } + execute: (dispatch, resource) => dispatch(openProjectCreator(resource.uuid)) }, { icon: RenameIcon, 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 index de3b954f..eb4a9a30 100644 --- 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 @@ -5,9 +5,8 @@ import { reset } from "redux-form"; import { ContextMenuActionSet } from "../context-menu-action-set"; -import { projectActions } from "~/store/project/project-action"; +import { openProjectCreator } from "~/store/project/project-action"; import { collectionCreateActions } from "~/store/collections/creator/collection-creator-action"; -import { PROJECT_CREATE_DIALOG } from "../../dialog-create/dialog-project-create"; import { COLLECTION_CREATE_DIALOG } from "../../dialog-create/dialog-collection-create"; import { NewProjectIcon, CollectionIcon } from "~/components/icon/icon"; @@ -15,10 +14,7 @@ export const rootProjectActionSet: ContextMenuActionSet = [[ { icon: NewProjectIcon, name: "New project", - execute: (dispatch, resource) => { - dispatch(reset(PROJECT_CREATE_DIALOG)); - dispatch(projectActions.OPEN_PROJECT_CREATOR({ ownerUuid: resource.uuid })); - } + execute: (dispatch, resource) => dispatch(openProjectCreator(resource.uuid)) }, { icon: CollectionIcon, diff --git a/src/views-components/data-explorer/renderers.tsx b/src/views-components/data-explorer/renderers.tsx index 1b07642a..abf18392 100644 --- a/src/views-components/data-explorer/renderers.tsx +++ b/src/views-components/data-explorer/renderers.tsx @@ -9,9 +9,14 @@ import { ResourceKind } from '~/models/resource'; import { ProjectIcon, CollectionIcon, ProcessIcon, DefaultIcon } from '~/components/icon/icon'; import { formatDate, formatFileSize } from '~/common/formatters'; import { resourceLabel } from '~/common/labels'; +import { connect } from 'react-redux'; +import { RootState } from '~/store/store'; +import { getResource } from '../../store/resources/resources'; +import { GroupContentsResource } from '~/services/groups-service/groups-service'; +import { ProcessResource } from '~/models/process'; -export const renderName = (item: {name: string; uuid: string, kind: string}) => +export const renderName = (item: { name: string; uuid: string, kind: string }) => {renderIcon(item)} @@ -28,8 +33,13 @@ export const renderName = (item: {name: string; uuid: string, kind: string}) => ; +export const ResourceName = connect( + (state: RootState, props: { uuid: string }) => { + const resource = getResource(props.uuid)(state.resources) as GroupContentsResource | undefined; + return resource || { name: '', uuid: '', kind: '' }; + })(renderName); -export const renderIcon = (item: {kind: string}) => { +export const renderIcon = (item: { kind: string }) => { switch (item.kind) { case ResourceKind.PROJECT: return ; @@ -46,22 +56,52 @@ export const renderDate = (date: string) => { return {formatDate(date)}; }; +export const ResourceLastModifiedDate = connect( + (state: RootState, props: { uuid: string }) => { + const resource = getResource(props.uuid)(state.resources) as GroupContentsResource | undefined; + return { date: resource ? resource.modifiedAt : '' }; + })((props: { date: string }) => renderDate(props.date)); + export const renderFileSize = (fileSize?: number) => {formatFileSize(fileSize)} ; +export const ResourceFileSize = connect( + (state: RootState, props: { uuid: string }) => { + const resource = getResource(props.uuid)(state.resources) as GroupContentsResource | undefined; + return {}; + })((props: { fileSize?: number }) => renderFileSize(props.fileSize)); + export const renderOwner = (owner: string) => {owner} ; +export const ResourceOwner = connect( + (state: RootState, props: { uuid: string }) => { + const resource = getResource(props.uuid)(state.resources) as GroupContentsResource | undefined; + return { owner: resource ? resource.ownerUuid : '' }; + })((props: { owner: string }) => renderOwner(props.owner)); + export const renderType = (type: string) => {resourceLabel(type)} ; -export const renderStatus = (item: {status?: string}) => +export const ResourceType = connect( + (state: RootState, props: { uuid: string }) => { + const resource = getResource(props.uuid)(state.resources) as GroupContentsResource | undefined; + return { type: resource ? resource.kind : '' }; + })((props: { type: string }) => renderType(props.type)); + +export const renderStatus = (item: { status?: string }) => {item.status || "-"} ; + +export const ProcessStatus = connect( + (state: RootState, props: { uuid: string }) => { + const resource = getResource(props.uuid)(state.resources) as ProcessResource | undefined; + return { status: resource ? resource.state : '-' }; + })((props: { status: string }) => renderType(props.status)); diff --git a/src/views-components/details-panel/details-panel.tsx b/src/views-components/details-panel/details-panel.tsx index 21d57aea..7aae7860 100644 --- a/src/views-components/details-panel/details-panel.tsx +++ b/src/views-components/details-panel/details-panel.tsx @@ -20,6 +20,7 @@ import { ProcessDetails } from "./process-details"; import { EmptyDetails } from "./empty-details"; import { DetailsData } from "./details-data"; import { DetailsResource } from "~/models/details"; +import { getResource } from '../../store/resources/resources'; type CssRules = 'drawerPaper' | 'container' | 'opened' | 'headerContainer' | 'headerIcon' | 'headerTitle' | 'tabContainer'; @@ -70,10 +71,13 @@ const getItem = (resource: DetailsResource): DetailsData => { } }; -const mapStateToProps = ({ detailsPanel }: RootState) => ({ - isOpened: detailsPanel.isOpened, - item: getItem(detailsPanel.item as DetailsResource) -}); +const mapStateToProps = ({ detailsPanel, resources }: RootState) => { + const resource = getResource(detailsPanel.resourceUuid)(resources) as DetailsResource; + return { + isOpened: detailsPanel.isOpened, + item: getItem(resource) + }; +}; const mapDispatchToProps = (dispatch: Dispatch) => ({ onCloseDrawer: () => { @@ -110,7 +114,7 @@ export const DetailsPanel = withStyles(styles)( const { tabsValue } = this.state; return ( + className={classnames([classes.container, { [classes.opened]: isOpened }])}> @@ -124,14 +128,14 @@ export const DetailsPanel = withStyles(styles)( - {} + {} - - + + {tabsValue === 0 && this.renderTabContainer( @@ -139,7 +143,7 @@ export const DetailsPanel = withStyles(styles)( )} {tabsValue === 1 && this.renderTabContainer( - + )} diff --git a/src/views-components/dialog-create/dialog-project-create.tsx b/src/views-components/dialog-create/dialog-project-create.tsx index e77114b3..d32582ea 100644 --- a/src/views-components/dialog-create/dialog-project-create.tsx +++ b/src/views-components/dialog-create/dialog-project-create.tsx @@ -10,6 +10,7 @@ import { Dialog, DialogActions, DialogContent, DialogTitle } from '@material-ui/ import { Button, StyleRulesCallback, WithStyles, withStyles, CircularProgress } from '@material-ui/core'; import { PROJECT_NAME_VALIDATION, PROJECT_DESCRIPTION_VALIDATION } from '~/validators/validators'; +import { PROJECT_CREATE_DIALOG } from '~/store/project/project-action'; type CssRules = "button" | "lastButton" | "formContainer" | "dialog" | "dialogTitle" | "createProgress" | "dialogActions"; @@ -52,8 +53,6 @@ interface DialogProjectProps { pristine: boolean; } -export const PROJECT_CREATE_DIALOG = "projectCreateDialog"; - export const DialogProjectCreate = compose( reduxForm({ form: PROJECT_CREATE_DIALOG }), withStyles(styles))( diff --git a/src/views-components/navigation-panel/navigation-panel.tsx b/src/views-components/navigation-panel/navigation-panel.tsx new file mode 100644 index 00000000..283e9be6 --- /dev/null +++ b/src/views-components/navigation-panel/navigation-panel.tsx @@ -0,0 +1,111 @@ +// 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/styles'; +import Drawer from '@material-ui/core/Drawer'; +import { connect } from "react-redux"; +import { ProjectTree } from '~/views-components/project-tree/project-tree'; +import { SidePanel, SidePanelItem } from '~/components/side-panel/side-panel'; +import { ArvadosTheme } from '~/common/custom-theme'; +import { RootState } from '~/store/store'; +import { TreeItem } from '~/components/tree/tree'; +import { ProjectResource } from '~/models/project'; +import { sidePanelActions } from '../../store/side-panel/side-panel-action'; +import { Dispatch } from 'redux'; +import { projectActions } from '~/store/project/project-action'; +import { navigateToResource } from '../../store/navigation/navigation-action'; +import { openContextMenu } from '~/store/context-menu/context-menu-actions'; +import { ContextMenuKind } from '~/views-components/context-menu/context-menu'; + + +const DRAWER_WITDH = 240; + +type CssRules = 'drawerPaper' | 'toolbar'; + +const styles: StyleRulesCallback = (theme: ArvadosTheme) => ({ + drawerPaper: { + position: 'relative', + width: DRAWER_WITDH, + display: 'flex', + flexDirection: 'column', + }, + toolbar: theme.mixins.toolbar +}); + +interface NavigationPanelDataProps { + projects: Array>; + sidePanelItems: SidePanelItem[]; +} + +interface NavigationPanelActionProps { + toggleSidePanelOpen: (panelItemId: string) => void; + toggleSidePanelActive: (panelItemId: string) => void; + toggleProjectOpen: (projectUuid: string) => void; + toggleProjectActive: (projectUuid: string) => void; + openRootContextMenu: (event: React.MouseEvent) => void; + openProjectContextMenu: (event: React.MouseEvent, item: TreeItem) => void; +} + +type NavigationPanelProps = NavigationPanelDataProps & NavigationPanelActionProps & WithStyles; + +const mapStateToProps = (state: RootState): NavigationPanelDataProps => ({ + projects: state.projects.items, + sidePanelItems: state.sidePanel +}); + +const mapDispatchToProps = (dispatch: Dispatch): NavigationPanelActionProps => ({ + toggleSidePanelOpen: panelItemId => { + dispatch(sidePanelActions.TOGGLE_SIDE_PANEL_ITEM_OPEN(panelItemId)); + }, + toggleSidePanelActive: panelItemId => { + dispatch(projectActions.RESET_PROJECT_TREE_ACTIVITY(panelItemId)); + + // const panelItem = this.props.sidePanelItems.find(it => it.id === itemId); + // if (panelItem && panelItem.activeAction) { + // panelItem.activeAction(this.props.dispatch, this.props.authService.getUuid()); + // } + }, + toggleProjectOpen: projectUuid => { + dispatch(navigateToResource(projectUuid)); + }, + toggleProjectActive: projectUuid => { + dispatch(navigateToResource(projectUuid)); + }, + openRootContextMenu: event => { + dispatch(openContextMenu(event, { + uuid: "", + name: "", + kind: ContextMenuKind.ROOT_PROJECT + })); + }, + openProjectContextMenu: (event, item) => { + dispatch(openContextMenu(event, { + uuid: item.data.uuid, + name: item.data.name, + kind: ContextMenuKind.PROJECT + })); + } +}); + +export const NavigationPanel = withStyles(styles)( + connect(mapStateToProps, mapDispatchToProps)( + ({ classes, sidePanelItems, projects, ...actions }: NavigationPanelProps) => +
+ + + + + ) +); diff --git a/src/views/collection-panel/collection-panel.tsx b/src/views/collection-panel/collection-panel.tsx index 559d4a9a..7621d95a 100644 --- a/src/views/collection-panel/collection-panel.tsx +++ b/src/views/collection-panel/collection-panel.tsx @@ -20,6 +20,10 @@ import { TagResource } from '~/models/tag'; import { CollectionTagForm } from './collection-tag-form'; import { deleteCollectionTag } from '~/store/collection-panel/collection-panel-action'; import { snackbarActions } from '~/store/snackbar/snackbar-actions'; +import { getResource } from '~/store/resources/resources'; +import { loadCollection } from '../../store/collection-panel/collection-panel-action'; +import { contextMenuActions } from '~/store/context-menu/context-menu-actions'; +import { ContextMenuKind } from '~/views-components/context-menu/context-menu'; type CssRules = 'card' | 'iconHeader' | 'tag' | 'copyIcon' | 'label' | 'value'; @@ -55,81 +59,96 @@ interface CollectionPanelDataProps { tags: TagResource[]; } -interface CollectionPanelActionProps { - onItemRouteChange: (collectionId: string) => void; - onContextMenu: (event: React.MouseEvent, item: CollectionResource) => void; -} - -type CollectionPanelProps = CollectionPanelDataProps & CollectionPanelActionProps & DispatchProp - & WithStyles & RouteComponentProps<{ id: string }>; +type CollectionPanelProps = CollectionPanelDataProps & DispatchProp + & WithStyles & RouteComponentProps<{ id: string }>; export const CollectionPanel = withStyles(styles)( - connect((state: RootState) => ({ - item: state.collectionPanel.item, - tags: state.collectionPanel.tags - }))( + connect((state: RootState, props: RouteComponentProps<{ id: string }>) => { + const collection = getResource(props.match.params.id)(state.resources); + return { + item: collection, + tags: state.collectionPanel.tags + }; + })( class extends React.Component { render() { - const { classes, item, tags, onContextMenu } = this.props; + const { classes, item, tags } = this.props; return
- - } - action={ - onContextMenu(event, item)}> - - - } - title={item && item.name } - subheader={item && item.description} /> - - - - - - this.onCopy() }> - - - - - - - - + + } + action={ + + + + } + title={item && item.name} + subheader={item && item.description} /> + + + + + + this.onCopy()}> + + + + + + + - - + + + - - - - - - - { - tags.map(tag => { - return ; - }) - } - + + + + + + + { + tags.map(tag => { + return ; + }) + } - - -
- -
-
; + + + +
+ +
+
; + } + + handleContextMenu = (event: React.MouseEvent) => { + event.preventDefault(); + const { uuid, name, description } = this.props.item; + const resource = { + uuid, + name, + description, + kind: ContextMenuKind.COLLECTION + }; + this.props.dispatch( + contextMenuActions.OPEN_CONTEXT_MENU({ + position: { x: event.clientX, y: event.clientY }, + resource + }) + ); } handleDelete = (uuid: string) => () => { @@ -143,9 +162,10 @@ export const CollectionPanel = withStyles(styles)( })); } - componentWillReceiveProps({ match, item, onItemRouteChange }: CollectionPanelProps) { - if (!item || match.params.id !== item.uuid) { - onItemRouteChange(match.params.id); + componentDidMount() { + const { match, item } = this.props; + if (!item && match.params.id) { + this.props.dispatch(loadCollection(match.params.id)); } } diff --git a/src/views/favorite-panel/favorite-panel-item.ts b/src/views/favorite-panel/favorite-panel-item.ts deleted file mode 100644 index 842b6d6b..00000000 --- a/src/views/favorite-panel/favorite-panel-item.ts +++ /dev/null @@ -1,29 +0,0 @@ -// Copyright (C) The Arvados Authors. All rights reserved. -// -// SPDX-License-Identifier: AGPL-3.0 - -import { GroupContentsResource } from "~/services/groups-service/groups-service"; -import { ResourceKind } from "~/models/resource"; - -export interface FavoritePanelItem { - uuid: string; - name: string; - kind: string; - url: string; - owner: string; - lastModified: string; - fileSize?: number; - status?: string; -} - -export function resourceToDataItem(r: GroupContentsResource): FavoritePanelItem { - return { - uuid: r.uuid, - name: r.name, - kind: r.kind, - url: "", - owner: r.ownerUuid, - lastModified: r.modifiedAt, - status: r.kind === ResourceKind.PROCESS ? r.state : undefined - }; -} diff --git a/src/views/favorite-panel/favorite-panel.tsx b/src/views/favorite-panel/favorite-panel.tsx index 125ea27d..dfe107a8 100644 --- a/src/views/favorite-panel/favorite-panel.tsx +++ b/src/views/favorite-panel/favorite-panel.tsx @@ -3,7 +3,6 @@ // SPDX-License-Identifier: AGPL-3.0 import * as React from 'react'; -import { FavoritePanelItem } from './favorite-panel-item'; import { StyleRulesCallback, WithStyles, withStyles } from '@material-ui/core'; import { DataExplorer } from "~/views-components/data-explorer/data-explorer"; import { DispatchProp, connect } from 'react-redux'; @@ -16,9 +15,14 @@ import { SortDirection } from '~/components/data-table/data-column'; import { ResourceKind } from '~/models/resource'; import { resourceLabel } from '~/common/labels'; import { ArvadosTheme } from '~/common/custom-theme'; -import { renderName, renderStatus, renderType, renderOwner, renderFileSize, renderDate } from '~/views-components/data-explorer/renderers'; -import { FAVORITE_PANEL_ID } from "~/store/favorite-panel/favorite-panel-action"; +import { FAVORITE_PANEL_ID, loadFavoritePanel } from "~/store/favorite-panel/favorite-panel-action"; +import { ResourceFileSize, ResourceLastModifiedDate, ProcessStatus, ResourceType, ResourceOwner, ResourceName } from '~/views-components/data-explorer/renderers'; import { FavoriteIcon } from '~/components/icon/icon'; +import { Dispatch } from 'redux'; +import { contextMenuActions } from '~/store/context-menu/context-menu-actions'; +import { ContextMenuKind } from '~/views-components/context-menu/context-menu'; +import { loadDetailsPanel } from '../../store/details-panel/details-panel-action'; +import { navigateToResource } from '~/store/navigation/navigation-action'; type CssRules = "toolbar" | "button"; @@ -45,14 +49,14 @@ export interface FavoritePanelFilter extends DataTableFilterItem { type: ResourceKind | ContainerRequestState; } -export const columns: DataColumns = [ +export const columns: DataColumns = [ { name: FavoritePanelColumnNames.NAME, selected: true, configurable: true, sortDirection: SortDirection.ASC, filters: [], - render: renderName, + render: uuid => , width: "450px" }, { @@ -77,7 +81,7 @@ export const columns: DataColumns = [ type: ContainerRequestState.UNCOMMITTED } ], - render: renderStatus, + render: uuid => , width: "75px" }, { @@ -102,7 +106,7 @@ export const columns: DataColumns = [ type: ResourceKind.PROJECT } ], - render: item => renderType(item.kind), + render: uuid => , width: "125px" }, { @@ -111,7 +115,7 @@ export const columns: DataColumns = [ configurable: true, sortDirection: SortDirection.NONE, filters: [], - render: item => renderOwner(item.owner), + render: uuid => , width: "200px" }, { @@ -120,7 +124,7 @@ export const columns: DataColumns = [ configurable: true, sortDirection: SortDirection.NONE, filters: [], - render: item => renderFileSize(item.fileSize), + render: uuid => , width: "50px" }, { @@ -129,7 +133,7 @@ export const columns: DataColumns = [ configurable: true, sortDirection: SortDirection.NONE, filters: [], - render: item => renderDate(item.lastModified), + render: uuid => , width: "150px" } ]; @@ -139,18 +143,40 @@ interface FavoritePanelDataProps { } interface FavoritePanelActionProps { - onItemClick: (item: FavoritePanelItem) => void; - onContextMenu: (event: React.MouseEvent, item: FavoritePanelItem) => void; + onItemClick: (item: string) => void; + onContextMenu: (event: React.MouseEvent, item: string) => void; onDialogOpen: (ownerUuid: string) => void; - onItemDoubleClick: (item: FavoritePanelItem) => void; - onItemRouteChange: (itemId: string) => void; + onItemDoubleClick: (item: string) => void; + onMount: () => void; } +const mapDispatchToProps = (dispatch: Dispatch): FavoritePanelActionProps => ({ + onContextMenu: (event, resourceUuid) => { + event.preventDefault(); + dispatch( + contextMenuActions.OPEN_CONTEXT_MENU({ + position: { x: event.clientX, y: event.clientY }, + resource: { name: '', uuid: resourceUuid, kind: ContextMenuKind.RESOURCE } + }) + ); + }, + onDialogOpen: (ownerUuid: string) => { return; }, + onItemClick: (resourceUuid: string) => { + dispatch(loadDetailsPanel(resourceUuid)); + }, + onItemDoubleClick: uuid => { + dispatch(navigateToResource(uuid)); + }, + onMount: () => { + dispatch(loadFavoritePanel()); + }, +}); + type FavoritePanelProps = FavoritePanelDataProps & FavoritePanelActionProps & DispatchProp - & WithStyles & RouteComponentProps<{ id: string }>; + & WithStyles & RouteComponentProps<{ id: string }>; export const FavoritePanel = withStyles(styles)( - connect((state: RootState) => ({ currentItemId: state.projects.currentItemId }))( + connect((state: RootState) => ({ currentItemId: state.projects.currentItemId }), mapDispatchToProps)( class extends React.Component { render() { return item.uuid} defaultIcon={FavoriteIcon} - defaultMessages={['Your favorites list is empty.']}/> - ; + defaultMessages={['Your favorites list is empty.']} />; } - componentWillReceiveProps({ match, currentItemId, onItemRouteChange }: FavoritePanelProps) { - if (match.params.id !== currentItemId) { - onItemRouteChange(match.params.id); - } + componentDidMount() { + this.props.onMount(); } } ) diff --git a/src/views/project-panel/project-panel-item.ts b/src/views/project-panel/project-panel-item.ts deleted file mode 100644 index f0318591..00000000 --- a/src/views/project-panel/project-panel-item.ts +++ /dev/null @@ -1,31 +0,0 @@ -// Copyright (C) The Arvados Authors. All rights reserved. -// -// SPDX-License-Identifier: AGPL-3.0 - -import { GroupContentsResource } from "~/services/groups-service/groups-service"; -import { ResourceKind } from "~/models/resource"; - -export interface ProjectPanelItem { - uuid: string; - name: string; - description?: string; - kind: string; - url: string; - owner: string; - lastModified: string; - fileSize?: number; - status?: string; -} - -export function resourceToDataItem(r: GroupContentsResource): ProjectPanelItem { - return { - uuid: r.uuid, - name: r.name, - description: r.description, - kind: r.kind, - url: "", - owner: r.ownerUuid, - lastModified: r.modifiedAt, - status: r.kind === ResourceKind.PROCESS ? r.state : undefined - }; -} diff --git a/src/views/project-panel/project-panel.tsx b/src/views/project-panel/project-panel.tsx index 0f958d2c..a2ae4cfd 100644 --- a/src/views/project-panel/project-panel.tsx +++ b/src/views/project-panel/project-panel.tsx @@ -3,7 +3,6 @@ // SPDX-License-Identifier: AGPL-3.0 import * as React from 'react'; -import { ProjectPanelItem } from './project-panel-item'; import { Button, StyleRulesCallback, WithStyles, withStyles } from '@material-ui/core'; import { DataExplorer } from "~/views-components/data-explorer/data-explorer"; import { DispatchProp, connect } from 'react-redux'; @@ -16,9 +15,21 @@ import { SortDirection } from '~/components/data-table/data-column'; import { ResourceKind } from '~/models/resource'; import { resourceLabel } from '~/common/labels'; import { ArvadosTheme } from '~/common/custom-theme'; -import { renderName, renderStatus, renderType, renderOwner, renderFileSize, renderDate } from '~/views-components/data-explorer/renderers'; -import { restoreBranch } from '~/store/navigation/navigation-action'; +import { ResourceFileSize, ResourceLastModifiedDate, ProcessStatus, ResourceType, ResourceOwner } from '~/views-components/data-explorer/renderers'; +import { restoreBranch, setProjectItem, ItemMode } from '~/store/navigation/navigation-action'; import { ProjectIcon } from '~/components/icon/icon'; +import { ResourceName } from '~/views-components/data-explorer/renderers'; +import { ResourcesState, getResource } from '~/store/resources/resources'; +import { loadDetailsPanel } from '~/store/details-panel/details-panel-action'; +import { ContextMenuKind } from '~/views-components/context-menu/context-menu'; +import { contextMenuActions } from '~/store/context-menu/context-menu-actions'; +import { CollectionResource } from '~/models/collection'; +import { ProjectResource } from '~/models/project'; +import { openProjectCreator } from '~/store/project/project-action'; +import { reset } from 'redux-form'; +import { COLLECTION_CREATE_DIALOG } from '~/views-components/dialog-create/dialog-collection-create'; +import { collectionCreateActions } from '~/store/collections/creator/collection-creator-action'; +import { navigateToResource } from '~/store/navigation/navigation-action'; type CssRules = 'root' | "toolbar" | "button"; @@ -50,14 +61,14 @@ export interface ProjectPanelFilter extends DataTableFilterItem { type: ResourceKind | ContainerRequestState; } -export const columns: DataColumns = [ +export const columns: DataColumns = [ { name: ProjectPanelColumnNames.NAME, selected: true, configurable: true, sortDirection: SortDirection.ASC, filters: [], - render: renderName, + render: uuid => , width: "450px" }, { @@ -82,7 +93,7 @@ export const columns: DataColumns = [ type: ContainerRequestState.UNCOMMITTED } ], - render: renderStatus, + render: uuid => , width: "75px" }, { @@ -107,7 +118,7 @@ export const columns: DataColumns = [ type: ResourceKind.PROJECT } ], - render: item => renderType(item.kind), + render: uuid => , width: "125px" }, { @@ -116,7 +127,7 @@ export const columns: DataColumns = [ configurable: true, sortDirection: SortDirection.NONE, filters: [], - render: item => renderOwner(item.owner), + render: uuid => , width: "200px" }, { @@ -125,7 +136,7 @@ export const columns: DataColumns = [ configurable: true, sortDirection: SortDirection.NONE, filters: [], - render: item => renderFileSize(item.fileSize), + render: uuid => , width: "50px" }, { @@ -134,7 +145,7 @@ export const columns: DataColumns = [ configurable: true, sortDirection: SortDirection.NONE, filters: [], - render: item => renderDate(item.lastModified), + render: uuid => , width: "150px" } ]; @@ -143,22 +154,14 @@ export const PROJECT_PANEL_ID = "projectPanel"; interface ProjectPanelDataProps { currentItemId: string; + resources: ResourcesState; } -interface ProjectPanelActionProps { - onItemClick: (item: ProjectPanelItem) => void; - onContextMenu: (event: React.MouseEvent, item: ProjectPanelItem) => void; - onProjectCreationDialogOpen: (ownerUuid: string) => void; - onCollectionCreationDialogOpen: (ownerUuid: string) => void; - onItemDoubleClick: (item: ProjectPanelItem) => void; - onItemRouteChange: (itemId: string) => void; -} - -type ProjectPanelProps = ProjectPanelDataProps & ProjectPanelActionProps & DispatchProp +type ProjectPanelProps = ProjectPanelDataProps & DispatchProp & WithStyles & RouteComponentProps<{ id: string }>; export const ProjectPanel = withStyles(styles)( - connect((state: RootState) => ({ currentItemId: state.projects.currentItemId }))( + connect((state: RootState) => ({ currentItemId: state.projects.currentItemId, resources: state.resources }))( class extends React.Component { render() { const { classes } = this.props; @@ -177,32 +180,64 @@ export const ProjectPanel = withStyles(styles)( item.uuid} + onRowClick={this.handleRowClick} + onRowDoubleClick={this.handleRowDoubleClick} + onContextMenu={this.handleContextMenu} defaultIcon={ProjectIcon} defaultMessages={['Your project is empty.', 'Please create a project or create a collection and upload a data.']} /> ; } handleNewProjectClick = () => { - this.props.onProjectCreationDialogOpen(this.props.currentItemId); + this.props.dispatch(openProjectCreator(this.props.currentItemId)); } handleNewCollectionClick = () => { - this.props.onCollectionCreationDialogOpen(this.props.currentItemId); + this.props.dispatch(reset(COLLECTION_CREATE_DIALOG)); + this.props.dispatch(collectionCreateActions.OPEN_COLLECTION_CREATOR({ ownerUuid: this.props.currentItemId })); } - componentWillReceiveProps({ match, currentItemId, onItemRouteChange }: ProjectPanelProps) { - if (match.params.id !== currentItemId) { - onItemRouteChange(match.params.id); + handleContextMenu = (event: React.MouseEvent, resourceUuid: string) => { + event.preventDefault(); + const resource = getResource(resourceUuid)(this.props.resources) as CollectionResource | ProjectResource | undefined; + if (resource) { + let kind: ContextMenuKind; + + if (resource.kind === ResourceKind.PROJECT) { + kind = ContextMenuKind.PROJECT; + } else if (resource.kind === ResourceKind.COLLECTION) { + kind = ContextMenuKind.COLLECTION_RESOURCE; + } else { + kind = ContextMenuKind.RESOURCE; + } + if (kind !== ContextMenuKind.RESOURCE) { + this.props.dispatch( + contextMenuActions.OPEN_CONTEXT_MENU({ + position: { x: event.clientX, y: event.clientY }, + resource: { + uuid: resource.uuid, + name: resource.name || '', + description: resource.description, + kind, + } + }) + ); + } } } - componentDidMount() { + handleRowDoubleClick = (uuid: string) => { + this.props.dispatch(navigateToResource(uuid)); + } + + handleRowClick = (uuid: string) => { + this.props.dispatch(loadDetailsPanel(uuid)); + } + + async componentDidMount() { if (this.props.match.params.id && this.props.currentItemId === '') { - this.props.dispatch(restoreBranch(this.props.match.params.id)); + await this.props.dispatch(restoreBranch(this.props.match.params.id)); + this.props.dispatch(setProjectItem(this.props.match.params.id, ItemMode.BOTH)); } } } diff --git a/src/views/workbench/workbench.tsx b/src/views/workbench/workbench.tsx index ed11eb10..2dda4d23 100644 --- a/src/views/workbench/workbench.tsx +++ b/src/views/workbench/workbench.tsx @@ -4,42 +4,30 @@ 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, RouteComponentProps, Switch, Redirect } from "react-router"; +import { Route, Switch, Redirect } from "react-router"; import { login, logout } from "~/store/auth/auth-action"; import { User } from "~/models/user"; import { RootState } from "~/store/store"; import { MainAppBar, MainAppBarActionProps, MainAppBarMenuItem } from '~/views-components/main-app-bar/main-app-bar'; import { Breadcrumb } from '~/components/breadcrumbs/breadcrumbs'; import { push } from 'react-router-redux'; -import { reset } from 'redux-form'; -import { ProjectTree } from '~/views-components/project-tree/project-tree'; import { TreeItem } from "~/components/tree/tree"; 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 { ItemMode, setProjectItem } from "~/store/navigation/navigation-action"; -import { projectActions } from "~/store/project/project-action"; -import { collectionCreateActions } from '~/store/collections/creator/collection-creator-action'; import { ProjectPanel } from "~/views/project-panel/project-panel"; import { DetailsPanel } from '~/views-components/details-panel/details-panel'; import { ArvadosTheme } from '~/common/custom-theme'; import { CreateProjectDialog } from "~/views-components/create-project-dialog/create-project-dialog"; - -import { detailsPanelActions, loadDetails } from "~/store/details-panel/details-panel-action"; -import { contextMenuActions } from "~/store/context-menu/context-menu-actions"; +import { detailsPanelActions, loadDetailsPanel } from "~/store/details-panel/details-panel-action"; +import { openContextMenu } from '~/store/context-menu/context-menu-actions'; import { ProjectResource } from '~/models/project'; -import { ResourceKind } from '~/models/resource'; import { ContextMenu, ContextMenuKind } from "~/views-components/context-menu/context-menu"; import { FavoritePanel } from "../favorite-panel/favorite-panel"; import { CurrentTokenDialog } from '~/views-components/current-token-dialog/current-token-dialog'; import { Snackbar } from '~/views-components/snackbar/snackbar'; -import { favoritePanelActions } from '~/store/favorite-panel/favorite-panel-action'; import { CreateCollectionDialog } from '~/views-components/create-collection-dialog/create-collection-dialog'; import { CollectionPanel } from '../collection-panel/collection-panel'; -import { loadCollection, loadCollectionTags } from '~/store/collection-panel/collection-panel-action'; -import { getCollectionUrl } from '~/models/collection'; import { UpdateCollectionDialog } from '~/views-components/update-collection-dialog/update-collection-dialog.'; import { UpdateProjectDialog } from '~/views-components/update-project-dialog/update-project-dialog'; import { AuthService } from "~/services/auth-service/auth-service"; @@ -47,18 +35,16 @@ import { RenameFileDialog } from '~/views-components/rename-file-dialog/rename-f import { FileRemoveDialog } from '~/views-components/file-remove-dialog/file-remove-dialog'; import { MultipleFilesRemoveDialog } from '~/views-components/file-remove-dialog/multiple-files-remove-dialog'; import { DialogCollectionCreateWithSelectedFile } from '~/views-components/create-collection-dialog-with-selected/create-collection-dialog-with-selected'; -import { COLLECTION_CREATE_DIALOG } from '~/views-components/dialog-create/dialog-collection-create'; -import { PROJECT_CREATE_DIALOG } from '~/views-components/dialog-create/dialog-project-create'; import { UploadCollectionFilesDialog } from '~/views-components/upload-collection-files-dialog/upload-collection-files-dialog'; import { ProjectCopyDialog } from '~/views-components/project-copy-dialog/project-copy-dialog'; import { CollectionPartialCopyDialog } from '../../views-components/collection-partial-copy-dialog/collection-partial-copy-dialog'; import { MoveProjectDialog } from '~/views-components/move-project-dialog/move-project-dialog'; import { MoveCollectionDialog } from '~/views-components/move-collection-dialog/move-collection-dialog'; +import { NavigationPanel } from '~/views-components/navigation-panel/navigation-panel'; -const DRAWER_WITDH = 240; const APP_BAR_HEIGHT = 100; -type CssRules = 'root' | 'appBar' | 'drawerPaper' | 'content' | 'contentWrapper' | 'toolbar'; +type CssRules = 'root' | 'appBar' | 'content' | 'contentWrapper'; const styles: StyleRulesCallback = (theme: ArvadosTheme) => ({ root: { @@ -75,12 +61,6 @@ const styles: StyleRulesCallback = (theme: ArvadosTheme) => ({ position: "absolute", width: "100%" }, - drawerPaper: { - position: 'relative', - width: DRAWER_WITDH, - display: 'flex', - flexDirection: 'column', - }, contentWrapper: { backgroundColor: theme.palette.background.default, display: "flex", @@ -94,7 +74,6 @@ const styles: StyleRulesCallback = (theme: ArvadosTheme) => ({ flexGrow: 1, position: 'relative' }, - toolbar: theme.mixins.toolbar }); interface WorkbenchDataProps { @@ -102,7 +81,6 @@ interface WorkbenchDataProps { currentProjectId: string; user?: User; currentToken?: string; - sidePanelItems: SidePanelItem[]; } interface WorkbenchGeneralProps { @@ -141,12 +119,10 @@ export const Workbench = withStyles(styles)( currentProjectId: state.projects.currentItemId, user: state.auth.user, currentToken: state.auth.apiToken, - sidePanelItems: state.sidePanel }) )( class extends React.Component { state = { - isCreationDialogOpen: false, isCurrentTokenDialogOpen: false, anchorEl: null, searchText: "", @@ -201,43 +177,14 @@ export const Workbench = withStyles(styles)( buildInfo={this.props.buildInfo} {...this.mainAppBarActions} /> - {user && - -
- this.openContextMenu(event, { - uuid: this.props.authService.getUuid() || "", - name: "", - kind: ContextMenuKind.ROOT_PROJECT - })}> - this.props.dispatch(setProjectItem(itemId, ItemMode.OPEN))} - onContextMenu={(event, item) => this.openContextMenu(event, { - uuid: item.data.uuid, - name: item.data.name, - kind: ContextMenuKind.PROJECT - })} - toggleActive={itemId => { - this.props.dispatch(setProjectItem(itemId, ItemMode.ACTIVE)); - this.props.dispatch(loadDetails(itemId, ResourceKind.PROJECT)); - }} /> - - } + {user && }
} /> - - - + + +
{user && } @@ -265,90 +212,10 @@ export const Workbench = withStyles(styles)( ); } - renderCollectionPanel = (props: RouteComponentProps<{ id: string }>) => { - this.props.dispatch(loadCollection(collectionId)); - this.props.dispatch(loadCollectionTags(collectionId)); - }} - onContextMenu={(event, item) => { - this.openContextMenu(event, { - uuid: item.uuid, - name: item.name, - description: item.description, - kind: ContextMenuKind.COLLECTION - }); - }} - {...props} /> - - renderProjectPanel = (props: RouteComponentProps<{ id: string }>) => this.props.dispatch(setProjectItem(itemId, ItemMode.ACTIVE))} - onContextMenu={(event, item) => { - let kind: ContextMenuKind; - - if (item.kind === ResourceKind.PROJECT) { - kind = ContextMenuKind.PROJECT; - } else if (item.kind === ResourceKind.COLLECTION) { - kind = ContextMenuKind.COLLECTION_RESOURCE; - } else { - kind = ContextMenuKind.RESOURCE; - } - - this.openContextMenu(event, { - uuid: item.uuid, - name: item.name, - description: item.description, - kind - }); - }} - onProjectCreationDialogOpen={this.handleProjectCreationDialogOpen} - onCollectionCreationDialogOpen={this.handleCollectionCreationDialogOpen} - onItemClick={item => { - this.props.dispatch(loadDetails(item.uuid, item.kind as ResourceKind)); - }} - onItemDoubleClick={item => { - switch (item.kind) { - case ResourceKind.COLLECTION: - this.props.dispatch(loadCollection(item.uuid)); - this.props.dispatch(push(getCollectionUrl(item.uuid))); - default: - this.props.dispatch(setProjectItem(item.uuid, ItemMode.ACTIVE)); - this.props.dispatch(loadDetails(item.uuid, item.kind as ResourceKind)); - } - - }} - {...props} /> - - renderFavoritePanel = (props: RouteComponentProps<{ id: string }>) => this.props.dispatch(favoritePanelActions.REQUEST_ITEMS())} - onContextMenu={(event, item) => { - const kind = item.kind === ResourceKind.PROJECT ? ContextMenuKind.PROJECT : ContextMenuKind.RESOURCE; - this.openContextMenu(event, { - uuid: item.uuid, - name: item.name, - kind, - }); - }} - onDialogOpen={this.handleProjectCreationDialogOpen} - onItemClick={item => { - this.props.dispatch(loadDetails(item.uuid, item.kind as ResourceKind)); - }} - onItemDoubleClick={item => { - switch (item.kind) { - case ResourceKind.COLLECTION: - this.props.dispatch(loadCollection(item.uuid)); - this.props.dispatch(push(getCollectionUrl(item.uuid))); - default: - this.props.dispatch(loadDetails(item.uuid, ResourceKind.PROJECT)); - this.props.dispatch(setProjectItem(item.uuid, ItemMode.ACTIVE)); - } - - }} - {...props} /> - mainAppBarActions: MainAppBarActionProps = { onBreadcrumbClick: ({ itemId }: NavBreadcrumb) => { this.props.dispatch(setProjectItem(itemId, ItemMode.BOTH)); - this.props.dispatch(loadDetails(itemId, ResourceKind.PROJECT)); + this.props.dispatch(loadDetailsPanel(itemId)); }, onSearch: searchText => { this.setState({ searchText }); @@ -359,47 +226,14 @@ export const Workbench = withStyles(styles)( this.props.dispatch(detailsPanelActions.TOGGLE_DETAILS_PANEL()); }, onContextMenu: (event: React.MouseEvent, breadcrumb: NavBreadcrumb) => { - this.openContextMenu(event, { + this.props.dispatch(openContextMenu(event, { uuid: breadcrumb.itemId, name: breadcrumb.label, kind: ContextMenuKind.PROJECT - }); + })); } }; - toggleSidePanelOpen = (itemId: string) => { - this.props.dispatch(sidePanelActions.TOGGLE_SIDE_PANEL_ITEM_OPEN(itemId)); - } - - toggleSidePanelActive = (itemId: string) => { - this.props.dispatch(projectActions.RESET_PROJECT_TREE_ACTIVITY(itemId)); - - const panelItem = this.props.sidePanelItems.find(it => it.id === itemId); - if (panelItem && panelItem.activeAction) { - panelItem.activeAction(this.props.dispatch, this.props.authService.getUuid()); - } - } - - handleProjectCreationDialogOpen = (itemUuid: string) => { - this.props.dispatch(reset(PROJECT_CREATE_DIALOG)); - this.props.dispatch(projectActions.OPEN_PROJECT_CREATOR({ ownerUuid: itemUuid })); - } - - handleCollectionCreationDialogOpen = (itemUuid: string) => { - this.props.dispatch(reset(COLLECTION_CREATE_DIALOG)); - this.props.dispatch(collectionCreateActions.OPEN_COLLECTION_CREATOR({ ownerUuid: itemUuid })); - } - - openContextMenu = (event: React.MouseEvent, resource: { name: string; uuid: string; description?: string; kind: ContextMenuKind; }) => { - event.preventDefault(); - this.props.dispatch( - contextMenuActions.OPEN_CONTEXT_MENU({ - position: { x: event.clientX, y: event.clientY }, - resource - }) - ); - } - toggleCurrentTokenModal = () => { this.setState({ isCurrentTokenDialogOpen: !this.state.isCurrentTokenDialogOpen }); } -- 2.30.2