-
-
- {content.warningCalloutContent.headingLabel}
-
-
+
+
+
+
+ {filterLetters && (
+
+
+
+ {content.warningCalloutContent.headingLabel}
+
+
+
-
- )}
-
+ )}
+
+
>
);
diff --git a/frontend/src/components/forms/ChooseTemplateType/form-action.ts b/frontend/src/components/forms/ChooseTemplateType/form-action.ts
new file mode 100644
index 0000000000..a4c1ca1b72
--- /dev/null
+++ b/frontend/src/components/forms/ChooseTemplateType/form-action.ts
@@ -0,0 +1,37 @@
+import {
+ createTemplateUrl,
+ legacyTemplateCreationPages,
+} from 'nhs-notify-web-template-management-utils';
+import { z } from 'zod';
+import { $ChooseTemplateType } from './schemas';
+import { NHSNotifyClientSideFormSubmitHandler } from '@atoms/NHSNotifyForm/Form';
+
+export const handleSubmit: NHSNotifyClientSideFormSubmitHandler =
+ (router, [, setState]) =>
+ async (event: React.SubmitEvent) => {
+ event.preventDefault();
+
+ const formData = new FormData(event.target);
+
+ const parsedForm = $ChooseTemplateType.safeParse(
+ Object.fromEntries(formData.entries())
+ );
+
+ if (!parsedForm.success) {
+ setState({
+ errorState: z.flattenError(parsedForm.error),
+ });
+
+ return;
+ }
+
+ if (parsedForm.data.letterAuthoringEnabled) {
+ const { templateType, letterType } = parsedForm.data;
+
+ router.push(createTemplateUrl(templateType, letterType));
+ } else {
+ const { templateType } = parsedForm.data;
+
+ router.push(legacyTemplateCreationPages(templateType));
+ }
+ };
diff --git a/frontend/src/components/forms/ChooseTemplateType/schemas.ts b/frontend/src/components/forms/ChooseTemplateType/schemas.ts
index 163af92102..86bf73bf38 100644
--- a/frontend/src/components/forms/ChooseTemplateType/schemas.ts
+++ b/frontend/src/components/forms/ChooseTemplateType/schemas.ts
@@ -8,14 +8,11 @@ const {
letterType: { error: letterTypeError },
} = content.components.chooseTemplateType.form;
-export const $ChooseTemplateTypeBase = z.object({
- templateType: z.enum(TEMPLATE_TYPE_LIST, {
- message: templateTypeError,
- }),
-});
-
-export const $ChooseTemplateTypeWithLetterAuthoring = z
+export const $ChooseTemplateType = z
.object({
+ letterAuthoringEnabled: z
+ .enum(['1', '0'])
+ .transform((letterAuthoringEnabled) => letterAuthoringEnabled === '1'),
templateType: z.enum(TEMPLATE_TYPE_LIST, {
message: templateTypeError,
}),
@@ -26,7 +23,11 @@ export const $ChooseTemplateTypeWithLetterAuthoring = z
.optional(),
})
.superRefine((data, ctx) => {
- if (data.templateType === 'LETTER' && !data.letterType) {
+ if (
+ data.templateType === 'LETTER' &&
+ data.letterAuthoringEnabled &&
+ !data.letterType
+ ) {
ctx.addIssue({
code: 'custom',
path: ['letterType'],
diff --git a/frontend/src/components/forms/ChooseTemplateType/server-action.ts b/frontend/src/components/forms/ChooseTemplateType/server-action.ts
deleted file mode 100644
index c857ef45d3..0000000000
--- a/frontend/src/components/forms/ChooseTemplateType/server-action.ts
+++ /dev/null
@@ -1,51 +0,0 @@
-'use server';
-
-import { redirect, RedirectType } from 'next/navigation';
-import {
- createTemplateUrl,
- legacyTemplateCreationPages,
-} from 'nhs-notify-web-template-management-utils';
-import { z } from 'zod';
-import { serverIsFeatureEnabled } from '@utils/server-features';
-import {
- $ChooseTemplateTypeBase,
- $ChooseTemplateTypeWithLetterAuthoring,
-} from './schemas';
-import { FormState } from '@utils/types';
-
-export async function chooseTemplateTypeAction(
- _: FormState,
- formData: FormData
-): Promise
{
- const hasLetterAuthoring = await serverIsFeatureEnabled('letterAuthoring');
-
- if (hasLetterAuthoring) {
- const parsedForm = $ChooseTemplateTypeWithLetterAuthoring.safeParse(
- Object.fromEntries(formData.entries())
- );
-
- if (!parsedForm.success) {
- return {
- errorState: z.flattenError(parsedForm.error),
- };
- }
-
- const { templateType, letterType } = parsedForm.data;
-
- redirect(createTemplateUrl(templateType, letterType), RedirectType.push);
- } else {
- const parsedForm = $ChooseTemplateTypeBase.safeParse(
- Object.fromEntries(formData.entries())
- );
-
- if (!parsedForm.success) {
- return {
- errorState: z.flattenError(parsedForm.error),
- };
- }
-
- const { templateType } = parsedForm.data;
-
- redirect(legacyTemplateCreationPages(templateType), RedirectType.push);
- }
-}
diff --git a/frontend/src/components/forms/ChooseTemplates/MovetoProduction.tsx b/frontend/src/components/forms/ChooseTemplates/MovetoProduction.tsx
index 4c8e4af2f2..d0f0ae5a4e 100644
--- a/frontend/src/components/forms/ChooseTemplates/MovetoProduction.tsx
+++ b/frontend/src/components/forms/ChooseTemplates/MovetoProduction.tsx
@@ -3,7 +3,7 @@
import Link from 'next/link';
import { NHSNotifyButton } from '@atoms/NHSNotifyButton/NHSNotifyButton';
import { NHSNotifyFormWrapper } from '@molecules/NHSNotifyFormWrapper/NHSNotifyFormWrapper';
-import { useNHSNotifyForm } from '@providers/form-provider';
+import { useNHSNotifyServerActionForm } from '@providers/form-provider';
import copy from '@content/content';
const content = copy.pages.editMessagePlan;
@@ -13,7 +13,7 @@ export function EditMessagePlanMoveToProductionForm({
}: {
messagePlanId: string;
}) {
- const [, action] = useNHSNotifyForm();
+ const [, action] = useNHSNotifyServerActionForm();
return (
diff --git a/frontend/src/components/forms/CopyTemplate/CopyTemplate.tsx b/frontend/src/components/forms/CopyTemplate/CopyTemplate.tsx
index 1527823bbd..2d38457652 100644
--- a/frontend/src/components/forms/CopyTemplate/CopyTemplate.tsx
+++ b/frontend/src/components/forms/CopyTemplate/CopyTemplate.tsx
@@ -1,20 +1,18 @@
'use client';
-import { useActionState, useState } from 'react';
import { NHSNotifyRadioButtonForm } from '@molecules/NHSNotifyRadioButtonForm/NHSNotifyRadioButtonForm';
-import { NhsNotifyErrorSummary } from '@molecules/NhsNotifyErrorSummary/NhsNotifyErrorSummary';
import content from '@content/content';
import { templateTypeDisplayMappings } from 'nhs-notify-web-template-management-utils';
import { NHSNotifyMain } from '@atoms/NHSNotifyMain/NHSNotifyMain';
-import { $CopyTemplate, copyTemplateAction } from './server-action';
+import { handleSubmit } from './form-action';
import type {
TemplateDto,
TemplateType,
} from 'nhs-notify-web-template-management-types';
-import { validate } from '@utils/client-validate-form';
import Link from 'next/link';
import NotifyBackLink from '@atoms/NHSNotifyBackLink/NHSNotifyBackLink';
-import { ErrorState } from '@utils/types';
+import { NHSNotifyClientSideFormProvider } from '@providers/form-provider';
+import * as NHSNotifyForm from '@atoms/NHSNotifyForm';
export type ValidCopyType = Exclude;
@@ -25,14 +23,6 @@ type CopyTemplate = {
export const CopyTemplate = ({ template }: CopyTemplate) => {
const copyTypes = ['NHS_APP', 'EMAIL', 'SMS'] as const;
- const [state, action] = useActionState(copyTemplateAction, { template });
-
- const [errorState, setErrorState] = useState(
- state.errorState
- );
-
- const formValidate = validate($CopyTemplate, setErrorState);
-
const options = copyTypes.map((templateType) => ({
id: templateType,
text: templateTypeDisplayMappings(templateType),
@@ -49,27 +39,54 @@ export const CopyTemplate = ({ template }: CopyTemplate) => {
{backLinkText}
-
-
-
-
{fullPageHeading}
-
+
+
+
+
+
{fullPageHeading}
+
+
+
+
+
+
+
-
+
>
);
diff --git a/frontend/src/components/forms/CopyTemplate/form-action.ts b/frontend/src/components/forms/CopyTemplate/form-action.ts
new file mode 100644
index 0000000000..ddca0d0f48
--- /dev/null
+++ b/frontend/src/components/forms/CopyTemplate/form-action.ts
@@ -0,0 +1,68 @@
+import { z } from 'zod';
+import { createTemplate } from '@utils/form-actions';
+import { format } from 'date-fns/format';
+import { TEMPLATE_TYPE_LIST } from 'nhs-notify-backend-client/schemas';
+import content from '@content/content';
+import { NHSNotifyClientSideFormSubmitHandler } from '@atoms/NHSNotifyForm/Form';
+
+export const $CopyTemplate = z.object({
+ templateType: z.enum(TEMPLATE_TYPE_LIST, {
+ message: content.components.copyTemplate.form.templateType.error,
+ }),
+ originalTemplateType: z.enum(TEMPLATE_TYPE_LIST),
+ name: z.string(),
+ subject: z.string(),
+ message: z.string(),
+});
+
+export const handleSubmit: NHSNotifyClientSideFormSubmitHandler =
+ (router, [, setState]) =>
+ async (event: React.SubmitEvent) => {
+ event.preventDefault();
+
+ const formData = new FormData(event.target);
+ const parsedForm = $CopyTemplate.safeParse(
+ Object.fromEntries(formData.entries())
+ );
+
+ if (!parsedForm.success) {
+ setState({
+ errorState: z.flattenError(parsedForm.error),
+ });
+
+ return;
+ }
+
+ const { templateType, originalTemplateType, name, subject, message } =
+ parsedForm.data;
+ const newSubject =
+ originalTemplateType === 'EMAIL' ? subject : 'Enter a subject line';
+
+ const copyName = `COPY (${format(new Date(), 'yyyy-MM-dd HH:mm:ss')}): ${name}`;
+
+ switch (templateType) {
+ case 'NHS_APP':
+ case 'SMS': {
+ await createTemplate({
+ name: copyName,
+ message,
+ templateType,
+ });
+
+ break;
+ }
+ case 'EMAIL': {
+ await createTemplate({
+ name: copyName,
+ message,
+ templateType,
+ subject: newSubject,
+ });
+
+ break;
+ }
+ // no default
+ }
+
+ router.push('/message-templates');
+ };
diff --git a/frontend/src/components/forms/CopyTemplate/server-action.ts b/frontend/src/components/forms/CopyTemplate/server-action.ts
deleted file mode 100644
index a0c4a62d2a..0000000000
--- a/frontend/src/components/forms/CopyTemplate/server-action.ts
+++ /dev/null
@@ -1,78 +0,0 @@
-import { redirect, RedirectType } from 'next/navigation';
-import type { FormState } from '@utils/types';
-import { z } from 'zod';
-import { createTemplate } from '@utils/form-actions';
-import { format } from 'date-fns/format';
-import { TEMPLATE_TYPE_LIST } from 'nhs-notify-backend-client/schemas';
-import type {
- TemplateDto,
- TemplateType,
-} from 'nhs-notify-web-template-management-types';
-import content from '@content/content';
-
-export const $CopyTemplate = z.object({
- templateType: z.enum(TEMPLATE_TYPE_LIST, {
- message: content.components.copyTemplate.form.templateType.error,
- }),
-});
-
-type CopyTemplateActionState = FormState & {
- template: TemplateDto & {
- templateType: Exclude
;
- };
-};
-type CopyTemplateAction = (
- formState: CopyTemplateActionState,
- formData: FormData
-) => Promise;
-
-export const copyTemplateAction: CopyTemplateAction = async (
- formState,
- formData
-) => {
- const parsedForm = $CopyTemplate.safeParse(
- Object.fromEntries(formData.entries())
- );
-
- if (!parsedForm.success) {
- return {
- ...formState,
- errorState: z.flattenError(parsedForm.error),
- };
- }
-
- const newTemplateType = parsedForm.data.templateType;
- const { name, message } = formState.template;
- const subject =
- formState.template.templateType === 'EMAIL'
- ? formState.template.subject
- : 'Enter a subject line';
-
- const copyName = `COPY (${format(new Date(), 'yyyy-MM-dd HH:mm:ss')}): ${name}`;
-
- switch (newTemplateType) {
- case 'NHS_APP':
- case 'SMS': {
- await createTemplate({
- name: copyName,
- message,
- templateType: newTemplateType,
- });
-
- break;
- }
- case 'EMAIL': {
- await createTemplate({
- name: copyName,
- message,
- templateType: newTemplateType,
- subject,
- });
-
- break;
- }
- // no default
- }
-
- return redirect(`/message-templates`, RedirectType.push);
-};
diff --git a/frontend/src/components/forms/MessagePlanForm/MessagePlanForm.tsx b/frontend/src/components/forms/MessagePlanForm/MessagePlanForm.tsx
index f6abf2d855..8eae186fd3 100644
--- a/frontend/src/components/forms/MessagePlanForm/MessagePlanForm.tsx
+++ b/frontend/src/components/forms/MessagePlanForm/MessagePlanForm.tsx
@@ -6,7 +6,7 @@ import content from '@content/content';
import { useTextInput } from '@hooks/use-text-input.hook';
import { ContentRenderer } from '@molecules/ContentRenderer/ContentRenderer';
import { NHSNotifyFormWrapper } from '@molecules/NHSNotifyFormWrapper/NHSNotifyFormWrapper';
-import { useNHSNotifyForm } from '@providers/form-provider';
+import { useNHSNotifyServerActionForm } from '@providers/form-provider';
import classNames from 'classnames';
import Link from 'next/link';
import type { RoutingConfig } from 'nhs-notify-web-template-management-types';
@@ -31,7 +31,7 @@ export function MessagePlanForm({
backLink: { href: string; text: string };
initialState?: Pick;
}>) {
- const [state, action] = useNHSNotifyForm();
+ const [state, action] = useNHSNotifyServerActionForm();
const [name, handleNameChange] = useTextInput(
initialState.name
diff --git a/frontend/src/components/forms/PdfLetterTemplateForm/form-schema.ts b/frontend/src/components/forms/PdfLetterTemplateForm/form-schema.ts
index 85d9be04f4..16a4f5c218 100644
--- a/frontend/src/components/forms/PdfLetterTemplateForm/form-schema.ts
+++ b/frontend/src/components/forms/PdfLetterTemplateForm/form-schema.ts
@@ -4,6 +4,7 @@ import {
} from 'nhs-notify-backend-client/schemas';
import { z } from 'zod';
import content from '@content/content';
+import { MAX_FILE_UPLOAD_SIZE } from '@utils/constants';
const {
components: {
@@ -30,7 +31,7 @@ export const $UploadLetterTemplateForm = z.object({
.instanceof(File, {
message: form.letterTemplatePdf.error.empty,
})
- .refine((pdf) => pdf.size <= 5 * 1024 * 1024, {
+ .refine((pdf) => pdf.size <= MAX_FILE_UPLOAD_SIZE, {
message: form.letterTemplatePdf.error.tooLarge,
})
.refine((pdf) => pdf.type === 'application/pdf', {
diff --git a/frontend/src/components/forms/PreviewEmailTemplate/PreviewEmailTemplate.tsx b/frontend/src/components/forms/PreviewEmailTemplate/PreviewEmailTemplate.tsx
index 8dc24617fe..988ca4109d 100644
--- a/frontend/src/components/forms/PreviewEmailTemplate/PreviewEmailTemplate.tsx
+++ b/frontend/src/components/forms/PreviewEmailTemplate/PreviewEmailTemplate.tsx
@@ -5,33 +5,21 @@ import PreviewTemplateDetailsEmail from '@molecules/PreviewTemplateDetails/Previ
import { PreviewDigitalTemplate } from '@organisms/PreviewDigitalTemplate';
import content from '@content/content';
import { EmailTemplate } from 'nhs-notify-web-template-management-utils';
-import { useActionState, useState } from 'react';
import { useSearchParams } from 'next/navigation';
import { NHSNotifyMain } from '@atoms/NHSNotifyMain/NHSNotifyMain';
-import { $FormSchema, previewEmailTemplateAction } from './server-actions';
-import { validate } from '@utils/client-validate-form';
+import { handleSubmit } from './form-action';
import NotifyBackLink from '@atoms/NHSNotifyBackLink/NHSNotifyBackLink';
-import { ErrorState, PageComponentProps } from '@utils/types';
export function PreviewEmailTemplate({
initialState,
-}: Readonly>) {
+}: {
+ initialState: EmailTemplate;
+}) {
const searchParams = useSearchParams();
const { form, sectionHeading, backLinkText } =
content.components.previewEmailTemplate;
- const [state, action] = useActionState(
- previewEmailTemplateAction,
- initialState
- );
-
- const [errorState, setErrorState] = useState(
- state.errorState
- );
-
- const formValidate = validate($FormSchema, setErrorState);
-
const isFromEditPage = searchParams.get('from') === 'edit';
return (
@@ -47,13 +35,9 @@ export function PreviewEmailTemplate({
sectionHeading={isFromEditPage ? sectionHeading : undefined}
form={{
...form,
- state: {
- errorState,
- },
- action,
formId: 'preview-email-template',
radiosId: 'previewEmailTemplateAction',
- formAttributes: { onSubmit: formValidate },
+ handleSubmit,
}}
editPath={`/edit-email-template/${initialState.id}`}
detailsComponent={
diff --git a/frontend/src/components/forms/PreviewEmailTemplate/form-action.ts b/frontend/src/components/forms/PreviewEmailTemplate/form-action.ts
new file mode 100644
index 0000000000..18ff971d02
--- /dev/null
+++ b/frontend/src/components/forms/PreviewEmailTemplate/form-action.ts
@@ -0,0 +1,52 @@
+import { z } from 'zod';
+import content from '@content/content';
+import { NHSNotifyClientSideFormSubmitHandler } from '@atoms/NHSNotifyForm/Form';
+
+const {
+ components: {
+ previewEmailTemplate: { form },
+ },
+} = content;
+
+export const $FormSchema = z.object({
+ previewEmailTemplateAction: z.enum(['email-edit', 'email-submit'], {
+ message: form.previewEmailTemplateAction.error.empty,
+ }),
+ templateId: z.string(),
+ lockNumber: z.string(),
+});
+
+export const handleSubmit: NHSNotifyClientSideFormSubmitHandler =
+ (router, [, setState]) =>
+ async (event: React.SubmitEvent) => {
+ event.preventDefault();
+
+ const formData = new FormData(event.target);
+ const formFields = Object.fromEntries(formData.entries());
+
+ const { success, error, data } = $FormSchema.safeParse(formFields);
+
+ if (!success) {
+ setState({
+ errorState: z.flattenError(error),
+ });
+
+ return;
+ }
+
+ if (data.previewEmailTemplateAction === 'email-edit') {
+ router.push(`/edit-email-template/${data.templateId}`);
+
+ return;
+ }
+
+ if (data.previewEmailTemplateAction === 'email-submit') {
+ router.push(
+ `/submit-email-template/${data.templateId}?lockNumber=${data.lockNumber}`
+ );
+
+ return;
+ }
+
+ throw new Error('Unknown preview email template action.');
+ };
diff --git a/frontend/src/components/forms/PreviewEmailTemplate/index.ts b/frontend/src/components/forms/PreviewEmailTemplate/index.ts
index 3816a8f6ba..2769d00148 100644
--- a/frontend/src/components/forms/PreviewEmailTemplate/index.ts
+++ b/frontend/src/components/forms/PreviewEmailTemplate/index.ts
@@ -1,2 +1,2 @@
export * from './PreviewEmailTemplate';
-export * from './server-actions';
+export * from './form-action';
diff --git a/frontend/src/components/forms/PreviewEmailTemplate/server-actions.ts b/frontend/src/components/forms/PreviewEmailTemplate/server-actions.ts
deleted file mode 100644
index 3aa40d732f..0000000000
--- a/frontend/src/components/forms/PreviewEmailTemplate/server-actions.ts
+++ /dev/null
@@ -1,46 +0,0 @@
-import { EmailTemplate } from 'nhs-notify-web-template-management-utils';
-import { redirect, RedirectType } from 'next/navigation';
-import { z } from 'zod';
-import content from '@content/content';
-import { TemplateFormState } from '@utils/types';
-
-const {
- components: {
- previewEmailTemplate: { form },
- },
-} = content;
-
-export const $FormSchema = z.object({
- previewEmailTemplateAction: z.enum(['email-edit', 'email-submit'], {
- message: form.previewEmailTemplateAction.error.empty,
- }),
-});
-
-export async function previewEmailTemplateAction(
- formState: TemplateFormState,
- formData: FormData
-): Promise> {
- const formFields = Object.fromEntries(formData.entries());
-
- const { success, error, data } = $FormSchema.safeParse(formFields);
-
- if (!success) {
- return {
- ...formState,
- errorState: z.flattenError(error),
- };
- }
-
- if (data.previewEmailTemplateAction === 'email-edit') {
- return redirect(`/edit-email-template/${formState.id}`, RedirectType.push);
- }
-
- if (data.previewEmailTemplateAction === 'email-submit') {
- return redirect(
- `/submit-email-template/${formState.id}?lockNumber=${formState.lockNumber}`,
- RedirectType.push
- );
- }
-
- throw new Error('Unknown preview email template action.');
-}
diff --git a/frontend/src/components/forms/PreviewNHSAppTemplate/PreviewNHSAppTemplate.tsx b/frontend/src/components/forms/PreviewNHSAppTemplate/PreviewNHSAppTemplate.tsx
index e7152be6d1..68e90f4410 100644
--- a/frontend/src/components/forms/PreviewNHSAppTemplate/PreviewNHSAppTemplate.tsx
+++ b/frontend/src/components/forms/PreviewNHSAppTemplate/PreviewNHSAppTemplate.tsx
@@ -6,29 +6,17 @@ import { PreviewDigitalTemplate } from '@organisms/PreviewDigitalTemplate';
import { NHSAppTemplate } from 'nhs-notify-web-template-management-utils';
import content from '@content/content';
import { useSearchParams } from 'next/navigation';
-import { useActionState, useState } from 'react';
import { NHSNotifyMain } from '@atoms/NHSNotifyMain/NHSNotifyMain';
-import { previewNhsAppTemplateAction, schema } from './server-action';
-import { validate } from '@utils/client-validate-form';
+import { handleSubmit } from './form-action';
import NotifyBackLink from '@atoms/NHSNotifyBackLink/NHSNotifyBackLink';
-import { ErrorState, PageComponentProps } from '@utils/types';
export function PreviewNHSAppTemplate({
initialState,
-}: Readonly>) {
+}: {
+ initialState: NHSAppTemplate;
+}) {
const searchParams = useSearchParams();
- const [state, action] = useActionState(
- previewNhsAppTemplateAction,
- initialState
- );
-
- const [errorState, setErrorState] = useState(
- state.errorState
- );
-
- const formValidate = validate(schema, setErrorState);
-
const isFromEditPage = searchParams.get('from') === 'edit';
const { sectionHeading, form, backLinkText } =
@@ -47,13 +35,9 @@ export function PreviewNHSAppTemplate({
sectionHeading={isFromEditPage ? sectionHeading : undefined}
form={{
...form,
- state: {
- errorState,
- },
- action,
formId: 'preview-nhs-app-template',
radiosId: 'previewNHSAppTemplateAction',
- formAttributes: { onSubmit: formValidate },
+ handleSubmit,
}}
detailsComponent={
diff --git a/frontend/src/components/forms/PreviewNHSAppTemplate/form-action.ts b/frontend/src/components/forms/PreviewNHSAppTemplate/form-action.ts
new file mode 100644
index 0000000000..f16707f2da
--- /dev/null
+++ b/frontend/src/components/forms/PreviewNHSAppTemplate/form-action.ts
@@ -0,0 +1,52 @@
+import { z } from 'zod';
+import content from '@content/content';
+import { NHSNotifyClientSideFormSubmitHandler } from '@atoms/NHSNotifyForm/Form';
+
+const {
+ components: {
+ previewNHSAppTemplate: { form },
+ },
+} = content;
+
+export const $FormSchema = z.object({
+ previewNHSAppTemplateAction: z.enum(['nhsapp-edit', 'nhsapp-submit'], {
+ message: form.previewNHSAppTemplateAction.error.empty,
+ }),
+ templateId: z.string(),
+ lockNumber: z.string(),
+});
+
+export const handleSubmit: NHSNotifyClientSideFormSubmitHandler =
+ (router, [, setState]) =>
+ async (event: React.SubmitEvent) => {
+ event.preventDefault();
+
+ const formData = new FormData(event.target);
+ const formFields = Object.fromEntries(formData.entries());
+
+ const { success, error, data } = $FormSchema.safeParse(formFields);
+
+ if (!success) {
+ setState({
+ errorState: z.flattenError(error),
+ });
+
+ return;
+ }
+
+ if (data.previewNHSAppTemplateAction === 'nhsapp-edit') {
+ router.push(`/edit-nhs-app-template/${data.templateId}`);
+
+ return;
+ }
+
+ if (data.previewNHSAppTemplateAction === 'nhsapp-submit') {
+ router.push(
+ `/submit-nhs-app-template/${data.templateId}?lockNumber=${data.lockNumber}`
+ );
+
+ return;
+ }
+
+ throw new Error('Unknown preview NHS App template action.');
+ };
diff --git a/frontend/src/components/forms/PreviewNHSAppTemplate/index.ts b/frontend/src/components/forms/PreviewNHSAppTemplate/index.ts
index 224743cc54..dd7e7eb739 100644
--- a/frontend/src/components/forms/PreviewNHSAppTemplate/index.ts
+++ b/frontend/src/components/forms/PreviewNHSAppTemplate/index.ts
@@ -1,2 +1,2 @@
export * from './PreviewNHSAppTemplate';
-export * from './server-action';
+export * from './form-action';
diff --git a/frontend/src/components/forms/PreviewNHSAppTemplate/server-action.ts b/frontend/src/components/forms/PreviewNHSAppTemplate/server-action.ts
deleted file mode 100644
index d2d27e2b7e..0000000000
--- a/frontend/src/components/forms/PreviewNHSAppTemplate/server-action.ts
+++ /dev/null
@@ -1,53 +0,0 @@
-import { redirect, RedirectType } from 'next/navigation';
-import { z } from 'zod';
-import { NHSAppTemplate } from 'nhs-notify-web-template-management-utils';
-import content from '@content/content';
-import { TemplateFormState } from '@utils/types';
-
-const {
- components: {
- previewNHSAppTemplate: { form },
- },
-} = content;
-
-function radioSelectionToRedirectUrl(
- selection: 'nhsapp-edit' | 'nhsapp-submit',
- id: string,
- lockNumber: number
-) {
- if (selection === 'nhsapp-edit') {
- return `/edit-nhs-app-template/${id}`;
- }
-
- return `/submit-nhs-app-template/${id}?lockNumber=${lockNumber}`;
-}
-
-export const schema = z.object({
- previewNHSAppTemplateAction: z.enum(['nhsapp-edit', 'nhsapp-submit'], {
- message: form.previewNHSAppTemplateAction.error.empty,
- }),
-});
-
-export function previewNhsAppTemplateAction(
- formState: TemplateFormState,
- formData: FormData
-): TemplateFormState {
- const formFields = Object.fromEntries(formData.entries());
- const validationResponse = schema.safeParse(formFields);
-
- if (!validationResponse.success) {
- return {
- ...formState,
- errorState: z.flattenError(validationResponse.error),
- };
- }
-
- return redirect(
- radioSelectionToRedirectUrl(
- validationResponse.data.previewNHSAppTemplateAction,
- formState.id,
- formState.lockNumber
- ),
- RedirectType.push
- );
-}
diff --git a/frontend/src/components/forms/PreviewSMSTemplate/PreviewSMSTemplate.tsx b/frontend/src/components/forms/PreviewSMSTemplate/PreviewSMSTemplate.tsx
index f38c3df324..ddc152ae2f 100644
--- a/frontend/src/components/forms/PreviewSMSTemplate/PreviewSMSTemplate.tsx
+++ b/frontend/src/components/forms/PreviewSMSTemplate/PreviewSMSTemplate.tsx
@@ -5,33 +5,21 @@ import PreviewTemplateDetailsSms from '@molecules/PreviewTemplateDetails/Preview
import { PreviewDigitalTemplate } from '@organisms/PreviewDigitalTemplate';
import content from '@content/content';
import { SMSTemplate } from 'nhs-notify-web-template-management-utils';
-import { useActionState, useState } from 'react';
import { useSearchParams } from 'next/navigation';
import { NHSNotifyMain } from '@atoms/NHSNotifyMain/NHSNotifyMain';
-import { $FormSchema, previewSmsTemplateAction } from './server-actions';
-import { validate } from '@utils/client-validate-form';
+import { handleSubmit } from './form-action';
import NotifyBackLink from '@atoms/NHSNotifyBackLink/NHSNotifyBackLink';
-import { ErrorState, PageComponentProps } from '@utils/types';
export function PreviewSMSTemplate({
initialState,
-}: Readonly>) {
+}: {
+ initialState: SMSTemplate;
+}) {
const searchParams = useSearchParams();
const { sectionHeading, form, backLinkText } =
content.components.previewSMSTemplate;
- const [state, action] = useActionState(
- previewSmsTemplateAction,
- initialState
- );
-
- const [errorState, setErrorState] = useState(
- state.errorState
- );
-
- const formValidate = validate($FormSchema, setErrorState);
-
const isFromEditPage = searchParams.get('from') === 'edit';
return (
@@ -47,13 +35,9 @@ export function PreviewSMSTemplate({
sectionHeading={isFromEditPage ? sectionHeading : undefined}
form={{
...form,
- state: {
- errorState,
- },
- action,
formId: 'preview-sms-template',
radiosId: 'previewSMSTemplateAction',
- formAttributes: { onSubmit: formValidate },
+ handleSubmit,
}}
detailsComponent={
diff --git a/frontend/src/components/forms/PreviewSMSTemplate/form-action.ts b/frontend/src/components/forms/PreviewSMSTemplate/form-action.ts
new file mode 100644
index 0000000000..dc3ac88713
--- /dev/null
+++ b/frontend/src/components/forms/PreviewSMSTemplate/form-action.ts
@@ -0,0 +1,52 @@
+import { z } from 'zod';
+import content from '@content/content';
+import { NHSNotifyClientSideFormSubmitHandler } from '@atoms/NHSNotifyForm/Form';
+
+const {
+ components: {
+ previewSMSTemplate: { form },
+ },
+} = content;
+
+export const $FormSchema = z.object({
+ previewSMSTemplateAction: z.enum(['sms-edit', 'sms-submit'], {
+ message: form.previewSMSTemplateAction.error.empty,
+ }),
+ templateId: z.string(),
+ lockNumber: z.string(),
+});
+
+export const handleSubmit: NHSNotifyClientSideFormSubmitHandler =
+ (router, [, setState]) =>
+ async (event: React.SubmitEvent) => {
+ event.preventDefault();
+
+ const formData = new FormData(event.target);
+ const formFields = Object.fromEntries(formData.entries());
+
+ const { success, error, data } = $FormSchema.safeParse(formFields);
+
+ if (!success) {
+ setState({
+ errorState: z.flattenError(error),
+ });
+
+ return;
+ }
+
+ if (data.previewSMSTemplateAction === 'sms-edit') {
+ router.push(`/edit-text-message-template/${data.templateId}`);
+
+ return;
+ }
+
+ if (data.previewSMSTemplateAction === 'sms-submit') {
+ router.push(
+ `/submit-text-message-template/${data.templateId}?lockNumber=${data.lockNumber}`
+ );
+
+ return;
+ }
+
+ throw new Error('Unknown preview sms template action.');
+ };
diff --git a/frontend/src/components/forms/PreviewSMSTemplate/index.ts b/frontend/src/components/forms/PreviewSMSTemplate/index.ts
index e86681ad64..622d58314b 100644
--- a/frontend/src/components/forms/PreviewSMSTemplate/index.ts
+++ b/frontend/src/components/forms/PreviewSMSTemplate/index.ts
@@ -1,2 +1,2 @@
export * from './PreviewSMSTemplate';
-export * from './server-actions';
+export * from './form-action';
diff --git a/frontend/src/components/forms/PreviewSMSTemplate/server-actions.ts b/frontend/src/components/forms/PreviewSMSTemplate/server-actions.ts
deleted file mode 100644
index b505304920..0000000000
--- a/frontend/src/components/forms/PreviewSMSTemplate/server-actions.ts
+++ /dev/null
@@ -1,49 +0,0 @@
-import { SMSTemplate } from 'nhs-notify-web-template-management-utils';
-import { redirect, RedirectType } from 'next/navigation';
-import { z } from 'zod';
-import content from '@content/content';
-import { TemplateFormState } from '@utils/types';
-
-const {
- components: {
- previewSMSTemplate: { form },
- },
-} = content;
-
-export const $FormSchema = z.object({
- previewSMSTemplateAction: z.enum(['sms-edit', 'sms-submit'], {
- message: form.previewSMSTemplateAction.error.empty,
- }),
-});
-
-export async function previewSmsTemplateAction(
- formState: TemplateFormState,
- formData: FormData
-): Promise> {
- const formFields = Object.fromEntries(formData.entries());
-
- const { success, error, data } = $FormSchema.safeParse(formFields);
-
- if (!success) {
- return {
- ...formState,
- errorState: z.flattenError(error),
- };
- }
-
- if (data.previewSMSTemplateAction === 'sms-edit') {
- return redirect(
- `/edit-text-message-template/${formState.id}`,
- RedirectType.push
- );
- }
-
- if (data.previewSMSTemplateAction === 'sms-submit') {
- return redirect(
- `/submit-text-message-template/${formState.id}?lockNumber=${formState.lockNumber}`,
- RedirectType.push
- );
- }
-
- throw new Error('Unknown preview sms template action.');
-}
diff --git a/frontend/src/components/molecules/LetterRender/LetterRenderForm.tsx b/frontend/src/components/molecules/LetterRender/LetterRenderForm.tsx
index 454e230253..ff7159f309 100644
--- a/frontend/src/components/molecules/LetterRender/LetterRenderForm.tsx
+++ b/frontend/src/components/molecules/LetterRender/LetterRenderForm.tsx
@@ -14,7 +14,7 @@ import { useLetterRenderError } from '@providers/letter-render-error-provider';
import type { PersonalisedRenderKey } from '@utils/types';
import styles from './LetterRenderForm.module.scss';
import { PERSONALISATION_FORMDATA_PREFIX } from '@utils/constants';
-import { useNHSNotifyForm } from '@providers/form-provider';
+import { useNHSNotifyServerActionForm } from '@providers/form-provider';
type LetterRenderFormProps = {
template: AuthoringLetterTemplate;
@@ -24,7 +24,7 @@ type LetterRenderFormProps = {
export function LetterRenderForm({ template, tab }: LetterRenderFormProps) {
const { letterRender: copy } = content.components;
const { isAnyTabPolling } = useLetterRenderPolling();
- const [_state, _action, isPending] = useNHSNotifyForm();
+ const [_state, _action, isPending] = useNHSNotifyServerActionForm();
const { setParentErrorState } = useLetterRenderError();
const exampleRecipients =
diff --git a/frontend/src/components/molecules/NHSNotifyRadioButtonForm/NHSNotifyRadioButtonForm.tsx b/frontend/src/components/molecules/NHSNotifyRadioButtonForm/NHSNotifyRadioButtonForm.tsx
index 44c4be6c67..85f51dce9f 100644
--- a/frontend/src/components/molecules/NHSNotifyRadioButtonForm/NHSNotifyRadioButtonForm.tsx
+++ b/frontend/src/components/molecules/NHSNotifyRadioButtonForm/NHSNotifyRadioButtonForm.tsx
@@ -1,15 +1,16 @@
import { Radios, Fieldset } from 'nhsuk-react-components';
-import type { FormState } from '@utils/types';
-import { NHSNotifyFormWrapper } from '@molecules/NHSNotifyFormWrapper/NHSNotifyFormWrapper';
import { NHSNotifyButton } from '@atoms/NHSNotifyButton/NHSNotifyButton';
-import { DetailedHTMLProps, FormHTMLAttributes, ReactNode } from 'react';
+import { ReactNode } from 'react';
import Link from 'next/link';
+import {
+ NHSNotifyClientSideForm,
+ NHSNotifyClientSideFormSubmitHandler,
+} from '@atoms/NHSNotifyForm/Form';
+import { useNHSNotifyForm } from '@providers/form-provider';
export type NHSNotifyRadioButtonFormProps = {
formId: string;
radiosId: string;
- action: string | ((payload: FormData) => void);
- state: FormState;
pageHeading: string;
options: {
id: string;
@@ -25,15 +26,12 @@ export type NHSNotifyRadioButtonFormProps = {
};
learnMoreLink?: string;
learnMoreText?: string;
- formAttributes?: DetailedHTMLProps<
- FormHTMLAttributes,
- HTMLFormElement
- >;
backLink?: {
text: string;
url: string;
};
children?: React.ReactNode;
+ handleSubmit: NHSNotifyClientSideFormSubmitHandler;
};
const normaliseId = (id: string) =>
@@ -42,76 +40,74 @@ const normaliseId = (id: string) =>
export const NHSNotifyRadioButtonForm = ({
formId,
radiosId,
- action,
- state,
pageHeading,
options,
buttonText,
- formAttributes,
legend = { isPgeHeading: true, size: 'l' },
hint = '',
learnMoreLink = '',
learnMoreText = '',
backLink,
children,
-}: NHSNotifyRadioButtonFormProps) => (
-
-
- {/* Render any passed children here */}
- {children}
-
- {buttonText}
-
- {backLink && (
-
- {backLink.text}
-
- )}
-
-);
+
+ );
+};
diff --git a/frontend/src/components/organisms/PreviewDigitalTemplate/PreviewDigitalTemplate.tsx b/frontend/src/components/organisms/PreviewDigitalTemplate/PreviewDigitalTemplate.tsx
index 3cb83cffbc..a3f0e603af 100644
--- a/frontend/src/components/organisms/PreviewDigitalTemplate/PreviewDigitalTemplate.tsx
+++ b/frontend/src/components/organisms/PreviewDigitalTemplate/PreviewDigitalTemplate.tsx
@@ -1,6 +1,5 @@
'use client';
-import { NhsNotifyErrorSummary } from '@molecules/NhsNotifyErrorSummary/NhsNotifyErrorSummary';
import { NHSNotifyRadioButtonForm } from '@molecules/NHSNotifyRadioButtonForm/NHSNotifyRadioButtonForm';
import { PreviewTemplateProps } from './preview-digital-template.types';
import { Button } from 'nhsuk-react-components';
@@ -11,6 +10,8 @@ import {
DigitalTemplateType,
sendDigitalTemplateTestMessageUrl,
} from 'nhs-notify-web-template-management-utils';
+import * as NHSNotifyForm from '@atoms/NHSNotifyForm';
+import { NHSNotifyClientSideFormProvider } from '@providers/form-provider';
const { editButton, sendTestMessageButton } =
content.components.previewDigitalTemplate;
@@ -67,8 +68,8 @@ export function PreviewDigitalTemplate(props: PreviewTemplateProps) {
)}
>
) : (
- <>
-
+
+
{props.detailsComponent}
- >
+ >
+
+
+
+
)}
>
);
diff --git a/frontend/src/components/providers/form-provider.tsx b/frontend/src/components/providers/form-provider.tsx
index 436d61a22b..77b7496587 100644
--- a/frontend/src/components/providers/form-provider.tsx
+++ b/frontend/src/components/providers/form-provider.tsx
@@ -3,8 +3,11 @@
import {
type PropsWithChildren,
createContext,
+ useState,
useActionState,
useContext,
+ Dispatch,
+ SetStateAction,
} from 'react';
import type { FormState } from '@utils/types';
@@ -12,18 +15,60 @@ type NHSNotifyFormActionState = ReturnType<
typeof useActionState
>;
-export const FormContext = createContext(null);
+export type NHSNotifyClientSideFormActionState = [
+ FormState,
+ Dispatch>,
+];
+
+export const FormContext = createContext<
+ NHSNotifyFormActionState | NHSNotifyClientSideFormActionState | null
+>(null);
export function useNHSNotifyForm() {
const context = useContext(FormContext);
if (!context)
throw new Error(
- 'useNHSNotifyForm must be used within NHSNotifyFormProvider'
+ 'useNHSNotifyForm must be used within NHSNotifyFormProvider or NHSNotifyClientSideFormProvider'
+ );
+
+ return context;
+}
+
+export function useNHSNotifyClientSideForm() {
+ const context = useContext(FormContext);
+ if (!context || context.length !== 2)
+ throw new Error(
+ 'useNHSNotifyClientSideForm must be used within NHSNotifyClientSideFormProvider'
);
return context;
}
+export function useNHSNotifyServerActionForm() {
+ const context = useContext(FormContext);
+ if (!context || context.length !== 3)
+ throw new Error(
+ 'useNHSNotifyServerActionForm must be used within NHSNotifyFormProvider'
+ );
+
+ return context;
+}
+
+export function NHSNotifyClientSideFormProvider({
+ children,
+ initialState = {},
+}: PropsWithChildren<{
+ initialState?: FormState;
+}>) {
+ const [state, setState] = useState(initialState);
+
+ return (
+
+ {children}
+
+ );
+}
+
export function NHSNotifyFormProvider({
children,
initialState = {},
diff --git a/frontend/src/content/content.ts b/frontend/src/content/content.ts
index e64695d69e..c7b11b5c65 100644
--- a/frontend/src/content/content.ts
+++ b/frontend/src/content/content.ts
@@ -2072,6 +2072,8 @@ const uploadDocxLetterTemplateForm = {
},
file: {
empty: 'Choose a template file',
+ tooLarge:
+ 'Your file is too large. The file must be smaller than 5MB. Upload a different letter template file',
},
language: {
empty: 'Choose a language',
diff --git a/frontend/src/utils/constants.ts b/frontend/src/utils/constants.ts
index bea5af7164..268883e614 100644
--- a/frontend/src/utils/constants.ts
+++ b/frontend/src/utils/constants.ts
@@ -15,3 +15,5 @@ export const INVALID_PERSONALISATION_FIELDS = [
// pipes are banned in personalisation keys, so this
// cannot clash with user provided keys
export const PERSONALISATION_FORMDATA_PREFIX = 'personalisation|';
+
+export const MAX_FILE_UPLOAD_SIZE = 5 * 1024 * 1024; // 5MB in bytes
diff --git a/tests/test-team/fixtures/letters/docx/too-large.docx b/tests/test-team/fixtures/letters/docx/too-large.docx
new file mode 100755
index 0000000000..0a0479bf75
Binary files /dev/null and b/tests/test-team/fixtures/letters/docx/too-large.docx differ
diff --git a/tests/test-team/fixtures/letters/index.ts b/tests/test-team/fixtures/letters/index.ts
index 8f1a087cf5..d2afd38867 100644
--- a/tests/test-team/fixtures/letters/index.ts
+++ b/tests/test-team/fixtures/letters/index.ts
@@ -73,5 +73,6 @@ export const docxFixtures = {
randomBytes: getFile('docx', 'random-bytes.docx'),
randomBytesZipped: getFile('docx', 'random-bytes-zipped.docx'),
standard: getFile('docx', 'standard-english-template.docx'),
+ tooLarge: getFile('docx', 'too-large.docx'),
unexpectedAddressLines: getFile('docx', 'unexpected-address-lines.docx'),
};
diff --git a/tests/test-team/template-mgmt-component-tests/letter/template-mgmt-upload-bsl-letter-template.component.spec.ts b/tests/test-team/template-mgmt-component-tests/letter/template-mgmt-upload-bsl-letter-template.component.spec.ts
index 81101ee86d..d1d8528ec1 100644
--- a/tests/test-team/template-mgmt-component-tests/letter/template-mgmt-upload-bsl-letter-template.component.spec.ts
+++ b/tests/test-team/template-mgmt-component-tests/letter/template-mgmt-upload-bsl-letter-template.component.spec.ts
@@ -101,6 +101,23 @@ test.describe('Upload BSL Letter Template Page', () => {
});
});
+ test('error when file that is too large is submitted', async ({ page }) => {
+ const uploadPage = new TemplateMgmtUploadBSLLetterTemplatePage(page);
+
+ await uploadPage.loadPage();
+
+ await uploadPage.nameInput.fill('template-name');
+
+ await uploadPage.fileInput.click();
+ await uploadPage.fileInput.setInputFiles(docxFixtures.tooLarge.filepath);
+
+ await uploadPage.submitButton.click();
+
+ await expect(uploadPage.errorSummaryList).toHaveText(
+ 'Your file is too large. The file must be smaller than 5MB. Upload a different letter template file'
+ );
+ });
+
test('displays error messages when blank form is submitted', async ({
page,
}) => {
diff --git a/tests/test-team/template-mgmt-component-tests/letter/template-mgmt-upload-large-print-letter-template.component.spec.ts b/tests/test-team/template-mgmt-component-tests/letter/template-mgmt-upload-large-print-letter-template.component.spec.ts
index 92119c8b53..8bc3d7fff9 100644
--- a/tests/test-team/template-mgmt-component-tests/letter/template-mgmt-upload-large-print-letter-template.component.spec.ts
+++ b/tests/test-team/template-mgmt-component-tests/letter/template-mgmt-upload-large-print-letter-template.component.spec.ts
@@ -121,6 +121,25 @@ test.describe('Upload Large Print Letter Template Page', () => {
'Choose a template file',
]);
});
+
+ test('error when file that is too large is submitted', async ({ page }) => {
+ const uploadPage = new TemplateMgmtUploadLargePrintLetterTemplatePage(
+ page
+ );
+
+ await uploadPage.loadPage();
+
+ await uploadPage.nameInput.fill('template-name');
+
+ await uploadPage.fileInput.click();
+ await uploadPage.fileInput.setInputFiles(docxFixtures.tooLarge.filepath);
+
+ await uploadPage.submitButton.click();
+
+ await expect(uploadPage.errorSummaryList).toHaveText(
+ 'Your file is too large. The file must be smaller than 5MB. Upload a different letter template file'
+ );
+ });
});
test.describe('multi-campaign client', () => {
diff --git a/tests/test-team/template-mgmt-component-tests/letter/template-mgmt-upload-other-language-letter-template.component.spec.ts b/tests/test-team/template-mgmt-component-tests/letter/template-mgmt-upload-other-language-letter-template.component.spec.ts
index 691090715f..2953ef7fab 100644
--- a/tests/test-team/template-mgmt-component-tests/letter/template-mgmt-upload-other-language-letter-template.component.spec.ts
+++ b/tests/test-team/template-mgmt-component-tests/letter/template-mgmt-upload-other-language-letter-template.component.spec.ts
@@ -105,6 +105,27 @@ test.describe('Upload Other Language Letter Template Page', () => {
});
});
+ test('error when file that is too large is submitted', async ({ page }) => {
+ const uploadPage = new TemplateMgmtUploadOtherLanguageLetterTemplatePage(
+ page
+ );
+
+ await uploadPage.loadPage();
+
+ await uploadPage.nameInput.fill('template-name');
+
+ await uploadPage.languageInput.selectOption('Spanish');
+
+ await uploadPage.fileInput.click();
+ await uploadPage.fileInput.setInputFiles(docxFixtures.tooLarge.filepath);
+
+ await uploadPage.submitButton.click();
+
+ await expect(uploadPage.errorSummaryList).toHaveText(
+ 'Your file is too large. The file must be smaller than 5MB. Upload a different letter template file'
+ );
+ });
+
test('displays error messages when blank form is submitted', async ({
page,
}) => {
diff --git a/tests/test-team/template-mgmt-component-tests/letter/template-mgmt-upload-standard-english-letter-template.component.spec.ts b/tests/test-team/template-mgmt-component-tests/letter/template-mgmt-upload-standard-english-letter-template.component.spec.ts
index bb757f4d60..3a97166adb 100644
--- a/tests/test-team/template-mgmt-component-tests/letter/template-mgmt-upload-standard-english-letter-template.component.spec.ts
+++ b/tests/test-team/template-mgmt-component-tests/letter/template-mgmt-upload-standard-english-letter-template.component.spec.ts
@@ -102,6 +102,24 @@ test.describe('Upload Standard English Letter Template Page', () => {
});
});
+ test('error when file that is too large is submitted', async ({ page }) => {
+ const uploadPage =
+ new TemplateMgmtUploadStandardEnglishLetterTemplatePage(page);
+
+ await uploadPage.loadPage();
+
+ await uploadPage.nameInput.fill('template-name');
+
+ await uploadPage.fileInput.click();
+ await uploadPage.fileInput.setInputFiles(docxFixtures.tooLarge.filepath);
+
+ await uploadPage.submitButton.click();
+
+ await expect(uploadPage.errorSummaryList).toHaveText(
+ 'Your file is too large. The file must be smaller than 5MB. Upload a different letter template file'
+ );
+ });
+
test('displays error messages when blank form is submitted', async ({
page,
}) => {