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