Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions i18n/en-US.properties
Original file line number Diff line number Diff line change
Expand Up @@ -149,6 +149,10 @@ be.contentSharing.noAccessError = You do not have access to this item.
# Message that appears when the item for the ContentSharing Element cannot be found.
be.contentSharing.notFoundError = Could not find shared link for this item.
# Message that appears when collaborators cannot be added to the shared link in the ContentSharing Element.
be.contentSharing.sendInvitationsError = {count, plural, one {Failed to invite a collaborator.} other {Failed to invite {count} collaborators.}}
# Message that appears when collaborators were added to the shared link in the ContentSharing Element.
be.contentSharing.sendInvitationsSuccess = {count, plural, one {Successfully invited a collaborator.} other {Successfully invited {count} collaborators.}}
# Message that appears when collaborators cannot be added to the shared link in the ContentSharing Element.
be.contentSharing.sendInvitesError = Could not send invites.
# Message that appears when collaborators were added to the shared link in the ContentSharing Element.
be.contentSharing.sendInvitesSuccess = Successfully invited collaborators.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,13 @@ jest.mock('../../utils/convertItemResponse');
jest.mock('../../utils/convertCollaborators');
jest.mock('../../sharingService');
jest.mock('../useInvites');
const mockFormatMessage = jest.fn(({ defaultMessage }) => defaultMessage);
jest.mock('react-intl', () => ({
...jest.requireActual('react-intl'),
useIntl: () => ({
formatMessage: mockFormatMessage,
}),
}));

const mockApi = {
getFileAPI: jest.fn(),
Expand Down Expand Up @@ -229,7 +236,7 @@ describe('elements/content-sharing/hooks/useSharingService', () => {
});
});

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

expect(convertCollabsRequest).toHaveBeenCalledWith(mockCollabRequest, mockCollaborators);
});

describe('sendInvitations notification rendering', () => {
const mockContacts = [
{ id: 'user-1', email: 'user1@test.com', type: 'user' },
{ id: 'user-2', email: 'user2@test.com', type: 'user' },
{ id: 'user-3', email: 'user3@test.com', type: 'user' },
];

test('should return success notification when all contacts are successfully invited', async () => {
const mockResult = [
{ id: 'result-1', email: 'user1@test.com' },
{ id: 'result-2', email: 'user2@test.com' },
{ id: 'result-3', email: 'user3@test.com' },
];
mockSendInvitations.mockResolvedValue(mockResult);
const { result } = renderHookWithProps();

const sendInvitationsResult = await result.current.sharingService.sendInvitations({
contacts: mockContacts,
role: 'editor',
});

expect(mockFormatMessage).toHaveBeenCalledWith(
expect.objectContaining({ id: 'be.contentSharing.sendInvitationsSuccess' }),
{ count: 3 }, // Counts of successfully invited collaborators
);
expect(sendInvitationsResult.messages[0].type).toEqual('success');
});

test('should return correct notification when some invitations are invited', async () => {
const mockResult = [
{ id: 'result-1', email: 'user1@test.com' },
{ id: 'result-2', email: 'user2@test.com' },
];
mockSendInvitations.mockResolvedValue(mockResult);
const { result } = renderHookWithProps();

const sendInvitationsResult = await result.current.sharingService.sendInvitations({
contacts: mockContacts,
role: 'editor',
});

expect(mockFormatMessage).toHaveBeenNthCalledWith(
1,
expect.objectContaining({ id: 'be.contentSharing.sendInvitationsError' }),
{ count: 1 }, // Counts of invitations not sent
);
expect(mockFormatMessage).toHaveBeenNthCalledWith(
2,
expect.objectContaining({ id: 'be.contentSharing.sendInvitationsSuccess' }),
{ count: 2 }, // Counts of successfully invited collaborators
);
expect(sendInvitationsResult.messages[0].type).toEqual('error');
expect(sendInvitationsResult.messages[1].type).toEqual('success');
});

test('should return error notification when no contacts are successfully invited', async () => {
const mockResult = [];
mockSendInvitations.mockResolvedValue(mockResult);
const { result } = renderHookWithProps();

const sendInvitationsResult = await result.current.sharingService.sendInvitations({
contacts: mockContacts,
role: 'editor',
});

expect(mockFormatMessage).toHaveBeenCalledWith(
expect.objectContaining({ id: 'be.contentSharing.sendInvitationsError' }),
{ count: 3 }, // Counts of invitations not sent
);
expect(sendInvitationsResult.messages[0].type).toEqual('error');
});

test('should return null when no result is returned from handleSendInvitations', async () => {
mockSendInvitations.mockResolvedValue(null);
const { result } = renderHookWithProps();

const sendInvitationsResult = await result.current.sharingService.sendInvitations({
contacts: mockContacts,
role: 'editor',
});

expect(sendInvitationsResult).toBeNull();
});
});
});
});
4 changes: 3 additions & 1 deletion src/elements/content-sharing/hooks/useContactService.ts
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,9 @@ export const useContactService = (api, itemId, currentUserId) => {

const getContactsAvatarUrls = React.useCallback(
async contacts => {
if (!contacts || contacts.length === 0) return Promise.resolve({});
if (!contacts || contacts.length === 0) {
return Promise.resolve({});
}

const collaborators = contacts.map(contact => ({
accessible_by: {
Expand Down
37 changes: 35 additions & 2 deletions src/elements/content-sharing/hooks/useSharingService.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,13 @@
import * as React from 'react';
import { useIntl } from 'react-intl';

import { TYPE_FILE, TYPE_FOLDER } from '../../../constants';
import { convertItemResponse, convertCollab, convertCollabsRequest } from '../utils';
import { createSharingService } from '../sharingService';
import useInvites from './useInvites';

import messages from '../messages';

export const useSharingService = ({
api,
avatarUrlMap,
Expand All @@ -19,6 +22,8 @@ export const useSharingService = ({
setItem,
setSharedLink,
}) => {
const { formatMessage } = useIntl();

// itemApiInstance should only be called once or the API will cause an issue where it gets cancelled
const itemApiInstance = React.useMemo(() => {
if (!item || !sharedLink) {
Expand Down Expand Up @@ -78,23 +83,51 @@ export const useSharingService = ({
const ownerEmailDomain = ownerEmail && /@/.test(ownerEmail) ? ownerEmail.split('@')[1] : null;
setCollaborators(prevList => {
const newCollab = convertCollab({
avatarUrlMap,
collab: response,
currentUserId,
isCurrentUserOwner: currentUserId === ownerId,
ownerEmailDomain,
avatarUrlMap,
});

return newCollab ? [...prevList, newCollab] : prevList;
});
};

const sendInvitations = useInvites(api, itemId, itemType, {
const handleSendInvitations = useInvites(api, itemId, itemType, {
collaborators,
handleSuccess,
isContentSharingV2Enabled: true,
transformRequest: data => convertCollabsRequest(data, collaborators),
});

const sendInvitations = (...request) => {
return handleSendInvitations(...request).then(response => {
const { contacts: collabRequest } = request[0];
if (!response || !collabRequest || collabRequest.length === 0) {
return null;
}

const successCount = response.length;
const errorCount = collabRequest.length - successCount;

const notification = [];
if (errorCount > 0) {
notification.push({
text: formatMessage(messages.sendInvitationsError, { count: errorCount }),
type: 'error',
});
}
if (successCount > 0) {
notification.push({
text: formatMessage(messages.sendInvitationsSuccess, { count: successCount }),
type: 'success',
});
}

return notification.length > 0 ? { messages: notification } : null;
});
};

return { sharingService: { ...sharingService, sendInvitations } };
};
14 changes: 14 additions & 0 deletions src/elements/content-sharing/messages.js
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,20 @@ const messages = defineMessages({
'Message that appears when collaborators were added to the shared link in the ContentSharing Element.',
id: 'be.contentSharing.sendInvitesSuccess',
},
sendInvitationsError: {
defaultMessage:
'{count, plural, one {Failed to invite a collaborator.} other {Failed to invite {count} collaborators.}}',
description:
'Message that appears when collaborators cannot be added to the shared link in the ContentSharing Element.',
id: 'be.contentSharing.sendInvitationsError',
},
sendInvitationsSuccess: {
defaultMessage:
'{count, plural, one {Successfully invited a collaborator.} other {Successfully invited {count} collaborators.}}',
description:
'Message that appears when collaborators were added to the shared link in the ContentSharing Element.',
id: 'be.contentSharing.sendInvitationsSuccess',
},
groupContactLabel: {
defaultMessage: 'Group',
description: 'Display text for a Group contact type',
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -64,11 +64,11 @@ describe('convertCollaborators', () => {
describe('convertCollab', () => {
test('should convert a valid collaboration to Collaborator format', () => {
const result = convertCollab({
avatarUrlMap: mockAvatarUrlMap,
collab: mockCollaborations[1],
currentUserId: mockOwnerId,
isCurrentUserOwner: false,
ownerEmailDomain,
avatarUrlMap: mockAvatarUrlMap,
});

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

test('should return null for collaboration with non-accepted status', () => {
const result = convertCollab({
avatarUrlMap: mockAvatarUrlMap,
collab: mockCollaborations[3],
currentUserId: mockOwnerId,
isCurrentUserOwner: false,
ownerEmailDomain,
avatarUrlMap: mockAvatarUrlMap,
});

expect(result).toBeNull();
});

test.each([undefined, null])('should return null for %s collaboration', collab => {
const result = convertCollab({
avatarUrlMap: mockAvatarUrlMap,
collab,
currentUserId: mockOwnerId,
isCurrentUserOwner: false,
ownerEmailDomain,
avatarUrlMap: mockAvatarUrlMap,
});

expect(result).toBeNull();
});

test('should identify current user correctly', () => {
const result = convertCollab({
avatarUrlMap: mockAvatarUrlMap,
collab: mockCollaborations[0],
currentUserId: mockOwnerId,
isCurrentUserOwner: true,
ownerEmailDomain,
avatarUrlMap: mockAvatarUrlMap,
});

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

test('should identify external user correctly', () => {
const result = convertCollab({
avatarUrlMap: mockAvatarUrlMap,
collab: mockCollaborations[2],
currentUserId: mockOwnerId,
isCurrentUserOwner: false,
ownerEmailDomain,
avatarUrlMap: mockAvatarUrlMap,
});

expect(result.isExternal).toBe(true);
Expand All @@ -151,11 +151,11 @@ describe('convertCollaborators', () => {
'should handle %s avatar URL map',
avatarUrlMap => {
const result = convertCollab({
avatarUrlMap,
collab: mockCollaborations[1],
currentUserId: mockOwnerId,
isCurrentUserOwner: false,
ownerEmailDomain,
avatarUrlMap,
});

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

const result = convertCollab({
avatarUrlMap: mockAvatarUrlMap,
collab: collabWithoutExpiration,
currentUserId: mockOwnerId,
isCurrentUserOwner: false,
ownerEmailDomain,
avatarUrlMap: mockAvatarUrlMap,
});

expect(result.expiresAt).toBeNull();
Expand Down Expand Up @@ -305,6 +305,45 @@ describe('convertCollaborators', () => {
});
});

test('should convert collab request with users without a type', () => {
const mockCollabRequest = {
role: 'editor',
contacts: [
{
id: 'user1',
email: 'user1@test.com',
type: 'user',
},
{
id: 'user2',
email: 'external@test.com',
},
],
};

const result = convertCollabsRequest(mockCollabRequest, null);

expect(result).toEqual({
groups: [],
users: [
{
accessible_by: {
login: 'user1@test.com',
type: 'user',
},
role: 'editor',
},
{
accessible_by: {
login: 'external@test.com',
type: 'user',
},
role: 'editor',
},
],
});
});

test('should handle empty contacts array', () => {
const emptyCollabRequest = {
role: 'editor',
Expand Down
Loading