17306: Added configurable options for favorite tree
[arvados-workbench2.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 } from '~/models/tree';
7 import { createCollectionFilesTree } 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 '~/views-components/projects-tree-picker/generic-projects-tree-picker';
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 { GroupResource } from "~/models/group";
25
26 export const treePickerActions = unionize({
27     LOAD_TREE_PICKER_NODE: ofType<{ id: string, pickerId: string }>(),
28     LOAD_TREE_PICKER_NODE_SUCCESS: ofType<{ id: string, nodes: Array<TreeNode<any>>, pickerId: string }>(),
29     APPEND_TREE_PICKER_NODE_SUBTREE: ofType<{ id: string, subtree: Tree<any>, pickerId: string }>(),
30     TOGGLE_TREE_PICKER_NODE_COLLAPSE: ofType<{ id: string, pickerId: string }>(),
31     ACTIVATE_TREE_PICKER_NODE: ofType<{ id: string, pickerId: string, relatedTreePickers?: string[] }>(),
32     DEACTIVATE_TREE_PICKER_NODE: ofType<{ pickerId: string }>(),
33     TOGGLE_TREE_PICKER_NODE_SELECTION: ofType<{ id: string, pickerId: string }>(),
34     SELECT_TREE_PICKER_NODE: ofType<{ id: string | string[], pickerId: string }>(),
35     DESELECT_TREE_PICKER_NODE: ofType<{ id: string | string[], pickerId: string }>(),
36     EXPAND_TREE_PICKER_NODES: ofType<{ ids: string[], pickerId: string }>(),
37     RESET_TREE_PICKER: ofType<{ pickerId: string }>()
38 });
39
40 export type TreePickerAction = UnionOf<typeof treePickerActions>;
41
42 export const getProjectsTreePickerIds = (pickerId: string) => ({
43     home: `${pickerId}_home`,
44     shared: `${pickerId}_shared`,
45     favorites: `${pickerId}_favorites`,
46     publicFavorites: `${pickerId}_publicFavorites`
47 });
48
49 export const getAllNodes = <Value>(pickerId: string, filter = (node: TreeNode<Value>) => true) => (state: TreePicker) =>
50     pipe(
51         () => values(getProjectsTreePickerIds(pickerId)),
52
53         ids => ids
54             .map(id => getTreePicker<Value>(id)(state)),
55
56         trees => trees
57             .map(getNodeDescendants(''))
58             .reduce((allNodes, nodes) => allNodes.concat(nodes), []),
59
60         allNodes => allNodes
61             .reduce((map, node) =>
62                 filter(node)
63                     ? map.set(node.id, node)
64                     : map, new Map<string, TreeNode<Value>>())
65             .values(),
66
67         uniqueNodes => Array.from(uniqueNodes),
68     )();
69 export const getSelectedNodes = <Value>(pickerId: string) => (state: TreePicker) =>
70     getAllNodes<Value>(pickerId, node => node.selected)(state);
71
72 export const initProjectsTreePicker = (pickerId: string) =>
73     async (dispatch: Dispatch, _: () => RootState, services: ServiceRepository) => {
74         const { home, shared, favorites, publicFavorites } = getProjectsTreePickerIds(pickerId);
75         dispatch<any>(initUserProject(home));
76         dispatch<any>(initSharedProject(shared));
77         dispatch<any>(initFavoritesProject(favorites));
78         dispatch<any>(initPublicFavoritesProject(publicFavorites));
79     };
80
81 interface ReceiveTreePickerDataParams<T> {
82     data: T[];
83     extractNodeData: (value: T) => { id: string, value: T, status?: TreeNodeStatus };
84     id: string;
85     pickerId: string;
86 }
87
88 export const receiveTreePickerData = <T>(params: ReceiveTreePickerDataParams<T>) =>
89     (dispatch: Dispatch) => {
90         const { data, extractNodeData, id, pickerId, } = params;
91         dispatch(treePickerActions.LOAD_TREE_PICKER_NODE_SUCCESS({
92             id,
93             nodes: data.map(item => initTreeNode(extractNodeData(item))),
94             pickerId,
95         }));
96         dispatch(treePickerActions.TOGGLE_TREE_PICKER_NODE_COLLAPSE({ id, pickerId }));
97     };
98
99 interface LoadProjectParams {
100     id: string;
101     pickerId: string;
102     includeCollections?: boolean;
103     includeFiles?: boolean;
104     loadShared?: boolean;
105 }
106 export const loadProject = (params: LoadProjectParams) =>
107     async (dispatch: Dispatch, _: () => RootState, services: ServiceRepository) => {
108         const { id, pickerId, includeCollections = false, includeFiles = false, loadShared = false } = params;
109
110         dispatch(treePickerActions.LOAD_TREE_PICKER_NODE({ id, pickerId }));
111
112         const filters = pipe(
113             (fb: FilterBuilder) => includeCollections
114                 ? fb.addIsA('uuid', [ResourceKind.PROJECT, ResourceKind.COLLECTION])
115                 : fb.addIsA('uuid', [ResourceKind.PROJECT]),
116             fb => fb.getFilters(),
117         )(new FilterBuilder());
118
119         const { items } = await services.groupsService.contents(loadShared ? '' : id, { filters, excludeHomeProject: loadShared || undefined });
120
121         dispatch<any>(receiveTreePickerData<GroupContentsResource>({
122             id,
123             pickerId,
124             data: items,
125             extractNodeData: item => ({
126                 id: item.uuid,
127                 value: item,
128                 status: item.kind === ResourceKind.PROJECT
129                     ? TreeNodeStatus.INITIAL
130                     : includeFiles
131                         ? TreeNodeStatus.INITIAL
132                         : TreeNodeStatus.LOADED
133             }),
134         }));
135     };
136
137 export const loadCollection = (id: string, pickerId: string) =>
138     async (dispatch: Dispatch, getState: () => RootState, services: ServiceRepository) => {
139         dispatch(treePickerActions.LOAD_TREE_PICKER_NODE({ id, pickerId }));
140
141         const picker = getTreePicker<ProjectsTreePickerItem>(pickerId)(getState().treePicker);
142         if (picker) {
143
144             const node = getNode(id)(picker);
145             if (node && 'kind' in node.value && node.value.kind === ResourceKind.COLLECTION) {
146
147                 const files = await services.collectionService.files(node.value.portableDataHash);
148                 const tree = createCollectionFilesTree(files);
149                 const sorted = sortFilesTree(tree);
150                 const filesTree = mapTreeValues(services.collectionService.extendFileURL)(sorted);
151
152                 dispatch(
153                     treePickerActions.APPEND_TREE_PICKER_NODE_SUBTREE({
154                         id,
155                         pickerId,
156                         subtree: mapTree(node => ({ ...node, status: TreeNodeStatus.LOADED }))(filesTree)
157                     }));
158
159                 dispatch(treePickerActions.TOGGLE_TREE_PICKER_NODE_COLLAPSE({ id, pickerId }));
160             }
161         }
162     };
163
164
165 export const initUserProject = (pickerId: string) =>
166     async (dispatch: Dispatch<any>, getState: () => RootState, services: ServiceRepository) => {
167         const uuid = getUserUuid(getState());
168         if (uuid) {
169             dispatch(receiveTreePickerData({
170                 id: '',
171                 pickerId,
172                 data: [{ uuid, name: 'Projects' }],
173                 extractNodeData: value => ({
174                     id: value.uuid,
175                     status: TreeNodeStatus.INITIAL,
176                     value,
177                 }),
178             }));
179         }
180     };
181 export const loadUserProject = (pickerId: string, includeCollections = false, includeFiles = false) =>
182     async (dispatch: Dispatch<any>, getState: () => RootState, services: ServiceRepository) => {
183         const uuid = getUserUuid(getState());
184         if (uuid) {
185             dispatch(loadProject({ id: uuid, pickerId, includeCollections, includeFiles }));
186         }
187     };
188
189 export const SHARED_PROJECT_ID = 'Shared with me';
190 export const initSharedProject = (pickerId: string) =>
191     async (dispatch: Dispatch<any>, getState: () => RootState, services: ServiceRepository) => {
192         dispatch(receiveTreePickerData({
193             id: '',
194             pickerId,
195             data: [{ uuid: SHARED_PROJECT_ID, name: SHARED_PROJECT_ID }],
196             extractNodeData: value => ({
197                 id: value.uuid,
198                 status: TreeNodeStatus.INITIAL,
199                 value,
200             }),
201         }));
202     };
203
204 export const FAVORITES_PROJECT_ID = 'Favorites';
205 export const initFavoritesProject = (pickerId: string) =>
206     async (dispatch: Dispatch<any>, getState: () => RootState, services: ServiceRepository) => {
207         dispatch(receiveTreePickerData({
208             id: '',
209             pickerId,
210             data: [{ uuid: FAVORITES_PROJECT_ID, name: FAVORITES_PROJECT_ID }],
211             extractNodeData: value => ({
212                 id: value.uuid,
213                 status: TreeNodeStatus.INITIAL,
214                 value,
215             }),
216         }));
217     };
218
219 export const PUBLIC_FAVORITES_PROJECT_ID = 'Public Favorites';
220 export const initPublicFavoritesProject = (pickerId: string) =>
221     async (dispatch: Dispatch<any>, getState: () => RootState, services: ServiceRepository) => {
222         dispatch(receiveTreePickerData({
223             id: '',
224             pickerId,
225             data: [{ uuid: PUBLIC_FAVORITES_PROJECT_ID, name: PUBLIC_FAVORITES_PROJECT_ID }],
226             extractNodeData: value => ({
227                 id: value.uuid,
228                 status: TreeNodeStatus.INITIAL,
229                 value,
230             }),
231         }));
232     };
233
234 interface LoadFavoritesProjectParams {
235     pickerId: string;
236     includeCollections?: boolean;
237     includeFiles?: boolean;
238 }
239
240 export const loadFavoritesProject = (params: LoadFavoritesProjectParams,
241     options: { showOnlyOwned: boolean, showOnlyWritable: boolean } = { showOnlyOwned: true, showOnlyWritable: false }) =>
242     async (dispatch: Dispatch<any>, getState: () => RootState, services: ServiceRepository) => {
243         const { pickerId, includeCollections = false, includeFiles = false } = params;
244         const uuid = getUserUuid(getState());
245         if (uuid) {
246             const filters = pipe(
247                 (fb: FilterBuilder) => includeCollections
248                     ? fb.addIsA('head_uuid', [ResourceKind.PROJECT, ResourceKind.COLLECTION])
249                     : fb.addIsA('head_uuid', [ResourceKind.PROJECT]),
250                 fb => fb.getFilters(),
251             )(new FilterBuilder());
252
253             const { items } = await services.favoriteService.list(uuid, { filters }, options.showOnlyOwned);
254
255             dispatch<any>(receiveTreePickerData<GroupContentsResource>({
256                 id: 'Favorites',
257                 pickerId,
258                 data: items.filter((item) => {
259                     if (options.showOnlyWritable && (item as GroupResource).writableBy && (item as GroupResource).writableBy.indexOf(uuid) === -1) {
260                         return false;
261                     }
262
263                     return true;
264                 }),
265                 extractNodeData: item => ({
266                     id: item.uuid,
267                     value: item,
268                     status: item.kind === ResourceKind.PROJECT
269                         ? TreeNodeStatus.INITIAL
270                         : includeFiles
271                             ? TreeNodeStatus.INITIAL
272                             : TreeNodeStatus.LOADED
273                 }),
274             }));
275         }
276     };
277
278 export const loadPublicFavoritesProject = (params: LoadFavoritesProjectParams) =>
279     async (dispatch: Dispatch<any>, getState: () => RootState, services: ServiceRepository) => {
280         const { pickerId, includeCollections = false, includeFiles = false } = params;
281         const uuidPrefix = getState().auth.config.uuidPrefix;
282         const publicProjectUuid = `${uuidPrefix}-j7d0g-publicfavorites`;
283
284         const filters = pipe(
285             (fb: FilterBuilder) => includeCollections
286                 ? fb.addIsA('head_uuid', [ResourceKind.PROJECT, ResourceKind.COLLECTION])
287                 : fb.addIsA('head_uuid', [ResourceKind.PROJECT]),
288             fb => fb
289                 .addEqual('link_class', LinkClass.STAR)
290                 .addEqual('owner_uuid', publicProjectUuid)
291                 .getFilters(),
292         )(new FilterBuilder());
293
294         const { items } = await services.linkService.list({ filters });
295
296         dispatch<any>(receiveTreePickerData<LinkResource>({
297             id: 'Public Favorites',
298             pickerId,
299             data: items,
300             extractNodeData: item => ({
301                 id: item.headUuid,
302                 value: item,
303                 status: item.headKind === ResourceKind.PROJECT
304                     ? TreeNodeStatus.INITIAL
305                     : includeFiles
306                         ? TreeNodeStatus.INITIAL
307                         : TreeNodeStatus.LOADED
308             }),
309         }));
310     };
311
312 export const receiveTreePickerProjectsData = (id: string, projects: ProjectResource[], pickerId: string) =>
313     (dispatch: Dispatch, getState: () => RootState, services: ServiceRepository) => {
314         dispatch(treePickerActions.LOAD_TREE_PICKER_NODE_SUCCESS({
315             id,
316             nodes: projects.map(project => initTreeNode({ id: project.uuid, value: project })),
317             pickerId,
318         }));
319
320         dispatch(treePickerActions.TOGGLE_TREE_PICKER_NODE_COLLAPSE({ id, pickerId }));
321     };
322
323 export const loadProjectTreePickerProjects = (id: string) =>
324     async (dispatch: Dispatch, getState: () => RootState, services: ServiceRepository) => {
325         dispatch(treePickerActions.LOAD_TREE_PICKER_NODE({ id, pickerId: TreePickerId.PROJECTS }));
326
327
328         const ownerUuid = id.length === 0 ? getUserUuid(getState()) || '' : id;
329         const { items } = await services.projectService.list(buildParams(ownerUuid));
330
331         dispatch<any>(receiveTreePickerProjectsData(id, items, TreePickerId.PROJECTS));
332     };
333
334 export const loadFavoriteTreePickerProjects = (id: string) =>
335     async (dispatch: Dispatch, getState: () => RootState, services: ServiceRepository) => {
336         const parentId = getUserUuid(getState()) || '';
337
338         if (id === '') {
339             dispatch(treePickerActions.LOAD_TREE_PICKER_NODE({ id: parentId, pickerId: TreePickerId.FAVORITES }));
340             const { items } = await services.favoriteService.list(parentId);
341             dispatch<any>(receiveTreePickerProjectsData(parentId, items as ProjectResource[], TreePickerId.FAVORITES));
342         } else {
343             dispatch(treePickerActions.LOAD_TREE_PICKER_NODE({ id, pickerId: TreePickerId.FAVORITES }));
344             const { items } = await services.projectService.list(buildParams(id));
345             dispatch<any>(receiveTreePickerProjectsData(id, items, TreePickerId.FAVORITES));
346         }
347
348     };
349
350 export const loadPublicFavoriteTreePickerProjects = (id: string) =>
351     async (dispatch: Dispatch, getState: () => RootState, services: ServiceRepository) => {
352         const parentId = getUserUuid(getState()) || '';
353
354         if (id === '') {
355             dispatch(treePickerActions.LOAD_TREE_PICKER_NODE({ id: parentId, pickerId: TreePickerId.PUBLIC_FAVORITES }));
356             const { items } = await services.favoriteService.list(parentId);
357             dispatch<any>(receiveTreePickerProjectsData(parentId, items as ProjectResource[], TreePickerId.PUBLIC_FAVORITES));
358         } else {
359             dispatch(treePickerActions.LOAD_TREE_PICKER_NODE({ id, pickerId: TreePickerId.PUBLIC_FAVORITES }));
360             const { items } = await services.projectService.list(buildParams(id));
361             dispatch<any>(receiveTreePickerProjectsData(id, items, TreePickerId.PUBLIC_FAVORITES));
362         }
363
364     };
365
366 const buildParams = (ownerUuid: string) => {
367     return {
368         filters: new FilterBuilder()
369             .addEqual('owner_uuid', ownerUuid)
370             .getFilters(),
371         order: new OrderBuilder<ProjectResource>()
372             .addAsc('name')
373             .getOrder()
374     };
375 };