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

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions src/constants.js
Original file line number Diff line number Diff line change
Expand Up @@ -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.';
Expand Down
14 changes: 9 additions & 5 deletions src/elements/content-sharing/ContentSharingV2.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -68,7 +72,7 @@ function ContentSharingV2({
const [hasError, setHasError] = React.useState<boolean>(false);
const [sharedLink, setSharedLink] = React.useState<SharedLink | null>(null);
const [sharingServiceProps, setSharingServiceProps] = React.useState(null);
const [currentUser, setCurrentUser] = React.useState<User | null>(null);
const [currentUser, setCurrentUser] = React.useState<ContentSharingUser | null>(null);
const [collaborationRoles, setCollaborationRoles] = React.useState<CollaborationRole[] | null>(null);
const [collaborators, setCollaborators] = React.useState<Collaborator[] | null>(null);
const [collaboratorsData, setCollaboratorsData] = React.useState<Collaborations | null>(null);
Expand All @@ -87,7 +91,7 @@ function ContentSharingV2({
api,
avatarUrlMap,
collaborators,
currentUserId: currentUser?.id,
currentUser,
item,
itemId,
itemType,
Expand Down Expand Up @@ -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/` : '',
Expand Down Expand Up @@ -266,7 +270,7 @@ function ContentSharingV2({
if (avatarUrlMap && collaboratorsData && currentUser && owner) {
const collaboratorsWithAvatars = convertCollabsResponse(
collaboratorsData,
currentUser.id,
currentUser,
owner,
avatarUrlMap,
);
Expand Down
Original file line number Diff line number Diff line change
@@ -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';

Expand All @@ -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);
Expand Down
4 changes: 2 additions & 2 deletions src/elements/content-sharing/apis/fetchCurrentUser.ts
Original file line number Diff line number Diff line change
@@ -1,14 +1,14 @@
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';

export const fetchCurrentUser = async ({ api, itemId }: BaseFetchProps): Promise<User | null> => {
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(),
},
});
});
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -70,7 +70,7 @@ const renderHookWithProps = (props = {}) => {
api: mockApi,
avatarUrlMap: {},
collaborators: [],
currentUserId: '123',
currentUser: { id: '123' },
item: mockItem,
itemId: mockItemId,
itemType: TYPE_FILE,
Expand Down Expand Up @@ -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, {
Expand All @@ -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
Expand All @@ -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,
Expand Down
8 changes: 4 additions & 4 deletions src/elements/content-sharing/hooks/useSharingService.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ export const useSharingService = ({
api,
avatarUrlMap,
collaborators,
currentUserId,
currentUser,
item,
itemId,
itemType,
Expand Down Expand Up @@ -92,15 +92,15 @@ export const useSharingService = ({
const nextCollab = convertCollab({
avatarUrlMap,
collab: response,
currentUserId,
isCurrentUserOwner: currentUserId === ownerId,
currentUser,
isCurrentUserOwner: currentUser.id === ownerId,
ownerEmailDomain,
Comment on lines +95 to 97
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Guard currentUser before dereferencing in invite success path.

Line 96 can throw when currentUser is still null (caller initializes it as null). Add a null-safe guard before using currentUser.id and appending collaborators.

Proposed fix
         setCollaborators(prevCollabs => {
+            if (!currentUser) {
+                return prevCollabs;
+            }
+
             const nextCollab = convertCollab({
                 avatarUrlMap,
                 collab: response,
                 currentUser,
                 isCurrentUserOwner: currentUser.id === ownerId,
                 ownerEmailDomain,
             });

-            return nextCollab ? [...prevCollabs, nextCollab] : prevCollabs;
+            return nextCollab ? [...(prevCollabs ?? []), nextCollab] : prevCollabs;
         });
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
currentUser,
isCurrentUserOwner: currentUser.id === ownerId,
ownerEmailDomain,
setCollaborators(prevCollabs => {
if (!currentUser) {
return prevCollabs;
}
const nextCollab = convertCollab({
avatarUrlMap,
collab: response,
currentUser,
isCurrentUserOwner: currentUser.id === ownerId,
ownerEmailDomain,
});
return nextCollab ? [...(prevCollabs ?? []), nextCollab] : prevCollabs;
});
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/elements/content-sharing/hooks/useSharingService.ts` around lines 95 -
97, The invite-success path dereferences currentUser (currentUser.id) without a
null check; update the logic where the object with currentUser,
isCurrentUserOwner, and ownerEmailDomain is built (the block referencing
currentUser and ownerId) to guard currentUser first — compute isCurrentUserOwner
only if currentUser exists (e.g., currentUser && currentUser.id === ownerId or
optional chaining), and only append to collaborators or access any currentUser
properties when currentUser is non-null; ensure the invite callback in
useSharingService.ts skips collaborator mutations or uses a safe fallback when
currentUser is null.

});

return nextCollab ? [...prevCollabs, nextCollab] : prevCollabs;
});
},
[avatarUrlMap, currentUserId, setCollaborators],
[avatarUrlMap, currentUser, setCollaborators],
);

const handleSendInvitations = useInvites(api, itemId, itemType, {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -94,7 +99,7 @@ describe('convertCollaborators', () => {
const result = convertCollab({
avatarUrlMap: mockAvatarUrlMap,
collab: mockCollaborations[1],
currentUserId: mockOwnerId,
currentUser: mockCurrentUser,
isCurrentUserOwner: false,
ownerEmailDomain,
});
Expand All @@ -118,8 +123,8 @@ describe('convertCollaborators', () => {
const result = convertCollab({
avatarUrlMap: mockAvatarUrlMap,
collab: mockCollaborations[5],
currentUserId: mockOwnerId,
isCurrentUserOwner: false,
currentUser: mockCurrentUser,
isCurrentUserOwner: true,
ownerEmailDomain,
});

Expand All @@ -141,7 +146,7 @@ describe('convertCollaborators', () => {
const result = convertCollab({
avatarUrlMap: mockAvatarUrlMap,
collab: mockCollaborations[3],
currentUserId: mockOwnerId,
currentUser: mockCurrentUser,
isCurrentUserOwner: false,
ownerEmailDomain,
});
Expand All @@ -165,7 +170,7 @@ describe('convertCollaborators', () => {
const result = convertCollab({
avatarUrlMap: mockAvatarUrlMap,
collab,
currentUserId: mockOwnerId,
currentUser: mockCurrentUser,
isCurrentUserOwner: false,
ownerEmailDomain,
});
Expand All @@ -177,7 +182,7 @@ describe('convertCollaborators', () => {
const result = convertCollab({
avatarUrlMap: mockAvatarUrlMap,
collab: mockCollaborations[4],
currentUserId: mockOwnerId,
currentUser: mockCurrentUser,
isCurrentUserOwner: false,
ownerEmailDomain,
});
Expand All @@ -189,7 +194,7 @@ describe('convertCollaborators', () => {
const result = convertCollab({
avatarUrlMap: mockAvatarUrlMap,
collab: mockCollaborations[6],
currentUserId: mockOwnerId,
currentUser: mockCurrentUser,
isCurrentUserOwner: false,
ownerEmailDomain,
});
Expand All @@ -201,7 +206,7 @@ describe('convertCollaborators', () => {
const result = convertCollab({
avatarUrlMap: mockAvatarUrlMap,
collab: mockCollaborations[0],
currentUserId: mockOwnerId,
currentUser: mockCurrentUser,
isCurrentUserOwner: true,
ownerEmailDomain,
});
Expand All @@ -224,8 +229,8 @@ describe('convertCollaborators', () => {
const result = convertCollab({
avatarUrlMap: mockAvatarUrlMap,
collab: mockCollaborations[2],
currentUserId: mockOwnerId,
isCurrentUserOwner: false,
currentUser: mockCurrentUser,
isCurrentUserOwner: true,
ownerEmailDomain,
});

Expand All @@ -238,7 +243,7 @@ describe('convertCollaborators', () => {
const result = convertCollab({
avatarUrlMap,
collab: mockCollaborations[1],
currentUserId: mockOwnerId,
currentUser: mockCurrentUser,
isCurrentUserOwner: false,
ownerEmailDomain,
});
Expand All @@ -257,7 +262,7 @@ describe('convertCollaborators', () => {
const result = convertCollab({
avatarUrlMap: mockAvatarUrlMap,
collab: collabWithoutExpiration,
currentUserId: mockOwnerId,
currentUser: mockCurrentUser,
isCurrentUserOwner: false,
ownerEmailDomain,
});
Expand All @@ -270,7 +275,7 @@ describe('convertCollaborators', () => {
test('should convert valid collaborations data to Collaborator array', () => {
const result = convertCollabsResponse(
mockCollaborationsFromApi,
mockOwnerId,
mockCurrentUser,
ownerFromApi,
mockAvatarUrlMap,
);
Expand Down Expand Up @@ -310,7 +315,7 @@ describe('convertCollaborators', () => {
hasCustomAvatar: false,
id: '124',
isCurrentUser: false,
isExternal: false,
isExternal: true,
isPending: false,
name: 'Raccoon Queen',
role: 'viewer',
Expand All @@ -336,7 +341,7 @@ describe('convertCollaborators', () => {
hasCustomAvatar: false,
id: '127',
isCurrentUser: false,
isExternal: false,
isExternal: true,
isPending: true,
name: 'bbear@external.example.com',
role: 'editor',
Expand All @@ -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();
Expand Down
23 changes: 15 additions & 8 deletions src/elements/content-sharing/utils/convertCollaborators.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -22,7 +22,7 @@ export interface ConvertCollabProps {
export const convertCollab = ({
avatarUrlMap,
collab,
currentUserId,
currentUser,
isCurrentUserOwner,
ownerEmailDomain,
}: ConvertCollabProps): Collaborator | null => {
Expand All @@ -48,8 +48,15 @@ export const convertCollab = ({
return null;
}

// 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 {
Expand All @@ -58,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,
Expand All @@ -69,15 +76,15 @@ export const convertCollab = ({

export const convertCollabsResponse = (
collabsApiData: Collaborations,
currentUserId: string,
currentUser: User,
owner: { id: string; email: string; name: string },
avatarUrlMap?: AvatarURLMap,
): Collaborator[] => {
const { entries = [] } = collabsApiData;
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 = {
Expand All @@ -92,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] : [];
});
};
Expand Down
Loading