Skip to content

Commit 051bd7c

Browse files
feat(content-sharing): Render notification for sendInvitations (#4347)
* feat(content-sharing): Render notification for sendInvitations * feat(content-sharing): Render notification for sendInvitations * fix: nits --------- Co-authored-by: mergify[bot] <37929162+mergify[bot]@users.noreply.github.com>
1 parent 051319c commit 051bd7c

7 files changed

Lines changed: 205 additions & 23 deletions

File tree

i18n/en-US.properties

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -149,6 +149,10 @@ be.contentSharing.noAccessError = You do not have access to this item.
149149
# Message that appears when the item for the ContentSharing Element cannot be found.
150150
be.contentSharing.notFoundError = Could not find shared link for this item.
151151
# Message that appears when collaborators cannot be added to the shared link in the ContentSharing Element.
152+
be.contentSharing.sendInvitationsError = {count, plural, one {Failed to invite a collaborator.} other {Failed to invite {count} collaborators.}}
153+
# Message that appears when collaborators were added to the shared link in the ContentSharing Element.
154+
be.contentSharing.sendInvitationsSuccess = {count, plural, one {Successfully invited a collaborator.} other {Successfully invited {count} collaborators.}}
155+
# Message that appears when collaborators cannot be added to the shared link in the ContentSharing Element.
152156
be.contentSharing.sendInvitesError = Could not send invites.
153157
# Message that appears when collaborators were added to the shared link in the ContentSharing Element.
154158
be.contentSharing.sendInvitesSuccess = Successfully invited collaborators.

src/elements/content-sharing/hooks/__tests__/useSharingService.test.ts

Lines changed: 93 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,13 @@ jest.mock('../../utils/convertItemResponse');
1010
jest.mock('../../utils/convertCollaborators');
1111
jest.mock('../../sharingService');
1212
jest.mock('../useInvites');
13+
const mockFormatMessage = jest.fn(({ defaultMessage }) => defaultMessage);
14+
jest.mock('react-intl', () => ({
15+
...jest.requireActual('react-intl'),
16+
useIntl: () => ({
17+
formatMessage: mockFormatMessage,
18+
}),
19+
}));
1320

1421
const mockApi = {
1522
getFileAPI: jest.fn(),
@@ -229,7 +236,7 @@ describe('elements/content-sharing/hooks/useSharingService', () => {
229236
});
230237
});
231238

232-
describe('sendInvitations', () => {
239+
describe('handleSendInvitations', () => {
233240
const mockCollaborators = [{ id: 'collab-1', email: 'existing@example.com', type: 'user' }];
234241
const mockAvatarUrlMap = { 'user-1': 'https://example.com/avatar.jpg' };
235242
const mockCurrentUserId = 'current-user-123';
@@ -293,5 +300,90 @@ describe('elements/content-sharing/hooks/useSharingService', () => {
293300

294301
expect(convertCollabsRequest).toHaveBeenCalledWith(mockCollabRequest, mockCollaborators);
295302
});
303+
304+
describe('sendInvitations notification rendering', () => {
305+
const mockContacts = [
306+
{ id: 'user-1', email: 'user1@test.com', type: 'user' },
307+
{ id: 'user-2', email: 'user2@test.com', type: 'user' },
308+
{ id: 'user-3', email: 'user3@test.com', type: 'user' },
309+
];
310+
311+
test('should return success notification when all contacts are successfully invited', async () => {
312+
const mockResult = [
313+
{ id: 'result-1', email: 'user1@test.com' },
314+
{ id: 'result-2', email: 'user2@test.com' },
315+
{ id: 'result-3', email: 'user3@test.com' },
316+
];
317+
mockSendInvitations.mockResolvedValue(mockResult);
318+
const { result } = renderHookWithProps();
319+
320+
const sendInvitationsResult = await result.current.sharingService.sendInvitations({
321+
contacts: mockContacts,
322+
role: 'editor',
323+
});
324+
325+
expect(mockFormatMessage).toHaveBeenCalledWith(
326+
expect.objectContaining({ id: 'be.contentSharing.sendInvitationsSuccess' }),
327+
{ count: 3 }, // Counts of successfully invited collaborators
328+
);
329+
expect(sendInvitationsResult.messages[0].type).toEqual('success');
330+
});
331+
332+
test('should return correct notification when some invitations are invited', async () => {
333+
const mockResult = [
334+
{ id: 'result-1', email: 'user1@test.com' },
335+
{ id: 'result-2', email: 'user2@test.com' },
336+
];
337+
mockSendInvitations.mockResolvedValue(mockResult);
338+
const { result } = renderHookWithProps();
339+
340+
const sendInvitationsResult = await result.current.sharingService.sendInvitations({
341+
contacts: mockContacts,
342+
role: 'editor',
343+
});
344+
345+
expect(mockFormatMessage).toHaveBeenNthCalledWith(
346+
1,
347+
expect.objectContaining({ id: 'be.contentSharing.sendInvitationsError' }),
348+
{ count: 1 }, // Counts of invitations not sent
349+
);
350+
expect(mockFormatMessage).toHaveBeenNthCalledWith(
351+
2,
352+
expect.objectContaining({ id: 'be.contentSharing.sendInvitationsSuccess' }),
353+
{ count: 2 }, // Counts of successfully invited collaborators
354+
);
355+
expect(sendInvitationsResult.messages[0].type).toEqual('error');
356+
expect(sendInvitationsResult.messages[1].type).toEqual('success');
357+
});
358+
359+
test('should return error notification when no contacts are successfully invited', async () => {
360+
const mockResult = [];
361+
mockSendInvitations.mockResolvedValue(mockResult);
362+
const { result } = renderHookWithProps();
363+
364+
const sendInvitationsResult = await result.current.sharingService.sendInvitations({
365+
contacts: mockContacts,
366+
role: 'editor',
367+
});
368+
369+
expect(mockFormatMessage).toHaveBeenCalledWith(
370+
expect.objectContaining({ id: 'be.contentSharing.sendInvitationsError' }),
371+
{ count: 3 }, // Counts of invitations not sent
372+
);
373+
expect(sendInvitationsResult.messages[0].type).toEqual('error');
374+
});
375+
376+
test('should return null when no result is returned from handleSendInvitations', async () => {
377+
mockSendInvitations.mockResolvedValue(null);
378+
const { result } = renderHookWithProps();
379+
380+
const sendInvitationsResult = await result.current.sharingService.sendInvitations({
381+
contacts: mockContacts,
382+
role: 'editor',
383+
});
384+
385+
expect(sendInvitationsResult).toBeNull();
386+
});
387+
});
296388
});
297389
});

src/elements/content-sharing/hooks/useContactService.ts

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,9 @@ export const useContactService = (api, itemId, currentUserId) => {
2525

2626
const getContactsAvatarUrls = React.useCallback(
2727
async contacts => {
28-
if (!contacts || contacts.length === 0) return Promise.resolve({});
28+
if (!contacts || contacts.length === 0) {
29+
return Promise.resolve({});
30+
}
2931

3032
const collaborators = contacts.map(contact => ({
3133
accessible_by: {

src/elements/content-sharing/hooks/useSharingService.ts

Lines changed: 35 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,13 @@
11
import * as React from 'react';
2+
import { useIntl } from 'react-intl';
23

34
import { TYPE_FILE, TYPE_FOLDER } from '../../../constants';
45
import { convertItemResponse, convertCollab, convertCollabsRequest } from '../utils';
56
import { createSharingService } from '../sharingService';
67
import useInvites from './useInvites';
78

9+
import messages from '../messages';
10+
811
export const useSharingService = ({
912
api,
1013
avatarUrlMap,
@@ -19,6 +22,8 @@ export const useSharingService = ({
1922
setItem,
2023
setSharedLink,
2124
}) => {
25+
const { formatMessage } = useIntl();
26+
2227
// itemApiInstance should only be called once or the API will cause an issue where it gets cancelled
2328
const itemApiInstance = React.useMemo(() => {
2429
if (!item || !sharedLink) {
@@ -78,23 +83,51 @@ export const useSharingService = ({
7883
const ownerEmailDomain = ownerEmail && /@/.test(ownerEmail) ? ownerEmail.split('@')[1] : null;
7984
setCollaborators(prevList => {
8085
const newCollab = convertCollab({
86+
avatarUrlMap,
8187
collab: response,
8288
currentUserId,
8389
isCurrentUserOwner: currentUserId === ownerId,
8490
ownerEmailDomain,
85-
avatarUrlMap,
8691
});
8792

8893
return newCollab ? [...prevList, newCollab] : prevList;
8994
});
9095
};
9196

92-
const sendInvitations = useInvites(api, itemId, itemType, {
97+
const handleSendInvitations = useInvites(api, itemId, itemType, {
9398
collaborators,
9499
handleSuccess,
95100
isContentSharingV2Enabled: true,
96101
transformRequest: data => convertCollabsRequest(data, collaborators),
97102
});
98103

104+
const sendInvitations = (...request) => {
105+
return handleSendInvitations(...request).then(response => {
106+
const { contacts: collabRequest } = request[0];
107+
if (!response || !collabRequest || collabRequest.length === 0) {
108+
return null;
109+
}
110+
111+
const successCount = response.length;
112+
const errorCount = collabRequest.length - successCount;
113+
114+
const notification = [];
115+
if (errorCount > 0) {
116+
notification.push({
117+
text: formatMessage(messages.sendInvitationsError, { count: errorCount }),
118+
type: 'error',
119+
});
120+
}
121+
if (successCount > 0) {
122+
notification.push({
123+
text: formatMessage(messages.sendInvitationsSuccess, { count: successCount }),
124+
type: 'success',
125+
});
126+
}
127+
128+
return notification.length > 0 ? { messages: notification } : null;
129+
});
130+
};
131+
99132
return { sharingService: { ...sharingService, sendInvitations } };
100133
};

src/elements/content-sharing/messages.js

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -59,6 +59,20 @@ const messages = defineMessages({
5959
'Message that appears when collaborators were added to the shared link in the ContentSharing Element.',
6060
id: 'be.contentSharing.sendInvitesSuccess',
6161
},
62+
sendInvitationsError: {
63+
defaultMessage:
64+
'{count, plural, one {Failed to invite a collaborator.} other {Failed to invite {count} collaborators.}}',
65+
description:
66+
'Message that appears when collaborators cannot be added to the shared link in the ContentSharing Element.',
67+
id: 'be.contentSharing.sendInvitationsError',
68+
},
69+
sendInvitationsSuccess: {
70+
defaultMessage:
71+
'{count, plural, one {Successfully invited a collaborator.} other {Successfully invited {count} collaborators.}}',
72+
description:
73+
'Message that appears when collaborators were added to the shared link in the ContentSharing Element.',
74+
id: 'be.contentSharing.sendInvitationsSuccess',
75+
},
6276
groupContactLabel: {
6377
defaultMessage: 'Group',
6478
description: 'Display text for a Group contact type',

src/elements/content-sharing/utils/__tests__/convertCollaborators.test.ts

Lines changed: 46 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -64,11 +64,11 @@ describe('convertCollaborators', () => {
6464
describe('convertCollab', () => {
6565
test('should convert a valid collaboration to Collaborator format', () => {
6666
const result = convertCollab({
67+
avatarUrlMap: mockAvatarUrlMap,
6768
collab: mockCollaborations[1],
6869
currentUserId: mockOwnerId,
6970
isCurrentUserOwner: false,
7071
ownerEmailDomain,
71-
avatarUrlMap: mockAvatarUrlMap,
7272
});
7373

7474
expect(result).toEqual({
@@ -89,35 +89,35 @@ describe('convertCollaborators', () => {
8989

9090
test('should return null for collaboration with non-accepted status', () => {
9191
const result = convertCollab({
92+
avatarUrlMap: mockAvatarUrlMap,
9293
collab: mockCollaborations[3],
9394
currentUserId: mockOwnerId,
9495
isCurrentUserOwner: false,
9596
ownerEmailDomain,
96-
avatarUrlMap: mockAvatarUrlMap,
9797
});
9898

9999
expect(result).toBeNull();
100100
});
101101

102102
test.each([undefined, null])('should return null for %s collaboration', collab => {
103103
const result = convertCollab({
104+
avatarUrlMap: mockAvatarUrlMap,
104105
collab,
105106
currentUserId: mockOwnerId,
106107
isCurrentUserOwner: false,
107108
ownerEmailDomain,
108-
avatarUrlMap: mockAvatarUrlMap,
109109
});
110110

111111
expect(result).toBeNull();
112112
});
113113

114114
test('should identify current user correctly', () => {
115115
const result = convertCollab({
116+
avatarUrlMap: mockAvatarUrlMap,
116117
collab: mockCollaborations[0],
117118
currentUserId: mockOwnerId,
118119
isCurrentUserOwner: true,
119120
ownerEmailDomain,
120-
avatarUrlMap: mockAvatarUrlMap,
121121
});
122122

123123
expect(result).toEqual({
@@ -137,11 +137,11 @@ describe('convertCollaborators', () => {
137137

138138
test('should identify external user correctly', () => {
139139
const result = convertCollab({
140+
avatarUrlMap: mockAvatarUrlMap,
140141
collab: mockCollaborations[2],
141142
currentUserId: mockOwnerId,
142143
isCurrentUserOwner: false,
143144
ownerEmailDomain,
144-
avatarUrlMap: mockAvatarUrlMap,
145145
});
146146

147147
expect(result.isExternal).toBe(true);
@@ -151,11 +151,11 @@ describe('convertCollaborators', () => {
151151
'should handle %s avatar URL map',
152152
avatarUrlMap => {
153153
const result = convertCollab({
154+
avatarUrlMap,
154155
collab: mockCollaborations[1],
155156
currentUserId: mockOwnerId,
156157
isCurrentUserOwner: false,
157158
ownerEmailDomain,
158-
avatarUrlMap,
159159
});
160160

161161
expect(result.avatarUrl).toBeUndefined();
@@ -170,11 +170,11 @@ describe('convertCollaborators', () => {
170170
};
171171

172172
const result = convertCollab({
173+
avatarUrlMap: mockAvatarUrlMap,
173174
collab: collabWithoutExpiration,
174175
currentUserId: mockOwnerId,
175176
isCurrentUserOwner: false,
176177
ownerEmailDomain,
177-
avatarUrlMap: mockAvatarUrlMap,
178178
});
179179

180180
expect(result.expiresAt).toBeNull();
@@ -305,6 +305,45 @@ describe('convertCollaborators', () => {
305305
});
306306
});
307307

308+
test('should convert collab request with users without a type', () => {
309+
const mockCollabRequest = {
310+
role: 'editor',
311+
contacts: [
312+
{
313+
id: 'user1',
314+
email: 'user1@test.com',
315+
type: 'user',
316+
},
317+
{
318+
id: 'user2',
319+
email: 'external@test.com',
320+
},
321+
],
322+
};
323+
324+
const result = convertCollabsRequest(mockCollabRequest, null);
325+
326+
expect(result).toEqual({
327+
groups: [],
328+
users: [
329+
{
330+
accessible_by: {
331+
login: 'user1@test.com',
332+
type: 'user',
333+
},
334+
role: 'editor',
335+
},
336+
{
337+
accessible_by: {
338+
login: 'external@test.com',
339+
type: 'user',
340+
},
341+
role: 'editor',
342+
},
343+
],
344+
});
345+
});
346+
308347
test('should handle empty contacts array', () => {
309348
const emptyCollabRequest = {
310349
role: 'editor',

0 commit comments

Comments
 (0)