[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