Merge branch 'master'
authorMichal Klobukowski <michal.klobukowski@contractors.roche.com>
Tue, 9 Oct 2018 11:36:57 +0000 (13:36 +0200)
committerMichal Klobukowski <michal.klobukowski@contractors.roche.com>
Tue, 9 Oct 2018 11:36:57 +0000 (13:36 +0200)
Feature #13862

Arvados-DCO-1.1-Signed-off-by: Michal Klobukowski <michal.klobukowski@contractors.roche.com>

30 files changed:
src/components/file-tree/file-tree.tsx
src/components/tree/tree.tsx
src/index.tsx
src/models/collection-file.ts
src/models/tree.test.ts
src/models/tree.ts
src/services/collection-files-service/collection-manifest-mapper.ts
src/store/breadcrumbs/breadcrumbs-actions.ts
src/store/collection-panel/collection-panel-files/collection-panel-files-reducer.test.ts
src/store/file-tree-picker/file-tree-picker-actions.ts [deleted file]
src/store/file-tree-picker/file-tree-picker-reducer.ts [deleted file]
src/store/file-tree-picker/file-tree-picker.ts [deleted file]
src/store/side-panel-tree/side-panel-tree-actions.ts
src/store/store.ts
src/store/tree-picker/tree-picker-actions.ts
src/store/tree-picker/tree-picker-reducer.test.ts
src/store/tree-picker/tree-picker-reducer.ts
src/store/tree-picker/tree-picker.ts
src/views-components/dialog-file-selection/dialog-file-selection.tsx [deleted file]
src/views-components/dialog-forms/file-selection-dialog.ts [deleted file]
src/views-components/file-tree-picker/file-tree-picker.tsx [deleted file]
src/views-components/file-tree-picker/main-file-tree-picker.ts [deleted file]
src/views-components/project-tree-picker/project-tree-picker.tsx
src/views-components/project-tree/project-tree.tsx
src/views-components/projects-tree-picker/projects-tree-picker.tsx [new file with mode: 0644]
src/views-components/projects-tree-picker/shared-projects-tree-picker.tsx [new file with mode: 0644]
src/views-components/projects-tree-picker/user-projects-tree-picker.tsx [new file with mode: 0644]
src/views-components/side-panel-tree/side-panel-tree.tsx
src/views-components/tree-picker/tree-picker.ts
src/views/workbench/workbench.tsx

index 06fc8b7855d26ad66fafd3e173fb1b05af458f37..0a96254c58607c2ccc477a1128c624fb75f76cc9 100644 (file)
@@ -24,7 +24,7 @@ export class FileTree extends React.Component<FileTreeProps> {
             onContextMenu={this.handleContextMenu}
             toggleItemActive={this.handleToggleActive}
             toggleItemOpen={this.handleToggle}
-            onSelectionChange={this.handleSelectionChange} />;
+            toggleItemSelection={this.handleSelectionChange} />;
     }
 
     handleContextMenu = (event: React.MouseEvent<any>, item: TreeItem<FileTreeData>) => {
@@ -32,7 +32,7 @@ export class FileTree extends React.Component<FileTreeProps> {
         this.props.onMenuOpen(event, item);
     }
 
-    handleToggle = (id: string, status: TreeItemStatus) => {
+    handleToggle = (event: React.MouseEvent<{}>, { id, status }: TreeItem<{}>) => {
         this.props.onCollapseToggle(id, status);
     }
 
index c892d7d2c8fb12b86da3e4904b9dc9c8198148e1..df3fc7f3bd76bbf396e8f16e0975dede4c87d0d7 100644 (file)
@@ -60,7 +60,8 @@ const styles: StyleRulesCallback<CssRules> = (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<T> {
 }
 
 export interface TreeProps<T> {
+    disableRipple?: boolean;
     items?: Array<TreeItem<T>>;
-    render: (item: TreeItem<T>, level?: number) => ReactElement<{}>;
-    toggleItemOpen: (id: string, status: TreeItemStatus) => void;
-    toggleItemActive: (id: string, status: TreeItemStatus) => void;
     level?: number;
     onContextMenu: (event: React.MouseEvent<HTMLElement>, item: TreeItem<T>) => void;
+    render: (item: TreeItem<T>, level?: number) => ReactElement<{}>;
     showSelection?: boolean;
-    onSelectionChange?: (event: React.MouseEvent<HTMLElement>, item: TreeItem<T>) => void;
-    disableRipple?: boolean;
+    toggleItemActive: (event: React.MouseEvent<HTMLElement>, item: TreeItem<T>) => void;
+    toggleItemOpen: (event: React.MouseEvent<HTMLElement>, item: TreeItem<T>) => void;
+    toggleItemSelection?: (event: React.MouseEvent<HTMLElement>, item: TreeItem<T>) => void;
 }
 
 export const Tree = withStyles(styles)(
@@ -103,11 +104,11 @@ export const Tree = withStyles(styles)(
                     <div key={`item/${level}/${idx}`}>
                         <ListItem button className={listItem} style={{ paddingLeft: (level + 1) * 20 }}
                             disableRipple={disableRipple}
-                            onClick={() => toggleItemActive(it.id, it.status)}
+                            onClick={event => toggleItemActive(event, it)}
                             onContextMenu={this.handleRowContextMenu(it)}>
                             {it.status === TreeItemStatus.PENDING ?
                                 <CircularProgress size={10} className={loader} /> : null}
-                            <i onClick={this.handleToggleItemOpen(it.id, it.status)}
+                            <i onClick={this.handleToggleItemOpen(it)}
                                 className={toggableIconContainer}>
                                 <ListItemIcon className={this.getToggableIconClassNames(it.open, it.active)}>
                                     {this.getProperArrowAnimation(it.status, it.items!)}
@@ -134,7 +135,7 @@ export const Tree = withStyles(styles)(
                                     toggleItemActive={toggleItemActive}
                                     level={level + 1}
                                     onContextMenu={onContextMenu}
-                                    onSelectionChange={this.props.onSelectionChange} />
+                                    toggleItemSelection={this.props.toggleItemSelection} />
                             </Collapse>}
                     </div>)}
             </List>;
@@ -164,17 +165,18 @@ export const Tree = withStyles(styles)(
                 this.props.onContextMenu(event, item)
 
         handleCheckboxChange = (item: TreeItem<T>) => {
-            const { onSelectionChange } = this.props;
-            return onSelectionChange
+            const { toggleItemSelection } = this.props;
+            return toggleItemSelection
                 ? (event: React.MouseEvent<HTMLElement>) => {
-                    onSelectionChange(event, item);
+                    event.stopPropagation();
+                    toggleItemSelection(event, item);
                 }
                 : undefined;
         }
 
-        handleToggleItemOpen = (id: string, status: TreeItemStatus) => (event: React.MouseEvent<HTMLElement>) => {
+        handleToggleItemOpen = (item: TreeItem<T>) => (event: React.MouseEvent<HTMLElement>) => {
             event.stopPropagation();
-            this.props.toggleItemOpen(id, status);
+            this.props.toggleItemOpen(event, item);
         }
     }
 );
index a4a5e366a26b1c93713e580c4ce717a1c4e4a117..84b765792bd7411043bf0d616a1fb1eb47fe8335 100644 (file)
@@ -41,7 +41,10 @@ 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 { receiveTreePickerData, loadUserProject } from '~/store/tree-picker/tree-picker-actions';
+import { loadProject, loadCollection, initUserProject } from './store/tree-picker/tree-picker-actions';
+import { ResourceKind } from '~/models/resource';
 
 const getBuildNumber = () => "BN-" + (process.env.REACT_APP_BUILD_NUMBER || "dev");
 const getGitCommit = () => "GIT-" + (process.env.REACT_APP_GIT_COMMIT || "latest").substr(0, 7);
@@ -114,24 +117,31 @@ const initListener = (history: History, store: RootStore, services: ServiceRepos
             await store.dispatch(loadWorkbench());
             addRouteChangeHandlers(history, store);
             // createEnumCollectorWorkflow(services);
+            store.dispatch(initUserProject('testPicker1'));
+            store.dispatch(initUserProject('testPicker2'));
+            store.dispatch(initUserProject('testPicker3'));
+            // await store.dispatch(loadCollection(
+            //     'c97qk-4zz18-9sn8ygaf62chkkd',
+            //     'testPicker',
+            // ));
         }
     };
 };
 
-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) => {
@@ -157,15 +167,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",
@@ -231,7 +241,7 @@ const createSampleProcess = ({ containerRequestService }: ServiceRepository) =>
                         "class": "File",
                         "location": "keep:233454526794c0a2d56a305baeff3d30+145/1.txt",
                         "basename": "fileA"
-                      }
+                    }
                 },
             }
         },
index d74ada6008b982fe40daca7e601fbfbb99f1ea57..37e18cfc0d247905900d3fec4ac6160a8d3afe01 100644 (file)
@@ -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<CollectionDirectory | CollectionFile>;
 
@@ -60,6 +60,11 @@ export const createCollectionFilesTree = (data: Array<CollectionDirectory | Coll
             children: [],
             id: item.id,
             parent: item.path,
-            value: item
+            value: item,
+            active: false,
+            selected: false,
+            expanded: false,
+            status: TreeNodeStatus.INITIAL
+
         })(tree), createTree<CollectionDirectory | CollectionFile>());
 };
\ No newline at end of file
index 375a012054f9bea3a26a7bd8033642b44697ea24..54b11d47aadd2795cc32f8ce1f2ef095270d9b8e 100644 (file)
@@ -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<string>;
@@ -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<string, number>(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
+});
index a5fb49cff4a3adb3945cdcfbcb1a1ec6b4ac5080..69d2e9376939f5a3d8ba6fa6180a1b1e0558b626 100644 (file)
@@ -2,6 +2,7 @@
 //
 // SPDX-License-Identifier: AGPL-3.0
 
+import { pipe } from 'lodash/fp';
 export type Tree<T> = Record<string, TreeNode<T>>;
 
 export const TREE_ROOT_ID = '';
@@ -11,6 +12,16 @@ export interface TreeNode<T = any> {
     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 = <T>(): Tree<T> => ({});
@@ -18,12 +29,12 @@ export const createTree = <T>(): Tree<T> => ({});
 export const getNode = (id: string) => <T>(tree: Tree<T>): TreeNode<T> | undefined => tree[id];
 
 export const setNode = <T>(node: TreeNode<T>) => (tree: Tree<T>): Tree<T> => {
-    const [newTree] = [tree]
-        .map(tree => getNode(node.id)(tree) === node
+    return pipe(
+        (tree: Tree<T>) => 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) => <T>(tree: Tree<T>) => {
@@ -95,6 +106,79 @@ export const getNodeChildrenIds = (id: string) => <T>(tree: Tree<T>): string[] =
 export const mapIdsToNodes = (ids: string[]) => <T>(tree: Tree<T>) =>
     ids.map(id => getNode(id)(tree)).filter((node): node is TreeNode<T> => node !== undefined);
 
+export const activateNode = (id: string) => <T>(tree: Tree<T>) =>
+    mapTree(node => node.id === id ? { ...node, active: true } : { ...node, active: false })(tree);
+
+
+export const expandNode = (...ids: string[]) => <T>(tree: Tree<T>) =>
+    mapTree(node => ids.some(id => id === node.id) ? { ...node, expanded: true } : node)(tree);
+
+export const collapseNode = (...ids: string[]) => <T>(tree: Tree<T>) =>
+    mapTree(node => ids.some(id => id === node.id) ? { ...node, expanded: false } : node)(tree);
+
+export const toggleNodeCollapse = (...ids: string[]) => <T>(tree: Tree<T>) =>
+    mapTree(node => ids.some(id => id === node.id) ? { ...node, expanded: !node.expanded } : node)(tree);
+
+export const setNodeStatus = (id: string) => (status: TreeNodeStatus) => <T>(tree: Tree<T>) => {
+    const node = getNode(id)(tree);
+    return node
+        ? setNode({ ...node, status })(tree)
+        : tree;
+};
+
+export const toggleNodeSelection = (id: string) => <T>(tree: Tree<T>) => {
+    const node = getNode(id)(tree);
+    return node
+        ? pipe(
+            setNode({ ...node, selected: !node.selected }),
+            toggleAncestorsSelection(id),
+            toggleDescendantsSelection(id))(tree)
+        : tree;
+
+};
+
+export const initTreeNode = <T>(data: Pick<TreeNode<T>, 'id' | 'value'> & {parent?: string}): TreeNode<T> => ({
+    children: [],
+    active: false,
+    selected: false,
+    expanded: false,
+    status: TreeNodeStatus.INITIAL,
+    parent: '',
+    ...data,
+});
+
+const toggleDescendantsSelection = (id: string) => <T>(tree: Tree<T>) => {
+    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) => <T>(tree: Tree<T>) => {
+    const ancestors = getNodeAncestorsIds(id)(tree).reverse();
+    return ancestors.reduce((newTree, parent) => parent ? toggleParentNodeSelection(parent)(newTree) : newTree, tree);
+};
+
+const toggleParentNodeSelection = (id: string) => <T>(tree: Tree<T>) => {
+    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 = <T, R>(mapFn: (value: T) => R) => (node: TreeNode<T>): TreeNode<R> =>
     ({ ...node, value: mapFn(node.value) });
 
index 0c7e91deecf4aba5bc9465569c40118f9401d536..6e64f833e533e1a9f6c4383cbffc4cf757df3827 100644 (file)
@@ -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<Coll
     children: [],
     id: file.id,
     parent: file.path,
-    value: file
+    value: file,
+    active: false,
+    selected: false,
+    expanded: false,
+    status: TreeNodeStatus.INITIAL,
 });
 
 export const manifestToCollectionFiles = (manifest: KeepManifest): Array<CollectionDirectory | CollectionFile> => ([
index a5ded34eaf25048a2df980ba56e6b41e2c533795..8b1eb2b0cc0f0be28b9c68059ecb4528327aa71b 100644 (file)
@@ -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 });
 };
 
index 90dedaaaa4798dedf2d84e66a29c1fe6b75033ad..3964ee4769586d3b988e321cbd03c38582943303 100644 (file)
@@ -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<CollectionFile | CollectionDirectory>());
 
     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 (file)
index 18d0f5f..0000000
+++ /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<TreePickerNode>, 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<typeof fileTreePickerActions>;
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 (file)
index 59e33a9..0000000
+++ /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<TreePickerNode>) => Tree<TreePickerNode>) => {
-    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<TreePickerNode>) => (parent: string) => (state: Tree<TreePickerNode>) => {
-    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<TreePickerNode> => ({
-    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 (file)
index 259a4b8..0000000
+++ /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<TreePickerNode> };
-
-export interface TreePickerNode<Value = any> {
-    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 = <Value = {}>(id: string) => (state: TreePicker): Tree<TreePickerNode<Value>> | undefined => state[id];
\ No newline at end of file
index 22a83dda8c9f71b130e5b8251b3e8d8c321bb714..ec593b9ba341ad7fb203a559b92351fb2f887719 100644 (file)
@@ -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<ProjectResource | string>(SIDE_PANEL_TREE)(treePicker);
 
-export const getSidePanelTreeBranch = (uuid: string) => (treePicker: TreePicker): Array<TreePickerNode<ProjectResource | string>> => {
+export const getSidePanelTreeBranch = (uuid: string) => (treePicker: TreePicker): Array<TreeNode<ProjectResource | string>> => {
     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<any>(activateSidePanelTreeProject(nodeId));
+        if (!isSidePanelTreeCategory(id)) {
+            await dispatch<any>(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<any>(loadSidePanelTreeProjects(nodeId));
+        const node = getSidePanelTreeNode(id)(treePicker);
+        if (node && node.status !== TreeNodeStatus.LOADED) {
+            await dispatch<any>(loadSidePanelTreeProjects(id));
         } else if (node === undefined) {
-            await dispatch<any>(activateSidePanelTreeBranch(nodeId));
+            await dispatch<any>(activateSidePanelTreeBranch(id));
         }
         dispatch(treePickerActions.EXPAND_TREE_PICKER_NODES({
-            nodeIds: getSidePanelTreeNodeAncestorsIds(nodeId)(treePicker),
+            ids: getSidePanelTreeNodeAncestorsIds(id)(treePicker),
             pickerId: SIDE_PANEL_TREE
         }));
-        dispatch<any>(expandSidePanelTreeItem(nodeId));
+        dispatch<any>(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<any>(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<any>(loadSidePanelTreeProjects(node.nodeId));
+        const node = getSidePanelTreeNode(id)(getState().treePicker);
+        if (node && node.status === TreeNodeStatus.INITIAL) {
+            await dispatch<any>(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)
         : [];
 };
index 8e753660b26e19c91f15e17de52239ed506ef6e1..d38689c5029d3223642da4519ecfe033e736304a 100644 (file)
@@ -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 { searchBarReducer } from './search-bar/search-bar-reducer';
 
 const composeEnhancers =
@@ -101,7 +100,6 @@ const createRootReducer = (services: ServiceRepository) => combineReducers({
     fileUploader: fileUploaderReducer,
     processPanel: processPanelReducer,
     progressIndicator: progressIndicatorReducer,
-    fileTreePicker: fileTreePickerReducer,
     runProcessPanel: runProcessPanelReducer,
     searchBar: searchBarReducer
 });
index 5b04389af6850888a3bad224dc11fc507005d5e3..3c3b052af3aefc7e1ef4f76676d55a40815fd69c 100644 (file)
 // 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<TreePickerNode>, 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<TreeNode<any>>, pickerId: string }>(),
+    TOGGLE_TREE_PICKER_NODE_COLLAPSE: ofType<{ id: string, pickerId: string }>(),
+    ACTIVATE_TREE_PICKER_NODE: ofType<{ id: string, 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<typeof treePickerActions>;
+
+interface ReceiveTreePickerDataParams<T> {
+    data: T[];
+    extractNodeData: (value: T) => { id: string, value: T, status?: TreeNodeStatus };
+    id: string;
+    pickerId: string;
+}
+export const receiveTreePickerData = <T>(params: ReceiveTreePickerDataParams<T>) =>
+    (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 }));
+    };
+export const loadProject = (id: string, pickerId: string, includeCollections = false, includeFiles = false) =>
+    async (dispatch: Dispatch, _: () => RootState, services: ServiceRepository) => {
+
+        dispatch(treePickerActions.LOAD_TREE_PICKER_NODE({ id, pickerId }));
+
+        const filters = pipe(
+            (fb: FilterBuilder) => fb.addEqual('ownerUuid', id),
+            fb => includeCollections
+                ? fb.addIsA('uuid', [ResourceKind.PROJECT, ResourceKind.COLLECTION])
+                : fb.addIsA('uuid', [ResourceKind.PROJECT]),
+            fb => fb.getFilters(),
+        )(new FilterBuilder());
+
+        const { items } = await services.groupsService.contents(id, { filters });
+
+        dispatch<any>(receiveTreePickerData<GroupContentsResource>({
+            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<any>(receiveTreePickerData<CollectionDirectory | CollectionFile>({
+            id,
+            pickerId,
+            data,
+            extractNodeData: value => ({
+                id: value.id,
+                status: TreeNodeStatus.LOADED,
+                value,
+            }),
+        }));
+    };
+
+
+export const initUserProject = (pickerId: string) =>
+    async (dispatch: Dispatch<any>, 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<any>, getState: () => RootState, services: ServiceRepository) => {
+        const uuid = services.authService.getUuid();
+        if (uuid) {
+            dispatch(loadProject(uuid, pickerId, includeCollections, includeFiles));
+        }
+    };
+
+
+export const initSharedProject = (pickerId: string) =>
+    async (dispatch: Dispatch<any>, 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,
+            }),
+        }));
+    };
index e09d12d777a485199325da74c146d76f6da375e6..3a0b60e1265d76f9be8b45112de1b02fd1f93c2c 100644 (file)
 //
 // 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<TreePickerNode>();
-        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<TreePickerNode>()
-        }]
-            .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<TreePickerNode>()
-        }]
-            .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<TreePickerNode>()
-        }]
-            .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<TreePickerNode>()
-        }]
-            .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<TreePickerNode>()
-        }]
-            .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<TreePickerNode>()
-        }]
-            .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,
         });
     });
 });
index b0d9bc94caa9843d6774b141ea8687737f773240..69c49052b56aa038177b6a183106a9b4b7aebf84 100644 (file)
@@ -2,72 +2,44 @@
 //
 // 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 } 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)),
+        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<TreePickerNode>) => Tree<TreePickerNode>) => {
+const updateOrCreatePicker = <V>(state: TreePicker, pickerId: string, func: (value: Tree<V>) => Tree<V>) => {
     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<TreePickerNode>) => (parent: string) => (state: Tree<TreePickerNode>) => {
+const receiveNodes = <V>(nodes: Array<TreeNode<V>>) => (parent: string) => (state: Tree<V>) => {
     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<TreePickerNode> => ({
-    children: [],
-    id: node.nodeId,
-    parent,
-    value: node
-});
index 259a4b8d53de78e1b7d9992ae85b4d69d5fe40ca..a4127fa3bba31a2b584459e93214d9d90d824414 100644 (file)
@@ -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<any> };
 
-export type TreePicker = { [key: string]: Tree<TreePickerNode> };
-
-export interface TreePickerNode<Value = any> {
-    nodeId: string;
-    value: Value;
-    selected: boolean;
-    collapsed: boolean;
-    status: TreeItemStatus;
-}
+export const getTreePicker = <Value = {}>(id: string) => (state: TreePicker): Tree<Value> | 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 = <Value = {}>(id: string) => (state: TreePicker): Tree<TreePickerNode<Value>> | 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 (file)
index e7185c0..0000000
+++ /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<CollectionCreateFormDialogData>;
-
-export const DialogFileSelection = (props: FileSelectionProps) =>
-    <FormDialog
-        dialogTitle='Choose a file'
-        formFields={FileSelectionFields}
-        submitLabel='Ok'
-        {...props}
-    />;
-
-const FileSelectionFields = () =>
-    <Field
-        name='tree'
-        validate={FILES_FIELD_VALIDATION}
-        component={FileTreePickerField} />;
-
-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 (file)
index 7c883cb..0000000
+++ /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 (file)
index 129cb75..0000000
+++ /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<MainFileTreePickerProps, 'onContextMenu' | 'toggleItemActive' | 'toggleItemOpen'>;
-
-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<any>(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<any>(loadProjectTreePicker(nodeId));
-            } else if (pickerId === TreePickerId.FAVORITES) {
-                dispatch<any>(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) =>
-    <div style={{ display: 'flex', flexDirection: 'column' }}>
-        <div style={{ flexGrow: 1, overflow: 'auto' }}>
-            <MainFileTreePicker {...props} render={renderTreeItem} pickerId={TreePickerId.PROJECTS} />
-            <MainFileTreePicker {...props} render={renderTreeItem} pickerId={TreePickerId.SHARED_WITH_ME} />
-            <MainFileTreePicker {...props} render={renderTreeItem} pickerId={TreePickerId.FAVORITES} />
-        </div>
-    </div>);
-
-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<any>(loadCollectionFiles(nodeId))
-            : await services.groupsService.contents(ownerUuid, { filters });
-
-        await dispatch<any>(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<any>(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<any>(loadCollectionFiles(nodeId))
-                : await services.groupsService.contents(parentId, { filters });
-
-
-            dispatch<any>(receiveTreePickerData(nodeId, items, TreePickerId.FAVORITES));
-        }
-    };
-
-const getProjectPickerIcon = (item: TreeItem<ProjectResource>) => {
-    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<GroupContentsResource>) => {
-    switch (item.data.kind) {
-        case ResourceKind.COLLECTION:
-            return CollectionIcon;
-        case ResourceKind.PROJECT:
-            return ProjectIcon;
-        default:
-            return ProjectIcon;
-    }
-};
-
-const renderTreeItem = (item: TreeItem<ProjectResource>) =>
-    <ListItemTextIcon
-        icon={getProjectPickerIcon(item)}
-        name={item.data.name}
-        isActive={item.active}
-        hasMargin={true} />;
-
-
-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) =>
-    <div style={{ height: '200px', display: 'flex', flexDirection: 'column' }}>
-        <FileTreePicker onChange={handleChange(props)} />
-        {props.meta.dirty && props.meta.error &&
-            <Typography variant='caption' color='error'>
-                {props.meta.error}
-            </Typography>}
-    </div>;
-
-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 (file)
index dc52ea6..0000000
+++ /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<HTMLElement>, 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<TreePickerNode>;
-    let mappedProps: Pick<TreeProps<any>, 'items'>;
-    return (state: RootState, props: MainFileTreePickerProps): Pick<TreeProps<any>, '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<TreeProps<any>, '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<TreePickerNode>) =>
-    (id: string): TreeItem<any> => {
-        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
-        };
-    };
-
index 9139ee7c20c531a65668995b0fbe4245323848f8..36104d43624dd37746794e232e140b2cd412f5f7 100644 (file)
@@ -12,40 +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<TreePickerProps, 'onContextMenu' | 'toggleItemActive' | 'toggleItemOpen'>;
+type ProjectTreePickerProps = Pick<TreePickerProps<ProjectResource>, 'onContextMenu' | 'toggleItemActive' | 'toggleItemOpen' | 'toggleItemSelection'>;
 
 const mapDispatchToProps = (dispatch: Dispatch, props: { onChange: (projectUuid: string) => void }): ProjectTreePickerProps => ({
     onContextMenu: () => { return; },
-    toggleItemActive: (nodeId, status, pickerId) => {
+    toggleItemActive: (_, { id }, pickerId) => {
         getNotSelectedTreePickerKind(pickerId)
-            .forEach(pickerId => dispatch(treePickerActions.TOGGLE_TREE_PICKER_NODE_SELECT({ nodeId: '', pickerId })));
-        dispatch(treePickerActions.TOGGLE_TREE_PICKER_NODE_SELECT({ nodeId, 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<any>(toggleItemOpen(id, status, pickerId));
+    },
+    toggleItemSelection: (_, { id }, pickerId) => {
+        dispatch<any>(treePickerActions.TOGGLE_TREE_PICKER_NODE_SELECTION({ id, pickerId }));
     },
-    toggleItemOpen: (nodeId, status, pickerId) => {
-        dispatch<any>(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<any>(loadProjectTreePickerProjects(nodeId));
+                dispatch<any>(loadProjectTreePickerProjects(id));
             } else if (pickerId === TreePickerId.FAVORITES) {
-                dispatch<any>(loadFavoriteTreePickerProjects(nodeId === services.authService.getUuid() ? '' : nodeId));
+                dispatch<any>(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 }));
         }
     };
 
@@ -73,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)
@@ -85,27 +88,27 @@ export const loadProjectTreePickerProjects = (nodeId: string) =>
 
         const { items } = await services.projectService.list({ filters });
 
-        dispatch<any>(receiveTreePickerData(nodeId, items, TreePickerId.PROJECTS));
+        dispatch<any>(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<any>(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<any>(receiveTreePickerData(nodeId, items, TreePickerId.FAVORITES));
+            dispatch<any>(receiveTreePickerData(id, items, TreePickerId.FAVORITES));
         }
 
     };
@@ -132,15 +135,15 @@ const renderTreeItem = (item: TreeItem<ProjectResource>) =>
 
 
 // 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) =>
index 8c1ed33059b75bb1ad3056ad2c9f34414eae4374..fe808af5c7c3e7e7124dcd68fb77a46558568ac7 100644 (file)
@@ -19,15 +19,15 @@ const styles: StyleRulesCallback<CssRules> = (theme: ArvadosTheme) => ({
     }
 });
 
-export interface ProjectTreeProps {
+export interface ProjectTreeProps<T> {
     projects: Array<TreeItem<ProjectResource>>;
-    toggleOpen: (id: string, status: TreeItemStatus) => void;
-    toggleActive: (id: string, status: TreeItemStatus) => void;
+    toggleOpen: (event: React.MouseEvent<HTMLElement>, item: TreeItem<T>) => void;
+    toggleActive: (event: React.MouseEvent<HTMLElement>, item: TreeItem<T>) => void;
     onContextMenu: (event: React.MouseEvent<HTMLElement>, item: TreeItem<ProjectResource>) => void;
 }
 
 export const ProjectTree = withStyles(styles)(
-    class ProjectTreeGeneric<T> extends React.Component<ProjectTreeProps & WithStyles<CssRules>> {
+    class ProjectTreeGeneric<T> extends React.Component<ProjectTreeProps<T> & WithStyles<CssRules>> {
         render(): ReactElement<any> {
             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} />
+                        } />
                 </div>
             );
         }
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 (file)
index 0000000..ac6d76e
--- /dev/null
@@ -0,0 +1,98 @@
+// 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;
+}
+
+type ProjectsTreePickerItem = ProjectsTreePickerRootItem | GroupContentsResource | CollectionDirectory | CollectionFile;
+type PickedTreePickerProps = Pick<TreePickerProps<ProjectsTreePickerItem>, 'onContextMenu' | 'toggleItemActive' | 'toggleItemOpen' | 'toggleItemSelection'>;
+
+export interface ProjectsTreePickerDataProps {
+    pickerId: string;
+    includeCollections?: boolean;
+    includeFiles?: boolean;
+    rootItemIcon: IconType;
+    loadRootItem: (item: TreeItem<ProjectsTreePickerRootItem>, pickerId: string, includeCollections?: boolean, inlcudeFiles?: boolean) => void;
+}
+
+export interface ProjectsTreePickerActionProps {
+}
+
+export type ProjectsTreePickerProps = ProjectsTreePickerDataProps & ProjectsTreePickerActionProps;
+
+const mapStateToProps = (_: any, { pickerId, rootItemIcon }: ProjectsTreePickerProps) => ({
+    render: renderTreeItem(rootItemIcon),
+    pickerId,
+});
+
+const mapDispatchToProps = (dispatch: Dispatch, props: ProjectsTreePickerProps): PickedTreePickerProps => ({
+    onContextMenu: () => { return; },
+    toggleItemActive: (_, { id }, pickerId) => {
+        dispatch(treePickerActions.ACTIVATE_TREE_PICKER_NODE({ id, pickerId }));
+    },
+    toggleItemOpen: (_, item, pickerId) => {
+        const { id, data, status } = item;
+        if (status === TreeItemStatus.INITIAL) {
+            if ('kind' in data) {
+                dispatch<any>(
+                    data.kind === ResourceKind.COLLECTION
+                        ? loadCollection(id, pickerId)
+                        : loadProject(id, pickerId, props.includeCollections, props.includeFiles)
+                );
+            } else if (!('type' in data) && props.loadRootItem) {
+                props.loadRootItem(item as TreeItem<ProjectsTreePickerRootItem>, pickerId, props.includeCollections, props.includeFiles);
+            }
+        } else if (status === TreeItemStatus.LOADED) {
+            dispatch(treePickerActions.TOGGLE_TREE_PICKER_NODE_COLLAPSE({ id, pickerId }));
+        }
+    },
+    toggleItemSelection: (_, { id }, pickerId) => {
+        dispatch<any>(treePickerActions.TOGGLE_TREE_PICKER_NODE_SELECTION({ id, pickerId }));
+    },
+});
+
+export const ProjectsTreePicker = connect(mapStateToProps, mapDispatchToProps)(TreePicker);
+
+const getProjectPickerIcon = ({ data }: TreeItem<ProjectsTreePickerItem>, 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 renderTreeItem = (rootItemIcon: IconType) => (item: TreeItem<ProjectResource>) =>
+    <ListItemTextIcon
+        icon={getProjectPickerIcon(item, rootItemIcon)}
+        name={item.data.name}
+        isActive={item.active}
+        hasMargin={true} />;
diff --git a/src/views-components/projects-tree-picker/shared-projects-tree-picker.tsx b/src/views-components/projects-tree-picker/shared-projects-tree-picker.tsx
new file mode 100644 (file)
index 0000000..2039db7
--- /dev/null
@@ -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/projects-tree-picker';
+import { Dispatch } from 'redux';
+import { loadUserProject } from '~/store/tree-picker/tree-picker-actions';
+import { ShareIcon } from '~/components/icon/icon';
+
+export const UserProjectsTreePicker = connect(() => ({
+    rootItemIcon: ShareIcon,
+}), (dispatch: Dispatch): Pick<ProjectsTreePickerProps, 'loadRootItem'> => ({
+    loadRootItem: (_, pickerId, includeCollections, includeFiles) => {
+        dispatch<any>(loadUserProject(pickerId, includeCollections, includeFiles));
+    },
+}))(ProjectsTreePicker);
\ No newline at end of file
diff --git a/src/views-components/projects-tree-picker/user-projects-tree-picker.tsx b/src/views-components/projects-tree-picker/user-projects-tree-picker.tsx
new file mode 100644 (file)
index 0000000..10a0b20
--- /dev/null
@@ -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/projects-tree-picker';
+import { Dispatch } from 'redux';
+import { loadUserProject } from '~/store/tree-picker/tree-picker-actions';
+import { ProjectIcon } from '~/components/icon/icon';
+
+export const UserProjectsTreePicker = connect(() => ({
+    rootItemIcon: ProjectIcon,
+}), (dispatch: Dispatch): Pick<ProjectsTreePickerProps, 'loadRootItem'> => ({
+    loadRootItem: (_, pickerId, includeCollections, includeFiles) => {
+        dispatch<any>(loadUserProject(pickerId, includeCollections, includeFiles));
+    },
+}))(ProjectsTreePicker);
\ No newline at end of file
index 4d4760fac37abf583bebde4342c26dc4f570cf75..0e34d3ad84704f7f123c5bf3fb5a47b79b0a8e5a 100644 (file)
@@ -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<TreePickerProps, 'onContextMenu' | 'toggleItemActive' | 'toggleItemOpen'>;
+type SidePanelTreeActionProps = Pick<TreePickerProps<ProjectResource | string>, 'onContextMenu' | 'toggleItemActive' | 'toggleItemOpen' | 'toggleItemSelection'>;
 
 const mapDispatchToProps = (dispatch: Dispatch, props: SidePanelTreeProps): SidePanelTreeActionProps => ({
-    onContextMenu: (event, id) => {
+    onContextMenu: (event, { id }) => {
         dispatch<any>(openSidePanelContextMenu(event, id));
     },
-    toggleItemActive: (nodeId) => {
-        dispatch<any>(activateSidePanelTreeItem(nodeId));
-        props.onItemActivation(nodeId);
+    toggleItemActive: (_, { id }) => {
+        dispatch<any>(activateSidePanelTreeItem(id));
+        props.onItemActivation(id);
     },
-    toggleItemOpen: (nodeId) => {
-        dispatch<any>(toggleSidePanelTreeItemCollapse(nodeId));
-    }
+    toggleItemOpen: (_, { id }) => {
+        dispatch<any>(toggleSidePanelTreeItemCollapse(id));
+    },
+    toggleItemSelection: noop,
 });
 
 export const SidePanelTree = connect(undefined, mapDispatchToProps)(
index 8b7630ab8cc62609ca9a3898653f1956bd821553..2e1d98ea8725e838aeb0f65b39bac450532d826c 100644 (file)
@@ -5,23 +5,25 @@
 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<T> = (event: React.MouseEvent<HTMLElement>, item: TreeItem<T>, pickerId: string) => void;
+export interface TreePickerProps<T> {
     pickerId: string;
-    onContextMenu: (event: React.MouseEvent<HTMLElement>, nodeId: string, pickerId: string) => void;
-    toggleItemOpen: (nodeId: string, status: TreeItemStatus, pickerId: string) => void;
-    toggleItemActive: (nodeId: string, status: TreeItemStatus, pickerId: string) => void;
+    onContextMenu: Callback<T>;
+    toggleItemOpen: Callback<T>;
+    toggleItemActive: Callback<T>;
+    toggleItemSelection: Callback<T>;
 }
 
 const memoizedMapStateToProps = () => {
-    let prevTree: Ttree<TreePickerNode>;
+    let prevTree: Ttree<any>;
     let mappedProps: Pick<TreeProps<any>, 'items'>;
-    return (state: RootState, props: TreePickerProps): Pick<TreeProps<any>, 'items'> => {
+    return <T>(state: RootState, props: TreePickerProps<T>): Pick<TreeProps<T>, 'items'> => {
         const tree = state.treePicker[props.pickerId] || createTree();
-        if(tree !== prevTree){
+        if (tree !== prevTree) {
             prevTree = tree;
             mappedProps = {
                 items: getNodeChildrenIds('')(tree)
@@ -32,26 +34,39 @@ const memoizedMapStateToProps = () => {
     };
 };
 
-const mapDispatchToProps = (dispatch: Dispatch, props: TreePickerProps): Pick<TreeProps<any>, '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<any>): Pick<TreeProps<any>, '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<TreePickerNode>) =>
+const treePickerToTreeItems = (tree: Ttree<any>) =>
     (id: string): TreeItem<any> => {
-        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;
+    }
+};
+
index 553afa4d09c3751ac5a4a8da871c0fb3eae149fd..998e40045d25aab3500dc86812e23015acd08762 100644 (file)
@@ -41,7 +41,12 @@ 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 { TreePicker } from '../../views-components/tree-picker/tree-picker';
+import { noop } from 'lodash';
+import { TreeItem } from '~/components/tree/tree';
+import { GroupContentsResource } from '~/services/groups-service/groups-service';
+import { ProjectsTreePicker } from '~/views-components/projects-tree-picker/projects-tree-picker';
+import { UserProjectsTreePicker } from '~/views-components/projects-tree-picker/user-projects-tree-picker';
 
 type CssRules = 'root' | 'container' | 'splitter' | 'asidePanel' | 'contentWrapper' | 'content';
 
@@ -75,8 +80,8 @@ const styles: StyleRulesCallback<CssRules> = (theme: ArvadosTheme) => ({
 
 type WorkbenchPanelProps = WithStyles<CssRules>;
 
-export const WorkbenchPanel = 
-    withStyles(styles)(({ classes }: WorkbenchPanelProps) => 
+export const WorkbenchPanel =
+    withStyles(styles)(({ classes }: WorkbenchPanelProps) =>
         <Grid container item xs className={classes.root}>
             <Grid container item xs className={classes.container}>
                 <SplitterLayout customClassName={classes.splitter} percentage={true}
@@ -89,6 +94,9 @@ export const WorkbenchPanel =
                             <MainContentBar />
                         </Grid>
                         <Grid item xs className={classes.content}>
+                            <UserProjectsTreePicker pickerId='testPicker1'/>
+                            <UserProjectsTreePicker pickerId='testPicker2' includeCollections/>
+                            <UserProjectsTreePicker pickerId='testPicker3' includeCollections includeFiles/>
                             <Switch>
                                 <Route path={Routes.PROJECTS} component={ProjectPanel} />
                                 <Route path={Routes.COLLECTIONS} component={CollectionPanel} />
@@ -114,8 +122,6 @@ export const WorkbenchPanel =
             <CreateProjectDialog />
             <CurrentTokenDialog />
             <FileRemoveDialog />
-            <FileRemoveDialog />
-            <FileSelectionDialog />
             <FilesUploadCollectionDialog />
             <MoveCollectionDialog />
             <MoveProcessDialog />