Date: Thu, 9 Apr 2026 12:40:28 +0100
Subject: [PATCH 08/12] Misc
---
.../server-action.test.ts | 27 +++++++++++++
.../CombinedLetterErrorSummary.tsx | 38 ++++++-------------
.../LetterRender/LetterRenderForm.tsx | 4 +-
.../LetterRender/LetterRenderTab.tsx | 7 ++--
4 files changed, 43 insertions(+), 33 deletions(-)
diff --git a/frontend/src/__tests__/app/preview-letter-template/server-action.test.ts b/frontend/src/__tests__/app/preview-letter-template/server-action.test.ts
index ae8d1a211..91680adf9 100644
--- a/frontend/src/__tests__/app/preview-letter-template/server-action.test.ts
+++ b/frontend/src/__tests__/app/preview-letter-template/server-action.test.ts
@@ -213,4 +213,31 @@ describe('submitAuthoringLetterAction', () => {
);
expect(redirectMock).not.toHaveBeenCalled();
});
+
+ it('should return error when long render exists but is not RENDERED status', async () => {
+ getTemplateMock.mockResolvedValue({
+ ...AUTHORING_LETTER_TEMPLATE,
+ files: {
+ ...AUTHORING_LETTER_TEMPLATE.files,
+ shortFormRender: {
+ status: 'RENDERED' as const,
+ fileName: 'short.pdf',
+ currentVersion: 'v1',
+ pageCount: 2,
+ },
+ longFormRender: { status: 'FAILED' as const },
+ },
+ });
+
+ const formData = new FormData();
+ formData.append('templateId', AUTHORING_LETTER_TEMPLATE.id);
+ formData.append('lockNumber', '1');
+
+ const result = await submitAuthoringLetterAction({}, formData);
+
+ expect(result.errorState?.fieldErrors?.['tab-long']).toContain(
+ approveErrors.longExampleRequired
+ );
+ expect(redirectMock).not.toHaveBeenCalled();
+ });
});
diff --git a/frontend/src/components/molecules/LetterRender/CombinedLetterErrorSummary.tsx b/frontend/src/components/molecules/LetterRender/CombinedLetterErrorSummary.tsx
index b3f0e6493..b7cf4ff7b 100644
--- a/frontend/src/components/molecules/LetterRender/CombinedLetterErrorSummary.tsx
+++ b/frontend/src/components/molecules/LetterRender/CombinedLetterErrorSummary.tsx
@@ -6,41 +6,25 @@ import { useLetterRenderError } from '@providers/letter-render-error-provider';
import { NhsNotifyErrorSummary } from '@molecules/NhsNotifyErrorSummary/NhsNotifyErrorSummary';
/**
- * Syncs the outer page-level form state into
- * CombinedLetterErrorSummary so it can be displayed and cleared independently
- * of letter render errors.
+ * Displays validation errors for both the letter rendering component and a parent form.
+ * Error state is stored in LetterRenderErrorProvider so it can be cleared when either form is submitted.
*
- * This mechanism is intentionally NOT used for letterRenderErrorState — that should be
- * handled via the parent form submit's onClick to avoid interfering with initial mount.
+ * Must be rendered inside NHSNotifyFormProvider (for the parent action state)
+ * and LetterRenderErrorProvider.
*/
-function ParentFormErrorSyncer() {
+export function CombinedLetterErrorSummary() {
const [state] = useNHSNotifyForm();
- const { setParentErrorState } = useLetterRenderError();
+
+ const { setParentErrorState, parentErrorState, letterRenderErrorState } =
+ useLetterRenderError();
useEffect(() => {
setParentErrorState(state.errorState);
}, [state, setParentErrorState]);
- return null;
-}
-
-/**
- * Displays validation errors for the preview letter template page.
- * Approve template and Update preview errors occupy separate slots so each
- * button's onClick can clear only its own errors.
- *
- * Must be rendered inside NHSNotifyFormProvider (for the parent action state)
- * and LetterRenderErrorProvider.
- */
-export function CombinedLetterErrorSummary() {
- const { parentErrorState, letterRenderErrorState } = useLetterRenderError();
-
return (
- <>
-
-
- >
+
);
}
diff --git a/frontend/src/components/molecules/LetterRender/LetterRenderForm.tsx b/frontend/src/components/molecules/LetterRender/LetterRenderForm.tsx
index e286b5d1a..a76fe10aa 100644
--- a/frontend/src/components/molecules/LetterRender/LetterRenderForm.tsx
+++ b/frontend/src/components/molecules/LetterRender/LetterRenderForm.tsx
@@ -23,7 +23,7 @@ type LetterRenderFormProps = {
export function LetterRenderForm({ template, tab }: LetterRenderFormProps) {
const { letterRender: copy } = content.components;
const { isAnyTabPolling } = useLetterRenderPolling();
- const { setParentErrorState: setApproveErrorState } = useLetterRenderError();
+ const { setParentErrorState } = useLetterRenderError();
const exampleRecipients =
tab === 'shortFormRender'
@@ -93,7 +93,7 @@ export function LetterRenderForm({ template, tab }: LetterRenderFormProps) {
secondary
className='nhsuk-u-margin-top-4'
disabled={isAnyTabPolling}
- onClick={() => setApproveErrorState(undefined)}
+ onClick={() => setParentErrorState(undefined)}
>
{copy.updatePreviewButton}
diff --git a/frontend/src/components/molecules/LetterRender/LetterRenderTab.tsx b/frontend/src/components/molecules/LetterRender/LetterRenderTab.tsx
index 8e5fe2f20..a130e5a26 100644
--- a/frontend/src/components/molecules/LetterRender/LetterRenderTab.tsx
+++ b/frontend/src/components/molecules/LetterRender/LetterRenderTab.tsx
@@ -81,12 +81,11 @@ function LetterRenderTabContent({
hideEditActions?: boolean;
}) {
const [state, _dispatch, isPending] = useNHSNotifyForm();
- const { setLetterRenderErrorState: setUpdatePreviewErrorState } =
- useLetterRenderError();
+ const { setLetterRenderErrorState } = useLetterRenderError();
useEffect(() => {
- setUpdatePreviewErrorState(state.errorState);
- }, [state, setUpdatePreviewErrorState]);
+ setLetterRenderErrorState(state.errorState);
+ }, [state, setLetterRenderErrorState]);
return (
From f935271070d873b1a0ce27bdda6539f7449f623b Mon Sep 17 00:00:00 2001
From: Clare Jones
Date: Thu, 9 Apr 2026 16:06:28 +0100
Subject: [PATCH 09/12] Fix select summary link and error styling
---
.../__snapshots__/page.test.tsx.snap | 2 +-
.../__snapshots__/page.test.tsx.snap | 8 +++----
.../__snapshots__/page.test.tsx.snap | 8 +++----
.../__snapshots__/page.test.tsx.snap | 8 +++----
.../__snapshots__/page.test.tsx.snap | 8 +++----
.../LetterTemplateForm.test.tsx.snap | 6 +++---
.../LetterRender/LetterRenderForm.test.tsx | 12 ++++++++---
.../LetterRender/LetterRenderTab.test.tsx | 4 +++-
.../LetterRender/server-action.test.ts | 21 ++++++++++++-------
.../atoms/FileUpload/FileUpload.tsx | 17 +++++++++++++--
.../components/atoms/NHSNotifyForm/Input.tsx | 17 ++++++++++++---
.../components/atoms/NHSNotifyForm/Select.tsx | 13 ++++++++----
.../LetterTemplateForm/LetterTemplateForm.tsx | 5 +++--
.../UploadDocxLetterTemplateForm/index.tsx | 7 ++-----
.../LetterRender/LetterRenderForm.tsx | 8 +++++--
.../molecules/LetterRender/server-action.ts | 19 ++++++++++++-----
.../components/providers/form-provider.tsx | 2 +-
...mgmt-preview-letter-page.component.spec.ts | 4 ++--
18 files changed, 111 insertions(+), 58 deletions(-)
diff --git a/frontend/src/__tests__/app/edit-template-name/[templateId]/__snapshots__/page.test.tsx.snap b/frontend/src/__tests__/app/edit-template-name/[templateId]/__snapshots__/page.test.tsx.snap
index fe3d1460e..5408e7009 100644
--- a/frontend/src/__tests__/app/edit-template-name/[templateId]/__snapshots__/page.test.tsx.snap
+++ b/frontend/src/__tests__/app/edit-template-name/[templateId]/__snapshots__/page.test.tsx.snap
@@ -266,7 +266,7 @@ exports[`valid template renders errors when blank form is submitted and error st
Enter a template name
{
errorState: {
formErrors: [],
fieldErrors: {
- systemPersonalisationPackId: ['Choose example recipient'],
+ 'system-personalisation-pack-id-shortFormRender': [
+ 'Choose example recipient',
+ ],
},
},
});
@@ -308,7 +310,9 @@ describe('LetterRenderForm', () => {
errorState: {
formErrors: [],
fieldErrors: {
- systemPersonalisationPackId: ['Choose example recipient'],
+ 'system-personalisation-pack-id-longFormRender': [
+ 'Choose example recipient',
+ ],
},
},
});
@@ -410,7 +414,9 @@ describe('LetterRenderForm', () => {
errorState: {
formErrors: [],
fieldErrors: {
- systemPersonalisationPackId: ['Choose example recipient'],
+ 'system-personalisation-pack-id-shortFormRender': [
+ 'Choose example recipient',
+ ],
},
},
});
diff --git a/frontend/src/__tests__/components/molecules/LetterRender/LetterRenderTab.test.tsx b/frontend/src/__tests__/components/molecules/LetterRender/LetterRenderTab.test.tsx
index d517577ff..91c23f8e2 100644
--- a/frontend/src/__tests__/components/molecules/LetterRender/LetterRenderTab.test.tsx
+++ b/frontend/src/__tests__/components/molecules/LetterRender/LetterRenderTab.test.tsx
@@ -537,7 +537,9 @@ describe('LetterRenderTab', () => {
errorState: {
formErrors: [],
fieldErrors: {
- systemPersonalisationPackId: ['Select an example recipient'],
+ 'system-personalisation-pack-id-shortFormRender': [
+ 'Select an example recipient',
+ ],
},
},
})
diff --git a/frontend/src/__tests__/components/molecules/LetterRender/server-action.test.ts b/frontend/src/__tests__/components/molecules/LetterRender/server-action.test.ts
index a2930e0c8..15b9a3a9b 100644
--- a/frontend/src/__tests__/components/molecules/LetterRender/server-action.test.ts
+++ b/frontend/src/__tests__/components/molecules/LetterRender/server-action.test.ts
@@ -146,10 +146,12 @@ describe('updateLetterPreview', () => {
const result = await updateLetterPreview({}, formData);
expect(result.errorState?.fieldErrors).toHaveProperty(
- 'systemPersonalisationPackId'
+ 'system-personalisation-pack-id-shortFormRender'
);
expect(
- result.errorState?.fieldErrors?.systemPersonalisationPackId
+ result.errorState?.fieldErrors?.[
+ 'system-personalisation-pack-id-shortFormRender'
+ ]
).toContain('Choose example recipient');
expect(result.fields?.systemPersonalisationPackId).toBe('');
});
@@ -163,7 +165,7 @@ describe('updateLetterPreview', () => {
const result = await updateLetterPreview({}, formData);
expect(result.errorState?.fieldErrors).toHaveProperty(
- 'systemPersonalisationPackId'
+ 'system-personalisation-pack-id-shortFormRender'
);
});
@@ -175,14 +177,14 @@ describe('updateLetterPreview', () => {
const result = await updateLetterPreview({}, formData);
expect(result.errorState?.fieldErrors).toHaveProperty(
- 'systemPersonalisationPackId'
+ 'system-personalisation-pack-id-shortFormRender'
);
expect(result.fields?.systemPersonalisationPackId).toBe(
'invalid-recipient-id'
);
});
- it('error key for recipient is the same regardless of tab', async () => {
+ it('error key for recipient includes the tab value', async () => {
const formData = buildFormData({
systemPersonalisationPackId: '',
tab: 'longFormRender',
@@ -191,7 +193,10 @@ describe('updateLetterPreview', () => {
const result = await updateLetterPreview({}, formData);
expect(result.errorState?.fieldErrors).toHaveProperty(
- 'systemPersonalisationPackId'
+ 'system-personalisation-pack-id-longFormRender'
+ );
+ expect(result.errorState?.fieldErrors).not.toHaveProperty(
+ 'system-personalisation-pack-id-shortFormRender'
);
});
@@ -204,7 +209,7 @@ describe('updateLetterPreview', () => {
const result = await updateLetterPreview({}, formData);
expect(result.errorState?.fieldErrors).toHaveProperty(
- 'systemPersonalisationPackId'
+ 'system-personalisation-pack-id-shortFormRender'
);
expect(result.fields?.systemPersonalisationPackId).toBe('long-1');
});
@@ -253,7 +258,7 @@ describe('updateLetterPreview', () => {
const result = await updateLetterPreview({}, formData);
expect(result.errorState?.fieldErrors).toHaveProperty(
- 'systemPersonalisationPackId'
+ 'system-personalisation-pack-id-shortFormRender'
);
expect(result.errorState?.fieldErrors).toHaveProperty(
'custom-appointmentDate-shortFormRender'
diff --git a/frontend/src/components/atoms/FileUpload/FileUpload.tsx b/frontend/src/components/atoms/FileUpload/FileUpload.tsx
index eb44d3c1f..d93c1c238 100644
--- a/frontend/src/components/atoms/FileUpload/FileUpload.tsx
+++ b/frontend/src/components/atoms/FileUpload/FileUpload.tsx
@@ -2,19 +2,32 @@ import classNames from 'classnames';
import { ErrorMessage, HintText, Label } from 'nhsuk-react-components';
import React, { HTMLProps } from 'react';
import styles from './FileUpload.module.scss';
+import { useNHSNotifyForm } from '@providers/form-provider';
interface FileUploadProps extends HTMLProps {
+ id: string;
error?: string;
hint?: string;
}
export function FileUploadInput({
className,
+ id,
...props
-}: Omit, 'type'>) {
+}: Omit, 'type'> & { id: string; name: string }) {
+ const [state] = useNHSNotifyForm();
+
+ const error = Boolean(state.errorState?.fieldErrors?.[id]?.length);
+
return (
diff --git a/frontend/src/components/atoms/NHSNotifyForm/Input.tsx b/frontend/src/components/atoms/NHSNotifyForm/Input.tsx
index 5b07c1787..2771028f0 100644
--- a/frontend/src/components/atoms/NHSNotifyForm/Input.tsx
+++ b/frontend/src/components/atoms/NHSNotifyForm/Input.tsx
@@ -6,16 +6,27 @@ import { useNHSNotifyForm } from '@providers/form-provider';
export function NHSNotifyFormInput({
className,
+ id,
name,
...props
-}: Omit, 'defaultValue'>) {
+}: Omit, 'defaultValue'> & {
+ id: string;
+ name: string;
+}) {
const [state] = useNHSNotifyForm();
+ const error = Boolean(state.errorState?.fieldErrors?.[id]?.length);
+
return (
);
diff --git a/frontend/src/components/atoms/NHSNotifyForm/Select.tsx b/frontend/src/components/atoms/NHSNotifyForm/Select.tsx
index 66bc80ced..80b5c198a 100644
--- a/frontend/src/components/atoms/NHSNotifyForm/Select.tsx
+++ b/frontend/src/components/atoms/NHSNotifyForm/Select.tsx
@@ -7,12 +7,16 @@ import { useNHSNotifyForm } from '@providers/form-provider';
export function NHSNotifyFormSelect({
children,
className,
+ id,
name,
...props
-}: Omit, 'defaultValue'>) {
+}: Omit, 'defaultValue'> & {
+ id: string;
+ name: string;
+}) {
const [state] = useNHSNotifyForm();
- const error = Boolean(name && state.errorState?.fieldErrors?.[name]?.length);
+ const error = Boolean(state.errorState?.fieldErrors?.[id]?.length);
return (