Merge branch '14420-tabs-underline-issue'
[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 { Dispatch } from 'redux';
8 import { RootState } from '~/store/store';
9 import { ServiceRepository } from '~/services/services';
10 import { FilterBuilder } from '~/services/api/filter-builder';
11 import { pipe, values } from 'lodash/fp';
12 import { ResourceKind } from '~/models/resource';
13 import { GroupContentsResource } from '~/services/groups-service/groups-service';
14 import { getTreePicker, TreePicker } from './tree-picker';
15 import { ProjectsTreePickerItem } from '~/views-components/projects-tree-picker/generic-projects-tree-picker';
16 import { OrderBuilder } from '~/services/api/order-builder';
17 import { ProjectResource } from '~/models/project';
18 import { mapTree } from '../../models/tree';
19
20 export const treePickerActions = unionize({
21     LOAD_TREE_PICKER_NODE: ofType<{ id: string, pickerId: string }>(),
22     LOAD_TREE_PICKER_NODE_SUCCESS: ofType<{ id: string, nodes: Array<TreeNode<any>>, pickerId: string }>(),
23     APPEND_TREE_PICKER_NODE_SUBTREE: ofType<{ id: string, subtree: Tree<any>, pickerId: string }>(),
24     TOGGLE_TREE_PICKER_NODE_COLLAPSE: ofType<{ id: string, pickerId: string }>(),
25     ACTIVATE_TREE_PICKER_NODE: ofType<{ id: string, pickerId: string, relatedTreePickers?: string[] }>(),
26     DEACTIVATE_TREE_PICKER_NODE: ofType<{ pickerId: string }>(),
27     TOGGLE_TREE_PICKER_NODE_SELECTION: ofType<{ id: string, pickerId: string }>(),
28     SELECT_TREE_PICKER_NODE: ofType<{ id: string | string[], pickerId: string }>(),
29     DESELECT_TREE_PICKER_NODE: ofType<{ id: string | string[], pickerId: string }>(),
30     EXPAND_TREE_PICKER_NODES: ofType<{ ids: string[], pickerId: string }>(),
31     RESET_TREE_PICKER: ofType<{ pickerId: string }>()
32 });
33
34 export type TreePickerAction = UnionOf<typeof treePickerActions>;
35
36 export const getProjectsTreePickerIds = (pickerId: string) => ({
37     home: `${pickerId}_home`,
38     shared: `${pickerId}_shared`,
39     favorites: `${pickerId}_favorites`,
40 });
41
42 export const getAllNodes = <Value>(pickerId: string, filter = (node: TreeNode<Value>) => true) => (state: TreePicker) =>
43     pipe(
44         () => values(getProjectsTreePickerIds(pickerId)),
45
46         ids => ids
47             .map(id => getTreePicker<Value>(id)(state)),
48
49         trees => trees
50             .map(getNodeDescendants(''))
51             .reduce((allNodes, nodes) => allNodes.concat(nodes), []),
52
53         allNodes => allNodes
54             .reduce((map, node) =>
55                 filter(node)
56                     ? map.set(node.id, node)
57                     : map, new Map<string, TreeNode<Value>>())
58             .values(),
59
60         uniqueNodes => Array.from(uniqueNodes),
61     )();
62 export const getSelectedNodes = <Value>(pickerId: string) => (state: TreePicker) =>
63     getAllNodes<Value>(pickerId, node => node.selected)(state);
64
65 export const initProjectsTreePicker = (pickerId: string) =>
66     async (dispatch: Dispatch, _: () => RootState, services: ServiceRepository) => {
67         const { home, shared, favorites } = getProjectsTreePickerIds(pickerId);
68         dispatch<any>(initUserProject(home));
69         dispatch<any>(initSharedProject(shared));
70         dispatch<any>(initFavoritesProject(favorites));
71     };
72
73 interface ReceiveTreePickerDataParams<T> {
74     data: T[];
75     extractNodeData: (value: T) => { id: string, value: T, status?: TreeNodeStatus };
76     id: string;
77     pickerId: string;
78 }
79
80 export const receiveTreePickerData = <T>(params: ReceiveTreePickerDataParams<T>) =>
81     (dispatch: Dispatch) => {
82         const { data, extractNodeData, id, pickerId, } = params;
83         dispatch(treePickerActions.LOAD_TREE_PICKER_NODE_SUCCESS({
84             id,
85             nodes: data.map(item => initTreeNode(extractNodeData(item))),
86             pickerId,
87         }));
88         dispatch(treePickerActions.TOGGLE_TREE_PICKER_NODE_COLLAPSE({ id, pickerId }));
89     };
90
91 interface LoadProjectParams {
92     id: string;
93     pickerId: string;
94     includeCollections?: boolean;
95     includeFiles?: boolean;
96     loadShared?: boolean;
97 }
98 export const loadProject = (params: LoadProjectParams) =>
99     async (dispatch: Dispatch, _: () => RootState, services: ServiceRepository) => {
100         const { id, pickerId, includeCollections = false, includeFiles = false, loadShared = false } = params;
101
102         dispatch(treePickerActions.LOAD_TREE_PICKER_NODE({ id, pickerId }));
103
104         const filters = pipe(
105             (fb: FilterBuilder) => includeCollections
106                 ? fb.addIsA('uuid', [ResourceKind.PROJECT, ResourceKind.COLLECTION])
107                 : fb.addIsA('uuid', [ResourceKind.PROJECT]),
108             fb => fb.getFilters(),
109         )(new FilterBuilder());
110
111         const { items } = await services.groupsService.contents(loadShared ? '' : id, { filters, excludeHomeProject: loadShared || undefined });
112
113         dispatch<any>(receiveTreePickerData<GroupContentsResource>({
114             id,
115             pickerId,
116             data: items,
117             extractNodeData: item => ({
118                 id: item.uuid,
119                 value: item,
120                 status: item.kind === ResourceKind.PROJECT
121                     ? TreeNodeStatus.INITIAL
122                     : includeFiles
123                         ? TreeNodeStatus.INITIAL
124                         : TreeNodeStatus.LOADED
125             }),
126         }));
127     };
128
129 export const loadCollection = (id: string, pickerId: string) =>
130     async (dispatch: Dispatch, getState: () => RootState, services: ServiceRepository) => {
131         dispatch(treePickerActions.LOAD_TREE_PICKER_NODE({ id, pickerId }));
132
133         const picker = getTreePicker<ProjectsTreePickerItem>(pickerId)(getState().treePicker);
134         if (picker) {
135
136             const node = getNode(id)(picker);
137             if (node && 'kind' in node.value && node.value.kind === ResourceKind.COLLECTION) {
138
139                 const filesTree = await services.collectionService.files(node.value.portableDataHash);
140                 
141                 dispatch(
142                     treePickerActions.APPEND_TREE_PICKER_NODE_SUBTREE({
143                         id,
144                         pickerId,
145                         subtree: mapTree(node => ({ ...node, status: TreeNodeStatus.LOADED }))(filesTree)
146                     }));
147
148                 dispatch(treePickerActions.TOGGLE_TREE_PICKER_NODE_COLLAPSE({ id, pickerId }));
149             }
150         }
151     };
152
153
154 export const initUserProject = (pickerId: string) =>
155     async (dispatch: Dispatch<any>, getState: () => RootState, services: ServiceRepository) => {
156         const uuid = services.authService.getUuid();
157         if (uuid) {
158             dispatch(receiveTreePickerData({
159                 id: '',
160                 pickerId,
161                 data: [{ uuid, name: 'Projects' }],
162                 extractNodeData: value => ({
163                     id: value.uuid,
164                     status: TreeNodeStatus.INITIAL,
165                     value,
166                 }),
167             }));
168         }
169     };
170 export const loadUserProject = (pickerId: string, includeCollections = false, includeFiles = false) =>
171     async (dispatch: Dispatch<any>, getState: () => RootState, services: ServiceRepository) => {
172         const uuid = services.authService.getUuid();
173         if (uuid) {
174             dispatch(loadProject({ id: uuid, pickerId, includeCollections, includeFiles }));
175         }
176     };
177
178
179 export const initSharedProject = (pickerId: string) =>
180     async (dispatch: Dispatch<any>, getState: () => RootState, services: ServiceRepository) => {
181         dispatch(receiveTreePickerData({
182             id: '',
183             pickerId,
184             data: [{ uuid: 'Shared with me', name: 'Shared with me' }],
185             extractNodeData: value => ({
186                 id: value.uuid,
187                 status: TreeNodeStatus.INITIAL,
188                 value,
189             }),
190         }));
191     };
192
193 export const initFavoritesProject = (pickerId: string) =>
194     async (dispatch: Dispatch<any>, getState: () => RootState, services: ServiceRepository) => {
195         dispatch(receiveTreePickerData({
196             id: '',
197             pickerId,
198             data: [{ uuid: 'Favorites', name: 'Favorites' }],
199             extractNodeData: value => ({
200                 id: value.uuid,
201                 status: TreeNodeStatus.INITIAL,
202                 value,
203             }),
204         }));
205     };
206
207 interface LoadFavoritesProjectParams {
208     pickerId: string;
209     includeCollections?: boolean;
210     includeFiles?: boolean;
211 }
212
213 export const loadFavoritesProject = (params: LoadFavoritesProjectParams) =>
214     async (dispatch: Dispatch<any>, getState: () => RootState, services: ServiceRepository) => {
215         const { pickerId, includeCollections = false, includeFiles = false } = params;
216         const uuid = services.authService.getUuid();
217         if (uuid) {
218
219             const filters = pipe(
220                 (fb: FilterBuilder) => includeCollections
221                     ? fb.addIsA('headUuid', [ResourceKind.PROJECT, ResourceKind.COLLECTION])
222                     : fb.addIsA('headUuid', [ResourceKind.PROJECT]),
223                 fb => fb.getFilters(),
224             )(new FilterBuilder());
225
226             const { items } = await services.favoriteService.list(uuid, { filters });
227
228             dispatch<any>(receiveTreePickerData<GroupContentsResource>({
229                 id: 'Favorites',
230                 pickerId,
231                 data: items,
232                 extractNodeData: item => ({
233                     id: item.uuid,
234                     value: item,
235                     status: item.kind === ResourceKind.PROJECT
236                         ? TreeNodeStatus.INITIAL
237                         : includeFiles
238                             ? TreeNodeStatus.INITIAL
239                             : TreeNodeStatus.LOADED
240                 }),
241             }));
242         }
243     };
244
245 export const receiveTreePickerProjectsData = (id: string, projects: ProjectResource[], pickerId: string) =>
246     (dispatch: Dispatch, getState: () => RootState, services: ServiceRepository) => {
247         dispatch(treePickerActions.LOAD_TREE_PICKER_NODE_SUCCESS({
248             id,
249             nodes: projects.map(project => initTreeNode({ id: project.uuid, value: project })),
250             pickerId,
251         }));
252
253         dispatch(treePickerActions.TOGGLE_TREE_PICKER_NODE_COLLAPSE({ id, pickerId }));
254     };
255
256 export const loadProjectTreePickerProjects = (id: string) =>
257     async (dispatch: Dispatch, getState: () => RootState, services: ServiceRepository) => {
258         dispatch(treePickerActions.LOAD_TREE_PICKER_NODE({ id, pickerId: TreePickerId.PROJECTS }));
259
260         const ownerUuid = id.length === 0 ? services.authService.getUuid() || '' : id;
261         const { items } = await services.projectService.list(buildParams(ownerUuid));
262
263         dispatch<any>(receiveTreePickerProjectsData(id, items, TreePickerId.PROJECTS));
264     };
265
266 export const loadFavoriteTreePickerProjects = (id: string) =>
267     async (dispatch: Dispatch, getState: () => RootState, services: ServiceRepository) => {
268         const parentId = services.authService.getUuid() || '';
269
270         if (id === '') {
271             dispatch(treePickerActions.LOAD_TREE_PICKER_NODE({ id: parentId, pickerId: TreePickerId.FAVORITES }));
272             const { items } = await services.favoriteService.list(parentId);
273             dispatch<any>(receiveTreePickerProjectsData(parentId, items as ProjectResource[], TreePickerId.FAVORITES));
274         } else {
275             dispatch(treePickerActions.LOAD_TREE_PICKER_NODE({ id, pickerId: TreePickerId.FAVORITES }));
276             const { items } = await services.projectService.list(buildParams(id));
277             dispatch<any>(receiveTreePickerProjectsData(id, items, TreePickerId.FAVORITES));
278         }
279
280     };
281
282 const buildParams = (ownerUuid: string) => {
283     return {
284         filters: new FilterBuilder()
285             .addEqual('ownerUuid', ownerUuid)
286             .getFilters(),
287         order: new OrderBuilder<ProjectResource>()
288             .addAsc('name')
289             .getOrder()
290     };
291 };