20031: Preselect current collection in move/copy to existing collection tree picker
[arvados.git] / src / store / tree-picker / tree-picker-actions.ts
1 // Copyright (C) The Arvados Authors. All rights reserved.
2 //
3 // SPDX-License-Identifier: AGPL-3.0
4
5 import { unionize, ofType, UnionOf } from "common/unionize";
6 import { TreeNode, initTreeNode, getNodeDescendants, TreeNodeStatus, getNode, TreePickerId, Tree, setNode, createTree } from 'models/tree';
7 import { CollectionFileType, createCollectionFilesTree, getCollectionResourceCollectionUuid } from "models/collection-file";
8 import { Dispatch } from 'redux';
9 import { RootState } from 'store/store';
10 import { getUserUuid } from "common/getuser";
11 import { ServiceRepository } from 'services/services';
12 import { FilterBuilder } from 'services/api/filter-builder';
13 import { pipe, values } from 'lodash/fp';
14 import { ResourceKind } from 'models/resource';
15 import { GroupContentsResource } from 'services/groups-service/groups-service';
16 import { getTreePicker, TreePicker } from './tree-picker';
17 import { ProjectsTreePickerItem } from './tree-picker-middleware';
18 import { OrderBuilder } from 'services/api/order-builder';
19 import { ProjectResource } from 'models/project';
20 import { mapTree } from '../../models/tree';
21 import { LinkResource, LinkClass } from "models/link";
22 import { mapTreeValues } from "models/tree";
23 import { sortFilesTree } from "services/collection-service/collection-service-files-response";
24 import { GroupClass, GroupResource } from "models/group";
25 import { CollectionResource } from "models/collection";
26
27 export const treePickerActions = unionize({
28     LOAD_TREE_PICKER_NODE: ofType<{ id: string, pickerId: string }>(),
29     LOAD_TREE_PICKER_NODE_SUCCESS: ofType<{ id: string, nodes: Array<TreeNode<any>>, pickerId: string }>(),
30     APPEND_TREE_PICKER_NODE_SUBTREE: ofType<{ id: string, subtree: Tree<any>, pickerId: string }>(),
31     TOGGLE_TREE_PICKER_NODE_COLLAPSE: ofType<{ id: string, pickerId: string }>(),
32     EXPAND_TREE_PICKER_NODE: ofType<{ id: string, pickerId: string }>(),
33     ACTIVATE_TREE_PICKER_NODE: ofType<{ id: string, pickerId: string, relatedTreePickers?: string[] }>(),
34     DEACTIVATE_TREE_PICKER_NODE: ofType<{ pickerId: string }>(),
35     TOGGLE_TREE_PICKER_NODE_SELECTION: ofType<{ id: string, pickerId: string }>(),
36     SELECT_TREE_PICKER_NODE: ofType<{ id: string | string[], pickerId: string }>(),
37     DESELECT_TREE_PICKER_NODE: ofType<{ id: string | string[], pickerId: string }>(),
38     EXPAND_TREE_PICKER_NODES: ofType<{ ids: string[], pickerId: string }>(),
39     RESET_TREE_PICKER: ofType<{ pickerId: string }>()
40 });
41
42 export type TreePickerAction = UnionOf<typeof treePickerActions>;
43
44 export interface LoadProjectParams {
45     includeCollections?: boolean;
46     includeDirectories?: boolean;
47     includeFiles?: boolean;
48     includeFilterGroups?: boolean;
49     options?: { showOnlyOwned: boolean; showOnlyWritable: boolean; };
50 }
51
52 export const treePickerSearchActions = unionize({
53     SET_TREE_PICKER_PROJECT_SEARCH: ofType<{ pickerId: string, projectSearchValue: string }>(),
54     SET_TREE_PICKER_COLLECTION_FILTER: ofType<{ pickerId: string, collectionFilterValue: string }>(),
55     SET_TREE_PICKER_LOAD_PARAMS: ofType<{ pickerId: string, params: LoadProjectParams }>(),
56     REFRESH_TREE_PICKER: ofType<{ pickerId: string }>(),
57 });
58
59 export type TreePickerSearchAction = UnionOf<typeof treePickerSearchActions>;
60
61 export const getProjectsTreePickerIds = (pickerId: string) => ({
62     home: `${pickerId}_home`,
63     shared: `${pickerId}_shared`,
64     favorites: `${pickerId}_favorites`,
65     publicFavorites: `${pickerId}_publicFavorites`,
66     search: `${pickerId}_search`,
67 });
68
69 export const getAllNodes = <Value>(pickerId: string, filter = (node: TreeNode<Value>) => true) => (state: TreePicker) =>
70     pipe(
71         () => values(getProjectsTreePickerIds(pickerId)),
72
73         ids => ids
74             .map(id => getTreePicker<Value>(id)(state)),
75
76         trees => trees
77             .map(getNodeDescendants(''))
78             .reduce((allNodes, nodes) => allNodes.concat(nodes), []),
79
80         allNodes => allNodes
81             .reduce((map, node) =>
82                 filter(node)
83                     ? map.set(node.id, node)
84                     : map, new Map<string, TreeNode<Value>>())
85             .values(),
86
87         uniqueNodes => Array.from(uniqueNodes),
88     )();
89 export const getSelectedNodes = <Value>(pickerId: string) => (state: TreePicker) =>
90     getAllNodes<Value>(pickerId, node => node.selected)(state);
91
92 export const initProjectsTreePicker = (pickerId: string, selectedItemUuid?: string) =>
93     async (dispatch: Dispatch, getState: () => RootState, services: ServiceRepository) => {
94         const { home, shared, favorites, publicFavorites, search } = getProjectsTreePickerIds(pickerId);
95         dispatch<any>(initUserProject(home));
96         dispatch<any>(initSharedProject(shared));
97         dispatch<any>(initFavoritesProject(favorites));
98         dispatch<any>(initPublicFavoritesProject(publicFavorites));
99         dispatch<any>(initSearchProject(search));
100
101         if (selectedItemUuid) {
102             dispatch<any>(loadInitialValue(selectedItemUuid, pickerId));
103         }
104     };
105
106 interface ReceiveTreePickerDataParams<T> {
107     data: T[];
108     extractNodeData: (value: T) => { id: string, value: T, status?: TreeNodeStatus };
109     id: string;
110     pickerId: string;
111 }
112
113 export const receiveTreePickerData = <T>(params: ReceiveTreePickerDataParams<T>) =>
114     (dispatch: Dispatch) => {
115         const { data, extractNodeData, id, pickerId, } = params;
116         dispatch(treePickerActions.LOAD_TREE_PICKER_NODE_SUCCESS({
117             id,
118             nodes: data.map(item => initTreeNode(extractNodeData(item))),
119             pickerId,
120         }));
121         dispatch(treePickerActions.EXPAND_TREE_PICKER_NODE({ id, pickerId }));
122     };
123
124 interface LoadProjectParamsWithId extends LoadProjectParams {
125     id: string;
126     pickerId: string;
127     loadShared?: boolean;
128     searchProjects?: boolean;
129 }
130
131 export const loadProject = (params: LoadProjectParamsWithId) =>
132     async (dispatch: Dispatch, getState: () => RootState, services: ServiceRepository) => {
133         const {
134             id,
135             pickerId,
136             includeCollections = false,
137             includeDirectories = false,
138             includeFiles = false,
139             includeFilterGroups = false,
140             loadShared = false,
141             options,
142             searchProjects = false
143         } = params;
144
145         dispatch(treePickerActions.LOAD_TREE_PICKER_NODE({ id, pickerId }));
146
147         let filterB = new FilterBuilder();
148
149         filterB = (includeCollections && !searchProjects)
150             ? filterB.addIsA('uuid', [ResourceKind.PROJECT, ResourceKind.COLLECTION])
151             : filterB.addIsA('uuid', [ResourceKind.PROJECT]);
152
153         const state = getState();
154
155         if (state.treePickerSearch.collectionFilterValues[pickerId]) {
156             filterB = filterB.addFullTextSearch(state.treePickerSearch.collectionFilterValues[pickerId], 'collections');
157         } else {
158             filterB = filterB.addNotIn("collections.properties.type", ["intermediate", "log"]);
159         }
160
161         if (searchProjects && state.treePickerSearch.projectSearchValues[pickerId]) {
162             filterB = filterB.addFullTextSearch(state.treePickerSearch.projectSearchValues[pickerId], 'groups');
163         }
164
165         const filters = filterB.getFilters();
166
167         const itemLimit = 200;
168
169         const { items, itemsAvailable } = await services.groupsService.contents((loadShared || searchProjects) ? '' : id, { filters, excludeHomeProject: loadShared || undefined, limit: itemLimit });
170
171         if (itemsAvailable > itemLimit) {
172             items.push({
173                 uuid: "more-items-available",
174                 kind: ResourceKind.WORKFLOW,
175                 name: `*** Not all items listed (${items.length} out of ${itemsAvailable}), reduce item count with search or filter ***`,
176                 description: "",
177                 definition: "",
178                 ownerUuid: "",
179                 createdAt: "",
180                 modifiedByClientUuid: "",
181                 modifiedByUserUuid: "",
182                 modifiedAt: "",
183                 href: "",
184                 etag: ""
185             });
186         }
187
188         dispatch<any>(receiveTreePickerData<GroupContentsResource>({
189             id,
190             pickerId,
191             data: items.filter((item) => {
192                 if (!includeFilterGroups && (item as GroupResource).groupClass && (item as GroupResource).groupClass === GroupClass.FILTER) {
193                     return false;
194                 }
195
196                 if (options && options.showOnlyWritable && item.hasOwnProperty('frozenByUuid') && (item as ProjectResource).frozenByUuid) {
197                     return false;
198                 }
199
200                 return true;
201             }),
202             extractNodeData: item => (
203                 item.uuid === "more-items-available" ?
204                     {
205                         id: item.uuid,
206                         value: item,
207                         status: TreeNodeStatus.LOADED
208                     }
209                     : {
210                         id: item.uuid,
211                         value: item,
212                         status: item.kind === ResourceKind.PROJECT
213                             ? TreeNodeStatus.INITIAL
214                             : includeDirectories || includeFiles
215                                 ? TreeNodeStatus.INITIAL
216                                 : TreeNodeStatus.LOADED
217                     }),
218         }));
219     };
220
221 export const loadCollection = (id: string, pickerId: string, includeDirectories?: boolean, includeFiles?: boolean) =>
222     async (dispatch: Dispatch, getState: () => RootState, services: ServiceRepository) => {
223         dispatch(treePickerActions.LOAD_TREE_PICKER_NODE({ id, pickerId }));
224
225         const picker = getTreePicker<ProjectsTreePickerItem>(pickerId)(getState().treePicker);
226         if (picker) {
227
228             const node = getNode(id)(picker);
229             if (node && 'kind' in node.value && node.value.kind === ResourceKind.COLLECTION) {
230                 const files = (await services.collectionService.files(node.value.uuid))
231                     .filter((file) => (
232                         (includeFiles) ||
233                         (includeDirectories && file.type === CollectionFileType.DIRECTORY)
234                     ));
235                 const tree = createCollectionFilesTree(files);
236                 const sorted = sortFilesTree(tree);
237                 const filesTree = mapTreeValues(services.collectionService.extendFileURL)(sorted);
238
239                 dispatch(
240                     treePickerActions.APPEND_TREE_PICKER_NODE_SUBTREE({
241                         id,
242                         pickerId,
243                         subtree: mapTree(node => ({ ...node, status: TreeNodeStatus.LOADED }))(filesTree)
244                     }));
245
246                 dispatch(treePickerActions.TOGGLE_TREE_PICKER_NODE_COLLAPSE({ id, pickerId }));
247             }
248         }
249     };
250
251 export const HOME_PROJECT_ID = 'Home Projects';
252 export const initUserProject = (pickerId: string) =>
253     async (dispatch: Dispatch<any>, getState: () => RootState, services: ServiceRepository) => {
254         const uuid = getUserUuid(getState());
255         if (uuid) {
256             dispatch(receiveTreePickerData({
257                 id: '',
258                 pickerId,
259                 data: [{ uuid, name: HOME_PROJECT_ID }],
260                 extractNodeData: value => ({
261                     id: value.uuid,
262                     status: TreeNodeStatus.INITIAL,
263                     value,
264                 }),
265             }));
266         }
267     };
268 export const loadUserProject = (pickerId: string, includeCollections = false, includeDirectories = false, includeFiles = false, options?: { showOnlyOwned: boolean, showOnlyWritable: boolean }) =>
269     async (dispatch: Dispatch<any>, getState: () => RootState, services: ServiceRepository) => {
270         const uuid = getUserUuid(getState());
271         if (uuid) {
272             dispatch(loadProject({ id: uuid, pickerId, includeCollections, includeDirectories, includeFiles, options }));
273         }
274     };
275
276 export const SHARED_PROJECT_ID = 'Shared with me';
277 export const initSharedProject = (pickerId: string) =>
278     async (dispatch: Dispatch<any>, getState: () => RootState, services: ServiceRepository) => {
279         dispatch(receiveTreePickerData({
280             id: '',
281             pickerId,
282             data: [{ uuid: SHARED_PROJECT_ID, name: SHARED_PROJECT_ID }],
283             extractNodeData: value => ({
284                 id: value.uuid,
285                 status: TreeNodeStatus.INITIAL,
286                 value,
287             }),
288         }));
289     };
290
291 export const loadInitialValue = (initialValue: string, pickerId: string) =>
292     async (dispatch: Dispatch<any>, getState: () => RootState, services: ServiceRepository) => {
293         const { home, shared } = getProjectsTreePickerIds(pickerId);
294         const homeUuid = getUserUuid(getState());
295         const ancestors = (await services.ancestorsService.ancestors(initialValue, ''))
296             .filter(item =>
297                 item.kind === ResourceKind.GROUP ||
298                 item.kind === ResourceKind.COLLECTION
299             ) as (GroupResource | CollectionResource)[];
300
301         if (ancestors.length) {
302             const isUserHomeProject = !!(homeUuid && ancestors.some(item => item.ownerUuid === homeUuid));
303             const pickerTreeId = isUserHomeProject ? home : shared;
304             const pickerTreeRootUuid: string = (homeUuid && isUserHomeProject) ? homeUuid : SHARED_PROJECT_ID;
305
306             ancestors[0].ownerUuid = '';
307             const tree = createInitialLocationTree(ancestors, initialValue);
308             dispatch(
309                 treePickerActions.APPEND_TREE_PICKER_NODE_SUBTREE({
310                     id: pickerTreeRootUuid,
311                     pickerId: pickerTreeId,
312                     subtree: tree
313                 }));
314             dispatch(treePickerActions.EXPAND_TREE_PICKER_NODES({ ids: [pickerTreeRootUuid], pickerId: pickerTreeId }));
315             dispatch(treePickerActions.ACTIVATE_TREE_PICKER_NODE({ id: initialValue, pickerId: pickerTreeId }));
316             dispatch(treePickerSearchActions.REFRESH_TREE_PICKER({ pickerId: pickerTreeId }));
317         }
318
319     }
320
321 export const FAVORITES_PROJECT_ID = 'Favorites';
322 export const initFavoritesProject = (pickerId: string) =>
323     async (dispatch: Dispatch<any>, getState: () => RootState, services: ServiceRepository) => {
324         dispatch(receiveTreePickerData({
325             id: '',
326             pickerId,
327             data: [{ uuid: FAVORITES_PROJECT_ID, name: FAVORITES_PROJECT_ID }],
328             extractNodeData: value => ({
329                 id: value.uuid,
330                 status: TreeNodeStatus.INITIAL,
331                 value,
332             }),
333         }));
334     };
335
336 export const PUBLIC_FAVORITES_PROJECT_ID = 'Public Favorites';
337 export const initPublicFavoritesProject = (pickerId: string) =>
338     async (dispatch: Dispatch<any>, getState: () => RootState, services: ServiceRepository) => {
339         dispatch(receiveTreePickerData({
340             id: '',
341             pickerId,
342             data: [{ uuid: PUBLIC_FAVORITES_PROJECT_ID, name: PUBLIC_FAVORITES_PROJECT_ID }],
343             extractNodeData: value => ({
344                 id: value.uuid,
345                 status: TreeNodeStatus.INITIAL,
346                 value,
347             }),
348         }));
349     };
350
351 export const SEARCH_PROJECT_ID = 'Search all Projects';
352 export const initSearchProject = (pickerId: string) =>
353     async (dispatch: Dispatch<any>, getState: () => RootState, services: ServiceRepository) => {
354         dispatch(receiveTreePickerData({
355             id: '',
356             pickerId,
357             data: [{ uuid: SEARCH_PROJECT_ID, name: SEARCH_PROJECT_ID }],
358             extractNodeData: value => ({
359                 id: value.uuid,
360                 status: TreeNodeStatus.INITIAL,
361                 value,
362             }),
363         }));
364     };
365
366
367 interface LoadFavoritesProjectParams {
368     pickerId: string;
369     includeCollections?: boolean;
370     includeDirectories?: boolean;
371     includeFiles?: boolean;
372     options?: { showOnlyOwned: boolean, showOnlyWritable: boolean };
373 }
374
375 export const loadFavoritesProject = (params: LoadFavoritesProjectParams,
376     options: { showOnlyOwned: boolean, showOnlyWritable: boolean } = { showOnlyOwned: true, showOnlyWritable: false }) =>
377     async (dispatch: Dispatch<any>, getState: () => RootState, services: ServiceRepository) => {
378         const { pickerId, includeCollections = false, includeDirectories = false, includeFiles = false } = params;
379         const uuid = getUserUuid(getState());
380         if (uuid) {
381             const filters = pipe(
382                 (fb: FilterBuilder) => includeCollections
383                     ? fb.addIsA('head_uuid', [ResourceKind.PROJECT, ResourceKind.COLLECTION])
384                     : fb.addIsA('head_uuid', [ResourceKind.PROJECT]),
385                 fb => fb.getFilters(),
386             )(new FilterBuilder());
387
388             const { items } = await services.favoriteService.list(uuid, { filters }, options.showOnlyOwned);
389
390             dispatch<any>(receiveTreePickerData<GroupContentsResource>({
391                 id: 'Favorites',
392                 pickerId,
393                 data: items.filter((item) => {
394                     if (options.showOnlyWritable && (item as GroupResource).writableBy && (item as GroupResource).writableBy.indexOf(uuid) === -1) {
395                         return false;
396                     }
397
398                     if (options.showOnlyWritable && item.hasOwnProperty('frozenByUuid') && (item as ProjectResource).frozenByUuid) {
399                         return false;
400                     }
401
402                     return true;
403                 }),
404                 extractNodeData: item => ({
405                     id: item.uuid,
406                     value: item,
407                     status: item.kind === ResourceKind.PROJECT
408                         ? TreeNodeStatus.INITIAL
409                         : includeDirectories || includeFiles
410                             ? TreeNodeStatus.INITIAL
411                             : TreeNodeStatus.LOADED
412                 }),
413             }));
414         }
415     };
416
417 export const loadPublicFavoritesProject = (params: LoadFavoritesProjectParams) =>
418     async (dispatch: Dispatch<any>, getState: () => RootState, services: ServiceRepository) => {
419         const { pickerId, includeCollections = false, includeDirectories = false, includeFiles = false } = params;
420         const uuidPrefix = getState().auth.config.uuidPrefix;
421         const publicProjectUuid = `${uuidPrefix}-j7d0g-publicfavorites`;
422
423         const filters = pipe(
424             (fb: FilterBuilder) => includeCollections
425                 ? fb.addIsA('head_uuid', [ResourceKind.PROJECT, ResourceKind.COLLECTION])
426                 : fb.addIsA('head_uuid', [ResourceKind.PROJECT]),
427             fb => fb
428                 .addEqual('link_class', LinkClass.STAR)
429                 .addEqual('owner_uuid', publicProjectUuid)
430                 .getFilters(),
431         )(new FilterBuilder());
432
433         const { items } = await services.linkService.list({ filters });
434
435         dispatch<any>(receiveTreePickerData<LinkResource>({
436             id: 'Public Favorites',
437             pickerId,
438             data: items.filter(item => {
439                 if (params.options && params.options.showOnlyWritable && item.hasOwnProperty('frozenByUuid') && (item as any).frozenByUuid) {
440                     return false;
441                 }
442
443                 return true;
444             }),
445             extractNodeData: item => ({
446                 id: item.headUuid,
447                 value: item,
448                 status: item.headKind === ResourceKind.PROJECT
449                     ? TreeNodeStatus.INITIAL
450                     : includeDirectories || includeFiles
451                         ? TreeNodeStatus.INITIAL
452                         : TreeNodeStatus.LOADED
453             }),
454         }));
455     };
456
457 export const receiveTreePickerProjectsData = (id: string, projects: ProjectResource[], pickerId: string) =>
458     (dispatch: Dispatch, getState: () => RootState, services: ServiceRepository) => {
459         dispatch(treePickerActions.LOAD_TREE_PICKER_NODE_SUCCESS({
460             id,
461             nodes: projects.map(project => initTreeNode({ id: project.uuid, value: project })),
462             pickerId,
463         }));
464
465         dispatch(treePickerActions.TOGGLE_TREE_PICKER_NODE_COLLAPSE({ id, pickerId }));
466     };
467
468 export const loadProjectTreePickerProjects = (id: string) =>
469     async (dispatch: Dispatch, getState: () => RootState, services: ServiceRepository) => {
470         dispatch(treePickerActions.LOAD_TREE_PICKER_NODE({ id, pickerId: TreePickerId.PROJECTS }));
471
472
473         const ownerUuid = id.length === 0 ? getUserUuid(getState()) || '' : id;
474         const { items } = await services.projectService.list(buildParams(ownerUuid));
475
476         dispatch<any>(receiveTreePickerProjectsData(id, items, TreePickerId.PROJECTS));
477     };
478
479 export const loadFavoriteTreePickerProjects = (id: string) =>
480     async (dispatch: Dispatch, getState: () => RootState, services: ServiceRepository) => {
481         const parentId = getUserUuid(getState()) || '';
482
483         if (id === '') {
484             dispatch(treePickerActions.LOAD_TREE_PICKER_NODE({ id: parentId, pickerId: TreePickerId.FAVORITES }));
485             const { items } = await services.favoriteService.list(parentId);
486             dispatch<any>(receiveTreePickerProjectsData(parentId, items as ProjectResource[], TreePickerId.FAVORITES));
487         } else {
488             dispatch(treePickerActions.LOAD_TREE_PICKER_NODE({ id, pickerId: TreePickerId.FAVORITES }));
489             const { items } = await services.projectService.list(buildParams(id));
490             dispatch<any>(receiveTreePickerProjectsData(id, items, TreePickerId.FAVORITES));
491         }
492
493     };
494
495 export const loadPublicFavoriteTreePickerProjects = (id: string) =>
496     async (dispatch: Dispatch, getState: () => RootState, services: ServiceRepository) => {
497         const parentId = getUserUuid(getState()) || '';
498
499         if (id === '') {
500             dispatch(treePickerActions.LOAD_TREE_PICKER_NODE({ id: parentId, pickerId: TreePickerId.PUBLIC_FAVORITES }));
501             const { items } = await services.favoriteService.list(parentId);
502             dispatch<any>(receiveTreePickerProjectsData(parentId, items as ProjectResource[], TreePickerId.PUBLIC_FAVORITES));
503         } else {
504             dispatch(treePickerActions.LOAD_TREE_PICKER_NODE({ id, pickerId: TreePickerId.PUBLIC_FAVORITES }));
505             const { items } = await services.projectService.list(buildParams(id));
506             dispatch<any>(receiveTreePickerProjectsData(id, items, TreePickerId.PUBLIC_FAVORITES));
507         }
508
509     };
510
511 const buildParams = (ownerUuid: string) => {
512     return {
513         filters: new FilterBuilder()
514             .addEqual('owner_uuid', ownerUuid)
515             .getFilters(),
516         order: new OrderBuilder<ProjectResource>()
517             .addAsc('name')
518             .getOrder()
519     };
520 };
521
522 /**
523  * Given a tree picker item, return collection uuid and path
524  *   if the item represents a valid target/destination location
525  */
526 export type FileOperationLocation = {
527     uuid: string;
528     path: string;
529 }
530 export const getFileOperationLocation = (item: ProjectsTreePickerItem): FileOperationLocation | undefined => {
531     if ('kind' in item && item.kind === ResourceKind.COLLECTION) {
532         return {
533             uuid: item.uuid,
534             path: '/'
535         };
536     } else if ('type' in item && item.type === CollectionFileType.DIRECTORY) {
537         const uuid = getCollectionResourceCollectionUuid(item.id);
538         if (uuid) {
539             return {
540                 uuid,
541                 path: [item.path, item.name].join('/')
542             };
543         } else {
544             return undefined;
545         }
546     } else {
547         return undefined;
548     }
549 };
550
551 /**
552  * Create an expanded tree picker subtree from array of nested projects/collection
553  *   Assumes the root item of the subtree already has an empty string ownerUuid
554  */
555 export const createInitialLocationTree = (data: Array<GroupResource | CollectionResource>, tailUuid: string) => {
556     return data
557         .reduce((tree, item) => setNode({
558             children: [],
559             id: item.uuid,
560             parent: item.ownerUuid,
561             value: item,
562             active: false,
563             selected: false,
564             expanded: item.uuid !== tailUuid,
565             status: item.uuid !== tailUuid ? TreeNodeStatus.LOADED : TreeNodeStatus.INITIAL,
566         })(tree), createTree<GroupResource | CollectionResource>());
567 };