Merge branch 'master' of git.curoverse.com:arvados-workbench2 into 14433_properties_i...
[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 export const SHARED_PROJECT_ID = 'Shared with me';
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_PROJECT_ID, name: SHARED_PROJECT_ID }],
185             extractNodeData: value => ({
186                 id: value.uuid,
187                 status: TreeNodeStatus.INITIAL,
188                 value,
189             }),
190         }));
191     };
192
193 export const FAVORITES_PROJECT_ID = 'Favorites';
194 export const initFavoritesProject = (pickerId: string) =>
195     async (dispatch: Dispatch<any>, getState: () => RootState, services: ServiceRepository) => {
196         dispatch(receiveTreePickerData({
197             id: '',
198             pickerId,
199             data: [{ uuid: FAVORITES_PROJECT_ID, name: FAVORITES_PROJECT_ID }],
200             extractNodeData: value => ({
201                 id: value.uuid,
202                 status: TreeNodeStatus.INITIAL,
203                 value,
204             }),
205         }));
206     };
207
208 interface LoadFavoritesProjectParams {
209     pickerId: string;
210     includeCollections?: boolean;
211     includeFiles?: boolean;
212 }
213
214 export const loadFavoritesProject = (params: LoadFavoritesProjectParams) =>
215     async (dispatch: Dispatch<any>, getState: () => RootState, services: ServiceRepository) => {
216         const { pickerId, includeCollections = false, includeFiles = false } = params;
217         const uuid = services.authService.getUuid();
218         if (uuid) {
219
220             const filters = pipe(
221                 (fb: FilterBuilder) => includeCollections
222                     ? fb.addIsA('headUuid', [ResourceKind.PROJECT, ResourceKind.COLLECTION])
223                     : fb.addIsA('headUuid', [ResourceKind.PROJECT]),
224                 fb => fb.getFilters(),
225             )(new FilterBuilder());
226
227             const { items } = await services.favoriteService.list(uuid, { filters });
228
229             dispatch<any>(receiveTreePickerData<GroupContentsResource>({
230                 id: 'Favorites',
231                 pickerId,
232                 data: items,
233                 extractNodeData: item => ({
234                     id: item.uuid,
235                     value: item,
236                     status: item.kind === ResourceKind.PROJECT
237                         ? TreeNodeStatus.INITIAL
238                         : includeFiles
239                             ? TreeNodeStatus.INITIAL
240                             : TreeNodeStatus.LOADED
241                 }),
242             }));
243         }
244     };
245
246 export const receiveTreePickerProjectsData = (id: string, projects: ProjectResource[], pickerId: string) =>
247     (dispatch: Dispatch, getState: () => RootState, services: ServiceRepository) => {
248         dispatch(treePickerActions.LOAD_TREE_PICKER_NODE_SUCCESS({
249             id,
250             nodes: projects.map(project => initTreeNode({ id: project.uuid, value: project })),
251             pickerId,
252         }));
253
254         dispatch(treePickerActions.TOGGLE_TREE_PICKER_NODE_COLLAPSE({ id, pickerId }));
255     };
256
257 export const loadProjectTreePickerProjects = (id: string) =>
258     async (dispatch: Dispatch, getState: () => RootState, services: ServiceRepository) => {
259         dispatch(treePickerActions.LOAD_TREE_PICKER_NODE({ id, pickerId: TreePickerId.PROJECTS }));
260
261         const ownerUuid = id.length === 0 ? services.authService.getUuid() || '' : id;
262         const { items } = await services.projectService.list(buildParams(ownerUuid));
263
264         dispatch<any>(receiveTreePickerProjectsData(id, items, TreePickerId.PROJECTS));
265     };
266
267 export const loadFavoriteTreePickerProjects = (id: string) =>
268     async (dispatch: Dispatch, getState: () => RootState, services: ServiceRepository) => {
269         const parentId = services.authService.getUuid() || '';
270
271         if (id === '') {
272             dispatch(treePickerActions.LOAD_TREE_PICKER_NODE({ id: parentId, pickerId: TreePickerId.FAVORITES }));
273             const { items } = await services.favoriteService.list(parentId);
274             dispatch<any>(receiveTreePickerProjectsData(parentId, items as ProjectResource[], TreePickerId.FAVORITES));
275         } else {
276             dispatch(treePickerActions.LOAD_TREE_PICKER_NODE({ id, pickerId: TreePickerId.FAVORITES }));
277             const { items } = await services.projectService.list(buildParams(id));
278             dispatch<any>(receiveTreePickerProjectsData(id, items, TreePickerId.FAVORITES));
279         }
280
281     };
282
283 const buildParams = (ownerUuid: string) => {
284     return {
285         filters: new FilterBuilder()
286             .addEqual('ownerUuid', ownerUuid)
287             .getFilters(),
288         order: new OrderBuilder<ProjectResource>()
289             .addAsc('name')
290             .getOrder()
291     };
292 };