From 8f8aa55115b5740ad7df12056fa1b3f9b5aba8b1 Mon Sep 17 00:00:00 2001 From: reneshen0328 Date: Thu, 16 Oct 2025 21:11:03 -0700 Subject: [PATCH 1/5] feat(content-sharing): Create contact service getContactByEmail --- .../__tests__/useContactsByEmail.test.js | 218 ++++++++++++------ .../hooks/__tests__/useContactService.test.ts | 41 +++- .../hooks/useContactService.ts | 10 +- .../hooks/useContactsByEmail.js | 39 +++- .../convertContactServiceData.test.ts | 106 ++++++++- .../utils/convertContactServiceData.ts | 21 ++ src/elements/content-sharing/utils/index.ts | 6 +- 7 files changed, 346 insertions(+), 95 deletions(-) diff --git a/src/elements/content-sharing/__tests__/useContactsByEmail.test.js b/src/elements/content-sharing/__tests__/useContactsByEmail.test.js index e2b27c2837..d0620fe1c0 100644 --- a/src/elements/content-sharing/__tests__/useContactsByEmail.test.js +++ b/src/elements/content-sharing/__tests__/useContactsByEmail.test.js @@ -1,8 +1,6 @@ // @flow -import React, { act } from 'react'; -import { mount } from 'enzyme'; -import API from '../../../api'; +import { renderHook, act } from '@testing-library/react'; import useContactsByEmail from '../hooks/useContactsByEmail'; import { MOCK_CONTACTS_API_RESPONSE, @@ -12,34 +10,12 @@ import { const handleSuccess = jest.fn(); const handleError = jest.fn(); -const transformUsersSpy = jest.fn().mockReturnValue(MOCK_CONTACTS_BY_EMAIL_CONVERTED_RESPONSE); +const mockTransformUsers = jest.fn().mockReturnValue(MOCK_CONTACTS_BY_EMAIL_CONVERTED_RESPONSE); const createAPIMock = markerBasedUsersAPI => ({ getMarkerBasedUsersAPI: jest.fn().mockReturnValue(markerBasedUsersAPI), }); -function FakeComponent({ api, transformUsers }: { api: API, transformUsers: Function }) { - const [getContactsByEmail, setGetContactsByEmail] = React.useState(null); - - const updatedGetContactsByEmailFn = useContactsByEmail(api, MOCK_ITEM_ID, { - handleSuccess, - handleError, - transformUsers, - }); - - if (updatedGetContactsByEmailFn && !getContactsByEmail) { - setGetContactsByEmail(() => updatedGetContactsByEmailFn); - } - - return ( - getContactsByEmail && ( - - ) - ); -} - const MOCK_EMAIL = 'contentsharing@box.com'; describe('elements/content-sharing/hooks/useContactsByEmail', () => { @@ -47,21 +23,30 @@ describe('elements/content-sharing/hooks/useContactsByEmail', () => { let mockAPI; describe('with a successful API call', () => { - beforeAll(() => { + beforeEach(() => { getUsersInEnterprise = jest.fn().mockImplementation((itemID, getUsersInEnterpriseSuccess) => { return getUsersInEnterpriseSuccess(MOCK_CONTACTS_API_RESPONSE); }); mockAPI = createAPIMock({ getUsersInEnterprise }); }); - test('should set the value of getContactsByEmail() and retrieve contacts on invocation', () => { - let fakeComponent; - act(() => { - fakeComponent = mount(); - }); - fakeComponent.update(); + afterEach(() => { + jest.resetAllMocks(); + }); - const contacts = fakeComponent.find('button').invoke('onClick')({ emails: [MOCK_EMAIL] }); + test('should set the value of getContactsByEmail() and retrieve contacts on invocation', async () => { + const { result } = renderHook(() => + useContactsByEmail(mockAPI, MOCK_ITEM_ID, { + handleSuccess, + handleError, + transformUsers: mockTransformUsers, + }), + ); + + let contacts; + await act(async () => { + contacts = await result.current({ emails: [MOCK_EMAIL] }); + }); expect(getUsersInEnterprise).toHaveBeenCalledWith( MOCK_ITEM_ID, @@ -70,18 +55,22 @@ describe('elements/content-sharing/hooks/useContactsByEmail', () => { { filter_term: MOCK_EMAIL }, ); expect(handleSuccess).toHaveBeenCalledWith(MOCK_CONTACTS_API_RESPONSE); - expect(transformUsersSpy).toHaveBeenCalledWith(MOCK_CONTACTS_API_RESPONSE); - return expect(contacts).resolves.toEqual(MOCK_CONTACTS_BY_EMAIL_CONVERTED_RESPONSE); + expect(mockTransformUsers).toHaveBeenCalledWith(MOCK_CONTACTS_API_RESPONSE); + expect(contacts).toEqual(MOCK_CONTACTS_BY_EMAIL_CONVERTED_RESPONSE); }); - test('should return the entries from the API data if transformUsers() is not provided', () => { - let fakeComponent; - act(() => { - fakeComponent = mount(); - }); - fakeComponent.update(); + test('should return the entries from the API data if transformUsers() is not provided', async () => { + const { result } = renderHook(() => + useContactsByEmail(mockAPI, MOCK_ITEM_ID, { + handleSuccess, + handleError, + }), + ); - const contacts = fakeComponent.find('button').invoke('onClick')({ emails: [MOCK_EMAIL] }); + let contacts; + await act(async () => { + contacts = await result.current({ emails: [MOCK_EMAIL] }); + }); expect(getUsersInEnterprise).toHaveBeenCalledWith( MOCK_ITEM_ID, @@ -90,28 +79,33 @@ describe('elements/content-sharing/hooks/useContactsByEmail', () => { { filter_term: MOCK_EMAIL }, ); expect(handleSuccess).toHaveBeenCalledWith(MOCK_CONTACTS_API_RESPONSE); - expect(transformUsersSpy).not.toHaveBeenCalled(); - expect(contacts).resolves.toEqual(MOCK_CONTACTS_API_RESPONSE.entries); + expect(mockTransformUsers).not.toHaveBeenCalled(); + expect(contacts).toEqual(MOCK_CONTACTS_API_RESPONSE.entries); }); - test('should set the value of getContactsByEmail() to an empty object when no results are found', () => { + test('should set the value of getContactsByEmail() to an empty object when no results are found', async () => { const EMPTY_USERS = { entries: [] }; getUsersInEnterprise = jest.fn().mockImplementation((itemID, getUsersInEnterpriseSuccess) => { return getUsersInEnterpriseSuccess(EMPTY_USERS); }); mockAPI = createAPIMock({ getUsersInEnterprise }); - let fakeComponent; - act(() => { - fakeComponent = mount(); - }); - fakeComponent.update(); + const { result } = renderHook(() => + useContactsByEmail(mockAPI, MOCK_ITEM_ID, { + handleSuccess, + handleError, + transformUsers: mockTransformUsers, + }), + ); - const contacts = fakeComponent.find('button').invoke('onClick')({ emails: [MOCK_EMAIL] }); + let contacts; + await act(async () => { + contacts = await result.current({ emails: [MOCK_EMAIL] }); + }); expect(handleSuccess).toHaveBeenCalledWith(EMPTY_USERS); - expect(transformUsersSpy).not.toHaveBeenCalled(); - return expect(contacts).resolves.toEqual({}); + expect(mockTransformUsers).not.toHaveBeenCalled(); + expect(contacts).toEqual({}); }); test.each` @@ -120,23 +114,95 @@ describe('elements/content-sharing/hooks/useContactsByEmail', () => { ${{ content: 'sharing' }} | ${'an object, but does not have an emails key'} ${{ emails: 'contentsharing' }} | ${'an object with the emails key, but filterTerm.emails is not an array'} ${{ emails: [] }} | ${'an object with the emails key, but filterTerm.emails is an empty array'} - `('should return an empty object when filterTerm is $description', ({ filterTerm }) => { - let fakeComponent; - act(() => { - fakeComponent = mount(); - }); - fakeComponent.update(); + `('should return an empty object when filterTerm is $description', async ({ filterTerm }) => { + const { result } = renderHook(() => + useContactsByEmail(mockAPI, MOCK_ITEM_ID, { + handleSuccess, + handleError, + }), + ); - const contacts = fakeComponent.find('button').invoke('onClick')(filterTerm); + let contacts; + await act(async () => { + contacts = await result.current(filterTerm); + }); expect(getUsersInEnterprise).not.toHaveBeenCalled(); expect(handleError).not.toHaveBeenCalled(); - return expect(contacts).resolves.toEqual({}); + expect(contacts).toEqual({}); + }); + + test('should set the value of getContactsByEmail() and retrieve contacts when isContentSharingV2Enabled is true and email is provided', async () => { + const mockUser1 = MOCK_CONTACTS_API_RESPONSE.entries[0]; + const { id, email, name, type } = mockUser1; + const expectedTransformedResult = { + id, + email, + name, + type, + value: email, + }; + const MOCK_CONTACT_BY_EMAIL_API_RESPONSE = { entries: [mockUser1] }; + const mockTransformUsersV2 = jest.fn().mockReturnValue(expectedTransformedResult); + getUsersInEnterprise = jest.fn().mockImplementation((itemID, getUsersInEnterpriseSuccess) => { + return getUsersInEnterpriseSuccess(MOCK_CONTACT_BY_EMAIL_API_RESPONSE); + }); + mockAPI = createAPIMock({ getUsersInEnterprise }); + + const { result } = renderHook(() => + useContactsByEmail(mockAPI, MOCK_ITEM_ID, { + isContentSharingV2Enabled: true, + handleSuccess, + handleError, + transformUsers: mockTransformUsersV2, + }), + ); + + let contacts; + await act(async () => { + contacts = await result.current('contentopenwith@box.com'); + }); + + expect(getUsersInEnterprise).toHaveBeenCalledWith( + MOCK_ITEM_ID, + expect.anything(Function), + expect.anything(Function), + { filter_term: 'contentopenwith@box.com' }, + ); + expect(handleSuccess).toHaveBeenCalledWith(MOCK_CONTACT_BY_EMAIL_API_RESPONSE); + expect(mockTransformUsersV2).toHaveBeenCalledWith(MOCK_CONTACT_BY_EMAIL_API_RESPONSE); + expect(contacts).toEqual(expectedTransformedResult); + }); + + test('should set the value of getContactsByEmail() to an empty object when isContentSharingV2Enabled is true and email is not provided', async () => { + const EMPTY_USERS = { entries: [] }; + getUsersInEnterprise = jest.fn().mockImplementation((itemID, getUsersInEnterpriseSuccess) => { + return getUsersInEnterpriseSuccess(EMPTY_USERS); + }); + mockAPI = createAPIMock({ getUsersInEnterprise }); + + const { result } = renderHook(() => + useContactsByEmail(mockAPI, MOCK_ITEM_ID, { + isContentSharingV2Enabled: true, + handleSuccess, + handleError, + transformUsers: mockTransformUsers, + }), + ); + + let contacts; + await act(async () => { + contacts = await result.current({ emails: [MOCK_EMAIL] }); + }); + + expect(handleSuccess).toHaveBeenCalledWith(EMPTY_USERS); + expect(mockTransformUsers).not.toHaveBeenCalled(); + expect(contacts).toEqual({}); }); }); describe('with a failed API call', () => { - beforeAll(() => { + beforeEach(() => { getUsersInEnterprise = jest .fn() .mockImplementation((itemID, getUsersInEnterpriseSuccess, getUsersInEnterpriseError) => { @@ -145,14 +211,25 @@ describe('elements/content-sharing/hooks/useContactsByEmail', () => { mockAPI = createAPIMock({ getUsersInEnterprise }); }); - test('should set the value of getContactsByEmail() and call handleError() when invoked', () => { - let fakeComponent; - act(() => { - fakeComponent = mount(); - }); - fakeComponent.update(); + afterEach(() => { + jest.resetAllMocks(); + }); + + test('should set the value of getContactsByEmail() and call handleError() when invoked', async () => { + const { result } = renderHook(() => + useContactsByEmail(mockAPI, MOCK_ITEM_ID, { + handleSuccess, + handleError, + transformUsers: mockTransformUsers, + }), + ); + + result.current({ emails: [MOCK_EMAIL] }); - const contacts = fakeComponent.find('button').invoke('onClick')({ emails: [MOCK_EMAIL] }); + // Wait a short time to ensure handleError is called + await act(async () => { + await new Promise(resolve => setTimeout(resolve, 100)); + }); expect(getUsersInEnterprise).toHaveBeenCalledWith( MOCK_ITEM_ID, @@ -161,7 +238,6 @@ describe('elements/content-sharing/hooks/useContactsByEmail', () => { { filter_term: MOCK_EMAIL }, ); expect(handleError).toHaveBeenCalled(); - expect(contacts).resolves.toBeFalsy(); }); }); }); diff --git a/src/elements/content-sharing/hooks/__tests__/useContactService.test.ts b/src/elements/content-sharing/hooks/__tests__/useContactService.test.ts index 9ce3382d4c..84aaa7cfe5 100644 --- a/src/elements/content-sharing/hooks/__tests__/useContactService.test.ts +++ b/src/elements/content-sharing/hooks/__tests__/useContactService.test.ts @@ -1,41 +1,55 @@ import { renderHook } from '@testing-library/react'; -import { convertGroupContactsResponse, convertUserContactsResponse } from '../../utils'; +import { + convertGroupContactsResponse, + convertUserContactByEmailResponse, + convertUserContactsResponse, +} from '../../utils'; import { useContactService } from '../useContactService'; import useContacts from '../useContacts'; +import useContactsByEmail from '../useContactsByEmail'; jest.mock('../useContacts'); +jest.mock('../useContactsByEmail'); jest.mock('../../utils'); const mockApi = { getMarkerBasedUsersAPI: jest.fn(), getMarkerBasedGroupsAPI: jest.fn(), }; -const mockItemID = '123456789'; -const mockCurrentUserID = '123'; +const mockItemId = '123456789'; +const mockCurrentUserId = '123'; const mockGetContacts = jest.fn(); +const mockGetContactByEmail = jest.fn(); describe('elements/content-sharing/hooks/useContactService', () => { beforeEach(() => { (useContacts as jest.Mock).mockReturnValue(mockGetContacts); + (useContactsByEmail as jest.Mock).mockReturnValue(mockGetContactByEmail); (convertGroupContactsResponse as jest.Mock).mockReturnValue([]); (convertUserContactsResponse as jest.Mock).mockReturnValue([]); + (convertUserContactByEmailResponse as jest.Mock).mockReturnValue([]); }); afterEach(() => { jest.clearAllMocks(); }); - test('should return contactService with getContacts function', () => { - const { result } = renderHook(() => useContactService(mockApi, mockItemID, mockCurrentUserID)); + test('should return contactService with getContactByEmail and getContacts functions', () => { + const { result } = renderHook(() => useContactService(mockApi, mockItemId, mockCurrentUserId)); - expect(useContacts).toHaveBeenCalledWith(mockApi, mockItemID, { - currentUserId: mockCurrentUserID, + expect(useContacts).toHaveBeenCalledWith(mockApi, mockItemId, { + currentUserId: mockCurrentUserId, isContentSharingV2Enabled: true, transformUsers: expect.any(Function), transformGroups: expect.any(Function), }); + expect(useContactsByEmail).toHaveBeenCalledWith(mockApi, mockItemId, { + isContentSharingV2Enabled: true, + transformUsers: expect.any(Function), + }); expect(result.current.contactService).toEqual({ + getContactByEmail: mockGetContactByEmail, getContacts: mockGetContacts, }); }); @@ -43,13 +57,16 @@ describe('elements/content-sharing/hooks/useContactService', () => { test('should pass transform functions that call correct conversion functions with params', () => { const mockTransformedUsers = [{ id: 'user1', email: 'user1@test.com' }]; const mockTransformedGroups = [{ id: 'group1', name: 'Test Group' }]; + const mockTransformedContactByEmail = [{ id: 'user2', email: 'user2@test.com' }]; const mockUserData = { entries: mockTransformedUsers }; const mockGroupData = { entries: mockTransformedGroups }; + const mockContactByEmailData = { entries: mockTransformedContactByEmail }; (convertUserContactsResponse as jest.Mock).mockReturnValue(mockTransformedUsers); (convertGroupContactsResponse as jest.Mock).mockReturnValue(mockTransformedGroups); + (convertUserContactByEmailResponse as jest.Mock).mockReturnValue(mockTransformedContactByEmail); - renderHook(() => useContactService(mockApi, mockItemID, mockCurrentUserID)); + renderHook(() => useContactService(mockApi, mockItemId, mockCurrentUserId)); // Get the transform functions that were passed to useContacts const transformUsersFn = useContacts.mock.calls[0][2].transformUsers; @@ -57,9 +74,15 @@ describe('elements/content-sharing/hooks/useContactService', () => { const resultUsers = transformUsersFn(mockUserData); const resultGroups = transformGroupsFn(mockGroupData); - expect(convertUserContactsResponse as jest.Mock).toHaveBeenCalledWith(mockUserData, mockCurrentUserID); + expect(convertUserContactsResponse as jest.Mock).toHaveBeenCalledWith(mockUserData, mockCurrentUserId); expect(convertGroupContactsResponse as jest.Mock).toHaveBeenCalledWith(mockGroupData); expect(resultUsers).toBe(mockTransformedUsers); expect(resultGroups).toBe(mockTransformedGroups); + + // Get the transform function that was passed to useContactsByEmail + const transformContactByEmailFn = useContactsByEmail.mock.calls[0][2].transformUsers; + const resultContactByEmail = transformContactByEmailFn(mockContactByEmailData); + expect(convertUserContactByEmailResponse as jest.Mock).toHaveBeenCalledWith(mockContactByEmailData); + expect(resultContactByEmail).toBe(mockTransformedContactByEmail); }); }); diff --git a/src/elements/content-sharing/hooks/useContactService.ts b/src/elements/content-sharing/hooks/useContactService.ts index 589c0547d8..4e029f9dc8 100644 --- a/src/elements/content-sharing/hooks/useContactService.ts +++ b/src/elements/content-sharing/hooks/useContactService.ts @@ -1,4 +1,5 @@ -import { convertGroupContactsResponse, convertUserContactsResponse } from '../utils'; +import { convertGroupContactsResponse, convertUserContactByEmailResponse, convertUserContactsResponse } from '../utils'; +import useContactsByEmail from './useContactsByEmail'; import useContacts from './useContacts'; export const useContactService = (api, itemId, currentUserId) => { @@ -9,5 +10,10 @@ export const useContactService = (api, itemId, currentUserId) => { transformGroups: data => convertGroupContactsResponse(data), }); - return { contactService: { getContacts } }; + const getContactByEmail = useContactsByEmail(api, itemId, { + isContentSharingV2Enabled: true, + transformUsers: data => convertUserContactByEmailResponse(data), + }); + + return { contactService: { getContactByEmail, getContacts } }; }; diff --git a/src/elements/content-sharing/hooks/useContactsByEmail.js b/src/elements/content-sharing/hooks/useContactsByEmail.js index d45c0eea36..32c840d4c8 100644 --- a/src/elements/content-sharing/hooks/useContactsByEmail.js +++ b/src/elements/content-sharing/hooks/useContactsByEmail.js @@ -20,7 +20,7 @@ function useContactsByEmail( options: ContentSharingHooksOptions, ): GetContactsByEmailFnType | null { const [getContactsByEmail, setGetContactsByEmail] = React.useState(null); - const { handleSuccess = noop, handleError = noop, transformUsers } = options; + const { handleSuccess = noop, handleError = noop, isContentSharingV2Enabled, transformUsers } = options; React.useEffect(() => { if (getContactsByEmail) return; @@ -38,25 +38,42 @@ function useContactsByEmail( return resolve({}); }; - const updatedGetContactsByEmailFn: GetContactsByEmailFnType = () => (filterTerm: { - [emails: string]: string, - }) => { - if (!filterTerm || !Array.isArray(filterTerm.emails) || !filterTerm.emails.length) { + const updatedGetContactsByEmailFn: GetContactsByEmailFnType = + () => (filterTerm: { [emails: string]: string }) => { + if (!filterTerm || !Array.isArray(filterTerm.emails) || !filterTerm.emails.length) { + return Promise.resolve({}); + } + const parsedFilterTerm = filterTerm.emails[0]; + + return new Promise((resolve: (result: ContactByEmailObject | Array) => void) => { + api.getMarkerBasedUsersAPI(false).getUsersInEnterprise( + itemID, + (response: UserCollection) => resolveAPICall(resolve, response, transformUsers), + handleError, + { filter_term: parsedFilterTerm }, + ); + }); + }; + + const getContactsByEmailV2 = () => email => { + if (!email) { return Promise.resolve({}); } - const parsedFilterTerm = filterTerm.emails[0]; - return new Promise((resolve: (result: ContactByEmailObject | Array) => void) => { + return new Promise(resolve => { api.getMarkerBasedUsersAPI(false).getUsersInEnterprise( itemID, - (response: UserCollection) => resolveAPICall(resolve, response, transformUsers), + response => resolveAPICall(resolve, response, transformUsers), handleError, - { filter_term: parsedFilterTerm }, + { filter_term: email }, ); }); }; - setGetContactsByEmail(updatedGetContactsByEmailFn); - }, [api, getContactsByEmail, handleError, handleSuccess, itemID, transformUsers]); + + isContentSharingV2Enabled + ? setGetContactsByEmail(getContactsByEmailV2) + : setGetContactsByEmail(updatedGetContactsByEmailFn); + }, [api, getContactsByEmail, handleError, handleSuccess, isContentSharingV2Enabled, itemID, transformUsers]); return getContactsByEmail; } diff --git a/src/elements/content-sharing/utils/__tests__/convertContactServiceData.test.ts b/src/elements/content-sharing/utils/__tests__/convertContactServiceData.test.ts index ba534f57fa..05caaaf234 100644 --- a/src/elements/content-sharing/utils/__tests__/convertContactServiceData.test.ts +++ b/src/elements/content-sharing/utils/__tests__/convertContactServiceData.test.ts @@ -1,5 +1,9 @@ import { STATUS_INACTIVE } from '../../../../constants'; -import { convertUserContactsResponse, convertGroupContactsResponse } from '../convertContactServiceData'; +import { + convertUserContactsResponse, + convertGroupContactsResponse, + convertUserContactByEmailResponse, +} from '../convertContactServiceData'; const mockCurrentUserId = '123'; @@ -329,4 +333,104 @@ describe('elements/content-sharing/utils/convertContactServiceData', () => { expect(result[2].name).toBe('Charlie Group'); }); }); + + describe('convertUserContactByEmailResponse', () => { + describe('basic conversion', () => { + test('should return empty object when entries is empty', () => { + const contactsApiData = { entries: [] }; + const result = convertUserContactByEmailResponse(contactsApiData); + expect(result).toEqual({}); + }); + + test('should convert valid user contact correctly', () => { + const contactsApiData = { + entries: [ + { + id: 'user-1', + login: 'jane.smith@example.com', + name: 'Jane Smith', + type: 'user', + }, + ], + }; + + const result = convertUserContactByEmailResponse(contactsApiData); + + expect(result).toEqual({ + id: 'user-1', + email: 'jane.smith@example.com', + name: 'Jane Smith', + type: 'user', + value: 'jane.smith@example.com', + }); + }); + + test('should handle user contact with missing login field', () => { + const contactsApiData = { + entries: [ + { + id: 'user-1', + name: 'Jane Smith', + type: 'user', + }, + ], + }; + + const result = convertUserContactByEmailResponse(contactsApiData); + + expect(result).toEqual({ + id: 'user-1', + email: '', + name: 'Jane Smith', + type: 'user', + value: '', + }); + }); + + test('should handle user contact with undefined login field', () => { + const contactsApiData = { + entries: [ + { + id: 'user-1', + name: 'Jane Smith', + type: 'user', + }, + ], + }; + + const result = convertUserContactByEmailResponse(contactsApiData); + + expect(result).toEqual({ + id: 'user-1', + email: '', + name: 'Jane Smith', + type: 'user', + value: '', + }); + }); + + test('should handle user contact with undefined login field', () => { + const contactsApiData = { + entries: [ + { + id: 'user-1', + login: undefined, + name: 'Jane Smith', + type: 'user', + }, + ], + }; + + const result = convertUserContactByEmailResponse(contactsApiData); + + expect(result).toEqual({ + id: 'user-1', + email: '', + name: 'Jane Smith', + type: 'user', + value: '', + }); + }); + }); + }); }); diff --git a/src/elements/content-sharing/utils/convertContactServiceData.ts b/src/elements/content-sharing/utils/convertContactServiceData.ts index 45d92e09ae..e639a72754 100644 --- a/src/elements/content-sharing/utils/convertContactServiceData.ts +++ b/src/elements/content-sharing/utils/convertContactServiceData.ts @@ -55,3 +55,24 @@ export const convertGroupContactsResponse = contactsApiData => { }) .sort(sortByName); }; + +/** + * Convert an enterprise users API response into an object of internal USM contacts, keyed by email, which is + * then passed to the mergeContacts function. + */ +export const convertUserContactByEmailResponse = contactsApiData => { + const { entries = [] } = contactsApiData; + const entry = entries[0]; + if (!entry) { + return {}; + } + + const { id, login: email = '', name, type } = entry; + return { + id, + email, + name, + type, + value: email, + }; +}; diff --git a/src/elements/content-sharing/utils/index.ts b/src/elements/content-sharing/utils/index.ts index a7de99c363..24f9717345 100644 --- a/src/elements/content-sharing/utils/index.ts +++ b/src/elements/content-sharing/utils/index.ts @@ -1,6 +1,10 @@ export { convertCollabsResponse } from './convertCollaborators'; export { convertItemResponse } from './convertItemResponse'; -export { convertGroupContactsResponse, convertUserContactsResponse } from './convertContactServiceData'; +export { + convertGroupContactsResponse, + convertUserContactByEmailResponse, + convertUserContactsResponse, +} from './convertContactServiceData'; export { convertSharedLinkPermissions, convertSharedLinkSettings } from './convertSharingServiceData'; export { getAllowedAccessLevels } from './getAllowedAccessLevels'; export { getAllowedPermissionLevels } from './getAllowedPermissionLevels'; From c5532e3cfb93670cc3616105389ddd5928d0d97b Mon Sep 17 00:00:00 2001 From: reneshen0328 Date: Fri, 17 Oct 2025 09:41:28 -0700 Subject: [PATCH 2/5] fix: types and improve code --- .../__tests__/useContactsByEmail.test.js | 2 +- .../hooks/useContactsByEmail.js | 55 +++++++++++-------- src/elements/content-sharing/types.js | 2 + .../utils/convertContactServiceData.ts | 3 +- 4 files changed, 35 insertions(+), 27 deletions(-) diff --git a/src/elements/content-sharing/__tests__/useContactsByEmail.test.js b/src/elements/content-sharing/__tests__/useContactsByEmail.test.js index d0620fe1c0..c6af64b54f 100644 --- a/src/elements/content-sharing/__tests__/useContactsByEmail.test.js +++ b/src/elements/content-sharing/__tests__/useContactsByEmail.test.js @@ -134,7 +134,7 @@ describe('elements/content-sharing/hooks/useContactsByEmail', () => { test('should set the value of getContactsByEmail() and retrieve contacts when isContentSharingV2Enabled is true and email is provided', async () => { const mockUser1 = MOCK_CONTACTS_API_RESPONSE.entries[0]; - const { id, email, name, type } = mockUser1; + const { id, login: email, name, type } = mockUser1; const expectedTransformedResult = { id, email, diff --git a/src/elements/content-sharing/hooks/useContactsByEmail.js b/src/elements/content-sharing/hooks/useContactsByEmail.js index 32c840d4c8..fd968bf1ec 100644 --- a/src/elements/content-sharing/hooks/useContactsByEmail.js +++ b/src/elements/content-sharing/hooks/useContactsByEmail.js @@ -4,7 +4,12 @@ import * as React from 'react'; import noop from 'lodash/noop'; import API from '../../../api'; import type { UserCollection, UserMini } from '../../../common/types/core'; -import type { ContactByEmailObject, ContentSharingHooksOptions, GetContactsByEmailFnType } from '../types'; +import type { + ContactByEmailObject, + ContentSharingHooksOptions, + GetContactByEmailFnType, + GetContactsByEmailFnType, +} from '../types'; /** * Generate the getContactsByEmail() function, which is used for looking up contacts added to the collaborators field in the USM. @@ -38,41 +43,43 @@ function useContactsByEmail( return resolve({}); }; - const updatedGetContactsByEmailFn: GetContactsByEmailFnType = - () => (filterTerm: { [emails: string]: string }) => { - if (!filterTerm || !Array.isArray(filterTerm.emails) || !filterTerm.emails.length) { + if (isContentSharingV2Enabled) { + const getContactsByEmailV2: GetContactByEmailFnType = () => email => { + if (!email) { return Promise.resolve({}); } - const parsedFilterTerm = filterTerm.emails[0]; - return new Promise((resolve: (result: ContactByEmailObject | Array) => void) => { + return new Promise(resolve => { api.getMarkerBasedUsersAPI(false).getUsersInEnterprise( itemID, - (response: UserCollection) => resolveAPICall(resolve, response, transformUsers), + response => resolveAPICall(resolve, response, transformUsers), handleError, - { filter_term: parsedFilterTerm }, + { filter_term: email }, ); }); }; - const getContactsByEmailV2 = () => email => { - if (!email) { - return Promise.resolve({}); - } + setGetContactsByEmail(getContactsByEmailV2); + } else { + const updatedGetContactsByEmailFn: GetContactsByEmailFnType = + () => (filterTerm: { [emails: string]: string }) => { + if (!filterTerm || !Array.isArray(filterTerm.emails) || !filterTerm.emails.length) { + return Promise.resolve({}); + } + const parsedFilterTerm = filterTerm.emails[0]; - return new Promise(resolve => { - api.getMarkerBasedUsersAPI(false).getUsersInEnterprise( - itemID, - response => resolveAPICall(resolve, response, transformUsers), - handleError, - { filter_term: email }, - ); - }); - }; + return new Promise((resolve: (result: ContactByEmailObject | Array) => void) => { + api.getMarkerBasedUsersAPI(false).getUsersInEnterprise( + itemID, + (response: UserCollection) => resolveAPICall(resolve, response, transformUsers), + handleError, + { filter_term: parsedFilterTerm }, + ); + }); + }; - isContentSharingV2Enabled - ? setGetContactsByEmail(getContactsByEmailV2) - : setGetContactsByEmail(updatedGetContactsByEmailFn); + setGetContactsByEmail(updatedGetContactsByEmailFn); + } }, [api, getContactsByEmail, handleError, handleSuccess, isContentSharingV2Enabled, itemID, transformUsers]); return getContactsByEmail; diff --git a/src/elements/content-sharing/types.js b/src/elements/content-sharing/types.js index 4a375dbc1d..fec4d92b91 100644 --- a/src/elements/content-sharing/types.js +++ b/src/elements/content-sharing/types.js @@ -140,6 +140,8 @@ export type GetContactsByEmailFnType = () => (filterTerm: { [emails: string]: string, }) => Promise> | null; +export type GetContactByEmailFnType = () => (email: string) => Promise> | null; + export type SendInvitesFnType = () => InviteCollaboratorsRequest => Promise>; export type ConnectToItemShareFnType = ({ diff --git a/src/elements/content-sharing/utils/convertContactServiceData.ts b/src/elements/content-sharing/utils/convertContactServiceData.ts index e639a72754..759791660b 100644 --- a/src/elements/content-sharing/utils/convertContactServiceData.ts +++ b/src/elements/content-sharing/utils/convertContactServiceData.ts @@ -57,8 +57,7 @@ export const convertGroupContactsResponse = contactsApiData => { }; /** - * Convert an enterprise users API response into an object of internal USM contacts, keyed by email, which is - * then passed to the mergeContacts function. + * Convert an enterprise users API response into a single internal USM contact object (from the first entry). */ export const convertUserContactByEmailResponse = contactsApiData => { const { entries = [] } = contactsApiData; From e3de11ce5dbcfc2da343a9d63e61636589405678 Mon Sep 17 00:00:00 2001 From: reneshen0328 Date: Fri, 17 Oct 2025 10:11:56 -0700 Subject: [PATCH 3/5] fix: previous pr comment --- i18n/en-US.properties | 2 ++ .../hooks/__tests__/useContactService.test.ts | 2 +- .../content-sharing/hooks/useContactService.ts | 9 ++++++++- src/elements/content-sharing/messages.js | 5 +++++ .../__tests__/convertContactServiceData.test.ts | 12 ++++++------ .../utils/convertContactServiceData.ts | 6 +++--- 6 files changed, 25 insertions(+), 11 deletions(-) diff --git a/i18n/en-US.properties b/i18n/en-US.properties index 6a4d3b5999..812247735f 100644 --- a/i18n/en-US.properties +++ b/i18n/en-US.properties @@ -138,6 +138,8 @@ be.contentInsights.trendYear = PAST YEAR be.contentSharing.badRequestError = The request for this item was malformed. # Message that appears when collaborators cannot be retrieved in the ContentSharing Element. be.contentSharing.collaboratorsLoadingError = Could not retrieve collaborators for this item. +# Display text for a Group contact type +be.contentSharing.contactServiceGroupDisplayText = Group # Message that appears when users cannot be retrieved in the ContentSharing Element. be.contentSharing.getContactsError = Could not retrieve contacts. # Message that appears when the ContentSharing Element cannot be loaded. diff --git a/src/elements/content-sharing/hooks/__tests__/useContactService.test.ts b/src/elements/content-sharing/hooks/__tests__/useContactService.test.ts index 84aaa7cfe5..21827cd58b 100644 --- a/src/elements/content-sharing/hooks/__tests__/useContactService.test.ts +++ b/src/elements/content-sharing/hooks/__tests__/useContactService.test.ts @@ -75,7 +75,7 @@ describe('elements/content-sharing/hooks/useContactService', () => { const resultGroups = transformGroupsFn(mockGroupData); expect(convertUserContactsResponse as jest.Mock).toHaveBeenCalledWith(mockUserData, mockCurrentUserId); - expect(convertGroupContactsResponse as jest.Mock).toHaveBeenCalledWith(mockGroupData); + expect(convertGroupContactsResponse as jest.Mock).toHaveBeenCalledWith(mockGroupData, 'Group'); expect(resultUsers).toBe(mockTransformedUsers); expect(resultGroups).toBe(mockTransformedGroups); diff --git a/src/elements/content-sharing/hooks/useContactService.ts b/src/elements/content-sharing/hooks/useContactService.ts index 4e029f9dc8..ab78b9d306 100644 --- a/src/elements/content-sharing/hooks/useContactService.ts +++ b/src/elements/content-sharing/hooks/useContactService.ts @@ -1,13 +1,20 @@ +import { useIntl } from 'react-intl'; + import { convertGroupContactsResponse, convertUserContactByEmailResponse, convertUserContactsResponse } from '../utils'; import useContactsByEmail from './useContactsByEmail'; import useContacts from './useContacts'; +import messages from '../messages'; + export const useContactService = (api, itemId, currentUserId) => { + const { formatMessage } = useIntl(); + const getContacts = useContacts(api, itemId, { currentUserId, isContentSharingV2Enabled: true, transformUsers: data => convertUserContactsResponse(data, currentUserId), - transformGroups: data => convertGroupContactsResponse(data), + transformGroups: data => + convertGroupContactsResponse(data, formatMessage(messages.contactServiceGroupDisplayText)), }); const getContactByEmail = useContactsByEmail(api, itemId, { diff --git a/src/elements/content-sharing/messages.js b/src/elements/content-sharing/messages.js index 1c9b8ddd3f..5876f49e83 100644 --- a/src/elements/content-sharing/messages.js +++ b/src/elements/content-sharing/messages.js @@ -59,6 +59,11 @@ const messages = defineMessages({ 'Message that appears when collaborators were added to the shared link in the ContentSharing Element.', id: 'be.contentSharing.sendInvitesSuccess', }, + contactServiceGroupDisplayText: { + defaultMessage: 'Group', + description: 'Display text for a Group contact type', + id: 'be.contentSharing.contactServiceGroupDisplayText', + }, }); export default messages; diff --git a/src/elements/content-sharing/utils/__tests__/convertContactServiceData.test.ts b/src/elements/content-sharing/utils/__tests__/convertContactServiceData.test.ts index 05caaaf234..62e2704af4 100644 --- a/src/elements/content-sharing/utils/__tests__/convertContactServiceData.test.ts +++ b/src/elements/content-sharing/utils/__tests__/convertContactServiceData.test.ts @@ -207,7 +207,7 @@ describe('elements/content-sharing/utils/convertContactServiceData', () => { describe('basic conversion', () => { test('should return empty array when entries is empty', () => { const contactsApiData = { entries: [] }; - const result = convertGroupContactsResponse(contactsApiData); + const result = convertGroupContactsResponse(contactsApiData, 'Group'); expect(result).toEqual([]); }); @@ -233,7 +233,7 @@ describe('elements/content-sharing/utils/convertContactServiceData', () => { ], }; - const result = convertGroupContactsResponse(contactsApiData); + const result = convertGroupContactsResponse(contactsApiData, 'Group'); expect(result).toEqual([ { @@ -241,14 +241,14 @@ describe('elements/content-sharing/utils/convertContactServiceData', () => { email: 'Group', name: 'Engineering Team', type: 'group', - value: 'Group', + value: 'group-1', }, { id: 'group-2', email: 'Group', name: 'Marketing Team', type: 'group', - value: 'Group', + value: 'group-2', }, ]); }); @@ -288,7 +288,7 @@ describe('elements/content-sharing/utils/convertContactServiceData', () => { ], }; - const result = convertGroupContactsResponse(contactsApiData); + const result = convertGroupContactsResponse(contactsApiData, 'Group'); expect(result).toHaveLength(1); expect(result[0].id).toBe('group-4'); @@ -325,7 +325,7 @@ describe('elements/content-sharing/utils/convertContactServiceData', () => { ], }; - const result = convertGroupContactsResponse(contactsApiData); + const result = convertGroupContactsResponse(contactsApiData, 'Group'); expect(result).toHaveLength(3); expect(result[0].name).toBe('Alice Group'); diff --git a/src/elements/content-sharing/utils/convertContactServiceData.ts b/src/elements/content-sharing/utils/convertContactServiceData.ts index 759791660b..b8213a8438 100644 --- a/src/elements/content-sharing/utils/convertContactServiceData.ts +++ b/src/elements/content-sharing/utils/convertContactServiceData.ts @@ -35,7 +35,7 @@ export const convertUserContactsResponse = (contactsApiData, currentUserId) => { /** * Convert an enterprise groups API response into an array of internal USM contacts. */ -export const convertGroupContactsResponse = contactsApiData => { +export const convertGroupContactsResponse = (contactsApiData, formatMessage) => { const { entries = [] } = contactsApiData; // Only return groups with the correct permissions @@ -47,10 +47,10 @@ export const convertGroupContactsResponse = contactsApiData => { const { id, name, type } = contact; return { id, - email: 'Group', // Need this for the avatar to work for isUserContactType + email: formatMessage, // Need this for the avatar to work for isUserContactType name, type, - value: 'Group', + value: id, }; }) .sort(sortByName); From 000a4f04fb1fbb67fb746b51185b37f2594b2c07 Mon Sep 17 00:00:00 2001 From: reneshen0328 Date: Fri, 17 Oct 2025 10:35:59 -0700 Subject: [PATCH 4/5] fix: nit --- .../__tests__/useContactsByEmail.test.js | 2 +- .../convertContactServiceData.test.ts | 23 ------------------- 2 files changed, 1 insertion(+), 24 deletions(-) diff --git a/src/elements/content-sharing/__tests__/useContactsByEmail.test.js b/src/elements/content-sharing/__tests__/useContactsByEmail.test.js index c6af64b54f..10c5c5ff35 100644 --- a/src/elements/content-sharing/__tests__/useContactsByEmail.test.js +++ b/src/elements/content-sharing/__tests__/useContactsByEmail.test.js @@ -192,7 +192,7 @@ describe('elements/content-sharing/hooks/useContactsByEmail', () => { let contacts; await act(async () => { - contacts = await result.current({ emails: [MOCK_EMAIL] }); + contacts = await result.current({ MOCK_EMAIL }); }); expect(handleSuccess).toHaveBeenCalledWith(EMPTY_USERS); diff --git a/src/elements/content-sharing/utils/__tests__/convertContactServiceData.test.ts b/src/elements/content-sharing/utils/__tests__/convertContactServiceData.test.ts index 62e2704af4..b535de4d0f 100644 --- a/src/elements/content-sharing/utils/__tests__/convertContactServiceData.test.ts +++ b/src/elements/content-sharing/utils/__tests__/convertContactServiceData.test.ts @@ -408,29 +408,6 @@ describe('elements/content-sharing/utils/convertContactServiceData', () => { value: '', }); }); - - test('should handle user contact with undefined login field', () => { - const contactsApiData = { - entries: [ - { - id: 'user-1', - login: undefined, - name: 'Jane Smith', - type: 'user', - }, - ], - }; - - const result = convertUserContactByEmailResponse(contactsApiData); - - expect(result).toEqual({ - id: 'user-1', - email: '', - name: 'Jane Smith', - type: 'user', - value: '', - }); - }); }); }); }); From 4d26ec893a0ab9e5e0bf61ad957f00ac0440f1d1 Mon Sep 17 00:00:00 2001 From: reneshen0328 Date: Fri, 17 Oct 2025 11:55:52 -0700 Subject: [PATCH 5/5] fix: flow --- i18n/en-US.properties | 4 +-- src/elements/content-sharing/SharingModal.js | 29 ++++++++++--------- .../hooks/useContactService.ts | 3 +- .../hooks/useContactsByEmail.js | 8 +++-- src/elements/content-sharing/messages.js | 4 +-- .../utils/convertContactServiceData.ts | 4 +-- 6 files changed, 28 insertions(+), 24 deletions(-) diff --git a/i18n/en-US.properties b/i18n/en-US.properties index 812247735f..ab057cdb44 100644 --- a/i18n/en-US.properties +++ b/i18n/en-US.properties @@ -138,10 +138,10 @@ be.contentInsights.trendYear = PAST YEAR be.contentSharing.badRequestError = The request for this item was malformed. # Message that appears when collaborators cannot be retrieved in the ContentSharing Element. be.contentSharing.collaboratorsLoadingError = Could not retrieve collaborators for this item. -# Display text for a Group contact type -be.contentSharing.contactServiceGroupDisplayText = Group # Message that appears when users cannot be retrieved in the ContentSharing Element. be.contentSharing.getContactsError = Could not retrieve contacts. +# Display text for a Group contact type +be.contentSharing.groupContactLabel = Group # Message that appears when the ContentSharing Element cannot be loaded. be.contentSharing.loadingError = Could not load shared link for this item. # Message that appears when the user cannot access the item for the ContentSharing Element. diff --git a/src/elements/content-sharing/SharingModal.js b/src/elements/content-sharing/SharingModal.js index 64e7e85341..d035828ab1 100644 --- a/src/elements/content-sharing/SharingModal.js +++ b/src/elements/content-sharing/SharingModal.js @@ -39,6 +39,7 @@ import type { import type { ContentSharingItemAPIResponse, ContentSharingSharedLinkType, + GetContactByEmailFnType, GetContactsFnType, GetContactsByEmailFnType, SendInvitesFnType, @@ -80,18 +81,16 @@ function SharingModal({ const [collaboratorsList, setCollaboratorsList] = React.useState(null); const [onAddLink, setOnAddLink] = React.useState(null); const [onRemoveLink, setOnRemoveLink] = React.useState(null); - const [ - changeSharedLinkAccessLevel, - setChangeSharedLinkAccessLevel, - ] = React.useState(null); - const [ - changeSharedLinkPermissionLevel, - setChangeSharedLinkPermissionLevel, - ] = React.useState(null); + const [changeSharedLinkAccessLevel, setChangeSharedLinkAccessLevel] = + React.useState(null); + const [changeSharedLinkPermissionLevel, setChangeSharedLinkPermissionLevel] = + React.useState(null); const [onSubmitSettings, setOnSubmitSettings] = React.useState(null); const [currentView, setCurrentView] = React.useState(CONTENT_SHARING_VIEWS.UNIFIED_SHARE_MODAL); const [getContacts, setGetContacts] = React.useState(null); - const [getContactsByEmail, setGetContactsByEmail] = React.useState(null); + const [getContactsByEmail, setGetContactsByEmail] = React.useState< + null | GetContactsByEmailFnType | GetContactByEmailFnType, + >(null); const [sendInvites, setSendInvites] = React.useState(null); const [isLoading, setIsLoading] = React.useState(true); @@ -191,11 +190,15 @@ function SharingModal({ // Set the getContactsByEmail function. This call is not associated with a banner notification, // which is why it exists at this level and not in SharingNotification - const getContactsByEmailFn: GetContactsByEmailFnType | null = useContactsByEmail(api, itemID, { - transformUsers: data => convertUserContactsByEmailResponse(data), - }); + const getContactsByEmailFn: GetContactsByEmailFnType | GetContactByEmailFnType | null = useContactsByEmail( + api, + itemID, + { + transformUsers: data => convertUserContactsByEmailResponse(data), + }, + ); if (getContactsByEmailFn && !getContactsByEmail) { - setGetContactsByEmail((): GetContactsByEmailFnType => getContactsByEmailFn); + setGetContactsByEmail((): GetContactsByEmailFnType | GetContactByEmailFnType => getContactsByEmailFn); } // Display a notification if there is an error in retrieving initial data diff --git a/src/elements/content-sharing/hooks/useContactService.ts b/src/elements/content-sharing/hooks/useContactService.ts index ab78b9d306..c48dbc937c 100644 --- a/src/elements/content-sharing/hooks/useContactService.ts +++ b/src/elements/content-sharing/hooks/useContactService.ts @@ -13,8 +13,7 @@ export const useContactService = (api, itemId, currentUserId) => { currentUserId, isContentSharingV2Enabled: true, transformUsers: data => convertUserContactsResponse(data, currentUserId), - transformGroups: data => - convertGroupContactsResponse(data, formatMessage(messages.contactServiceGroupDisplayText)), + transformGroups: data => convertGroupContactsResponse(data, formatMessage(messages.groupContactLabel)), }); const getContactByEmail = useContactsByEmail(api, itemId, { diff --git a/src/elements/content-sharing/hooks/useContactsByEmail.js b/src/elements/content-sharing/hooks/useContactsByEmail.js index fd968bf1ec..90eacc08f9 100644 --- a/src/elements/content-sharing/hooks/useContactsByEmail.js +++ b/src/elements/content-sharing/hooks/useContactsByEmail.js @@ -17,14 +17,16 @@ import type { * @param {API} api * @param {string} itemID * @param {ContentSharingHooksOptions} options - * @returns {GetContactsByEmailFnType | null} + * @returns {GetContactsByEmailFnType | GetContactByEmailFnType | null} */ function useContactsByEmail( api: API, itemID: string, options: ContentSharingHooksOptions, -): GetContactsByEmailFnType | null { - const [getContactsByEmail, setGetContactsByEmail] = React.useState(null); +): GetContactsByEmailFnType | GetContactByEmailFnType | null { + const [getContactsByEmail, setGetContactsByEmail] = React.useState< + null | GetContactsByEmailFnType | GetContactByEmailFnType, + >(null); const { handleSuccess = noop, handleError = noop, isContentSharingV2Enabled, transformUsers } = options; React.useEffect(() => { diff --git a/src/elements/content-sharing/messages.js b/src/elements/content-sharing/messages.js index 5876f49e83..4e2d5a1273 100644 --- a/src/elements/content-sharing/messages.js +++ b/src/elements/content-sharing/messages.js @@ -59,10 +59,10 @@ const messages = defineMessages({ 'Message that appears when collaborators were added to the shared link in the ContentSharing Element.', id: 'be.contentSharing.sendInvitesSuccess', }, - contactServiceGroupDisplayText: { + groupContactLabel: { defaultMessage: 'Group', description: 'Display text for a Group contact type', - id: 'be.contentSharing.contactServiceGroupDisplayText', + id: 'be.contentSharing.groupContactLabel', }, }); diff --git a/src/elements/content-sharing/utils/convertContactServiceData.ts b/src/elements/content-sharing/utils/convertContactServiceData.ts index b8213a8438..d7805e3a54 100644 --- a/src/elements/content-sharing/utils/convertContactServiceData.ts +++ b/src/elements/content-sharing/utils/convertContactServiceData.ts @@ -35,7 +35,7 @@ export const convertUserContactsResponse = (contactsApiData, currentUserId) => { /** * Convert an enterprise groups API response into an array of internal USM contacts. */ -export const convertGroupContactsResponse = (contactsApiData, formatMessage) => { +export const convertGroupContactsResponse = (contactsApiData, emailMessage) => { const { entries = [] } = contactsApiData; // Only return groups with the correct permissions @@ -47,7 +47,7 @@ export const convertGroupContactsResponse = (contactsApiData, formatMessage) => const { id, name, type } = contact; return { id, - email: formatMessage, // Need this for the avatar to work for isUserContactType + email: emailMessage, // Need this for the avatar to work for isUserContactType name, type, value: id,