From 66661be29b1cc1bbc54469b4d53e2f8714fe9bb7 Mon Sep 17 00:00:00 2001 From: Ken Date: Thu, 19 Dec 2024 22:03:17 +0800 Subject: [PATCH 1/5] chore: remove unused export, mute fast-refresh warning --- .../components/CreateFormModal/CreateFormWizardContext.tsx | 1 + .../components/CreateFormModal/CreateFormWizardProvider.tsx | 3 ++- 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/frontend/src/features/workspace/components/CreateFormModal/CreateFormWizardContext.tsx b/frontend/src/features/workspace/components/CreateFormModal/CreateFormWizardContext.tsx index e22de0cd19..8b216a9309 100644 --- a/frontend/src/features/workspace/components/CreateFormModal/CreateFormWizardContext.tsx +++ b/frontend/src/features/workspace/components/CreateFormModal/CreateFormWizardContext.tsx @@ -1,3 +1,4 @@ +/* eslint-disable react-refresh/only-export-components */ import { createContext, useContext } from 'react' import { UseFormHandleSubmit, UseFormReturn } from 'react-hook-form' diff --git a/frontend/src/features/workspace/components/CreateFormModal/CreateFormWizardProvider.tsx b/frontend/src/features/workspace/components/CreateFormModal/CreateFormWizardProvider.tsx index 924c0ea709..904b916638 100644 --- a/frontend/src/features/workspace/components/CreateFormModal/CreateFormWizardProvider.tsx +++ b/frontend/src/features/workspace/components/CreateFormModal/CreateFormWizardProvider.tsx @@ -1,3 +1,4 @@ +/* eslint-disable react-refresh/only-export-components */ import { useMemo, useState } from 'react' import { useForm } from 'react-hook-form' @@ -15,7 +16,7 @@ import { CreateFormWizardInputProps, } from './CreateFormWizardContext' -export const INITIAL_STEP_STATE: [CreateFormFlowStates, -1 | 1 | 0] = [ +const INITIAL_STEP_STATE: [CreateFormFlowStates, -1 | 1 | 0] = [ CreateFormFlowStates.Details, -1, ] From 041081b7aa4685730a4b7a8c3c3d6ebce1d57b92 Mon Sep 17 00:00:00 2001 From: Ken Date: Fri, 20 Dec 2024 01:58:33 +0800 Subject: [PATCH 2/5] feat: add feedbackform on create wizard --- .template-env | 5 +- docker-compose.yml | 1 + .../features/public-form/PublicFormService.ts | 14 +++ frontend/src/features/public-form/queries.ts | 17 +++ .../features/workspace/WorkspaceService.ts | 78 +++++++++++- .../CreateFormDetailsScreen.tsx | 10 +- .../CreateFormModalContent.tsx | 5 + .../EmailModeFeedbackAndCreateScreen.tsx | 100 +++++++++++++++ .../FormResponseOptions.tsx | 118 +++++++++--------- .../CreateFormWizardContext.tsx | 11 +- .../CreateFormWizardProvider.tsx | 35 ++++-- frontend/src/features/workspace/mutations.ts | 16 ++- shared/types/submission.ts | 8 ++ src/app/config/config.ts | 2 + src/app/config/schema.ts | 9 ++ .../api/v3/forms/public-form.middleware.ts | 9 ++ .../api/v3/forms/public-forms.form.routes.ts | 16 +++ .../forms/public-forms.submissions.routes.ts | 13 ++ src/types/config.ts | 4 + 19 files changed, 400 insertions(+), 71 deletions(-) create mode 100644 frontend/src/features/workspace/components/CreateFormModal/CreateFormModalContent/EmailModeFeedbackAndCreateScreen.tsx create mode 100644 src/app/routes/api/v3/forms/public-form.middleware.ts diff --git a/.template-env b/.template-env index e3d4b5dd20..969a5de269 100644 --- a/.template-env +++ b/.template-env @@ -119,4 +119,7 @@ FORMSG_SDK_MODE= # AZURE_OPENAI_API_KEY= # AZURE_OPENAI_ENDPOINT= # AZURE_OPENAI_DEPLOYMENT_NAME= -# AZURE_OPENAI_API_VERSION= \ No newline at end of file +# AZURE_OPENAI_API_VERSION= + +## Kill email mode configs, provide a valid storage form id +# KILL_EMAIL_MODE_FEEDBACK_FORMID= \ No newline at end of file diff --git a/docker-compose.yml b/docker-compose.yml index ee7b758a11..3c70dde1ec 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -131,6 +131,7 @@ services: - POSTMAN_BASE_URL=https://test.postman.gov.sg/api/v2 - DOWNLOAD_FORM_WHITELIST_RATE_LIMIT - UPLOAD_FORM_WHITELIST_RATE_LIMIT + - KILL_EMAIL_MODE_FEEDBACK_FORMID=67642a79b207c811692cad51 mockpass: build: https://github.com/opengovsg/mockpass.git#v4.3.1 diff --git a/frontend/src/features/public-form/PublicFormService.ts b/frontend/src/features/public-form/PublicFormService.ts index 50aeaa73fa..1aac2e8d6a 100644 --- a/frontend/src/features/public-form/PublicFormService.ts +++ b/frontend/src/features/public-form/PublicFormService.ts @@ -63,6 +63,20 @@ export const getPublicFormView = async ( .then(transformAllIsoStringsToDate) } +/** + * TODO: (Kill Email Mode) Remove this route after kill email mode is fully implemented. + * Gets the BE defined feedback form for admins to answer why they are using email mode + * @returns Public view of form for admin email feedback form + */ +export const getAdminUseEmailModeFeedbackFormView = + async (): Promise => { + return ApiService.get( + `${PUBLIC_FORMS_ENDPOINT}/admin-use-email-feedback`, + ) + .then(({ data }) => data) + .then(transformAllIsoStringsToDate) + } + /** * Gets the redirect url for public form login * @param formId form id of form to log in. diff --git a/frontend/src/features/public-form/queries.ts b/frontend/src/features/public-form/queries.ts index ea21b436c5..30ef639747 100644 --- a/frontend/src/features/public-form/queries.ts +++ b/frontend/src/features/public-form/queries.ts @@ -7,6 +7,7 @@ import { ApiError } from '~typings/core' import { MONGODB_ID_REGEX } from '~constants/routes' import { + getAdminUseEmailModeFeedbackFormView, getMultirespondentSubmissionById, getPublicFormView, } from './PublicFormService' @@ -62,3 +63,19 @@ export const useEncryptedSubmission = ( }, ) } + +/** + * TODO: (Kill Email Mode) Remove this after kill email mode is fully implemented. + * Queries the BE defined feedback form for admins to answer why they are using email mode + * @returns + */ +export const useAdminUseEmailModeFormView = (): UseQueryResult< + PublicFormViewDto, + ApiError +> => { + return useQuery( + publicFormKeys.id('useAdminUseEmailModeFormView'), + () => getAdminUseEmailModeFeedbackFormView(), + {}, + ) +} diff --git a/frontend/src/features/workspace/WorkspaceService.ts b/frontend/src/features/workspace/WorkspaceService.ts index e4010c281b..f4de45e506 100644 --- a/frontend/src/features/workspace/WorkspaceService.ts +++ b/frontend/src/features/workspace/WorkspaceService.ts @@ -1,4 +1,10 @@ -import { AdminFeedbackDto, AdminFeedbackRating } from '~shared/types' +import { + AdminFeedbackDto, + AdminFeedbackRating, + AdminUseEmailModeFeedbackDto, + BasicField, + ErrorDto, +} from '~shared/types' import { AdminDashboardFormMetaDto, CreateEmailFormBodyDto, @@ -7,10 +13,15 @@ import { DuplicateFormBodyDto, FormDto, FormId, + PublicFormViewDto, } from '~shared/types/form/form' import { WorkspaceDto } from '~shared/types/workspace' +import { removeAt } from '~shared/utils/immutable-array-fns' import { ApiService } from '~services/ApiService' +import { CHECKBOX_OTHERS_INPUT_VALUE } from '~templates/Field/Checkbox/constants' + +import { PUBLIC_FORMS_ENDPOINT } from '~features/public-form/PublicFormService' export const ADMIN_FORM_ENDPOINT = '/admin/forms' const ADMIN_WORKSPACES_ENDPOINT = '/admin/workspaces' @@ -112,6 +123,71 @@ export const createMultirespondentModeForm = async ( ) } +const createFeedbackResponses = ( + formInputs: AdminUseEmailModeFeedbackDto, + feedbackForm: PublicFormViewDto, +) => { + // const feedbackFormFieldsStructure: [string, number][] = [['reason', 0]] + + const { fieldType, title, _id } = feedbackForm.form.form_fields[0] + + const reasonCheckboxAnswerArray = formInputs['reason'] + let answerArray: string[] = [] + if ( + reasonCheckboxAnswerArray !== undefined && + reasonCheckboxAnswerArray.value + ) { + const othersIndex = reasonCheckboxAnswerArray.value.findIndex( + (v) => v === CHECKBOX_OTHERS_INPUT_VALUE, + ) + // Others is checked, so we need to add the input at othersInput to the answer array + if (othersIndex !== -1) { + answerArray = removeAt(reasonCheckboxAnswerArray.value, othersIndex) + answerArray.push(`Others: ${reasonCheckboxAnswerArray.othersInput}`) + } else { + answerArray = reasonCheckboxAnswerArray.value + } + } + + const responses = [ + { + _id, + question: title, + answerArray, + fieldType, + }, + ] + return responses +} + +const createAdminEmailModeUseFeedback = ( + formInputs: AdminUseEmailModeFeedbackDto, + feedbackForm: PublicFormViewDto, +) => { + const responses = createFeedbackResponses(formInputs, feedbackForm) + // convert content to FormData object + const formData = new FormData() + formData.append('body', JSON.stringify({ responses, version: 2.1 })) + + return formData +} + +// TODO: (Kill Email Mode) Remove this route after kill email mode is fully implemented. +export const submitUseEmailFormFeedback = async ({ + body, + feedbackForm, +}: { + body: AdminUseEmailModeFeedbackDto + feedbackForm: PublicFormViewDto +}): Promise => { + if (!feedbackForm) return new Error('feedback form not provided') + const formData = createAdminEmailModeUseFeedback(body, feedbackForm) + return ApiService.post( + `${PUBLIC_FORMS_ENDPOINT}/submissions/storage/email-mode-feedback?captchaResponse=null`, + formData, + ).then(({ data }) => data) +} + export const dupeEmailModeForm = async ( formId: string, body: DuplicateFormBodyDto, diff --git a/frontend/src/features/workspace/components/CreateFormModal/CreateFormModalContent/CreateFormDetailsScreen.tsx b/frontend/src/features/workspace/components/CreateFormModal/CreateFormModalContent/CreateFormDetailsScreen.tsx index 16bd8005c6..bce731b80d 100644 --- a/frontend/src/features/workspace/components/CreateFormModal/CreateFormModalContent/CreateFormDetailsScreen.tsx +++ b/frontend/src/features/workspace/components/CreateFormModal/CreateFormModalContent/CreateFormDetailsScreen.tsx @@ -51,6 +51,7 @@ export const CreateFormDetailsScreen = (): JSX.Element => { const { formMethods, handleDetailsSubmit, + handleEmailFeedbackSubmit, isLoading, isFetching, modalHeader, @@ -65,6 +66,9 @@ export const CreateFormDetailsScreen = (): JSX.Element => { const titleInputValue = watch('title') const responseModeValue = watch('responseMode') + const handleEmailButtonPress = () => { + handleEmailFeedbackSubmit() + } return ( <> @@ -98,7 +102,11 @@ export const CreateFormDetailsScreen = (): JSX.Element => { control={control} render={({ field }) => ( - + )} rules={{ required: 'Please select a form response mode' }} diff --git a/frontend/src/features/workspace/components/CreateFormModal/CreateFormModalContent/CreateFormModalContent.tsx b/frontend/src/features/workspace/components/CreateFormModal/CreateFormModalContent/CreateFormModalContent.tsx index 591038f291..189ac3dddc 100644 --- a/frontend/src/features/workspace/components/CreateFormModal/CreateFormModalContent/CreateFormModalContent.tsx +++ b/frontend/src/features/workspace/components/CreateFormModal/CreateFormModalContent/CreateFormModalContent.tsx @@ -6,6 +6,7 @@ import { } from '../CreateFormWizardContext' import { CreateFormDetailsScreen } from './CreateFormDetailsScreen' +import { EmailModeFeedbackAndCreateScreen } from './EmailModeFeedbackAndCreateScreen' import { SaveSecretKeyScreen } from './SaveSecretKeyScreen' /** @@ -21,6 +22,10 @@ export const CreateFormModalContent = () => { )} {currentStep === CreateFormFlowStates.Landing && } + {/* TODO: (Kill Email Mode) Remove this route after kill email mode is fully implemented. */} + {currentStep === CreateFormFlowStates.EmailFeedback && ( + + )} ) } diff --git a/frontend/src/features/workspace/components/CreateFormModal/CreateFormModalContent/EmailModeFeedbackAndCreateScreen.tsx b/frontend/src/features/workspace/components/CreateFormModal/CreateFormModalContent/EmailModeFeedbackAndCreateScreen.tsx new file mode 100644 index 0000000000..10bdff6e1a --- /dev/null +++ b/frontend/src/features/workspace/components/CreateFormModal/CreateFormModalContent/EmailModeFeedbackAndCreateScreen.tsx @@ -0,0 +1,100 @@ +import { FormProvider } from 'react-hook-form' +import { BiRightArrowAlt } from 'react-icons/bi' +import { + Container, + FormControl, + FormErrorMessage, + Input, + ModalBody, + Text, +} from '@chakra-ui/react' + +import { BasicField } from '~shared/types' + +import { GUIDE_PREVENT_EMAIL_BOUNCE } from '~constants/links' +import { FORM_TITLE_VALIDATION_RULES } from '~utils/formValidation' +import Button from '~components/Button' +import FormLabel from '~components/FormControl/FormLabel' +import { CheckboxField, CheckboxFieldSchema } from '~templates/Field' + +import { useAdminUseEmailModeFormView } from '~features/public-form/queries' + +import { useCreateFormWizard } from '../CreateFormWizardContext' + +import { EmailFormRecipientsInput } from './EmailFormRecipientsInput' + +const CHECKBOX_FIELD_SCHEMA: CheckboxFieldSchema = { + _id: 'reason', + fieldOptions: [ + 'I need to collect Sensitive High data', + 'I need to receive attachments via email', + 'I use the JSON in Email mode responses for automations', + ], + othersRadioButton: true, + ValidationOptions: { customMax: null, customMin: null }, + validateByValue: false, + fieldType: BasicField.Checkbox, + title: 'Why are you creating an Email mode form?', + description: '', + required: true, + disabled: false, +} + +// TODO: (Kill Email Mode) Remove this route after kill email mode is fully implemented. +export const EmailModeFeedbackAndCreateScreen = (): JSX.Element => { + const { + formMethods, + + handleCreateEmailModeForm, + isLoading, + isFetching, + } = useCreateFormWizard() + const { + register, + formState: { errors }, + } = formMethods + + const { data: feedbackForm } = useAdminUseEmailModeFormView() + if (!feedbackForm) return <> + + return ( + + + + + Form name + + + {errors.title?.message} + + + + + Notifications for new responses + + + + + + + + + + + + ) +} diff --git a/frontend/src/features/workspace/components/CreateFormModal/CreateFormModalContent/FormResponseOptions.tsx b/frontend/src/features/workspace/components/CreateFormModal/CreateFormModalContent/FormResponseOptions.tsx index 7a79679cb1..b4f6377a90 100644 --- a/frontend/src/features/workspace/components/CreateFormModal/CreateFormModalContent/FormResponseOptions.tsx +++ b/frontend/src/features/workspace/components/CreateFormModal/CreateFormModalContent/FormResponseOptions.tsx @@ -1,14 +1,17 @@ import { BiLockAlt, BiMailSend } from 'react-icons/bi' -import { forwardRef, Stack, UnorderedList } from '@chakra-ui/react' +import { forwardRef, Stack, Text, UnorderedList } from '@chakra-ui/react' import { FormResponseMode } from '~shared/types/form/form' import { MultiParty } from '~assets/icons' import Badge from '~components/Badge' +import InlineMessage from '~components/InlineMessage' +import Link from '~components/Link' import Tile from '~components/Tile' export interface FormResponseOptionsProps { onChange: (option: FormResponseMode) => void + handleEmailButtonPress: () => void value: FormResponseMode isSingpass: boolean } @@ -47,64 +50,61 @@ const OptionDescription = ({ export const FormResponseOptions = forwardRef< FormResponseOptionsProps, 'button' ->(({ value, onChange, isSingpass }, ref) => { +>(({ value, onChange, isSingpass, handleEmailButtonPress }, ref) => { return ( - - onChange(FormResponseMode.Encrypt)} - flex={1} - > - Storage mode form - - View and download responses in FormSG or receive responses in your - inbox - - - - onChange(FormResponseMode.Multirespondent)} - flex={1} - isDisabled={isSingpass} - > - Multi-respondent form - - Collect responses from multiple people by adding a workflow to your - form and assigning fields to each person - - - - onChange(FormResponseMode.Email)} - flex={1} - > - Email mode form - Receive responses in your inbox only - - - + <> + + onChange(FormResponseMode.Encrypt)} + flex={1} + > + Storage mode form + + View and download responses in FormSG or receive responses in your + inbox + + + + onChange(FormResponseMode.Multirespondent)} + flex={1} + isDisabled={isSingpass} + > + Multi-respondent form + + Collect responses from multiple people by adding a workflow to your + form and assigning fields to each person + + + + + {/* TODO: (Kill Email Mode) Remove this route after kill email mode is fully implemented. */} + + + We're phasing out Email mode in the coming months. Don't worry! You + can still{' '} + handleEmailButtonPress()}>use it for now, + but we'd love to hear why. + + + ) }) diff --git a/frontend/src/features/workspace/components/CreateFormModal/CreateFormWizardContext.tsx b/frontend/src/features/workspace/components/CreateFormModal/CreateFormWizardContext.tsx index 8b216a9309..fcc3af65f4 100644 --- a/frontend/src/features/workspace/components/CreateFormModal/CreateFormWizardContext.tsx +++ b/frontend/src/features/workspace/components/CreateFormModal/CreateFormWizardContext.tsx @@ -2,13 +2,15 @@ import { createContext, useContext } from 'react' import { UseFormHandleSubmit, UseFormReturn } from 'react-hook-form' -import { FormResponseMode } from '~shared/types/form/form' +import { FormResponseMode, PublicFormViewDto } from '~shared/types/form/form' import formsgSdk from '~utils/formSdk' +import { CheckboxFieldValues } from '~templates/Field' export enum CreateFormFlowStates { Landing = 'landing', Details = 'details', + EmailFeedback = 'emailFeedback', } export type CreateFormWizardInputProps = { @@ -18,6 +20,9 @@ export type CreateFormWizardInputProps = { emails: string[] // Storage form props storageAck?: boolean + + // TODO: (Kill Email Mode) Remove this route after kill email mode is fully implemented. + reason?: CheckboxFieldValues // for kill email mode } export type CreateFormWizardContextReturn = { @@ -27,6 +32,10 @@ export type CreateFormWizardContextReturn = { handleDetailsSubmit: ReturnType< UseFormHandleSubmit > + handleEmailFeedbackSubmit: () => void + handleCreateEmailModeForm: ( + feedbackForm: PublicFormViewDto, + ) => ReturnType> handleCreateStorageModeOrMultirespondentForm: ReturnType< UseFormHandleSubmit > diff --git a/frontend/src/features/workspace/components/CreateFormModal/CreateFormWizardProvider.tsx b/frontend/src/features/workspace/components/CreateFormModal/CreateFormWizardProvider.tsx index 904b916638..98a0c4c3be 100644 --- a/frontend/src/features/workspace/components/CreateFormModal/CreateFormWizardProvider.tsx +++ b/frontend/src/features/workspace/components/CreateFormModal/CreateFormWizardProvider.tsx @@ -2,7 +2,7 @@ import { useMemo, useState } from 'react' import { useForm } from 'react-hook-form' -import { FormResponseMode } from '~shared/types' +import { FormResponseMode, PublicFormViewDto } from '~shared/types' import formsgSdk from '~utils/formSdk' @@ -58,12 +58,13 @@ const useCreateFormWizardContext = (): CreateFormWizardContextReturn => { }, }) - const { handleSubmit } = formMethods + const { handleSubmit, setValue } = formMethods const { createEmailModeFormMutation, createStorageModeFormMutation, createMultirespondentModeFormMutation, + emailModeFeedbackMutation, } = useCreateFormMutations() const { activeWorkspace, isDefaultWorkspace } = useWorkspaceContext() @@ -101,16 +102,34 @@ const useCreateFormWizardContext = (): CreateFormWizardContextReturn => { }, ) - const handleDetailsSubmit = handleSubmit((inputs) => { - if (inputs.responseMode === FormResponseMode.Email) { + // TODO: (Kill Email Mode) Remove this route after kill email mode is fully implemented. + // Collect email mode usage feedback before creating the form + const handleEmailFeedbackSubmit = () => { + // explicit set response to email as email feedback "button" interaction + // is not handled handled in FormResponseOptions + setValue('responseMode', FormResponseMode.Email) + setCurrentStep([CreateFormFlowStates.EmailFeedback, 1]) + } + + // TODO: (Kill Email Mode) Remove this route after kill email mode is fully implemented. + const handleCreateEmailModeForm = (feedbackForm: PublicFormViewDto) => + handleSubmit((inputs) => { + if (!inputs.reason) { + return new Error('Reason is required') + } + emailModeFeedbackMutation.mutate({ + body: { reason: inputs.reason }, + feedbackForm, + }) return createEmailModeFormMutation.mutate({ emails: inputs.emails.filter(Boolean), title: inputs.title, - responseMode: inputs.responseMode, + responseMode: FormResponseMode.Email, workspaceId, }) - } - // Display secret key for all other form modes + }) + + const handleDetailsSubmit = handleSubmit(() => { setCurrentStep([CreateFormFlowStates.Landing, 1]) }) @@ -125,6 +144,8 @@ const useCreateFormWizardContext = (): CreateFormWizardContextReturn => { direction, formMethods, handleDetailsSubmit, + handleCreateEmailModeForm, + handleEmailFeedbackSubmit, handleCreateStorageModeOrMultirespondentForm, isSingpass: false, modalHeader: 'Set up your form', diff --git a/frontend/src/features/workspace/mutations.ts b/frontend/src/features/workspace/mutations.ts index 206a95407e..4a55fe8327 100644 --- a/frontend/src/features/workspace/mutations.ts +++ b/frontend/src/features/workspace/mutations.ts @@ -2,7 +2,10 @@ import { useCallback } from 'react' import { useMutation, useQueryClient } from 'react-query' import { useNavigate } from 'react-router-dom' -import { AdminFeedbackRating } from '~shared/types' +import { + AdminFeedbackRating, + AdminUseEmailModeFeedbackDto, +} from '~shared/types' import { CreateEmailFormBodyDto, CreateMultirespondentFormBodyDto, @@ -10,6 +13,7 @@ import { DuplicateFormBodyDto, FormDto, FormId, + PublicFormViewDto, } from '~shared/types/form/form' import { ApiError } from '~typings/core' @@ -34,6 +38,7 @@ import { dupeStorageModeForm, moveFormsToWorkspace, removeFormsFromWorkspaces, + submitUseEmailFormFeedback, updateAdminFeedback, updateWorkspaceTitle, } from './WorkspaceService' @@ -98,10 +103,19 @@ export const useCreateFormMutations = () => { onError: handleError, }) + // TODO: (Kill Email Mode) Remove this route after kill email mode is fully implemented. + const emailModeFeedbackMutation = useMutation( + (params: { + body: AdminUseEmailModeFeedbackDto + feedbackForm: PublicFormViewDto + }) => submitUseEmailFormFeedback(params), + ) + return { createEmailModeFormMutation, createStorageModeFormMutation, createMultirespondentModeFormMutation, + emailModeFeedbackMutation, } } diff --git a/shared/types/submission.ts b/shared/types/submission.ts index aca2f5a74c..702b0d375b 100644 --- a/shared/types/submission.ts +++ b/shared/types/submission.ts @@ -296,3 +296,11 @@ export type StorageModeSubmissionContentDto = { export type PaymentSubmissionData = { paymentId: string } + +// TODO: (Kill Email Mode) Remove this route after kill email mode is fully implemented. +export type AdminUseEmailModeFeedbackDto = { + reason: { + value: string[] | false + othersInput?: string + } +} diff --git a/src/app/config/config.ts b/src/app/config/config.ts index 00dfd15985..e0454cae97 100644 --- a/src/app/config/config.ts +++ b/src/app/config/config.ts @@ -252,6 +252,8 @@ const config: Config = { adminBannerContent: basicVars.banner.adminBannerContent, rateLimitConfig: basicVars.rateLimit, reactMigration: basicVars.reactMigration, + // TODO: (Kill Email Mode) Remove this route after kill email mode is fully implemented. + killEmailMode: basicVars.killEmailMode, configureAws, secretEnv: basicVars.core.secretEnv, envSiteName: basicVars.core.envSiteName, diff --git a/src/app/config/schema.ts b/src/app/config/schema.ts index 7870dc5b48..79ff02c9b6 100644 --- a/src/app/config/schema.ts +++ b/src/app/config/schema.ts @@ -401,6 +401,15 @@ export const optionalVarsSchema: Schema = { env: 'REACT_MIGRATION_USE_FETCH_FOR_SUBMISSIONS', }, }, + // TODO: (Kill Email Mode) Remove this route after kill email mode is fully implemented. + killEmailMode: { + feedbackFormid: { + doc: 'Form ID for feedback form in kill email mode', + format: String, + default: null, + env: 'KILL_EMAIL_MODE_FEEDBACK_FORMID', + }, + }, publicApi: { apiKeyVersion: { doc: 'API key version', diff --git a/src/app/routes/api/v3/forms/public-form.middleware.ts b/src/app/routes/api/v3/forms/public-form.middleware.ts new file mode 100644 index 0000000000..4811086f02 --- /dev/null +++ b/src/app/routes/api/v3/forms/public-form.middleware.ts @@ -0,0 +1,9 @@ +import { killEmailMode } from '../../../../config/config' +import { ControllerHandler } from '../../../../modules/core/core.types' + +// TODO: (Kill Email Mode) Remove this route after kill email mode is fully implemented. +export const injectFeedbackFormUrl: ControllerHandler = (req, res, next) => { + const formId = killEmailMode.feedbackFormid + req.params = { formId: formId } + return next() +} diff --git a/src/app/routes/api/v3/forms/public-forms.form.routes.ts b/src/app/routes/api/v3/forms/public-forms.form.routes.ts index b72c967a0b..5563880fd3 100644 --- a/src/app/routes/api/v3/forms/public-forms.form.routes.ts +++ b/src/app/routes/api/v3/forms/public-forms.form.routes.ts @@ -3,6 +3,8 @@ import { Router } from 'express' import * as PublicFormController from '../../../../modules/form/public-form/public-form.controller' import * as WogaaController from '../../../../modules/wogaa/wogaa.controller' +import { injectFeedbackFormUrl } from './public-form.middleware' + export const PublicFormsFormRouter = Router() /** @@ -37,3 +39,17 @@ PublicFormsFormRouter.route('/:formId([a-fA-F0-9]{24})').get( PublicFormsFormRouter.route('/:formId([a-fA-F0-9]{24})/sample-submission').get( PublicFormController.handleGetPublicFormSampleSubmission, ) +/** + * TODO: (Kill Email Mode) Remove this route after kill email mode is fully implemented. + * + * @route GET /admin-use-email-feedback + * + * @returns 200 with form when form exists and is public + * @returns 404 when form is private or form with given ID does not exist + * @returns 410 when form is archived + * @returns 500 when database error occurs + */ +PublicFormsFormRouter.route('/admin-use-email-feedback').get( + injectFeedbackFormUrl, + PublicFormController.handleGetPublicForm, +) diff --git a/src/app/routes/api/v3/forms/public-forms.submissions.routes.ts b/src/app/routes/api/v3/forms/public-forms.submissions.routes.ts index e53836ac2d..e34aeeac56 100644 --- a/src/app/routes/api/v3/forms/public-forms.submissions.routes.ts +++ b/src/app/routes/api/v3/forms/public-forms.submissions.routes.ts @@ -8,6 +8,8 @@ import * as SubmissionController from '../../../../modules/submission/submission import * as WogaaController from '../../../../modules/wogaa/wogaa.controller' import { limitRate } from '../../../../utils/limit-rate' +import { injectFeedbackFormUrl } from './public-form.middleware' + export const PublicFormsSubmissionsRouter = Router() /** @@ -51,6 +53,17 @@ PublicFormsSubmissionsRouter.route( EncryptSubmissionController.handleStorageSubmission, ) +/** + * TODO: (Kill Email Mode) Remove this after kill email mode is fully implemented. + */ +PublicFormsSubmissionsRouter.route( + '/submissions/storage/email-mode-feedback', +).post( + limitRate({ max: rateLimitConfig.submissions }), + injectFeedbackFormUrl, + EncryptSubmissionController.handleStorageSubmission, +) + /** * Submit a form response before public key encryption, performs pre-encryption * steps (e.g. field validation, virus scanning) and stores the encrypted contents. diff --git a/src/types/config.ts b/src/types/config.ts index 5e6d9795ee..8558368d5c 100644 --- a/src/types/config.ts +++ b/src/types/config.ts @@ -105,6 +105,7 @@ export type Config = { adminBannerContent: string rateLimitConfig: RateLimitConfig reactMigration: ReactMigrationConfig + killEmailMode: IOptionalVarsSchema['killEmailMode'] secretEnv: string envSiteName: string publicApiConfig: PublicApiConfig @@ -204,6 +205,9 @@ export interface IOptionalVarsSchema { // TODO (#5826): Toggle to use fetch for submissions instead of axios. Remove once network error is resolved useFetchForSubmissions: boolean } + killEmailMode: { + feedbackFormid: string + } publicApi: { apiKeyVersion: string } From b4b56d1083a3ec8e9caec2245c9eaceee1aab9cb Mon Sep 17 00:00:00 2001 From: Ken Date: Fri, 20 Dec 2024 02:29:09 +0800 Subject: [PATCH 3/5] fix: add feedback mutation to DupeForm and UseTemplate wizard --- .../UseTemplateWizardProvider.tsx | 36 +++++++++++++++- .../CreateFormWizardProvider.tsx | 8 +++- .../DupeFormWizardProvider.tsx | 42 +++++++++++++++---- frontend/src/features/workspace/mutations.ts | 21 ++++++---- 4 files changed, 86 insertions(+), 21 deletions(-) diff --git a/frontend/src/features/admin-form/template/UseTemplateModal/UseTemplateWizardProvider.tsx b/frontend/src/features/admin-form/template/UseTemplateModal/UseTemplateWizardProvider.tsx index 8a0a9779da..9c12d40a4b 100644 --- a/frontend/src/features/admin-form/template/UseTemplateModal/UseTemplateWizardProvider.tsx +++ b/frontend/src/features/admin-form/template/UseTemplateModal/UseTemplateWizardProvider.tsx @@ -1,6 +1,6 @@ import { useEffect } from 'react' -import { FormResponseMode } from '~shared/types' +import { FormResponseMode, PublicFormViewDto } from '~shared/types' import { useFormTemplate } from '~features/admin-form/common/queries' import { @@ -9,6 +9,7 @@ import { CreateFormWizardContextReturn, } from '~features/workspace/components/CreateFormModal/CreateFormWizardContext' import { useCommonFormWizardProvider } from '~features/workspace/components/CreateFormModal/CreateFormWizardProvider' +import { useEmailModeFeedbackMutation } from '~features/workspace/mutations' import { useUseTemplateMutations } from '../mutation' @@ -41,7 +42,7 @@ export const useUseTemplateWizardContext = ( }) }, [reset, getValues, isTemplateFormLoading, templateFormData?.form.title]) - const { handleSubmit } = formMethods + const { handleSubmit, setValue } = formMethods const { useEmailModeFormTemplateMutation, @@ -49,6 +50,8 @@ export const useUseTemplateWizardContext = ( useMultirespondentFormTemplateMutation, } = useUseTemplateMutations() + const { emailModeFeedbackMutation } = useEmailModeFeedbackMutation() + const handleCreateStorageModeOrMultirespondentForm = handleSubmit( ({ title, responseMode }) => { if (!formId) return @@ -81,6 +84,33 @@ export const useUseTemplateWizardContext = ( }, ) + // TODO: (Kill Email Mode) Remove this route after kill email mode is fully implemented. + // Collect email mode usage feedback before creating the form + const handleEmailFeedbackSubmit = () => { + // explicit set response to email as email feedback "button" interaction + // is not handled handled in FormResponseOptions + setValue('responseMode', FormResponseMode.Email) + setCurrentStep([CreateFormFlowStates.EmailFeedback, 1]) + } + + // TODO: (Kill Email Mode) Remove this route after kill email mode is fully implemented. + const handleCreateEmailModeForm = (feedbackForm: PublicFormViewDto) => + handleSubmit((inputs) => { + if (!inputs.reason) { + return new Error('Reason is required') + } + emailModeFeedbackMutation.mutate({ + body: { reason: inputs.reason }, + feedbackForm, + }) + return useEmailModeFormTemplateMutation.mutate({ + formIdToDuplicate: formId, + emails: inputs.emails.filter(Boolean), + title: inputs.title, + responseMode: FormResponseMode.Email, + }) + }) + const handleDetailsSubmit = handleSubmit((inputs) => { if (!formId) return if (inputs.responseMode === FormResponseMode.Email) { @@ -106,6 +136,8 @@ export const useUseTemplateWizardContext = ( formMethods, handleDetailsSubmit, handleCreateStorageModeOrMultirespondentForm, + handleEmailFeedbackSubmit, + handleCreateEmailModeForm, isSingpass, modalHeader: 'Duplicate form', } diff --git a/frontend/src/features/workspace/components/CreateFormModal/CreateFormWizardProvider.tsx b/frontend/src/features/workspace/components/CreateFormModal/CreateFormWizardProvider.tsx index 98a0c4c3be..be33bc8da7 100644 --- a/frontend/src/features/workspace/components/CreateFormModal/CreateFormWizardProvider.tsx +++ b/frontend/src/features/workspace/components/CreateFormModal/CreateFormWizardProvider.tsx @@ -6,7 +6,10 @@ import { FormResponseMode, PublicFormViewDto } from '~shared/types' import formsgSdk from '~utils/formSdk' -import { useCreateFormMutations } from '~features/workspace/mutations' +import { + useCreateFormMutations, + useEmailModeFeedbackMutation, +} from '~features/workspace/mutations' import { useWorkspaceContext } from '~features/workspace/WorkspaceContext' import { @@ -64,9 +67,10 @@ const useCreateFormWizardContext = (): CreateFormWizardContextReturn => { createEmailModeFormMutation, createStorageModeFormMutation, createMultirespondentModeFormMutation, - emailModeFeedbackMutation, } = useCreateFormMutations() + const { emailModeFeedbackMutation } = useEmailModeFeedbackMutation() + const { activeWorkspace, isDefaultWorkspace } = useWorkspaceContext() // do not mutate with workspaceId if it is 'All Forms' (default workspace) diff --git a/frontend/src/features/workspace/components/DuplicateFormModal/DupeFormWizardProvider.tsx b/frontend/src/features/workspace/components/DuplicateFormModal/DupeFormWizardProvider.tsx index 485c704e0a..1503fcd077 100644 --- a/frontend/src/features/workspace/components/DuplicateFormModal/DupeFormWizardProvider.tsx +++ b/frontend/src/features/workspace/components/DuplicateFormModal/DupeFormWizardProvider.tsx @@ -1,9 +1,12 @@ import { useEffect } from 'react' -import { FormResponseMode } from '~shared/types' +import { FormResponseMode, PublicFormViewDto } from '~shared/types' import { usePreviewForm } from '~features/admin-form/common/queries' -import { useDuplicateFormMutations } from '~features/workspace/mutations' +import { + useDuplicateFormMutations, + useEmailModeFeedbackMutation, +} from '~features/workspace/mutations' import { useDashboard } from '~features/workspace/queries' import { makeDuplicateFormTitle } from '~features/workspace/utils/createDuplicateFormTitle' import { useWorkspaceContext } from '~features/workspace/WorkspaceContext' @@ -57,7 +60,7 @@ export const useDupeFormWizardContext = (): CreateFormWizardContextReturn => { dashboardForms, ]) - const { handleSubmit } = formMethods + const { handleSubmit, setValue } = formMethods const { dupeEmailModeFormMutation, @@ -65,6 +68,8 @@ export const useDupeFormWizardContext = (): CreateFormWizardContextReturn => { dupeMultirespondentModeFormMutation, } = useDuplicateFormMutations() + const { emailModeFeedbackMutation } = useEmailModeFeedbackMutation() + const { activeWorkspace, isDefaultWorkspace } = useWorkspaceContext() // do not mutate with workspaceId if it is 'All Forms' (default workspace) @@ -105,17 +110,36 @@ export const useDupeFormWizardContext = (): CreateFormWizardContextReturn => { }, ) - const handleDetailsSubmit = handleSubmit((inputs) => { - if (!activeFormMeta?._id) return - if (inputs.responseMode === FormResponseMode.Email) { + // TODO: (Kill Email Mode) Remove this route after kill email mode is fully implemented. + // Collect email mode usage feedback before creating the form + const handleEmailFeedbackSubmit = () => { + // explicit set response to email as email feedback "button" interaction + // is not handled handled in FormResponseOptions + setValue('responseMode', FormResponseMode.Email) + setCurrentStep([CreateFormFlowStates.EmailFeedback, 1]) + } + + // TODO: (Kill Email Mode) Remove this route after kill email mode is fully implemented. + const handleCreateEmailModeForm = (feedbackForm: PublicFormViewDto) => + handleSubmit((inputs) => { + if (!activeFormMeta?._id) return + if (!inputs.reason) { + return new Error('Reason is required') + } + emailModeFeedbackMutation.mutate({ + body: { reason: inputs.reason }, + feedbackForm, + }) return dupeEmailModeFormMutation.mutate({ formIdToDuplicate: activeFormMeta._id, emails: inputs.emails.filter(Boolean), title: inputs.title, - responseMode: inputs.responseMode, + responseMode: FormResponseMode.Email, workspaceId, }) - } + }) + + const handleDetailsSubmit = handleSubmit(() => { setCurrentStep([CreateFormFlowStates.Landing, 1]) }) @@ -131,6 +155,8 @@ export const useDupeFormWizardContext = (): CreateFormWizardContextReturn => { formMethods, handleDetailsSubmit, handleCreateStorageModeOrMultirespondentForm, + handleEmailFeedbackSubmit, + handleCreateEmailModeForm, isSingpass, modalHeader: 'Duplicate form', } diff --git a/frontend/src/features/workspace/mutations.ts b/frontend/src/features/workspace/mutations.ts index 4a55fe8327..f23d16dbc7 100644 --- a/frontend/src/features/workspace/mutations.ts +++ b/frontend/src/features/workspace/mutations.ts @@ -103,19 +103,10 @@ export const useCreateFormMutations = () => { onError: handleError, }) - // TODO: (Kill Email Mode) Remove this route after kill email mode is fully implemented. - const emailModeFeedbackMutation = useMutation( - (params: { - body: AdminUseEmailModeFeedbackDto - feedbackForm: PublicFormViewDto - }) => submitUseEmailFormFeedback(params), - ) - return { createEmailModeFormMutation, createStorageModeFormMutation, createMultirespondentModeFormMutation, - emailModeFeedbackMutation, } } @@ -297,3 +288,15 @@ export const useWorkspaceMutations = () => { removeFormFromWorkspacesMutation, } } + +// TODO: (Kill Email Mode) Remove this route after kill email mode is fully implemented. +export const useEmailModeFeedbackMutation = () => { + const emailModeFeedbackMutation = useMutation( + (params: { + body: AdminUseEmailModeFeedbackDto + feedbackForm: PublicFormViewDto + }) => submitUseEmailFormFeedback(params), + ) + + return { emailModeFeedbackMutation } +} From 3621c805a4a6480d0b322b47f9f51187bfeb448a Mon Sep 17 00:00:00 2001 From: Ken Date: Fri, 20 Dec 2024 02:29:30 +0800 Subject: [PATCH 4/5] feat: log user session during feedback form injection --- .../api/v3/forms/public-form.middleware.ts | 27 ++++++++++++++++++- .../api/v3/forms/public-forms.form.routes.ts | 4 +-- .../forms/public-forms.submissions.routes.ts | 4 +-- 3 files changed, 30 insertions(+), 5 deletions(-) diff --git a/src/app/routes/api/v3/forms/public-form.middleware.ts b/src/app/routes/api/v3/forms/public-form.middleware.ts index 4811086f02..8cf77c2b89 100644 --- a/src/app/routes/api/v3/forms/public-form.middleware.ts +++ b/src/app/routes/api/v3/forms/public-form.middleware.ts @@ -1,9 +1,34 @@ +import { AuthedSessionData } from 'express-session' + import { killEmailMode } from '../../../../config/config' +import { createLoggerWithLabel } from '../../../../config/logger' import { ControllerHandler } from '../../../../modules/core/core.types' +import { createReqMeta } from '../../../../utils/request' + +const logger = createLoggerWithLabel(module) // TODO: (Kill Email Mode) Remove this route after kill email mode is fully implemented. -export const injectFeedbackFormUrl: ControllerHandler = (req, res, next) => { +export const authAndInjectFeedbackFormUrl: ControllerHandler = ( + req, + res, + next, +) => { const formId = killEmailMode.feedbackFormid req.params = { formId: formId } + const sessionUserId = (req.session as AuthedSessionData).user._id + + const logMeta = { + action: 'authAndInjectFeedbackFormUrl', + formId, + method: req.method, + sessionUserId, + ...createReqMeta(req), + } + + logger.info({ + message: 'Feedback form injected', + meta: logMeta, + }) + return next() } diff --git a/src/app/routes/api/v3/forms/public-forms.form.routes.ts b/src/app/routes/api/v3/forms/public-forms.form.routes.ts index 5563880fd3..b27d794bc2 100644 --- a/src/app/routes/api/v3/forms/public-forms.form.routes.ts +++ b/src/app/routes/api/v3/forms/public-forms.form.routes.ts @@ -3,7 +3,7 @@ import { Router } from 'express' import * as PublicFormController from '../../../../modules/form/public-form/public-form.controller' import * as WogaaController from '../../../../modules/wogaa/wogaa.controller' -import { injectFeedbackFormUrl } from './public-form.middleware' +import { authAndInjectFeedbackFormUrl } from './public-form.middleware' export const PublicFormsFormRouter = Router() @@ -50,6 +50,6 @@ PublicFormsFormRouter.route('/:formId([a-fA-F0-9]{24})/sample-submission').get( * @returns 500 when database error occurs */ PublicFormsFormRouter.route('/admin-use-email-feedback').get( - injectFeedbackFormUrl, + authAndInjectFeedbackFormUrl, PublicFormController.handleGetPublicForm, ) diff --git a/src/app/routes/api/v3/forms/public-forms.submissions.routes.ts b/src/app/routes/api/v3/forms/public-forms.submissions.routes.ts index e34aeeac56..35bddf5333 100644 --- a/src/app/routes/api/v3/forms/public-forms.submissions.routes.ts +++ b/src/app/routes/api/v3/forms/public-forms.submissions.routes.ts @@ -8,7 +8,7 @@ import * as SubmissionController from '../../../../modules/submission/submission import * as WogaaController from '../../../../modules/wogaa/wogaa.controller' import { limitRate } from '../../../../utils/limit-rate' -import { injectFeedbackFormUrl } from './public-form.middleware' +import { authAndInjectFeedbackFormUrl } from './public-form.middleware' export const PublicFormsSubmissionsRouter = Router() @@ -60,7 +60,7 @@ PublicFormsSubmissionsRouter.route( '/submissions/storage/email-mode-feedback', ).post( limitRate({ max: rateLimitConfig.submissions }), - injectFeedbackFormUrl, + authAndInjectFeedbackFormUrl, EncryptSubmissionController.handleStorageSubmission, ) From c52f1285cd27eeaf5fac3170d7109b2ca7271fbf Mon Sep 17 00:00:00 2001 From: Ken Date: Fri, 20 Dec 2024 02:40:22 +0800 Subject: [PATCH 5/5] chore: fix typo, adds failsafe on missing formid --- __tests__/setup/.test-env | 3 ++- src/app/config/schema.ts | 6 ++++-- src/app/routes/api/v3/forms/public-form.middleware.ts | 2 +- src/types/config.ts | 2 +- 4 files changed, 8 insertions(+), 5 deletions(-) diff --git a/__tests__/setup/.test-env b/__tests__/setup/.test-env index 552f85f4b1..11da47d5fa 100644 --- a/__tests__/setup/.test-env +++ b/__tests__/setup/.test-env @@ -93,4 +93,5 @@ POSTMAN_MOP_CAMPAIGN_ID=campaign_tesst POSTMAN_MOP_CAMPAIGN_API_KEY=key_test_123 POSTMAN_INTERNAL_CAMPAIGN_ID=campaign_tesst POSTMAN_INTERNAL_CAMPAIGN_API_KEY=key_test_123 -POSTMAN_BASE_URL=https://test.postman.gov.sg/api/v2 \ No newline at end of file +POSTMAN_BASE_URL=https://test.postman.gov.sg/api/v2 +KILL_EMAIL_MODE_FEEDBACK_FORMID=123123132 \ No newline at end of file diff --git a/src/app/config/schema.ts b/src/app/config/schema.ts index 79ff02c9b6..edad77728a 100644 --- a/src/app/config/schema.ts +++ b/src/app/config/schema.ts @@ -403,10 +403,12 @@ export const optionalVarsSchema: Schema = { }, // TODO: (Kill Email Mode) Remove this route after kill email mode is fully implemented. killEmailMode: { - feedbackFormid: { + feedbackFormId: { doc: 'Form ID for feedback form in kill email mode', format: String, - default: null, + // this form id is not part of the critical path, + // thus we don't want to crash the system even if this is not available + default: '', env: 'KILL_EMAIL_MODE_FEEDBACK_FORMID', }, }, diff --git a/src/app/routes/api/v3/forms/public-form.middleware.ts b/src/app/routes/api/v3/forms/public-form.middleware.ts index 8cf77c2b89..c4ca8a2427 100644 --- a/src/app/routes/api/v3/forms/public-form.middleware.ts +++ b/src/app/routes/api/v3/forms/public-form.middleware.ts @@ -13,7 +13,7 @@ export const authAndInjectFeedbackFormUrl: ControllerHandler = ( res, next, ) => { - const formId = killEmailMode.feedbackFormid + const formId = killEmailMode.feedbackFormId req.params = { formId: formId } const sessionUserId = (req.session as AuthedSessionData).user._id diff --git a/src/types/config.ts b/src/types/config.ts index 8558368d5c..5871628d7e 100644 --- a/src/types/config.ts +++ b/src/types/config.ts @@ -206,7 +206,7 @@ export interface IOptionalVarsSchema { useFetchForSubmissions: boolean } killEmailMode: { - feedbackFormid: string + feedbackFormId: string } publicApi: { apiKeyVersion: string