diff --git a/src/elements/content-sharing/ContentSharingV2.tsx b/src/elements/content-sharing/ContentSharingV2.tsx index 1acf69a643..39832e0952 100644 --- a/src/elements/content-sharing/ContentSharingV2.tsx +++ b/src/elements/content-sharing/ContentSharingV2.tsx @@ -53,11 +53,15 @@ function ContentSharingV2({ const { sharingService } = useSharingService({ api, + avatarURLMap, + collaborators, + currentUserId: currentUser?.id, item, itemId: itemID, itemType, sharedLink, sharingServiceProps, + setCollaborators, setItem, setSharedLink, }); @@ -111,7 +115,10 @@ function ContentSharingV2({ id, enterprise: { name: enterprise ? enterprise.name : '' }, }); - setSharedLink(prevSharedLink => ({ ...prevSharedLink, serverURL: hostname ? `${hostname}v/` : '' })); + setSharingServiceProps(prevSharingServiceProps => ({ + ...prevSharingServiceProps, + serverUrl: hostname ? `${hostname}v/` : '', + })); }; (async () => { @@ -136,13 +143,23 @@ function ContentSharingV2({ // Get avatars when collaborators are available React.useEffect(() => { - if (avatarURLMap || !collaboratorsData || !collaboratorsData.entries) return; - + if (avatarURLMap || !collaboratorsData || !collaboratorsData.entries || !owner.id) return; (async () => { - const response = await fetchAvatars({ api, itemID, collaborators: collaboratorsData.entries }); + const ownerEntry = { + accessible_by: { + id: owner.id, + login: owner.email, + name: owner.name, + }, + }; + const response = await fetchAvatars({ + api, + itemID, + collaborators: [...collaboratorsData.entries, ownerEntry], + }); setAvatarURLMap(response); })(); - }, [api, avatarURLMap, collaboratorsData, itemID]); + }, [api, avatarURLMap, collaboratorsData, itemID, owner]); React.useEffect(() => { if (avatarURLMap && collaboratorsData && currentUser && owner) { diff --git a/src/elements/content-sharing/SharingNotification.js b/src/elements/content-sharing/SharingNotification.js index 7a6a1d74cc..926396003b 100644 --- a/src/elements/content-sharing/SharingNotification.js +++ b/src/elements/content-sharing/SharingNotification.js @@ -170,42 +170,37 @@ function SharingNotification({ }; // Generate shared link CRUD functions for the item - const { - changeSharedLinkAccessLevel, - changeSharedLinkPermissionLevel, - onAddLink, - onRemoveLink, - onSubmitSettings, - } = useSharedLink(api, itemID, itemType, permissions, accessLevel, { - handleUpdateSharedLinkError: () => { - createNotification(TYPE_ERROR, contentSharingMessages.sharedLinkUpdateError); - setIsLoading(false); - closeSettings(); - }, - handleUpdateSharedLinkSuccess: itemData => { - createNotification(TYPE_INFO, contentSharingMessages.sharedLinkSettingsUpdateSuccess); - handleUpdateSharedLinkSuccess(itemData); - setIsLoading(false); - closeSettings(); - }, - handleRemoveSharedLinkError: () => { - createNotification(TYPE_ERROR, contentSharingMessages.sharedLinkUpdateError); - setIsLoading(false); - closeComponent(); // if this function is provided, it will close the modal - }, - handleRemoveSharedLinkSuccess: itemData => { - createNotification(TYPE_INFO, contentSharingMessages.sharedLinkRemovalSuccess); - handleRemoveSharedLinkSuccess(itemData); - setIsLoading(false); - closeComponent(); - }, - setIsLoading, - transformAccess: newAccessLevel => USM_TO_API_ACCESS_LEVEL_MAP[newAccessLevel], - transformPermissions: newSharedLinkPermissionLevel => - convertSharedLinkPermissions(newSharedLinkPermissionLevel), - transformSettings: (settings, access) => - convertSharedLinkSettings(settings, access, isDownloadAvailable, serverURL), - }); + const { changeSharedLinkAccessLevel, changeSharedLinkPermissionLevel, onAddLink, onRemoveLink, onSubmitSettings } = + useSharedLink(api, itemID, itemType, permissions, accessLevel, { + handleUpdateSharedLinkError: () => { + createNotification(TYPE_ERROR, contentSharingMessages.sharedLinkUpdateError); + setIsLoading(false); + closeSettings(); + }, + handleUpdateSharedLinkSuccess: itemData => { + createNotification(TYPE_INFO, contentSharingMessages.sharedLinkSettingsUpdateSuccess); + handleUpdateSharedLinkSuccess(itemData); + setIsLoading(false); + closeSettings(); + }, + handleRemoveSharedLinkError: () => { + createNotification(TYPE_ERROR, contentSharingMessages.sharedLinkUpdateError); + setIsLoading(false); + closeComponent(); // if this function is provided, it will close the modal + }, + handleRemoveSharedLinkSuccess: itemData => { + createNotification(TYPE_INFO, contentSharingMessages.sharedLinkRemovalSuccess); + handleRemoveSharedLinkSuccess(itemData); + setIsLoading(false); + closeComponent(); + }, + setIsLoading, + transformAccess: newAccessLevel => USM_TO_API_ACCESS_LEVEL_MAP[newAccessLevel], + transformPermissions: newSharedLinkPermissionLevel => + convertSharedLinkPermissions(newSharedLinkPermissionLevel), + transformSettings: (settings, access) => + convertSharedLinkSettings(settings, access, isDownloadAvailable, serverURL), + }); setChangeSharedLinkAccessLevel(() => changeSharedLinkAccessLevel); setChangeSharedLinkPermissionLevel(() => changeSharedLinkPermissionLevel); diff --git a/src/elements/content-sharing/__tests__/sharingService.test.ts b/src/elements/content-sharing/__tests__/sharingService.test.ts index 0301bd59b1..d0f8a539eb 100644 --- a/src/elements/content-sharing/__tests__/sharingService.test.ts +++ b/src/elements/content-sharing/__tests__/sharingService.test.ts @@ -162,7 +162,7 @@ describe('elements/content-sharing/sharingService', () => { sharedLinkSettings, undefined, // access undefined, // isDownloadAvailable - undefined, // serverURL + undefined, // serverUrl ); expect(mockItemApiInstance.updateSharedLink).toHaveBeenCalledWith( options, @@ -173,7 +173,7 @@ describe('elements/content-sharing/sharingService', () => { ); }); - test('should call updateSharedLink with options including access, isDownloadAvailable, and serverURL', async () => { + test('should call updateSharedLink with options including access, isDownloadAvailable, and serverUrl', async () => { const mockConvertedSharedLinkSettings = { password: 'test-password', permissions: { can_download: false, can_preview: true }, @@ -191,7 +191,7 @@ describe('elements/content-sharing/sharingService', () => { ...options, access: 'open', isDownloadAvailable: true, - serverURL: 'https://example.com/server-url', + serverUrl: 'https://example.com/server-url', }, }); diff --git a/src/elements/content-sharing/__tests__/useInvites.test.js b/src/elements/content-sharing/__tests__/useInvites.test.js index dc9988a286..80ef67c0e2 100644 --- a/src/elements/content-sharing/__tests__/useInvites.test.js +++ b/src/elements/content-sharing/__tests__/useInvites.test.js @@ -4,6 +4,8 @@ import API from '../../../api'; jest.mock('../../../api'); +const mockCollaborators = [{ id: '123', type: 'user', email: 'user@test.com', role: 'editor' }]; + describe('useInvites hook', () => { let mockApi; let mockHandleSuccess; @@ -93,24 +95,43 @@ describe('useInvites hook', () => { expect(mockHandleError).not.toHaveBeenCalled(); }); - test('processes multiple users and groups in a single call', async () => { - const { result } = renderHook(() => - useInvites(mockApi, '123', 'folder', { - handleSuccess: mockHandleSuccess, - handleError: mockHandleError, - transformRequest: mockTransformRequest, - transformResponse: mockTransformResponse, - }), - ); + describe('when isContentSharingV2Enabled is true', () => { + test('processes multiple users and groups in a single call', async () => { + const { result } = renderHook(() => + useInvites(mockApi, '123', 'folder', { + collaborators: mockCollaborators, + handleSuccess: mockHandleSuccess, + handleError: mockHandleError, + isContentSharingV2Enabled: true, + transformRequest: mockTransformRequest, + transformResponse: mockTransformResponse, + }), + ); - act(() => { - result.current({ - users: [{ email: 'user@example.com', role: 'editor' }], - groups: [{ id: 'group123', role: 'viewer' }], + act(() => { + result.current({ + users: [{ email: 'user@example.com', role: 'editor' }], + groups: [{ id: 'group123', role: 'viewer' }], + }); }); + + expect(mockHandleSuccess).toHaveBeenCalledTimes(2); + expect(mockTransformResponse).toHaveBeenCalledTimes(2); }); - expect(mockHandleSuccess).toHaveBeenCalledTimes(2); - expect(mockTransformResponse).toHaveBeenCalledTimes(2); + test('Should early return null if collaborators is not provided', async () => { + const { result } = renderHook(() => + useInvites(mockApi, '123', 'folder', { + handleSuccess: mockHandleSuccess, + handleError: mockHandleError, + isContentSharingV2Enabled: true, + transformResponse: mockTransformResponse, + }), + ); + + expect(result.current).toBeNull(); + expect(mockHandleSuccess).not.toHaveBeenCalled(); + expect(mockHandleError).not.toHaveBeenCalled(); + }); }); }); diff --git a/src/elements/content-sharing/constants.js b/src/elements/content-sharing/constants.js index 6589024dfb..eea0b912b9 100644 --- a/src/elements/content-sharing/constants.js +++ b/src/elements/content-sharing/constants.js @@ -81,3 +81,6 @@ export const API_TO_USM_CLASSIFICATION_COLORS_MAP = { export const ANYONE_WITH_LINK = 'peopleWithTheLink'; export const ANYONE_IN_COMPANY = 'peopleInYourCompany'; export const PEOPLE_IN_ITEM = 'peopleInThisItem'; + +export const COLLAB_USER_TYPE = 'user'; +export const COLLAB_GROUP_TYPE = 'group'; diff --git a/src/elements/content-sharing/hooks/__tests__/useSharingService.test.ts b/src/elements/content-sharing/hooks/__tests__/useSharingService.test.ts index 61d7d9c1fa..307b445653 100644 --- a/src/elements/content-sharing/hooks/__tests__/useSharingService.test.ts +++ b/src/elements/content-sharing/hooks/__tests__/useSharingService.test.ts @@ -2,11 +2,14 @@ import { renderHook } from '@testing-library/react'; import { TYPE_FILE, TYPE_FOLDER } from '../../../../constants'; import { createSharingService } from '../../sharingService'; -import { convertItemResponse } from '../../utils/convertItemResponse'; +import { convertCollab, convertCollabsRequest, convertItemResponse } from '../../utils'; import { useSharingService } from '../useSharingService'; +import useInvites from '../useInvites'; jest.mock('../../utils/convertItemResponse'); +jest.mock('../../utils/convertCollaborators'); jest.mock('../../sharingService'); +jest.mock('../useInvites'); const mockApi = { getFileAPI: jest.fn(), @@ -19,30 +22,28 @@ const mockSharingService = { deleteSharedLink: jest.fn(), updateSharedLink: jest.fn(), }; +const mockSendInvitations = jest.fn(); const mockItemId = '123'; +const mockSharingServiceProps = { + can_set_share_access: true, + can_share: true, + serverUrl: 'https://example.com/server-url', +}; const mockItem = { id: mockItemId, permissions: { can_download: true, can_preview: false, }, - sharingServiceProps: { - can_set_share_access: true, - can_share: true, - }, + sharingServiceProps: mockSharingServiceProps, }; const mockSharedLink = { access: 'open', - serverURL: 'https://example.com/server-url', settings: { isDownloadAvailable: true, }, }; -const mockSharingServiceProps = { - can_set_share_access: true, - can_share: true, -}; const mockConvertedData = { item: mockItem, @@ -51,16 +52,21 @@ const mockConvertedData = { const mockSetItem = jest.fn(); const mockSetSharedLink = jest.fn(); +const mockSetCollaborators = jest.fn(); const renderHookWithProps = (props = {}) => { return renderHook(() => useSharingService({ api: mockApi, + avatarURLMap: {}, + collaborators: [], + currentUserId: '123', item: mockItem, itemId: mockItemId, itemType: TYPE_FILE, sharedLink: mockSharedLink, sharingServiceProps: mockSharingServiceProps, + setCollaborators: mockSetCollaborators, setItem: mockSetItem, setSharedLink: mockSetSharedLink, ...props, @@ -75,6 +81,9 @@ describe('elements/content-sharing/hooks/useSharingService', () => { item: mockItem, sharedLink: {}, }); + (useInvites as jest.Mock).mockReturnValue(mockSendInvitations); + (convertCollab as jest.Mock).mockReturnValue({ id: 'collab-1', email: 'test@example.com' }); + (convertCollabsRequest as jest.Mock).mockReturnValue({ users: [], groups: [] }); }); afterEach(() => { @@ -84,7 +93,7 @@ describe('elements/content-sharing/hooks/useSharingService', () => { test('should return null itemApiInstance and sharingService when item is null', () => { const { result } = renderHookWithProps({ item: null }); - expect(result.current.sharingService).toBeNull(); + expect(result.current.sharingService).toEqual({ sendInvitations: expect.any(Function) }); expect(mockApi.getFileAPI).not.toHaveBeenCalled(); expect(mockApi.getFolderAPI).not.toHaveBeenCalled(); expect(createSharingService).not.toHaveBeenCalled(); @@ -93,7 +102,7 @@ describe('elements/content-sharing/hooks/useSharingService', () => { test('should return null itemApiInstance and sharingService when sharedLink is null', () => { const { result } = renderHookWithProps({ sharedLink: null }); - expect(result.current.sharingService).toBeNull(); + expect(result.current.sharingService).toEqual({ sendInvitations: expect.any(Function) }); expect(mockApi.getFileAPI).not.toHaveBeenCalled(); expect(mockApi.getFolderAPI).not.toHaveBeenCalled(); expect(createSharingService).not.toHaveBeenCalled(); @@ -102,7 +111,7 @@ describe('elements/content-sharing/hooks/useSharingService', () => { test('should return null itemApiInstance and sharingService when itemType is neither TYPE_FILE nor TYPE_FOLDER', () => { const { result } = renderHookWithProps({ itemType: 'hubs' }); - expect(result.current.sharingService).toBeNull(); + expect(result.current.sharingService).toEqual({ sendInvitations: expect.any(Function) }); expect(mockApi.getFileAPI).not.toHaveBeenCalled(); expect(mockApi.getFolderAPI).not.toHaveBeenCalled(); expect(createSharingService).not.toHaveBeenCalled(); @@ -115,10 +124,14 @@ describe('elements/content-sharing/hooks/useSharingService', () => { test('should create file API instance and sharing service', () => { const { result } = renderHookWithProps(); + const { can_set_share_access, can_share, serverUrl } = mockItem.sharingServiceProps; expect(mockApi.getFileAPI).toHaveBeenCalled(); expect(mockApi.getFolderAPI).not.toHaveBeenCalled(); - expect(result.current.sharingService).toBe(mockSharingService); + expect(result.current.sharingService).toEqual({ + ...mockSharingService, + sendInvitations: expect.any(Function), + }); expect(createSharingService).toHaveBeenCalledWith({ itemApiInstance: {}, onUpdateSharedLink: expect.any(Function), @@ -127,8 +140,8 @@ describe('elements/content-sharing/hooks/useSharingService', () => { access: mockSharedLink.access, isDownloadAvailable: mockSharedLink.settings.isDownloadAvailable, id: mockItemId, - permissions: mockItem.sharingServiceProps, - serverURL: mockSharedLink.serverURL, + permissions: { can_set_share_access, can_share }, + serverUrl, }, }); }); @@ -167,10 +180,14 @@ describe('elements/content-sharing/hooks/useSharingService', () => { test('should create folder API instance and sharing service', () => { const { result } = renderHookWithProps({ itemType: TYPE_FOLDER }); + const { can_set_share_access, can_share, serverUrl } = mockItem.sharingServiceProps; expect(mockApi.getFolderAPI).toHaveBeenCalled(); expect(mockApi.getFileAPI).not.toHaveBeenCalled(); - expect(result.current.sharingService).toBe(mockSharingService); + expect(result.current.sharingService).toEqual({ + ...mockSharingService, + sendInvitations: expect.any(Function), + }); expect(createSharingService).toHaveBeenCalledWith({ itemApiInstance: {}, onUpdateSharedLink: expect.any(Function), @@ -179,8 +196,8 @@ describe('elements/content-sharing/hooks/useSharingService', () => { access: mockSharedLink.access, isDownloadAvailable: mockSharedLink.settings.isDownloadAvailable, id: mockItemId, - permissions: mockItem.sharingServiceProps, - serverURL: mockSharedLink.serverURL, + permissions: { can_set_share_access, can_share }, + serverUrl, }, }); }); @@ -211,4 +228,70 @@ describe('elements/content-sharing/hooks/useSharingService', () => { expect(mockSetSharedLink).toHaveBeenCalledTimes(1); }); }); + + describe('sendInvitations', () => { + 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'; + + test('should call useInvites with correct parameters', () => { + renderHookWithProps({ + collaborators: mockCollaborators, + avatarURLMap: mockAvatarURLMap, + currentUserId: mockCurrentUserId, + }); + + expect(useInvites).toHaveBeenCalledWith(mockApi, mockItemId, TYPE_FILE, { + collaborators: mockCollaborators, + handleSuccess: expect.any(Function), + isContentSharingV2Enabled: true, + transformRequest: expect.any(Function), + }); + }); + + test('should handle success callback correctly', () => { + const mockResponse = { + created_by: { + id: 'owner-123', + login: 'owner@test.com', + }, + }; + + renderHookWithProps({ + collaborators: mockCollaborators, + avatarURLMap: mockAvatarURLMap, + currentUserId: mockCurrentUserId, + }); + + // Get the handleSuccess and setCollaborators function that was passed to useInvites + const useInvitesCallOptions = (useInvites as jest.Mock).mock.calls[0][3]; + useInvitesCallOptions.handleSuccess(mockResponse); + const setCollaboratorsCallback = mockSetCollaborators.mock.calls[0][0]; + setCollaboratorsCallback(mockCollaborators); + + expect(convertCollab).toHaveBeenCalledWith({ + collab: mockResponse, + currentUserId: mockCurrentUserId, + isCurrentUserOwner: false, + ownerEmailDomain: 'test.com', + avatarURLMap: mockAvatarURLMap, + }); + }); + + test('should call transformRequest with convertCollabsRequest', () => { + const mockCollabRequest = { + contacts: [{ id: 'user-1', email: 'user@test.com', type: 'user' }], + role: 'editor', + }; + + renderHookWithProps({ + collaborators: mockCollaborators, + }); + + const useInvitesCallOptions = (useInvites as jest.Mock).mock.calls[0][3]; + useInvitesCallOptions.transformRequest(mockCollabRequest); + + expect(convertCollabsRequest).toHaveBeenCalledWith(mockCollabRequest, mockCollaborators); + }); + }); }); diff --git a/src/elements/content-sharing/hooks/useInvites.js b/src/elements/content-sharing/hooks/useInvites.js index c97aaa5b08..9b692c4ea6 100644 --- a/src/elements/content-sharing/hooks/useInvites.js +++ b/src/elements/content-sharing/hooks/useInvites.js @@ -18,15 +18,17 @@ import type { ItemType } from '../../../common/types/core'; function useInvites(api: API, itemID: string, itemType: ItemType, options: UseInvitesOptions) { const [sendInvites, setSendInvites] = useState(null); const { + collaborators, handleSuccess = noop, handleError = noop, + isContentSharingV2Enabled, setIsLoading = noop, transformRequest, transformResponse = arg => arg, } = options; React.useEffect(() => { - if (sendInvites) return; + if (sendInvites || (isContentSharingV2Enabled && !collaborators)) return; const itemData = { id: itemID, @@ -45,25 +47,26 @@ function useInvites(api: API, itemID: string, itemType: ItemType, options: UseIn ); }; - const createPostCollaborationFn: SendInvitesFnType = () => async ( - collabRequest: InviteCollaboratorsRequest, - ) => { - if (!transformRequest) return Promise.resolve(null); + const createPostCollaborationFn: SendInvitesFnType = + () => async (collabRequest: InviteCollaboratorsRequest) => { + if (!transformRequest) return Promise.resolve(null); - const { users, groups } = transformRequest(collabRequest); - return Promise.all([ - users.map(user => sendCollabRequest(user)), - groups.map(group => sendCollabRequest(group)), - ]); - }; + const { users, groups } = transformRequest(collabRequest); + return Promise.all([ + ...users.map(user => sendCollabRequest(user)), + ...groups.map(group => sendCollabRequest(group)), + ]); + }; if (!sendInvites) { setSendInvites(createPostCollaborationFn); } }, [ api, + collaborators, handleError, handleSuccess, + isContentSharingV2Enabled, itemID, itemType, sendInvites, diff --git a/src/elements/content-sharing/hooks/useSharingService.ts b/src/elements/content-sharing/hooks/useSharingService.ts index 90edab4177..28115bd865 100644 --- a/src/elements/content-sharing/hooks/useSharingService.ts +++ b/src/elements/content-sharing/hooks/useSharingService.ts @@ -1,19 +1,25 @@ import * as React from 'react'; import { TYPE_FILE, TYPE_FOLDER } from '../../../constants'; -import { convertItemResponse } from '../utils/convertItemResponse'; +import { convertItemResponse, convertCollab, convertCollabsRequest } from '../utils'; import { createSharingService } from '../sharingService'; +import useInvites from './useInvites'; export const useSharingService = ({ api, + avatarURLMap, + collaborators, + currentUserId, item, itemId, itemType, sharedLink, sharingServiceProps, + setCollaborators, setItem, setSharedLink, }) => { + // itemApiInstance should only be called once or the API will cause an issue where it gets cancelled const itemApiInstance = React.useMemo(() => { if (!item || !sharedLink) { return null; @@ -38,8 +44,11 @@ export const useSharingService = ({ const options = { id: itemId, access: sharedLink.access, - permissions: sharingServiceProps, - serverURL: sharedLink.serverURL, + permissions: { + can_set_share_access: sharingServiceProps.can_set_share_access, + can_share: sharingServiceProps.can_share, + }, + serverUrl: sharingServiceProps.serverUrl, isDownloadAvailable: sharedLink.settings?.isDownloadAvailable ?? false, }; @@ -63,5 +72,29 @@ export const useSharingService = ({ }); }, [itemApiInstance, itemId, sharedLink, sharingServiceProps, setItem, setSharedLink]); - return { sharingService }; + // Create the sendInvitations callbacks using the existing memoized useInvites hook + const handleSuccess = response => { + const { id: ownerId, login: ownerEmail } = response.created_by; + const ownerEmailDomain = ownerEmail && /@/.test(ownerEmail) ? ownerEmail.split('@')[1] : null; + setCollaborators(prevList => { + const newCollab = convertCollab({ + collab: response, + currentUserId, + isCurrentUserOwner: currentUserId === ownerId, + ownerEmailDomain, + avatarURLMap, + }); + + return newCollab ? [...prevList, newCollab] : prevList; + }); + }; + + const sendInvitations = useInvites(api, itemId, itemType, { + collaborators, + handleSuccess, + isContentSharingV2Enabled: true, + transformRequest: data => convertCollabsRequest(data, collaborators), + }); + + return { sharingService: { ...sharingService, sendInvitations } }; }; diff --git a/src/elements/content-sharing/sharingService.ts b/src/elements/content-sharing/sharingService.ts index 41c53aab58..2987e74453 100644 --- a/src/elements/content-sharing/sharingService.ts +++ b/src/elements/content-sharing/sharingService.ts @@ -16,7 +16,7 @@ export interface ItemData { export interface Options extends ItemData { access?: string; isDownloadAvailable?: boolean; - serverURL?: string; + serverUrl?: string; } export interface CreateSharingServiceArgs { @@ -55,11 +55,11 @@ export const createSharingService = ({ }; const updateSharedLink = async (sharedLinkSettings: SharedLinkSettings) => { - const { access, isDownloadAvailable, serverURL } = options; + const { access, isDownloadAvailable, serverUrl } = options; return itemApiInstance.updateSharedLink( { id, permissions }, - convertSharedLinkSettings(sharedLinkSettings, access, isDownloadAvailable, serverURL), + convertSharedLinkSettings(sharedLinkSettings, access, isDownloadAvailable, serverUrl), onUpdateSharedLink, {}, CONTENT_SHARING_SHARED_LINK_UPDATE_PARAMS, diff --git a/src/elements/content-sharing/types.js b/src/elements/content-sharing/types.js index fec4d92b91..16acebba5a 100644 --- a/src/elements/content-sharing/types.js +++ b/src/elements/content-sharing/types.js @@ -1,5 +1,5 @@ // @flow -import type { CollaborationRole, DateValue, Item, SharedLink } from '@box/unified-share-modal'; +import type { CollaborationRole, Collaborator, DateValue, Item, SharedLink } from '@box/unified-share-modal'; import API from '../../api'; import type { @@ -125,6 +125,7 @@ export type ContentSharingCollaborationsRequest = { }; export type UseInvitesOptions = ContentSharingHooksOptions & { + collaborators?: Array, transformRequest: InviteCollaboratorsRequest => ContentSharingCollaborationsRequest, }; diff --git a/src/elements/content-sharing/utils/__tests__/convertCollaborators.test.ts b/src/elements/content-sharing/utils/__tests__/convertCollaborators.test.ts index ddfef60da2..92d0838e62 100644 --- a/src/elements/content-sharing/utils/__tests__/convertCollaborators.test.ts +++ b/src/elements/content-sharing/utils/__tests__/convertCollaborators.test.ts @@ -1,5 +1,5 @@ import { STATUS_ACCEPTED } from '../../../../constants'; -import { convertCollab, convertCollabsResponse } from '../convertCollaborators'; +import { convertCollab, convertCollabsResponse, convertCollabsRequest } from '../convertCollaborators'; import { collabUser1, collabUser2, @@ -254,4 +254,68 @@ describe('convertCollaborators', () => { }); }); }); + + describe('convertCollabsRequest', () => { + test('should convert collab request with users and groups correctly', () => { + const mockCollabRequest = { + role: 'editor', + contacts: [ + { + id: 'user1', + email: 'user1@test.com', + type: 'user', + }, + { + id: 'group1', + email: 'Group', + type: 'group', + }, + { + id: 'user2', + email: 'existing@test.com', + type: 'user', + }, + ], + }; + + const mockExistingCollaboratorsList = [{ userId: 'user2' }, { userId: 'group2' }]; + + const result = convertCollabsRequest(mockCollabRequest, mockExistingCollaboratorsList); + + expect(result).toEqual({ + groups: [ + { + accessible_by: { + id: 'group1', + type: 'group', + }, + role: 'editor', + }, + ], + users: [ + { + accessible_by: { + login: 'user1@test.com', + type: 'user', + }, + role: 'editor', + }, + // The existing collaborator is filtered out + ], + }); + }); + + test('should handle empty contacts array', () => { + const emptyCollabRequest = { + role: 'editor', + contacts: [], + }; + + const result = convertCollabsRequest(emptyCollabRequest, null); + expect(result).toEqual({ + groups: [], + users: [], + }); + }); + }); }); diff --git a/src/elements/content-sharing/utils/__tests__/convertItemResponse.test.ts b/src/elements/content-sharing/utils/__tests__/convertItemResponse.test.ts index fdd3378f88..bd0b27dfed 100644 --- a/src/elements/content-sharing/utils/__tests__/convertItemResponse.test.ts +++ b/src/elements/content-sharing/utils/__tests__/convertItemResponse.test.ts @@ -44,6 +44,8 @@ describe('convertItemResponse', () => { sharingService: { can_set_share_access: true, can_share: true, + ownerEmail: mockOwnerEmail, + ownerId: mockOwnerId, }, }); }); diff --git a/src/elements/content-sharing/utils/__tests__/convertSharingServiceData.test.ts b/src/elements/content-sharing/utils/__tests__/convertSharingServiceData.test.ts index 90dd47a845..c03224556d 100644 --- a/src/elements/content-sharing/utils/__tests__/convertSharingServiceData.test.ts +++ b/src/elements/content-sharing/utils/__tests__/convertSharingServiceData.test.ts @@ -36,7 +36,7 @@ describe('elements/content-sharing/utils/convertSharingServiceData', () => { }); describe('convertSharedLinkSettings', () => { - const mockServerURL = 'https://example.com/server-url/'; + const mockServerUrl = 'https://example.com/server-url/'; const mockSettings = { expiration: new Date('2024-12-31T23:59:59Z'), isDownloadEnabled: true, @@ -48,7 +48,7 @@ describe('elements/content-sharing/utils/convertSharingServiceData', () => { describe('basic conversion', () => { test('should return settings with correct values', () => { - const result = convertSharedLinkSettings(mockSettings, ACCESS_OPEN, true, mockServerURL); + const result = convertSharedLinkSettings(mockSettings, ACCESS_OPEN, true, mockServerUrl); expect(result).toEqual({ password: 'test-password', @@ -92,7 +92,7 @@ describe('elements/content-sharing/utils/convertSharingServiceData', () => { expiration: new Date('2024-12-31T23:59:59Z'), }; - const result = convertSharedLinkSettings(settingsWithoutExpiration, ACCESS_OPEN, true, mockServerURL); + const result = convertSharedLinkSettings(settingsWithoutExpiration, ACCESS_OPEN, true, mockServerUrl); expect(result.unshared_at).toBeNull(); }); @@ -104,7 +104,7 @@ describe('elements/content-sharing/utils/convertSharingServiceData', () => { expiration, }; - const result = convertSharedLinkSettings(settingsWithNullExpiration, ACCESS_OPEN, true, mockServerURL); + const result = convertSharedLinkSettings(settingsWithNullExpiration, ACCESS_OPEN, true, mockServerUrl); expect(result.unshared_at).toBeNull(); }); @@ -117,12 +117,12 @@ describe('elements/content-sharing/utils/convertSharingServiceData', () => { ['https://example.com/server-url/', ''], ])( 'should return empty string when at least one of serverURL or vanityName is empty', - (serverURL, vanityName) => { + (serverUrl, vanityName) => { const result = convertSharedLinkSettings( { ...mockSettings, vanityName }, ACCESS_OPEN, true, - serverURL, + serverUrl, ); expect(result.vanity_url).toBe(''); @@ -132,7 +132,7 @@ describe('elements/content-sharing/utils/convertSharingServiceData', () => { describe('permissions handling', () => { test('should not set permissions for ACCESS_COLLAB access level', () => { - const result = convertSharedLinkSettings(mockSettings, ACCESS_COLLAB, true, mockServerURL); + const result = convertSharedLinkSettings(mockSettings, ACCESS_COLLAB, true, mockServerUrl); expect(result.permissions).toBeUndefined(); }); @@ -147,7 +147,7 @@ describe('elements/content-sharing/utils/convertSharingServiceData', () => { settingsWithDownloadDisabled, ACCESS_OPEN, true, - mockServerURL, + mockServerUrl, ); expect(result.permissions).toEqual({ @@ -157,7 +157,7 @@ describe('elements/content-sharing/utils/convertSharingServiceData', () => { }); test('should not set can_download when isDownloadAvailable is false', () => { - const result = convertSharedLinkSettings(mockSettings, ACCESS_OPEN, false, mockServerURL); + const result = convertSharedLinkSettings(mockSettings, ACCESS_OPEN, false, mockServerUrl); expect(result.permissions).toEqual({ can_preview: false, @@ -178,7 +178,7 @@ describe('elements/content-sharing/utils/convertSharingServiceData', () => { settingsWithPasswordDisabled, ACCESS_OPEN, true, - mockServerURL, + mockServerUrl, ); expect(result.password).toBeNull(); @@ -191,13 +191,13 @@ describe('elements/content-sharing/utils/convertSharingServiceData', () => { password: '', }; - const result = convertSharedLinkSettings(settingsWithEmptyPassword, ACCESS_OPEN, true, mockServerURL); + const result = convertSharedLinkSettings(settingsWithEmptyPassword, ACCESS_OPEN, true, mockServerUrl); expect(result.password).toBeUndefined(); }); test.each([ACCESS_COLLAB, ACCESS_COMPANY])('should not set password for non open access level', access => { - const result = convertSharedLinkSettings(mockSettings, access, true, mockServerURL); + const result = convertSharedLinkSettings(mockSettings, access, true, mockServerUrl); expect(result.password).toBeUndefined(); }); diff --git a/src/elements/content-sharing/utils/convertCollaborators.ts b/src/elements/content-sharing/utils/convertCollaborators.ts index 9064b4cfa6..a99198723b 100644 --- a/src/elements/content-sharing/utils/convertCollaborators.ts +++ b/src/elements/content-sharing/utils/convertCollaborators.ts @@ -1,6 +1,7 @@ import { Collaborator } from '@box/unified-share-modal'; import { INVITEE_ROLE_OWNER, STATUS_ACCEPTED } from '../../../constants'; +import { COLLAB_USER_TYPE, COLLAB_GROUP_TYPE } from '../constants'; import type { Collaboration, Collaborations } from '../../../common/types/core'; import type { AvatarURLMap } from '../types'; @@ -73,10 +74,49 @@ export const convertCollabsResponse = ( name: ownerName, }, }; - entries.unshift(itemOwner); - return entries.flatMap(collab => { + return [itemOwner, ...entries].flatMap(collab => { const converted = convertCollab({ collab, currentUserId, isCurrentUserOwner, ownerEmailDomain, avatarURLMap }); return converted ? [converted] : []; }); }; + +export const convertCollabsRequest = (collabRequest, existingCollaboratorsList) => { + const existingCollab = []; + if (existingCollaboratorsList && existingCollaboratorsList.length > 0) { + existingCollaboratorsList.forEach(collab => { + existingCollab.push(collab.userId); + }); + } + + const groups = []; + const users = []; + const { role } = collabRequest; + collabRequest.contacts.forEach(contact => { + if (existingCollab.includes(contact.id)) { + return; + } + + if (contact.type === COLLAB_USER_TYPE) { + users.push({ + accessible_by: { + login: contact.email, + type: COLLAB_USER_TYPE, + }, + role, + }); + } + + if (contact.type === COLLAB_GROUP_TYPE) { + groups.push({ + accessible_by: { + id: contact.id, + type: COLLAB_GROUP_TYPE, + }, + role, + }); + } + }); + + return { groups, users }; +}; diff --git a/src/elements/content-sharing/utils/convertItemResponse.ts b/src/elements/content-sharing/utils/convertItemResponse.ts index 7c72e86ecb..032dcc237f 100644 --- a/src/elements/content-sharing/utils/convertItemResponse.ts +++ b/src/elements/content-sharing/utils/convertItemResponse.ts @@ -103,6 +103,8 @@ export const convertItemResponse = (itemAPIData: ContentSharingItemAPIResponse): sharingService: { can_set_share_access: canChangeAccessLevel, can_share: canShare, + ownerEmail: ownedBy.login, + ownerId: ownedBy.id, }, ownedBy, }; diff --git a/src/elements/content-sharing/utils/convertSharingServiceData.ts b/src/elements/content-sharing/utils/convertSharingServiceData.ts index 8d6a3ebeea..f9eea83c0b 100644 --- a/src/elements/content-sharing/utils/convertSharingServiceData.ts +++ b/src/elements/content-sharing/utils/convertSharingServiceData.ts @@ -36,7 +36,7 @@ export const convertSharedLinkSettings = ( newSettings: SharedLinkSettings, accessLevel: string, isDownloadAvailable: boolean, - serverURL: string, + serverUrl: string, ): ConvertSharedLinkSettingsReturnType => { const { expiration, isDownloadEnabled, isExpirationEnabled, isPasswordEnabled, password, vanityName } = newSettings; @@ -45,7 +45,7 @@ export const convertSharedLinkSettings = ( expiration && isExpirationEnabled ? convertISOStringToUTCDate(new Date(expiration).toISOString()).toISOString() : null, - vanity_url: serverURL && vanityName ? `${serverURL}${vanityName}` : '', + vanity_url: serverUrl && vanityName ? `${serverUrl}${vanityName}` : '', }; // Download permissions can only be set on "company" or "open" shared links. diff --git a/src/elements/content-sharing/utils/index.ts b/src/elements/content-sharing/utils/index.ts index 24f9717345..2856ba4b19 100644 --- a/src/elements/content-sharing/utils/index.ts +++ b/src/elements/content-sharing/utils/index.ts @@ -1,4 +1,4 @@ -export { convertCollabsResponse } from './convertCollaborators'; +export { convertCollabsResponse, convertCollab, convertCollabsRequest } from './convertCollaborators'; export { convertItemResponse } from './convertItemResponse'; export { convertGroupContactsResponse,