From f42b4edb97f51764c50348a587e2ef83faf9ec64 Mon Sep 17 00:00:00 2001 From: reneshen0328 Date: Thu, 23 Oct 2025 17:26:13 -0700 Subject: [PATCH 1/5] feat(content-sharing): Handle error when fetch init data failed --- i18n/en-US.properties | 4 + .../content-sharing/ContentSharing.js | 19 ++- .../content-sharing/ContentSharingV2.tsx | 148 +++++++++++------- .../__tests__/ContentSharingV2.test.tsx | 79 ++++++++-- .../apis/__tests__/fetchAvatars.test.ts | 18 +-- .../content-sharing/apis/fetchAvatars.ts | 3 +- src/elements/content-sharing/messages.js | 10 ++ .../stories/ContentSharing.stories.js | 1 + 8 files changed, 189 insertions(+), 93 deletions(-) diff --git a/i18n/en-US.properties b/i18n/en-US.properties index a4b9ee8fed..c806e24cce 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. +# Icon label for the error notifications +be.contentSharing.errorNoticeIcon = Error # 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 @@ -148,6 +150,8 @@ be.contentSharing.loadingError = Could not load shared link for this item. be.contentSharing.noAccessError = You do not have access to this item. # Message that appears when the item for the ContentSharing Element cannot be found. be.contentSharing.notFoundError = Could not find shared link for this item. +# Close button aria label for the notifications +be.contentSharing.noticeCloseLabel = Close # Message that appears when collaborators cannot be added to the shared link in the ContentSharing Element. be.contentSharing.sendInvitationsError = {count, plural, one {Failed to invite a collaborator.} other {Failed to invite {count} collaborators.}} # Message that appears when collaborators were added to the shared link in the ContentSharing Element. diff --git a/src/elements/content-sharing/ContentSharing.js b/src/elements/content-sharing/ContentSharing.js index 9999adf84d..5c425753b8 100644 --- a/src/elements/content-sharing/ContentSharing.js +++ b/src/elements/content-sharing/ContentSharing.js @@ -12,6 +12,8 @@ import API from '../../api'; // $FlowFixMe import { withBlueprintModernization } from '../common/withBlueprintModernization'; import { isFeatureEnabled } from '../common/feature-checking'; +import Internationalize from '../common/Internationalize'; +import Providers from '../common/Providers'; import SharingModal from './SharingModal'; // $FlowFixMe import ContentSharingV2 from './ContentSharingV2'; @@ -117,16 +119,13 @@ function ContentSharing({ if (isFeatureEnabled(features, 'contentSharingV2')) { return ( api && ( - - {children} - + + + + {children} + + + ) ); } diff --git a/src/elements/content-sharing/ContentSharingV2.tsx b/src/elements/content-sharing/ContentSharingV2.tsx index 8f5dc7eef4..08084ea228 100644 --- a/src/elements/content-sharing/ContentSharingV2.tsx +++ b/src/elements/content-sharing/ContentSharingV2.tsx @@ -1,20 +1,23 @@ import * as React from 'react'; +import { useIntl } from 'react-intl'; import isEmpty from 'lodash/isEmpty'; - +import { useNotification } from '@box/blueprint-web'; import { UnifiedShareModal } from '@box/unified-share-modal'; import type { CollaborationRole, Collaborator, Item, SharedLink, User } from '@box/unified-share-modal'; import API from '../../api'; -import Internationalize from '../common/Internationalize'; -import Providers from '../common/Providers'; import { withBlueprintModernization } from '../common/withBlueprintModernization'; import { fetchAvatars, fetchCollaborators, fetchCurrentUser, fetchItem } from './apis'; +import { CONTENT_SHARING_ERRORS } from './constants'; import { useContactService, useSharingService } from './hooks'; import { convertCollabsResponse, convertItemResponse } from './utils'; -import type { Collaborations, ItemType, StringMap } from '../../common/types/core'; +import type { Collaborations, ItemType } from '../../common/types/core'; +import type { ElementsXhrError } from '../../common/types/api'; import type { AvatarURLMap } from './types'; +import messages from './messages'; + export interface ContentSharingV2Props { /** api - API instance */ api: API; @@ -24,25 +27,12 @@ export interface ContentSharingV2Props { itemId: string; /** itemType - "file" or "folder" */ itemType: ItemType; - /** hasProviders - Whether the element has providers for USM already */ - hasProviders?: boolean; - /** language - Language used for the element */ - language?: string; - /** messages - Localized strings used by the element */ - messages?: StringMap; } -function ContentSharingV2({ - api, - children, - itemId, - itemType, - hasProviders, - language, - messages, -}: ContentSharingV2Props) { +function ContentSharingV2({ api, children, itemId, itemType }: ContentSharingV2Props) { const [avatarUrlMap, setAvatarUrlMap] = React.useState(null); const [item, setItem] = React.useState(null); + const [errorMessage, setErrorMessage] = React.useState(false); const [sharedLink, setSharedLink] = React.useState(null); const [sharingServiceProps, setSharingServiceProps] = React.useState(null); const [currentUser, setCurrentUser] = React.useState(null); @@ -51,6 +41,8 @@ function ContentSharingV2({ const [collaboratorsData, setCollaboratorsData] = React.useState(null); const [owner, setOwner] = React.useState({ id: '', email: '', name: '' }); + const { formatMessage } = useIntl(); + const { addNotification } = useNotification(); const { sharingService } = useSharingService({ api, avatarUrlMap, @@ -84,6 +76,35 @@ function ContentSharingV2({ setOwner({ id: ownedBy.id, email: ownedBy.login, name: ownedBy.name }); }, []); + // Handle initial data retrieval errors + const getError = React.useCallback( + (error: ElementsXhrError) => { + // display only one component-level notification at a time + if (errorMessage) { + return; + } + + let errorObject; + if (error.status) { + errorObject = messages[CONTENT_SHARING_ERRORS[error.status]]; + } else if (error.response && error.response.status) { + errorObject = messages[CONTENT_SHARING_ERRORS[error.response.status]]; + } else { + errorObject = messages.loadingError; + } + + setErrorMessage(errorObject.defaultMessage); + addNotification({ + closeButtonAriaLabel: formatMessage(messages.noticeCloseLabel), + sensitivity: 'foreground' as const, + typeIconAriaLabel: formatMessage(messages.errorNoticeIcon), + variant: 'error', + styledText: errorObject.defaultMessage, + }); + }, + [errorMessage, addNotification, formatMessage], + ); + // Reset state if the API has changed React.useEffect(() => { setItem(null); @@ -100,10 +121,14 @@ function ContentSharingV2({ if (!api || isEmpty(api) || item) return; (async () => { - const itemData = await fetchItem({ api, itemId, itemType }); - handleGetItemSuccess(itemData); + try { + const itemData = await fetchItem({ api, itemId, itemType }); + handleGetItemSuccess(itemData); + } catch (error) { + getError(error); + } })(); - }, [api, item, itemId, itemType, sharedLink, handleGetItemSuccess]); + }, [api, item, itemId, itemType, sharedLink, handleGetItemSuccess, getError]); // Get current user React.useEffect(() => { @@ -122,10 +147,14 @@ function ContentSharingV2({ }; (async () => { - const userData = await fetchCurrentUser({ api, itemId }); - getUserSuccess(userData); + try { + const userData = await fetchCurrentUser({ api, itemId }); + getUserSuccess(userData); + } catch (error) { + getError(error); + } })(); - }, [api, currentUser, item, itemId, itemType, sharedLink]); + }, [api, currentUser, item, itemId, itemType, sharedLink, getError]); // Get collaborators React.useEffect(() => { @@ -135,31 +164,36 @@ function ContentSharingV2({ try { const response = await fetchCollaborators({ api, itemId, itemType }); setCollaboratorsData(response); - } catch { + } catch (error) { setCollaboratorsData({ entries: [], next_marker: null }); + getError(error); } })(); - }, [api, collaboratorsData, item, itemId, itemType]); + }, [api, collaboratorsData, item, itemId, itemType, getError]); // Get avatars when collaborators are available React.useEffect(() => { if (avatarUrlMap || !collaboratorsData || !collaboratorsData.entries || !owner.id) return; (async () => { - const ownerEntry = { - accessible_by: { - id: owner.id, - login: owner.email, - name: owner.name, - }, - }; - const response = await fetchAvatars({ - api, - itemId, - collaborators: [...collaboratorsData.entries, ownerEntry], - }); - setAvatarUrlMap(response); + try { + const ownerEntry = { + accessible_by: { + id: owner.id, + login: owner.email, + name: owner.name, + }, + }; + const response = await fetchAvatars({ + api, + itemId, + collaborators: [...collaboratorsData.entries, ownerEntry], + }); + setAvatarUrlMap(response); + } catch (error) { + getError(error); + } })(); - }, [api, avatarUrlMap, collaboratorsData, itemId, owner]); + }, [api, avatarUrlMap, collaboratorsData, itemId, owner, getError]); React.useEffect(() => { if (avatarUrlMap && collaboratorsData && currentUser && owner) { @@ -176,24 +210,20 @@ function ContentSharingV2({ const config = { sharedLinkEmail: false }; return ( - - - {item && ( - - {children} - - )} - - + item && ( + + {children} + + ) ); } diff --git a/src/elements/content-sharing/__tests__/ContentSharingV2.test.tsx b/src/elements/content-sharing/__tests__/ContentSharingV2.test.tsx index 3b267bb6bd..ec573b4861 100644 --- a/src/elements/content-sharing/__tests__/ContentSharingV2.test.tsx +++ b/src/elements/content-sharing/__tests__/ContentSharingV2.test.tsx @@ -1,6 +1,6 @@ import React from 'react'; import { render, type RenderResult, screen, waitFor } from '@testing-library/react'; - +import { Notification, TooltipProvider } from '@box/blueprint-web'; import { useSharingService } from '../hooks/useSharingService'; import { DEFAULT_ITEM_API_RESPONSE, @@ -12,7 +12,6 @@ import { mockAvatarURLMap, } from '../utils/__mocks__/ContentSharingV2Mocks'; import { CONTENT_SHARING_ITEM_FIELDS } from '../constants'; - import ContentSharingV2 from '../ContentSharingV2'; const createApiMock = (fileApi, folderApi, usersApi, collaborationsApi) => ({ @@ -47,19 +46,22 @@ const defaultApiMock = createApiMock( { getCollaborations: getCollaborationsMock }, ); +const mockAddNotification = jest.fn(); +jest.mock('@box/blueprint-web', () => ({ + ...jest.requireActual('@box/blueprint-web'), + useNotification: jest.fn(() => ({ addNotification: mockAddNotification })), +})); jest.mock('../hooks/useSharingService', () => ({ useSharingService: jest.fn().mockReturnValue({ sharingService: null }), })); const renderComponent = (props = {}): RenderResult => render( - , + + + + + , ); describe('elements/content-sharing/ContentSharingV2', () => { @@ -73,9 +75,6 @@ describe('elements/content-sharing/ContentSharingV2', () => { expect(getDefaultFileMock).toHaveBeenCalledWith(MOCK_ITEM.id, expect.any(Function), expect.any(Function), { fields: CONTENT_SHARING_ITEM_FIELDS, }); - expect(screen.getByRole('heading', { name: /Box Development Guide.pdf/i })).toBeVisible(); - expect(screen.getByRole('combobox', { name: 'Invite People' })).toBeVisible(); - expect(screen.getByRole('switch', { name: 'Shared link' })).toBeVisible(); }); }); @@ -170,4 +169,60 @@ describe('elements/content-sharing/ContentSharingV2', () => { expect(screen.getByRole('heading', { name: /Box Development Guide.pdf/i })).toBeVisible(); }); }); + + describe('getError function', () => { + const createErrorApi = error => ({ + ...defaultApiMock, + getFileAPI: jest.fn().mockReturnValue({ + getFile: jest.fn().mockImplementation((id, successFn, errorFn) => { + errorFn(error); + }), + }), + }); + + test('should render bad request message for error.status 400', async () => { + const error = { status: 400 }; + renderComponent({ api: createErrorApi(error) }); + + await waitFor(() => { + expect(mockAddNotification).toHaveBeenCalledWith({ + closeButtonAriaLabel: 'Close', + sensitivity: 'foreground', + styledText: 'The request for this item was malformed.', + typeIconAriaLabel: 'Error', + variant: 'error', + }); + }); + }); + + test('should render no access message for error.response.status 401', async () => { + const error = { response: { status: 401 } }; + renderComponent({ api: createErrorApi(error) }); + + await waitFor(() => { + expect(mockAddNotification).toHaveBeenCalledWith({ + closeButtonAriaLabel: 'Close', + sensitivity: 'foreground', + styledText: 'You do not have access to this item.', + typeIconAriaLabel: 'Error', + variant: 'error', + }); + }); + }); + + test('should render loading error message when no status is provided', async () => { + const error = { message: 'Network error' }; + renderComponent({ api: createErrorApi(error) }); + + await waitFor(() => { + expect(mockAddNotification).toHaveBeenCalledWith({ + closeButtonAriaLabel: 'Close', + sensitivity: 'foreground', + styledText: 'Could not load shared link for this item.', + typeIconAriaLabel: 'Error', + variant: 'error', + }); + }); + }); + }); }); diff --git a/src/elements/content-sharing/apis/__tests__/fetchAvatars.test.ts b/src/elements/content-sharing/apis/__tests__/fetchAvatars.test.ts index 732399cfda..78415f0b96 100644 --- a/src/elements/content-sharing/apis/__tests__/fetchAvatars.test.ts +++ b/src/elements/content-sharing/apis/__tests__/fetchAvatars.test.ts @@ -50,17 +50,13 @@ describe('content-sharing/apis/fetchAvatars', () => { .mockRejectedValueOnce(new Error('Avatar fetch failed')) .mockResolvedValueOnce('https://example.com/avatar3.jpg'); - const result = await fetchAvatars({ - api: defaultApiMock, - itemId: MOCK_ITEM.id, - collaborators: mockCollaborations, - }); - - expect(result).toEqual({ - 123: 'https://example.com/avatar1.jpg', - 456: null, - 789: 'https://example.com/avatar3.jpg', - }); + await expect( + fetchAvatars({ + api: defaultApiMock, + itemId: MOCK_ITEM.id, + collaborators: mockCollaborations, + }), + ).rejects.toThrow('Avatar fetch failed'); }); test('should handle collaborators without accessible_by', async () => { diff --git a/src/elements/content-sharing/apis/fetchAvatars.ts b/src/elements/content-sharing/apis/fetchAvatars.ts index 4bc50eee84..b89bd452ab 100644 --- a/src/elements/content-sharing/apis/fetchAvatars.ts +++ b/src/elements/content-sharing/apis/fetchAvatars.ts @@ -12,8 +12,9 @@ export const fetchAvatars = async ({ api, itemId, collaborators }: FetchCollabor try { const url = await usersApi.getAvatarUrlWithAccessToken(userId.toString(), itemId); avatarUrlMap[userId] = url; - } catch { + } catch (error) { avatarUrlMap[userId] = null; + throw error; } }); diff --git a/src/elements/content-sharing/messages.js b/src/elements/content-sharing/messages.js index 325043856a..7f89ffabfe 100644 --- a/src/elements/content-sharing/messages.js +++ b/src/elements/content-sharing/messages.js @@ -78,6 +78,16 @@ const messages = defineMessages({ description: 'Display text for a Group contact type', id: 'be.contentSharing.groupContactLabel', }, + noticeCloseLabel: { + defaultMessage: 'Close', + description: 'Close button aria label for the notifications', + id: 'be.contentSharing.noticeCloseLabel', + }, + errorNoticeIcon: { + defaultMessage: 'Error', + description: 'Icon label for the error notifications', + id: 'be.contentSharing.errorNoticeIcon', + }, }); export default messages; diff --git a/src/elements/content-sharing/stories/ContentSharing.stories.js b/src/elements/content-sharing/stories/ContentSharing.stories.js index c53757d03a..1ccba911af 100644 --- a/src/elements/content-sharing/stories/ContentSharing.stories.js +++ b/src/elements/content-sharing/stories/ContentSharing.stories.js @@ -20,6 +20,7 @@ export const withContentSharingV2Enabled = { ...global.FEATURE_FLAGS, contentSharingV2: true, }, + hasProviders: true, }, }; From 319fc60c6309fc73ca7e903b9def397fc6189c4f Mon Sep 17 00:00:00 2001 From: reneshen0328 Date: Thu, 23 Oct 2025 18:34:32 -0700 Subject: [PATCH 2/5] fix: nit --- src/elements/content-sharing/ContentSharingV2.tsx | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/src/elements/content-sharing/ContentSharingV2.tsx b/src/elements/content-sharing/ContentSharingV2.tsx index 08084ea228..9e184cfd48 100644 --- a/src/elements/content-sharing/ContentSharingV2.tsx +++ b/src/elements/content-sharing/ContentSharingV2.tsx @@ -32,7 +32,7 @@ export interface ContentSharingV2Props { function ContentSharingV2({ api, children, itemId, itemType }: ContentSharingV2Props) { const [avatarUrlMap, setAvatarUrlMap] = React.useState(null); const [item, setItem] = React.useState(null); - const [errorMessage, setErrorMessage] = React.useState(false); + const [hasError, setHasError] = React.useState(false); const [sharedLink, setSharedLink] = React.useState(null); const [sharingServiceProps, setSharingServiceProps] = React.useState(null); const [currentUser, setCurrentUser] = React.useState(null); @@ -80,7 +80,7 @@ function ContentSharingV2({ api, children, itemId, itemType }: ContentSharingV2P const getError = React.useCallback( (error: ElementsXhrError) => { // display only one component-level notification at a time - if (errorMessage) { + if (hasError) { return; } @@ -93,20 +93,21 @@ function ContentSharingV2({ api, children, itemId, itemType }: ContentSharingV2P errorObject = messages.loadingError; } - setErrorMessage(errorObject.defaultMessage); + setHasError(true); addNotification({ closeButtonAriaLabel: formatMessage(messages.noticeCloseLabel), sensitivity: 'foreground' as const, typeIconAriaLabel: formatMessage(messages.errorNoticeIcon), variant: 'error', - styledText: errorObject.defaultMessage, + styledText: formatMessage(errorObject), }); }, - [errorMessage, addNotification, formatMessage], + [hasError, addNotification, formatMessage], ); // Reset state if the API has changed React.useEffect(() => { + setHasError(false); setItem(null); setSharedLink(null); setCurrentUser(null); From 3df221a46319c4f345c85b8768b24e19477c3d1b Mon Sep 17 00:00:00 2001 From: reneshen0328 Date: Thu, 23 Oct 2025 21:26:00 -0700 Subject: [PATCH 3/5] fix: tests --- .../stories/ContentSharingV2.stories.tsx | 13 ++-------- .../tests/ContentSharingV2-visual.stories.tsx | 13 ++-------- .../utils/__mocks__/ContentSharingV2Mocks.js | 24 +++++++++++++++++++ 3 files changed, 28 insertions(+), 22 deletions(-) diff --git a/src/elements/content-sharing/stories/ContentSharingV2.stories.tsx b/src/elements/content-sharing/stories/ContentSharingV2.stories.tsx index 9e663a277f..eca25b96bd 100644 --- a/src/elements/content-sharing/stories/ContentSharingV2.stories.tsx +++ b/src/elements/content-sharing/stories/ContentSharingV2.stories.tsx @@ -1,8 +1,5 @@ -import * as React from 'react'; - import { TYPE_FILE, TYPE_FOLDER } from '../../../constants'; -import { mockApiWithSharedLink, mockApiWithoutSharedLink } from '../utils/__mocks__/ContentSharingV2Mocks'; -import ContentSharingV2 from '../ContentSharingV2'; +import { ContentSharingV2Template, mockApiWithSharedLink } from '../utils/__mocks__/ContentSharingV2Mocks'; export const basic = {}; @@ -14,13 +11,7 @@ export const withSharedLink = { export default { title: 'Elements/ContentSharingV2', - component: ContentSharingV2, - args: { - api: mockApiWithoutSharedLink, - children: , - itemType: TYPE_FILE, - itemId: global.FILE_ID, - }, + component: ContentSharingV2Template, argTypes: { itemType: { options: [TYPE_FILE, TYPE_FOLDER], diff --git a/src/elements/content-sharing/stories/tests/ContentSharingV2-visual.stories.tsx b/src/elements/content-sharing/stories/tests/ContentSharingV2-visual.stories.tsx index d9b42c109b..8e951282fe 100644 --- a/src/elements/content-sharing/stories/tests/ContentSharingV2-visual.stories.tsx +++ b/src/elements/content-sharing/stories/tests/ContentSharingV2-visual.stories.tsx @@ -1,15 +1,11 @@ -import * as React from 'react'; import { expect, screen, userEvent, waitFor, within } from 'storybook/test'; -import { Button } from '@box/blueprint-web'; - -import { TYPE_FILE } from '../../../../constants'; import { + ContentSharingV2Template, mockApiWithCollaborators, mockApiWithSharedLink, mockApiWithoutSharedLink, } from '../../utils/__mocks__/ContentSharingV2Mocks'; -import ContentSharingV2 from '../../ContentSharingV2'; export const withModernization = { args: { @@ -69,10 +65,5 @@ export const withCollaborators = { export default { title: 'Elements/ContentSharingV2/tests/visual-regression-tests', - component: ContentSharingV2, - args: { - children: , - itemType: TYPE_FILE, - itemId: global.FILE_ID, - }, + component: ContentSharingV2Template, }; diff --git a/src/elements/content-sharing/utils/__mocks__/ContentSharingV2Mocks.js b/src/elements/content-sharing/utils/__mocks__/ContentSharingV2Mocks.js index 14c9787b46..706fd5201e 100644 --- a/src/elements/content-sharing/utils/__mocks__/ContentSharingV2Mocks.js +++ b/src/elements/content-sharing/utils/__mocks__/ContentSharingV2Mocks.js @@ -1,3 +1,10 @@ +import * as React from 'react'; + +import { Button, Notification, TooltipProvider } from '@box/blueprint-web'; + +import { TYPE_FILE } from '../../../../constants'; +import ContentSharingV2 from '../../ContentSharingV2'; + export const MOCK_PERMISSIONS = { can_download: true, can_invite_collaborator: true, @@ -193,3 +200,20 @@ export const mockApiWithCollaborators = createMockApi( DEFAULT_USER_API_RESPONSE, MOCK_COLLABORATIONS_RESPONSE, ); + +export const ContentSharingV2Template = (props = {}) => { + return ( + + + + + + + + ); +}; From 8328250df953b68946c4ca27e3e33a49359fbb81 Mon Sep 17 00:00:00 2001 From: reneshen0328 Date: Thu, 23 Oct 2025 22:01:52 -0700 Subject: [PATCH 4/5] fix: handle default error message case --- i18n/en-US.properties | 2 + .../content-sharing/ContentSharingV2.tsx | 49 +++++++++---------- .../__tests__/ContentSharingV2.test.tsx | 15 ++++++ .../apis/__tests__/fetchAvatars.test.ts | 18 ++++--- .../content-sharing/apis/fetchAvatars.ts | 3 +- src/elements/content-sharing/messages.js | 5 ++ 6 files changed, 58 insertions(+), 34 deletions(-) diff --git a/i18n/en-US.properties b/i18n/en-US.properties index c806e24cce..cb13434219 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. +# Default error notification text rendered when API fails +be.contentSharing.defaultErrorNoticeText = Something went wrong. Please try again later. # Icon label for the error notifications be.contentSharing.errorNoticeIcon = Error # Message that appears when users cannot be retrieved in the ContentSharing Element. diff --git a/src/elements/content-sharing/ContentSharingV2.tsx b/src/elements/content-sharing/ContentSharingV2.tsx index 9e184cfd48..803bfde965 100644 --- a/src/elements/content-sharing/ContentSharingV2.tsx +++ b/src/elements/content-sharing/ContentSharingV2.tsx @@ -84,13 +84,17 @@ function ContentSharingV2({ api, children, itemId, itemType }: ContentSharingV2P return; } - let errorObject; + let errorMessage; if (error.status) { - errorObject = messages[CONTENT_SHARING_ERRORS[error.status]]; + errorMessage = messages[CONTENT_SHARING_ERRORS[error.status]]; } else if (error.response && error.response.status) { - errorObject = messages[CONTENT_SHARING_ERRORS[error.response.status]]; + errorMessage = messages[CONTENT_SHARING_ERRORS[error.response.status]]; } else { - errorObject = messages.loadingError; + errorMessage = messages.loadingError; + } + + if (!errorMessage) { + errorMessage = messages.defaultErrorNoticeText; } setHasError(true); @@ -99,7 +103,7 @@ function ContentSharingV2({ api, children, itemId, itemType }: ContentSharingV2P sensitivity: 'foreground' as const, typeIconAriaLabel: formatMessage(messages.errorNoticeIcon), variant: 'error', - styledText: formatMessage(errorObject), + styledText: formatMessage(errorMessage), }); }, [hasError, addNotification, formatMessage], @@ -167,34 +171,29 @@ function ContentSharingV2({ api, children, itemId, itemType }: ContentSharingV2P setCollaboratorsData(response); } catch (error) { setCollaboratorsData({ entries: [], next_marker: null }); - getError(error); } })(); - }, [api, collaboratorsData, item, itemId, itemType, getError]); + }, [api, collaboratorsData, item, itemId, itemType]); // Get avatars when collaborators are available React.useEffect(() => { if (avatarUrlMap || !collaboratorsData || !collaboratorsData.entries || !owner.id) return; (async () => { - try { - const ownerEntry = { - accessible_by: { - id: owner.id, - login: owner.email, - name: owner.name, - }, - }; - const response = await fetchAvatars({ - api, - itemId, - collaborators: [...collaboratorsData.entries, ownerEntry], - }); - setAvatarUrlMap(response); - } catch (error) { - getError(error); - } + const ownerEntry = { + accessible_by: { + id: owner.id, + login: owner.email, + name: owner.name, + }, + }; + const response = await fetchAvatars({ + api, + itemId, + collaborators: [...collaboratorsData.entries, ownerEntry], + }); + setAvatarUrlMap(response); })(); - }, [api, avatarUrlMap, collaboratorsData, itemId, owner, getError]); + }, [api, avatarUrlMap, collaboratorsData, itemId, owner]); React.useEffect(() => { if (avatarUrlMap && collaboratorsData && currentUser && owner) { diff --git a/src/elements/content-sharing/__tests__/ContentSharingV2.test.tsx b/src/elements/content-sharing/__tests__/ContentSharingV2.test.tsx index ec573b4861..23139057cb 100644 --- a/src/elements/content-sharing/__tests__/ContentSharingV2.test.tsx +++ b/src/elements/content-sharing/__tests__/ContentSharingV2.test.tsx @@ -224,5 +224,20 @@ describe('elements/content-sharing/ContentSharingV2', () => { }); }); }); + + test('should render default error message when no corresponding error status is provided', async () => { + const error = { status: 503 }; + renderComponent({ api: createErrorApi(error) }); + + await waitFor(() => { + expect(mockAddNotification).toHaveBeenCalledWith({ + closeButtonAriaLabel: 'Close', + sensitivity: 'foreground', + styledText: 'Something went wrong. Please try again later.', + typeIconAriaLabel: 'Error', + variant: 'error', + }); + }); + }); }); }); diff --git a/src/elements/content-sharing/apis/__tests__/fetchAvatars.test.ts b/src/elements/content-sharing/apis/__tests__/fetchAvatars.test.ts index 78415f0b96..732399cfda 100644 --- a/src/elements/content-sharing/apis/__tests__/fetchAvatars.test.ts +++ b/src/elements/content-sharing/apis/__tests__/fetchAvatars.test.ts @@ -50,13 +50,17 @@ describe('content-sharing/apis/fetchAvatars', () => { .mockRejectedValueOnce(new Error('Avatar fetch failed')) .mockResolvedValueOnce('https://example.com/avatar3.jpg'); - await expect( - fetchAvatars({ - api: defaultApiMock, - itemId: MOCK_ITEM.id, - collaborators: mockCollaborations, - }), - ).rejects.toThrow('Avatar fetch failed'); + const result = await fetchAvatars({ + api: defaultApiMock, + itemId: MOCK_ITEM.id, + collaborators: mockCollaborations, + }); + + expect(result).toEqual({ + 123: 'https://example.com/avatar1.jpg', + 456: null, + 789: 'https://example.com/avatar3.jpg', + }); }); test('should handle collaborators without accessible_by', async () => { diff --git a/src/elements/content-sharing/apis/fetchAvatars.ts b/src/elements/content-sharing/apis/fetchAvatars.ts index b89bd452ab..4bc50eee84 100644 --- a/src/elements/content-sharing/apis/fetchAvatars.ts +++ b/src/elements/content-sharing/apis/fetchAvatars.ts @@ -12,9 +12,8 @@ export const fetchAvatars = async ({ api, itemId, collaborators }: FetchCollabor try { const url = await usersApi.getAvatarUrlWithAccessToken(userId.toString(), itemId); avatarUrlMap[userId] = url; - } catch (error) { + } catch { avatarUrlMap[userId] = null; - throw error; } }); diff --git a/src/elements/content-sharing/messages.js b/src/elements/content-sharing/messages.js index 7f89ffabfe..98135e1039 100644 --- a/src/elements/content-sharing/messages.js +++ b/src/elements/content-sharing/messages.js @@ -1,6 +1,11 @@ import { defineMessages } from 'react-intl'; const messages = defineMessages({ + defaultErrorNoticeText: { + defaultMessage: 'Something went wrong. Please try again later.', + description: 'Default error notification text rendered when API fails', + id: 'be.contentSharing.defaultErrorNoticeText', + }, badRequestError: { defaultMessage: 'The request for this item was malformed.', description: 'Message that appears when the request for the ContentSharing Element is malformed.', From 3849ac1db5cb1c1318f2dbcd8daec7699f92f6cf Mon Sep 17 00:00:00 2001 From: reneshen0328 Date: Thu, 23 Oct 2025 22:35:56 -0700 Subject: [PATCH 5/5] fix: nit --- src/elements/content-sharing/ContentSharingV2.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/elements/content-sharing/ContentSharingV2.tsx b/src/elements/content-sharing/ContentSharingV2.tsx index 803bfde965..2f076b0838 100644 --- a/src/elements/content-sharing/ContentSharingV2.tsx +++ b/src/elements/content-sharing/ContentSharingV2.tsx @@ -169,7 +169,7 @@ function ContentSharingV2({ api, children, itemId, itemType }: ContentSharingV2P try { const response = await fetchCollaborators({ api, itemId, itemType }); setCollaboratorsData(response); - } catch (error) { + } catch { setCollaboratorsData({ entries: [], next_marker: null }); } })();