Merge branch 'master' into 13902-ui-move-to-popup
[arvados-workbench2.git] / src / views-components / project-tree-picker / project-tree-picker.tsx
1 // Copyright (C) The Arvados Authors. All rights reserved.
2 //
3 // SPDX-License-Identifier: AGPL-3.0
4
5 import * as React from "react";
6 import { Dispatch } from "redux";
7 import { connect } from "react-redux";
8 import { Typography } from "@material-ui/core";
9 import { TreePicker, TreePickerProps } from "../tree-picker/tree-picker";
10 import { TreeItem, TreeItemStatus } from "~/components/tree/tree";
11 import { ProjectResource } from "~/models/project";
12 import { treePickerActions } from "~/store/tree-picker/tree-picker-actions";
13 import { ListItemTextIcon } from "~/components/list-item-text-icon/list-item-text-icon";
14 import { ProjectIcon, FavoriteIcon, ProjectsIcon, ShareMeIcon } from "~/components/icon/icon";
15 import { createTreePickerNode } from "~/store/tree-picker/tree-picker";
16 import { RootState } from "~/store/store";
17 import { ServiceRepository } from "~/services/services";
18 import { FilterBuilder } from "~/common/api/filter-builder";
19 import { mockProjectResource } from "~/models/test-utils";
20
21 type ProjectTreePickerProps = Pick<TreePickerProps, 'toggleItemActive' | 'toggleItemOpen'>;
22
23 const mapDispatchToProps = (dispatch: Dispatch, props: { onChange: (projectUuid: string) => void }): ProjectTreePickerProps => ({
24     toggleItemActive: (id, status, pickerKind) => {
25         getNotSelectedTreePickerKind(pickerKind)
26             .forEach(pickerKind => dispatch(treePickerActions.TOGGLE_TREE_PICKER_NODE_SELECT({ id: '', pickerKind })));
27         dispatch(treePickerActions.TOGGLE_TREE_PICKER_NODE_SELECT({ id, pickerKind }));
28
29         props.onChange(id);
30     },
31     toggleItemOpen: (id, status, pickerKind) => {
32         dispatch<any>(toggleItemOpen(id, status, pickerKind));
33     }
34 });
35
36 const toggleItemOpen = (id: string, status: TreeItemStatus, pickerKind: string) =>
37     (dispatch: Dispatch, getState: () => RootState, services: ServiceRepository) => {
38         if (status === TreeItemStatus.INITIAL) {
39             if (pickerKind === TreePickerKind.PROJECTS) {
40                 dispatch<any>(loadProjectTreePickerProjects(id));
41             } else if (pickerKind === TreePickerKind.FAVORITES) {
42                 dispatch<any>(loadFavoriteTreePickerProjects(id === services.authService.getUuid() ? '' : id));
43             } else {
44                 // TODO: load sharedWithMe
45             }
46         } else {
47             dispatch(treePickerActions.TOGGLE_TREE_PICKER_NODE_COLLAPSE({ id, pickerKind }));
48         }
49     };
50
51 const getNotSelectedTreePickerKind = (pickerKind: string) => {
52     return [TreePickerKind.PROJECTS, TreePickerKind.FAVORITES, TreePickerKind.SHARED_WITH_ME].filter(id => id !== pickerKind);
53 };
54
55 export enum TreePickerKind {
56     PROJECTS = 'Projects',
57     SHARED_WITH_ME = 'Shared with me',
58     FAVORITES = 'Favorites'
59 }
60
61 export const ProjectTreePicker = connect(undefined, mapDispatchToProps)((props: ProjectTreePickerProps) =>
62     <div style={{ display: 'flex', flexDirection: 'column' }}>
63         <Typography variant='caption' style={{ flexShrink: 0 }}>
64             Select a project
65         </Typography>
66         <div style={{ flexGrow: 1, overflow: 'auto' }}>
67             <TreePicker {...props} render={renderTreeItem} pickerKind={TreePickerKind.PROJECTS} />
68             <TreePicker {...props} render={renderTreeItem} pickerKind={TreePickerKind.SHARED_WITH_ME} />
69             <TreePicker {...props} render={renderTreeItem} pickerKind={TreePickerKind.FAVORITES} />
70         </div>
71     </div>);
72
73
74 // TODO: move action creator to store directory
75 export const loadProjectTreePickerProjects = (id: string) =>
76     async (dispatch: Dispatch, getState: () => RootState, services: ServiceRepository) => {
77         dispatch(treePickerActions.LOAD_TREE_PICKER_NODE({ id, pickerKind: TreePickerKind.PROJECTS }));
78
79         const ownerUuid = id.length === 0 ? services.authService.getUuid() || '' : id;
80
81         const filters = new FilterBuilder()
82             .addEqual('ownerUuid', ownerUuid)
83             .getFilters();
84
85         const { items } = await services.projectService.list({ filters });
86
87         dispatch<any>(receiveTreePickerData(id, items, TreePickerKind.PROJECTS));
88     };
89
90 export const loadFavoriteTreePickerProjects = (id: string) =>
91     async (dispatch: Dispatch, getState: () => RootState, services: ServiceRepository) => {
92         const parentId = services.authService.getUuid() || '';
93
94         if (id === '') {
95             dispatch(treePickerActions.LOAD_TREE_PICKER_NODE({ id: parentId, pickerKind: TreePickerKind.FAVORITES }));
96             const { items } = await services.favoriteService.list(parentId);
97
98             dispatch<any>(receiveTreePickerData(parentId, items as ProjectResource[], TreePickerKind.FAVORITES));
99         } else {
100             dispatch(treePickerActions.LOAD_TREE_PICKER_NODE({ id, pickerKind: TreePickerKind.FAVORITES }));
101             const filters = new FilterBuilder()
102                 .addEqual('ownerUuid', id)
103                 .getFilters();
104
105             const { items } = await services.projectService.list({ filters });
106
107             dispatch<any>(receiveTreePickerData(id, items, TreePickerKind.FAVORITES));
108         }
109
110     };
111
112 const getProjectPickerIcon = (item: TreeItem<ProjectResource>) => {
113     switch (item.data.name) {
114         case TreePickerKind.FAVORITES:
115             return FavoriteIcon;
116         case TreePickerKind.PROJECTS:
117             return ProjectsIcon;
118         case TreePickerKind.SHARED_WITH_ME:
119             return ShareMeIcon;
120         default:
121             return ProjectIcon;
122     }
123 };
124
125 const renderTreeItem = (item: TreeItem<ProjectResource>) =>
126     <ListItemTextIcon
127         icon={getProjectPickerIcon(item)}
128         name={item.data.name}
129         isActive={item.active}
130         hasMargin={true} />;
131
132
133 // TODO: move action creator to store directory
134 export const receiveTreePickerData = (id: string, projects: ProjectResource[], pickerKind: string) =>
135     (dispatch: Dispatch) => {
136         dispatch(treePickerActions.LOAD_TREE_PICKER_NODE_SUCCESS({
137             id,
138             nodes: projects.map(project => createTreePickerNode({ id: project.uuid, value: project })),
139             pickerKind,
140         }));
141
142         dispatch(treePickerActions.TOGGLE_TREE_PICKER_NODE_COLLAPSE({ id, pickerKind }));
143     };
144
145 export const initPickerProjectTree = () => (dispatch: Dispatch, getState: () => RootState, services: ServiceRepository) => {
146     const uuid = services.authService.getUuid();
147
148     dispatch<any>(getPickerTreeProjects(uuid));
149     dispatch<any>(getSharedWithMeProjectsPickerTree(uuid));
150     dispatch<any>(getFavoritesProjectsPickerTree(uuid));
151 };
152
153 const getPickerTreeProjects = (uuid: string = '') => {
154     return getProjectsPickerTree(uuid, TreePickerKind.PROJECTS);
155 };
156
157 const getSharedWithMeProjectsPickerTree = (uuid: string = '') => {
158     return getProjectsPickerTree(uuid, TreePickerKind.SHARED_WITH_ME);
159 };
160
161 const getFavoritesProjectsPickerTree = (uuid: string = '') => {
162     return getProjectsPickerTree(uuid, TreePickerKind.FAVORITES);
163 };
164
165 const getProjectsPickerTree = (uuid: string, kind: string) => {
166     return receiveTreePickerData(
167         '',
168         [mockProjectResource({ uuid, name: kind })],
169         kind
170     );
171 };
172