15768: fixed single v multi move bug Arvados-DCO-1.1-Signed-off-by: Lisa Knox <lisa...
[arvados.git] / src / store / context-menu / context-menu-actions.ts
1 // Copyright (C) The Arvados Authors. All rights reserved.
2 //
3 // SPDX-License-Identifier: AGPL-3.0
4
5 import { unionize, ofType, UnionOf } from "common/unionize";
6 import { ContextMenuPosition } from "./context-menu-reducer";
7 import { ContextMenuKind } from "views-components/context-menu/context-menu";
8 import { Dispatch } from "redux";
9 import { RootState } from "store/store";
10 import { getResource, getResourceWithEditableStatus } from "../resources/resources";
11 import { UserResource } from "models/user";
12 import { isSidePanelTreeCategory } from "store/side-panel-tree/side-panel-tree-actions";
13 import { extractUuidKind, ResourceKind, EditableResource, Resource } from "models/resource";
14 import { Process } from "store/processes/process";
15 import { RepositoryResource } from "models/repositories";
16 import { SshKeyResource } from "models/ssh-key";
17 import { VirtualMachinesResource } from "models/virtual-machines";
18 import { KeepServiceResource } from "models/keep-services";
19 import { ProcessResource } from "models/process";
20 import { CollectionResource } from "models/collection";
21 import { GroupClass, GroupResource } from "models/group";
22 import { GroupContentsResource } from "services/groups-service/groups-service";
23 import { LinkResource } from "models/link";
24 import { resourceIsFrozen } from "common/frozen-resources";
25 import { ProjectResource } from "models/project";
26
27 export const contextMenuActions = unionize({
28     OPEN_CONTEXT_MENU: ofType<{ position: ContextMenuPosition; resource: ContextMenuResource }>(),
29     CLOSE_CONTEXT_MENU: ofType<{}>(),
30 });
31
32 export type ContextMenuAction = UnionOf<typeof contextMenuActions>;
33
34 export type ContextMenuResource = {
35     name: string;
36     uuid: string;
37     ownerUuid: string;
38     description?: string;
39     kind: ResourceKind;
40     menuKind: ContextMenuKind | string;
41     isTrashed?: boolean;
42     isEditable?: boolean;
43     outputUuid?: string;
44     workflowUuid?: string;
45     isAdmin?: boolean;
46     isFrozen?: boolean;
47     storageClassesDesired?: string[];
48     properties?: { [key: string]: string | string[] };
49     isMulti?: boolean;
50 };
51
52 export const isKeyboardClick = (event: React.MouseEvent<HTMLElement>) => event.nativeEvent.detail === 0;
53
54 export const openContextMenu = (event: React.MouseEvent<HTMLElement>, resource: ContextMenuResource) => (dispatch: Dispatch) => {
55     event.preventDefault();
56     const { left, top } = event.currentTarget.getBoundingClientRect();
57     dispatch(
58         contextMenuActions.OPEN_CONTEXT_MENU({
59             position: {
60                 x: event.clientX || left,
61                 y: event.clientY || top,
62             },
63             resource,
64         })
65     );
66 };
67
68 export const openCollectionFilesContextMenu =
69     (event: React.MouseEvent<HTMLElement>, isWritable: boolean) => (dispatch: Dispatch, getState: () => RootState) => {
70         const isCollectionFileSelected = JSON.stringify(getState().collectionPanelFiles).includes('"selected":true');
71         dispatch<any>(
72             openContextMenu(event, {
73                 name: "",
74                 uuid: "",
75                 ownerUuid: "",
76                 description: "",
77                 kind: ResourceKind.COLLECTION,
78                 menuKind: isCollectionFileSelected
79                     ? isWritable
80                         ? ContextMenuKind.COLLECTION_FILES
81                         : ContextMenuKind.READONLY_COLLECTION_FILES
82                     : ContextMenuKind.COLLECTION_FILES_NOT_SELECTED,
83             })
84         );
85     };
86
87 export const openRepositoryContextMenu =
88     (event: React.MouseEvent<HTMLElement>, repository: RepositoryResource) => (dispatch: Dispatch, getState: () => RootState) => {
89         dispatch<any>(
90             openContextMenu(event, {
91                 name: "",
92                 uuid: repository.uuid,
93                 ownerUuid: repository.ownerUuid,
94                 kind: ResourceKind.REPOSITORY,
95                 menuKind: ContextMenuKind.REPOSITORY,
96             })
97         );
98     };
99
100 export const openVirtualMachinesContextMenu =
101     (event: React.MouseEvent<HTMLElement>, repository: VirtualMachinesResource) => (dispatch: Dispatch, getState: () => RootState) => {
102         dispatch<any>(
103             openContextMenu(event, {
104                 name: "",
105                 uuid: repository.uuid,
106                 ownerUuid: repository.ownerUuid,
107                 kind: ResourceKind.VIRTUAL_MACHINE,
108                 menuKind: ContextMenuKind.VIRTUAL_MACHINE,
109             })
110         );
111     };
112
113 export const openSshKeyContextMenu = (event: React.MouseEvent<HTMLElement>, sshKey: SshKeyResource) => (dispatch: Dispatch) => {
114     dispatch<any>(
115         openContextMenu(event, {
116             name: "",
117             uuid: sshKey.uuid,
118             ownerUuid: sshKey.ownerUuid,
119             kind: ResourceKind.SSH_KEY,
120             menuKind: ContextMenuKind.SSH_KEY,
121         })
122     );
123 };
124
125 export const openKeepServiceContextMenu = (event: React.MouseEvent<HTMLElement>, keepService: KeepServiceResource) => (dispatch: Dispatch) => {
126     dispatch<any>(
127         openContextMenu(event, {
128             name: "",
129             uuid: keepService.uuid,
130             ownerUuid: keepService.ownerUuid,
131             kind: ResourceKind.KEEP_SERVICE,
132             menuKind: ContextMenuKind.KEEP_SERVICE,
133         })
134     );
135 };
136
137 export const openApiClientAuthorizationContextMenu = (event: React.MouseEvent<HTMLElement>, resourceUuid: string) => (dispatch: Dispatch) => {
138     dispatch<any>(
139         openContextMenu(event, {
140             name: "",
141             uuid: resourceUuid,
142             ownerUuid: "",
143             kind: ResourceKind.API_CLIENT_AUTHORIZATION,
144             menuKind: ContextMenuKind.API_CLIENT_AUTHORIZATION,
145         })
146     );
147 };
148
149 export const openRootProjectContextMenu =
150     (event: React.MouseEvent<HTMLElement>, projectUuid: string) => (dispatch: Dispatch, getState: () => RootState) => {
151         const res = getResource<UserResource>(projectUuid)(getState().resources);
152         if (res) {
153             dispatch<any>(
154                 openContextMenu(event, {
155                     name: "",
156                     uuid: res.uuid,
157                     ownerUuid: res.uuid,
158                     kind: res.kind,
159                     menuKind: ContextMenuKind.ROOT_PROJECT,
160                     isTrashed: false,
161                 })
162             );
163         }
164     };
165
166 export const openProjectContextMenu =
167     (event: React.MouseEvent<HTMLElement>, resourceUuid: string) => (dispatch: Dispatch, getState: () => RootState) => {
168         const res = getResource<GroupContentsResource>(resourceUuid)(getState().resources);
169         const menuKind = dispatch<any>(resourceUuidToContextMenuKind(resourceUuid));
170         if (res && menuKind) {
171             dispatch<any>(
172                 openContextMenu(event, {
173                     name: res.name,
174                     uuid: res.uuid,
175                     kind: res.kind,
176                     menuKind,
177                     description: res.description,
178                     ownerUuid: res.ownerUuid,
179                     isTrashed: "isTrashed" in res ? res.isTrashed : false,
180                     isFrozen: !!(res as ProjectResource).frozenByUuid,
181                 })
182             );
183         }
184     };
185
186 export const openSidePanelContextMenu = (event: React.MouseEvent<HTMLElement>, id: string) => (dispatch: Dispatch, getState: () => RootState) => {
187     if (!isSidePanelTreeCategory(id)) {
188         const kind = extractUuidKind(id);
189         if (kind === ResourceKind.USER) {
190             dispatch<any>(openRootProjectContextMenu(event, id));
191         } else if (kind === ResourceKind.PROJECT) {
192             dispatch<any>(openProjectContextMenu(event, id));
193         }
194     }
195 };
196
197 export const openProcessContextMenu = (event: React.MouseEvent<HTMLElement>, process: Process) => (dispatch: Dispatch, getState: () => RootState) => {
198     const res = getResource<ProcessResource>(process.containerRequest.uuid)(getState().resources);
199     if (res) {
200         dispatch<any>(
201             openContextMenu(event, {
202                 uuid: res.uuid,
203                 ownerUuid: res.ownerUuid,
204                 kind: ResourceKind.PROCESS,
205                 name: res.name,
206                 description: res.description,
207                 outputUuid: res.outputUuid || "",
208                 workflowUuid: res.properties.template_uuid || "",
209                 menuKind: ContextMenuKind.PROCESS_RESOURCE,
210             })
211         );
212     }
213 };
214
215 export const openPermissionEditContextMenu =
216     (event: React.MouseEvent<HTMLElement>, link: LinkResource) => (dispatch: Dispatch, getState: () => RootState) => {
217         if (link) {
218             dispatch<any>(
219                 openContextMenu(event, {
220                     name: link.name,
221                     uuid: link.uuid,
222                     kind: link.kind,
223                     menuKind: ContextMenuKind.PERMISSION_EDIT,
224                     ownerUuid: link.ownerUuid,
225                 })
226             );
227         }
228     };
229
230 export const openUserContextMenu = (event: React.MouseEvent<HTMLElement>, user: UserResource) => (dispatch: Dispatch, getState: () => RootState) => {
231     dispatch<any>(
232         openContextMenu(event, {
233             name: "",
234             uuid: user.uuid,
235             ownerUuid: user.ownerUuid,
236             kind: user.kind,
237             menuKind: ContextMenuKind.USER,
238         })
239     );
240 };
241
242 export const resourceUuidToContextMenuKind =
243     (uuid: string, readonly = false) =>
244     (dispatch: Dispatch, getState: () => RootState) => {
245         const { isAdmin: isAdminUser, uuid: userUuid } = getState().auth.user!;
246         const kind = extractUuidKind(uuid);
247         const resource = getResourceWithEditableStatus<GroupResource & EditableResource>(uuid, userUuid)(getState().resources);
248         const isFrozen = resourceIsFrozen(resource, getState().resources);
249         const isEditable = (isAdminUser || (resource || ({} as EditableResource)).isEditable) && !readonly && !isFrozen;
250
251         switch (kind) {
252             case ResourceKind.PROJECT:
253                 if (isFrozen) {
254                     return isAdminUser ? ContextMenuKind.FROZEN_PROJECT_ADMIN : ContextMenuKind.FROZEN_PROJECT;
255                 }
256
257                 return isAdminUser && !readonly
258                     ? resource && resource.groupClass !== GroupClass.FILTER
259                         ? ContextMenuKind.PROJECT_ADMIN
260                         : ContextMenuKind.FILTER_GROUP_ADMIN
261                     : isEditable
262                     ? resource && resource.groupClass !== GroupClass.FILTER
263                         ? ContextMenuKind.PROJECT
264                         : ContextMenuKind.FILTER_GROUP
265                     : ContextMenuKind.READONLY_PROJECT;
266             case ResourceKind.COLLECTION:
267                 const c = getResource<CollectionResource>(uuid)(getState().resources);
268                 if (c === undefined) {
269                     return;
270                 }
271                 const isOldVersion = c.uuid !== c.currentVersionUuid;
272                 const isTrashed = c.isTrashed;
273                 return isOldVersion
274                     ? ContextMenuKind.OLD_VERSION_COLLECTION
275                     : isTrashed && isEditable
276                     ? ContextMenuKind.TRASHED_COLLECTION
277                     : isAdminUser && isEditable
278                     ? ContextMenuKind.COLLECTION_ADMIN
279                     : isEditable
280                     ? ContextMenuKind.COLLECTION
281                     : ContextMenuKind.READONLY_COLLECTION;
282             case ResourceKind.PROCESS:
283                 return isAdminUser && isEditable
284                     ? ContextMenuKind.PROCESS_ADMIN
285                     : readonly
286                     ? ContextMenuKind.READONLY_PROCESS_RESOURCE
287                     : ContextMenuKind.PROCESS_RESOURCE;
288             case ResourceKind.USER:
289                 return ContextMenuKind.ROOT_PROJECT;
290             case ResourceKind.LINK:
291                 return ContextMenuKind.LINK;
292             case ResourceKind.WORKFLOW:
293                 return ContextMenuKind.WORKFLOW;
294             default:
295                 return;
296         }
297     };
298
299 export const openSearchResultsContextMenu =
300     (event: React.MouseEvent<HTMLElement>, uuid: string) => (dispatch: Dispatch, getState: () => RootState) => {
301         const res = getResource<Resource>(uuid)(getState().resources);
302         if (res) {
303             dispatch<any>(
304                 openContextMenu(event, {
305                     name: "",
306                     uuid: res.uuid,
307                     ownerUuid: "",
308                     kind: res.kind,
309                     menuKind: ContextMenuKind.SEARCH_RESULTS,
310                 })
311             );
312         }
313     };