From 122f6c12be148787b481d00e46cf175c6e724f6f Mon Sep 17 00:00:00 2001 From: Mayank Sharma Date: Mon, 24 Aug 2026 14:20:05 +0530 Subject: [PATCH 1/2] fix(ShareModal): preserve host visibility error message on failure Signed-off-by: Mayank Sharma --- .../ShareModalWireContract.test.tsx | 59 +++++++++++++++++++ src/custom/ShareModal/ShareModal.tsx | 18 +++++- 2 files changed, 74 insertions(+), 3 deletions(-) diff --git a/src/__testing__/ShareModalWireContract.test.tsx b/src/__testing__/ShareModalWireContract.test.tsx index e1222954a..457474241 100644 --- a/src/__testing__/ShareModalWireContract.test.tsx +++ b/src/__testing__/ShareModalWireContract.test.tsx @@ -188,4 +188,63 @@ describe('ShareModal resource-access wire contract', () => { ); expect(resourceAccessMutator).not.toHaveBeenCalled(); }); + + it('surfaces host error text when visibility update fails with a string error', async () => { + const handleUpdateVisibility = jest + .fn() + .mockResolvedValue({ error: 'visibility rejected' }); + const { props } = renderShareModal({ + handleUpdateVisibility + } as Partial); + + fireEvent.mouseDown(document.getElementById('share-menu')!); + fireEvent.click(screen.getByRole('option', { name: 'Public' })); + + await waitFor(() => expect(handleUpdateVisibility).toHaveBeenCalledWith('public')); + await waitFor(() => + expect(props.notify).toHaveBeenCalledWith({ + message: 'Failed to update visibility. visibility rejected', + event_type: 'error' + }) + ); + }); + + it('surfaces nested RTK error text when visibility update returns an error object', async () => { + const handleUpdateVisibility = jest + .fn() + .mockResolvedValue({ error: { error: 'permission denied' } }); + const { props } = renderShareModal({ + handleUpdateVisibility + } as Partial); + + fireEvent.mouseDown(document.getElementById('share-menu')!); + fireEvent.click(screen.getByRole('option', { name: 'Public' })); + + await waitFor(() => expect(handleUpdateVisibility).toHaveBeenCalledWith('public')); + await waitFor(() => + expect(props.notify).toHaveBeenCalledWith({ + message: 'Failed to update visibility. permission denied', + event_type: 'error' + }) + ); + }); + + it('notifies success when visibility update succeeds', async () => { + const handleUpdateVisibility = jest.fn().mockResolvedValue({ error: '' }); + const { props } = renderShareModal({ + handleUpdateVisibility + } as Partial); + + fireEvent.mouseDown(document.getElementById('share-menu')!); + fireEvent.click(screen.getByRole('option', { name: 'Public' })); + + await waitFor(() => expect(handleUpdateVisibility).toHaveBeenCalledWith('public')); + await waitFor(() => + expect(props.notify).toHaveBeenCalledWith({ + message: "Design 'My Design' is now public", + event_type: 'success' + }) + ); + }); }); + diff --git a/src/custom/ShareModal/ShareModal.tsx b/src/custom/ShareModal/ShareModal.tsx index 8045d6694..963749fd1 100644 --- a/src/custom/ShareModal/ShareModal.tsx +++ b/src/custom/ShareModal/ShareModal.tsx @@ -425,9 +425,21 @@ const ShareModal: React.FC = ({ const UPDATE_VISIBILITY_MSG = Array.isArray(selectedResource) ? `${startCase(dataName)}s (${selectedResource.length}) are now ${value}` : `${startCase(dataName)} '${selectedResource.name}' is now ${value}`; - const FAILED_TO_UPDATE_VISIBILITY_MSG = `Failed to update visibility. ${res?.error?.error || ''}`; - - if (!res.error) { + const detail = + typeof res?.error === 'string' + ? res.error + : typeof res?.error?.error === 'string' + ? res.error.error + : typeof res?.error?.data?.message === 'string' + ? res.error.data.message + : typeof res?.error?.message === 'string' + ? res.error.message + : ''; + const FAILED_TO_UPDATE_VISIBILITY_MSG = detail + ? `Failed to update visibility. ${detail}` + : 'Failed to update visibility.'; + + if (!res?.error) { notify({ message: UPDATE_VISIBILITY_MSG, event_type: 'success' From 418a522a80798bcd135f75f391115cb39a094aad Mon Sep 17 00:00:00 2001 From: Mayank Sharma Date: Mon, 24 Aug 2026 14:31:06 +0530 Subject: [PATCH 2/2] refactor(ShareModal): type visibility update response and remove any parameters Signed-off-by: Mayank Sharma --- src/custom/ShareModal/ShareModal.tsx | 55 +++++++++++++++++++++------- src/custom/ShareModal/index.tsx | 7 +++- src/index.tsx | 4 +- 3 files changed, 50 insertions(+), 16 deletions(-) diff --git a/src/custom/ShareModal/ShareModal.tsx b/src/custom/ShareModal/ShareModal.tsx index 963749fd1..7d92fdf24 100644 --- a/src/custom/ShareModal/ShareModal.tsx +++ b/src/custom/ShareModal/ShareModal.tsx @@ -174,6 +174,30 @@ export type ResourceAccessArg = { resourceAccessMappingPayload: ResourceAccessMappingPayload; }; +export type VisibilityUpdateError = + | string + | { + error?: string; + message?: string; + data?: + | { + message?: string; + [key: string]: unknown; + } + | string; + [key: string]: unknown; + }; + +export type VisibilityUpdateResponse = + | { + error?: VisibilityUpdateError; + data?: unknown; + [key: string]: unknown; + } + | void + | null + | undefined; + export interface ShareModalProps { /** Function to close the share modal */ handleShareModalClose: () => void; @@ -187,7 +211,7 @@ export interface ShareModalProps { fetchAccessActors: () => Promise; /** Optional URL of the host application. Defaults to `null` if not provided */ hostURL?: string | null; - handleUpdateVisibility: (value: string) => Promise<{ error: string }>; + handleUpdateVisibility: (value: string) => Promise; /** * @deprecated Unused - never read. The component defines its own * `handleShareWithNewUsers`, which shadows this prop and shares through @@ -420,26 +444,28 @@ const ShareModal: React.FC = ({ const handleDelete = async (actor: User) => handleRevokeAccess([actor]); - /* eslint-disable @typescript-eslint/no-explicit-any */ - const notifyVisibilityChange = (res: any, value: any) => { + const notifyVisibilityChange = (res: VisibilityUpdateResponse, value: string) => { const UPDATE_VISIBILITY_MSG = Array.isArray(selectedResource) ? `${startCase(dataName)}s (${selectedResource.length}) are now ${value}` : `${startCase(dataName)} '${selectedResource.name}' is now ${value}`; + const err = res && typeof res === 'object' && 'error' in res ? res.error : undefined; const detail = - typeof res?.error === 'string' - ? res.error - : typeof res?.error?.error === 'string' - ? res.error.error - : typeof res?.error?.data?.message === 'string' - ? res.error.data.message - : typeof res?.error?.message === 'string' - ? res.error.message - : ''; + typeof err === 'string' + ? err + : typeof err === 'object' && err !== null + ? typeof err.error === 'string' + ? err.error + : typeof err.data === 'object' && err.data !== null && typeof err.data.message === 'string' + ? err.data.message + : typeof err.message === 'string' + ? err.message + : '' + : ''; const FAILED_TO_UPDATE_VISIBILITY_MSG = detail ? `Failed to update visibility. ${detail}` : 'Failed to update visibility.'; - if (!res?.error) { + if (!err) { notify({ message: UPDATE_VISIBILITY_MSG, event_type: 'success' @@ -464,7 +490,8 @@ const ShareModal: React.FC = ({ setUpdatingVisibility(true); const res = await handleUpdateVisibility(value); notifyVisibilityChange(res, value); - if (!res?.error) { + const err = res && typeof res === 'object' && 'error' in res ? res.error : undefined; + if (!err) { setVisibility(value); } } finally { diff --git a/src/custom/ShareModal/index.tsx b/src/custom/ShareModal/index.tsx index e809d6422..b6915e1e5 100644 --- a/src/custom/ShareModal/index.tsx +++ b/src/custom/ShareModal/index.tsx @@ -8,5 +8,10 @@ export { type ResourceAccessActor, type ResourceAccessMappingPayload } from './resourceAccessPayload'; -export type { ResourceAccessArg, ShareModalProps } from './ShareModal'; +export type { + ResourceAccessArg, + ShareModalProps, + VisibilityUpdateError, + VisibilityUpdateResponse +} from './ShareModal'; export { ShareModal }; diff --git a/src/index.tsx b/src/index.tsx index d104a9220..300b12218 100644 --- a/src/index.tsx +++ b/src/index.tsx @@ -140,7 +140,9 @@ export { type ResourceAccessActor, type ResourceAccessArg, type ResourceAccessMappingPayload, - type ShareModalProps + type ShareModalProps, + type VisibilityUpdateError, + type VisibilityUpdateResponse } from './custom/ShareModal'; // Same nested-barrel dts-drop quirk as FeedbackButton above, and the whole