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 './tree-picker-middleware';
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 import { GroupClass, GroupResource } from "models/group";
26 export const treePickerActions = unionize({
27 LOAD_TREE_PICKER_NODE: ofType<{ id: string, pickerId: string }>(),
28 LOAD_TREE_PICKER_NODE_SUCCESS: ofType<{ id: string, nodes: Array<TreeNode<any>>, pickerId: string }>(),
29 APPEND_TREE_PICKER_NODE_SUBTREE: ofType<{ id: string, subtree: Tree<any>, pickerId: string }>(),
30 TOGGLE_TREE_PICKER_NODE_COLLAPSE: ofType<{ id: string, pickerId: string }>(),
31 EXPAND_TREE_PICKER_NODE: ofType<{ id: string, pickerId: string }>(),
32 ACTIVATE_TREE_PICKER_NODE: ofType<{ id: string, pickerId: string, relatedTreePickers?: string[] }>(),
33 DEACTIVATE_TREE_PICKER_NODE: ofType<{ pickerId: string }>(),
34 TOGGLE_TREE_PICKER_NODE_SELECTION: ofType<{ id: string, pickerId: string }>(),
35 SELECT_TREE_PICKER_NODE: ofType<{ id: string | string[], pickerId: string }>(),
36 DESELECT_TREE_PICKER_NODE: ofType<{ id: string | string[], pickerId: string }>(),
37 EXPAND_TREE_PICKER_NODES: ofType<{ ids: string[], pickerId: string }>(),
38 RESET_TREE_PICKER: ofType<{ pickerId: string }>()
41 export type TreePickerAction = UnionOf<typeof treePickerActions>;
43 export interface LoadProjectParams {
44 includeCollections?: boolean;
45 includeFiles?: boolean;
46 includeFilterGroups?: boolean;
47 options?: { showOnlyOwned: boolean; showOnlyWritable: boolean; };
50 export const treePickerSearchActions = unionize({
51 SET_TREE_PICKER_PROJECT_SEARCH: ofType<{ pickerId: string, projectSearchValue: string }>(),
52 SET_TREE_PICKER_COLLECTION_FILTER: ofType<{ pickerId: string, collectionFilterValue: string }>(),
53 SET_TREE_PICKER_LOAD_PARAMS: ofType<{ pickerId: string, params: LoadProjectParams }>(),
56 export type TreePickerSearchAction = UnionOf<typeof treePickerSearchActions>;
58 export const getProjectsTreePickerIds = (pickerId: string) => ({
59 home: `${pickerId}_home`,
60 shared: `${pickerId}_shared`,
61 favorites: `${pickerId}_favorites`,
62 publicFavorites: `${pickerId}_publicFavorites`,
63 search: `${pickerId}_search`,
66 export const getAllNodes = <Value>(pickerId: string, filter = (node: TreeNode<Value>) => true) => (state: TreePicker) =>
68 () => values(getProjectsTreePickerIds(pickerId)),
71 .map(id => getTreePicker<Value>(id)(state)),
74 .map(getNodeDescendants(''))
75 .reduce((allNodes, nodes) => allNodes.concat(nodes), []),
78 .reduce((map, node) =>
80 ? map.set(node.id, node)
81 : map, new Map<string, TreeNode<Value>>())
84 uniqueNodes => Array.from(uniqueNodes),
86 export const getSelectedNodes = <Value>(pickerId: string) => (state: TreePicker) =>
87 getAllNodes<Value>(pickerId, node => node.selected)(state);
89 export const initProjectsTreePicker = (pickerId: string) =>
90 async (dispatch: Dispatch, _: () => RootState, services: ServiceRepository) => {
91 const { home, shared, favorites, publicFavorites, search } = getProjectsTreePickerIds(pickerId);
92 dispatch<any>(initUserProject(home));
93 dispatch<any>(initSharedProject(shared));
94 dispatch<any>(initFavoritesProject(favorites));
95 dispatch<any>(initPublicFavoritesProject(publicFavorites));
96 dispatch<any>(initSearchProject(search));
99 interface ReceiveTreePickerDataParams<T> {
101 extractNodeData: (value: T) => { id: string, value: T, status?: TreeNodeStatus };
106 export const receiveTreePickerData = <T>(params: ReceiveTreePickerDataParams<T>) =>
107 (dispatch: Dispatch) => {
108 const { data, extractNodeData, id, pickerId, } = params;
109 dispatch(treePickerActions.LOAD_TREE_PICKER_NODE_SUCCESS({
111 nodes: data.map(item => initTreeNode(extractNodeData(item))),
114 dispatch(treePickerActions.EXPAND_TREE_PICKER_NODE({ id, pickerId }));
117 interface LoadProjectParamsWithId extends LoadProjectParams {
120 loadShared?: boolean;
121 searchProjects?: boolean;
124 export const loadProject = (params: LoadProjectParamsWithId) =>
125 async (dispatch: Dispatch, getState: () => RootState, services: ServiceRepository) => {
126 const { id, pickerId, includeCollections = false, includeFiles = false, includeFilterGroups = false, loadShared = false, options, searchProjects = false } = params;
128 dispatch(treePickerActions.LOAD_TREE_PICKER_NODE({ id, pickerId }));
130 let filterB = new FilterBuilder();
132 filterB = (includeCollections && !searchProjects)
133 ? filterB.addIsA('uuid', [ResourceKind.PROJECT, ResourceKind.COLLECTION])
134 : filterB.addIsA('uuid', [ResourceKind.PROJECT]);
136 const state = getState();
138 if (state.treePickerSearch.collectionFilterValues[pickerId]) {
139 filterB = filterB.addFullTextSearch(state.treePickerSearch.collectionFilterValues[pickerId], 'collections');
141 filterB = filterB.addNotIn("collections.properties.type", ["intermediate", "log"]);
144 if (searchProjects && state.treePickerSearch.projectSearchValues[pickerId]) {
145 filterB = filterB.addFullTextSearch(state.treePickerSearch.projectSearchValues[pickerId], 'groups');
148 const filters = filterB.getFilters();
150 const itemLimit = 200;
152 const { items, itemsAvailable } = await services.groupsService.contents((loadShared || searchProjects) ? '' : id, { filters, excludeHomeProject: loadShared || undefined, limit: itemLimit });
154 if (itemsAvailable > itemLimit) {
156 uuid: "more-items-available",
157 kind: ResourceKind.WORKFLOW,
158 name: `*** Not all items listed (${items.length} out of ${itemsAvailable}), reduce item count with search or filter ***`,
163 modifiedByClientUuid: "",
164 modifiedByUserUuid: "",
171 dispatch<any>(receiveTreePickerData<GroupContentsResource>({
174 data: items.filter((item) => {
175 if (!includeFilterGroups && (item as GroupResource).groupClass && (item as GroupResource).groupClass === GroupClass.FILTER) {
179 if (options && options.showOnlyWritable && item.hasOwnProperty('frozenByUuid') && (item as ProjectResource).frozenByUuid) {
185 extractNodeData: item => (
186 item.uuid === "more-items-available" ?
190 status: TreeNodeStatus.LOADED
195 status: item.kind === ResourceKind.PROJECT
196 ? TreeNodeStatus.INITIAL
198 ? TreeNodeStatus.INITIAL
199 : TreeNodeStatus.LOADED
204 export const loadCollection = (id: string, pickerId: string) =>
205 async (dispatch: Dispatch, getState: () => RootState, services: ServiceRepository) => {
206 dispatch(treePickerActions.LOAD_TREE_PICKER_NODE({ id, pickerId }));
208 const picker = getTreePicker<ProjectsTreePickerItem>(pickerId)(getState().treePicker);
211 const node = getNode(id)(picker);
212 if (node && 'kind' in node.value && node.value.kind === ResourceKind.COLLECTION) {
213 const files = await services.collectionService.files(node.value.uuid);
214 const tree = createCollectionFilesTree(files);
215 const sorted = sortFilesTree(tree);
216 const filesTree = mapTreeValues(services.collectionService.extendFileURL)(sorted);
219 treePickerActions.APPEND_TREE_PICKER_NODE_SUBTREE({
222 subtree: mapTree(node => ({ ...node, status: TreeNodeStatus.LOADED }))(filesTree)
225 dispatch(treePickerActions.TOGGLE_TREE_PICKER_NODE_COLLAPSE({ id, pickerId }));
231 export const initUserProject = (pickerId: string) =>
232 async (dispatch: Dispatch<any>, getState: () => RootState, services: ServiceRepository) => {
233 const uuid = getUserUuid(getState());
235 dispatch(receiveTreePickerData({
238 data: [{ uuid, name: 'Home Projects' }],
239 extractNodeData: value => ({
241 status: TreeNodeStatus.INITIAL,
247 export const loadUserProject = (pickerId: string, includeCollections = false, includeFiles = false, options?: { showOnlyOwned: boolean, showOnlyWritable: boolean }) =>
248 async (dispatch: Dispatch<any>, getState: () => RootState, services: ServiceRepository) => {
249 const uuid = getUserUuid(getState());
251 dispatch(loadProject({ id: uuid, pickerId, includeCollections, includeFiles, options }));
255 export const SHARED_PROJECT_ID = 'Shared with me';
256 export const initSharedProject = (pickerId: string) =>
257 async (dispatch: Dispatch<any>, getState: () => RootState, services: ServiceRepository) => {
258 dispatch(receiveTreePickerData({
261 data: [{ uuid: SHARED_PROJECT_ID, name: SHARED_PROJECT_ID }],
262 extractNodeData: value => ({
264 status: TreeNodeStatus.INITIAL,
270 export const FAVORITES_PROJECT_ID = 'Favorites';
271 export const initFavoritesProject = (pickerId: string) =>
272 async (dispatch: Dispatch<any>, getState: () => RootState, services: ServiceRepository) => {
273 dispatch(receiveTreePickerData({
276 data: [{ uuid: FAVORITES_PROJECT_ID, name: FAVORITES_PROJECT_ID }],
277 extractNodeData: value => ({
279 status: TreeNodeStatus.INITIAL,
285 export const PUBLIC_FAVORITES_PROJECT_ID = 'Public Favorites';
286 export const initPublicFavoritesProject = (pickerId: string) =>
287 async (dispatch: Dispatch<any>, getState: () => RootState, services: ServiceRepository) => {
288 dispatch(receiveTreePickerData({
291 data: [{ uuid: PUBLIC_FAVORITES_PROJECT_ID, name: PUBLIC_FAVORITES_PROJECT_ID }],
292 extractNodeData: value => ({
294 status: TreeNodeStatus.INITIAL,
300 export const SEARCH_PROJECT_ID = 'Search all Projects';
301 export const initSearchProject = (pickerId: string) =>
302 async (dispatch: Dispatch<any>, getState: () => RootState, services: ServiceRepository) => {
303 dispatch(receiveTreePickerData({
306 data: [{ uuid: SEARCH_PROJECT_ID, name: SEARCH_PROJECT_ID }],
307 extractNodeData: value => ({
309 status: TreeNodeStatus.INITIAL,
316 interface LoadFavoritesProjectParams {
318 includeCollections?: boolean;
319 includeFiles?: boolean;
320 options?: { showOnlyOwned: boolean, showOnlyWritable: boolean };
323 export const loadFavoritesProject = (params: LoadFavoritesProjectParams,
324 options: { showOnlyOwned: boolean, showOnlyWritable: boolean } = { showOnlyOwned: true, showOnlyWritable: false }) =>
325 async (dispatch: Dispatch<any>, getState: () => RootState, services: ServiceRepository) => {
326 const { pickerId, includeCollections = false, includeFiles = false } = params;
327 const uuid = getUserUuid(getState());
329 const filters = pipe(
330 (fb: FilterBuilder) => includeCollections
331 ? fb.addIsA('head_uuid', [ResourceKind.PROJECT, ResourceKind.COLLECTION])
332 : fb.addIsA('head_uuid', [ResourceKind.PROJECT]),
333 fb => fb.getFilters(),
334 )(new FilterBuilder());
336 const { items } = await services.favoriteService.list(uuid, { filters }, options.showOnlyOwned);
338 dispatch<any>(receiveTreePickerData<GroupContentsResource>({
341 data: items.filter((item) => {
342 if (options.showOnlyWritable && (item as GroupResource).writableBy && (item as GroupResource).writableBy.indexOf(uuid) === -1) {
346 if (options.showOnlyWritable && item.hasOwnProperty('frozenByUuid') && (item as ProjectResource).frozenByUuid) {
352 extractNodeData: item => ({
355 status: item.kind === ResourceKind.PROJECT
356 ? TreeNodeStatus.INITIAL
358 ? TreeNodeStatus.INITIAL
359 : TreeNodeStatus.LOADED
365 export const loadPublicFavoritesProject = (params: LoadFavoritesProjectParams) =>
366 async (dispatch: Dispatch<any>, getState: () => RootState, services: ServiceRepository) => {
367 const { pickerId, includeCollections = false, includeFiles = false } = params;
368 const uuidPrefix = getState().auth.config.uuidPrefix;
369 const publicProjectUuid = `${uuidPrefix}-j7d0g-publicfavorites`;
371 const filters = pipe(
372 (fb: FilterBuilder) => includeCollections
373 ? fb.addIsA('head_uuid', [ResourceKind.PROJECT, ResourceKind.COLLECTION])
374 : fb.addIsA('head_uuid', [ResourceKind.PROJECT]),
376 .addEqual('link_class', LinkClass.STAR)
377 .addEqual('owner_uuid', publicProjectUuid)
379 )(new FilterBuilder());
381 const { items } = await services.linkService.list({ filters });
383 dispatch<any>(receiveTreePickerData<LinkResource>({
384 id: 'Public Favorites',
386 data: items.filter(item => {
387 if (params.options && params.options.showOnlyWritable && item.hasOwnProperty('frozenByUuid') && (item as any).frozenByUuid) {
393 extractNodeData: item => ({
396 status: item.headKind === ResourceKind.PROJECT
397 ? TreeNodeStatus.INITIAL
399 ? TreeNodeStatus.INITIAL
400 : TreeNodeStatus.LOADED
405 export const receiveTreePickerProjectsData = (id: string, projects: ProjectResource[], pickerId: string) =>
406 (dispatch: Dispatch, getState: () => RootState, services: ServiceRepository) => {
407 dispatch(treePickerActions.LOAD_TREE_PICKER_NODE_SUCCESS({
409 nodes: projects.map(project => initTreeNode({ id: project.uuid, value: project })),
413 dispatch(treePickerActions.TOGGLE_TREE_PICKER_NODE_COLLAPSE({ id, pickerId }));
416 export const loadProjectTreePickerProjects = (id: string) =>
417 async (dispatch: Dispatch, getState: () => RootState, services: ServiceRepository) => {
418 dispatch(treePickerActions.LOAD_TREE_PICKER_NODE({ id, pickerId: TreePickerId.PROJECTS }));
421 const ownerUuid = id.length === 0 ? getUserUuid(getState()) || '' : id;
422 const { items } = await services.projectService.list(buildParams(ownerUuid));
424 dispatch<any>(receiveTreePickerProjectsData(id, items, TreePickerId.PROJECTS));
427 export const loadFavoriteTreePickerProjects = (id: string) =>
428 async (dispatch: Dispatch, getState: () => RootState, services: ServiceRepository) => {
429 const parentId = getUserUuid(getState()) || '';
432 dispatch(treePickerActions.LOAD_TREE_PICKER_NODE({ id: parentId, pickerId: TreePickerId.FAVORITES }));
433 const { items } = await services.favoriteService.list(parentId);
434 dispatch<any>(receiveTreePickerProjectsData(parentId, items as ProjectResource[], TreePickerId.FAVORITES));
436 dispatch(treePickerActions.LOAD_TREE_PICKER_NODE({ id, pickerId: TreePickerId.FAVORITES }));
437 const { items } = await services.projectService.list(buildParams(id));
438 dispatch<any>(receiveTreePickerProjectsData(id, items, TreePickerId.FAVORITES));
443 export const loadPublicFavoriteTreePickerProjects = (id: string) =>
444 async (dispatch: Dispatch, getState: () => RootState, services: ServiceRepository) => {
445 const parentId = getUserUuid(getState()) || '';
448 dispatch(treePickerActions.LOAD_TREE_PICKER_NODE({ id: parentId, pickerId: TreePickerId.PUBLIC_FAVORITES }));
449 const { items } = await services.favoriteService.list(parentId);
450 dispatch<any>(receiveTreePickerProjectsData(parentId, items as ProjectResource[], TreePickerId.PUBLIC_FAVORITES));
452 dispatch(treePickerActions.LOAD_TREE_PICKER_NODE({ id, pickerId: TreePickerId.PUBLIC_FAVORITES }));
453 const { items } = await services.projectService.list(buildParams(id));
454 dispatch<any>(receiveTreePickerProjectsData(id, items, TreePickerId.PUBLIC_FAVORITES));
459 const buildParams = (ownerUuid: string) => {
461 filters: new FilterBuilder()
462 .addEqual('owner_uuid', ownerUuid)
464 order: new OrderBuilder<ProjectResource>()