1 // Copyright (C) The Arvados Authors. All rights reserved.
3 // SPDX-License-Identifier: AGPL-3.0
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 import { getResource } from "store/resources/resources";
27 import { updateResources } from "store/resources/resources-actions";
28 import { SnackbarKind, snackbarActions } from "store/snackbar/snackbar-actions";
30 export const treePickerActions = unionize({
31 LOAD_TREE_PICKER_NODE: ofType<{ id: string, pickerId: string }>(),
32 LOAD_TREE_PICKER_NODE_SUCCESS: ofType<{ id: string, nodes: Array<TreeNode<any>>, pickerId: string }>(),
33 APPEND_TREE_PICKER_NODE_SUBTREE: ofType<{ id: string, subtree: Tree<any>, pickerId: string }>(),
34 TOGGLE_TREE_PICKER_NODE_COLLAPSE: ofType<{ id: string, pickerId: string }>(),
35 EXPAND_TREE_PICKER_NODE: ofType<{ id: string, pickerId: string }>(),
36 EXPAND_TREE_PICKER_NODE_ANCESTORS: ofType<{ id: string, pickerId: string }>(),
37 ACTIVATE_TREE_PICKER_NODE: ofType<{ id: string, pickerId: string, relatedTreePickers?: string[] }>(),
38 DEACTIVATE_TREE_PICKER_NODE: ofType<{ pickerId: string }>(),
39 TOGGLE_TREE_PICKER_NODE_SELECTION: ofType<{ id: string, pickerId: string, cascade: boolean }>(),
40 SELECT_TREE_PICKER_NODE: ofType<{ id: string | string[], pickerId: string, cascade: boolean }>(),
41 DESELECT_TREE_PICKER_NODE: ofType<{ id: string | string[], pickerId: string, cascade: boolean }>(),
42 EXPAND_TREE_PICKER_NODES: ofType<{ ids: string[], pickerId: string }>(),
43 RESET_TREE_PICKER: ofType<{ pickerId: string }>()
46 export type TreePickerAction = UnionOf<typeof treePickerActions>;
48 export interface LoadProjectParams {
49 includeCollections?: boolean;
50 includeDirectories?: boolean;
51 includeFiles?: boolean;
52 includeFilterGroups?: boolean;
53 options?: { showOnlyOwned: boolean; showOnlyWritable: boolean; };
56 export const treePickerSearchActions = unionize({
57 SET_TREE_PICKER_PROJECT_SEARCH: ofType<{ pickerId: string, projectSearchValue: string }>(),
58 SET_TREE_PICKER_COLLECTION_FILTER: ofType<{ pickerId: string, collectionFilterValue: string }>(),
59 SET_TREE_PICKER_LOAD_PARAMS: ofType<{ pickerId: string, params: LoadProjectParams }>(),
60 REFRESH_TREE_PICKER: ofType<{ pickerId: string }>(),
63 export type TreePickerSearchAction = UnionOf<typeof treePickerSearchActions>;
65 export const getProjectsTreePickerIds = (pickerId: string) => ({
66 home: `${pickerId}_home`,
67 shared: `${pickerId}_shared`,
68 favorites: `${pickerId}_favorites`,
69 publicFavorites: `${pickerId}_publicFavorites`,
70 search: `${pickerId}_search`,
73 export const getAllNodes = <Value>(pickerId: string, filter = (node: TreeNode<Value>) => true) => (state: TreePicker) =>
75 () => values(getProjectsTreePickerIds(pickerId)),
78 .map(id => getTreePicker<Value>(id)(state)),
81 .map(getNodeDescendants(''))
82 .reduce((allNodes, nodes) => allNodes.concat(nodes), []),
85 .reduce((map, node) =>
87 ? map.set(node.id, node)
88 : map, new Map<string, TreeNode<Value>>())
91 uniqueNodes => Array.from(uniqueNodes),
93 export const getSelectedNodes = <Value>(pickerId: string) => (state: TreePicker) =>
94 getAllNodes<Value>(pickerId, node => node.selected)(state);
96 interface TreePickerPreloadParams {
97 selectedItemUuids: string[];
98 includeDirectories: boolean;
99 includeFiles: boolean;
103 export const initProjectsTreePicker = (pickerId: string, preloadParams?: TreePickerPreloadParams) =>
104 async (dispatch: Dispatch, getState: () => RootState, services: ServiceRepository) => {
105 const { home, shared, favorites, publicFavorites, search } = getProjectsTreePickerIds(pickerId);
106 dispatch<any>(initUserProject(home));
107 dispatch<any>(initSharedProject(shared));
108 dispatch<any>(initFavoritesProject(favorites));
109 dispatch<any>(initPublicFavoritesProject(publicFavorites));
110 dispatch<any>(initSearchProject(search));
112 if (preloadParams && preloadParams.selectedItemUuids.length) {
113 await dispatch<any>(loadInitialValue(
114 preloadParams.selectedItemUuids,
116 preloadParams.includeDirectories,
117 preloadParams.includeFiles,
123 interface ReceiveTreePickerDataParams<T> {
125 extractNodeData: (value: T) => { id: string, value: T, status?: TreeNodeStatus };
130 export const receiveTreePickerData = <T>(params: ReceiveTreePickerDataParams<T>) =>
131 (dispatch: Dispatch) => {
132 const { data, extractNodeData, id, pickerId, } = params;
133 dispatch(treePickerActions.LOAD_TREE_PICKER_NODE_SUCCESS({
135 nodes: data.map(item => initTreeNode(extractNodeData(item))),
138 dispatch(treePickerActions.EXPAND_TREE_PICKER_NODE({ id, pickerId }));
141 export const extractGroupContentsNodeData = (expandableCollections: boolean) => (item: GroupContentsResource) => (
142 item.uuid === "more-items-available"
146 status: TreeNodeStatus.LOADED
151 status: item.kind === ResourceKind.PROJECT
152 ? TreeNodeStatus.INITIAL
153 : item.kind === ResourceKind.COLLECTION && expandableCollections
154 ? TreeNodeStatus.INITIAL
155 : TreeNodeStatus.LOADED
158 interface LoadProjectParamsWithId extends LoadProjectParams {
161 loadShared?: boolean;
162 searchProjects?: boolean;
166 * loadProject is used to load or refresh a project node in a tree picker
167 * Errors are caught and a toast is shown if the project fails to load
169 export const loadProject = (params: LoadProjectParamsWithId) =>
170 async (dispatch: Dispatch, getState: () => RootState, services: ServiceRepository) => {
174 includeCollections = false,
175 includeDirectories = false,
176 includeFiles = false,
177 includeFilterGroups = false,
180 searchProjects = false
183 dispatch(treePickerActions.LOAD_TREE_PICKER_NODE({ id, pickerId }));
185 let filterB = new FilterBuilder();
187 filterB = (includeCollections && !searchProjects)
188 ? filterB.addIsA('uuid', [ResourceKind.PROJECT, ResourceKind.COLLECTION])
189 : filterB.addIsA('uuid', [ResourceKind.PROJECT]);
191 const state = getState();
193 if (state.treePickerSearch.collectionFilterValues[pickerId]) {
194 filterB = filterB.addFullTextSearch(state.treePickerSearch.collectionFilterValues[pickerId], 'collections');
196 filterB = filterB.addNotIn("collections.properties.type", ["intermediate", "log"]);
199 if (searchProjects && state.treePickerSearch.projectSearchValues[pickerId]) {
200 filterB = filterB.addFullTextSearch(state.treePickerSearch.projectSearchValues[pickerId], 'groups');
203 const filters = filterB.getFilters();
205 const itemLimit = 200;
208 const { items, itemsAvailable } = await services.groupsService.contents((loadShared || searchProjects) ? '' : id, { filters, excludeHomeProject: loadShared || undefined, limit: itemLimit });
209 dispatch<any>(updateResources(items));
211 if (itemsAvailable > itemLimit) {
213 uuid: "more-items-available",
214 kind: ResourceKind.WORKFLOW,
215 name: `*** Not all items listed (${items.length} out of ${itemsAvailable}), reduce item count with search or filter ***`,
220 modifiedByClientUuid: "",
221 modifiedByUserUuid: "",
228 dispatch<any>(receiveTreePickerData<GroupContentsResource>({
231 data: items.filter((item) => {
232 if (!includeFilterGroups && (item as GroupResource).groupClass && (item as GroupResource).groupClass === GroupClass.FILTER) {
236 if (options && options.showOnlyWritable && item.hasOwnProperty('frozenByUuid') && (item as ProjectResource).frozenByUuid) {
242 extractNodeData: extractGroupContentsNodeData(includeDirectories || includeFiles),
245 console.error("Failed to load project into tree picker:", e);;
246 dispatch<any>(snackbarActions.OPEN_SNACKBAR({ message: `Failed to load project`, kind: SnackbarKind.ERROR }));
250 export const loadCollection = (id: string, pickerId: string, includeDirectories?: boolean, includeFiles?: boolean) =>
251 async (dispatch: Dispatch, getState: () => RootState, services: ServiceRepository) => {
252 dispatch(treePickerActions.LOAD_TREE_PICKER_NODE({ id, pickerId }));
254 const picker = getTreePicker<ProjectsTreePickerItem>(pickerId)(getState().treePicker);
257 const node = getNode(id)(picker);
258 if (node && 'kind' in node.value && node.value.kind === ResourceKind.COLLECTION) {
259 const files = (await services.collectionService.files(node.value.uuid))
262 (includeDirectories && file.type === CollectionFileType.DIRECTORY)
264 const tree = createCollectionFilesTree(files);
265 const sorted = sortFilesTree(tree);
266 const filesTree = mapTreeValues(services.collectionService.extendFileURL)(sorted);
268 // await tree modifications so that consumers can guarantee node presence
270 treePickerActions.APPEND_TREE_PICKER_NODE_SUBTREE({
273 subtree: mapTree(node => ({ ...node, status: TreeNodeStatus.LOADED }))(filesTree)
276 // Expand collection root node
277 dispatch(treePickerActions.TOGGLE_TREE_PICKER_NODE_COLLAPSE({ id, pickerId }));
282 export const HOME_PROJECT_ID = 'Home Projects';
283 export const initUserProject = (pickerId: string) =>
284 async (dispatch: Dispatch<any>, getState: () => RootState, services: ServiceRepository) => {
285 const uuid = getUserUuid(getState());
287 dispatch(receiveTreePickerData({
290 data: [{ uuid, name: HOME_PROJECT_ID }],
291 extractNodeData: value => ({
293 status: TreeNodeStatus.INITIAL,
299 export const loadUserProject = (pickerId: string, includeCollections = false, includeDirectories = false, includeFiles = false, options?: { showOnlyOwned: boolean, showOnlyWritable: boolean }) =>
300 async (dispatch: Dispatch<any>, getState: () => RootState, services: ServiceRepository) => {
301 const uuid = getUserUuid(getState());
303 dispatch(loadProject({ id: uuid, pickerId, includeCollections, includeDirectories, includeFiles, options }));
307 export const SHARED_PROJECT_ID = 'Shared with me';
308 export const initSharedProject = (pickerId: string) =>
309 async (dispatch: Dispatch<any>, getState: () => RootState, services: ServiceRepository) => {
310 dispatch(receiveTreePickerData({
313 data: [{ uuid: SHARED_PROJECT_ID, name: SHARED_PROJECT_ID }],
314 extractNodeData: value => ({
316 status: TreeNodeStatus.INITIAL,
322 type PickerItemPreloadData = {
324 mainItemUuid: string;
325 ancestors: (GroupResource | CollectionResource)[];
326 isHomeProjectItem: boolean;
329 type PickerTreePreloadData = {
330 tree: Tree<GroupResource | CollectionResource>;
331 pickerTreeId: string;
332 pickerTreeRootUuid: string;
335 export const loadInitialValue = (pickerItemIds: string[], pickerId: string, includeDirectories: boolean, includeFiles: boolean, multi: boolean,) =>
336 async (dispatch: Dispatch<any>, getState: () => RootState, services: ServiceRepository) => {
337 const homeUuid = getUserUuid(getState());
339 // Request ancestor trees in paralell and save home project status
340 const pickerItemsData: PickerItemPreloadData[] = await Promise.allSettled(pickerItemIds.map(async itemId => {
341 const mainItemUuid = itemId.includes('/') ? itemId.split('/')[0] : itemId;
343 const ancestors = (await services.ancestorsService.ancestors(mainItemUuid, ''))
345 item.kind === ResourceKind.GROUP ||
346 item.kind === ResourceKind.COLLECTION
347 ) as (GroupResource | CollectionResource)[];
349 if (ancestors.length === 0) {
350 return Promise.reject({item: itemId});
353 const isHomeProjectItem = !!(homeUuid && ancestors.some(item => item.ownerUuid === homeUuid));
362 // Show toast if any selections failed to restore
363 const rejectedPromises = res.filter((promiseResult): promiseResult is PromiseRejectedResult => (promiseResult.status === 'rejected'));
364 if (rejectedPromises.length) {
365 rejectedPromises.forEach(item => {
366 console.error("The following item failed to load into the tree picker", item.reason);
368 dispatch<any>(snackbarActions.OPEN_SNACKBAR({ message: `Some selections failed to load and were removed. See console for details.`, kind: SnackbarKind.ERROR }));
370 // Filter out any failed promises and map to resulting preload data with ancestors
371 return res.filter((promiseResult): promiseResult is PromiseFulfilledResult<PickerItemPreloadData> => (
372 promiseResult.status === 'fulfilled'
373 )).map(res => res.value)
376 // Group items to preload / ancestor data by home/shared picker and create initial Trees to preload
377 const initialTreePreloadData: PickerTreePreloadData[] = [
378 pickerItemsData.filter((item) => item.isHomeProjectItem),
379 pickerItemsData.filter((item) => !item.isHomeProjectItem),
381 .filter((items) => items.length > 0)
384 (preloadTree, itemData) => ({
385 tree: createInitialPickerTree(
387 itemData.mainItemUuid,
390 pickerTreeId: getPickerItemTreeId(itemData, homeUuid, pickerId),
391 pickerTreeRootUuid: getPickerItemRootUuid(itemData, homeUuid),
394 tree: createTree<GroupResource | CollectionResource>(),
396 pickerTreeRootUuid: '',
397 } as PickerTreePreloadData
401 // Load initial trees into corresponding picker store
402 await Promise.all(initialTreePreloadData.map(preloadTree => (
404 treePickerActions.APPEND_TREE_PICKER_NODE_SUBTREE({
405 id: preloadTree.pickerTreeRootUuid,
406 pickerId: preloadTree.pickerTreeId,
407 subtree: preloadTree.tree,
412 // Await loading collection before attempting to select items
413 await Promise.all(pickerItemsData.map(async itemData => {
414 const pickerTreeId = getPickerItemTreeId(itemData, homeUuid, pickerId);
416 // Selected item resides in collection subpath
417 if (itemData.itemId.includes('/')) {
418 // Load collection into tree
419 // loadCollection includes more than dispatched actions and must be awaited
420 await dispatch(loadCollection(itemData.mainItemUuid, pickerTreeId, includeDirectories, includeFiles));
422 // Expand nodes down to destination
423 dispatch(treePickerActions.EXPAND_TREE_PICKER_NODE_ANCESTORS({ id: itemData.itemId, pickerId: pickerTreeId }));
426 // Select or activate nodes
427 pickerItemsData.forEach(itemData => {
428 const pickerTreeId = getPickerItemTreeId(itemData, homeUuid, pickerId);
431 dispatch(treePickerActions.SELECT_TREE_PICKER_NODE({ id: itemData.itemId, pickerId: pickerTreeId, cascade: false}));
433 dispatch(treePickerActions.ACTIVATE_TREE_PICKER_NODE({ id: itemData.itemId, pickerId: pickerTreeId }));
437 // Refresh triggers loading in all adjacent items that were not included in the ancestor tree
438 await initialTreePreloadData.map(preloadTree => dispatch(treePickerSearchActions.REFRESH_TREE_PICKER({ pickerId: preloadTree.pickerTreeId })));
441 const getPickerItemTreeId = (itemData: PickerItemPreloadData, homeUuid: string | undefined, pickerId: string) => {
442 const { home, shared } = getProjectsTreePickerIds(pickerId);
443 return ((itemData.isHomeProjectItem && homeUuid) ? home : shared);
446 const getPickerItemRootUuid = (itemData: PickerItemPreloadData, homeUuid: string | undefined) => {
447 return (itemData.isHomeProjectItem && homeUuid) ? homeUuid : SHARED_PROJECT_ID;
450 export const FAVORITES_PROJECT_ID = 'Favorites';
451 export const initFavoritesProject = (pickerId: string) =>
452 async (dispatch: Dispatch<any>, getState: () => RootState, services: ServiceRepository) => {
453 dispatch(receiveTreePickerData({
456 data: [{ uuid: FAVORITES_PROJECT_ID, name: FAVORITES_PROJECT_ID }],
457 extractNodeData: value => ({
459 status: TreeNodeStatus.INITIAL,
465 export const PUBLIC_FAVORITES_PROJECT_ID = 'Public Favorites';
466 export const initPublicFavoritesProject = (pickerId: string) =>
467 async (dispatch: Dispatch<any>, getState: () => RootState, services: ServiceRepository) => {
468 dispatch(receiveTreePickerData({
471 data: [{ uuid: PUBLIC_FAVORITES_PROJECT_ID, name: PUBLIC_FAVORITES_PROJECT_ID }],
472 extractNodeData: value => ({
474 status: TreeNodeStatus.INITIAL,
480 export const SEARCH_PROJECT_ID = 'Search all Projects';
481 export const initSearchProject = (pickerId: string) =>
482 async (dispatch: Dispatch<any>, getState: () => RootState, services: ServiceRepository) => {
483 dispatch(receiveTreePickerData({
486 data: [{ uuid: SEARCH_PROJECT_ID, name: SEARCH_PROJECT_ID }],
487 extractNodeData: value => ({
489 status: TreeNodeStatus.INITIAL,
496 interface LoadFavoritesProjectParams {
498 includeCollections?: boolean;
499 includeDirectories?: boolean;
500 includeFiles?: boolean;
501 options?: { showOnlyOwned: boolean, showOnlyWritable: boolean };
504 export const loadFavoritesProject = (params: LoadFavoritesProjectParams,
505 options: { showOnlyOwned: boolean, showOnlyWritable: boolean } = { showOnlyOwned: true, showOnlyWritable: false }) =>
506 async (dispatch: Dispatch<any>, getState: () => RootState, services: ServiceRepository) => {
507 const { pickerId, includeCollections = false, includeDirectories = false, includeFiles = false } = params;
508 const uuid = getUserUuid(getState());
510 const filters = pipe(
511 (fb: FilterBuilder) => includeCollections
512 ? fb.addIsA('head_uuid', [ResourceKind.PROJECT, ResourceKind.COLLECTION])
513 : fb.addIsA('head_uuid', [ResourceKind.PROJECT]),
514 fb => fb.getFilters(),
515 )(new FilterBuilder());
517 const { items } = await services.favoriteService.list(uuid, { filters }, options.showOnlyOwned);
519 dispatch<any>(receiveTreePickerData<GroupContentsResource>({
522 data: items.filter((item) => {
523 if (options.showOnlyWritable && !(item as GroupResource).canWrite) {
527 if (options.showOnlyWritable && item.hasOwnProperty('frozenByUuid') && (item as ProjectResource).frozenByUuid) {
533 extractNodeData: extractGroupContentsNodeData(includeDirectories || includeFiles),
538 export const loadPublicFavoritesProject = (params: LoadFavoritesProjectParams) =>
539 async (dispatch: Dispatch<any>, getState: () => RootState, services: ServiceRepository) => {
540 const { pickerId, includeCollections = false, includeDirectories = false, includeFiles = false } = params;
541 const uuidPrefix = getState().auth.config.uuidPrefix;
542 const publicProjectUuid = `${uuidPrefix}-j7d0g-publicfavorites`;
544 const filters = pipe(
545 (fb: FilterBuilder) => includeCollections
546 ? fb.addIsA('head_uuid', [ResourceKind.PROJECT, ResourceKind.COLLECTION])
547 : fb.addIsA('head_uuid', [ResourceKind.PROJECT]),
549 .addEqual('link_class', LinkClass.STAR)
550 .addEqual('owner_uuid', publicProjectUuid)
552 )(new FilterBuilder());
554 const { items } = await services.linkService.list({ filters });
556 dispatch<any>(receiveTreePickerData<LinkResource>({
557 id: 'Public Favorites',
559 data: items.filter(item => {
560 if (params.options && params.options.showOnlyWritable && item.hasOwnProperty('frozenByUuid') && (item as any).frozenByUuid) {
566 extractNodeData: item => ({
569 status: item.headKind === ResourceKind.PROJECT
570 ? TreeNodeStatus.INITIAL
571 : includeDirectories || includeFiles
572 ? TreeNodeStatus.INITIAL
573 : TreeNodeStatus.LOADED
578 export const receiveTreePickerProjectsData = (id: string, projects: ProjectResource[], pickerId: string) =>
579 (dispatch: Dispatch, getState: () => RootState, services: ServiceRepository) => {
580 dispatch(treePickerActions.LOAD_TREE_PICKER_NODE_SUCCESS({
582 nodes: projects.map(project => initTreeNode({ id: project.uuid, value: project })),
586 dispatch(treePickerActions.TOGGLE_TREE_PICKER_NODE_COLLAPSE({ id, pickerId }));
589 export const loadProjectTreePickerProjects = (id: string) =>
590 async (dispatch: Dispatch, getState: () => RootState, services: ServiceRepository) => {
591 dispatch(treePickerActions.LOAD_TREE_PICKER_NODE({ id, pickerId: TreePickerId.PROJECTS }));
594 const ownerUuid = id.length === 0 ? getUserUuid(getState()) || '' : id;
595 const { items } = await services.projectService.list(buildParams(ownerUuid));
597 dispatch<any>(receiveTreePickerProjectsData(id, items, TreePickerId.PROJECTS));
600 export const loadFavoriteTreePickerProjects = (id: string) =>
601 async (dispatch: Dispatch, getState: () => RootState, services: ServiceRepository) => {
602 const parentId = getUserUuid(getState()) || '';
605 dispatch(treePickerActions.LOAD_TREE_PICKER_NODE({ id: parentId, pickerId: TreePickerId.FAVORITES }));
606 const { items } = await services.favoriteService.list(parentId);
607 dispatch<any>(receiveTreePickerProjectsData(parentId, items as ProjectResource[], TreePickerId.FAVORITES));
609 dispatch(treePickerActions.LOAD_TREE_PICKER_NODE({ id, pickerId: TreePickerId.FAVORITES }));
610 const { items } = await services.projectService.list(buildParams(id));
611 dispatch<any>(receiveTreePickerProjectsData(id, items, TreePickerId.FAVORITES));
616 export const loadPublicFavoriteTreePickerProjects = (id: string) =>
617 async (dispatch: Dispatch, getState: () => RootState, services: ServiceRepository) => {
618 const parentId = getUserUuid(getState()) || '';
621 dispatch(treePickerActions.LOAD_TREE_PICKER_NODE({ id: parentId, pickerId: TreePickerId.PUBLIC_FAVORITES }));
622 const { items } = await services.favoriteService.list(parentId);
623 dispatch<any>(receiveTreePickerProjectsData(parentId, items as ProjectResource[], TreePickerId.PUBLIC_FAVORITES));
625 dispatch(treePickerActions.LOAD_TREE_PICKER_NODE({ id, pickerId: TreePickerId.PUBLIC_FAVORITES }));
626 const { items } = await services.projectService.list(buildParams(id));
627 dispatch<any>(receiveTreePickerProjectsData(id, items, TreePickerId.PUBLIC_FAVORITES));
632 const buildParams = (ownerUuid: string) => {
634 filters: new FilterBuilder()
635 .addEqual('owner_uuid', ownerUuid)
637 order: new OrderBuilder<ProjectResource>()
644 * Given a tree picker item, return collection uuid and path
645 * if the item represents a valid target/destination location
647 export type FileOperationLocation = {
653 export const getFileOperationLocation = (item: ProjectsTreePickerItem) =>
654 async (dispatch: Dispatch, getState: () => RootState, services: ServiceRepository): Promise<FileOperationLocation | undefined> => {
655 if ('kind' in item && item.kind === ResourceKind.COLLECTION) {
659 pdh: item.portableDataHash,
662 } else if ('type' in item && item.type === CollectionFileType.DIRECTORY) {
663 const uuid = getCollectionResourceCollectionUuid(item.id);
665 const collection = getResource<CollectionResource>(uuid)(getState().resources);
667 const itemPath = [item.path, item.name].join('/');
672 pdh: collection.portableDataHash,
682 * Create an expanded tree picker subtree from array of nested projects/collection
683 * First item is assumed to be root and gets empty parent id
684 * Nodes must be sorted from top down to prevent orphaned nodes
686 export const createInitialPickerTree = (sortedAncestors: Array<GroupResource | CollectionResource>, tailUuid: string, initialTree: Tree<GroupResource | CollectionResource>) => {
687 return sortedAncestors
688 .reduce((tree, item, index) => {
689 if (getNode(item.uuid)(tree)) {
695 parent: index === 0 ? '' : item.ownerUuid,
700 status: item.uuid !== tailUuid ? TreeNodeStatus.LOADED : TreeNodeStatus.INITIAL,
706 export const fileOperationLocationToPickerId = (location: FileOperationLocation): string => {
707 let id = location.uuid;
708 if (location.subpath.length && location.subpath !== '/') {
709 id = id + location.subpath;