From 1223a1b7def439123f2950a6f4627a170483e779 Mon Sep 17 00:00:00 2001 From: Pawel Kowalczyk Date: Fri, 28 Sep 2018 13:01:58 +0200 Subject: [PATCH] init-file-selection-tree Feature #14231 Arvados-DCO-1.1-Signed-off-by: Pawel Kowalczyk --- src/store/store.ts | 4 +- .../workflow-tree-picker-actions.ts | 18 ++ .../workflow-tree-picker-reducer.ts | 73 ++++++++ .../workflow-tree-picker.ts | 25 +++ .../dialog-move/dialog-move-to.tsx | 4 +- .../main-workflow-tree-picker.ts | 57 +++++++ .../workflow-tree-picker.tsx | 156 ++++++++++++++++++ .../workflow-panel/workflow-panel-view.tsx | 1 + 8 files changed, 335 insertions(+), 3 deletions(-) create mode 100644 src/store/workflow-tree-picker/workflow-tree-picker-actions.ts create mode 100644 src/store/workflow-tree-picker/workflow-tree-picker-reducer.ts create mode 100644 src/store/workflow-tree-picker/workflow-tree-picker.ts create mode 100644 src/views-components/workflow-tree-picker/main-workflow-tree-picker.ts create mode 100644 src/views-components/workflow-tree-picker/workflow-tree-picker.tsx diff --git a/src/store/store.ts b/src/store/store.ts index 16d0d055e3..ab4b851dcd 100644 --- a/src/store/store.ts +++ b/src/store/store.ts @@ -37,6 +37,7 @@ import { SharedWithMeMiddlewareService } from './shared-with-me-panel/shared-wit import { progressIndicatorReducer } from './progress-indicator/progress-indicator-reducer'; import { WorkflowMiddlewareService } from './workflow-panel/workflow-middleware-service'; import { WORKFLOW_PANEL_ID } from './workflow-panel/workflow-panel-actions'; +import { workflowTreePickerReducer } from './workflow-tree-picker/workflow-tree-picker-reducer'; const composeEnhancers = (process.env.NODE_ENV === 'development' && @@ -97,5 +98,6 @@ const createRootReducer = (services: ServiceRepository) => combineReducers({ treePicker: treePickerReducer, fileUploader: fileUploaderReducer, processPanel: processPanelReducer, - progressIndicator: progressIndicatorReducer + progressIndicator: progressIndicatorReducer, + workflowTreePicker: workflowTreePickerReducer }); diff --git a/src/store/workflow-tree-picker/workflow-tree-picker-actions.ts b/src/store/workflow-tree-picker/workflow-tree-picker-actions.ts new file mode 100644 index 0000000000..6fd66ec1e8 --- /dev/null +++ b/src/store/workflow-tree-picker/workflow-tree-picker-actions.ts @@ -0,0 +1,18 @@ +// Copyright (C) The Arvados Authors. All rights reserved. +// +// SPDX-License-Identifier: AGPL-3.0 + +import { unionize, ofType, UnionOf } from "~/common/unionize"; + +import { TreePickerNode } from "./workflow-tree-picker"; + +export const workflowTreePickerActions = unionize({ + LOAD_TREE_PICKER_NODE: ofType<{ nodeId: string, pickerId: string }>(), + LOAD_TREE_PICKER_NODE_SUCCESS: ofType<{ nodeId: string, nodes: Array, pickerId: string }>(), + TOGGLE_TREE_PICKER_NODE_COLLAPSE: ofType<{ nodeId: string, pickerId: string }>(), + TOGGLE_TREE_PICKER_NODE_SELECT: ofType<{ nodeId: string, pickerId: string }>(), + EXPAND_TREE_PICKER_NODES: ofType<{ nodeIds: string[], pickerId: string }>(), + RESET_TREE_PICKER: ofType<{ pickerId: string }>() +}); + +export type WorkflowTreePickerAction = UnionOf; diff --git a/src/store/workflow-tree-picker/workflow-tree-picker-reducer.ts b/src/store/workflow-tree-picker/workflow-tree-picker-reducer.ts new file mode 100644 index 0000000000..a0c2d48661 --- /dev/null +++ b/src/store/workflow-tree-picker/workflow-tree-picker-reducer.ts @@ -0,0 +1,73 @@ +// Copyright (C) The Arvados Authors. All rights reserved. +// +// SPDX-License-Identifier: AGPL-3.0 + +import { createTree, setNodeValueWith, TreeNode, setNode, mapTreeValues, Tree } from "~/models/tree"; +import { TreePicker, TreePickerNode } from "./workflow-tree-picker"; +import { workflowTreePickerActions, WorkflowTreePickerAction } from "./workflow-tree-picker-actions"; +import { TreeItemStatus } from "~/components/tree/tree"; +import { compose } from "redux"; +import { getNode } from '~/models/tree'; + +export const workflowTreePickerReducer = (state: TreePicker = {}, action: WorkflowTreePickerAction) => + workflowTreePickerActions.match(action, { + LOAD_TREE_PICKER_NODE: ({ nodeId, pickerId }) => + updateOrCreatePicker(state, pickerId, setNodeValueWith(setPending)(nodeId)), + LOAD_TREE_PICKER_NODE_SUCCESS: ({ nodeId, nodes, pickerId }) => + updateOrCreatePicker(state, pickerId, compose(receiveNodes(nodes)(nodeId), setNodeValueWith(setLoaded)(nodeId))), + TOGGLE_TREE_PICKER_NODE_COLLAPSE: ({ nodeId, pickerId }) => + updateOrCreatePicker(state, pickerId, setNodeValueWith(toggleCollapse)(nodeId)), + TOGGLE_TREE_PICKER_NODE_SELECT: ({ nodeId, pickerId }) => + updateOrCreatePicker(state, pickerId, mapTreeValues(toggleSelect(nodeId))), + RESET_TREE_PICKER: ({ pickerId }) => + updateOrCreatePicker(state, pickerId, createTree), + EXPAND_TREE_PICKER_NODES: ({ pickerId, nodeIds }) => + updateOrCreatePicker(state, pickerId, mapTreeValues(expand(nodeIds))), + default: () => state + }); + +const updateOrCreatePicker = (state: TreePicker, pickerId: string, func: (value: Tree) => Tree) => { + const picker = state[pickerId] || createTree(); + const updatedPicker = func(picker); + return { ...state, [pickerId]: updatedPicker }; +}; + +const expand = (ids: string[]) => (node: TreePickerNode): TreePickerNode => + ids.some(id => id === node.nodeId) + ? { ...node, collapsed: false } + : node; + +const setPending = (value: TreePickerNode): TreePickerNode => + ({ ...value, status: TreeItemStatus.PENDING }); + +const setLoaded = (value: TreePickerNode): TreePickerNode => + ({ ...value, status: TreeItemStatus.LOADED }); + +const toggleCollapse = (value: TreePickerNode): TreePickerNode => + ({ ...value, collapsed: !value.collapsed }); + +const toggleSelect = (nodeId: string) => (value: TreePickerNode): TreePickerNode => + value.nodeId === nodeId + ? ({ ...value, selected: !value.selected }) + : ({ ...value, selected: false }); + +const receiveNodes = (nodes: Array) => (parent: string) => (state: Tree) => { + const parentNode = getNode(parent)(state); + let newState = state; + if (parentNode) { + newState = setNode({ ...parentNode, children: [] })(state); + } + return nodes.reduce((tree, node) => { + const oldNode = getNode(node.nodeId)(state) || { value: {} }; + const newNode = createTreeNode(parent)(node); + const value = { ...oldNode.value, ...newNode.value }; + return setNode({ ...newNode, value })(tree); + }, newState); +}; + +const createTreeNode = (parent: string) => (node: TreePickerNode): TreeNode => ({ + children: [], + id: node.nodeId, + parent, + value: node +}); diff --git a/src/store/workflow-tree-picker/workflow-tree-picker.ts b/src/store/workflow-tree-picker/workflow-tree-picker.ts new file mode 100644 index 0000000000..259a4b8d53 --- /dev/null +++ b/src/store/workflow-tree-picker/workflow-tree-picker.ts @@ -0,0 +1,25 @@ +// Copyright (C) The Arvados Authors. All rights reserved. +// +// SPDX-License-Identifier: AGPL-3.0 + +import { Tree } from "~/models/tree"; +import { TreeItemStatus } from "~/components/tree/tree"; + +export type TreePicker = { [key: string]: Tree }; + +export interface TreePickerNode { + nodeId: string; + value: Value; + selected: boolean; + collapsed: boolean; + status: TreeItemStatus; +} + +export const createTreePickerNode = (data: { nodeId: string, value: any }) => ({ + ...data, + selected: false, + collapsed: true, + status: TreeItemStatus.INITIAL +}); + +export const getTreePicker = (id: string) => (state: TreePicker): Tree> | undefined => state[id]; \ No newline at end of file diff --git a/src/views-components/dialog-move/dialog-move-to.tsx b/src/views-components/dialog-move/dialog-move-to.tsx index 425b9e462a..6b0ac88100 100644 --- a/src/views-components/dialog-move/dialog-move-to.tsx +++ b/src/views-components/dialog-move/dialog-move-to.tsx @@ -6,7 +6,7 @@ import * as React from "react"; import { InjectedFormProps, Field } from 'redux-form'; import { WithDialogProps } from '~/store/dialog/with-dialog'; import { FormDialog } from '~/components/form-dialog/form-dialog'; -import { ProjectTreePickerField } from '~/views-components/project-tree-picker/project-tree-picker'; +import { WorkflowTreePickerField } from '~/views-components/workflow-tree-picker/workflow-tree-picker'; import { MOVE_TO_VALIDATION } from '~/validators/validators'; import { MoveToFormDialogData } from '~/store/move-to-dialog/move-to-dialog'; @@ -21,6 +21,6 @@ export const DialogMoveTo = (props: WithDialogProps & InjectedFormProps< const MoveToDialogFields = () => ; diff --git a/src/views-components/workflow-tree-picker/main-workflow-tree-picker.ts b/src/views-components/workflow-tree-picker/main-workflow-tree-picker.ts new file mode 100644 index 0000000000..4868160a6c --- /dev/null +++ b/src/views-components/workflow-tree-picker/main-workflow-tree-picker.ts @@ -0,0 +1,57 @@ +// Copyright (C) The Arvados Authors. All rights reserved. +// +// SPDX-License-Identifier: AGPL-3.0 + +import { connect } from "react-redux"; +import { Tree, TreeProps, TreeItem, TreeItemStatus } from "~/components/tree/tree"; +import { RootState } from "~/store/store"; +import { createTreePickerNode, TreePickerNode } from "~/store/workflow-tree-picker/workflow-tree-picker"; +import { getNodeValue, getNodeChildrenIds, Tree as Ttree, createTree } from "~/models/tree"; +import { Dispatch } from "redux"; + +export interface MainWorkflowTreePickerProps { + pickerId: string; + onContextMenu: (event: React.MouseEvent, nodeId: string, pickerId: string) => void; + toggleItemOpen: (nodeId: string, status: TreeItemStatus, pickerId: string) => void; + toggleItemActive: (nodeId: string, status: TreeItemStatus, pickerId: string) => void; +} + +const memoizedMapStateToProps = () => { + let prevTree: Ttree; + let mappedProps: Pick, 'items'>; + return (state: RootState, props: MainWorkflowTreePickerProps): Pick, 'items'> => { + const tree = state.treePicker[props.pickerId] || createTree(); + if(tree !== prevTree){ + prevTree = tree; + mappedProps = { + items: getNodeChildrenIds('')(tree) + .map(treePickerToTreeItems(tree)) + }; + } + return mappedProps; + }; +}; + +const mapDispatchToProps = (dispatch: Dispatch, props: MainWorkflowTreePickerProps): Pick, 'onContextMenu' | 'toggleItemOpen' | 'toggleItemActive'> => ({ + onContextMenu: (event, item) => props.onContextMenu(event, item.id, props.pickerId), + toggleItemActive: (id, status) => props.toggleItemActive(id, status, props.pickerId), + toggleItemOpen: (id, status) => props.toggleItemOpen(id, status, props.pickerId) +}); + +export const MainWorkflowTreePicker = connect(memoizedMapStateToProps(), mapDispatchToProps)(Tree); + +const treePickerToTreeItems = (tree: Ttree) => + (id: string): TreeItem => { + const node: TreePickerNode = getNodeValue(id)(tree) || createTreePickerNode({ nodeId: '', value: 'InvalidNode' }); + const items = getNodeChildrenIds(node.nodeId)(tree) + .map(treePickerToTreeItems(tree)); + return { + active: node.selected, + data: node.value, + id: node.nodeId, + items: items.length > 0 ? items : undefined, + open: !node.collapsed, + status: node.status + }; + }; + diff --git a/src/views-components/workflow-tree-picker/workflow-tree-picker.tsx b/src/views-components/workflow-tree-picker/workflow-tree-picker.tsx new file mode 100644 index 0000000000..1dd8c107ab --- /dev/null +++ b/src/views-components/workflow-tree-picker/workflow-tree-picker.tsx @@ -0,0 +1,156 @@ +// Copyright (C) The Arvados Authors. All rights reserved. +// +// SPDX-License-Identifier: AGPL-3.0 + +import * as React from "react"; +import { Dispatch } from "redux"; +import { connect } from "react-redux"; +import { Typography } from "@material-ui/core"; +import { MainWorkflowTreePicker, MainWorkflowTreePickerProps } from "./main-workflow-tree-picker"; +import { TreeItem, TreeItemStatus } from "~/components/tree/tree"; +import { ProjectResource } from "~/models/project"; +import { workflowTreePickerActions } from "~/store/workflow-tree-picker/workflow-tree-picker-actions"; +import { ListItemTextIcon } from "~/components/list-item-text-icon/list-item-text-icon"; +import { ProjectIcon, FavoriteIcon, ProjectsIcon, ShareMeIcon } from "~/components/icon/icon"; +import { createTreePickerNode } from "~/store/tree-picker/tree-picker"; +import { RootState } from "~/store/store"; +import { ServiceRepository } from "~/services/services"; +import { FilterBuilder } from "~/services/api/filter-builder"; +import { WrappedFieldProps } from 'redux-form'; + +type WorkflowTreePickerProps = Pick; + +const mapDispatchToProps = (dispatch: Dispatch, props: { onChange: (projectUuid: string) => void }): WorkflowTreePickerProps => ({ + onContextMenu: () => { return; }, + toggleItemActive: (nodeId, status, pickerId) => { + getNotSelectedTreePickerKind(pickerId) + .forEach(pickerId => dispatch(workflowTreePickerActions.TOGGLE_TREE_PICKER_NODE_SELECT({ nodeId: '', pickerId }))); + dispatch(workflowTreePickerActions.TOGGLE_TREE_PICKER_NODE_SELECT({ nodeId, pickerId })); + + props.onChange(nodeId); + }, + toggleItemOpen: (nodeId, status, pickerId) => { + dispatch(toggleItemOpen(nodeId, status, pickerId)); + } +}); + +const toggleItemOpen = (nodeId: string, status: TreeItemStatus, pickerId: string) => + (dispatch: Dispatch, getState: () => RootState, services: ServiceRepository) => { + if (status === TreeItemStatus.INITIAL) { + if (pickerId === TreePickerId.PROJECTS) { + dispatch(loadProjectTreePickerProjects(nodeId)); + } else if (pickerId === TreePickerId.FAVORITES) { + dispatch(loadFavoriteTreePickerProjects(nodeId === services.authService.getUuid() ? '' : nodeId)); + } else { + // TODO: load sharedWithMe + } + } else { + dispatch(workflowTreePickerActions.TOGGLE_TREE_PICKER_NODE_COLLAPSE({ nodeId, pickerId })); + } + }; + +const getNotSelectedTreePickerKind = (pickerId: string) => { + return [TreePickerId.PROJECTS, TreePickerId.FAVORITES, TreePickerId.SHARED_WITH_ME].filter(nodeId => nodeId !== pickerId); +}; + +export enum TreePickerId { + PROJECTS = 'Projects', + SHARED_WITH_ME = 'Shared with me', + FAVORITES = 'Favorites' +} + +export const WorkflowTreePicker = connect(undefined, mapDispatchToProps)((props: WorkflowTreePickerProps) => +
+ + Select a project + +
+ + + +
+
); + +export const loadProjectTreePickerProjects = (nodeId: string) => + async (dispatch: Dispatch, getState: () => RootState, services: ServiceRepository) => { + dispatch(workflowTreePickerActions.LOAD_TREE_PICKER_NODE({ nodeId, pickerId: TreePickerId.PROJECTS })); + + const ownerUuid = nodeId.length === 0 ? services.authService.getUuid() || '' : nodeId; + + const filters = new FilterBuilder() + .addEqual('ownerUuid', ownerUuid) + .getFilters(); + + const { items } = await services.projectService.list({ filters }); + + dispatch(receiveTreePickerData(nodeId, items, TreePickerId.PROJECTS)); + }; + +export const loadFavoriteTreePickerProjects = (nodeId: string) => + async (dispatch: Dispatch, getState: () => RootState, services: ServiceRepository) => { + const parentId = services.authService.getUuid() || ''; + + if (nodeId === '') { + dispatch(workflowTreePickerActions.LOAD_TREE_PICKER_NODE({ nodeId: parentId, pickerId: TreePickerId.FAVORITES })); + const { items } = await services.favoriteService.list(parentId); + + dispatch(receiveTreePickerData(parentId, items as ProjectResource[], TreePickerId.FAVORITES)); + } else { + dispatch(workflowTreePickerActions.LOAD_TREE_PICKER_NODE({ nodeId, pickerId: TreePickerId.FAVORITES })); + const filters = new FilterBuilder() + .addEqual('ownerUuid', nodeId) + .getFilters(); + + const { items } = await services.projectService.list({ filters }); + + dispatch(receiveTreePickerData(nodeId, items, TreePickerId.FAVORITES)); + } + + }; + +const getProjectPickerIcon = (item: TreeItem) => { + switch (item.data.name) { + case TreePickerId.FAVORITES: + return FavoriteIcon; + case TreePickerId.PROJECTS: + return ProjectsIcon; + case TreePickerId.SHARED_WITH_ME: + return ShareMeIcon; + default: + return ProjectIcon; + } +}; + +const renderTreeItem = (item: TreeItem) => + ; + + +export const receiveTreePickerData = (nodeId: string, projects: ProjectResource[], pickerId: string) => + (dispatch: Dispatch) => { + dispatch(workflowTreePickerActions.LOAD_TREE_PICKER_NODE_SUCCESS({ + nodeId, + nodes: projects.map(project => createTreePickerNode({ nodeId: project.uuid, value: project })), + pickerId, + })); + + dispatch(workflowTreePickerActions.TOGGLE_TREE_PICKER_NODE_COLLAPSE({ nodeId, pickerId })); + }; + +export const WorkflowTreePickerField = (props: WrappedFieldProps) => +
+ + {props.meta.dirty && props.meta.error && + + {props.meta.error} + } +
; + +const handleChange = (props: WrappedFieldProps) => (value: string) => + props.input.value === value + ? props.input.onChange('') + : props.input.onChange(value); + diff --git a/src/views/workflow-panel/workflow-panel-view.tsx b/src/views/workflow-panel/workflow-panel-view.tsx index 8a29cb7f16..094c1e6bf8 100644 --- a/src/views/workflow-panel/workflow-panel-view.tsx +++ b/src/views/workflow-panel/workflow-panel-view.tsx @@ -111,6 +111,7 @@ export const WorkflowPanelView = ({...props}) => { onRowClick={props.handleRowClick} onRowDoubleClick={props.handleRowDoubleClick} contextMenuColumn={false} + onContextMenu={e=>e} dataTableDefaultView={} /> -- 2.39.5