From f98030acbc07b651e02550836b5b23cecc173142 Mon Sep 17 00:00:00 2001 From: Kim Yungju <145027166+kimyungju@users.noreply.github.com> Date: Tue, 2 Jun 2026 16:28:33 +0800 Subject: [PATCH 1/4] fix(admin-form): show error state when webhook settings fetch fails (#9523) * feat(admin-form): add WebhooksErrorMsg settings error state * fix(admin-form): show error state when webhook settings fetch fails A failed settings query left settings undefined, which collapsed into the unsupported-response-mode branch and told storage-mode admins their form does not support webhooks. Read isError from the query and render an explicit retry state before evaluating the response mode. * test(admin-form): add Error story for webhook settings fetch failure * test(admin-form): assert error state hides unsupported webhook msg * fix(admin-form): announce webhook settings load error to a11y The error state appears after an async settings-fetch failure; add role="alert" so screen readers announce it instead of leaving AT users unaware that anything changed. Covered by a getByRole('alert') assertion. * refactor(admin-form): use isRefetching for webhook retry state isFetching is also true for background refetches (window focus/reconnect), which could spin the retry button without a user action. isRefetching reflects the user-initiated retry more precisely. * fix(settings/webhooks): enforce strict equality Co-authored-by: LoneRifle Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com> --- .../settings/SettingsWebhooksPage.stories.tsx | 17 ++++++++++ .../settings/SettingsWebhooksPage.test.tsx | 34 +++++++++++++++++++ .../settings/SettingsWebhooksPage.tsx | 17 ++++++++-- .../WebhooksSection/WebhooksErrorMsg.tsx | 34 +++++++++++++++++++ 4 files changed, 100 insertions(+), 2 deletions(-) create mode 100644 apps/frontend/src/features/admin-form/settings/SettingsWebhooksPage.test.tsx create mode 100644 apps/frontend/src/features/admin-form/settings/components/WebhooksSection/WebhooksErrorMsg.tsx diff --git a/apps/frontend/src/features/admin-form/settings/SettingsWebhooksPage.stories.tsx b/apps/frontend/src/features/admin-form/settings/SettingsWebhooksPage.stories.tsx index 120d8c2872..533ab90a3b 100644 --- a/apps/frontend/src/features/admin-form/settings/SettingsWebhooksPage.stories.tsx +++ b/apps/frontend/src/features/admin-form/settings/SettingsWebhooksPage.stories.tsx @@ -1,4 +1,5 @@ import { Meta, StoryFn } from '@storybook/react' +import { http, HttpResponse } from 'msw' import { FormResponseMode, FormSettings } from 'formsg-shared/types/form' @@ -75,6 +76,22 @@ Loading.parameters = { msw: { handlers: { default: buildMswRoutes({ delay: 'infinite' }) } }, } +export const Error = Template.bind({}) +Error.parameters = { + msw: { + handlers: { + default: [ + http.get('/api/v3/admin/forms/:formId/settings', () => + HttpResponse.json( + { message: 'Internal Server Error' }, + { status: 500 }, + ), + ), + ], + }, + }, +} + export const Mobile = Template.bind({}) Mobile.parameters = { ...StorageModeRetryEnabled.parameters, diff --git a/apps/frontend/src/features/admin-form/settings/SettingsWebhooksPage.test.tsx b/apps/frontend/src/features/admin-form/settings/SettingsWebhooksPage.test.tsx new file mode 100644 index 0000000000..cf1f0feae3 --- /dev/null +++ b/apps/frontend/src/features/admin-form/settings/SettingsWebhooksPage.test.tsx @@ -0,0 +1,34 @@ +import { composeStories } from '@storybook/react' +import { act, render, screen } from '@testing-library/react' + +import * as stories from './SettingsWebhooksPage.stories' + +const { Error: ErrorStory, UnsupportedEmailMode } = composeStories(stories) + +const UNSUPPORTED_MSG = /webhooks are only available in storage mode/i +const ERROR_MSG = /couldn't load webhook settings/i + +describe('SettingsWebhooksPage', () => { + it('shows an error state, not the unsupported-mode message, when the settings fetch fails', async () => { + await act(async () => { + render() + }) + + await screen.findByText(ERROR_MSG) + // The error is announced to assistive tech (it appears after an async failure). + expect(screen.getByRole('alert')).toBeInTheDocument() + expect( + screen.getByRole('button', { name: /try again/i }), + ).toBeInTheDocument() + expect(screen.queryByText(UNSUPPORTED_MSG)).not.toBeInTheDocument() + }) + + it('still shows the unsupported-mode message for a form whose mode genuinely lacks webhook support', async () => { + await act(async () => { + render() + }) + + await screen.findByText(UNSUPPORTED_MSG) + expect(screen.queryByText(ERROR_MSG)).not.toBeInTheDocument() + }) +}) diff --git a/apps/frontend/src/features/admin-form/settings/SettingsWebhooksPage.tsx b/apps/frontend/src/features/admin-form/settings/SettingsWebhooksPage.tsx index e05844a8df..0869297000 100644 --- a/apps/frontend/src/features/admin-form/settings/SettingsWebhooksPage.tsx +++ b/apps/frontend/src/features/admin-form/settings/SettingsWebhooksPage.tsx @@ -10,13 +10,20 @@ import { useUser } from '~features/user/queries' import { CategoryHeader } from './components/CategoryHeader' import { WebhooksSection } from './components/WebhooksSection' +import { WebhooksErrorMsg } from './components/WebhooksSection/WebhooksErrorMsg' import { WebhooksUnsupportedMsg } from './components/WebhooksSection/WebhooksUnsupportedMsg' import { useAdminFormSettings } from './queries' export const SettingsWebhooksPage = (): JSX.Element => { const { t } = useTranslation() const gb = useGrowthBook() - const { data: settings, isLoading } = useAdminFormSettings() + const { + data: settings, + isLoading, + isError, + isRefetching, + refetch, + } = useAdminFormSettings() const userRes = useUser() useEffect(() => { @@ -27,9 +34,15 @@ export const SettingsWebhooksPage = (): JSX.Element => { const enableMrfWebhooks = useFeatureIsOn(featureFlags.enableMrfWebhooks) + // The settings fetch failed. Show an explicit error/retry state instead of + // misreporting the form's response mode as unsupported. + if (isError) { + return + } + const enableWebhooks = !isLoading && - (settings?.responseMode == FormResponseMode.Encrypt || + (settings?.responseMode === FormResponseMode.Encrypt || (settings?.responseMode === FormResponseMode.Multirespondent && enableMrfWebhooks)) diff --git a/apps/frontend/src/features/admin-form/settings/components/WebhooksSection/WebhooksErrorMsg.tsx b/apps/frontend/src/features/admin-form/settings/components/WebhooksSection/WebhooksErrorMsg.tsx new file mode 100644 index 0000000000..bb188164ec --- /dev/null +++ b/apps/frontend/src/features/admin-form/settings/components/WebhooksSection/WebhooksErrorMsg.tsx @@ -0,0 +1,34 @@ +import { Flex, Text } from '@chakra-ui/react' + +import Button from '~components/Button' + +export interface WebhooksErrorMsgProps { + onRetry: () => void + isRetrying?: boolean +} + +export const WebhooksErrorMsg = ({ + onRetry, + isRetrying = false, +}: WebhooksErrorMsgProps): JSX.Element => { + return ( + + + Couldn't load webhook settings + + + Something went wrong while loading this form's settings. This does not + affect your form or its responses. Please try again. + + + + + + ) +} From b781fcb6ec57b24cc2cdf0d1d5ed9a724fd57c7f Mon Sep 17 00:00:00 2001 From: LoneRifle Date: Tue, 2 Jun 2026 16:36:12 +0800 Subject: [PATCH 2/4] fix(settings/webhooks): invoke `onRetry()` w/o args Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com> --- .../settings/components/WebhooksSection/WebhooksErrorMsg.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/apps/frontend/src/features/admin-form/settings/components/WebhooksSection/WebhooksErrorMsg.tsx b/apps/frontend/src/features/admin-form/settings/components/WebhooksSection/WebhooksErrorMsg.tsx index bb188164ec..6e72181f9a 100644 --- a/apps/frontend/src/features/admin-form/settings/components/WebhooksSection/WebhooksErrorMsg.tsx +++ b/apps/frontend/src/features/admin-form/settings/components/WebhooksSection/WebhooksErrorMsg.tsx @@ -24,7 +24,7 @@ export const WebhooksErrorMsg = ({ From 069ce243cfc0ee177ddb6729636771d714ca397a Mon Sep 17 00:00:00 2001 From: LoneRifle Date: Tue, 2 Jun 2026 17:07:53 +0800 Subject: [PATCH 3/4] chore(settings/webhooks): remove comments, code self-documenting --- .../src/features/admin-form/settings/SettingsWebhooksPage.tsx | 2 -- 1 file changed, 2 deletions(-) diff --git a/apps/frontend/src/features/admin-form/settings/SettingsWebhooksPage.tsx b/apps/frontend/src/features/admin-form/settings/SettingsWebhooksPage.tsx index 0869297000..0a9f50b510 100644 --- a/apps/frontend/src/features/admin-form/settings/SettingsWebhooksPage.tsx +++ b/apps/frontend/src/features/admin-form/settings/SettingsWebhooksPage.tsx @@ -34,8 +34,6 @@ export const SettingsWebhooksPage = (): JSX.Element => { const enableMrfWebhooks = useFeatureIsOn(featureFlags.enableMrfWebhooks) - // The settings fetch failed. Show an explicit error/retry state instead of - // misreporting the form's response mode as unsupported. if (isError) { return } From 3881d1f4a3b692b812ffd21adaf5f11d982fda27 Mon Sep 17 00:00:00 2001 From: LoneRifle Date: Tue, 2 Jun 2026 17:49:58 +0800 Subject: [PATCH 4/4] feat(i18n): add webhook error text --- .../WebhooksSection/WebhooksErrorMsg.tsx | 17 ++++++++++++----- .../admin-form/settings/webhooks/en-sg.ts | 8 ++++++++ .../admin-form/settings/webhooks/index.ts | 8 ++++++++ 3 files changed, 28 insertions(+), 5 deletions(-) diff --git a/apps/frontend/src/features/admin-form/settings/components/WebhooksSection/WebhooksErrorMsg.tsx b/apps/frontend/src/features/admin-form/settings/components/WebhooksSection/WebhooksErrorMsg.tsx index 6e72181f9a..411de32705 100644 --- a/apps/frontend/src/features/admin-form/settings/components/WebhooksSection/WebhooksErrorMsg.tsx +++ b/apps/frontend/src/features/admin-form/settings/components/WebhooksSection/WebhooksErrorMsg.tsx @@ -1,3 +1,4 @@ +import { useTranslation } from 'react-i18next' import { Flex, Text } from '@chakra-ui/react' import Button from '~components/Button' @@ -11,22 +12,28 @@ export const WebhooksErrorMsg = ({ onRetry, isRetrying = false, }: WebhooksErrorMsgProps): JSX.Element => { + const { t } = useTranslation() + const { title, body, button } = t( + 'features.adminForm.settings.webhooks.error', + { + returnObjects: true, + }, + ) return ( - Couldn't load webhook settings + {title} - Something went wrong while loading this form's settings. This does not - affect your form or its responses. Please try again. + {body} diff --git a/apps/frontend/src/i18n/locales/features/admin-form/settings/webhooks/en-sg.ts b/apps/frontend/src/i18n/locales/features/admin-form/settings/webhooks/en-sg.ts index 89daa00739..4ac0b70eb9 100644 --- a/apps/frontend/src/i18n/locales/features/admin-form/settings/webhooks/en-sg.ts +++ b/apps/frontend/src/i18n/locales/features/admin-form/settings/webhooks/en-sg.ts @@ -9,4 +9,12 @@ export const enSG = { label: 'Enable retries', description: `Your system must meet certain requirements before retries can be safely enabled. [Learn more]({url})`, }, + error: { + title: "Couldn't load webhook settings", + body: "Something went wrong while loading this form's settings. This does not affect your form or its responses. Please try again.", + button: { + label: 'Try again', + loadingText: 'Trying again…', + }, + }, } diff --git a/apps/frontend/src/i18n/locales/features/admin-form/settings/webhooks/index.ts b/apps/frontend/src/i18n/locales/features/admin-form/settings/webhooks/index.ts index 67d797b927..e5583d2151 100644 --- a/apps/frontend/src/i18n/locales/features/admin-form/settings/webhooks/index.ts +++ b/apps/frontend/src/i18n/locales/features/admin-form/settings/webhooks/index.ts @@ -11,4 +11,12 @@ export interface Webhooks extends HasTitle { label: string description: string } + error: { + title: string + body: string + button: { + label: string + loadingText: string + } + } }