From 167833d83c2533c8cc57b6e8f37c96e1602cf287 Mon Sep 17 00:00:00 2001 From: reneshen0328 Date: Fri, 22 May 2026 16:10:06 -0700 Subject: [PATCH 1/3] fix(content-sharing): render external badge logic --- src/elements/content-sharing/utils/convertCollaborators.ts | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/elements/content-sharing/utils/convertCollaborators.ts b/src/elements/content-sharing/utils/convertCollaborators.ts index f9dbcc27b6..51ab0f2692 100644 --- a/src/elements/content-sharing/utils/convertCollaborators.ts +++ b/src/elements/content-sharing/utils/convertCollaborators.ts @@ -48,8 +48,10 @@ export const convertCollab = ({ return null; } + // External collaborator icons will only be displayed in the USM if the current user owns + // the item and if the collaborator's email domain differs from the owner's email domain. const isExternal = - !isCurrentUserOwner && !!collabEmail && !!ownerEmailDomain && collabEmail.split('@')[1] !== ownerEmailDomain; + isCurrentUserOwner && !!collabEmail && !!ownerEmailDomain && collabEmail.split('@')[1] !== ownerEmailDomain; const avatarUrl = avatarUrlMap ? avatarUrlMap[collabId] : undefined; return { From ade9bdf8cbc0c855499cd930179f42bf82eecf44 Mon Sep 17 00:00:00 2001 From: reneshen0328 Date: Fri, 22 May 2026 16:17:32 -0700 Subject: [PATCH 2/3] fix: update test --- .../utils/__tests__/convertCollaborators.test.ts | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/elements/content-sharing/utils/__tests__/convertCollaborators.test.ts b/src/elements/content-sharing/utils/__tests__/convertCollaborators.test.ts index fccb6ec13f..553b8654f8 100644 --- a/src/elements/content-sharing/utils/__tests__/convertCollaborators.test.ts +++ b/src/elements/content-sharing/utils/__tests__/convertCollaborators.test.ts @@ -119,7 +119,7 @@ describe('convertCollaborators', () => { avatarUrlMap: mockAvatarUrlMap, collab: mockCollaborations[5], currentUserId: mockOwnerId, - isCurrentUserOwner: false, + isCurrentUserOwner: true, ownerEmailDomain, }); @@ -225,7 +225,7 @@ describe('convertCollaborators', () => { avatarUrlMap: mockAvatarUrlMap, collab: mockCollaborations[2], currentUserId: mockOwnerId, - isCurrentUserOwner: false, + isCurrentUserOwner: true, ownerEmailDomain, }); @@ -310,7 +310,7 @@ describe('convertCollaborators', () => { hasCustomAvatar: false, id: '124', isCurrentUser: false, - isExternal: false, + isExternal: true, isPending: false, name: 'Raccoon Queen', role: 'viewer', @@ -336,7 +336,7 @@ describe('convertCollaborators', () => { hasCustomAvatar: false, id: '127', isCurrentUser: false, - isExternal: false, + isExternal: true, isPending: true, name: 'bbear@external.example.com', role: 'editor', From 03ac124cf99baf81a18f63416d249ef95fd5d9a4 Mon Sep 17 00:00:00 2001 From: reneshen0328 Date: Fri, 22 May 2026 17:49:34 -0700 Subject: [PATCH 3/3] fix: update external badge render logic to match webapp --- src/constants.js | 1 + .../content-sharing/ContentSharingV2.tsx | 14 ++++++--- .../apis/__tests__/fetchCurrentUser.test.ts | 4 +-- .../content-sharing/apis/fetchCurrentUser.ts | 4 +-- .../hooks/__tests__/useSharingService.test.ts | 10 +++--- .../hooks/useSharingService.ts | 8 ++--- .../__tests__/convertCollaborators.test.ts | 31 +++++++++++-------- .../utils/convertCollaborators.ts | 25 +++++++++------ 8 files changed, 56 insertions(+), 41 deletions(-) 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..df5f6ded36 100644 --- a/src/elements/content-sharing/ContentSharingV2.tsx +++ b/src/elements/content-sharing/ContentSharingV2.tsx @@ -28,6 +28,10 @@ import type { AvatarURLMap } from './types'; import messages from './messages'; +interface ContentSharingUser extends User { + email?: string; +} + export interface ContentSharingV2Props { /** api - API instance */ api: API; @@ -68,7 +72,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 +91,7 @@ function ContentSharingV2({ api, avatarUrlMap, collaborators, - currentUserId: currentUser?.id, + currentUser, item, itemId, itemType, @@ -187,8 +191,8 @@ function ContentSharingV2({ return; } - const { enterprise, hostname, id } = userData; - setCurrentUser({ id }); + const { enterprise, hostname, id, login } = userData; + setCurrentUser({ id, email: login }); setSharingServiceProps(prevSharingServiceProps => ({ ...prevSharingServiceProps, serverUrl: hostname ? `${hostname}v/` : '', @@ -266,7 +270,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..ae6c953623 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,7 +313,7 @@ describe('elements/content-sharing/hooks/useSharingService', () => { expect(convertCollab).toHaveBeenCalledWith({ collab: mockResponse, - currentUserId: mockCurrentUserId, + currentUser: mockCurrentUser, isCurrentUserOwner: false, 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..c8d97f58b0 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, @@ -92,15 +92,15 @@ export const useSharingService = ({ const nextCollab = convertCollab({ avatarUrlMap, collab: response, - currentUserId, - isCurrentUserOwner: currentUserId === ownerId, + currentUser, + isCurrentUserOwner: currentUser.id === ownerId, 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/utils/__tests__/convertCollaborators.test.ts b/src/elements/content-sharing/utils/__tests__/convertCollaborators.test.ts index 553b8654f8..a3d4697c87 100644 --- a/src/elements/content-sharing/utils/__tests__/convertCollaborators.test.ts +++ b/src/elements/content-sharing/utils/__tests__/convertCollaborators.test.ts @@ -18,6 +18,11 @@ const ownerFromApi = { email: mockOwnerEmail, name: mockOwnerName, }; +const mockCurrentUser = { + id: mockOwnerId, + email: mockOwnerEmail, + name: mockOwnerName, +}; const itemOwner = { id: mockOwnerEmail, status: STATUS_ACCEPTED, @@ -94,7 +99,7 @@ describe('convertCollaborators', () => { const result = convertCollab({ avatarUrlMap: mockAvatarUrlMap, collab: mockCollaborations[1], - currentUserId: mockOwnerId, + currentUser: mockCurrentUser, isCurrentUserOwner: false, ownerEmailDomain, }); @@ -118,7 +123,7 @@ describe('convertCollaborators', () => { const result = convertCollab({ avatarUrlMap: mockAvatarUrlMap, collab: mockCollaborations[5], - currentUserId: mockOwnerId, + currentUser: mockCurrentUser, isCurrentUserOwner: true, ownerEmailDomain, }); @@ -141,7 +146,7 @@ describe('convertCollaborators', () => { const result = convertCollab({ avatarUrlMap: mockAvatarUrlMap, collab: mockCollaborations[3], - currentUserId: mockOwnerId, + currentUser: mockCurrentUser, isCurrentUserOwner: false, ownerEmailDomain, }); @@ -165,7 +170,7 @@ describe('convertCollaborators', () => { const result = convertCollab({ avatarUrlMap: mockAvatarUrlMap, collab, - currentUserId: mockOwnerId, + currentUser: mockCurrentUser, isCurrentUserOwner: false, ownerEmailDomain, }); @@ -177,7 +182,7 @@ describe('convertCollaborators', () => { const result = convertCollab({ avatarUrlMap: mockAvatarUrlMap, collab: mockCollaborations[4], - currentUserId: mockOwnerId, + currentUser: mockCurrentUser, isCurrentUserOwner: false, ownerEmailDomain, }); @@ -189,7 +194,7 @@ describe('convertCollaborators', () => { const result = convertCollab({ avatarUrlMap: mockAvatarUrlMap, collab: mockCollaborations[6], - currentUserId: mockOwnerId, + currentUser: mockCurrentUser, isCurrentUserOwner: false, ownerEmailDomain, }); @@ -201,7 +206,7 @@ describe('convertCollaborators', () => { const result = convertCollab({ avatarUrlMap: mockAvatarUrlMap, collab: mockCollaborations[0], - currentUserId: mockOwnerId, + currentUser: mockCurrentUser, isCurrentUserOwner: true, ownerEmailDomain, }); @@ -224,7 +229,7 @@ describe('convertCollaborators', () => { const result = convertCollab({ avatarUrlMap: mockAvatarUrlMap, collab: mockCollaborations[2], - currentUserId: mockOwnerId, + currentUser: mockCurrentUser, isCurrentUserOwner: true, ownerEmailDomain, }); @@ -238,7 +243,7 @@ describe('convertCollaborators', () => { const result = convertCollab({ avatarUrlMap, collab: mockCollaborations[1], - currentUserId: mockOwnerId, + currentUser: mockCurrentUser, isCurrentUserOwner: false, ownerEmailDomain, }); @@ -257,7 +262,7 @@ describe('convertCollaborators', () => { const result = convertCollab({ avatarUrlMap: mockAvatarUrlMap, collab: collabWithoutExpiration, - currentUserId: mockOwnerId, + currentUser: mockCurrentUser, isCurrentUserOwner: false, ownerEmailDomain, }); @@ -270,7 +275,7 @@ describe('convertCollaborators', () => { test('should convert valid collaborations data to Collaborator array', () => { const result = convertCollabsResponse( mockCollaborationsFromApi, - mockOwnerId, + mockCurrentUser, ownerFromApi, mockAvatarUrlMap, ); @@ -346,13 +351,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 51ab0f2692..6194a91764 100644 --- a/src/elements/content-sharing/utils/convertCollaborators.ts +++ b/src/elements/content-sharing/utils/convertCollaborators.ts @@ -8,12 +8,12 @@ import { USM_TO_API_COLLAB_ROLE_MAP, } from '../constants'; -import type { Collaboration, Collaborations } from '../../../common/types/core'; +import type { Collaboration, Collaborations, User } from '../../../common/types/core'; import type { AvatarURLMap } from '../types'; export interface ConvertCollabProps { collab: Collaboration; - currentUserId: string; + currentUser: User; isCurrentUserOwner: boolean; ownerEmailDomain: string; avatarUrlMap?: AvatarURLMap; @@ -22,7 +22,7 @@ export interface ConvertCollabProps { export const convertCollab = ({ avatarUrlMap, collab, - currentUserId, + currentUser, isCurrentUserOwner, ownerEmailDomain, }: ConvertCollabProps): Collaborator | null => { @@ -48,10 +48,15 @@ export const convertCollab = ({ return null; } - // External collaborator icons will only be displayed in the USM if the current user owns - // the item and if the collaborator's email domain differs from the owner's email domain. + // External collaborator icons will only be displayed in the USM if the current user is the item owner or + // belongs to the same enterprise as the owner, and if the collaborator's email domain differs from the owner's enterprise email domain. + const currentUserEmailDomain = + !!currentUser?.email && /@/.test(currentUser.email) ? currentUser.email.split('@')[1] : null; const isExternal = - isCurrentUserOwner && !!collabEmail && !!ownerEmailDomain && collabEmail.split('@')[1] !== ownerEmailDomain; + (isCurrentUserOwner || currentUserEmailDomain === ownerEmailDomain) && + !!collabEmail && + !!ownerEmailDomain && + collabEmail.split('@')[1] !== ownerEmailDomain; const avatarUrl = avatarUrlMap ? avatarUrlMap[collabId] : undefined; return { @@ -60,7 +65,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, @@ -71,7 +76,7 @@ export const convertCollab = ({ export const convertCollabsResponse = ( collabsApiData: Collaborations, - currentUserId: string, + currentUser: User, owner: { id: string; email: string; name: string }, avatarUrlMap?: AvatarURLMap, ): Collaborator[] => { @@ -79,7 +84,7 @@ export const convertCollabsResponse = ( if (!entries.length) return []; const { id: ownerId, email: ownerEmail, name: ownerName } = owner; - const isCurrentUserOwner = currentUserId === ownerId; + const isCurrentUserOwner = currentUser.id === ownerId; const ownerEmailDomain = ownerEmail && /@/.test(ownerEmail) ? ownerEmail.split('@')[1] : null; const itemOwner = { @@ -94,7 +99,7 @@ export const convertCollabsResponse = ( }; return [itemOwner, ...entries].flatMap(collab => { - const converted = convertCollab({ avatarUrlMap, collab, currentUserId, isCurrentUserOwner, ownerEmailDomain }); + const converted = convertCollab({ avatarUrlMap, collab, currentUser, isCurrentUserOwner, ownerEmailDomain }); return converted ? [converted] : []; }); };