diff --git a/apps/frontend/src/features/admin-form/template/UseTemplateModal/UseTemplateWizardProvider.tsx b/apps/frontend/src/features/admin-form/template/UseTemplateModal/UseTemplateWizardProvider.tsx index 4cc12b5c5c..83ae4fa866 100644 --- a/apps/frontend/src/features/admin-form/template/UseTemplateModal/UseTemplateWizardProvider.tsx +++ b/apps/frontend/src/features/admin-form/template/UseTemplateModal/UseTemplateWizardProvider.tsx @@ -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: () => { @@ -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, }) diff --git a/apps/frontend/src/features/workspace/components/CreateFormModal/CreateFormModal.stories.tsx b/apps/frontend/src/features/workspace/components/CreateFormModal/CreateFormModal.stories.tsx index 424830a7b7..3fa71ecca7 100644 --- a/apps/frontend/src/features/workspace/components/CreateFormModal/CreateFormModal.stories.tsx +++ b/apps/frontend/src/features/workspace/components/CreateFormModal/CreateFormModal.stories.tsx @@ -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' @@ -81,6 +83,79 @@ const Template: StoryFn = (args) => { } export const Default = Template.bind({}) +const mrfCutoverOn = new GrowthBook({ + features: { [featureFlags.mrfCutover]: { defaultValue: true } }, +}) + +export const MrfCutoverOn = Template.bind({}) +MrfCutoverOn.decorators = [ + (Story) => ( + + + + ), +] + +export const MrfCutoverOnChildrenBeta = Template.bind({}) +MrfCutoverOnChildrenBeta.decorators = [ + (Story) => ( + + + + ), +] +MrfCutoverOnChildrenBeta.parameters = { + msw: [ + getUser({ + delay: 0, + mockUser: { ...MOCK_USER, betaFlags: { children: true } }, + }), + ], +} + +export const MrfCutoverOnWebhookV1Beta = Template.bind({}) +MrfCutoverOnWebhookV1Beta.decorators = [ + (Story) => ( + + + + ), +] +MrfCutoverOnWebhookV1Beta.parameters = { + msw: [ + getUser({ + delay: 0, + mockUser: { + ...MOCK_USER, + betaFlags: { createStorageModeForV1Webhook: true }, + }, + }), + ], +} + +export const MrfCutoverOnAllExceptions = Template.bind({}) +MrfCutoverOnAllExceptions.decorators = [ + (Story) => ( + + + + ), +] +MrfCutoverOnAllExceptions.parameters = { + msw: [ + getUser({ + delay: 0, + mockUser: { + ...MOCK_USER, + betaFlags: { + children: true, + createStorageModeForV1Webhook: true, + }, + }, + }), + ], +} + export const StorageModeAckScreen = () => { const secretKey = 'mock-secret-key' diff --git a/apps/frontend/src/features/workspace/components/CreateFormModal/CreateFormModalContent/CreateFormDetailsScreen.tsx b/apps/frontend/src/features/workspace/components/CreateFormModal/CreateFormModalContent/CreateFormDetailsScreen.tsx index 7bd9c253a1..1bcc3b73ec 100644 --- a/apps/frontend/src/features/workspace/components/CreateFormModal/CreateFormModalContent/CreateFormDetailsScreen.tsx +++ b/apps/frontend/src/features/workspace/components/CreateFormModal/CreateFormModalContent/CreateFormDetailsScreen.tsx @@ -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, @@ -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, @@ -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 ( <> @@ -88,89 +79,78 @@ export const CreateFormDetailsScreen = (): JSX.Element => { - - - {t('features.workspace.modals.forms.create.details.name.label')} - - - , - )} - /> - - {errors.title?.message} - {titleInputValue?.length > FORM_TITLE_LENGTH_WARNING ? ( - - {t( - 'features.workspace.modals.forms.create.details.name.message', - )} - - ) : null} - - - - {t('features.workspace.modals.forms.create.details.type.label')} - - - ( - - )} - rules={{ - required: t( - 'features.workspace.modals.forms.create.errors.responseMode.required', - ), - }} - /> - - {errors.responseMode?.message} - {hasMyInfoChildren && ( - - {t( - 'features.workspace.modals.forms.create.errors.noMyInfoChildrenInMrf', - )} - - )} - - {(responseModeValue === FormResponseMode.Encrypt || - responseModeValue === FormResponseMode.Email) && ( + + {isMrfCutoverEnabled ? ( + + + + ) : ( - {t( - 'features.workspace.modals.forms.create.details.notifications.label', - )} + {t('features.workspace.modals.forms.create.details.type.label')} - + + ( + + )} + rules={{ + required: t( + 'features.workspace.modals.forms.create.errors.responseMode.required', + ), + }} + /> + + + {errors.responseMode?.message} + + {hasMyInfoChildren && ( + + {t( + 'features.workspace.modals.forms.create.errors.noMyInfoChildrenInMrf', + )} + + )} )} - + {!isMrfCutoverEnabled && + (responseModeValue === FormResponseMode.Encrypt || + responseModeValue === FormResponseMode.Email) && ( + + + {t( + 'features.workspace.modals.forms.create.details.notifications.label', + )} + + + + )} + {!isMrfCutoverEnabled && } + + + + ) +} diff --git a/apps/frontend/src/features/workspace/components/CreateFormModal/CreateFormModalContent/EscapeHatchLink.tsx b/apps/frontend/src/features/workspace/components/CreateFormModal/CreateFormModalContent/EscapeHatchLink.tsx new file mode 100644 index 0000000000..d84520667c --- /dev/null +++ b/apps/frontend/src/features/workspace/components/CreateFormModal/CreateFormModalContent/EscapeHatchLink.tsx @@ -0,0 +1,29 @@ +import { Text } from '@chakra-ui/react' + +import { composeEscapeHatchCopy } from '~utils/escapeHatchCopy' +import InlineMessage from '~components/InlineMessage' +import Link from '~components/Link' + +import { useUser } from '~features/user/queries' + +interface EscapeHatchLinkProps { + onClick: () => void +} + +export const EscapeHatchLink = ({ + onClick, +}: EscapeHatchLinkProps): JSX.Element => { + const { user } = useUser() + const { prefix, linkText, suffix } = composeEscapeHatchCopy(user?.betaFlags) + return ( + + + {prefix} + + {linkText} + + {suffix} + + + ) +} diff --git a/apps/frontend/src/features/workspace/components/CreateFormModal/CreateFormModalContent/FormTitleInput.tsx b/apps/frontend/src/features/workspace/components/CreateFormModal/CreateFormModalContent/FormTitleInput.tsx new file mode 100644 index 0000000000..5ed0187d8d --- /dev/null +++ b/apps/frontend/src/features/workspace/components/CreateFormModal/CreateFormModalContent/FormTitleInput.tsx @@ -0,0 +1,62 @@ +import { RegisterOptions } from 'react-hook-form' +import { useTranslation } from 'react-i18next' +import { FormControl, FormControlProps, Skeleton } from '@chakra-ui/react' + +import { useFormTitleValidationRules } from '~utils/formValidation' +import FormErrorMessage from '~components/FormControl/FormErrorMessage' +import FormFieldMessage from '~components/FormControl/FormFieldMessage' +import FormLabel from '~components/FormControl/FormLabel' +import Input from '~components/Input' + +import { + CreateFormWizardInputProps, + useCreateFormWizard, +} from '../CreateFormWizardContext' + +/** The length of form title to start showing warning text */ +export const FORM_TITLE_LENGTH_WARNING = 65 + +interface FormTitleInputProps { + mb?: FormControlProps['mb'] +} + +export const FormTitleInput = ({ + mb = '2.25rem', +}: FormTitleInputProps): JSX.Element => { + const { t } = useTranslation() + const { formMethods, isFetching } = useCreateFormWizard() + const { + register, + formState: { errors }, + watch, + } = formMethods + + const titleInputValue = watch('title') + const formTitleValidationRules = useFormTitleValidationRules() + + return ( + + + {t('features.workspace.modals.forms.create.details.name.label')} + + + , + )} + /> + + {errors.title?.message} + {titleInputValue?.length > FORM_TITLE_LENGTH_WARNING ? ( + + {t('features.workspace.modals.forms.create.details.name.message')} + + ) : null} + + ) +} diff --git a/apps/frontend/src/features/workspace/components/CreateFormModal/CreateFormWizardContext.tsx b/apps/frontend/src/features/workspace/components/CreateFormModal/CreateFormWizardContext.tsx index 0abe037131..abcb8a6611 100644 --- a/apps/frontend/src/features/workspace/components/CreateFormModal/CreateFormWizardContext.tsx +++ b/apps/frontend/src/features/workspace/components/CreateFormModal/CreateFormWizardContext.tsx @@ -23,7 +23,7 @@ export interface CreateFormWizardInputProps { title: string responseMode: FormResponseMode // Email form props - emails: string[] + emails?: string[] // Storage form props storageAck?: boolean diff --git a/apps/frontend/src/features/workspace/components/CreateFormModal/CreateFormWizardProvider.tsx b/apps/frontend/src/features/workspace/components/CreateFormModal/CreateFormWizardProvider.tsx index c397c83c9b..4cc5726144 100644 --- a/apps/frontend/src/features/workspace/components/CreateFormModal/CreateFormWizardProvider.tsx +++ b/apps/frontend/src/features/workspace/components/CreateFormModal/CreateFormWizardProvider.tsx @@ -56,9 +56,10 @@ export const useCommonFormWizardProvider = ({ const { setValue } = formMethods + // TODO [MRF-CUTOVER]: Remove after cutover. -1 is used temporarily as there is an existing animation bug with +1. const goToStorageModeDetails = () => { setValue('responseMode', FormResponseMode.Encrypt) - setCurrentStep([CreateFormFlowStates.StorageModeDetails, 1]) + setCurrentStep([CreateFormFlowStates.StorageModeDetails, -1]) } const goToMrfDetails = () => { setValue('responseMode', FormResponseMode.Multirespondent) @@ -118,14 +119,15 @@ const useCreateFormWizardContext = ( const handleCreateStorageModeOrMultirespondentForm = handleSubmit( ({ title, responseMode, emails }) => { switch (responseMode) { - case FormResponseMode.Encrypt: + case FormResponseMode.Encrypt: { + const defaultEmails = adminEmail ? [adminEmail] : [] return createStorageModeFormMutation.mutate( { title, responseMode, publicKey: keypair.publicKey, workspaceId, - emails: emails.filter(Boolean), + emails: (emails ?? defaultEmails).filter(Boolean), }, { onSuccess: () => { @@ -133,6 +135,7 @@ const useCreateFormWizardContext = ( }, }, ) + } case FormResponseMode.Email: return case FormResponseMode.Multirespondent: @@ -197,7 +200,7 @@ const useCreateFormWizardContext = ( const handleCreateEmailModeForm = () => { return handleSubmit((inputs) => { createEmailModeFormMutation.mutate({ - emails: inputs.emails.filter(Boolean), + emails: (inputs.emails ?? []).filter(Boolean), title: inputs.title, responseMode: FormResponseMode.Email, workspaceId, diff --git a/apps/frontend/src/features/workspace/components/DuplicateFormModal/DupeFormWizardProvider.tsx b/apps/frontend/src/features/workspace/components/DuplicateFormModal/DupeFormWizardProvider.tsx index c72260d8f3..ac9418e7fb 100644 --- a/apps/frontend/src/features/workspace/components/DuplicateFormModal/DupeFormWizardProvider.tsx +++ b/apps/frontend/src/features/workspace/components/DuplicateFormModal/DupeFormWizardProvider.tsx @@ -105,7 +105,8 @@ export const useDupeFormWizardContext = ( } switch (responseMode) { - case FormResponseMode.Encrypt: + case FormResponseMode.Encrypt: { + const defaultEmails = adminEmail ? [adminEmail] : [] return dupeStorageModeFormMutation.mutate( { formIdToDuplicate: sourceFormId, @@ -113,7 +114,7 @@ export const useDupeFormWizardContext = ( responseMode, publicKey: keypair.publicKey, workspaceId, - emails: emails.filter(Boolean), + emails: (emails ?? defaultEmails).filter(Boolean), }, { onSuccess: () => { @@ -121,6 +122,7 @@ export const useDupeFormWizardContext = ( }, }, ) + } case FormResponseMode.Email: return case FormResponseMode.Multirespondent: @@ -185,7 +187,7 @@ export const useDupeFormWizardContext = ( return dupeEmailModeFormMutation.mutate({ formIdToDuplicate: sourceFormId, - emails: inputs.emails.filter(Boolean), + emails: (inputs.emails ?? []).filter(Boolean), title: inputs.title, responseMode: FormResponseMode.Email, workspaceId,