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 { 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";
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 }>()
39 export type TreePickerAction = UnionOf<typeof treePickerActions>;
41 export const getProjectsTreePickerIds = (pickerId: string) => ({
42 home: `${pickerId}_home`,
43 shared: `${pickerId}_shared`,
44 favorites: `${pickerId}_favorites`,
45 publicFavorites: `${pickerId}_publicFavorites`
48 export const getAllNodes = <Value>(pickerId: string, filter = (node: TreeNode<Value>) => true) => (state: TreePicker) =>
50 () => values(getProjectsTreePickerIds(pickerId)),
53 .map(id => getTreePicker<Value>(id)(state)),
56 .map(getNodeDescendants(''))
57 .reduce((allNodes, nodes) => allNodes.concat(nodes), []),
60 .reduce((map, node) =>
62 ? map.set(node.id, node)
63 : map, new Map<string, TreeNode<Value>>())
66 uniqueNodes => Array.from(uniqueNodes),
68 export const getSelectedNodes = <Value>(pickerId: string) => (state: TreePicker) =>
69 getAllNodes<Value>(pickerId, node => node.selected)(state);
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));
80 interface ReceiveTreePickerDataParams<T> {
82 extractNodeData: (value: T) => { id: string, value: T, status?: TreeNodeStatus };
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({
92 nodes: data.map(item => initTreeNode(extractNodeData(item))),
95 dispatch(treePickerActions.TOGGLE_TREE_PICKER_NODE_COLLAPSE({ id, pickerId }));
98 interface LoadProjectParams {
101 includeCollections?: boolean;
102 includeFiles?: boolean;
103 loadShared?: boolean;
105 export const loadProject = (params: LoadProjectParams) =>
106 async (dispatch: Dispatch, _: () => RootState, services: ServiceRepository) => {
107 const { id, pickerId, includeCollections = false, includeFiles = false, loadShared = false } = params;
109 dispatch(treePickerActions.LOAD_TREE_PICKER_NODE({ id, pickerId }));
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());
118 const { items } = await services.groupsService.contents(loadShared ? '' : id, { filters, excludeHomeProject: loadShared || undefined });
120 dispatch<any>(receiveTreePickerData<GroupContentsResource>({
124 extractNodeData: item => ({
127 status: item.kind === ResourceKind.PROJECT
128 ? TreeNodeStatus.INITIAL
130 ? TreeNodeStatus.INITIAL
131 : TreeNodeStatus.LOADED
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 }));
140 const picker = getTreePicker<ProjectsTreePickerItem>(pickerId)(getState().treePicker);
143 const node = getNode(id)(picker);
144 if (node && 'kind' in node.value && node.value.kind === ResourceKind.COLLECTION) {
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);
152 treePickerActions.APPEND_TREE_PICKER_NODE_SUBTREE({
155 subtree: mapTree(node => ({ ...node, status: TreeNodeStatus.LOADED }))(filesTree)
158 dispatch(treePickerActions.TOGGLE_TREE_PICKER_NODE_COLLAPSE({ id, pickerId }));
164 export const initUserProject = (pickerId: string) =>
165 async (dispatch: Dispatch<any>, getState: () => RootState, services: ServiceRepository) => {
166 const uuid = getUserUuid(getState());
168 dispatch(receiveTreePickerData({
171 data: [{ uuid, name: 'Projects' }],
172 extractNodeData: value => ({
174 status: TreeNodeStatus.INITIAL,
180 export const loadUserProject = (pickerId: string, includeCollections = false, includeFiles = false) =>
181 async (dispatch: Dispatch<any>, getState: () => RootState, services: ServiceRepository) => {
182 const uuid = getUserUuid(getState());
184 dispatch(loadProject({ id: uuid, pickerId, includeCollections, includeFiles }));
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({
194 data: [{ uuid: SHARED_PROJECT_ID, name: SHARED_PROJECT_ID }],
195 extractNodeData: value => ({
197 status: TreeNodeStatus.INITIAL,
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({
209 data: [{ uuid: FAVORITES_PROJECT_ID, name: FAVORITES_PROJECT_ID }],
210 extractNodeData: value => ({
212 status: TreeNodeStatus.INITIAL,
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({
224 data: [{ uuid: PUBLIC_FAVORITES_PROJECT_ID, name: PUBLIC_FAVORITES_PROJECT_ID }],
225 extractNodeData: value => ({
227 status: TreeNodeStatus.INITIAL,
233 interface LoadFavoritesProjectParams {
235 includeCollections?: boolean;
236 includeFiles?: boolean;
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());
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());
251 const { items } = await services.favoriteService.list(uuid, { filters });
253 dispatch<any>(receiveTreePickerData<GroupContentsResource>({
257 extractNodeData: item => ({
260 status: item.kind === ResourceKind.PROJECT
261 ? TreeNodeStatus.INITIAL
263 ? TreeNodeStatus.INITIAL
264 : TreeNodeStatus.LOADED
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`;
277 const filters = pipe(
278 (fb: FilterBuilder) => includeCollections
279 ? fb.addIsA('head_uuid', [ResourceKind.PROJECT, ResourceKind.COLLECTION])
280 : fb.addIsA('head_uuid', [ResourceKind.PROJECT]),
282 .addEqual('link_class', LinkClass.STAR)
283 .addEqual('owner_uuid', uuid)
285 )(new FilterBuilder());
287 const { items } = await services.linkService.list({ filters });
289 dispatch<any>(receiveTreePickerData<LinkResource>({
290 id: 'Public Favorites',
293 extractNodeData: item => ({
296 status: item.headKind === ResourceKind.PROJECT
297 ? TreeNodeStatus.INITIAL
299 ? TreeNodeStatus.INITIAL
300 : TreeNodeStatus.LOADED
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({
310 nodes: projects.map(project => initTreeNode({ id: project.uuid, value: project })),
314 dispatch(treePickerActions.TOGGLE_TREE_PICKER_NODE_COLLAPSE({ id, pickerId }));
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 }));
322 const ownerUuid = id.length === 0 ? getUserUuid(getState()) || '' : id;
323 const { items } = await services.projectService.list(buildParams(ownerUuid));
325 dispatch<any>(receiveTreePickerProjectsData(id, items, TreePickerId.PROJECTS));
328 export const loadFavoriteTreePickerProjects = (id: string) =>
329 async (dispatch: Dispatch, getState: () => RootState, services: ServiceRepository) => {
330 const parentId = getUserUuid(getState()) || '';
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));
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));
344 export const loadPublicFavoriteTreePickerProjects = (id: string) =>
345 async (dispatch: Dispatch, getState: () => RootState, services: ServiceRepository) => {
346 const parentId = getUserUuid(getState()) || '';
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));
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));
360 const buildParams = (ownerUuid: string) => {
362 filters: new FilterBuilder()
363 .addEqual('owner_uuid', ownerUuid)
365 order: new OrderBuilder<ProjectResource>()