1 // Copyright (C) The Arvados Authors. All rights reserved.
3 // SPDX-License-Identifier: AGPL-3.0
5 import { dialogActions } from "store/dialog/dialog-actions";
6 import { withDialog } from "store/dialog/with-dialog";
9 SHARING_INVITATION_FORM_NAME,
10 SharingManagementFormData,
11 SharingInvitationFormData,
12 getSharingMangementFormData,
13 } from './sharing-dialog-types';
14 import { Dispatch } from 'redux';
15 import { ServiceRepository } from "services/services";
16 import { FilterBuilder } from 'services/api/filter-builder';
17 import { initialize, getFormValues, reset } from 'redux-form';
18 import { SHARING_MANAGEMENT_FORM_NAME } from 'store/sharing-dialog/sharing-dialog-types';
19 import { RootState } from 'store/store';
20 import { getDialog } from 'store/dialog/dialog-reducer';
21 import { PermissionLevel } from 'models/permission';
22 import { PermissionResource } from 'models/permission';
23 import { differenceWith } from "lodash";
24 import { withProgress } from "store/progress-indicator/with-progress";
25 import { progressIndicatorActions } from 'store/progress-indicator/progress-indicator-actions';
26 import { snackbarActions, SnackbarKind } from "../snackbar/snackbar-actions";
28 extractUuidObjectType,
30 } from "models/resource";
31 import { resourcesActions } from "store/resources/resources-actions";
33 export const openSharingDialog = (resourceUuid: string, refresh?: () => void) =>
34 (dispatch: Dispatch) => {
35 dispatch(dialogActions.OPEN_DIALOG({ id: SHARING_DIALOG_NAME, data: {resourceUuid, refresh} }));
36 dispatch<any>(loadSharingDialog);
39 export const closeSharingDialog = () =>
40 dialogActions.CLOSE_DIALOG({ id: SHARING_DIALOG_NAME });
42 export const connectSharingDialog = withDialog(SHARING_DIALOG_NAME);
43 export const connectSharingDialogProgress = withProgress(SHARING_DIALOG_NAME);
46 export const saveSharingDialogChanges = async (dispatch: Dispatch, getState: () => RootState) => {
47 dispatch(progressIndicatorActions.START_WORKING(SHARING_DIALOG_NAME));
48 await dispatch<any>(saveManagementChanges);
49 await dispatch<any>(sendInvitations);
50 dispatch(reset(SHARING_INVITATION_FORM_NAME));
51 await dispatch<any>(loadSharingDialog);
52 dispatch(progressIndicatorActions.STOP_WORKING(SHARING_DIALOG_NAME));
54 const dialog = getDialog<SharingDialogData>(getState().dialog, SHARING_DIALOG_NAME);
55 if (dialog && dialog.data.refresh) {
56 dialog.data.refresh();
60 export interface SharingDialogData {
65 export const createSharingToken = async (dispatch: Dispatch, getState: () => RootState, { apiClientAuthorizationService }: ServiceRepository) => {
66 const dialog = getDialog<SharingDialogData>(getState().dialog, SHARING_DIALOG_NAME);
68 const resourceUuid = dialog.data.resourceUuid;
69 if (extractUuidObjectType(resourceUuid) === ResourceObjectType.COLLECTION) {
70 dispatch(progressIndicatorActions.START_WORKING(SHARING_DIALOG_NAME));
72 const sharingToken = await apiClientAuthorizationService.createCollectionSharingToken(resourceUuid);
73 dispatch(resourcesActions.SET_RESOURCES([sharingToken]));
74 dispatch(snackbarActions.OPEN_SNACKBAR({
75 message: 'Sharing URL created',
77 kind: SnackbarKind.SUCCESS,
80 dispatch(snackbarActions.OPEN_SNACKBAR({
81 message: 'Failed to create sharing URL',
83 kind: SnackbarKind.ERROR,
86 dispatch(progressIndicatorActions.STOP_WORKING(SHARING_DIALOG_NAME));
92 export const deleteSharingToken = (uuid: string) => async (dispatch: Dispatch, getState: () => RootState, { apiClientAuthorizationService }: ServiceRepository) => {
93 dispatch(progressIndicatorActions.START_WORKING(SHARING_DIALOG_NAME));
95 await apiClientAuthorizationService.delete(uuid);
96 dispatch(resourcesActions.DELETE_RESOURCES([uuid]));
97 dispatch(snackbarActions.OPEN_SNACKBAR({
98 message: 'Sharing URL removed',
100 kind: SnackbarKind.SUCCESS,
103 dispatch(snackbarActions.OPEN_SNACKBAR({
104 message: 'Failed to remove sharing URL',
106 kind: SnackbarKind.ERROR,
109 dispatch(progressIndicatorActions.STOP_WORKING(SHARING_DIALOG_NAME));
113 const loadSharingDialog = async (dispatch: Dispatch, getState: () => RootState, { permissionService, apiClientAuthorizationService }: ServiceRepository) => {
115 const dialog = getDialog<SharingDialogData>(getState().dialog, SHARING_DIALOG_NAME);
117 dispatch(progressIndicatorActions.START_WORKING(SHARING_DIALOG_NAME));
119 const resourceUuid = dialog.data.resourceUuid;
120 const { items } = await permissionService.listResourcePermissions(resourceUuid);
121 await dispatch<any>(initializeManagementForm(items));
122 // For collections, we need to load the public sharing tokens
123 if (extractUuidObjectType(resourceUuid) === ResourceObjectType.COLLECTION) {
124 const sharingTokens = await apiClientAuthorizationService.listCollectionSharingTokens(resourceUuid);
125 dispatch(resourcesActions.SET_RESOURCES([...sharingTokens.items]));
128 dispatch(snackbarActions.OPEN_SNACKBAR({
129 message: 'You do not have access to share this item',
131 kind: SnackbarKind.ERROR }));
132 dispatch(dialogActions.CLOSE_DIALOG({ id: SHARING_DIALOG_NAME }));
134 dispatch(progressIndicatorActions.STOP_WORKING(SHARING_DIALOG_NAME));
139 const initializeManagementForm = (permissionLinks: PermissionResource[]) =>
140 async (dispatch: Dispatch, getState: () => RootState, { userService, groupsService }: ServiceRepository) => {
142 const filters = new FilterBuilder()
143 .addIn('uuid', permissionLinks.map(({ tailUuid }) => tailUuid))
146 const { items: users } = await userService.list({ filters, count: "none" });
147 const { items: groups } = await groupsService.list({ filters, count: "none" });
149 const getEmail = (tailUuid: string) => {
150 const user = users.find(({ uuid }) => uuid === tailUuid);
151 const group = groups.find(({ uuid }) => uuid === tailUuid);
159 const managementPermissions = permissionLinks
160 .map(({ tailUuid, name, uuid }) => ({
161 email: getEmail(tailUuid),
162 permissions: name as PermissionLevel,
163 permissionUuid: uuid,
166 const managementFormData: SharingManagementFormData = {
167 permissions: managementPermissions,
168 initialPermissions: managementPermissions,
171 dispatch(initialize(SHARING_MANAGEMENT_FORM_NAME, managementFormData));
174 const saveManagementChanges = async (_: Dispatch, getState: () => RootState, { permissionService }: ServiceRepository) => {
175 const state = getState();
176 const { user } = state.auth;
177 const dialog = getDialog<string>(state.dialog, SHARING_DIALOG_NAME);
178 if (dialog && user) {
179 const { initialPermissions, permissions } = getSharingMangementFormData(state);
180 const cancelledPermissions = differenceWith(
183 (a, b) => a.permissionUuid === b.permissionUuid
186 const deletions = cancelledPermissions.map(({ permissionUuid }) =>
187 permissionService.delete(permissionUuid));
188 const updates = permissions.map(update =>
189 permissionService.update(update.permissionUuid, { name: update.permissions }));
190 await Promise.all([...deletions, ...updates]);
194 const sendInvitations = async (_: Dispatch, getState: () => RootState, { permissionService }: ServiceRepository) => {
195 const state = getState();
196 const { user } = state.auth;
197 const dialog = getDialog<SharingDialogData>(state.dialog, SHARING_DIALOG_NAME);
198 if (dialog && user) {
199 const invitations = getFormValues(SHARING_INVITATION_FORM_NAME)(state) as SharingInvitationFormData;
200 const data = invitations.invitedPeople.map(invitee => ({
201 ownerUuid: user.uuid,
202 headUuid: dialog.data.resourceUuid,
203 tailUuid: invitee.uuid,
204 name: invitations.permissions
206 const changes = data.map( invitation => permissionService.create(invitation));
207 await Promise.all(changes);