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;
48 options?: { showOnlyOwned: boolean; showOnlyWritable: boolean; };
51 export const treePickerSearchActions = unionize({
52 SET_TREE_PICKER_PROJECT_SEARCH: ofType<{ pickerId: string, projectSearchValue: string }>(),
53 SET_TREE_PICKER_COLLECTION_FILTER: ofType<{ pickerId: string, collectionFilterValue: string }>(),
54 SET_TREE_PICKER_LOAD_PARAMS: ofType<{ pickerId: string, params: LoadProjectParams }>(),
57 export type TreePickerSearchAction = UnionOf<typeof treePickerSearchActions>;
59 export const getProjectsTreePickerIds = (pickerId: string) => ({
60 home: `${pickerId}_home`,
61 shared: `${pickerId}_shared`,
62 favorites: `${pickerId}_favorites`,
63 publicFavorites: `${pickerId}_publicFavorites`
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 } = getProjectsTreePickerIds(pickerId);
92 dispatch<any>(initUserProject(home));
93 dispatch<any>(initSharedProject(shared));
94 dispatch<any>(initFavoritesProject(favorites));
95 dispatch<any>(initPublicFavoritesProject(publicFavorites));
98 interface ReceiveTreePickerDataParams<T> {
100 extractNodeData: (value: T) => { id: string, value: T, status?: TreeNodeStatus };
105 export const receiveTreePickerData = <T>(params: ReceiveTreePickerDataParams<T>) =>
106 (dispatch: Dispatch) => {
107 const { data, extractNodeData, id, pickerId, } = params;
108 dispatch(treePickerActions.LOAD_TREE_PICKER_NODE_SUCCESS({
110 nodes: data.map(item => initTreeNode(extractNodeData(item))),
113 dispatch(treePickerActions.EXPAND_TREE_PICKER_NODE({ id, pickerId }));
116 interface LoadProjectParamsWithId extends LoadProjectParams {
119 includeCollections?: boolean;
120 includeFiles?: boolean;
121 includeFilterGroups?: boolean;
122 loadShared?: boolean;
123 options?: { showOnlyOwned: boolean; showOnlyWritable: boolean; };
126 export const loadProject = (params: LoadProjectParamsWithId) =>
127 async (dispatch: Dispatch, getState: () => RootState, services: ServiceRepository) => {
128 const { id, pickerId, includeCollections = false, includeFiles = false, includeFilterGroups = false, loadShared = false, options } = params;
130 dispatch(treePickerActions.LOAD_TREE_PICKER_NODE({ id, pickerId }));
133 (fb: FilterBuilder) => includeCollections
134 ? fb.addIsA('uuid', [ResourceKind.PROJECT, ResourceKind.COLLECTION])
135 : fb.addIsA('uuid', [ResourceKind.PROJECT]),
136 fb => fb.addNotIn("collections.properties.type", ["intermediate", "log"]),
137 )(new FilterBuilder());
139 const state = getState();
141 if (state.treePickerSearch.collectionFilterValues[pickerId]) {
142 filterB = filterB.addILike('collections.name', state.treePickerSearch.collectionFilterValues[pickerId]);
145 const filters = filterB.getFilters();
147 const { items, itemsAvailable } = await services.groupsService.contents(loadShared ? '' : id, { filters, excludeHomeProject: loadShared || undefined, limit: 1000 });
149 if (itemsAvailable > 1000) {
151 uuid: "more-items-available",
152 kind: ResourceKind.WORKFLOW,
153 name: "*** Not all items were loaded (limit 1000 items) ***",
158 modifiedByClientUuid: "",
159 modifiedByUserUuid: "",
166 dispatch<any>(receiveTreePickerData<GroupContentsResource>({
169 data: items.filter((item) => {
170 if (!includeFilterGroups && (item as GroupResource).groupClass && (item as GroupResource).groupClass === GroupClass.FILTER) {
174 if (options && options.showOnlyWritable && item.hasOwnProperty('frozenByUuid') && (item as ProjectResource).frozenByUuid) {
180 extractNodeData: item => (
181 item.uuid === "more-items-available" ?
185 status: TreeNodeStatus.LOADED
190 status: item.kind === ResourceKind.PROJECT
191 ? TreeNodeStatus.INITIAL
193 ? TreeNodeStatus.INITIAL
194 : TreeNodeStatus.LOADED
199 export const loadCollection = (id: string, pickerId: string) =>
200 async (dispatch: Dispatch, getState: () => RootState, services: ServiceRepository) => {
201 dispatch(treePickerActions.LOAD_TREE_PICKER_NODE({ id, pickerId }));
203 const picker = getTreePicker<ProjectsTreePickerItem>(pickerId)(getState().treePicker);
206 const node = getNode(id)(picker);
207 if (node && 'kind' in node.value && node.value.kind === ResourceKind.COLLECTION) {
208 const files = await services.collectionService.files(node.value.portableDataHash);
209 const tree = createCollectionFilesTree(files);
210 const sorted = sortFilesTree(tree);
211 const filesTree = mapTreeValues(services.collectionService.extendFileURL)(sorted);
214 treePickerActions.APPEND_TREE_PICKER_NODE_SUBTREE({
217 subtree: mapTree(node => ({ ...node, status: TreeNodeStatus.LOADED }))(filesTree)
220 dispatch(treePickerActions.TOGGLE_TREE_PICKER_NODE_COLLAPSE({ id, pickerId }));
226 export const initUserProject = (pickerId: string) =>
227 async (dispatch: Dispatch<any>, getState: () => RootState, services: ServiceRepository) => {
228 const uuid = getUserUuid(getState());
230 dispatch(receiveTreePickerData({
233 data: [{ uuid, name: 'Projects' }],
234 extractNodeData: value => ({
236 status: TreeNodeStatus.INITIAL,
242 export const loadUserProject = (pickerId: string, includeCollections = false, includeFiles = false, options?: { showOnlyOwned: boolean, showOnlyWritable: boolean }) =>
243 async (dispatch: Dispatch<any>, getState: () => RootState, services: ServiceRepository) => {
244 const uuid = getUserUuid(getState());
246 dispatch(loadProject({ id: uuid, pickerId, includeCollections, includeFiles, options }));
250 export const SHARED_PROJECT_ID = 'Shared with me';
251 export const initSharedProject = (pickerId: string) =>
252 async (dispatch: Dispatch<any>, getState: () => RootState, services: ServiceRepository) => {
253 dispatch(receiveTreePickerData({
256 data: [{ uuid: SHARED_PROJECT_ID, name: SHARED_PROJECT_ID }],
257 extractNodeData: value => ({
259 status: TreeNodeStatus.INITIAL,
265 export const FAVORITES_PROJECT_ID = 'Favorites';
266 export const initFavoritesProject = (pickerId: string) =>
267 async (dispatch: Dispatch<any>, getState: () => RootState, services: ServiceRepository) => {
268 dispatch(receiveTreePickerData({
271 data: [{ uuid: FAVORITES_PROJECT_ID, name: FAVORITES_PROJECT_ID }],
272 extractNodeData: value => ({
274 status: TreeNodeStatus.INITIAL,
280 export const PUBLIC_FAVORITES_PROJECT_ID = 'Public Favorites';
281 export const initPublicFavoritesProject = (pickerId: string) =>
282 async (dispatch: Dispatch<any>, getState: () => RootState, services: ServiceRepository) => {
283 dispatch(receiveTreePickerData({
286 data: [{ uuid: PUBLIC_FAVORITES_PROJECT_ID, name: PUBLIC_FAVORITES_PROJECT_ID }],
287 extractNodeData: value => ({
289 status: TreeNodeStatus.INITIAL,
295 interface LoadFavoritesProjectParams {
297 includeCollections?: boolean;
298 includeFiles?: boolean;
299 options?: { showOnlyOwned: boolean, showOnlyWritable: boolean };
302 export const loadFavoritesProject = (params: LoadFavoritesProjectParams,
303 options: { showOnlyOwned: boolean, showOnlyWritable: boolean } = { showOnlyOwned: true, showOnlyWritable: false }) =>
304 async (dispatch: Dispatch<any>, getState: () => RootState, services: ServiceRepository) => {
305 const { pickerId, includeCollections = false, includeFiles = false } = params;
306 const uuid = getUserUuid(getState());
308 const filters = pipe(
309 (fb: FilterBuilder) => includeCollections
310 ? fb.addIsA('head_uuid', [ResourceKind.PROJECT, ResourceKind.COLLECTION])
311 : fb.addIsA('head_uuid', [ResourceKind.PROJECT]),
312 fb => fb.getFilters(),
313 )(new FilterBuilder());
315 const { items } = await services.favoriteService.list(uuid, { filters }, options.showOnlyOwned);
317 dispatch<any>(receiveTreePickerData<GroupContentsResource>({
320 data: items.filter((item) => {
321 if (options.showOnlyWritable && (item as GroupResource).writableBy && (item as GroupResource).writableBy.indexOf(uuid) === -1) {
325 if (options.showOnlyWritable && item.hasOwnProperty('frozenByUuid') && (item as ProjectResource).frozenByUuid) {
331 extractNodeData: item => ({
334 status: item.kind === ResourceKind.PROJECT
335 ? TreeNodeStatus.INITIAL
337 ? TreeNodeStatus.INITIAL
338 : TreeNodeStatus.LOADED
344 export const loadPublicFavoritesProject = (params: LoadFavoritesProjectParams) =>
345 async (dispatch: Dispatch<any>, getState: () => RootState, services: ServiceRepository) => {
346 const { pickerId, includeCollections = false, includeFiles = false } = params;
347 const uuidPrefix = getState().auth.config.uuidPrefix;
348 const publicProjectUuid = `${uuidPrefix}-j7d0g-publicfavorites`;
350 const filters = pipe(
351 (fb: FilterBuilder) => includeCollections
352 ? fb.addIsA('head_uuid', [ResourceKind.PROJECT, ResourceKind.COLLECTION])
353 : fb.addIsA('head_uuid', [ResourceKind.PROJECT]),
355 .addEqual('link_class', LinkClass.STAR)
356 .addEqual('owner_uuid', publicProjectUuid)
358 )(new FilterBuilder());
360 const { items } = await services.linkService.list({ filters });
362 dispatch<any>(receiveTreePickerData<LinkResource>({
363 id: 'Public Favorites',
365 data: items.filter(item => {
366 if (params.options && params.options.showOnlyWritable && item.hasOwnProperty('frozenByUuid') && (item as any).frozenByUuid) {
372 extractNodeData: item => ({
375 status: item.headKind === ResourceKind.PROJECT
376 ? TreeNodeStatus.INITIAL
378 ? TreeNodeStatus.INITIAL
379 : TreeNodeStatus.LOADED
384 export const receiveTreePickerProjectsData = (id: string, projects: ProjectResource[], pickerId: string) =>
385 (dispatch: Dispatch, getState: () => RootState, services: ServiceRepository) => {
386 dispatch(treePickerActions.LOAD_TREE_PICKER_NODE_SUCCESS({
388 nodes: projects.map(project => initTreeNode({ id: project.uuid, value: project })),
392 dispatch(treePickerActions.TOGGLE_TREE_PICKER_NODE_COLLAPSE({ id, pickerId }));
395 export const loadProjectTreePickerProjects = (id: string) =>
396 async (dispatch: Dispatch, getState: () => RootState, services: ServiceRepository) => {
397 dispatch(treePickerActions.LOAD_TREE_PICKER_NODE({ id, pickerId: TreePickerId.PROJECTS }));
400 const ownerUuid = id.length === 0 ? getUserUuid(getState()) || '' : id;
401 const { items } = await services.projectService.list(buildParams(ownerUuid));
403 dispatch<any>(receiveTreePickerProjectsData(id, items, TreePickerId.PROJECTS));
406 export const loadFavoriteTreePickerProjects = (id: string) =>
407 async (dispatch: Dispatch, getState: () => RootState, services: ServiceRepository) => {
408 const parentId = getUserUuid(getState()) || '';
411 dispatch(treePickerActions.LOAD_TREE_PICKER_NODE({ id: parentId, pickerId: TreePickerId.FAVORITES }));
412 const { items } = await services.favoriteService.list(parentId);
413 dispatch<any>(receiveTreePickerProjectsData(parentId, items as ProjectResource[], TreePickerId.FAVORITES));
415 dispatch(treePickerActions.LOAD_TREE_PICKER_NODE({ id, pickerId: TreePickerId.FAVORITES }));
416 const { items } = await services.projectService.list(buildParams(id));
417 dispatch<any>(receiveTreePickerProjectsData(id, items, TreePickerId.FAVORITES));
422 export const loadPublicFavoriteTreePickerProjects = (id: string) =>
423 async (dispatch: Dispatch, getState: () => RootState, services: ServiceRepository) => {
424 const parentId = getUserUuid(getState()) || '';
427 dispatch(treePickerActions.LOAD_TREE_PICKER_NODE({ id: parentId, pickerId: TreePickerId.PUBLIC_FAVORITES }));
428 const { items } = await services.favoriteService.list(parentId);
429 dispatch<any>(receiveTreePickerProjectsData(parentId, items as ProjectResource[], TreePickerId.PUBLIC_FAVORITES));
431 dispatch(treePickerActions.LOAD_TREE_PICKER_NODE({ id, pickerId: TreePickerId.PUBLIC_FAVORITES }));
432 const { items } = await services.projectService.list(buildParams(id));
433 dispatch<any>(receiveTreePickerProjectsData(id, items, TreePickerId.PUBLIC_FAVORITES));
438 const buildParams = (ownerUuid: string) => {
440 filters: new FilterBuilder()
441 .addEqual('owner_uuid', ownerUuid)
443 order: new OrderBuilder<ProjectResource>()