[ARVADOS-WORKBENCH2] updated: 1.1.4-470-g772cf27

Git user git at public.curoverse.com
Wed Aug 1 11:25:19 EDT 2018


Summary of changes:
 .../collection-file.test.ts}                       |  14 +--
 .../collection-file.ts}                            |  74 +++++++++------
 src/models/tree.test.ts                            | 100 +++++++++++++++++++++
 src/models/tree.ts                                 |  95 ++++++++++++++++++++
 .../collection-panel/collection-panel-action.ts    |   8 +-
 .../collection-panel-files-actions.ts              |   3 +-
 .../collection-panel-files-state.ts                |  92 +++----------------
 .../collections-panel-files-reducer.ts             |  92 +++++++++++--------
 .../collection-panel-files.ts                      |  64 +++++++------
 9 files changed, 356 insertions(+), 186 deletions(-)
 rename src/{store/collection-panel/collection-panel-files/collection-panel-files-state.test.ts => models/collection-file.test.ts} (81%)
 copy src/{store/collection-panel/collection-panel-files/collection-panel-files-state.ts => models/collection-file.ts} (51%)
 create mode 100644 src/models/tree.test.ts
 create mode 100644 src/models/tree.ts

       via  772cf27f6c086cc59a71211ca9df74c33414a859 (commit)
      from  be2f555375b863b5171cebe6375acb4c0267e818 (commit)

Those revisions listed above that are new to this repository have
not appeared on any other notification email; so we list those
revisions in full, below.


commit 772cf27f6c086cc59a71211ca9df74c33414a859
Author: Michal Klobukowski <michal.klobukowski at contractors.roche.com>
Date:   Wed Aug 1 17:24:59 2018 +0200

    Extract tree data structure
    
    Feature #13855
    
    Arvados-DCO-1.1-Signed-off-by: Michal Klobukowski <michal.klobukowski at contractors.roche.com>

diff --git a/src/store/collection-panel/collection-panel-files/collection-panel-files-state.test.ts b/src/models/collection-file.test.ts
similarity index 81%
rename from src/store/collection-panel/collection-panel-files/collection-panel-files-state.test.ts
rename to src/models/collection-file.test.ts
index 3550bc5..7feb13d 100644
--- a/src/store/collection-panel/collection-panel-files/collection-panel-files-state.test.ts
+++ b/src/models/collection-file.test.ts
@@ -2,8 +2,8 @@
 //
 // SPDX-License-Identifier: AGPL-3.0
 
-import { parseKeepManifestText } from "../../../models/keep-manifest";
-import { mapManifestToFiles, mapManifestToDirectories } from './collection-panel-files-state';
+import { parseKeepManifestText } from "./keep-manifest";
+import { mapManifestToFiles, mapManifestToDirectories } from './collection-file';
 
 test('mapManifestToFiles', () => {
     const manifestText = `. 930625b054ce894ac40596c3f5a0d947+33 0:0:a 0:0:b 0:33:output.txt\n./c d41d8cd98f00b204e9800998ecf8427e+0 0:0:d`;
@@ -14,28 +14,24 @@ test('mapManifestToFiles', () => {
         id: '/a',
         name: 'a',
         size: 0,
-        selected: false,
         type: 'file'
     }, {
         parentId: '',
         id: '/b',
         name: 'b',
         size: 0,
-        selected: false,
         type: 'file'
     }, {
         parentId: '',
         id: '/output.txt',
         name: 'output.txt',
         size: 33,
-        selected: false,
         type: 'file'
     }, {
         parentId: '/c',
         id: '/c/d',
         name: 'd',
         size: 0,
-        selected: false,
         type: 'file'
     },]);
 });
@@ -48,22 +44,16 @@ test('mapManifestToDirectories', () => {
         parentId: "",
         id: '/c',
         name: 'c',
-        collapsed: true,
-        selected: false,
         type: 'directory'
     }, {
         parentId: '/c',
         id: '/c/user',
         name: 'user',
-        collapsed: true,
-        selected: false,
         type: 'directory'
     }, {
         parentId: '/c/user',
         id: '/c/user/results',
         name: 'results',
-        collapsed: true,
-        selected: false,
         type: 'directory'
     },]);
 });
\ No newline at end of file
diff --git a/src/store/collection-panel/collection-panel-files/collection-panel-files-state.ts b/src/models/collection-file.ts
similarity index 51%
copy from src/store/collection-panel/collection-panel-files/collection-panel-files-state.ts
copy to src/models/collection-file.ts
index 6af2550..3a7e55c 100644
--- a/src/store/collection-panel/collection-panel-files/collection-panel-files-state.ts
+++ b/src/models/collection-file.ts
@@ -3,36 +3,50 @@
 // SPDX-License-Identifier: AGPL-3.0
 
 import { uniqBy } from 'lodash';
-import { KeepManifestStream, KeepManifestStreamFile, KeepManifest } from "../../../models/keep-manifest";
+import { KeepManifestStream, KeepManifestStreamFile, KeepManifest } from "./keep-manifest";
+import { Tree, TreeNode, setNode, createTree } from './tree';
 
-export type CollectionPanelFilesState = Array<CollectionPanelItem>;
+export type CollectionFilesTree = Tree<CollectionDirectory | CollectionFile>;
 
-export type CollectionPanelItem = CollectionPanelDirectory | CollectionPanelFile;
+export enum CollectionFileType {
+    DIRECTORY = 'directory',
+    FILE = 'file'
+}
 
-export interface CollectionPanelDirectory {
-    parentId?: string;
+export interface CollectionDirectory {
+    parentId: string;
     id: string;
     name: string;
-    collapsed: boolean;
-    selected: boolean;
-    type: 'directory';
+    type: CollectionFileType.DIRECTORY;
 }
 
-export interface CollectionPanelFile {
-    parentId?: string;
+export interface CollectionFile {
+    parentId: string;
     id: string;
     name: string;
-    selected: boolean;
     size: number;
-    type: 'file';
+    type: CollectionFileType.FILE;
 }
 
-export const mapManifestToItems = (manifest: KeepManifest): CollectionPanelItem[] => ([
+export const mapManifestToCollectionFilesTree = (manifest: KeepManifest): CollectionFilesTree =>
+    manifestToCollectionFiles(manifest)
+        .map(mapCollectionFileToTreeNode)
+        .reduce((tree, node) => setNode(node)(tree), createTree<CollectionFile>());
+
+
+export const mapCollectionFileToTreeNode = (file: CollectionFile): TreeNode<CollectionFile> => ({
+    children: [],
+    id: file.id,
+    parent: file.parentId,
+    value: file
+});
+
+export const manifestToCollectionFiles = (manifest: KeepManifest): Array<CollectionDirectory | CollectionFile> => ([
     ...mapManifestToDirectories(manifest),
     ...mapManifestToFiles(manifest)
 ]);
 
-export const mapManifestToDirectories = (manifest: KeepManifest): CollectionPanelDirectory[] =>
+export const mapManifestToDirectories = (manifest: KeepManifest): CollectionDirectory[] =>
     uniqBy(
         manifest
             .map(mapStreamDirectory)
@@ -40,19 +54,19 @@ export const mapManifestToDirectories = (manifest: KeepManifest): CollectionPane
             .reduce((all, splitted) => ([...all, ...splitted]), []),
         directory => directory.id);
 
-export const mapManifestToFiles = (manifest: KeepManifest): CollectionPanelFile[] =>
+export const mapManifestToFiles = (manifest: KeepManifest): CollectionFile[] =>
     manifest
         .map(stream => stream.files.map(mapStreamFile(stream)))
         .reduce((all, current) => ([...all, ...current]), []);
 
-const splitDirectory = (directory: CollectionPanelDirectory): CollectionPanelDirectory[] => {
+const splitDirectory = (directory: CollectionDirectory): CollectionDirectory[] => {
     return directory.name
         .split('/')
         .slice(1)
         .map(mapPathComponentToDirectory);
 };
 
-const mapPathComponentToDirectory = (component: string, index: number, components: string[]): CollectionPanelDirectory =>
+const mapPathComponentToDirectory = (component: string, index: number, components: string[]): CollectionDirectory =>
     createDirectory({
         parentId: index === 0 ? '' : joinPathComponents(components, index),
         id: joinPathComponents(components, index + 1),
@@ -62,7 +76,7 @@ const mapPathComponentToDirectory = (component: string, index: number, component
 const joinPathComponents = (components: string[], index: number) =>
     `/${components.slice(0, index).join('/')}`;
 
-const mapStreamDirectory = (stream: KeepManifestStream): CollectionPanelDirectory =>
+const mapStreamDirectory = (stream: KeepManifestStream): CollectionDirectory =>
     createDirectory({
         parentId: '',
         id: stream.name,
@@ -70,7 +84,7 @@ const mapStreamDirectory = (stream: KeepManifestStream): CollectionPanelDirector
     });
 
 const mapStreamFile = (stream: KeepManifestStream) =>
-    (file: KeepManifestStreamFile): CollectionPanelFile =>
+    (file: KeepManifestStreamFile): CollectionFile =>
         createFile({
             parentId: stream.name,
             id: `${stream.name}/${file.name}`,
@@ -78,15 +92,19 @@ const mapStreamFile = (stream: KeepManifestStream) =>
             size: file.size,
         });
 
-const createDirectory = (data: { parentId: string, id: string, name: string }): CollectionPanelDirectory => ({
-    ...data,
-    collapsed: true,
-    selected: false,
-    type: 'directory'
+export const createDirectory = (data: Partial<CollectionDirectory>): CollectionDirectory => ({
+    id: '',
+    name: '',
+    parentId: '',
+    type: CollectionFileType.DIRECTORY,
+    ...data
 });
 
-const createFile = (data: { parentId: string, id: string, name: string, size: number }): CollectionPanelFile => ({
-    ...data,
-    selected: false,
-    type: 'file'
+export const createFile = (data: Partial<CollectionFile>): CollectionFile => ({
+    id: '',
+    name: '',
+    parentId: '',
+    size: 0,
+    type: CollectionFileType.FILE,
+    ...data
 });
\ No newline at end of file
diff --git a/src/models/tree.test.ts b/src/models/tree.test.ts
new file mode 100644
index 0000000..1ddb083
--- /dev/null
+++ b/src/models/tree.test.ts
@@ -0,0 +1,100 @@
+// Copyright (C) The Arvados Authors. All rights reserved.
+//
+// SPDX-License-Identifier: AGPL-3.0
+
+import * as Tree from './tree';
+
+describe('Tree', () => {
+    let tree: Tree.Tree<string>;
+
+    beforeEach(() => {
+        tree = Tree.createTree();
+    });
+
+    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' });
+    });
+
+    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' }));
+
+        expect(Tree.getNode('Node 1')(newTree)).toEqual({ children: ['Node 2'], id: 'Node 1', parent: '', value: 'Value 1' });
+    });
+
+    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' }
+        ].reduce((tree, node) => Tree.setNode(node)(tree), tree);
+        expect(Tree.getNodeAncestors('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' }
+        ].reduce((tree, node) => Tree.setNode(node)(tree), tree);
+        expect(Tree.getNodeDescendants('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' }
+        ].reduce((tree, node) => Tree.setNode(node)(tree), tree);
+        expect(Tree.getNodeDescendants('')(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' }
+        ].reduce((tree, node) => Tree.setNode(node)(tree), tree);
+        expect(Tree.getNodeChildren('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' }
+        ].reduce((tree, node) => Tree.setNode(node)(tree), tree);
+        expect(Tree.getNodeChildren('')(newTree)).toEqual(['Node 1', 'Node 3']);
+    });
+    
+    it('maps nodes', () => {
+        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' }
+        ].reduce((tree, node) => Tree.setNode(node)(tree), tree);
+        const updatedTree = Tree.mapNodes(['Node 2.1', 'Node 3.1'])(node => ({...node, value: `Updated ${node.value}`}))(newTree);
+        expect(Tree.getNode('Node 2.1')(updatedTree)).toEqual({ children: [], id: 'Node 2.1', parent: 'Node 2', value: 'Updated Value 1' },);
+    });
+    
+    it('maps tree', () => {
+        const newTree = [
+            { children: [], id: 'Node 1', parent: '', value: 'Value 1' },
+            { children: [], id: 'Node 2', parent: 'Node 1', value: 'Value 2' },
+        ].reduce((tree, node) => Tree.setNode(node)(tree), tree);
+        const mappedTree = Tree.mapTree<string, number>(node => ({...node, value: parseInt(node.value.split(' ')[1], 10)}))(newTree);
+        expect(Tree.getNode('Node 2')(mappedTree)).toEqual({ children: [], id: 'Node 2', parent: 'Node 1', value: 2 },);
+    });
+});
\ No newline at end of file
diff --git a/src/models/tree.ts b/src/models/tree.ts
new file mode 100644
index 0000000..1f1e308
--- /dev/null
+++ b/src/models/tree.ts
@@ -0,0 +1,95 @@
+import { Children } from "../../node_modules/@types/react";
+
+// Copyright (C) The Arvados Authors. All rights reserved.
+//
+// SPDX-License-Identifier: AGPL-3.0
+
+export type Tree<T> = Record<string, TreeNode<T>>;
+
+export const TREE_ROOT_ID = '';
+
+export interface TreeNode<T> {
+    children: string[];
+    value: T;
+    id: string;
+    parent: string;
+}
+
+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
+            ? tree
+            : Object.assign({}, tree, { [node.id]: node }))
+        .map(addChild(node.parent, node.id));
+    return newTree;
+};
+
+export const addChild = (parentId: string, childId: string) => <T>(tree: Tree<T>): Tree<T> => {
+    const node = getNode(parentId)(tree);
+    if (node) {
+        const children = node.children.some(id => id === childId)
+            ? node.children
+            : [...node.children, childId];
+
+        const newNode = children === node.children
+            ? node
+            : { ...node, children };
+
+        return setNode(newNode)(tree);
+    }
+    return tree;
+};
+
+
+export const getNodeAncestors = (id: string) => <T>(tree: Tree<T>): string[] => {
+    const node = getNode(id)(tree);
+    return node && node.parent
+        ? [...getNodeAncestors(node.parent)(tree), node.parent]
+        : [];
+};
+
+export const getNodeDescendants = (id: string, limit = Infinity) => <T>(tree: Tree<T>): string[] => {
+    const node = getNode(id)(tree);
+    const children = node ? node.children :
+        id === TREE_ROOT_ID
+            ? getRootNodeChildren(tree)
+            : [];
+
+    return children
+        .concat(limit < 1
+            ? []
+            : children
+                .map(id => getNodeDescendants(id, limit - 1)(tree))
+                .reduce((nodes, nodeChildren) => [...nodes, ...nodeChildren], []));
+};
+
+export const getNodeChildren = (id: string) => <T>(tree: Tree<T>): string[] =>
+    getNodeDescendants(id, 0)(tree);
+
+export const mapNodes = (ids: string[]) => <T>(mapFn: (node: TreeNode<T>) => TreeNode<T>) => (tree: Tree<T>): Tree<T> =>
+    ids
+        .map(id => getNode(id)(tree))
+        .map(mapFn)
+        .map(setNode)
+        .reduce((tree, update) => update(tree), tree);
+
+export const mapTree = <T, R>(mapFn: (node: TreeNode<T>) => TreeNode<R>) => (tree: Tree<T>): Tree<R> =>
+    getNodeDescendants('')(tree)
+        .map(id => getNode(id)(tree))
+        .map(mapFn)
+        .reduce((newTree, node) => setNode(node)(newTree), createTree<R>());
+
+export const mapNodeValue = <T>(mapFn: (value: T) => T) => (node: TreeNode<T>) =>
+    ({ ...node, value: mapFn(node.value) });
+
+const getRootNodeChildren = <T>(tree: Tree<T>) =>
+    Object
+        .keys(tree)
+        .filter(id => getNode(id)(tree)!.parent === TREE_ROOT_ID);
+
+
+
diff --git a/src/store/collection-panel/collection-panel-action.ts b/src/store/collection-panel/collection-panel-action.ts
index adc6647..e526c6a 100644
--- a/src/store/collection-panel/collection-panel-action.ts
+++ b/src/store/collection-panel/collection-panel-action.ts
@@ -9,6 +9,8 @@ import { CollectionResource } from "../../models/collection";
 import { collectionService } from "../../services/services";
 import { collectionPanelFilesAction } from "./collection-panel-files/collection-panel-files-actions";
 import { parseKeepManifestText } from "../../models/keep-manifest";
+import { mapManifestToCollectionFilesTree } from "../../models/collection-file";
+import { getNodeChildren, createTree } from "../../models/tree";
 
 export const collectionPanelActions = unionize({
     LOAD_COLLECTION: ofType<{ uuid: string, kind: ResourceKind }>(),
@@ -20,13 +22,13 @@ export type CollectionPanelAction = UnionOf<typeof collectionPanelActions>;
 export const loadCollection = (uuid: string, kind: ResourceKind) =>
     (dispatch: Dispatch) => {
         dispatch(collectionPanelActions.LOAD_COLLECTION({ uuid, kind }));
-        dispatch(collectionPanelFilesAction.SET_COLLECTION_FILES({ manifest: [] }));
+        dispatch(collectionPanelFilesAction.SET_COLLECTION_FILES({ files: createTree() }));
         return collectionService
             .get(uuid)
             .then(item => {
                 dispatch(collectionPanelActions.LOAD_COLLECTION_SUCCESS({ item }));
-                const manifest = parseKeepManifestText(item.manifestText);
-                dispatch(collectionPanelFilesAction.SET_COLLECTION_FILES({ manifest }));
+                const files = mapManifestToCollectionFilesTree(parseKeepManifestText(item.manifestText));
+                dispatch(collectionPanelFilesAction.SET_COLLECTION_FILES({ files }));
             });
     };
 
diff --git a/src/store/collection-panel/collection-panel-files/collection-panel-files-actions.ts b/src/store/collection-panel/collection-panel-files/collection-panel-files-actions.ts
index 90fcf3e..7423c49 100644
--- a/src/store/collection-panel/collection-panel-files/collection-panel-files-actions.ts
+++ b/src/store/collection-panel/collection-panel-files/collection-panel-files-actions.ts
@@ -4,9 +4,10 @@
 
 import { default as unionize, ofType, UnionOf } from "unionize";
 import { KeepManifest } from "../../../models/keep-manifest";
+import { CollectionFilesTree } from "../../../models/collection-file";
 
 export const collectionPanelFilesAction = unionize({
-    SET_COLLECTION_FILES: ofType<{ manifest: KeepManifest }>(),
+    SET_COLLECTION_FILES: ofType<{ files: CollectionFilesTree }>(),
     TOGGLE_COLLECTION_FILE_COLLAPSE: ofType<{ id: string }>(),
     TOGGLE_COLLECTION_FILE_SELECTION: ofType<{ id: string }>(),
     SELECT_ALL_COLLECTION_FILES: ofType<{}>(),
diff --git a/src/store/collection-panel/collection-panel-files/collection-panel-files-state.ts b/src/store/collection-panel/collection-panel-files/collection-panel-files-state.ts
index 6af2550..d6f2fa4 100644
--- a/src/store/collection-panel/collection-panel-files/collection-panel-files-state.ts
+++ b/src/store/collection-panel/collection-panel-files/collection-panel-files-state.ts
@@ -2,91 +2,25 @@
 //
 // SPDX-License-Identifier: AGPL-3.0
 
-import { uniqBy } from 'lodash';
-import { KeepManifestStream, KeepManifestStreamFile, KeepManifest } from "../../../models/keep-manifest";
+import { CollectionFile, CollectionDirectory, CollectionFileType } from '../../../models/collection-file';
+import { Tree, TreeNode } from '../../../models/tree';
 
-export type CollectionPanelFilesState = Array<CollectionPanelItem>;
+export type CollectionPanelFilesState = Tree<CollectionPanelDirectory | CollectionPanelFile>;
 
-export type CollectionPanelItem = CollectionPanelDirectory | CollectionPanelFile;
-
-export interface CollectionPanelDirectory {
-    parentId?: string;
-    id: string;
-    name: string;
+export interface CollectionPanelDirectory extends CollectionDirectory {
     collapsed: boolean;
     selected: boolean;
-    type: 'directory';
 }
 
-export interface CollectionPanelFile {
-    parentId?: string;
-    id: string;
-    name: string;
+export interface CollectionPanelFile extends CollectionFile {
     selected: boolean;
-    size: number;
-    type: 'file';
 }
 
-export const mapManifestToItems = (manifest: KeepManifest): CollectionPanelItem[] => ([
-    ...mapManifestToDirectories(manifest),
-    ...mapManifestToFiles(manifest)
-]);
-
-export const mapManifestToDirectories = (manifest: KeepManifest): CollectionPanelDirectory[] =>
-    uniqBy(
-        manifest
-            .map(mapStreamDirectory)
-            .map(splitDirectory)
-            .reduce((all, splitted) => ([...all, ...splitted]), []),
-        directory => directory.id);
-
-export const mapManifestToFiles = (manifest: KeepManifest): CollectionPanelFile[] =>
-    manifest
-        .map(stream => stream.files.map(mapStreamFile(stream)))
-        .reduce((all, current) => ([...all, ...current]), []);
-
-const splitDirectory = (directory: CollectionPanelDirectory): CollectionPanelDirectory[] => {
-    return directory.name
-        .split('/')
-        .slice(1)
-        .map(mapPathComponentToDirectory);
-};
-
-const mapPathComponentToDirectory = (component: string, index: number, components: string[]): CollectionPanelDirectory =>
-    createDirectory({
-        parentId: index === 0 ? '' : joinPathComponents(components, index),
-        id: joinPathComponents(components, index + 1),
-        name: component,
-    });
-
-const joinPathComponents = (components: string[], index: number) =>
-    `/${components.slice(0, index).join('/')}`;
-
-const mapStreamDirectory = (stream: KeepManifestStream): CollectionPanelDirectory =>
-    createDirectory({
-        parentId: '',
-        id: stream.name,
-        name: stream.name,
-    });
-
-const mapStreamFile = (stream: KeepManifestStream) =>
-    (file: KeepManifestStreamFile): CollectionPanelFile =>
-        createFile({
-            parentId: stream.name,
-            id: `${stream.name}/${file.name}`,
-            name: file.name,
-            size: file.size,
-        });
-
-const createDirectory = (data: { parentId: string, id: string, name: string }): CollectionPanelDirectory => ({
-    ...data,
-    collapsed: true,
-    selected: false,
-    type: 'directory'
-});
-
-const createFile = (data: { parentId: string, id: string, name: string, size: number }): CollectionPanelFile => ({
-    ...data,
-    selected: false,
-    type: 'file'
-});
\ No newline at end of file
+export const mapCollectionFileToCollectionPanelFile = (node: TreeNode<CollectionDirectory | CollectionFile>): TreeNode<CollectionPanelDirectory | CollectionPanelFile> => {
+    return {
+        ...node,
+        value: node.value.type === CollectionFileType.DIRECTORY
+            ? { ...node.value, selected: false, collapsed: true }
+            : { ...node.value, selected: false }
+    };
+};
\ No newline at end of file
diff --git a/src/store/collection-panel/collection-panel-files/collections-panel-files-reducer.ts b/src/store/collection-panel/collection-panel-files/collections-panel-files-reducer.ts
index d4f3bad..c31f6c6 100644
--- a/src/store/collection-panel/collection-panel-files/collections-panel-files-reducer.ts
+++ b/src/store/collection-panel/collection-panel-files/collections-panel-files-reducer.ts
@@ -2,56 +2,74 @@
 //
 // SPDX-License-Identifier: AGPL-3.0
 
-import { CollectionPanelFilesState, CollectionPanelFile, CollectionPanelDirectory, CollectionPanelItem, mapManifestToItems } from "./collection-panel-files-state";
+import { CollectionPanelFilesState, CollectionPanelFile, CollectionPanelDirectory, mapCollectionFileToCollectionPanelFile } from "./collection-panel-files-state";
 import { CollectionPanelFilesAction, collectionPanelFilesAction } from "./collection-panel-files-actions";
+import { createTree, mapTree, TreeNode, mapNodes, getNode, setNode, getNodeAncestors, getNodeDescendants, mapNodeValue } from "../../../models/tree";
+import { CollectionFileType } from "../../../models/collection-file";
 
-export const collectionPanelFilesReducer = (state: CollectionPanelFilesState = [], action: CollectionPanelFilesAction) => {
+export const collectionPanelFilesReducer = (state: CollectionPanelFilesState = createTree(), action: CollectionPanelFilesAction) => {
     return collectionPanelFilesAction.match(action, {
-        SET_COLLECTION_FILES: ({manifest}) => mapManifestToItems(manifest),
-        TOGGLE_COLLECTION_FILE_COLLAPSE: data => toggleCollapsed(state, data.id),
-        TOGGLE_COLLECTION_FILE_SELECTION: data => toggleSelected(state, data.id),
-        SELECT_ALL_COLLECTION_FILES: () => state.map(file => ({ ...file, selected: true })),
-        UNSELECT_ALL_COLLECTION_FILES: () => state.map(file => ({ ...file, selected: false })),
+        SET_COLLECTION_FILES: ({ files }) =>
+            mapTree(mapCollectionFileToCollectionPanelFile)(files),
+
+        TOGGLE_COLLECTION_FILE_COLLAPSE: data =>
+            toggleCollapse(data.id)(state),
+
+        TOGGLE_COLLECTION_FILE_SELECTION: data => [state]
+            .map(toggleSelected(data.id))
+            .map(toggleAncestors(data.id))
+            .map(toggleDescendants(data.id))[0],
+
+        SELECT_ALL_COLLECTION_FILES: () =>
+            mapTree(mapNodeValue(v => ({ ...v, selected: true })))(state),
+
+        UNSELECT_ALL_COLLECTION_FILES: () =>
+            mapTree(mapNodeValue(v => ({ ...v, selected: false })))(state),
+            
         default: () => state
     });
 };
 
-const toggleCollapsed = (state: CollectionPanelFilesState, id: string) =>
-    state.map((item: CollectionPanelItem) =>
-        item.type === 'directory' && item.id === id
-            ? { ...item, collapsed: !item.collapsed }
-            : item);
+const toggleCollapse = (id: string) => (tree: CollectionPanelFilesState) =>
+    mapNodes
+        ([id])
+        (mapNodeValue((v: CollectionPanelDirectory | CollectionPanelFile) =>
+            v.type === CollectionFileType.DIRECTORY
+                ? { ...v, collapsed: !v.collapsed }
+                : v))
+        (tree);
 
-const toggleSelected = (state: CollectionPanelFilesState, id: string) =>
-    toggleAncestors(toggleDescendants(state, id), id);
+const toggleSelected = (id: string) => (tree: CollectionPanelFilesState) =>
+    mapNodes
+        ([id])
+        (mapNodeValue((v: CollectionPanelDirectory | CollectionPanelFile) => ({ ...v, selected: !v.selected })))
+        (tree);
 
-const toggleDescendants = (state: CollectionPanelFilesState, id: string) => {
-    const ids = getDescendants(state)({ id }).map(file => file.id);
-    if (ids.length > 0) {
-        const selected = !state.find(f => f.id === ids[0])!.selected;
-        return state.map(file => ids.some(id => file.id === id) ? { ...file, selected } : file);
+
+const toggleDescendants = (id: string) => (tree: CollectionPanelFilesState) => {
+    const node = getNode(id)(tree);
+    if (node && node.value.type === CollectionFileType.DIRECTORY) {
+        return mapNodes(getNodeDescendants(id)(tree))(mapNodeValue(v => ({ ...v, selected: node.value.selected })))(tree);
     }
-    return state;
+    return tree;
 };
 
-const toggleAncestors = (state: CollectionPanelFilesState, id: string): CollectionPanelItem[] => {
-    const file = state.find(f => f.id === id);
-    if (file) {
-        const selected = state
-            .filter(f => f.parentId === file.parentId)
-            .every(f => f.selected);
-        if (!selected) {
-            const newState = state.map(f => f.id === file.parentId ? { ...f, selected } : f);
-            return toggleAncestors(newState, file.parentId || "");
-        }
-    }
-    return state;
+const toggleAncestors = (id: string) => (tree: CollectionPanelFilesState) => {
+    const ancestors = getNodeAncestors(id)(tree)
+        .map(id => getNode(id)(tree))
+        .reverse();
+    return ancestors.reduce((newTree, parent) => parent !== undefined ? toggleParentNode(parent)(newTree) : newTree, tree);
 };
 
-const getDescendants = (state: CollectionPanelFilesState) => ({ id }: { id: string }): CollectionPanelItem[] => {
-    const root = state.find(item => item.id === id);
-    if (root) {
-        return [root].concat(...state.filter(item => item.parentId === id).map(getDescendants(state)));
-    } else { return []; }
+const toggleParentNode = (node: TreeNode<CollectionPanelDirectory | CollectionPanelFile>) => (tree: CollectionPanelFilesState) => {
+    const parentNode = getNode(node.id)(tree);
+    if (parentNode) {
+        const selected = parentNode.children
+            .map(id => getNode(id)(tree))
+            .every(node => node !== undefined && node.value.selected);
+        return setNode(mapNodeValue(v => ({ ...v, selected }))(parentNode))(tree);
+    }
+    return setNode(node)(tree);
 };
 
+
diff --git a/src/views-components/collection-panel-files/collection-panel-files.ts b/src/views-components/collection-panel-files/collection-panel-files.ts
index 09fdd67..bf98834 100644
--- a/src/views-components/collection-panel-files/collection-panel-files.ts
+++ b/src/views-components/collection-panel-files/collection-panel-files.ts
@@ -6,26 +6,27 @@ import { connect } from "react-redux";
 import { CollectionPanelFiles as Component, CollectionPanelFilesProps } from "../../components/collection-panel-files/collection-panel-files";
 import { RootState } from "../../store/store";
 import { TreeItemStatus, TreeItem } from "../../components/tree/tree";
-import { CollectionPanelItem, CollectionPanelFilesState } from "../../store/collection-panel/collection-panel-files/collection-panel-files-state";
+import { CollectionPanelFilesState, CollectionPanelDirectory, CollectionPanelFile } from "../../store/collection-panel/collection-panel-files/collection-panel-files-state";
 import { FileTreeData } from "../../components/file-tree/file-tree-data";
 import { Dispatch } from "redux";
 import { collectionPanelFilesAction } from "../../store/collection-panel/collection-panel-files/collection-panel-files-actions";
 import { contextMenuActions } from "../../store/context-menu/context-menu-actions";
 import { ContextMenuKind } from "../context-menu/context-menu";
+import { Tree, getNodeChildren, getNode } from "../../models/tree";
+import { CollectionFileType } from "../../models/collection-file";
 
-const mapStateToProps = () => {
-    let lastState: CollectionPanelFilesState;
-    let lastTree: Array<TreeItem<FileTreeData>>;
+const memoizedMapStateToProps = () => {
+    let prevState: CollectionPanelFilesState;
+    let prevTree: Array<TreeItem<FileTreeData>>;
 
     return (state: RootState): Pick<CollectionPanelFilesProps, "items"> => {
-        if (lastState !== state.collectionPanelFiles) {
-            lastState = state.collectionPanelFiles;
-            lastTree = state.collectionPanelFiles
-                .filter(item => item.parentId === '')
+        if (prevState !== state.collectionPanelFiles) {
+            prevState = state.collectionPanelFiles;
+            prevTree = getNodeChildren('')(state.collectionPanelFiles)
                 .map(collectionItemToTreeItem(state.collectionPanelFiles));
         }
         return {
-            items: lastTree
+            items: prevTree
         };
     };
 };
@@ -52,22 +53,33 @@ const mapDispatchToProps = (dispatch: Dispatch): Pick<CollectionPanelFilesProps,
 });
 
 
-export const CollectionPanelFiles = connect(mapStateToProps(), mapDispatchToProps)(Component);
+export const CollectionPanelFiles = connect(memoizedMapStateToProps(), mapDispatchToProps)(Component);
 
-const collectionItemToTreeItem = (items: CollectionPanelItem[]) => (item: CollectionPanelItem): TreeItem<FileTreeData> => {
-    return {
-        active: false,
-        data: {
-            name: item.name,
-            size: item.type === 'file' ? item.size : undefined,
-            type: item.type
-        },
-        id: item.id,
-        items: items
-            .filter(i => i.parentId === item.id)
-            .map(collectionItemToTreeItem(items)),
-        open: item.type === 'directory' ? !item.collapsed : false,
-        selected: item.selected,
-        status: TreeItemStatus.LOADED
+const collectionItemToTreeItem = (tree: Tree<CollectionPanelDirectory | CollectionPanelFile>) =>
+    (id: string): TreeItem<FileTreeData> => {
+        const node = getNode(id)(tree) || {
+            id: '',
+            children: [],
+            parent: '',
+            value: {
+                name: 'Invalid node',
+                type: CollectionFileType.DIRECTORY,
+                selected: false,
+                collapsed: true
+            }
+        };
+        return {
+            active: false,
+            data: {
+                name: node.value.name,
+                size: node.value.type === CollectionFileType.FILE ? node.value.size : undefined,
+                type: node.value.type
+            },
+            id: node.id,
+            items: getNodeChildren(node.id)(tree)
+                .map(collectionItemToTreeItem(tree)),
+            open: node.value.type === CollectionFileType.DIRECTORY ? !node.value.collapsed : false,
+            selected: node.value.selected,
+            status: TreeItemStatus.LOADED
+        };
     };
-};

-----------------------------------------------------------------------


hooks/post-receive
-- 




More information about the arvados-commits mailing list