Skip to content
Original file line number Diff line number Diff line change
Expand Up @@ -75,13 +75,14 @@ export const useUseTemplateWizardContext = (
if (!formId) return
switch (responseMode) {
case FormResponseMode.Encrypt: {
const defaultEmails = adminEmail ? [adminEmail] : []
return useStorageModeFormTemplateMutation.mutate(
{
formIdToDuplicate: formId,
title,
responseMode,
publicKey: keypair.publicKey,
emails: emails.filter(Boolean),
emails: (emails ?? defaultEmails).filter(Boolean),
},
{
onSuccess: () => {
Expand Down Expand Up @@ -152,7 +153,7 @@ export const useUseTemplateWizardContext = (
handleSubmit((inputs) => {
return useEmailModeFormTemplateMutation.mutate({
formIdToDuplicate: formId,
emails: inputs.emails.filter(Boolean),
emails: (inputs.emails ?? []).filter(Boolean),
title: inputs.title,
responseMode: FormResponseMode.Email,
})
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,13 +9,15 @@ import {
useClipboard,
useDisclosure,
} from '@chakra-ui/react'
import { GrowthBook, GrowthBookProvider } from '@growthbook/growthbook-react'
import { Meta, StoryFn } from '@storybook/react'

import { featureFlags } from 'formsg-shared/constants'
import { UserId } from 'formsg-shared/types'
import { PublicFormViewDto } from 'formsg-shared/types/form'
import { Workspace, WorkspaceId } from 'formsg-shared/types/workspace'

import { userHandlers } from '~/mocks/msw/handlers/user'
import { getUser, MOCK_USER, userHandlers } from '~/mocks/msw/handlers/user'

import { ApiError } from '~typings/core'

Expand Down Expand Up @@ -81,6 +83,79 @@ const Template: StoryFn<CreateFormModalProps> = (args) => {
}
export const Default = Template.bind({})

const mrfCutoverOn = new GrowthBook({
features: { [featureFlags.mrfCutover]: { defaultValue: true } },
})

export const MrfCutoverOn = Template.bind({})

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

issue (non-blocking): the storage-mode page itself has no Storybook story / Chromatic baseline.

What's happening: the added stories (MrfCutoverOn + the three beta-flag variants) all render only the MRF default screen. None has a play / userEvent step that clicks the escape-hatch link through to CreateFormStorageModeScreen, so that screen is never captured.

Why it matters: issue #9453 asks for stories covering "escape-hatch open / storage-mode page", and without one Chromatic won't baseline the new screen.

Suggestion: add one story with a play function that clicks the escape-hatch link and asserts the storage-mode page renders.

Confidence: 75 — verified unmet acceptance criterion; scored just under the Spec gate's 80 bar, included here as a follow-up.

🤖 This comment was generated by an AI code review. Please verify before acting on it.

@kevin9foong kevin9foong Jun 4, 2026

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

lets skip this for now, it is a good to have, but not strictly necessary.

MrfCutoverOn.decorators = [
(Story) => (
<GrowthBookProvider growthbook={mrfCutoverOn}>
<Story />
</GrowthBookProvider>
),
]

export const MrfCutoverOnChildrenBeta = Template.bind({})
MrfCutoverOnChildrenBeta.decorators = [
(Story) => (
<GrowthBookProvider growthbook={mrfCutoverOn}>
<Story />
</GrowthBookProvider>
),
]
MrfCutoverOnChildrenBeta.parameters = {
msw: [
getUser({
delay: 0,
mockUser: { ...MOCK_USER, betaFlags: { children: true } },
}),
],
}

export const MrfCutoverOnWebhookV1Beta = Template.bind({})
MrfCutoverOnWebhookV1Beta.decorators = [
(Story) => (
<GrowthBookProvider growthbook={mrfCutoverOn}>
<Story />
</GrowthBookProvider>
),
]
MrfCutoverOnWebhookV1Beta.parameters = {
msw: [
getUser({
delay: 0,
mockUser: {
...MOCK_USER,
betaFlags: { createStorageModeForV1Webhook: true },
},
}),
],
}

export const MrfCutoverOnAllExceptions = Template.bind({})
MrfCutoverOnAllExceptions.decorators = [
(Story) => (
<GrowthBookProvider growthbook={mrfCutoverOn}>
<Story />
</GrowthBookProvider>
),
]
MrfCutoverOnAllExceptions.parameters = {
msw: [
getUser({
delay: 0,
mockUser: {
...MOCK_USER,
betaFlags: {
children: true,
createStorageModeForV1Webhook: true,
},
},
}),
],
}

export const StorageModeAckScreen = () => {
const secretKey = 'mock-secret-key'

Expand Down
Original file line number Diff line number Diff line change
@@ -1,7 +1,8 @@
import { Controller, RegisterOptions } from 'react-hook-form'
import { Controller } from 'react-hook-form'
import { useTranslation } from 'react-i18next'
import { BiRightArrowAlt } from 'react-icons/bi'
import {
Box,
Container,
FormControl,
ModalBody,
Expand All @@ -13,26 +14,19 @@ import {
import { FormResponseMode } from 'formsg-shared/types/form/form'

import { GUIDE_PREVENT_EMAIL_BOUNCE } from '~constants/links'
import { useFormTitleValidationRules } from '~utils/formValidation'
import Button from '~components/Button'
import FormErrorMessage from '~components/FormControl/FormErrorMessage'
import FormFieldMessage from '~components/FormControl/FormFieldMessage'
import FormLabel from '~components/FormControl/FormLabel'
import InlineMessage from '~components/InlineMessage'
import Input from '~components/Input'

import DataClassificationInfoBox from '~features/admin-form/settings/components/DataClassificationInfoBox'

import {
CreateFormWizardInputProps,
useCreateFormWizard,
} from '../CreateFormWizardContext'
import { useCreateFormWizard } from '../CreateFormWizardContext'

import { EmailFormRecipientsInput } from './EmailFormRecipientsInput'
import { EscapeHatchLink } from './EscapeHatchLink'
import { FormResponseOptions } from './FormResponseOptions'

/** The length of form title to start showing warning text */
const FORM_TITLE_LENGTH_WARNING = 65
import { FormTitleInput } from './FormTitleInput'

const getTrackingSubmissionActionName = (
responseModeValue: FormResponseMode,
Expand Down Expand Up @@ -61,24 +55,21 @@ export const CreateFormDetailsScreen = (): JSX.Element => {
isLoading,
isFetching,
modalHeader,
isSingpass,
hasMyInfoChildren,
isMrfCutoverEnabled,
goToStorageModeDetails,
} = useCreateFormWizard()
const {
register,
control,
formState: { errors },
watch,
} = formMethods

const titleInputValue = watch('title')
const responseModeValue = watch('responseMode')
const handleEmailButtonPress = () => {
handleEmailFeedbackSubmit()
}

const formTitleValidationRules = useFormTitleValidationRules()

return (
<>
<ModalHeader color="secondary.700">
Expand All @@ -88,89 +79,78 @@ export const CreateFormDetailsScreen = (): JSX.Element => {
</ModalHeader>
<ModalBody whiteSpace="pre-wrap">
<Container maxW="45rem" p={0}>
<FormControl isRequired isInvalid={!!errors.title} mb="2.25rem">
<FormLabel useMarkdownForDescription>
{t('features.workspace.modals.forms.create.details.name.label')}
</FormLabel>
<Skeleton isLoaded={!isFetching}>
<Input
autoFocus
{...register(
'title',
formTitleValidationRules as RegisterOptions<
CreateFormWizardInputProps,
'title'
>,
)}
/>
</Skeleton>
<FormErrorMessage>{errors.title?.message}</FormErrorMessage>
{titleInputValue?.length > FORM_TITLE_LENGTH_WARNING ? (
<FormFieldMessage>
{t(
'features.workspace.modals.forms.create.details.name.message',
)}
</FormFieldMessage>
) : null}
</FormControl>
<FormControl isRequired isInvalid={!!errors.responseMode} mb="2.5rem">
<FormLabel
description={t(
'features.workspace.modals.forms.create.details.type.description',
)}
>
{t('features.workspace.modals.forms.create.details.type.label')}
</FormLabel>
<Skeleton isLoaded={!isFetching}>
<Controller
name="responseMode"
control={control}
render={({ field }) => (
<FormResponseOptions
{...field}
hasMyInfoChildren={hasMyInfoChildren}
handleEmailButtonPress={handleEmailButtonPress}
/>
)}
rules={{
required: t(
'features.workspace.modals.forms.create.errors.responseMode.required',
),
}}
/>
</Skeleton>
<FormErrorMessage>{errors.responseMode?.message}</FormErrorMessage>
{hasMyInfoChildren && (
<InlineMessage mt="2rem">
{t(
'features.workspace.modals.forms.create.errors.noMyInfoChildrenInMrf',
)}
</InlineMessage>
)}
</FormControl>
{(responseModeValue === FormResponseMode.Encrypt ||
responseModeValue === FormResponseMode.Email) && (
<FormTitleInput />
{isMrfCutoverEnabled ? (
<Box my="2.5rem">
<EscapeHatchLink onClick={goToStorageModeDetails} />
</Box>
) : (
<FormControl
isRequired={responseModeValue === FormResponseMode.Email}
isInvalid={!!errors.emails}
mb="2.25rem"
isRequired
isInvalid={!!errors.responseMode}
mb="2.5rem"
>
<FormLabel
isRequired={responseModeValue === FormResponseMode.Email}
useMarkdownForDescription
description={t(
'features.workspace.modals.forms.create.details.notifications.description',
{ GUIDE_PREVENT_EMAIL_BOUNCE },
'features.workspace.modals.forms.create.details.type.description',
)}
>
{t(
'features.workspace.modals.forms.create.details.notifications.label',
)}
{t('features.workspace.modals.forms.create.details.type.label')}
</FormLabel>
<EmailFormRecipientsInput />
<Skeleton isLoaded={!isFetching}>
<Controller
name="responseMode"
control={control}
render={({ field }) => (
<FormResponseOptions
{...field}
hasMyInfoChildren={hasMyInfoChildren}
handleEmailButtonPress={handleEmailButtonPress}
/>
)}
rules={{
required: t(
'features.workspace.modals.forms.create.errors.responseMode.required',
),
}}
/>
</Skeleton>
<FormErrorMessage>
{errors.responseMode?.message}
</FormErrorMessage>
{hasMyInfoChildren && (
<InlineMessage mt="2rem">
{t(
'features.workspace.modals.forms.create.errors.noMyInfoChildrenInMrf',
)}
</InlineMessage>
)}
</FormControl>
)}
<DataClassificationInfoBox />
{!isMrfCutoverEnabled &&
(responseModeValue === FormResponseMode.Encrypt ||
responseModeValue === FormResponseMode.Email) && (
<FormControl
isRequired={responseModeValue === FormResponseMode.Email}
isInvalid={!!errors.emails}
mb="2.25rem"
>
<FormLabel
isRequired={responseModeValue === FormResponseMode.Email}
useMarkdownForDescription
description={t(
'features.workspace.modals.forms.create.details.notifications.description',
{ GUIDE_PREVENT_EMAIL_BOUNCE },
)}
>
{t(
'features.workspace.modals.forms.create.details.notifications.label',
)}
</FormLabel>
<EmailFormRecipientsInput />
</FormControl>
)}
{!isMrfCutoverEnabled && <DataClassificationInfoBox />}
<Button
rightIcon={<BiRightArrowAlt fontSize="1.5rem" />}
type="submit"
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import {
} from '../CreateFormWizardContext'

import { CreateFormDetailsScreen } from './CreateFormDetailsScreen'
import { CreateFormStorageModeScreen } from './CreateFormStorageModeScreen'
import {
EmailModeCreationScreen,
EmailModeFeedbackScreen,
Expand All @@ -28,6 +29,9 @@ export const CreateFormModalContent = () => {
{currentStep === CreateFormFlowStates.Details && (
<CreateFormDetailsScreen />
)}
{currentStep === CreateFormFlowStates.StorageModeDetails && (
<CreateFormStorageModeScreen />
)}
{currentStep === CreateFormFlowStates.Landing && (
<SaveSecretKeyScreen />
)}
Expand Down
Loading
Loading