From: Michal Klobukowski Date: Thu, 11 Oct 2018 13:20:49 +0000 (+0200) Subject: Merge branch 'master' X-Git-Tag: 1.3.0~62^2~10^2~1 X-Git-Url: https://git.arvados.org/arvados-workbench2.git/commitdiff_plain/f26a160eb9a476f1d5037d899e0636cf150fb573?hp=23c3b5d3792225aafe5b66d81263aa1d3d42eeb9 Merge branch 'master' Feature #13862 Arvados-DCO-1.1-Signed-off-by: Michal Klobukowski --- diff --git a/package.json b/package.json index 1d4a15b3..34e87afe 100644 --- a/package.json +++ b/package.json @@ -32,6 +32,7 @@ "redux": "4.0.0", "redux-form": "7.4.2", "redux-thunk": "2.3.0", + "reselect": "4.0.0", "shell-quote": "1.6.1", "unionize": "2.1.2", "uuid": "3.3.2" diff --git a/src/components/file-tree/file-tree.tsx b/src/components/file-tree/file-tree.tsx index 06fc8b78..0a96254c 100644 --- a/src/components/file-tree/file-tree.tsx +++ b/src/components/file-tree/file-tree.tsx @@ -24,7 +24,7 @@ export class FileTree extends React.Component { onContextMenu={this.handleContextMenu} toggleItemActive={this.handleToggleActive} toggleItemOpen={this.handleToggle} - onSelectionChange={this.handleSelectionChange} />; + toggleItemSelection={this.handleSelectionChange} />; } handleContextMenu = (event: React.MouseEvent, item: TreeItem) => { @@ -32,7 +32,7 @@ export class FileTree extends React.Component { this.props.onMenuOpen(event, item); } - handleToggle = (id: string, status: TreeItemStatus) => { + handleToggle = (event: React.MouseEvent<{}>, { id, status }: TreeItem<{}>) => { this.props.onCollapseToggle(id, status); } diff --git a/src/components/tree/tree.tsx b/src/components/tree/tree.tsx index c892d7d2..b6d77197 100644 --- a/src/components/tree/tree.tsx +++ b/src/components/tree/tree.tsx @@ -60,7 +60,8 @@ const styles: StyleRulesCallback = (theme: ArvadosTheme) => ({ width: theme.spacing.unit * 3, height: theme.spacing.unit * 3, margin: `0 ${theme.spacing.unit}px`, - color: theme.palette.grey["500"] + padding: 0, + color: theme.palette.grey["500"], } }); @@ -81,15 +82,15 @@ export interface TreeItem { } export interface TreeProps { + disableRipple?: boolean; items?: Array>; - render: (item: TreeItem, level?: number) => ReactElement<{}>; - toggleItemOpen: (id: string, status: TreeItemStatus) => void; - toggleItemActive: (id: string, status: TreeItemStatus) => void; level?: number; onContextMenu: (event: React.MouseEvent, item: TreeItem) => void; - showSelection?: boolean; - onSelectionChange?: (event: React.MouseEvent, item: TreeItem) => void; - disableRipple?: boolean; + render: (item: TreeItem, level?: number) => ReactElement<{}>; + showSelection?: boolean | ((item: TreeItem) => boolean); + toggleItemActive: (event: React.MouseEvent, item: TreeItem) => void; + toggleItemOpen: (event: React.MouseEvent, item: TreeItem) => void; + toggleItemSelection?: (event: React.MouseEvent, item: TreeItem) => void; } export const Tree = withStyles(styles)( @@ -98,22 +99,26 @@ export const Tree = withStyles(styles)( const level = this.props.level ? this.props.level : 0; const { classes, render, toggleItemOpen, items, toggleItemActive, onContextMenu, disableRipple } = this.props; const { list, listItem, loader, toggableIconContainer, renderContainer } = classes; + const isCheckboxVisible = typeof this.props.showSelection === 'function' + ? this.props.showSelection + : () => this.props.showSelection ? true : false; + return {items && items.map((it: TreeItem, idx: number) =>
toggleItemActive(it.id, it.status)} + onClick={event => toggleItemActive(event, it)} onContextMenu={this.handleRowContextMenu(it)}> {it.status === TreeItemStatus.PENDING ? : null} - {this.getProperArrowAnimation(it.status, it.items!)} - {this.props.showSelection && + { isCheckboxVisible(it) && + toggleItemSelection={this.props.toggleItemSelection} /> }
)}
; @@ -164,17 +169,18 @@ export const Tree = withStyles(styles)( this.props.onContextMenu(event, item) handleCheckboxChange = (item: TreeItem) => { - const { onSelectionChange } = this.props; - return onSelectionChange + const { toggleItemSelection } = this.props; + return toggleItemSelection ? (event: React.MouseEvent) => { - onSelectionChange(event, item); + event.stopPropagation(); + toggleItemSelection(event, item); } : undefined; } - handleToggleItemOpen = (id: string, status: TreeItemStatus) => (event: React.MouseEvent) => { + handleToggleItemOpen = (item: TreeItem) => (event: React.MouseEvent) => { event.stopPropagation(); - this.props.toggleItemOpen(id, status); + this.props.toggleItemOpen(event, item); } } ); diff --git a/src/index.tsx b/src/index.tsx index b3f351e0..c5add22e 100644 --- a/src/index.tsx +++ b/src/index.tsx @@ -41,7 +41,8 @@ import { progressIndicatorActions } from '~/store/progress-indicator/progress-in import { setUuidPrefix } from '~/store/workflow-panel/workflow-panel-actions'; import { trashedCollectionActionSet } from '~/views-components/context-menu/action-sets/trashed-collection-action-set'; import { ContainerRequestState } from '~/models/container-request'; -import { MountKind } from './models/mount-types'; +import { MountKind } from '~/models/mount-types'; +import { initProjectsTreePicker } from './store/tree-picker/tree-picker-actions'; import { setBuildInfo } from '~/store/app-info/app-info-actions'; import { getBuildInfo } from '~/common/app-info'; @@ -110,25 +111,24 @@ const initListener = (history: History, store: RootStore, services: ServiceRepos initWebSocket(config, services.authService, store); await store.dispatch(loadWorkbench()); addRouteChangeHandlers(history, store); - // createEnumCollectorWorkflow(services); } }; }; -const createPrimitivesCollectorWorkflow = ({workflowService}:ServiceRepository) => { +const createPrimitivesCollectorWorkflow = ({ workflowService }: ServiceRepository) => { workflowService.create({ - name: 'Primitive values collector', - description: 'Workflow for collecting primitive values', - definition: "cwlVersion: v1.0\n$graph:\n- class: CommandLineTool\n requirements:\n - listing:\n - entryname: input_collector.log\n entry: |\n \"flag\":\n $(inputs.example_flag)\n \"string\":\n $(inputs.example_string)\n \"int\":\n $(inputs.example_int)\n \"long\":\n $(inputs.example_long)\n \"float\":\n $(inputs.example_float)\n \"double\":\n $(inputs.example_double)\n class: InitialWorkDirRequirement\n inputs:\n - type: double\n id: '#input_collector.cwl/example_double'\n - type: boolean\n id: '#input_collector.cwl/example_flag'\n - type: float\n id: '#input_collector.cwl/example_float'\n - type: int\n id: '#input_collector.cwl/example_int'\n - type: long\n id: '#input_collector.cwl/example_long'\n - type: string\n id: '#input_collector.cwl/example_string'\n outputs:\n - type: File\n outputBinding:\n glob: '*'\n id: '#input_collector.cwl/output'\n baseCommand: [echo]\n id: '#input_collector.cwl'\n- class: Workflow\n doc: Workflw for collecting primitive values\n inputs:\n - type: double\n label: Double value\n doc: This should allow for entering a decimal number (64-bit).\n id: '#main/example_double'\n default: 0.3333333333333333\n - type: boolean\n label: Boolean Flag\n doc: This should render as in checkbox.\n id: '#main/example_flag'\n default: true\n - type: float\n label: Float value\n doc: This should allow for entering a decimal number (32-bit).\n id: '#main/example_float'\n default: 0.15625\n - type: int\n label: Integer Number\n doc: This should allow for entering a number (32-bit signed).\n id: '#main/example_int'\n default: 2147483647\n - type: long\n label: Long Number\n doc: This should allow for entering a number (64-bit signed).\n id: '#main/example_long'\n default: 9223372036854775807\n - type: string\n label: Freetext\n doc: This should allow for entering an arbitrary char sequence.\n id: '#main/example_string'\n default: This is a string\n outputs:\n - type: File\n outputSource: '#main/input_collector/output'\n id: '#main/log_file'\n steps:\n - run: '#input_collector.cwl'\n in:\n - source: '#main/example_double'\n id: '#main/input_collector/example_double'\n - source: '#main/example_flag'\n id: '#main/input_collector/example_flag'\n - source: '#main/example_float'\n id: '#main/input_collector/example_float'\n - source: '#main/example_int'\n id: '#main/input_collector/example_int'\n - source: '#main/example_long'\n id: '#main/input_collector/example_long'\n - source: '#main/example_string'\n id: '#main/input_collector/example_string'\n out: ['#main/input_collector/output']\n id: '#main/input_collector'\n id: '#main'\n", - }); + name: 'Primitive values collector', + description: 'Workflow for collecting primitive values', + definition: "cwlVersion: v1.0\n$graph:\n- class: CommandLineTool\n requirements:\n - listing:\n - entryname: input_collector.log\n entry: |\n \"flag\":\n $(inputs.example_flag)\n \"string\":\n $(inputs.example_string)\n \"int\":\n $(inputs.example_int)\n \"long\":\n $(inputs.example_long)\n \"float\":\n $(inputs.example_float)\n \"double\":\n $(inputs.example_double)\n class: InitialWorkDirRequirement\n inputs:\n - type: double\n id: '#input_collector.cwl/example_double'\n - type: boolean\n id: '#input_collector.cwl/example_flag'\n - type: float\n id: '#input_collector.cwl/example_float'\n - type: int\n id: '#input_collector.cwl/example_int'\n - type: long\n id: '#input_collector.cwl/example_long'\n - type: string\n id: '#input_collector.cwl/example_string'\n outputs:\n - type: File\n outputBinding:\n glob: '*'\n id: '#input_collector.cwl/output'\n baseCommand: [echo]\n id: '#input_collector.cwl'\n- class: Workflow\n doc: Workflw for collecting primitive values\n inputs:\n - type: double\n label: Double value\n doc: This should allow for entering a decimal number (64-bit).\n id: '#main/example_double'\n default: 0.3333333333333333\n - type: boolean\n label: Boolean Flag\n doc: This should render as in checkbox.\n id: '#main/example_flag'\n default: true\n - type: float\n label: Float value\n doc: This should allow for entering a decimal number (32-bit).\n id: '#main/example_float'\n default: 0.15625\n - type: int\n label: Integer Number\n doc: This should allow for entering a number (32-bit signed).\n id: '#main/example_int'\n default: 2147483647\n - type: long\n label: Long Number\n doc: This should allow for entering a number (64-bit signed).\n id: '#main/example_long'\n default: 9223372036854775807\n - type: string\n label: Freetext\n doc: This should allow for entering an arbitrary char sequence.\n id: '#main/example_string'\n default: This is a string\n outputs:\n - type: File\n outputSource: '#main/input_collector/output'\n id: '#main/log_file'\n steps:\n - run: '#input_collector.cwl'\n in:\n - source: '#main/example_double'\n id: '#main/input_collector/example_double'\n - source: '#main/example_flag'\n id: '#main/input_collector/example_flag'\n - source: '#main/example_float'\n id: '#main/input_collector/example_float'\n - source: '#main/example_int'\n id: '#main/input_collector/example_int'\n - source: '#main/example_long'\n id: '#main/input_collector/example_long'\n - source: '#main/example_string'\n id: '#main/input_collector/example_string'\n out: ['#main/input_collector/output']\n id: '#main/input_collector'\n id: '#main'\n", + }); }; -const createEnumCollectorWorkflow = ({workflowService}:ServiceRepository) => { +const createEnumCollectorWorkflow = ({ workflowService }: ServiceRepository) => { workflowService.create({ - name: 'Enum values collector', - description: 'Workflow for collecting enum values', - definition: "cwlVersion: v1.0\n$graph:\n- class: CommandLineTool\n requirements:\n - listing:\n - entryname: input_collector.log\n entry: |\n \"enum_type\":\n $(inputs.enum_type)\n\n class: InitialWorkDirRequirement\n inputs:\n - type:\n type: enum\n symbols: ['#input_collector.cwl/enum_type/OTU table', '#input_collector.cwl/enum_type/Pathway\n table', '#input_collector.cwl/enum_type/Function table', '#input_collector.cwl/enum_type/Ortholog\n table']\n id: '#input_collector.cwl/enum_type'\n outputs:\n - type: File\n outputBinding:\n glob: '*'\n id: '#input_collector.cwl/output'\n baseCommand: [echo]\n id: '#input_collector.cwl'\n- class: Workflow\n doc: This is the description of the workflow\n inputs:\n - type:\n type: enum\n symbols: ['#main/enum_type/OTU table', '#main/enum_type/Pathway table', '#main/enum_type/Function\n table', '#main/enum_type/Ortholog table']\n name: '#enum_typef4179c7f-45f9-482d-a5db-1abb86698384'\n label: Enumeration Type\n doc: This should render as a drop-down menu.\n id: '#main/enum_type'\n default: OTU table\n outputs:\n - type: File\n outputSource: '#main/input_collector/output'\n id: '#main/log_file'\n steps:\n - run: '#input_collector.cwl'\n in:\n - source: '#main/enum_type'\n id: '#main/input_collector/enum_type'\n out: ['#main/input_collector/output']\n id: '#main/input_collector'\n id: '#main'\n", - }); + name: 'Enum values collector', + description: 'Workflow for collecting enum values', + definition: "cwlVersion: v1.0\n$graph:\n- class: CommandLineTool\n requirements:\n - listing:\n - entryname: input_collector.log\n entry: |\n \"enum_type\":\n $(inputs.enum_type)\n\n class: InitialWorkDirRequirement\n inputs:\n - type:\n type: enum\n symbols: ['#input_collector.cwl/enum_type/OTU table', '#input_collector.cwl/enum_type/Pathway\n table', '#input_collector.cwl/enum_type/Function table', '#input_collector.cwl/enum_type/Ortholog\n table']\n id: '#input_collector.cwl/enum_type'\n outputs:\n - type: File\n outputBinding:\n glob: '*'\n id: '#input_collector.cwl/output'\n baseCommand: [echo]\n id: '#input_collector.cwl'\n- class: Workflow\n doc: This is the description of the workflow\n inputs:\n - type:\n type: enum\n symbols: ['#main/enum_type/OTU table', '#main/enum_type/Pathway table', '#main/enum_type/Function\n table', '#main/enum_type/Ortholog table']\n name: '#enum_typef4179c7f-45f9-482d-a5db-1abb86698384'\n label: Enumeration Type\n doc: This should render as a drop-down menu.\n id: '#main/enum_type'\n default: OTU table\n outputs:\n - type: File\n outputSource: '#main/input_collector/output'\n id: '#main/log_file'\n steps:\n - run: '#input_collector.cwl'\n in:\n - source: '#main/enum_type'\n id: '#main/input_collector/enum_type'\n out: ['#main/input_collector/output']\n id: '#main/input_collector'\n id: '#main'\n", + }); }; const createSampleProcess = ({ containerRequestService }: ServiceRepository) => { @@ -154,15 +154,15 @@ const createSampleProcess = ({ containerRequestService }: ServiceRepository) => "class": "CommandLineTool", "requirements": [ { - "listing": [ - { - "entryname": "input_collector.log", - "entry": "$(inputs.single_file.basename)\n" - } - ], - "class": "InitialWorkDirRequirement" + "listing": [ + { + "entryname": "input_collector.log", + "entry": "$(inputs.single_file.basename)\n" + } + ], + "class": "InitialWorkDirRequirement" } - ], + ], "inputs": [ { "type": "File", @@ -228,7 +228,7 @@ const createSampleProcess = ({ containerRequestService }: ServiceRepository) => "class": "File", "location": "keep:233454526794c0a2d56a305baeff3d30+145/1.txt", "basename": "fileA" - } + } }, } }, diff --git a/src/models/collection-file.ts b/src/models/collection-file.ts index d74ada60..37e18cfc 100644 --- a/src/models/collection-file.ts +++ b/src/models/collection-file.ts @@ -2,7 +2,7 @@ // // SPDX-License-Identifier: AGPL-3.0 -import { Tree, createTree, setNode } from './tree'; +import { Tree, createTree, setNode, TreeNodeStatus } from './tree'; export type CollectionFilesTree = Tree; @@ -60,6 +60,11 @@ export const createCollectionFilesTree = (data: Array()); }; \ No newline at end of file diff --git a/src/models/tree.test.ts b/src/models/tree.test.ts index 375a0120..54b11d47 100644 --- a/src/models/tree.test.ts +++ b/src/models/tree.test.ts @@ -3,6 +3,8 @@ // SPDX-License-Identifier: AGPL-3.0 import * as Tree from './tree'; +import { initTreeNode } from './tree'; +import { pipe } from 'lodash/fp'; describe('Tree', () => { let tree: Tree.Tree; @@ -12,77 +14,81 @@ describe('Tree', () => { }); it('sets new node', () => { - const newTree = Tree.setNode({ children: [], id: 'Node 1', parent: '', value: 'Value 1' })(tree); - expect(Tree.getNode('Node 1')(newTree)).toEqual({ children: [], id: 'Node 1', parent: '', value: 'Value 1' }); + const newTree = Tree.setNode(initTreeNode({ id: 'Node 1', value: 'Value 1' }))(tree); + expect(Tree.getNode('Node 1')(newTree)).toEqual(initTreeNode({ id: 'Node 1', value: 'Value 1' })); }); it('adds new node reference to parent children', () => { - const [newTree] = [tree] - .map(Tree.setNode({ children: [], id: 'Node 1', parent: '', value: 'Value 1' })) - .map(Tree.setNode({ children: [], id: 'Node 2', parent: 'Node 1', value: 'Value 2' })); + const newTree = pipe( + Tree.setNode(initTreeNode({ id: 'Node 1', parent: '', value: 'Value 1' })), + Tree.setNode(initTreeNode({ id: 'Node 2', parent: 'Node 1', value: 'Value 2' })), + )(tree); - expect(Tree.getNode('Node 1')(newTree)).toEqual({ children: ['Node 2'], id: 'Node 1', parent: '', value: 'Value 1' }); + expect(Tree.getNode('Node 1')(newTree)).toEqual({ + ...initTreeNode({ id: 'Node 1', parent: '', value: 'Value 1' }), + children: ['Node 2'] + }); }); it('gets node ancestors', () => { const newTree = [ - { children: [], id: 'Node 1', parent: '', value: 'Value 1' }, - { children: [], id: 'Node 2', parent: 'Node 1', value: 'Value 1' }, - { children: [], id: 'Node 3', parent: 'Node 2', value: 'Value 1' } + initTreeNode({ id: 'Node 1', parent: '', value: 'Value 1' }), + initTreeNode({ id: 'Node 2', parent: 'Node 1', value: 'Value 1' }), + initTreeNode({ id: 'Node 3', parent: 'Node 2', value: 'Value 1' }), ].reduce((tree, node) => Tree.setNode(node)(tree), tree); expect(Tree.getNodeAncestorsIds('Node 3')(newTree)).toEqual(['Node 1', 'Node 2']); }); it('gets node descendants', () => { const newTree = [ - { children: [], id: 'Node 1', parent: '', value: 'Value 1' }, - { children: [], id: 'Node 2', parent: 'Node 1', value: 'Value 1' }, - { children: [], id: 'Node 2.1', parent: 'Node 2', value: 'Value 1' }, - { children: [], id: 'Node 3', parent: 'Node 1', value: 'Value 1' }, - { children: [], id: 'Node 3.1', parent: 'Node 3', value: 'Value 1' } + initTreeNode({ id: 'Node 1', parent: '', value: 'Value 1' }), + initTreeNode({ id: 'Node 2', parent: 'Node 1', value: 'Value 1' }), + initTreeNode({ id: 'Node 2.1', parent: 'Node 2', value: 'Value 1' }), + initTreeNode({ id: 'Node 3', parent: 'Node 1', value: 'Value 1' }), + initTreeNode({ id: 'Node 3.1', parent: 'Node 3', value: 'Value 1' }), ].reduce((tree, node) => Tree.setNode(node)(tree), tree); expect(Tree.getNodeDescendantsIds('Node 1')(newTree)).toEqual(['Node 2', 'Node 3', 'Node 2.1', 'Node 3.1']); }); it('gets root descendants', () => { const newTree = [ - { children: [], id: 'Node 1', parent: '', value: 'Value 1' }, - { children: [], id: 'Node 2', parent: 'Node 1', value: 'Value 1' }, - { children: [], id: 'Node 2.1', parent: 'Node 2', value: 'Value 1' }, - { children: [], id: 'Node 3', parent: 'Node 1', value: 'Value 1' }, - { children: [], id: 'Node 3.1', parent: 'Node 3', value: 'Value 1' } + initTreeNode({ id: 'Node 1', parent: '', value: 'Value 1' }), + initTreeNode({ id: 'Node 2', parent: 'Node 1', value: 'Value 1' }), + initTreeNode({ id: 'Node 2.1', parent: 'Node 2', value: 'Value 1' }), + initTreeNode({ id: 'Node 3', parent: 'Node 1', value: 'Value 1' }), + initTreeNode({ id: 'Node 3.1', parent: 'Node 3', value: 'Value 1' }), ].reduce((tree, node) => Tree.setNode(node)(tree), tree); expect(Tree.getNodeDescendantsIds('')(newTree)).toEqual(['Node 1', 'Node 2', 'Node 3', 'Node 2.1', 'Node 3.1']); }); it('gets node children', () => { const newTree = [ - { children: [], id: 'Node 1', parent: '', value: 'Value 1' }, - { children: [], id: 'Node 2', parent: 'Node 1', value: 'Value 1' }, - { children: [], id: 'Node 2.1', parent: 'Node 2', value: 'Value 1' }, - { children: [], id: 'Node 3', parent: 'Node 1', value: 'Value 1' }, - { children: [], id: 'Node 3.1', parent: 'Node 3', value: 'Value 1' } + initTreeNode({ id: 'Node 1', parent: '', value: 'Value 1' }), + initTreeNode({ id: 'Node 2', parent: 'Node 1', value: 'Value 1' }), + initTreeNode({ id: 'Node 2.1', parent: 'Node 2', value: 'Value 1' }), + initTreeNode({ id: 'Node 3', parent: 'Node 1', value: 'Value 1' }), + initTreeNode({ id: 'Node 3.1', parent: 'Node 3', value: 'Value 1' }), ].reduce((tree, node) => Tree.setNode(node)(tree), tree); expect(Tree.getNodeChildrenIds('Node 1')(newTree)).toEqual(['Node 2', 'Node 3']); }); it('gets root children', () => { const newTree = [ - { children: [], id: 'Node 1', parent: '', value: 'Value 1' }, - { children: [], id: 'Node 2', parent: 'Node 1', value: 'Value 1' }, - { children: [], id: 'Node 2.1', parent: 'Node 2', value: 'Value 1' }, - { children: [], id: 'Node 3', parent: '', value: 'Value 1' }, - { children: [], id: 'Node 3.1', parent: 'Node 3', value: 'Value 1' } + initTreeNode({ id: 'Node 1', parent: '', value: 'Value 1' }), + initTreeNode({ id: 'Node 2', parent: 'Node 1', value: 'Value 1' }), + initTreeNode({ id: 'Node 2.1', parent: 'Node 2', value: 'Value 1' }), + initTreeNode({ id: 'Node 3', parent: '', value: 'Value 1' }), + initTreeNode({ id: 'Node 3.1', parent: 'Node 3', value: 'Value 1' }), ].reduce((tree, node) => Tree.setNode(node)(tree), tree); expect(Tree.getNodeChildrenIds('')(newTree)).toEqual(['Node 1', 'Node 3']); }); it('maps tree', () => { const newTree = [ - { children: [], id: 'Node 1', parent: '', value: 'Value 1' }, - { children: [], id: 'Node 2', parent: 'Node 1', value: 'Value 2' }, + initTreeNode({ id: 'Node 1', parent: '', value: 'Value 1' }), + initTreeNode({ id: 'Node 2', parent: 'Node 1', value: 'Value 2' }), ].reduce((tree, node) => Tree.setNode(node)(tree), tree); const mappedTree = Tree.mapTreeValues(value => parseInt(value.split(' ')[1], 10))(newTree); - expect(Tree.getNode('Node 2')(mappedTree)).toEqual({ children: [], id: 'Node 2', parent: 'Node 1', value: 2 }, ); + expect(Tree.getNode('Node 2')(mappedTree)).toEqual(initTreeNode({id: 'Node 2', parent: 'Node 1', value: 2 })); }); -}); \ No newline at end of file +}); diff --git a/src/models/tree.ts b/src/models/tree.ts index a5fb49cf..cce27b12 100644 --- a/src/models/tree.ts +++ b/src/models/tree.ts @@ -2,6 +2,7 @@ // // SPDX-License-Identifier: AGPL-3.0 +import { pipe } from 'lodash/fp'; export type Tree = Record>; export const TREE_ROOT_ID = ''; @@ -11,6 +12,16 @@ export interface TreeNode { value: T; id: string; parent: string; + active: boolean; + selected: boolean; + expanded: boolean; + status: TreeNodeStatus; +} + +export enum TreeNodeStatus { + INITIAL = 'INITIAL', + PENDING = 'PENDING', + LOADED = 'LOADED', } export const createTree = (): Tree => ({}); @@ -18,12 +29,12 @@ export const createTree = (): Tree => ({}); export const getNode = (id: string) => (tree: Tree): TreeNode | undefined => tree[id]; export const setNode = (node: TreeNode) => (tree: Tree): Tree => { - const [newTree] = [tree] - .map(tree => getNode(node.id)(tree) === node + return pipe( + (tree: Tree) => getNode(node.id)(tree) === node ? tree - : { ...tree, [node.id]: node }) - .map(addChild(node.parent, node.id)); - return newTree; + : { ...tree, [node.id]: node }, + addChild(node.parent, node.id) + )(tree); }; export const getNodeValue = (id: string) => (tree: Tree) => { @@ -95,6 +106,81 @@ export const getNodeChildrenIds = (id: string) => (tree: Tree): string[] = export const mapIdsToNodes = (ids: string[]) => (tree: Tree) => ids.map(id => getNode(id)(tree)).filter((node): node is TreeNode => node !== undefined); +export const activateNode = (id: string) => (tree: Tree) => + mapTree(node => node.id === id ? { ...node, active: true } : { ...node, active: false })(tree); + +export const deactivateNode = (tree: Tree) => + mapTree(node => node.active ? { ...node, active: false } : node)(tree); + +export const expandNode = (...ids: string[]) => (tree: Tree) => + mapTree(node => ids.some(id => id === node.id) ? { ...node, expanded: true } : node)(tree); + +export const collapseNode = (...ids: string[]) => (tree: Tree) => + mapTree(node => ids.some(id => id === node.id) ? { ...node, expanded: false } : node)(tree); + +export const toggleNodeCollapse = (...ids: string[]) => (tree: Tree) => + mapTree(node => ids.some(id => id === node.id) ? { ...node, expanded: !node.expanded } : node)(tree); + +export const setNodeStatus = (id: string) => (status: TreeNodeStatus) => (tree: Tree) => { + const node = getNode(id)(tree); + return node + ? setNode({ ...node, status })(tree) + : tree; +}; + +export const toggleNodeSelection = (id: string) => (tree: Tree) => { + const node = getNode(id)(tree); + return node + ? pipe( + setNode({ ...node, selected: !node.selected }), + toggleAncestorsSelection(id), + toggleDescendantsSelection(id))(tree) + : tree; + +}; + +export const initTreeNode = (data: Pick, 'id' | 'value'> & {parent?: string}): TreeNode => ({ + children: [], + active: false, + selected: false, + expanded: false, + status: TreeNodeStatus.INITIAL, + parent: '', + ...data, +}); + +const toggleDescendantsSelection = (id: string) => (tree: Tree) => { + const node = getNode(id)(tree); + if (node) { + return getNodeDescendants(id)(tree) + .reduce((newTree, subNode) => + setNode({ ...subNode, selected: node.selected })(newTree), + tree); + } + return tree; +}; + +const toggleAncestorsSelection = (id: string) => (tree: Tree) => { + const ancestors = getNodeAncestorsIds(id)(tree).reverse(); + return ancestors.reduce((newTree, parent) => parent ? toggleParentNodeSelection(parent)(newTree) : newTree, tree); +}; + +const toggleParentNodeSelection = (id: string) => (tree: Tree) => { + const node = getNode(id)(tree); + if (node) { + const parentNode = getNode(node.id)(tree); + if (parentNode) { + const selected = parentNode.children + .map(id => getNode(id)(tree)) + .every(node => node !== undefined && node.selected); + return setNode({ ...parentNode, selected })(tree); + } + return setNode(node)(tree); + } + return tree; +}; + + const mapNodeValue = (mapFn: (value: T) => R) => (node: TreeNode): TreeNode => ({ ...node, value: mapFn(node.value) }); diff --git a/src/services/collection-files-service/collection-manifest-mapper.ts b/src/services/collection-files-service/collection-manifest-mapper.ts index 0c7e91de..6e64f833 100644 --- a/src/services/collection-files-service/collection-manifest-mapper.ts +++ b/src/services/collection-files-service/collection-manifest-mapper.ts @@ -4,7 +4,7 @@ import { uniqBy, groupBy } from 'lodash'; import { KeepManifestStream, KeepManifestStreamFile, KeepManifest } from "~/models/keep-manifest"; -import { TreeNode, setNode, createTree, getNodeDescendantsIds, getNodeValue } from '~/models/tree'; +import { TreeNode, setNode, createTree, getNodeDescendantsIds, getNodeValue, TreeNodeStatus } from '~/models/tree'; import { CollectionFilesTree, CollectionFile, CollectionDirectory, createCollectionDirectory, createCollectionFile, CollectionFileType } from '../../models/collection-file'; export const mapCollectionFilesTreeToManifest = (tree: CollectionFilesTree): KeepManifest => { @@ -30,7 +30,11 @@ export const mapCollectionFileToTreeNode = (file: CollectionFile): TreeNode => ([ diff --git a/src/services/groups-service/groups-service.ts b/src/services/groups-service/groups-service.ts index b0c1b56e..bdb51198 100644 --- a/src/services/groups-service/groups-service.ts +++ b/src/services/groups-service/groups-service.ts @@ -20,6 +20,7 @@ export interface ContentsArguments { filters?: string; recursive?: boolean; includeTrash?: boolean; + excludeHomeProject?: boolean; } export interface SharedArguments extends ListArguments { diff --git a/src/store/breadcrumbs/breadcrumbs-actions.ts b/src/store/breadcrumbs/breadcrumbs-actions.ts index a5ded34e..8b1eb2b0 100644 --- a/src/store/breadcrumbs/breadcrumbs-actions.ts +++ b/src/store/breadcrumbs/breadcrumbs-actions.ts @@ -28,7 +28,7 @@ const getSidePanelTreeBreadcrumbs = (uuid: string) => (treePicker: TreePicker): const nodes = getSidePanelTreeBranch(uuid)(treePicker); return nodes.map(node => typeof node.value === 'string' - ? { label: node.value, uuid: node.nodeId } + ? { label: node.value, uuid: node.id } : { label: node.value.name, uuid: node.value.uuid }); }; diff --git a/src/store/collection-panel/collection-panel-files/collection-panel-files-reducer.test.ts b/src/store/collection-panel/collection-panel-files/collection-panel-files-reducer.test.ts index 90dedaaa..3964ee47 100644 --- a/src/store/collection-panel/collection-panel-files/collection-panel-files-reducer.test.ts +++ b/src/store/collection-panel/collection-panel-files/collection-panel-files-reducer.test.ts @@ -5,7 +5,7 @@ import { collectionPanelFilesReducer } from "./collection-panel-files-reducer"; import { collectionPanelFilesAction } from "./collection-panel-files-actions"; import { CollectionFile, CollectionDirectory, createCollectionFile, createCollectionDirectory } from "~/models/collection-file"; -import { createTree, setNode, getNodeValue, mapTreeValues } from "~/models/tree"; +import { createTree, setNode, getNodeValue, mapTreeValues, TreeNodeStatus } from "~/models/tree"; import { CollectionPanelFile, CollectionPanelDirectory } from "./collection-panel-files-state"; describe('CollectionPanelFilesReducer', () => { @@ -26,7 +26,11 @@ describe('CollectionPanelFilesReducer', () => { children: [], id: file.id, parent: file.path, - value: file + value: file, + active: false, + selected: false, + expanded: false, + status: TreeNodeStatus.INITIAL, })(tree), createTree()); const collectionPanelFilesTree = collectionPanelFilesReducer( diff --git a/src/store/file-tree-picker/file-tree-picker-actions.ts b/src/store/file-tree-picker/file-tree-picker-actions.ts deleted file mode 100644 index 18d0f5f8..00000000 --- a/src/store/file-tree-picker/file-tree-picker-actions.ts +++ /dev/null @@ -1,18 +0,0 @@ -// Copyright (C) The Arvados Authors. All rights reserved. -// -// SPDX-License-Identifier: AGPL-3.0 - -import { unionize, ofType, UnionOf } from "~/common/unionize"; - -import { TreePickerNode } from "./file-tree-picker"; - -export const fileTreePickerActions = 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 FileTreePickerAction = UnionOf; diff --git a/src/store/file-tree-picker/file-tree-picker-reducer.ts b/src/store/file-tree-picker/file-tree-picker-reducer.ts deleted file mode 100644 index 59e33a99..00000000 --- a/src/store/file-tree-picker/file-tree-picker-reducer.ts +++ /dev/null @@ -1,73 +0,0 @@ -// 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 "./file-tree-picker"; -import { fileTreePickerActions, FileTreePickerAction } from "./file-tree-picker-actions"; -import { TreeItemStatus } from "~/components/tree/tree"; -import { compose } from "redux"; -import { getNode } from '~/models/tree'; - -export const fileTreePickerReducer = (state: TreePicker = {}, action: FileTreePickerAction) => - fileTreePickerActions.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/file-tree-picker/file-tree-picker.ts b/src/store/file-tree-picker/file-tree-picker.ts deleted file mode 100644 index 259a4b8d..00000000 --- a/src/store/file-tree-picker/file-tree-picker.ts +++ /dev/null @@ -1,25 +0,0 @@ -// 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/store/side-panel-tree/side-panel-tree-actions.ts b/src/store/side-panel-tree/side-panel-tree-actions.ts index 22a83dda..ec593b9b 100644 --- a/src/store/side-panel-tree/side-panel-tree-actions.ts +++ b/src/store/side-panel-tree/side-panel-tree-actions.ts @@ -4,14 +4,12 @@ import { Dispatch } from 'redux'; import { treePickerActions } from "~/store/tree-picker/tree-picker-actions"; -import { createTreePickerNode, TreePickerNode } from '~/store/tree-picker/tree-picker'; import { RootState } from '../store'; import { ServiceRepository } from '~/services/services'; import { FilterBuilder } from '~/services/api/filter-builder'; import { resourcesActions } from '../resources/resources-actions'; import { getTreePicker, TreePicker } from '../tree-picker/tree-picker'; -import { TreeItemStatus } from "~/components/tree/tree"; -import { getNodeAncestors, getNodeValue, getNodeAncestorsIds, getNode } from '~/models/tree'; +import { getNodeAncestors, getNodeAncestorsIds, getNode, TreeNode, initTreeNode, TreeNodeStatus } from '~/models/tree'; import { ProjectResource } from '~/models/project'; import { OrderBuilder } from '../../services/api/order-builder'; @@ -29,11 +27,11 @@ export const SIDE_PANEL_TREE = 'sidePanelTree'; export const getSidePanelTree = (treePicker: TreePicker) => getTreePicker(SIDE_PANEL_TREE)(treePicker); -export const getSidePanelTreeBranch = (uuid: string) => (treePicker: TreePicker): Array> => { +export const getSidePanelTreeBranch = (uuid: string) => (treePicker: TreePicker): Array> => { const tree = getSidePanelTree(treePicker); if (tree) { - const ancestors = getNodeAncestors(uuid)(tree).map(node => node.value); - const node = getNodeValue(uuid)(tree); + const ancestors = getNodeAncestors(uuid)(tree); + const node = getNode(uuid)(tree); if (node) { return [...ancestors, node]; } @@ -54,16 +52,16 @@ export const isSidePanelTreeCategory = (id: string) => SIDE_PANEL_CATEGORIES.som export const initSidePanelTree = () => (dispatch: Dispatch, getState: () => RootState, { authService }: ServiceRepository) => { const rootProjectUuid = authService.getUuid() || ''; - const nodes = SIDE_PANEL_CATEGORIES.map(nodeId => createTreePickerNode({ nodeId, value: nodeId })); - const projectsNode = createTreePickerNode({ nodeId: rootProjectUuid, value: SidePanelTreeCategory.PROJECTS }); + const nodes = SIDE_PANEL_CATEGORIES.map(id => initTreeNode({ id, value: id })); + const projectsNode = initTreeNode({ id: rootProjectUuid, value: SidePanelTreeCategory.PROJECTS }); dispatch(treePickerActions.LOAD_TREE_PICKER_NODE_SUCCESS({ - nodeId: '', + id: '', pickerId: SIDE_PANEL_TREE, nodes: [projectsNode, ...nodes] })); SIDE_PANEL_CATEGORIES.forEach(category => { dispatch(treePickerActions.LOAD_TREE_PICKER_NODE_SUCCESS({ - nodeId: category, + id: category, pickerId: SIDE_PANEL_TREE, nodes: [] })); @@ -75,7 +73,7 @@ export const loadSidePanelTreeProjects = (projectUuid: string) => const treePicker = getTreePicker(SIDE_PANEL_TREE)(getState().treePicker); const node = treePicker ? getNode(projectUuid)(treePicker) : undefined; if (node || projectUuid === '') { - dispatch(treePickerActions.LOAD_TREE_PICKER_NODE({ nodeId: projectUuid, pickerId: SIDE_PANEL_TREE })); + dispatch(treePickerActions.LOAD_TREE_PICKER_NODE({ id: projectUuid, pickerId: SIDE_PANEL_TREE })); const params = { filters: new FilterBuilder() .addEqual('ownerUuid', projectUuid) @@ -86,81 +84,81 @@ export const loadSidePanelTreeProjects = (projectUuid: string) => }; const { items } = await services.projectService.list(params); dispatch(treePickerActions.LOAD_TREE_PICKER_NODE_SUCCESS({ - nodeId: projectUuid, + id: projectUuid, pickerId: SIDE_PANEL_TREE, - nodes: items.map(item => createTreePickerNode({ nodeId: item.uuid, value: item })), + nodes: items.map(item => initTreeNode({ id: item.uuid, value: item })), })); dispatch(resourcesActions.SET_RESOURCES(items)); } }; -export const activateSidePanelTreeItem = (nodeId: string) => +export const activateSidePanelTreeItem = (id: string) => async (dispatch: Dispatch, getState: () => RootState, services: ServiceRepository) => { - const node = getSidePanelTreeNode(nodeId)(getState().treePicker); - if (node && !node.selected) { - dispatch(treePickerActions.TOGGLE_TREE_PICKER_NODE_SELECT({ nodeId, pickerId: SIDE_PANEL_TREE })); + const node = getSidePanelTreeNode(id)(getState().treePicker); + if (node && !node.active) { + dispatch(treePickerActions.ACTIVATE_TREE_PICKER_NODE({ id, pickerId: SIDE_PANEL_TREE })); } - if (!isSidePanelTreeCategory(nodeId)) { - await dispatch(activateSidePanelTreeProject(nodeId)); + if (!isSidePanelTreeCategory(id)) { + await dispatch(activateSidePanelTreeProject(id)); } }; -export const activateSidePanelTreeProject = (nodeId: string) => +export const activateSidePanelTreeProject = (id: string) => async (dispatch: Dispatch, getState: () => RootState, services: ServiceRepository) => { const { treePicker } = getState(); - const node = getSidePanelTreeNode(nodeId)(treePicker); - if (node && node.status !== TreeItemStatus.LOADED) { - await dispatch(loadSidePanelTreeProjects(nodeId)); + const node = getSidePanelTreeNode(id)(treePicker); + if (node && node.status !== TreeNodeStatus.LOADED) { + await dispatch(loadSidePanelTreeProjects(id)); } else if (node === undefined) { - await dispatch(activateSidePanelTreeBranch(nodeId)); + await dispatch(activateSidePanelTreeBranch(id)); } dispatch(treePickerActions.EXPAND_TREE_PICKER_NODES({ - nodeIds: getSidePanelTreeNodeAncestorsIds(nodeId)(treePicker), + ids: getSidePanelTreeNodeAncestorsIds(id)(treePicker), pickerId: SIDE_PANEL_TREE })); - dispatch(expandSidePanelTreeItem(nodeId)); + dispatch(expandSidePanelTreeItem(id)); }; -export const activateSidePanelTreeBranch = (nodeId: string) => +export const activateSidePanelTreeBranch = (id: string) => async (dispatch: Dispatch, getState: () => RootState, services: ServiceRepository) => { - const ancestors = await services.ancestorsService.ancestors(nodeId, services.authService.getUuid() || ''); + const ancestors = await services.ancestorsService.ancestors(id, services.authService.getUuid() || ''); for (const ancestor of ancestors) { await dispatch(loadSidePanelTreeProjects(ancestor.uuid)); } dispatch(treePickerActions.EXPAND_TREE_PICKER_NODES({ - nodeIds: ancestors.map(ancestor => ancestor.uuid), + ids: ancestors.map(ancestor => ancestor.uuid), pickerId: SIDE_PANEL_TREE })); - dispatch(treePickerActions.TOGGLE_TREE_PICKER_NODE_SELECT({ nodeId, pickerId: SIDE_PANEL_TREE })); + dispatch(treePickerActions.ACTIVATE_TREE_PICKER_NODE({ id, pickerId: SIDE_PANEL_TREE })); }; -export const toggleSidePanelTreeItemCollapse = (nodeId: string) => +export const toggleSidePanelTreeItemCollapse = (id: string) => async (dispatch: Dispatch, getState: () => RootState) => { - const node = getSidePanelTreeNode(nodeId)(getState().treePicker); - if (node && node.status === TreeItemStatus.INITIAL) { - await dispatch(loadSidePanelTreeProjects(node.nodeId)); + const node = getSidePanelTreeNode(id)(getState().treePicker); + if (node && node.status === TreeNodeStatus.INITIAL) { + await dispatch(loadSidePanelTreeProjects(node.id)); } - dispatch(treePickerActions.TOGGLE_TREE_PICKER_NODE_COLLAPSE({ nodeId, pickerId: SIDE_PANEL_TREE })); + dispatch(treePickerActions.TOGGLE_TREE_PICKER_NODE_COLLAPSE({ id, pickerId: SIDE_PANEL_TREE })); }; -export const expandSidePanelTreeItem = (nodeId: string) => +export const expandSidePanelTreeItem = (id: string) => async (dispatch: Dispatch, getState: () => RootState) => { - const node = getSidePanelTreeNode(nodeId)(getState().treePicker); - if (node && node.collapsed) { - dispatch(treePickerActions.TOGGLE_TREE_PICKER_NODE_COLLAPSE({ nodeId, pickerId: SIDE_PANEL_TREE })); + const node = getSidePanelTreeNode(id)(getState().treePicker); + if (node && !node.expanded) { + dispatch(treePickerActions.TOGGLE_TREE_PICKER_NODE_COLLAPSE({ id, pickerId: SIDE_PANEL_TREE })); } }; -export const getSidePanelTreeNode = (nodeId: string) => (treePicker: TreePicker) => { +export const getSidePanelTreeNode = (id: string) => (treePicker: TreePicker) => { const sidePanelTree = getTreePicker(SIDE_PANEL_TREE)(treePicker); return sidePanelTree - ? getNodeValue(nodeId)(sidePanelTree) + ? getNode(id)(sidePanelTree) : undefined; }; -export const getSidePanelTreeNodeAncestorsIds = (nodeId: string) => (treePicker: TreePicker) => { +export const getSidePanelTreeNodeAncestorsIds = (id: string) => (treePicker: TreePicker) => { const sidePanelTree = getTreePicker(SIDE_PANEL_TREE)(treePicker); return sidePanelTree - ? getNodeAncestorsIds(nodeId)(sidePanelTree) + ? getNodeAncestorsIds(id)(sidePanelTree) : []; }; diff --git a/src/store/store.ts b/src/store/store.ts index 60267807..064faeca 100644 --- a/src/store/store.ts +++ b/src/store/store.ts @@ -38,7 +38,6 @@ import { progressIndicatorReducer } from './progress-indicator/progress-indicato import { runProcessPanelReducer } from '~/store/run-process-panel/run-process-panel-reducer'; import { WorkflowMiddlewareService } from './workflow-panel/workflow-middleware-service'; import { WORKFLOW_PANEL_ID } from './workflow-panel/workflow-panel-actions'; -import { fileTreePickerReducer } from './file-tree-picker/file-tree-picker-reducer'; import { appInfoReducer } from '~/store/app-info/app-info-reducer'; import { searchBarReducer } from './search-bar/search-bar-reducer'; @@ -102,7 +101,6 @@ const createRootReducer = (services: ServiceRepository) => combineReducers({ fileUploader: fileUploaderReducer, processPanel: processPanelReducer, progressIndicator: progressIndicatorReducer, - fileTreePicker: fileTreePickerReducer, runProcessPanel: runProcessPanelReducer, appInfo: appInfoReducer, searchBar: searchBarReducer diff --git a/src/store/tree-picker/tree-picker-actions.ts b/src/store/tree-picker/tree-picker-actions.ts index 5b04389a..06f73c34 100644 --- a/src/store/tree-picker/tree-picker-actions.ts +++ b/src/store/tree-picker/tree-picker-actions.ts @@ -3,16 +3,203 @@ // SPDX-License-Identifier: AGPL-3.0 import { unionize, ofType, UnionOf } from "~/common/unionize"; - -import { TreePickerNode } from "./tree-picker"; +import { TreeNode, initTreeNode, getNodeDescendants, getNodeDescendantsIds, getNodeValue, TreeNodeStatus } from '~/models/tree'; +import { Dispatch } from 'redux'; +import { RootState } from '~/store/store'; +import { ServiceRepository } from '~/services/services'; +import { FilterBuilder } from '~/services/api/filter-builder'; +import { pipe } from 'lodash/fp'; +import { ResourceKind } from '~/models/resource'; +import { GroupContentsResource } from '../../services/groups-service/groups-service'; +import { CollectionDirectory, CollectionFile } from '../../models/collection-file'; export const treePickerActions = 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 }>(), + LOAD_TREE_PICKER_NODE: ofType<{ id: string, pickerId: string }>(), + LOAD_TREE_PICKER_NODE_SUCCESS: ofType<{ id: string, nodes: Array>, pickerId: string }>(), + TOGGLE_TREE_PICKER_NODE_COLLAPSE: ofType<{ id: string, pickerId: string }>(), + ACTIVATE_TREE_PICKER_NODE: ofType<{ id: string, pickerId: string }>(), + DEACTIVATE_TREE_PICKER_NODE: ofType<{ pickerId: string }>(), + TOGGLE_TREE_PICKER_NODE_SELECTION: ofType<{ id: string, pickerId: string }>(), + EXPAND_TREE_PICKER_NODES: ofType<{ ids: string[], pickerId: string }>(), RESET_TREE_PICKER: ofType<{ pickerId: string }>() }); export type TreePickerAction = UnionOf; + +export const getProjectsTreePickerIds = (pickerId: string) => ({ + home: `${pickerId}_home`, + shared: `${pickerId}_shared`, + favorites: `${pickerId}_favorites`, +}); +export const initProjectsTreePicker = (pickerId: string) => +async (dispatch: Dispatch, _: () => RootState, services: ServiceRepository) => { + const {home, shared, favorites} = getProjectsTreePickerIds(pickerId); + dispatch(initUserProject(home)); + dispatch(initSharedProject(shared)); + dispatch(initFavoritesProject(favorites)); +}; + +interface ReceiveTreePickerDataParams { + data: T[]; + extractNodeData: (value: T) => { id: string, value: T, status?: TreeNodeStatus }; + id: string; + pickerId: string; +} +export const receiveTreePickerData = (params: ReceiveTreePickerDataParams) => + (dispatch: Dispatch) => { + const { data, extractNodeData, id, pickerId, } = params; + dispatch(treePickerActions.LOAD_TREE_PICKER_NODE_SUCCESS({ + id, + nodes: data.map(item => initTreeNode(extractNodeData(item))), + pickerId, + })); + dispatch(treePickerActions.TOGGLE_TREE_PICKER_NODE_COLLAPSE({ id, pickerId })); + }; + +interface LoadProjectParams { + id: string; + pickerId: string; + includeCollections?: boolean; + includeFiles?: boolean; + loadShared?: boolean; +} +export const loadProject = (params: LoadProjectParams) => + async (dispatch: Dispatch, _: () => RootState, services: ServiceRepository) => { + const { id, pickerId, includeCollections = false, includeFiles = false, loadShared = false } = params; + + dispatch(treePickerActions.LOAD_TREE_PICKER_NODE({ id, pickerId })); + + const filters = pipe( + (fb: FilterBuilder) => includeCollections + ? fb.addIsA('uuid', [ResourceKind.PROJECT, ResourceKind.COLLECTION]) + : fb.addIsA('uuid', [ResourceKind.PROJECT]), + fb => fb.getFilters(), + )(new FilterBuilder()); + + const { items } = await services.groupsService.contents(loadShared ? '' : id, { filters, excludeHomeProject: loadShared || undefined }); + + dispatch(receiveTreePickerData({ + id, + pickerId, + data: items, + extractNodeData: item => ({ + id: item.uuid, + value: item, + status: item.kind === ResourceKind.PROJECT + ? TreeNodeStatus.INITIAL + : includeFiles + ? TreeNodeStatus.INITIAL + : TreeNodeStatus.LOADED + }), + })); + }; + +export const loadCollection = (id: string, pickerId: string) => + async (dispatch: Dispatch, _: () => RootState, services: ServiceRepository) => { + dispatch(treePickerActions.LOAD_TREE_PICKER_NODE({ id, pickerId })); + + const files = await services.collectionService.files(id); + const data = getNodeDescendants('')(files).map(node => node.value); + + dispatch(receiveTreePickerData({ + id, + pickerId, + data, + extractNodeData: value => ({ + id: value.id, + status: TreeNodeStatus.LOADED, + value, + }), + })); + }; + + +export const initUserProject = (pickerId: string) => + async (dispatch: Dispatch, getState: () => RootState, services: ServiceRepository) => { + const uuid = services.authService.getUuid(); + if (uuid) { + dispatch(receiveTreePickerData({ + id: '', + pickerId, + data: [{ uuid, name: 'Projects' }], + extractNodeData: value => ({ + id: value.uuid, + status: TreeNodeStatus.INITIAL, + value, + }), + })); + } + }; +export const loadUserProject = (pickerId: string, includeCollections = false, includeFiles = false) => + async (dispatch: Dispatch, getState: () => RootState, services: ServiceRepository) => { + const uuid = services.authService.getUuid(); + if (uuid) { + dispatch(loadProject({ id: uuid, pickerId, includeCollections, includeFiles })); + } + }; + + +export const initSharedProject = (pickerId: string) => + async (dispatch: Dispatch, getState: () => RootState, services: ServiceRepository) => { + dispatch(receiveTreePickerData({ + id: '', + pickerId, + data: [{ uuid: 'Shared with me', name: 'Shared with me' }], + extractNodeData: value => ({ + id: value.uuid, + status: TreeNodeStatus.INITIAL, + value, + }), + })); + }; + +export const initFavoritesProject = (pickerId: string) => + async (dispatch: Dispatch, getState: () => RootState, services: ServiceRepository) => { + dispatch(receiveTreePickerData({ + id: '', + pickerId, + data: [{ uuid: 'Favorites', name: 'Favorites' }], + extractNodeData: value => ({ + id: value.uuid, + status: TreeNodeStatus.INITIAL, + value, + }), + })); + }; + +interface LoadFavoritesProjectParams { + pickerId: string; + includeCollections?: boolean; + includeFiles?: boolean; +} +export const loadFavoritesProject = (params: LoadFavoritesProjectParams) => + async (dispatch: Dispatch, getState: () => RootState, services: ServiceRepository) => { + const { pickerId, includeCollections = false, includeFiles = false } = params; + const uuid = services.authService.getUuid(); + if (uuid) { + + const filters = pipe( + (fb: FilterBuilder) => includeCollections + ? fb.addIsA('headUuid', [ResourceKind.PROJECT, ResourceKind.COLLECTION]) + : fb.addIsA('headUuid', [ResourceKind.PROJECT]), + fb => fb.getFilters(), + )(new FilterBuilder()); + + const { items } = await services.favoriteService.list(uuid, { filters }); + + dispatch(receiveTreePickerData({ + id: 'Favorites', + pickerId, + data: items, + extractNodeData: item => ({ + id: item.uuid, + value: item, + status: item.kind === ResourceKind.PROJECT + ? TreeNodeStatus.INITIAL + : includeFiles + ? TreeNodeStatus.INITIAL + : TreeNodeStatus.LOADED + }), + })); + } + }; diff --git a/src/store/tree-picker/tree-picker-reducer.test.ts b/src/store/tree-picker/tree-picker-reducer.test.ts index e09d12d7..3a0b60e1 100644 --- a/src/store/tree-picker/tree-picker-reducer.test.ts +++ b/src/store/tree-picker/tree-picker-reducer.test.ts @@ -2,105 +2,104 @@ // // SPDX-License-Identifier: AGPL-3.0 -import { createTree, getNodeValue, getNodeChildrenIds } from "~/models/tree"; -import { TreePickerNode, createTreePickerNode } from "./tree-picker"; +import { createTree, getNodeChildrenIds, getNode, TreeNodeStatus } from '~/models/tree'; +import { pipe } from 'lodash/fp'; import { treePickerReducer } from "./tree-picker-reducer"; import { treePickerActions } from "./tree-picker-actions"; -import { TreeItemStatus } from "~/components/tree/tree"; +import { TreePicker } from './tree-picker'; +import { initTreeNode } from '~/models/tree'; describe('TreePickerReducer', () => { it('LOAD_TREE_PICKER_NODE - initial state', () => { - const tree = createTree(); - const newState = treePickerReducer({}, treePickerActions.LOAD_TREE_PICKER_NODE({ nodeId: '1', pickerId: "projects" })); + const tree = createTree<{}>(); + const newState = treePickerReducer({}, treePickerActions.LOAD_TREE_PICKER_NODE({ id: '1', pickerId: "projects" })); expect(newState).toEqual({ 'projects': tree }); }); it('LOAD_TREE_PICKER_NODE', () => { - const node = createTreePickerNode({ nodeId: '1', value: '1' }); - const [newState] = [{ - projects: createTree() - }] - .map(state => treePickerReducer(state, treePickerActions.LOAD_TREE_PICKER_NODE_SUCCESS({ nodeId: '', nodes: [node], pickerId: "projects" }))) - .map(state => treePickerReducer(state, treePickerActions.LOAD_TREE_PICKER_NODE({ nodeId: '1', pickerId: "projects" }))); + const node = initTreeNode({ id: '1', value: '1' }); + const newState = pipe( + (state: TreePicker) => treePickerReducer(state, treePickerActions.LOAD_TREE_PICKER_NODE_SUCCESS({ id: '', nodes: [node], pickerId: "projects" })), + state => treePickerReducer(state, treePickerActions.LOAD_TREE_PICKER_NODE({ id: '1', pickerId: "projects" })) + )({ projects: createTree<{}>() }); - expect(getNodeValue('1')(newState.projects)).toEqual({ - ...createTreePickerNode({ nodeId: '1', value: '1' }), - status: TreeItemStatus.PENDING + expect(getNode('1')(newState.projects)).toEqual({ + ...initTreeNode({ id: '1', value: '1' }), + status: TreeNodeStatus.PENDING }); }); it('LOAD_TREE_PICKER_NODE_SUCCESS - initial state', () => { - const subNode = createTreePickerNode({ nodeId: '1.1', value: '1.1' }); - const newState = treePickerReducer({}, treePickerActions.LOAD_TREE_PICKER_NODE_SUCCESS({ nodeId: '', nodes: [subNode], pickerId: "projects" })); + const subNode = initTreeNode({ id: '1.1', value: '1.1' }); + const newState = treePickerReducer({}, treePickerActions.LOAD_TREE_PICKER_NODE_SUCCESS({ id: '', nodes: [subNode], pickerId: "projects" })); expect(getNodeChildrenIds('')(newState.projects)).toEqual(['1.1']); }); it('LOAD_TREE_PICKER_NODE_SUCCESS', () => { - const node = createTreePickerNode({ nodeId: '1', value: '1' }); - const subNode = createTreePickerNode({ nodeId: '1.1', value: '1.1' }); - const [newState] = [{ - projects: createTree() - }] - .map(state => treePickerReducer(state, treePickerActions.LOAD_TREE_PICKER_NODE_SUCCESS({ nodeId: '', nodes: [node], pickerId: "projects" }))) - .map(state => treePickerReducer(state, treePickerActions.LOAD_TREE_PICKER_NODE_SUCCESS({ nodeId: '1', nodes: [subNode], pickerId: "projects" }))); + const node = initTreeNode({ id: '1', value: '1' }); + const subNode = initTreeNode({ id: '1.1', value: '1.1' }); + const newState = pipe( + (state: TreePicker) => treePickerReducer(state, treePickerActions.LOAD_TREE_PICKER_NODE_SUCCESS({ id: '', nodes: [node], pickerId: "projects" })), + state => treePickerReducer(state, treePickerActions.LOAD_TREE_PICKER_NODE_SUCCESS({ id: '1', nodes: [subNode], pickerId: "projects" })) + )({ projects: createTree<{}>() }); expect(getNodeChildrenIds('1')(newState.projects)).toEqual(['1.1']); - expect(getNodeValue('1')(newState.projects)).toEqual({ - ...createTreePickerNode({ nodeId: '1', value: '1' }), - status: TreeItemStatus.LOADED + expect(getNode('1')(newState.projects)).toEqual({ + ...initTreeNode({ id: '1', value: '1' }), + children: ['1.1'], + status: TreeNodeStatus.LOADED }); }); - it('TOGGLE_TREE_PICKER_NODE_COLLAPSE - collapsed', () => { - const node = createTreePickerNode({ nodeId: '1', value: '1' }); - const [newState] = [{ - projects: createTree() - }] - .map(state => treePickerReducer(state, treePickerActions.LOAD_TREE_PICKER_NODE_SUCCESS({ nodeId: '', nodes: [node], pickerId: "projects" }))) - .map(state => treePickerReducer(state, treePickerActions.TOGGLE_TREE_PICKER_NODE_COLLAPSE({ nodeId: '1', pickerId: "projects" }))); - expect(getNodeValue('1')(newState.projects)).toEqual({ - ...createTreePickerNode({ nodeId: '1', value: '1' }), - collapsed: false + it('TOGGLE_TREE_PICKER_NODE_COLLAPSE - expanded', () => { + const node = initTreeNode({ id: '1', value: '1' }); + const newState = pipe( + (state: TreePicker) => treePickerReducer(state, treePickerActions.LOAD_TREE_PICKER_NODE_SUCCESS({ id: '', nodes: [node], pickerId: "projects" })), + state => treePickerReducer(state, treePickerActions.TOGGLE_TREE_PICKER_NODE_COLLAPSE({ id: '1', pickerId: "projects" })) + )({ projects: createTree<{}>() }); + expect(getNode('1')(newState.projects)).toEqual({ + ...initTreeNode({ id: '1', value: '1' }), + expanded: true }); }); it('TOGGLE_TREE_PICKER_NODE_COLLAPSE - expanded', () => { - const node = createTreePickerNode({ nodeId: '1', value: '1' }); - const [newState] = [{ - projects: createTree() - }] - .map(state => treePickerReducer(state, treePickerActions.LOAD_TREE_PICKER_NODE_SUCCESS({ nodeId: '', nodes: [node], pickerId: "projects" }))) - .map(state => treePickerReducer(state, treePickerActions.TOGGLE_TREE_PICKER_NODE_COLLAPSE({ nodeId: '1', pickerId: "projects" }))) - .map(state => treePickerReducer(state, treePickerActions.TOGGLE_TREE_PICKER_NODE_COLLAPSE({ nodeId: '1', pickerId: "projects" }))); - expect(getNodeValue('1')(newState.projects)).toEqual({ - ...createTreePickerNode({ nodeId: '1', value: '1' }), - collapsed: true + const node = initTreeNode({ id: '1', value: '1' }); + const newState = pipe( + (state: TreePicker) => treePickerReducer(state, treePickerActions.LOAD_TREE_PICKER_NODE_SUCCESS({ id: '', nodes: [node], pickerId: "projects" })), + state => treePickerReducer(state, treePickerActions.TOGGLE_TREE_PICKER_NODE_COLLAPSE({ id: '1', pickerId: "projects" })), + state => treePickerReducer(state, treePickerActions.TOGGLE_TREE_PICKER_NODE_COLLAPSE({ id: '1', pickerId: "projects" })), + )({ projects: createTree<{}>() }); + expect(getNode('1')(newState.projects)).toEqual({ + ...initTreeNode({ id: '1', value: '1' }), + expanded: false }); }); - it('TOGGLE_TREE_PICKER_NODE_SELECT - selected', () => { - const node = createTreePickerNode({ nodeId: '1', value: '1' }); - const [newState] = [{ - projects: createTree() - }] - .map(state => treePickerReducer(state, treePickerActions.LOAD_TREE_PICKER_NODE_SUCCESS({ nodeId: '', nodes: [node], pickerId: "projects" }))) - .map(state => treePickerReducer(state, treePickerActions.TOGGLE_TREE_PICKER_NODE_SELECT({ nodeId: '1', pickerId: "projects" }))); - expect(getNodeValue('1')(newState.projects)).toEqual({ - ...createTreePickerNode({ nodeId: '1', value: '1' }), - selected: true + it('ACTIVATE_TREE_PICKER_NODE', () => { + const node = initTreeNode({ id: '1', value: '1' }); + const newState = pipe( + (state: TreePicker) => treePickerReducer(state, treePickerActions.LOAD_TREE_PICKER_NODE_SUCCESS({ id: '', nodes: [node], pickerId: "projects" })), + state => treePickerReducer(state, treePickerActions.ACTIVATE_TREE_PICKER_NODE({ id: '1', pickerId: "projects" })), + )({ projects: createTree<{}>() }); + expect(getNode('1')(newState.projects)).toEqual({ + ...initTreeNode({ id: '1', value: '1' }), + active: true }); }); - it('TOGGLE_TREE_PICKER_NODE_SELECT - not selected', () => { - const node = createTreePickerNode({ nodeId: '1', value: '1' }); - const [newState] = [{ - projects: createTree() - }] - .map(state => treePickerReducer(state, treePickerActions.LOAD_TREE_PICKER_NODE_SUCCESS({ nodeId: '', nodes: [node], pickerId: "projects" }))) - .map(state => treePickerReducer(state, treePickerActions.TOGGLE_TREE_PICKER_NODE_SELECT({ nodeId: '1', pickerId: "projects" }))) - .map(state => treePickerReducer(state, treePickerActions.TOGGLE_TREE_PICKER_NODE_SELECT({ nodeId: '1', pickerId: "projects" }))); - expect(getNodeValue('1')(newState.projects)).toEqual({ - ...createTreePickerNode({ nodeId: '1', value: '1' }), - selected: false + it('TOGGLE_TREE_PICKER_NODE_SELECTION', () => { + const node = initTreeNode({ id: '1', value: '1' }); + const subNode = initTreeNode({ id: '1.1', value: '1.1' }); + const newState = pipe( + (state: TreePicker) => treePickerReducer(state, treePickerActions.LOAD_TREE_PICKER_NODE_SUCCESS({ id: '', nodes: [node], pickerId: "projects" })), + state => treePickerReducer(state, treePickerActions.LOAD_TREE_PICKER_NODE_SUCCESS({ id: '1', nodes: [subNode], pickerId: "projects" })), + state => treePickerReducer(state, treePickerActions.TOGGLE_TREE_PICKER_NODE_SELECTION({ id: '1.1', pickerId: "projects" })), + )({ projects: createTree<{}>() }); + expect(getNode('1')(newState.projects)).toEqual({ + ...initTreeNode({ id: '1', value: '1' }), + selected: true, + children: ['1.1'], + status: TreeNodeStatus.LOADED, }); }); }); diff --git a/src/store/tree-picker/tree-picker-reducer.ts b/src/store/tree-picker/tree-picker-reducer.ts index b0d9bc94..2df567ef 100644 --- a/src/store/tree-picker/tree-picker-reducer.ts +++ b/src/store/tree-picker/tree-picker-reducer.ts @@ -2,72 +2,46 @@ // // SPDX-License-Identifier: AGPL-3.0 -import { createTree, setNodeValueWith, TreeNode, setNode, mapTreeValues, Tree } from "~/models/tree"; -import { TreePicker, TreePickerNode } from "./tree-picker"; +import { createTree, TreeNode, setNode, Tree, TreeNodeStatus, setNodeStatus, expandNode, deactivateNode } from '~/models/tree'; +import { TreePicker } from "./tree-picker"; import { treePickerActions, TreePickerAction } from "./tree-picker-actions"; -import { TreeItemStatus } from "~/components/tree/tree"; import { compose } from "redux"; -import { getNode } from '../../models/tree'; +import { activateNode, getNode, toggleNodeCollapse, toggleNodeSelection } from '~/models/tree'; export const treePickerReducer = (state: TreePicker = {}, action: TreePickerAction) => treePickerActions.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))), + LOAD_TREE_PICKER_NODE: ({ id, pickerId }) => + updateOrCreatePicker(state, pickerId, setNodeStatus(id)(TreeNodeStatus.PENDING)), + LOAD_TREE_PICKER_NODE_SUCCESS: ({ id, nodes, pickerId }) => + updateOrCreatePicker(state, pickerId, compose(receiveNodes(nodes)(id), setNodeStatus(id)(TreeNodeStatus.LOADED))), + TOGGLE_TREE_PICKER_NODE_COLLAPSE: ({ id, pickerId }) => + updateOrCreatePicker(state, pickerId, toggleNodeCollapse(id)), + ACTIVATE_TREE_PICKER_NODE: ({ id, pickerId }) => + updateOrCreatePicker(state, pickerId, activateNode(id)), + DEACTIVATE_TREE_PICKER_NODE: ({ pickerId }) => + updateOrCreatePicker(state, pickerId, deactivateNode), + TOGGLE_TREE_PICKER_NODE_SELECTION: ({ id, pickerId }) => + updateOrCreatePicker(state, pickerId, toggleNodeSelection(id)), RESET_TREE_PICKER: ({ pickerId }) => updateOrCreatePicker(state, pickerId, createTree), - EXPAND_TREE_PICKER_NODES: ({ pickerId, nodeIds }) => - updateOrCreatePicker(state, pickerId, mapTreeValues(expand(nodeIds))), + EXPAND_TREE_PICKER_NODES: ({ pickerId, ids }) => + updateOrCreatePicker(state, pickerId, expandNode(...ids)), default: () => state }); -const updateOrCreatePicker = (state: TreePicker, pickerId: string, func: (value: Tree) => Tree) => { +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 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); + return setNode({ ...node, parent })(tree); }, newState); }; - -const createTreeNode = (parent: string) => (node: TreePickerNode): TreeNode => ({ - children: [], - id: node.nodeId, - parent, - value: node -}); diff --git a/src/store/tree-picker/tree-picker.ts b/src/store/tree-picker/tree-picker.ts index 259a4b8d..a4127fa3 100644 --- a/src/store/tree-picker/tree-picker.ts +++ b/src/store/tree-picker/tree-picker.ts @@ -3,17 +3,10 @@ // SPDX-License-Identifier: AGPL-3.0 import { Tree } from "~/models/tree"; -import { TreeItemStatus } from "~/components/tree/tree"; +import { TreeItemStatus } from '~/components/tree/tree'; +export type TreePicker = { [key: string]: Tree }; -export type TreePicker = { [key: string]: Tree }; - -export interface TreePickerNode { - nodeId: string; - value: Value; - selected: boolean; - collapsed: boolean; - status: TreeItemStatus; -} +export const getTreePicker = (id: string) => (state: TreePicker): Tree | undefined => state[id]; export const createTreePickerNode = (data: { nodeId: string, value: any }) => ({ ...data, @@ -21,5 +14,3 @@ export const createTreePickerNode = (data: { nodeId: string, value: any }) => ({ 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-file-selection/dialog-file-selection.tsx b/src/views-components/dialog-file-selection/dialog-file-selection.tsx deleted file mode 100644 index e7185c05..00000000 --- a/src/views-components/dialog-file-selection/dialog-file-selection.tsx +++ /dev/null @@ -1,29 +0,0 @@ -// Copyright (C) The Arvados Authors. All rights reserved. -// -// SPDX-License-Identifier: AGPL-3.0 - -import * as React from 'react'; -import { InjectedFormProps, Field } from 'redux-form'; -import { WithDialogProps } from '~/store/dialog/with-dialog'; -import { CollectionCreateFormDialogData } from '~/store/collections/collection-create-actions'; -import { FormDialog } from '~/components/form-dialog/form-dialog'; -import { require } from '~/validators/require'; -import { FileTreePickerField } from '~/views-components/file-tree-picker/file-tree-picker'; - -type FileSelectionProps = WithDialogProps<{}> & InjectedFormProps; - -export const DialogFileSelection = (props: FileSelectionProps) => - ; - -const FileSelectionFields = () => - ; - -const FILES_FIELD_VALIDATION = [require]; \ No newline at end of file diff --git a/src/views-components/dialog-forms/file-selection-dialog.ts b/src/views-components/dialog-forms/file-selection-dialog.ts deleted file mode 100644 index 7c883cbf..00000000 --- a/src/views-components/dialog-forms/file-selection-dialog.ts +++ /dev/null @@ -1,21 +0,0 @@ -// Copyright (C) The Arvados Authors. All rights reserved. -// -// SPDX-License-Identifier: AGPL-3.0 - -import { compose } from "redux"; -import { reduxForm } from 'redux-form'; -import { withDialog } from "~/store/dialog/with-dialog"; -import { FILE_SELECTION } from '~/store/file-selection/file-selection-actions'; -import { DialogFileSelection } from '~/views-components/dialog-file-selection/dialog-file-selection'; -import { dialogActions } from '~/store/dialog/dialog-actions'; - -export const FileSelectionDialog = compose( - withDialog(FILE_SELECTION), - reduxForm({ - form: FILE_SELECTION, - onSubmit: (data, dispatch) => { - dispatch(dialogActions.CLOSE_DIALOG({ id: FILE_SELECTION })); - return data; - } - }) -)(DialogFileSelection); \ No newline at end of file diff --git a/src/views-components/file-tree-picker/file-tree-picker.tsx b/src/views-components/file-tree-picker/file-tree-picker.tsx deleted file mode 100644 index 129cb750..00000000 --- a/src/views-components/file-tree-picker/file-tree-picker.tsx +++ /dev/null @@ -1,173 +0,0 @@ -// 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 { MainFileTreePicker, MainFileTreePickerProps } from "./main-file-tree-picker"; -import { TreeItem, TreeItemStatus } from "~/components/tree/tree"; -import { ProjectResource } from "~/models/project"; -import { fileTreePickerActions } from "~/store/file-tree-picker/file-tree-picker-actions"; -import { ListItemTextIcon } from "~/components/list-item-text-icon/list-item-text-icon"; -import { ProjectIcon, FavoriteIcon, ProjectsIcon, ShareMeIcon, CollectionIcon } 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'; -import { ResourceKind, extractUuidKind } from '~/models/resource'; -import { GroupContentsResource } from '~/services/groups-service/groups-service'; -import { loadCollectionFiles } from '~/store/collection-panel/collection-panel-files/collection-panel-files-actions'; - -type FileTreePickerProps = Pick; - -const mapDispatchToProps = (dispatch: Dispatch, props: { onChange: (projectUuid: string) => void }): FileTreePickerProps => ({ - onContextMenu: () => { return; }, - toggleItemActive: (nodeId, status, pickerId) => { - getNotSelectedTreePickerKind(pickerId) - .forEach(pickerId => dispatch(fileTreePickerActions.TOGGLE_TREE_PICKER_NODE_SELECT({ nodeId: '', pickerId }))); - dispatch(fileTreePickerActions.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(loadProjectTreePicker(nodeId)); - } else if (pickerId === TreePickerId.FAVORITES) { - dispatch(loadFavoriteTreePicker(nodeId === services.authService.getUuid() ? '' : nodeId)); - } else { - // TODO: load sharedWithMe - } - } else { - dispatch(fileTreePickerActions.TOGGLE_TREE_PICKER_NODE_COLLAPSE({ nodeId, pickerId })); - } - }; - -const getNotSelectedTreePickerKind = (pickerId: string) => { - return [TreePickerId.PROJECTS, TreePickerId.FAVORITES, TreePickerId.SHARED_WITH_ME].filter(nodeId => nodeId !== pickerId); -}; - -enum TreePickerId { - PROJECTS = 'Projects', - SHARED_WITH_ME = 'Shared with me', - FAVORITES = 'Favorites' -} - -export const FileTreePicker = connect(undefined, mapDispatchToProps)((props: FileTreePickerProps) => -
-
- - - -
-
); - -export const loadProjectTreePicker = (nodeId: string) => - async (dispatch: Dispatch, getState: () => RootState, services: ServiceRepository) => { - dispatch(fileTreePickerActions.LOAD_TREE_PICKER_NODE({ nodeId, pickerId: TreePickerId.PROJECTS })); - - const ownerUuid = nodeId.length === 0 ? services.authService.getUuid() || '' : nodeId; - - const filters = new FilterBuilder() - .addIsA("uuid", [ResourceKind.PROJECT, ResourceKind.COLLECTION]) - .addEqual('ownerUuid', ownerUuid) - .getFilters(); - - // TODO: loadfiles from collections - const { items } = (extractUuidKind(nodeId) === ResourceKind.COLLECTION) - ? dispatch(loadCollectionFiles(nodeId)) - : await services.groupsService.contents(ownerUuid, { filters }); - - await dispatch(receiveTreePickerData(nodeId, items, TreePickerId.PROJECTS)); - }; - -export const loadFavoriteTreePicker = (nodeId: string) => - async (dispatch: Dispatch, getState: () => RootState, services: ServiceRepository) => { - const parentId = services.authService.getUuid() || ''; - - if (nodeId === '') { - dispatch(fileTreePickerActions.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(fileTreePickerActions.LOAD_TREE_PICKER_NODE({ nodeId, pickerId: TreePickerId.FAVORITES })); - const filters = new FilterBuilder() - .addEqual('ownerUuid', nodeId) - .getFilters(); - - const { items } = (extractUuidKind(nodeId) === ResourceKind.COLLECTION) - ? dispatch(loadCollectionFiles(nodeId)) - : await services.groupsService.contents(parentId, { 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 getResourceIcon(item); - } -}; - -const getResourceIcon = (item: TreeItem) => { - switch (item.data.kind) { - case ResourceKind.COLLECTION: - return CollectionIcon; - case ResourceKind.PROJECT: - return ProjectIcon; - default: - return ProjectIcon; - } -}; - -const renderTreeItem = (item: TreeItem) => - ; - - -export const receiveTreePickerData = (nodeId: string, items: GroupContentsResource[] = [], pickerId: string) => - (dispatch: Dispatch) => { - dispatch(fileTreePickerActions.LOAD_TREE_PICKER_NODE_SUCCESS({ - nodeId, - nodes: items.map(item => createTreePickerNode({ nodeId: item.uuid, value: item })), - pickerId, - })); - - dispatch(fileTreePickerActions.TOGGLE_TREE_PICKER_NODE_COLLAPSE({ nodeId, pickerId })); - }; - -export const FileTreePickerField = (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-components/file-tree-picker/main-file-tree-picker.ts b/src/views-components/file-tree-picker/main-file-tree-picker.ts deleted file mode 100644 index dc52ea6c..00000000 --- a/src/views-components/file-tree-picker/main-file-tree-picker.ts +++ /dev/null @@ -1,57 +0,0 @@ -// 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/file-tree-picker/file-tree-picker"; -import { getNodeValue, getNodeChildrenIds, Tree as Ttree, createTree } from "~/models/tree"; -import { Dispatch } from "redux"; - -export interface MainFileTreePickerProps { - 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: MainFileTreePickerProps): 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: MainFileTreePickerProps): 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 MainFileTreePicker = 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/project-tree-picker/project-tree-picker.tsx b/src/views-components/project-tree-picker/project-tree-picker.tsx index 86795295..36104d43 100644 --- a/src/views-components/project-tree-picker/project-tree-picker.tsx +++ b/src/views-components/project-tree-picker/project-tree-picker.tsx @@ -12,42 +12,43 @@ import { ProjectResource } from "~/models/project"; import { treePickerActions } from "~/store/tree-picker/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'; +import { initTreeNode } from '~/models/tree'; -type ProjectTreePickerProps = Pick; +type ProjectTreePickerProps = Pick, 'onContextMenu' | 'toggleItemActive' | 'toggleItemOpen' | 'toggleItemSelection'>; const mapDispatchToProps = (dispatch: Dispatch, props: { onChange: (projectUuid: string) => void }): ProjectTreePickerProps => ({ onContextMenu: () => { return; }, - toggleItemActive: (nodeId, status, pickerId) => { - if (nodeId !== TreePickerId.FAVORITES && nodeId !== TreePickerId.SHARED_WITH_ME) { - getNotSelectedTreePickerKind(pickerId) - .forEach(pickerId => dispatch(treePickerActions.TOGGLE_TREE_PICKER_NODE_SELECT({ nodeId: '', pickerId }))); - dispatch(treePickerActions.TOGGLE_TREE_PICKER_NODE_SELECT({ nodeId, pickerId })); + toggleItemActive: (_, { id }, pickerId) => { + getNotSelectedTreePickerKind(pickerId) + .forEach(pickerId => dispatch(treePickerActions.ACTIVATE_TREE_PICKER_NODE({ id: '', pickerId }))); + dispatch(treePickerActions.ACTIVATE_TREE_PICKER_NODE({ id, pickerId })); - props.onChange(nodeId); - } + props.onChange(id); + }, + toggleItemOpen: (_, { id, status }, pickerId) => { + dispatch(toggleItemOpen(id, status, pickerId)); + }, + toggleItemSelection: (_, { id }, pickerId) => { + dispatch(treePickerActions.TOGGLE_TREE_PICKER_NODE_SELECTION({ id, pickerId })); }, - toggleItemOpen: (nodeId, status, pickerId) => { - dispatch(toggleItemOpen(nodeId, status, pickerId)); - } }); -const toggleItemOpen = (nodeId: string, status: TreeItemStatus, pickerId: string) => +const toggleItemOpen = (id: string, status: TreeItemStatus, pickerId: string) => (dispatch: Dispatch, getState: () => RootState, services: ServiceRepository) => { if (status === TreeItemStatus.INITIAL) { if (pickerId === TreePickerId.PROJECTS) { - dispatch(loadProjectTreePickerProjects(nodeId)); + dispatch(loadProjectTreePickerProjects(id)); } else if (pickerId === TreePickerId.FAVORITES) { - dispatch(loadFavoriteTreePickerProjects(nodeId === services.authService.getUuid() ? '' : nodeId)); + dispatch(loadFavoriteTreePickerProjects(id === services.authService.getUuid() ? '' : id)); } else { // TODO: load sharedWithMe } } else { - dispatch(treePickerActions.TOGGLE_TREE_PICKER_NODE_COLLAPSE({ nodeId, pickerId })); + dispatch(treePickerActions.TOGGLE_TREE_PICKER_NODE_COLLAPSE({ id, pickerId })); } }; @@ -75,11 +76,11 @@ export const ProjectTreePicker = connect(undefined, mapDispatchToProps)((props: // TODO: move action creator to store directory -export const loadProjectTreePickerProjects = (nodeId: string) => +export const loadProjectTreePickerProjects = (id: string) => async (dispatch: Dispatch, getState: () => RootState, services: ServiceRepository) => { - dispatch(treePickerActions.LOAD_TREE_PICKER_NODE({ nodeId, pickerId: TreePickerId.PROJECTS })); + dispatch(treePickerActions.LOAD_TREE_PICKER_NODE({ id, pickerId: TreePickerId.PROJECTS })); - const ownerUuid = nodeId.length === 0 ? services.authService.getUuid() || '' : nodeId; + const ownerUuid = id.length === 0 ? services.authService.getUuid() || '' : id; const filters = new FilterBuilder() .addEqual('ownerUuid', ownerUuid) @@ -87,27 +88,27 @@ export const loadProjectTreePickerProjects = (nodeId: string) => const { items } = await services.projectService.list({ filters }); - dispatch(receiveTreePickerData(nodeId, items, TreePickerId.PROJECTS)); + dispatch(receiveTreePickerData(id, items, TreePickerId.PROJECTS)); }; -export const loadFavoriteTreePickerProjects = (nodeId: string) => +export const loadFavoriteTreePickerProjects = (id: string) => async (dispatch: Dispatch, getState: () => RootState, services: ServiceRepository) => { const parentId = services.authService.getUuid() || ''; - if (nodeId === '') { - dispatch(treePickerActions.LOAD_TREE_PICKER_NODE({ nodeId: parentId, pickerId: TreePickerId.FAVORITES })); + if (id === '') { + dispatch(treePickerActions.LOAD_TREE_PICKER_NODE({ id: parentId, pickerId: TreePickerId.FAVORITES })); const { items } = await services.favoriteService.list(parentId); dispatch(receiveTreePickerData(parentId, items as ProjectResource[], TreePickerId.FAVORITES)); } else { - dispatch(treePickerActions.LOAD_TREE_PICKER_NODE({ nodeId, pickerId: TreePickerId.FAVORITES })); + dispatch(treePickerActions.LOAD_TREE_PICKER_NODE({ id, pickerId: TreePickerId.FAVORITES })); const filters = new FilterBuilder() - .addEqual('ownerUuid', nodeId) + .addEqual('ownerUuid', id) .getFilters(); const { items } = await services.projectService.list({ filters }); - dispatch(receiveTreePickerData(nodeId, items, TreePickerId.FAVORITES)); + dispatch(receiveTreePickerData(id, items, TreePickerId.FAVORITES)); } }; @@ -134,15 +135,15 @@ const renderTreeItem = (item: TreeItem) => // TODO: move action creator to store directory -export const receiveTreePickerData = (nodeId: string, projects: ProjectResource[], pickerId: string) => +export const receiveTreePickerData = (id: string, projects: ProjectResource[], pickerId: string) => (dispatch: Dispatch) => { dispatch(treePickerActions.LOAD_TREE_PICKER_NODE_SUCCESS({ - nodeId, - nodes: projects.map(project => createTreePickerNode({ nodeId: project.uuid, value: project })), + id, + nodes: projects.map(project => initTreeNode({ id: project.uuid, value: project })), pickerId, })); - dispatch(treePickerActions.TOGGLE_TREE_PICKER_NODE_COLLAPSE({ nodeId, pickerId })); + dispatch(treePickerActions.TOGGLE_TREE_PICKER_NODE_COLLAPSE({ id, pickerId })); }; export const ProjectTreePickerField = (props: WrappedFieldProps) => diff --git a/src/views-components/project-tree/project-tree.tsx b/src/views-components/project-tree/project-tree.tsx index 8c1ed330..fe808af5 100644 --- a/src/views-components/project-tree/project-tree.tsx +++ b/src/views-components/project-tree/project-tree.tsx @@ -19,15 +19,15 @@ const styles: StyleRulesCallback = (theme: ArvadosTheme) => ({ } }); -export interface ProjectTreeProps { +export interface ProjectTreeProps { projects: Array>; - toggleOpen: (id: string, status: TreeItemStatus) => void; - toggleActive: (id: string, status: TreeItemStatus) => void; + toggleOpen: (event: React.MouseEvent, item: TreeItem) => void; + toggleActive: (event: React.MouseEvent, item: TreeItem) => void; onContextMenu: (event: React.MouseEvent, item: TreeItem) => void; } export const ProjectTree = withStyles(styles)( - class ProjectTreeGeneric extends React.Component> { + class ProjectTreeGeneric extends React.Component & WithStyles> { render(): ReactElement { const { classes, projects, toggleOpen, toggleActive, onContextMenu } = this.props; return ( @@ -42,8 +42,8 @@ export const ProjectTree = withStyles(styles)( icon={ProjectIcon} name={project.data.name} isActive={project.active} - hasMargin={true}/> - }/> + hasMargin={true} /> + } /> ); } diff --git a/src/views-components/projects-tree-picker/favorites-tree-picker.tsx b/src/views-components/projects-tree-picker/favorites-tree-picker.tsx new file mode 100644 index 00000000..09704066 --- /dev/null +++ b/src/views-components/projects-tree-picker/favorites-tree-picker.tsx @@ -0,0 +1,17 @@ +// Copyright (C) The Arvados Authors. All rights reserved. +// +// SPDX-License-Identifier: AGPL-3.0 + +import { connect } from 'react-redux'; +import { ProjectsTreePicker, ProjectsTreePickerProps } from '~/views-components/projects-tree-picker/generic-projects-tree-picker'; +import { Dispatch } from 'redux'; +import { FavoriteIcon } from '~/components/icon/icon'; +import { loadFavoritesProject } from '~/store/tree-picker/tree-picker-actions'; + +export const FavoritesTreePicker = connect(() => ({ + rootItemIcon: FavoriteIcon, +}), (dispatch: Dispatch): Pick => ({ + loadRootItem: (_, pickerId, includeCollections, includeFiles) => { + dispatch(loadFavoritesProject({ pickerId, includeCollections, includeFiles })); + }, +}))(ProjectsTreePicker); \ No newline at end of file diff --git a/src/views-components/projects-tree-picker/generic-projects-tree-picker.tsx b/src/views-components/projects-tree-picker/generic-projects-tree-picker.tsx new file mode 100644 index 00000000..21839ff5 --- /dev/null +++ b/src/views-components/projects-tree-picker/generic-projects-tree-picker.tsx @@ -0,0 +1,109 @@ +// 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 { TreeItem, TreeItemStatus } from '~/components/tree/tree'; +import { ProjectResource } from "~/models/project"; +import { treePickerActions } from "~/store/tree-picker/tree-picker-actions"; +import { ListItemTextIcon } from "~/components/list-item-text-icon/list-item-text-icon"; +import { ProjectIcon, InputIcon, IconType, CollectionIcon } from '~/components/icon/icon'; +import { loadProject, loadCollection } from '~/store/tree-picker/tree-picker-actions'; +import { GroupContentsResource } from '~/services/groups-service/groups-service'; +import { CollectionDirectory, CollectionFile, CollectionFileType } from '~/models/collection-file'; +import { ResourceKind } from '~/models/resource'; +import { TreePickerProps, TreePicker } from "~/views-components/tree-picker/tree-picker"; + +export interface ProjectsTreePickerRootItem { + id: string; + name: string; +} + +export type ProjectsTreePickerItem = ProjectsTreePickerRootItem | GroupContentsResource | CollectionDirectory | CollectionFile; +type PickedTreePickerProps = Pick, 'onContextMenu' | 'toggleItemActive' | 'toggleItemOpen' | 'toggleItemSelection'>; + +export interface ProjectsTreePickerDataProps { + includeCollections?: boolean; + includeFiles?: boolean; + rootItemIcon: IconType; + showSelection?: boolean; + loadRootItem: (item: TreeItem, pickerId: string, includeCollections?: boolean, inlcudeFiles?: boolean) => void; +} + +export type ProjectsTreePickerProps = ProjectsTreePickerDataProps & Partial; + +const mapStateToProps = (_: any, { rootItemIcon, showSelection }: ProjectsTreePickerProps) => ({ + render: renderTreeItem(rootItemIcon), + showSelection: isSelectionVisible(showSelection), +}); + +const mapDispatchToProps = (dispatch: Dispatch, { loadRootItem, includeCollections, includeFiles, ...props }: ProjectsTreePickerProps): PickedTreePickerProps => ({ + onContextMenu: () => { return; }, + toggleItemActive: (event, item, pickerId) => { + dispatch(treePickerActions.ACTIVATE_TREE_PICKER_NODE({ id: item.id, pickerId })); + if (props.toggleItemActive) { + props.toggleItemActive(event, item, pickerId); + } + }, + toggleItemOpen: (_, item, pickerId) => { + const { id, data, status } = item; + if (status === TreeItemStatus.INITIAL) { + if ('kind' in data) { + dispatch( + data.kind === ResourceKind.COLLECTION + ? loadCollection(id, pickerId) + : loadProject({ id, pickerId, includeCollections, includeFiles }) + ); + } else if (!('type' in data) && loadRootItem) { + loadRootItem(item as TreeItem, pickerId, includeCollections, includeFiles); + } + } else if (status === TreeItemStatus.LOADED) { + dispatch(treePickerActions.TOGGLE_TREE_PICKER_NODE_COLLAPSE({ id, pickerId })); + } + }, + toggleItemSelection: (_, { id }, pickerId) => { + dispatch(treePickerActions.TOGGLE_TREE_PICKER_NODE_SELECTION({ id, pickerId })); + }, +}); + +export const ProjectsTreePicker = connect(mapStateToProps, mapDispatchToProps)(TreePicker); + +const getProjectPickerIcon = ({ data }: TreeItem, rootIcon: IconType): IconType => { + if ('kind' in data) { + switch (data.kind) { + case ResourceKind.COLLECTION: + return CollectionIcon; + default: + return ProjectIcon; + } + } else if ('type' in data) { + switch (data.type) { + case CollectionFileType.FILE: + return InputIcon; + default: + return ProjectIcon; + } + } else { + return rootIcon; + } +}; + +const isSelectionVisible = (shouldBeVisible?: boolean) => + ({ status, items }: TreeItem): boolean => { + if (shouldBeVisible) { + if (items && items.length > 0) { + return items.every(isSelectionVisible(shouldBeVisible)); + } + return status === TreeItemStatus.LOADED; + } + return false; + }; + +const renderTreeItem = (rootItemIcon: IconType) => (item: TreeItem) => + ; diff --git a/src/views-components/projects-tree-picker/home-tree-picker.tsx b/src/views-components/projects-tree-picker/home-tree-picker.tsx new file mode 100644 index 00000000..45f0b5c0 --- /dev/null +++ b/src/views-components/projects-tree-picker/home-tree-picker.tsx @@ -0,0 +1,17 @@ +// Copyright (C) The Arvados Authors. All rights reserved. +// +// SPDX-License-Identifier: AGPL-3.0 + +import { connect } from 'react-redux'; +import { ProjectsTreePicker, ProjectsTreePickerProps } from '~/views-components/projects-tree-picker/generic-projects-tree-picker'; +import { Dispatch } from 'redux'; +import { loadUserProject } from '~/store/tree-picker/tree-picker-actions'; +import { ProjectIcon } from '~/components/icon/icon'; + +export const HomeTreePicker = connect(() => ({ + rootItemIcon: ProjectIcon, +}), (dispatch: Dispatch): Pick => ({ + loadRootItem: (_, pickerId, includeCollections, includeFiles) => { + dispatch(loadUserProject(pickerId, includeCollections, includeFiles)); + }, +}))(ProjectsTreePicker); \ No newline at end of file diff --git a/src/views-components/projects-tree-picker/projects-tree-picker.tsx b/src/views-components/projects-tree-picker/projects-tree-picker.tsx new file mode 100644 index 00000000..5d6bb78c --- /dev/null +++ b/src/views-components/projects-tree-picker/projects-tree-picker.tsx @@ -0,0 +1,28 @@ +// Copyright (C) The Arvados Authors. All rights reserved. +// +// SPDX-License-Identifier: AGPL-3.0 + +import * as React from 'react'; +import { HomeTreePicker } from '~/views-components/projects-tree-picker/home-tree-picker'; +import { SharedTreePicker } from '~/views-components/projects-tree-picker/shared-tree-picker'; +import { FavoritesTreePicker } from '~/views-components/projects-tree-picker/favorites-tree-picker'; +import { getProjectsTreePickerIds } from '~/store/tree-picker/tree-picker-actions'; +import { TreeItem } from '~/components/tree/tree'; +import { ProjectsTreePickerItem } from './generic-projects-tree-picker'; + +export interface ProjectsTreePickerProps { + pickerId: string; + includeCollections?: boolean; + includeFiles?: boolean; + showSelection?: boolean; + toggleItemActive?: (event: React.MouseEvent, item: TreeItem, pickerId: string) => void; +} + +export const ProjectsTreePicker = ({ pickerId, ...props }: ProjectsTreePickerProps) => { + const { home, shared, favorites } = getProjectsTreePickerIds(pickerId); + return
+ + + +
; +}; diff --git a/src/views-components/projects-tree-picker/shared-tree-picker.tsx b/src/views-components/projects-tree-picker/shared-tree-picker.tsx new file mode 100644 index 00000000..a986b1bb --- /dev/null +++ b/src/views-components/projects-tree-picker/shared-tree-picker.tsx @@ -0,0 +1,17 @@ +// Copyright (C) The Arvados Authors. All rights reserved. +// +// SPDX-License-Identifier: AGPL-3.0 + +import { connect } from 'react-redux'; +import { ProjectsTreePicker, ProjectsTreePickerProps } from '~/views-components/projects-tree-picker/generic-projects-tree-picker'; +import { Dispatch } from 'redux'; +import { ShareMeIcon } from '~/components/icon/icon'; +import { loadProject } from '~/store/tree-picker/tree-picker-actions'; + +export const SharedTreePicker = connect(() => ({ + rootItemIcon: ShareMeIcon, +}), (dispatch: Dispatch): Pick => ({ + loadRootItem: (_, pickerId, includeCollections, includeFiles) => { + dispatch(loadProject({ id: 'Shared with me', pickerId, includeCollections, includeFiles, loadShared: true })); + }, +}))(ProjectsTreePicker); \ No newline at end of file diff --git a/src/views-components/side-panel-tree/side-panel-tree.tsx b/src/views-components/side-panel-tree/side-panel-tree.tsx index 4d4760fa..0e34d3ad 100644 --- a/src/views-components/side-panel-tree/side-panel-tree.tsx +++ b/src/views-components/side-panel-tree/side-panel-tree.tsx @@ -13,25 +13,26 @@ import { ProjectIcon, FavoriteIcon, ProjectsIcon, ShareMeIcon, TrashIcon } from import { RecentIcon, WorkflowIcon } from '~/components/icon/icon'; import { activateSidePanelTreeItem, toggleSidePanelTreeItemCollapse, SIDE_PANEL_TREE, SidePanelTreeCategory } from '~/store/side-panel-tree/side-panel-tree-actions'; import { openSidePanelContextMenu } from '~/store/context-menu/context-menu-actions'; - +import { noop } from 'lodash'; export interface SidePanelTreeProps { onItemActivation: (id: string) => void; sidePanelProgress?: boolean; } -type SidePanelTreeActionProps = Pick; +type SidePanelTreeActionProps = Pick, 'onContextMenu' | 'toggleItemActive' | 'toggleItemOpen' | 'toggleItemSelection'>; const mapDispatchToProps = (dispatch: Dispatch, props: SidePanelTreeProps): SidePanelTreeActionProps => ({ - onContextMenu: (event, id) => { + onContextMenu: (event, { id }) => { dispatch(openSidePanelContextMenu(event, id)); }, - toggleItemActive: (nodeId) => { - dispatch(activateSidePanelTreeItem(nodeId)); - props.onItemActivation(nodeId); + toggleItemActive: (_, { id }) => { + dispatch(activateSidePanelTreeItem(id)); + props.onItemActivation(id); }, - toggleItemOpen: (nodeId) => { - dispatch(toggleSidePanelTreeItemCollapse(nodeId)); - } + toggleItemOpen: (_, { id }) => { + dispatch(toggleSidePanelTreeItemCollapse(id)); + }, + toggleItemSelection: noop, }); export const SidePanelTree = connect(undefined, mapDispatchToProps)( diff --git a/src/views-components/tree-picker/tree-picker.ts b/src/views-components/tree-picker/tree-picker.ts index 8b7630ab..cb829059 100644 --- a/src/views-components/tree-picker/tree-picker.ts +++ b/src/views-components/tree-picker/tree-picker.ts @@ -5,25 +5,28 @@ import { connect } from "react-redux"; import { Tree, TreeProps, TreeItem, TreeItemStatus } from "~/components/tree/tree"; import { RootState } from "~/store/store"; -import { createTreePickerNode, TreePickerNode } from "~/store/tree-picker/tree-picker"; -import { getNodeValue, getNodeChildrenIds, Tree as Ttree, createTree } from "~/models/tree"; +import { getNodeChildrenIds, Tree as Ttree, createTree, getNode, TreeNodeStatus } from '~/models/tree'; import { Dispatch } from "redux"; +import { initTreeNode } from '../../models/tree'; -export interface TreePickerProps { +type Callback = (event: React.MouseEvent, item: TreeItem, pickerId: string) => void; +export interface TreePickerProps { 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; + onContextMenu: Callback; + toggleItemOpen: Callback; + toggleItemActive: Callback; + toggleItemSelection: Callback; } const memoizedMapStateToProps = () => { - let prevTree: Ttree; - let mappedProps: Pick, 'items'>; - return (state: RootState, props: TreePickerProps): Pick, 'items'> => { + let prevTree: Ttree; + let mappedProps: Pick, 'items' | 'disableRipple'>; + return (state: RootState, props: TreePickerProps): Pick, 'items' | 'disableRipple'> => { const tree = state.treePicker[props.pickerId] || createTree(); - if(tree !== prevTree){ + if (tree !== prevTree) { prevTree = tree; mappedProps = { + disableRipple: true, items: getNodeChildrenIds('')(tree) .map(treePickerToTreeItems(tree)) }; @@ -32,26 +35,39 @@ const memoizedMapStateToProps = () => { }; }; -const mapDispatchToProps = (dispatch: Dispatch, props: TreePickerProps): 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) +const mapDispatchToProps = (_: Dispatch, props: TreePickerProps): Pick, 'onContextMenu' | 'toggleItemOpen' | 'toggleItemActive' | 'toggleItemSelection'> => ({ + onContextMenu: (event, item) => props.onContextMenu(event, item, props.pickerId), + toggleItemActive: (event, item) => props.toggleItemActive(event, item, props.pickerId), + toggleItemOpen: (event, item) => props.toggleItemOpen(event, item, props.pickerId), + toggleItemSelection: (event, item) => props.toggleItemSelection(event, item, props.pickerId), }); export const TreePicker = connect(memoizedMapStateToProps(), mapDispatchToProps)(Tree); -const treePickerToTreeItems = (tree: Ttree) => +const treePickerToTreeItems = (tree: Ttree) => (id: string): TreeItem => { - const node: TreePickerNode = getNodeValue(id)(tree) || createTreePickerNode({ nodeId: '', value: 'InvalidNode' }); - const items = getNodeChildrenIds(node.nodeId)(tree) + const node = getNode(id)(tree) || initTreeNode({ id: '', value: 'InvalidNode' }); + const items = getNodeChildrenIds(node.id)(tree) .map(treePickerToTreeItems(tree)); return { - active: node.selected, + active: node.active, data: node.value, - id: node.nodeId, + id: node.id, items: items.length > 0 ? items : undefined, - open: !node.collapsed, - status: node.status + open: node.expanded, + selected: node.selected, + status: treeNodeStatusToTreeItem(node.status), }; }; +export const treeNodeStatusToTreeItem = (status: TreeNodeStatus) => { + switch (status) { + case TreeNodeStatus.INITIAL: + return TreeItemStatus.INITIAL; + case TreeNodeStatus.PENDING: + return TreeItemStatus.PENDING; + case TreeNodeStatus.LOADED: + return TreeItemStatus.LOADED; + } +}; + diff --git a/src/views/workbench/workbench.tsx b/src/views/workbench/workbench.tsx index 553afa4d..d326c313 100644 --- a/src/views/workbench/workbench.tsx +++ b/src/views/workbench/workbench.tsx @@ -41,7 +41,10 @@ import { SharedWithMePanel } from '~/views/shared-with-me-panel/shared-with-me-p import { RunProcessPanel } from '~/views/run-process-panel/run-process-panel'; import SplitterLayout from 'react-splitter-layout'; import { WorkflowPanel } from '~/views/workflow-panel/workflow-panel'; -import { FileSelectionDialog } from '~/views-components/dialog-forms/file-selection-dialog'; +import { HomeTreePicker } from '~/views-components/projects-tree-picker/home-tree-picker'; +import { SharedTreePicker } from '~/views-components/projects-tree-picker/shared-tree-picker'; +import { FavoritesTreePicker } from '../../views-components/projects-tree-picker/favorites-tree-picker'; +import { ProjectsTreePicker } from '~/views-components/projects-tree-picker/projects-tree-picker'; type CssRules = 'root' | 'container' | 'splitter' | 'asidePanel' | 'contentWrapper' | 'content'; @@ -75,8 +78,8 @@ const styles: StyleRulesCallback = (theme: ArvadosTheme) => ({ type WorkbenchPanelProps = WithStyles; -export const WorkbenchPanel = - withStyles(styles)(({ classes }: WorkbenchPanelProps) => +export const WorkbenchPanel = + withStyles(styles)(({ classes }: WorkbenchPanelProps) => - - diff --git a/yarn.lock b/yarn.lock index e6f3764c..906596ca 100644 --- a/yarn.lock +++ b/yarn.lock @@ -6854,6 +6854,10 @@ requires-port@^1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/requires-port/-/requires-port-1.0.0.tgz#925d2601d39ac485e091cf0da5c6e694dc3dcaff" +reselect@4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/reselect/-/reselect-4.0.0.tgz#f2529830e5d3d0e021408b246a206ef4ea4437f7" + resolve-cwd@^2.0.0: version "2.0.0" resolved "https://registry.yarnpkg.com/resolve-cwd/-/resolve-cwd-2.0.0.tgz#00a9f7387556e27038eae232caa372a6a59b665a"