//
// 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`;
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'
},]);
});
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
--- /dev/null
+// Copyright (C) The Arvados Authors. All rights reserved.
+//
+// SPDX-License-Identifier: AGPL-3.0
+
+import { uniqBy } from 'lodash';
+import { KeepManifestStream, KeepManifestStreamFile, KeepManifest } from "./keep-manifest";
+import { Tree, TreeNode, setNode, createTree } from './tree';
+
+export type CollectionFilesTree = Tree<CollectionDirectory | CollectionFile>;
+
+export enum CollectionFileType {
+ DIRECTORY = 'directory',
+ FILE = 'file'
+}
+
+export interface CollectionDirectory {
+ parentId: string;
+ id: string;
+ name: string;
+ type: CollectionFileType.DIRECTORY;
+}
+
+export interface CollectionFile {
+ parentId: string;
+ id: string;
+ name: string;
+ size: number;
+ type: CollectionFileType.FILE;
+}
+
+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): CollectionDirectory[] =>
+ uniqBy(
+ manifest
+ .map(mapStreamDirectory)
+ .map(splitDirectory)
+ .reduce((all, splitted) => ([...all, ...splitted]), []),
+ directory => directory.id);
+
+export const mapManifestToFiles = (manifest: KeepManifest): CollectionFile[] =>
+ manifest
+ .map(stream => stream.files.map(mapStreamFile(stream)))
+ .reduce((all, current) => ([...all, ...current]), []);
+
+const splitDirectory = (directory: CollectionDirectory): CollectionDirectory[] => {
+ return directory.name
+ .split('/')
+ .slice(1)
+ .map(mapPathComponentToDirectory);
+};
+
+const mapPathComponentToDirectory = (component: string, index: number, components: string[]): CollectionDirectory =>
+ 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): CollectionDirectory =>
+ createDirectory({
+ parentId: '',
+ id: stream.name,
+ name: stream.name,
+ });
+
+const mapStreamFile = (stream: KeepManifestStream) =>
+ (file: KeepManifestStreamFile): CollectionFile =>
+ createFile({
+ parentId: stream.name,
+ id: `${stream.name}/${file.name}`,
+ name: file.name,
+ size: file.size,
+ });
+
+export const createDirectory = (data: Partial<CollectionDirectory>): CollectionDirectory => ({
+ id: '',
+ name: '',
+ parentId: '',
+ type: CollectionFileType.DIRECTORY,
+ ...data
+});
+
+export const createFile = (data: Partial<CollectionFile>): CollectionFile => ({
+ id: '',
+ name: '',
+ parentId: '',
+ size: 0,
+ type: CollectionFileType.FILE,
+ ...data
+});
\ No newline at end of file
--- /dev/null
+// 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
--- /dev/null
+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);
+
+
+
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 }>(),
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 }));
});
};
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<{}>(),
//
// 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
//
// 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);
};
+
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
};
};
};
});
-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
+ };
};
-};