diff --git a/src/constants.js b/src/constants.js index 0f8a4b8777..da51241d2d 100644 --- a/src/constants.js +++ b/src/constants.js @@ -171,6 +171,7 @@ export const FIELD_UPLOADER_DISPLAY_NAME: 'uploader_display_name' = 'uploader_di export const FIELD_CLASSIFICATION: 'classification' = 'classification'; export const FIELD_ENTERPRISE: 'enterprise' = 'enterprise'; export const FIELD_HOSTNAME: 'hostname' = 'hostname'; +export const FIELD_LOGIN: 'login' = 'login'; /* ----------------------- Item-Prefixed Fields for MD Query API --------------------------- */ const ITEM_PREFIX = 'item.'; diff --git a/src/elements/content-sharing/ContentSharingV2.tsx b/src/elements/content-sharing/ContentSharingV2.tsx index 35d84423fa..a3fa0bc61b 100644 --- a/src/elements/content-sharing/ContentSharingV2.tsx +++ b/src/elements/content-sharing/ContentSharingV2.tsx @@ -11,7 +11,6 @@ import type { Item, SharedLink, SharingService, - User, VariantType, } from '@box/unified-share-modal'; @@ -24,7 +23,7 @@ import { convertCollabsResponse, convertItemResponse } from './utils'; import type { ElementsXhrError } from '../../common/types/api'; import type { Collaborations, ItemType } from '../../common/types/core'; -import type { AvatarURLMap } from './types'; +import type { AvatarURLMap, ContentSharingUser } from './types'; import messages from './messages'; @@ -68,7 +67,7 @@ function ContentSharingV2({ const [hasError, setHasError] = React.useState(false); const [sharedLink, setSharedLink] = React.useState(null); const [sharingServiceProps, setSharingServiceProps] = React.useState(null); - const [currentUser, setCurrentUser] = React.useState(null); + const [currentUser, setCurrentUser] = React.useState(null); const [collaborationRoles, setCollaborationRoles] = React.useState(null); const [collaborators, setCollaborators] = React.useState(null); const [collaboratorsData, setCollaboratorsData] = React.useState(null); @@ -87,7 +86,7 @@ function ContentSharingV2({ api, avatarUrlMap, collaborators, - currentUserId: currentUser?.id, + currentUser, item, itemId, itemType, @@ -187,8 +186,9 @@ function ContentSharingV2({ return; } - const { enterprise, hostname, id } = userData; - setCurrentUser({ id }); + const { enterprise, hostname, id, login } = userData; + const emailDomain = login && /@/.test(login) ? login.split('@')[1] : undefined; + setCurrentUser({ id, email: login, emailDomain }); setSharingServiceProps(prevSharingServiceProps => ({ ...prevSharingServiceProps, serverUrl: hostname ? `${hostname}v/` : '', @@ -266,7 +266,7 @@ function ContentSharingV2({ if (avatarUrlMap && collaboratorsData && currentUser && owner) { const collaboratorsWithAvatars = convertCollabsResponse( collaboratorsData, - currentUser.id, + currentUser, owner, avatarUrlMap, ); diff --git a/src/elements/content-sharing/apis/__tests__/fetchCurrentUser.test.ts b/src/elements/content-sharing/apis/__tests__/fetchCurrentUser.test.ts index f0c7365f72..40df2188ca 100644 --- a/src/elements/content-sharing/apis/__tests__/fetchCurrentUser.test.ts +++ b/src/elements/content-sharing/apis/__tests__/fetchCurrentUser.test.ts @@ -1,5 +1,5 @@ import { DEFAULT_USER_API_RESPONSE, MOCK_ITEM } from '../../utils/__mocks__/ContentSharingV2Mocks'; -import { FIELD_ENTERPRISE, FIELD_HOSTNAME } from '../../../../constants'; +import { FIELD_ENTERPRISE, FIELD_HOSTNAME, FIELD_LOGIN } from '../../../../constants'; import { fetchCurrentUser } from '..'; import { createSuccessMock, createUsersApiMock } from './testUtils'; @@ -17,7 +17,7 @@ describe('content-sharing/apis/fetchCurrentUser', () => { expect(defaultApiMock.getUsersAPI).toHaveBeenCalledWith(false); expect(getDefaultUserMock).toHaveBeenCalledWith(MOCK_ITEM.id, expect.any(Function), expect.any(Function), { params: { - fields: [FIELD_ENTERPRISE, FIELD_HOSTNAME].toString(), + fields: [FIELD_ENTERPRISE, FIELD_HOSTNAME, FIELD_LOGIN].toString(), }, }); expect(result).toEqual(DEFAULT_USER_API_RESPONSE); diff --git a/src/elements/content-sharing/apis/fetchCurrentUser.ts b/src/elements/content-sharing/apis/fetchCurrentUser.ts index 069035cc6a..859e1416dc 100644 --- a/src/elements/content-sharing/apis/fetchCurrentUser.ts +++ b/src/elements/content-sharing/apis/fetchCurrentUser.ts @@ -1,6 +1,6 @@ import type { User } from '@box/unified-share-modal'; -import { FIELD_ENTERPRISE, FIELD_HOSTNAME } from '../../../constants'; +import { FIELD_ENTERPRISE, FIELD_HOSTNAME, FIELD_LOGIN } from '../../../constants'; import type { BaseFetchProps } from '../types'; @@ -8,7 +8,7 @@ export const fetchCurrentUser = async ({ api, itemId }: BaseFetchProps): Promise return new Promise((resolve, reject) => { api.getUsersAPI(false).getUser(itemId, resolve, reject, { params: { - fields: [FIELD_ENTERPRISE, FIELD_HOSTNAME].toString(), + fields: [FIELD_ENTERPRISE, FIELD_HOSTNAME, FIELD_LOGIN].toString(), }, }); }); diff --git a/src/elements/content-sharing/hooks/__tests__/useSharingService.test.ts b/src/elements/content-sharing/hooks/__tests__/useSharingService.test.ts index de48198c3b..0565aeea3c 100644 --- a/src/elements/content-sharing/hooks/__tests__/useSharingService.test.ts +++ b/src/elements/content-sharing/hooks/__tests__/useSharingService.test.ts @@ -70,7 +70,7 @@ const renderHookWithProps = (props = {}) => { api: mockApi, avatarUrlMap: {}, collaborators: [], - currentUserId: '123', + currentUser: { id: '123' }, item: mockItem, itemId: mockItemId, itemType: TYPE_FILE, @@ -274,13 +274,13 @@ describe('elements/content-sharing/hooks/useSharingService', () => { 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'; + const mockCurrentUser = { id: 'current-user-123' }; test('should call useInvites with correct parameters', () => { renderHookWithProps({ collaborators: mockCollaborators, avatarUrlMap: mockAvatarUrlMap, - currentUserId: mockCurrentUserId, + currentUser: mockCurrentUser, }); expect(useInvites).toHaveBeenCalledWith(mockApi, mockItemId, TYPE_FILE, { @@ -302,7 +302,7 @@ describe('elements/content-sharing/hooks/useSharingService', () => { renderHookWithProps({ collaborators: mockCollaborators, avatarUrlMap: mockAvatarUrlMap, - currentUserId: mockCurrentUserId, + currentUser: mockCurrentUser, }); // Get the handleSuccess and setCollaborators function that was passed to useInvites @@ -313,8 +313,7 @@ describe('elements/content-sharing/hooks/useSharingService', () => { expect(convertCollab).toHaveBeenCalledWith({ collab: mockResponse, - currentUserId: mockCurrentUserId, - isCurrentUserOwner: false, + currentUser: mockCurrentUser, ownerEmailDomain: 'test.com', avatarUrlMap: mockAvatarUrlMap, }); diff --git a/src/elements/content-sharing/hooks/useSharingService.ts b/src/elements/content-sharing/hooks/useSharingService.ts index db1b26b019..fff815ee2f 100644 --- a/src/elements/content-sharing/hooks/useSharingService.ts +++ b/src/elements/content-sharing/hooks/useSharingService.ts @@ -15,7 +15,7 @@ export const useSharingService = ({ api, avatarUrlMap, collaborators, - currentUserId, + currentUser, item, itemId, itemType, @@ -85,22 +85,21 @@ export const useSharingService = ({ // Create the sendInvitations callbacks using the existing memoized useInvites hook const handleSuccess = React.useCallback( response => { - const { id: ownerId, login: ownerEmail } = response.created_by; + const { login: ownerEmail } = response.created_by; const ownerEmailDomain = ownerEmail && /@/.test(ownerEmail) ? ownerEmail.split('@')[1] : null; setCollaborators(prevCollabs => { const nextCollab = convertCollab({ avatarUrlMap, collab: response, - currentUserId, - isCurrentUserOwner: currentUserId === ownerId, + currentUser, ownerEmailDomain, }); return nextCollab ? [...prevCollabs, nextCollab] : prevCollabs; }); }, - [avatarUrlMap, currentUserId, setCollaborators], + [avatarUrlMap, currentUser, setCollaborators], ); const handleSendInvitations = useInvites(api, itemId, itemType, { diff --git a/src/elements/content-sharing/types.js b/src/elements/content-sharing/types.js index 89cd2b01d0..5e7c796536 100644 --- a/src/elements/content-sharing/types.js +++ b/src/elements/content-sharing/types.js @@ -1,5 +1,5 @@ // @flow -import type { CollaborationRole, Collaborator, DateValue, Item, SharedLink } from '@box/unified-share-modal'; +import type { CollaborationRole, Collaborator, DateValue, Item, SharedLink, User } from '@box/unified-share-modal'; import API from '../../api'; @@ -190,3 +190,8 @@ export interface SharedLinkSettings { password: string; vanityName: string; } + +export interface ContentSharingUser extends User { + email?: string; + emailDomain?: string; +} diff --git a/src/elements/content-sharing/utils/__mocks__/ContentSharingV2Mocks.js b/src/elements/content-sharing/utils/__mocks__/ContentSharingV2Mocks.js index 1333ff692d..794cb25ef1 100644 --- a/src/elements/content-sharing/utils/__mocks__/ContentSharingV2Mocks.js +++ b/src/elements/content-sharing/utils/__mocks__/ContentSharingV2Mocks.js @@ -40,8 +40,9 @@ export const mockAvatarUrlMap = { }; export const mockOwnerEmail = 'aotter@example.com'; +export const mockOwnerEmailDomain = 'example.com'; export const mockOwnerName = 'Astronaut Otter'; -export const mockOwnerId = 789; +export const mockOwnerId = '789'; export const collabUser1 = { id: 456, @@ -94,6 +95,7 @@ export const EMPTY_COLLABORATIONS_RESPONSE = { export const DEFAULT_USER_API_RESPONSE = { id: '789', enterprise: { id: '12345', name: 'Test Enterprise' }, + login: mockOwner.login, }; export const FREE_USER_API_RESPONSE = { diff --git a/src/elements/content-sharing/utils/__tests__/convertCollaborators.test.ts b/src/elements/content-sharing/utils/__tests__/convertCollaborators.test.ts index fccb6ec13f..53808a26a2 100644 --- a/src/elements/content-sharing/utils/__tests__/convertCollaborators.test.ts +++ b/src/elements/content-sharing/utils/__tests__/convertCollaborators.test.ts @@ -7,17 +7,22 @@ import { mockAvatarUrlMap, mockOwnerId, mockOwnerEmail, + mockOwnerEmailDomain, mockOwnerName, } from '../__mocks__/ContentSharingV2Mocks'; import type { Collaborations } from '../../../../common/types/core'; -const ownerEmailDomain = 'example.com'; const ownerFromApi = { id: mockOwnerId, email: mockOwnerEmail, name: mockOwnerName, }; +const mockCurrentUser = { + id: mockOwnerId, + email: mockOwnerEmail, + emailDomain: mockOwnerEmailDomain, +}; const itemOwner = { id: mockOwnerEmail, status: STATUS_ACCEPTED, @@ -94,9 +99,8 @@ describe('convertCollaborators', () => { const result = convertCollab({ avatarUrlMap: mockAvatarUrlMap, collab: mockCollaborations[1], - currentUserId: mockOwnerId, - isCurrentUserOwner: false, - ownerEmailDomain, + currentUser: mockCurrentUser, + ownerEmailDomain: mockOwnerEmailDomain, }); expect(result).toEqual({ @@ -118,9 +122,8 @@ describe('convertCollaborators', () => { const result = convertCollab({ avatarUrlMap: mockAvatarUrlMap, collab: mockCollaborations[5], - currentUserId: mockOwnerId, - isCurrentUserOwner: false, - ownerEmailDomain, + currentUser: mockCurrentUser, + ownerEmailDomain: mockOwnerEmailDomain, }); expect(result).toEqual({ @@ -141,9 +144,8 @@ describe('convertCollaborators', () => { const result = convertCollab({ avatarUrlMap: mockAvatarUrlMap, collab: mockCollaborations[3], - currentUserId: mockOwnerId, - isCurrentUserOwner: false, - ownerEmailDomain, + currentUser: mockCurrentUser, + ownerEmailDomain: mockOwnerEmailDomain, }); expect(result).toEqual({ @@ -165,9 +167,8 @@ describe('convertCollaborators', () => { const result = convertCollab({ avatarUrlMap: mockAvatarUrlMap, collab, - currentUserId: mockOwnerId, - isCurrentUserOwner: false, - ownerEmailDomain, + currentUser: mockCurrentUser, + ownerEmailDomain: mockOwnerEmailDomain, }); expect(result).toBeNull(); @@ -177,9 +178,8 @@ describe('convertCollaborators', () => { const result = convertCollab({ avatarUrlMap: mockAvatarUrlMap, collab: mockCollaborations[4], - currentUserId: mockOwnerId, - isCurrentUserOwner: false, - ownerEmailDomain, + currentUser: mockCurrentUser, + ownerEmailDomain: mockOwnerEmailDomain, }); expect(result).toBeNull(); @@ -189,9 +189,8 @@ describe('convertCollaborators', () => { const result = convertCollab({ avatarUrlMap: mockAvatarUrlMap, collab: mockCollaborations[6], - currentUserId: mockOwnerId, - isCurrentUserOwner: false, - ownerEmailDomain, + currentUser: mockCurrentUser, + ownerEmailDomain: mockOwnerEmailDomain, }); expect(result).toBeNull(); @@ -201,9 +200,8 @@ describe('convertCollaborators', () => { const result = convertCollab({ avatarUrlMap: mockAvatarUrlMap, collab: mockCollaborations[0], - currentUserId: mockOwnerId, - isCurrentUserOwner: true, - ownerEmailDomain, + currentUser: mockCurrentUser, + ownerEmailDomain: mockOwnerEmailDomain, }); expect(result).toEqual({ @@ -224,23 +222,33 @@ describe('convertCollaborators', () => { const result = convertCollab({ avatarUrlMap: mockAvatarUrlMap, collab: mockCollaborations[2], - currentUserId: mockOwnerId, - isCurrentUserOwner: false, - ownerEmailDomain, + currentUser: mockCurrentUser, + ownerEmailDomain: mockOwnerEmailDomain, }); expect(result.isExternal).toBe(true); }); + test('should not mark collaborator as external when currentUser has no emailDomain', () => { + const currentUserWithoutDomain = { id: mockOwnerId, email: mockOwnerEmail }; + const result = convertCollab({ + avatarUrlMap: mockAvatarUrlMap, + collab: mockCollaborations[2], + currentUser: currentUserWithoutDomain, + ownerEmailDomain: mockOwnerEmailDomain, + }); + + expect(result.isExternal).toBe(false); + }); + test.each([null, undefined, {}, { 999: 'https://example.com/different-user-avatar.jpg' }])( 'should handle %s avatar URL map', avatarUrlMap => { const result = convertCollab({ avatarUrlMap, collab: mockCollaborations[1], - currentUserId: mockOwnerId, - isCurrentUserOwner: false, - ownerEmailDomain, + currentUser: mockCurrentUser, + ownerEmailDomain: mockOwnerEmailDomain, }); expect(result.avatarUrl).toBeUndefined(); @@ -257,9 +265,8 @@ describe('convertCollaborators', () => { const result = convertCollab({ avatarUrlMap: mockAvatarUrlMap, collab: collabWithoutExpiration, - currentUserId: mockOwnerId, - isCurrentUserOwner: false, - ownerEmailDomain, + currentUser: mockCurrentUser, + ownerEmailDomain: mockOwnerEmailDomain, }); expect(result.expiresAt).toBeNull(); @@ -270,7 +277,7 @@ describe('convertCollaborators', () => { test('should convert valid collaborations data to Collaborator array', () => { const result = convertCollabsResponse( mockCollaborationsFromApi, - mockOwnerId, + mockCurrentUser, ownerFromApi, mockAvatarUrlMap, ); @@ -310,7 +317,7 @@ describe('convertCollaborators', () => { hasCustomAvatar: false, id: '124', isCurrentUser: false, - isExternal: false, + isExternal: true, isPending: false, name: 'Raccoon Queen', role: 'viewer', @@ -336,7 +343,7 @@ describe('convertCollaborators', () => { hasCustomAvatar: false, id: '127', isCurrentUser: false, - isExternal: false, + isExternal: true, isPending: true, name: 'bbear@external.example.com', role: 'editor', @@ -346,13 +353,13 @@ describe('convertCollaborators', () => { test('should return empty array for empty entries', () => { const emptyCollaborations: Collaborations = { entries: [] }; - const result = convertCollabsResponse(emptyCollaborations, mockOwnerId, ownerFromApi, mockAvatarUrlMap); + const result = convertCollabsResponse(emptyCollaborations, mockCurrentUser, ownerFromApi, mockAvatarUrlMap); expect(result).toEqual([]); }); test('should handle null avatar URL map', () => { - const collabs = convertCollabsResponse(mockCollaborationsFromApi, mockOwnerId, ownerFromApi, null); + const collabs = convertCollabsResponse(mockCollaborationsFromApi, mockCurrentUser, ownerFromApi, null); collabs.map(collab => { expect(collab.avatarUrl).toBeUndefined(); diff --git a/src/elements/content-sharing/utils/convertCollaborators.ts b/src/elements/content-sharing/utils/convertCollaborators.ts index f9dbcc27b6..8e061dc8c0 100644 --- a/src/elements/content-sharing/utils/convertCollaborators.ts +++ b/src/elements/content-sharing/utils/convertCollaborators.ts @@ -9,12 +9,11 @@ import { } from '../constants'; import type { Collaboration, Collaborations } from '../../../common/types/core'; -import type { AvatarURLMap } from '../types'; +import type { AvatarURLMap, ContentSharingUser } from '../types'; export interface ConvertCollabProps { collab: Collaboration; - currentUserId: string; - isCurrentUserOwner: boolean; + currentUser: ContentSharingUser; ownerEmailDomain: string; avatarUrlMap?: AvatarURLMap; } @@ -22,8 +21,7 @@ export interface ConvertCollabProps { export const convertCollab = ({ avatarUrlMap, collab, - currentUserId, - isCurrentUserOwner, + currentUser, ownerEmailDomain, }: ConvertCollabProps): Collaborator | null => { if (!collab || collab.status === STATUS_REJECTED) { @@ -48,8 +46,13 @@ export const convertCollab = ({ return null; } + // The external badge on the collaborator avatar is only visible if the current user is + // in the same enterprise as the item owner and the collaborator is not in the same enterprise. const isExternal = - !isCurrentUserOwner && !!collabEmail && !!ownerEmailDomain && collabEmail.split('@')[1] !== ownerEmailDomain; + currentUser?.emailDomain === ownerEmailDomain && + !!collabEmail && + !!ownerEmailDomain && + collabEmail.split('@')[1] !== ownerEmailDomain; const avatarUrl = avatarUrlMap ? avatarUrlMap[collabId] : undefined; return { @@ -58,7 +61,7 @@ export const convertCollab = ({ expiresAt, hasCustomAvatar: !!avatarUrl, id: `${id}`, - isCurrentUser: collabId != null && collabId === currentUserId, + isCurrentUser: collabId != null && collabId === currentUser?.id, isExternal, isPending: status === STATUS_PENDING, name: collabName || collabEmail, @@ -69,7 +72,7 @@ export const convertCollab = ({ export const convertCollabsResponse = ( collabsApiData: Collaborations, - currentUserId: string, + currentUser: ContentSharingUser, owner: { id: string; email: string; name: string }, avatarUrlMap?: AvatarURLMap, ): Collaborator[] => { @@ -77,7 +80,6 @@ export const convertCollabsResponse = ( if (!entries.length) return []; const { id: ownerId, email: ownerEmail, name: ownerName } = owner; - const isCurrentUserOwner = currentUserId === ownerId; const ownerEmailDomain = ownerEmail && /@/.test(ownerEmail) ? ownerEmail.split('@')[1] : null; const itemOwner = { @@ -92,7 +94,7 @@ export const convertCollabsResponse = ( }; return [itemOwner, ...entries].flatMap(collab => { - const converted = convertCollab({ avatarUrlMap, collab, currentUserId, isCurrentUserOwner, ownerEmailDomain }); + const converted = convertCollab({ avatarUrlMap, collab, currentUser, ownerEmailDomain }); return converted ? [converted] : []; }); };