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 } 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 import { LinkResource, LinkClass } from "~/models/link";
21 export const treePickerActions = unionize({
22 LOAD_TREE_PICKER_NODE: ofType<{ id: string, pickerId: string }>(),
23 LOAD_TREE_PICKER_NODE_SUCCESS: ofType<{ id: string, nodes: Array<TreeNode<any>>, pickerId: string }>(),
24 APPEND_TREE_PICKER_NODE_SUBTREE: ofType<{ id: string, subtree: Tree<any>, pickerId: string }>(),
25 TOGGLE_TREE_PICKER_NODE_COLLAPSE: ofType<{ id: string, pickerId: string }>(),
26 ACTIVATE_TREE_PICKER_NODE: ofType<{ id: string, pickerId: string, relatedTreePickers?: string[] }>(),
27 DEACTIVATE_TREE_PICKER_NODE: ofType<{ pickerId: string }>(),
28 TOGGLE_TREE_PICKER_NODE_SELECTION: ofType<{ id: string, pickerId: string }>(),
29 SELECT_TREE_PICKER_NODE: ofType<{ id: string | string[], pickerId: string }>(),
30 DESELECT_TREE_PICKER_NODE: ofType<{ id: string | string[], pickerId: string }>(),
31 EXPAND_TREE_PICKER_NODES: ofType<{ ids: string[], pickerId: string }>(),
32 RESET_TREE_PICKER: ofType<{ pickerId: string }>()
35 export type TreePickerAction = UnionOf<typeof treePickerActions>;
37 export const getProjectsTreePickerIds = (pickerId: string) => ({
38 home: `${pickerId}_home`,
39 shared: `${pickerId}_shared`,
40 favorites: `${pickerId}_favorites`,
41 publicFavorites: `${pickerId}_publicFavorites`
44 export const getAllNodes = <Value>(pickerId: string, filter = (node: TreeNode<Value>) => true) => (state: TreePicker) =>
46 () => values(getProjectsTreePickerIds(pickerId)),
49 .map(id => getTreePicker<Value>(id)(state)),
52 .map(getNodeDescendants(''))
53 .reduce((allNodes, nodes) => allNodes.concat(nodes), []),
56 .reduce((map, node) =>
58 ? map.set(node.id, node)
59 : map, new Map<string, TreeNode<Value>>())
62 uniqueNodes => Array.from(uniqueNodes),
64 export const getSelectedNodes = <Value>(pickerId: string) => (state: TreePicker) =>
65 getAllNodes<Value>(pickerId, node => node.selected)(state);
67 export const initProjectsTreePicker = (pickerId: string) =>
68 async (dispatch: Dispatch, _: () => RootState, services: ServiceRepository) => {
69 const { home, shared, favorites, publicFavorites } = getProjectsTreePickerIds(pickerId);
70 dispatch<any>(initUserProject(home));
71 dispatch<any>(initSharedProject(shared));
72 dispatch<any>(initFavoritesProject(favorites));
73 dispatch<any>(initPublicFavoritesProject(publicFavorites));
76 interface ReceiveTreePickerDataParams<T> {
78 extractNodeData: (value: T) => { id: string, value: T, status?: TreeNodeStatus };
83 export const receiveTreePickerData = <T>(params: ReceiveTreePickerDataParams<T>) =>
84 (dispatch: Dispatch) => {
85 const { data, extractNodeData, id, pickerId, } = params;
86 dispatch(treePickerActions.LOAD_TREE_PICKER_NODE_SUCCESS({
88 nodes: data.map(item => initTreeNode(extractNodeData(item))),
91 dispatch(treePickerActions.TOGGLE_TREE_PICKER_NODE_COLLAPSE({ id, pickerId }));
94 interface LoadProjectParams {
97 includeCollections?: boolean;
98 includeFiles?: boolean;
101 export const loadProject = (params: LoadProjectParams) =>
102 async (dispatch: Dispatch, _: () => RootState, services: ServiceRepository) => {
103 const { id, pickerId, includeCollections = false, includeFiles = false, loadShared = false } = params;
105 dispatch(treePickerActions.LOAD_TREE_PICKER_NODE({ id, pickerId }));
107 const filters = pipe(
108 (fb: FilterBuilder) => includeCollections
109 ? fb.addIsA('uuid', [ResourceKind.PROJECT, ResourceKind.COLLECTION])
110 : fb.addIsA('uuid', [ResourceKind.PROJECT]),
111 fb => fb.getFilters(),
112 )(new FilterBuilder());
114 const { items } = await services.groupsService.contents(loadShared ? '' : id, { filters, excludeHomeProject: loadShared || undefined });
116 dispatch<any>(receiveTreePickerData<GroupContentsResource>({
120 extractNodeData: item => ({
123 status: item.kind === ResourceKind.PROJECT
124 ? TreeNodeStatus.INITIAL
126 ? TreeNodeStatus.INITIAL
127 : TreeNodeStatus.LOADED
132 export const loadCollection = (id: string, pickerId: string) =>
133 async (dispatch: Dispatch, getState: () => RootState, services: ServiceRepository) => {
134 dispatch(treePickerActions.LOAD_TREE_PICKER_NODE({ id, pickerId }));
136 const picker = getTreePicker<ProjectsTreePickerItem>(pickerId)(getState().treePicker);
139 const node = getNode(id)(picker);
140 if (node && 'kind' in node.value && node.value.kind === ResourceKind.COLLECTION) {
142 const filesTree = await services.collectionService.files(node.value.portableDataHash);
145 treePickerActions.APPEND_TREE_PICKER_NODE_SUBTREE({
148 subtree: mapTree(node => ({ ...node, status: TreeNodeStatus.LOADED }))(filesTree)
151 dispatch(treePickerActions.TOGGLE_TREE_PICKER_NODE_COLLAPSE({ id, pickerId }));
157 export const initUserProject = (pickerId: string) =>
158 async (dispatch: Dispatch<any>, getState: () => RootState, services: ServiceRepository) => {
159 const uuid = services.authService.getUuid();
161 dispatch(receiveTreePickerData({
164 data: [{ uuid, name: 'Projects' }],
165 extractNodeData: value => ({
167 status: TreeNodeStatus.INITIAL,
173 export const loadUserProject = (pickerId: string, includeCollections = false, includeFiles = false) =>
174 async (dispatch: Dispatch<any>, getState: () => RootState, services: ServiceRepository) => {
175 const uuid = services.authService.getUuid();
177 dispatch(loadProject({ id: uuid, pickerId, includeCollections, includeFiles }));
181 export const SHARED_PROJECT_ID = 'Shared with me';
182 export const initSharedProject = (pickerId: string) =>
183 async (dispatch: Dispatch<any>, getState: () => RootState, services: ServiceRepository) => {
184 dispatch(receiveTreePickerData({
187 data: [{ uuid: SHARED_PROJECT_ID, name: SHARED_PROJECT_ID }],
188 extractNodeData: value => ({
190 status: TreeNodeStatus.INITIAL,
196 export const FAVORITES_PROJECT_ID = 'Favorites';
197 export const initFavoritesProject = (pickerId: string) =>
198 async (dispatch: Dispatch<any>, getState: () => RootState, services: ServiceRepository) => {
199 dispatch(receiveTreePickerData({
202 data: [{ uuid: FAVORITES_PROJECT_ID, name: FAVORITES_PROJECT_ID }],
203 extractNodeData: value => ({
205 status: TreeNodeStatus.INITIAL,
211 export const PUBLIC_FAVORITES_PROJECT_ID = 'Public Favorites';
212 export const initPublicFavoritesProject = (pickerId: string) =>
213 async (dispatch: Dispatch<any>, getState: () => RootState, services: ServiceRepository) => {
214 dispatch(receiveTreePickerData({
217 data: [{ uuid: PUBLIC_FAVORITES_PROJECT_ID, name: PUBLIC_FAVORITES_PROJECT_ID }],
218 extractNodeData: value => ({
220 status: TreeNodeStatus.INITIAL,
226 interface LoadFavoritesProjectParams {
228 includeCollections?: boolean;
229 includeFiles?: boolean;
232 export const loadFavoritesProject = (params: LoadFavoritesProjectParams) =>
233 async (dispatch: Dispatch<any>, getState: () => RootState, services: ServiceRepository) => {
234 const { pickerId, includeCollections = false, includeFiles = false } = params;
235 const uuid = services.authService.getUuid();
238 const filters = pipe(
239 (fb: FilterBuilder) => includeCollections
240 ? fb.addIsA('headUuid', [ResourceKind.PROJECT, ResourceKind.COLLECTION])
241 : fb.addIsA('headUuid', [ResourceKind.PROJECT]),
242 fb => fb.getFilters(),
243 )(new FilterBuilder());
245 const { items } = await services.favoriteService.list(uuid, { filters });
247 dispatch<any>(receiveTreePickerData<GroupContentsResource>({
251 extractNodeData: item => ({
254 status: item.kind === ResourceKind.PROJECT
255 ? TreeNodeStatus.INITIAL
257 ? TreeNodeStatus.INITIAL
258 : TreeNodeStatus.LOADED
264 export const loadPublicFavoritesProject = (params: LoadFavoritesProjectParams) =>
265 async (dispatch: Dispatch<any>, getState: () => RootState, services: ServiceRepository) => {
266 const { pickerId, includeCollections = false, includeFiles = false } = params;
267 const uuidPrefix = getState().config.uuidPrefix;
268 const uuid = `${uuidPrefix}-j7d0g-fffffffffffffff`;
271 const filters = pipe(
272 (fb: FilterBuilder) => includeCollections
273 ? fb.addIsA('headUuid', [ResourceKind.PROJECT, ResourceKind.COLLECTION])
274 : fb.addIsA('headUuid', [ResourceKind.PROJECT]),
276 .addEqual('linkClass', LinkClass.STAR)
277 .addEqual('ownerUuid', uuid)
280 )(new FilterBuilder());
282 const { items } = await services.linkService.list({ filters });
284 dispatch<any>(receiveTreePickerData<LinkResource>({
285 id: 'Public Favorites',
288 extractNodeData: item => ({
291 status: item.headKind === ResourceKind.PROJECT
292 ? TreeNodeStatus.INITIAL
294 ? TreeNodeStatus.INITIAL
295 : TreeNodeStatus.LOADED
301 export const receiveTreePickerProjectsData = (id: string, projects: ProjectResource[], pickerId: string) =>
302 (dispatch: Dispatch, getState: () => RootState, services: ServiceRepository) => {
303 dispatch(treePickerActions.LOAD_TREE_PICKER_NODE_SUCCESS({
305 nodes: projects.map(project => initTreeNode({ id: project.uuid, value: project })),
309 dispatch(treePickerActions.TOGGLE_TREE_PICKER_NODE_COLLAPSE({ id, pickerId }));
312 export const loadProjectTreePickerProjects = (id: string) =>
313 async (dispatch: Dispatch, getState: () => RootState, services: ServiceRepository) => {
314 dispatch(treePickerActions.LOAD_TREE_PICKER_NODE({ id, pickerId: TreePickerId.PROJECTS }));
316 const ownerUuid = id.length === 0 ? services.authService.getUuid() || '' : id;
317 const { items } = await services.projectService.list(buildParams(ownerUuid));
319 dispatch<any>(receiveTreePickerProjectsData(id, items, TreePickerId.PROJECTS));
322 export const loadFavoriteTreePickerProjects = (id: string) =>
323 async (dispatch: Dispatch, getState: () => RootState, services: ServiceRepository) => {
324 const parentId = services.authService.getUuid() || '';
327 dispatch(treePickerActions.LOAD_TREE_PICKER_NODE({ id: parentId, pickerId: TreePickerId.FAVORITES }));
328 const { items } = await services.favoriteService.list(parentId);
329 dispatch<any>(receiveTreePickerProjectsData(parentId, items as ProjectResource[], TreePickerId.FAVORITES));
331 dispatch(treePickerActions.LOAD_TREE_PICKER_NODE({ id, pickerId: TreePickerId.FAVORITES }));
332 const { items } = await services.projectService.list(buildParams(id));
333 dispatch<any>(receiveTreePickerProjectsData(id, items, TreePickerId.FAVORITES));
338 export const loadPublicFavoriteTreePickerProjects = (id: string) =>
339 async (dispatch: Dispatch, getState: () => RootState, services: ServiceRepository) => {
340 const parentId = services.authService.getUuid() || '';
343 dispatch(treePickerActions.LOAD_TREE_PICKER_NODE({ id: parentId, pickerId: TreePickerId.PUBLIC_FAVORITES }));
344 const { items } = await services.favoriteService.list(parentId);
345 dispatch<any>(receiveTreePickerProjectsData(parentId, items as ProjectResource[], TreePickerId.PUBLIC_FAVORITES));
347 dispatch(treePickerActions.LOAD_TREE_PICKER_NODE({ id, pickerId: TreePickerId.PUBLIC_FAVORITES }));
348 const { items } = await services.projectService.list(buildParams(id));
349 dispatch<any>(receiveTreePickerProjectsData(id, items, TreePickerId.PUBLIC_FAVORITES));
354 const buildParams = (ownerUuid: string) => {
356 filters: new FilterBuilder()
357 .addEqual('ownerUuid', ownerUuid)
359 order: new OrderBuilder<ProjectResource>()