16243: Fixed copy of selected items to new collection
[arvados-workbench2.git] / src / store / collection-panel / collection-panel-files / collection-panel-files-reducer.ts
1 // Copyright (C) The Arvados Authors. All rights reserved.
2 //
3 // SPDX-License-Identifier: AGPL-3.0
4
5 import { CollectionPanelFilesState, CollectionPanelFile, CollectionPanelDirectory, mapCollectionFileToCollectionPanelFile, mergeCollectionPanelFilesStates } from './collection-panel-files-state';
6 import { CollectionPanelFilesAction, collectionPanelFilesAction } from "./collection-panel-files-actions";
7 import { createTree, mapTreeValues, getNode, setNode, getNodeAncestorsIds, getNodeDescendantsIds, setNodeValueWith, mapTree } from "~/models/tree";
8 import { CollectionFileType } from "~/models/collection-file";
9
10 let fetchedFiles: any = {};
11
12 export const collectionPanelFilesReducer = (state: CollectionPanelFilesState = createTree(), action: CollectionPanelFilesAction) => {
13     // Low-level tree handling setNode() func does in-place data modifications
14     // for performance reasons, so we pass a copy of 'state' to avoid side effects.
15     return collectionPanelFilesAction.match(action, {
16         SET_COLLECTION_FILES: files => {
17             fetchedFiles = files;
18             return mergeCollectionPanelFilesStates({ ...state }, mapTree(mapCollectionFileToCollectionPanelFile)(files));
19         },
20
21         TOGGLE_COLLECTION_FILE_COLLAPSE: data =>
22             toggleCollapse(data.id)({ ...state }),
23
24         TOGGLE_COLLECTION_FILE_SELECTION: data => [{ ...state }]
25             .map(toggleSelected(data.id))
26             .map(toggleAncestors(data.id))
27             .map(toggleDescendants(data.id))[0],
28
29         ON_SEARCH_CHANGE: (searchValue) => {
30             const fileIds: string[] = [];
31             const directoryIds: string[] = [];
32             const filteredFiles = Object.keys(fetchedFiles)
33                 .filter((key: string) => {
34                     const node = fetchedFiles[key];
35
36                     if (node.value === undefined) {
37                         return false;
38                     }
39
40                     const { id, value: { type, name } } = node;
41
42                     if (type === CollectionFileType.DIRECTORY) {
43                         directoryIds.push(id);
44                         return true;
45                     }
46
47                     const includeFile = name.toLowerCase().indexOf(searchValue.toLowerCase()) > -1;
48
49                     if (includeFile) {
50                         fileIds.push(id);
51                     }
52
53                     return includeFile;
54                 })
55                 .reduce((prev, next) => {
56                     const node = JSON.parse(JSON.stringify(fetchedFiles[next]));
57                     const { value: { type }, children } = node;
58
59                     node.children = node.children.filter((key: string) => {
60                         const isFile = directoryIds.indexOf(key) === -1;
61                         return isFile ?
62                           fileIds.indexOf(key) > -1 :
63                           !!fileIds.find(id => id.indexOf(key) > -1);
64                     });
65
66                     if (type === CollectionFileType.FILE || children.length > 0) {
67                         prev[next] = node;
68                     }
69
70                     return prev;
71                 }, {});
72
73             return mapTreeValues((v: CollectionPanelDirectory | CollectionPanelFile) => {
74                 if (v.type === CollectionFileType.DIRECTORY) {
75                     return ({ 
76                         ...v,
77                         collapsed: searchValue.length === 0,
78                     });
79                 }
80
81                 return ({ ...v });
82             })({ ...filteredFiles });
83         },
84
85         SELECT_ALL_COLLECTION_FILES: () =>
86             mapTreeValues(v => ({ ...v, selected: true }))({ ...state }),
87
88         UNSELECT_ALL_COLLECTION_FILES: () =>
89             mapTreeValues(v => ({ ...v, selected: false }))({ ...state }),
90
91         default: () => state
92     }) as CollectionPanelFilesState;
93 };
94
95 const toggleCollapse = (id: string) => (tree: CollectionPanelFilesState) =>
96     setNodeValueWith((v: CollectionPanelDirectory | CollectionPanelFile) =>
97         v.type === CollectionFileType.DIRECTORY
98             ? { ...v, collapsed: !v.collapsed }
99             : v)(id)(tree);
100
101
102 const toggleSelected = (id: string) => (tree: CollectionPanelFilesState) =>
103     setNodeValueWith((v: CollectionPanelDirectory | CollectionPanelFile) => ({ ...v, selected: !v.selected }))(id)(tree);
104
105
106 const toggleDescendants = (id: string) => (tree: CollectionPanelFilesState) => {
107     const node = getNode(id)(tree);
108     if (node && node.value.type === CollectionFileType.DIRECTORY) {
109         return getNodeDescendantsIds(id)(tree)
110             .reduce((newTree, id) =>
111                 setNodeValueWith(v => ({ ...v, selected: node.value.selected }))(id)(newTree), tree);
112     }
113     return tree;
114 };
115
116 const toggleAncestors = (id: string) => (tree: CollectionPanelFilesState) => {
117     const ancestors = getNodeAncestorsIds(id)(tree).reverse();
118     return ancestors.reduce((newTree, parent) => parent ? toggleParentNode(parent)(newTree) : newTree, tree);
119 };
120
121 const toggleParentNode = (id: string) => (tree: CollectionPanelFilesState) => {
122     const node = getNode(id)(tree);
123     if (node) {
124         const parentNode = getNode(node.id)(tree);
125         if (parentNode) {
126             const selected = parentNode.children
127                 .map(id => getNode(id)(tree))
128                 .every(node => node !== undefined && node.value.selected);
129             return setNodeValueWith(v => ({ ...v, selected }))(parentNode.id)(tree);
130         }
131         return setNode(node)(tree);
132     }
133     return tree;
134 };
135
136