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";
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 ACTIVATE_TREE_PICKER_NODE: ofType<{ id: string, pickerId: string, relatedTreePickers?: string[] }>(),
32 DEACTIVATE_TREE_PICKER_NODE: ofType<{ pickerId: string }>(),
33 TOGGLE_TREE_PICKER_NODE_SELECTION: ofType<{ id: string, pickerId: string }>(),
34 SELECT_TREE_PICKER_NODE: ofType<{ id: string | string[], pickerId: string }>(),
35 DESELECT_TREE_PICKER_NODE: ofType<{ id: string | string[], pickerId: string }>(),
36 EXPAND_TREE_PICKER_NODES: ofType<{ ids: string[], pickerId: string }>(),
37 RESET_TREE_PICKER: ofType<{ pickerId: string }>()
40 export type TreePickerAction = UnionOf<typeof treePickerActions>;
42 export const getProjectsTreePickerIds = (pickerId: string) => ({
43 home: `${pickerId}_home`,
44 shared: `${pickerId}_shared`,
45 favorites: `${pickerId}_favorites`,
46 publicFavorites: `${pickerId}_publicFavorites`
49 export const getAllNodes = <Value>(pickerId: string, filter = (node: TreeNode<Value>) => true) => (state: TreePicker) =>
51 () => values(getProjectsTreePickerIds(pickerId)),
54 .map(id => getTreePicker<Value>(id)(state)),
57 .map(getNodeDescendants(''))
58 .reduce((allNodes, nodes) => allNodes.concat(nodes), []),
61 .reduce((map, node) =>
63 ? map.set(node.id, node)
64 : map, new Map<string, TreeNode<Value>>())
67 uniqueNodes => Array.from(uniqueNodes),
69 export const getSelectedNodes = <Value>(pickerId: string) => (state: TreePicker) =>
70 getAllNodes<Value>(pickerId, node => node.selected)(state);
72 export const initProjectsTreePicker = (pickerId: string) =>
73 async (dispatch: Dispatch, _: () => RootState, services: ServiceRepository) => {
74 const { home, shared, favorites, publicFavorites } = getProjectsTreePickerIds(pickerId);
75 dispatch<any>(initUserProject(home));
76 dispatch<any>(initSharedProject(shared));
77 dispatch<any>(initFavoritesProject(favorites));
78 dispatch<any>(initPublicFavoritesProject(publicFavorites));
81 interface ReceiveTreePickerDataParams<T> {
83 extractNodeData: (value: T) => { id: string, value: T, status?: TreeNodeStatus };
88 export const receiveTreePickerData = <T>(params: ReceiveTreePickerDataParams<T>) =>
89 (dispatch: Dispatch) => {
90 const { data, extractNodeData, id, pickerId, } = params;
91 dispatch(treePickerActions.LOAD_TREE_PICKER_NODE_SUCCESS({
93 nodes: data.map(item => initTreeNode(extractNodeData(item))),
96 dispatch(treePickerActions.TOGGLE_TREE_PICKER_NODE_COLLAPSE({ id, pickerId }));
99 interface LoadProjectParams {
102 includeCollections?: boolean;
103 includeFiles?: boolean;
104 includeFilterGroups?: boolean;
105 loadShared?: boolean;
106 options?: { showOnlyOwned: boolean; showOnlyWritable: boolean; };
108 export const loadProject = (params: LoadProjectParams) =>
109 async (dispatch: Dispatch, _: () => RootState, services: ServiceRepository) => {
110 const { id, pickerId, includeCollections = false, includeFiles = false, includeFilterGroups = false, loadShared = false, options } = params;
112 dispatch(treePickerActions.LOAD_TREE_PICKER_NODE({ id, pickerId }));
114 const filters = pipe(
115 (fb: FilterBuilder) => includeCollections
116 ? fb.addIsA('uuid', [ResourceKind.PROJECT, ResourceKind.COLLECTION])
117 : fb.addIsA('uuid', [ResourceKind.PROJECT]),
118 fb => fb.addNotIn("collections.properties.type", ["intermediate", "log"]),
119 fb => fb.getFilters(),
120 )(new FilterBuilder());
122 const { items, itemsAvailable } = await services.groupsService.contents(loadShared ? '' : id, { filters, excludeHomeProject: loadShared || undefined, limit: 1000 });
124 if (itemsAvailable > 1000) {
126 uuid: "more-items-available",
127 kind: ResourceKind.WORKFLOW,
128 name: "*** Not all items were loaded (limit 1000 items) ***",
133 modifiedByClientUuid: "",
134 modifiedByUserUuid: "",
141 dispatch<any>(receiveTreePickerData<GroupContentsResource>({
144 data: items.filter((item) => {
145 if (!includeFilterGroups && (item as GroupResource).groupClass && (item as GroupResource).groupClass === GroupClass.FILTER) {
149 if (options && options.showOnlyWritable && item.hasOwnProperty('frozenByUuid') && (item as ProjectResource).frozenByUuid) {
155 extractNodeData: item => (
156 item.uuid === "more-items-available" ?
160 status: TreeNodeStatus.LOADED
165 status: item.kind === ResourceKind.PROJECT
166 ? TreeNodeStatus.INITIAL
168 ? TreeNodeStatus.INITIAL
169 : TreeNodeStatus.LOADED
174 export const loadCollection = (id: string, pickerId: string) =>
175 async (dispatch: Dispatch, getState: () => RootState, services: ServiceRepository) => {
176 dispatch(treePickerActions.LOAD_TREE_PICKER_NODE({ id, pickerId }));
178 const picker = getTreePicker<ProjectsTreePickerItem>(pickerId)(getState().treePicker);
181 const node = getNode(id)(picker);
182 if (node && 'kind' in node.value && node.value.kind === ResourceKind.COLLECTION) {
183 const files = await services.collectionService.files(node.value.portableDataHash);
184 const tree = createCollectionFilesTree(files);
185 const sorted = sortFilesTree(tree);
186 const filesTree = mapTreeValues(services.collectionService.extendFileURL)(sorted);
189 treePickerActions.APPEND_TREE_PICKER_NODE_SUBTREE({
192 subtree: mapTree(node => ({ ...node, status: TreeNodeStatus.LOADED }))(filesTree)
195 dispatch(treePickerActions.TOGGLE_TREE_PICKER_NODE_COLLAPSE({ id, pickerId }));
201 export const initUserProject = (pickerId: string) =>
202 async (dispatch: Dispatch<any>, getState: () => RootState, services: ServiceRepository) => {
203 const uuid = getUserUuid(getState());
205 dispatch(receiveTreePickerData({
208 data: [{ uuid, name: 'Projects' }],
209 extractNodeData: value => ({
211 status: TreeNodeStatus.INITIAL,
217 export const loadUserProject = (pickerId: string, includeCollections = false, includeFiles = false, options?: { showOnlyOwned: boolean, showOnlyWritable: boolean }) =>
218 async (dispatch: Dispatch<any>, getState: () => RootState, services: ServiceRepository) => {
219 const uuid = getUserUuid(getState());
221 dispatch(loadProject({ id: uuid, pickerId, includeCollections, includeFiles, options }));
225 export const SHARED_PROJECT_ID = 'Shared with me';
226 export const initSharedProject = (pickerId: string) =>
227 async (dispatch: Dispatch<any>, getState: () => RootState, services: ServiceRepository) => {
228 dispatch(receiveTreePickerData({
231 data: [{ uuid: SHARED_PROJECT_ID, name: SHARED_PROJECT_ID }],
232 extractNodeData: value => ({
234 status: TreeNodeStatus.INITIAL,
240 export const FAVORITES_PROJECT_ID = 'Favorites';
241 export const initFavoritesProject = (pickerId: string) =>
242 async (dispatch: Dispatch<any>, getState: () => RootState, services: ServiceRepository) => {
243 dispatch(receiveTreePickerData({
246 data: [{ uuid: FAVORITES_PROJECT_ID, name: FAVORITES_PROJECT_ID }],
247 extractNodeData: value => ({
249 status: TreeNodeStatus.INITIAL,
255 export const PUBLIC_FAVORITES_PROJECT_ID = 'Public Favorites';
256 export const initPublicFavoritesProject = (pickerId: string) =>
257 async (dispatch: Dispatch<any>, getState: () => RootState, services: ServiceRepository) => {
258 dispatch(receiveTreePickerData({
261 data: [{ uuid: PUBLIC_FAVORITES_PROJECT_ID, name: PUBLIC_FAVORITES_PROJECT_ID }],
262 extractNodeData: value => ({
264 status: TreeNodeStatus.INITIAL,
270 interface LoadFavoritesProjectParams {
272 includeCollections?: boolean;
273 includeFiles?: boolean;
274 options?: { showOnlyOwned: boolean, showOnlyWritable: boolean };
277 export const loadFavoritesProject = (params: LoadFavoritesProjectParams,
278 options: { showOnlyOwned: boolean, showOnlyWritable: boolean } = { showOnlyOwned: true, showOnlyWritable: false }) =>
279 async (dispatch: Dispatch<any>, getState: () => RootState, services: ServiceRepository) => {
280 const { pickerId, includeCollections = false, includeFiles = false } = params;
281 const uuid = getUserUuid(getState());
283 const filters = pipe(
284 (fb: FilterBuilder) => includeCollections
285 ? fb.addIsA('head_uuid', [ResourceKind.PROJECT, ResourceKind.COLLECTION])
286 : fb.addIsA('head_uuid', [ResourceKind.PROJECT]),
287 fb => fb.getFilters(),
288 )(new FilterBuilder());
290 const { items } = await services.favoriteService.list(uuid, { filters }, options.showOnlyOwned);
292 dispatch<any>(receiveTreePickerData<GroupContentsResource>({
295 data: items.filter((item) => {
296 if (options.showOnlyWritable && (item as GroupResource).writableBy && (item as GroupResource).writableBy.indexOf(uuid) === -1) {
300 if (options.showOnlyWritable && item.hasOwnProperty('frozenByUuid') && (item as ProjectResource).frozenByUuid) {
306 extractNodeData: item => ({
309 status: item.kind === ResourceKind.PROJECT
310 ? TreeNodeStatus.INITIAL
312 ? TreeNodeStatus.INITIAL
313 : TreeNodeStatus.LOADED
319 export const loadPublicFavoritesProject = (params: LoadFavoritesProjectParams) =>
320 async (dispatch: Dispatch<any>, getState: () => RootState, services: ServiceRepository) => {
321 const { pickerId, includeCollections = false, includeFiles = false } = params;
322 const uuidPrefix = getState().auth.config.uuidPrefix;
323 const publicProjectUuid = `${uuidPrefix}-j7d0g-publicfavorites`;
325 const filters = pipe(
326 (fb: FilterBuilder) => includeCollections
327 ? fb.addIsA('head_uuid', [ResourceKind.PROJECT, ResourceKind.COLLECTION])
328 : fb.addIsA('head_uuid', [ResourceKind.PROJECT]),
330 .addEqual('link_class', LinkClass.STAR)
331 .addEqual('owner_uuid', publicProjectUuid)
333 )(new FilterBuilder());
335 const { items } = await services.linkService.list({ filters });
337 dispatch<any>(receiveTreePickerData<LinkResource>({
338 id: 'Public Favorites',
340 data: items.filter(item => {
341 if (params.options && params.options.showOnlyWritable && item.hasOwnProperty('frozenByUuid') && (item as any).frozenByUuid) {
347 extractNodeData: item => ({
350 status: item.headKind === ResourceKind.PROJECT
351 ? TreeNodeStatus.INITIAL
353 ? TreeNodeStatus.INITIAL
354 : TreeNodeStatus.LOADED
359 export const receiveTreePickerProjectsData = (id: string, projects: ProjectResource[], pickerId: string) =>
360 (dispatch: Dispatch, getState: () => RootState, services: ServiceRepository) => {
361 dispatch(treePickerActions.LOAD_TREE_PICKER_NODE_SUCCESS({
363 nodes: projects.map(project => initTreeNode({ id: project.uuid, value: project })),
367 dispatch(treePickerActions.TOGGLE_TREE_PICKER_NODE_COLLAPSE({ id, pickerId }));
370 export const loadProjectTreePickerProjects = (id: string) =>
371 async (dispatch: Dispatch, getState: () => RootState, services: ServiceRepository) => {
372 dispatch(treePickerActions.LOAD_TREE_PICKER_NODE({ id, pickerId: TreePickerId.PROJECTS }));
375 const ownerUuid = id.length === 0 ? getUserUuid(getState()) || '' : id;
376 const { items } = await services.projectService.list(buildParams(ownerUuid));
378 dispatch<any>(receiveTreePickerProjectsData(id, items, TreePickerId.PROJECTS));
381 export const loadFavoriteTreePickerProjects = (id: string) =>
382 async (dispatch: Dispatch, getState: () => RootState, services: ServiceRepository) => {
383 const parentId = getUserUuid(getState()) || '';
386 dispatch(treePickerActions.LOAD_TREE_PICKER_NODE({ id: parentId, pickerId: TreePickerId.FAVORITES }));
387 const { items } = await services.favoriteService.list(parentId);
388 dispatch<any>(receiveTreePickerProjectsData(parentId, items as ProjectResource[], TreePickerId.FAVORITES));
390 dispatch(treePickerActions.LOAD_TREE_PICKER_NODE({ id, pickerId: TreePickerId.FAVORITES }));
391 const { items } = await services.projectService.list(buildParams(id));
392 dispatch<any>(receiveTreePickerProjectsData(id, items, TreePickerId.FAVORITES));
397 export const loadPublicFavoriteTreePickerProjects = (id: string) =>
398 async (dispatch: Dispatch, getState: () => RootState, services: ServiceRepository) => {
399 const parentId = getUserUuid(getState()) || '';
402 dispatch(treePickerActions.LOAD_TREE_PICKER_NODE({ id: parentId, pickerId: TreePickerId.PUBLIC_FAVORITES }));
403 const { items } = await services.favoriteService.list(parentId);
404 dispatch<any>(receiveTreePickerProjectsData(parentId, items as ProjectResource[], TreePickerId.PUBLIC_FAVORITES));
406 dispatch(treePickerActions.LOAD_TREE_PICKER_NODE({ id, pickerId: TreePickerId.PUBLIC_FAVORITES }));
407 const { items } = await services.projectService.list(buildParams(id));
408 dispatch<any>(receiveTreePickerProjectsData(id, items, TreePickerId.PUBLIC_FAVORITES));
413 const buildParams = (ownerUuid: string) => {
415 filters: new FilterBuilder()
416 .addEqual('owner_uuid', ownerUuid)
418 order: new OrderBuilder<ProjectResource>()