@defra/forms-engine-plugin 3.0.3 → 3.0.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,52 @@
1
+ // Jest Snapshot v1, https://jestjs.io/docs/snapshot-testing
2
+
3
+ exports[`SummaryViewModel Check answers (0 items) should use correct summary labels 1`] = `
4
+ [
5
+ {
6
+ "label": "How would you like to receive your pizza?",
7
+ "name": "orderType",
8
+ "title": "How you would like to receive your pizza",
9
+ "value": "Collection",
10
+ },
11
+ {
12
+ "label": "Pizza",
13
+ "name": "pizza",
14
+ "title": "Pizzas",
15
+ "value": "",
16
+ },
17
+ ]
18
+ `;
19
+
20
+ exports[`SummaryViewModel Check answers (1 item) should use correct summary labels 1`] = `
21
+ [
22
+ {
23
+ "label": "How would you like to receive your pizza?",
24
+ "name": "orderType",
25
+ "title": "How you would like to receive your pizza",
26
+ "value": "Delivery",
27
+ },
28
+ {
29
+ "label": "Pizza",
30
+ "name": "pizza",
31
+ "title": "Pizza added",
32
+ "value": "You added 1 Pizza",
33
+ },
34
+ ]
35
+ `;
36
+
37
+ exports[`SummaryViewModel Check answers (2 items) should use correct summary labels 1`] = `
38
+ [
39
+ {
40
+ "label": "How would you like to receive your pizza?",
41
+ "name": "orderType",
42
+ "title": "How you would like to receive your pizza",
43
+ "value": "Delivery",
44
+ },
45
+ {
46
+ "label": "Pizza",
47
+ "name": "pizza",
48
+ "title": "Pizzas added",
49
+ "value": "You added 2 Pizzas",
50
+ },
51
+ ]
52
+ `;
@@ -1,11 +1,12 @@
1
- import { format as machineV2 } from "../machine/v2.js";
1
+ import { categoriseData } from "../machine/v2.js";
2
2
  import { FormAdapterSubmissionSchemaVersion } from "../../types/enums.js";
3
3
  import { FormStatus } from "../../../../routes/types.js";
4
4
  export function format(context, items, model, submitResponse, formStatus, formMetadata) {
5
- const v2DataString = machineV2(context, items, model, submitResponse, formStatus);
6
- const v2DataParsed = JSON.parse(v2DataString);
7
5
  const csvFiles = extractCsvFiles(submitResponse);
8
- const transformedData = v2DataParsed.data;
6
+ const {
7
+ main: v2Main,
8
+ ...v2Data
9
+ } = categoriseData(items);
9
10
  const versionMetadata = getVersionMetadata(context.submittedVersionNumber, formMetadata);
10
11
  const meta = {
11
12
  schemaVersion: FormAdapterSubmissionSchemaVersion.V1,
@@ -21,7 +22,16 @@ export function format(context, items, model, submitResponse, formStatus, formMe
21
22
  if (versionMetadata) {
22
23
  meta.versionMetadata = versionMetadata;
23
24
  }
24
- const data = transformedData;
25
+ const main = Object.fromEntries(Object.entries(v2Main).map(([key, value]) => {
26
+ if (value === undefined) {
27
+ return [key, null];
28
+ }
29
+ return [key, value];
30
+ }));
31
+ const data = {
32
+ main,
33
+ ...v2Data
34
+ };
25
35
  const result = {
26
36
  files: csvFiles
27
37
  };
@@ -1 +1 @@
1
- {"version":3,"file":"v1.js","names":["format","machineV2","FormAdapterSubmissionSchemaVersion","FormStatus","context","items","model","submitResponse","formStatus","formMetadata","v2DataString","v2DataParsed","JSON","parse","csvFiles","extractCsvFiles","transformedData","data","versionMetadata","getVersionMetadata","submittedVersionNumber","meta","schemaVersion","V1","timestamp","Date","referenceNumber","formName","name","formId","id","formSlug","slug","status","isPreview","Draft","Live","notificationEmail","result","files","payload","stringify","versions","length","undefined","submittedVersion","find","v","versionNumber","createdAt","firstVersion","main","repeaters"],"sources":["../../../../../../src/server/plugins/engine/outputFormatters/adapter/v1.ts"],"sourcesContent":["import {\n type FormMetadata,\n type SubmitResponsePayload\n} from '@defra/forms-model'\n\nimport { type checkFormStatus } from '~/src/server/plugins/engine/helpers.js'\nimport { type FormModel } from '~/src/server/plugins/engine/models/FormModel.js'\nimport { type DetailItem } from '~/src/server/plugins/engine/models/types.js'\nimport { format as machineV2 } from '~/src/server/plugins/engine/outputFormatters/machine/v2.js'\nimport { FormAdapterSubmissionSchemaVersion } from '~/src/server/plugins/engine/types/enums.js'\nimport {\n type FormAdapterSubmissionMessageData,\n type FormAdapterSubmissionMessageMeta,\n type FormAdapterSubmissionMessagePayload,\n type FormAdapterSubmissionMessageResult,\n type FormContext\n} from '~/src/server/plugins/engine/types.js'\nimport { FormStatus } from '~/src/server/routes/types.js'\n\nexport function format(\n context: FormContext,\n items: DetailItem[],\n model: FormModel,\n submitResponse: SubmitResponsePayload,\n formStatus: ReturnType<typeof checkFormStatus>,\n formMetadata?: FormMetadata\n): string {\n const v2DataString = machineV2(\n context,\n items,\n model,\n submitResponse,\n formStatus\n )\n const v2DataParsed = JSON.parse(v2DataString) as {\n data: FormAdapterSubmissionMessageData\n }\n\n const csvFiles = extractCsvFiles(submitResponse)\n\n const transformedData = v2DataParsed.data\n\n const versionMetadata = getVersionMetadata(\n context.submittedVersionNumber,\n formMetadata\n )\n\n const meta: FormAdapterSubmissionMessageMeta = {\n schemaVersion: FormAdapterSubmissionSchemaVersion.V1,\n timestamp: new Date(),\n referenceNumber: context.referenceNumber,\n formName: model.name,\n formId: formMetadata?.id ?? '',\n formSlug: formMetadata?.slug ?? '',\n status: formStatus.isPreview ? FormStatus.Draft : FormStatus.Live,\n isPreview: formStatus.isPreview,\n notificationEmail: formMetadata?.notificationEmail ?? ''\n }\n\n if (versionMetadata) {\n meta.versionMetadata = versionMetadata\n }\n const data: FormAdapterSubmissionMessageData = transformedData\n\n const result: FormAdapterSubmissionMessageResult = {\n files: csvFiles\n }\n\n const payload: FormAdapterSubmissionMessagePayload = {\n meta,\n data,\n result\n }\n\n return JSON.stringify(payload)\n}\n\nexport function getVersionMetadata(\n submittedVersionNumber: number | undefined,\n formMetadata?: FormMetadata\n): { versionNumber: number; createdAt: Date } | undefined {\n if (!formMetadata?.versions?.length) {\n return undefined\n }\n\n if (submittedVersionNumber !== undefined) {\n const submittedVersion = formMetadata.versions.find(\n (v) => v.versionNumber === submittedVersionNumber\n )\n if (submittedVersion) {\n return {\n versionNumber: submittedVersion.versionNumber,\n createdAt: submittedVersion.createdAt\n }\n }\n }\n\n // fallback to first available version\n const firstVersion = formMetadata.versions[0]\n return {\n versionNumber: firstVersion.versionNumber,\n createdAt: firstVersion.createdAt\n }\n}\n\nfunction extractCsvFiles(\n submitResponse: SubmitResponsePayload\n): FormAdapterSubmissionMessageResult['files'] {\n const result =\n submitResponse.result as Partial<FormAdapterSubmissionMessageResult>\n\n return {\n main: result.files?.main ?? '',\n repeaters: result.files?.repeaters ?? {}\n }\n}\n"],"mappings":"AAQA,SAASA,MAAM,IAAIC,SAAS;AAC5B,SAASC,kCAAkC;AAQ3C,SAASC,UAAU;AAEnB,OAAO,SAASH,MAAMA,CACpBI,OAAoB,EACpBC,KAAmB,EACnBC,KAAgB,EAChBC,cAAqC,EACrCC,UAA8C,EAC9CC,YAA2B,EACnB;EACR,MAAMC,YAAY,GAAGT,SAAS,CAC5BG,OAAO,EACPC,KAAK,EACLC,KAAK,EACLC,cAAc,EACdC,UACF,CAAC;EACD,MAAMG,YAAY,GAAGC,IAAI,CAACC,KAAK,CAACH,YAAY,CAE3C;EAED,MAAMI,QAAQ,GAAGC,eAAe,CAACR,cAAc,CAAC;EAEhD,MAAMS,eAAe,GAAGL,YAAY,CAACM,IAAI;EAEzC,MAAMC,eAAe,GAAGC,kBAAkB,CACxCf,OAAO,CAACgB,sBAAsB,EAC9BX,YACF,CAAC;EAED,MAAMY,IAAsC,GAAG;IAC7CC,aAAa,EAAEpB,kCAAkC,CAACqB,EAAE;IACpDC,SAAS,EAAE,IAAIC,IAAI,CAAC,CAAC;IACrBC,eAAe,EAAEtB,OAAO,CAACsB,eAAe;IACxCC,QAAQ,EAAErB,KAAK,CAACsB,IAAI;IACpBC,MAAM,EAAEpB,YAAY,EAAEqB,EAAE,IAAI,EAAE;IAC9BC,QAAQ,EAAEtB,YAAY,EAAEuB,IAAI,IAAI,EAAE;IAClCC,MAAM,EAAEzB,UAAU,CAAC0B,SAAS,GAAG/B,UAAU,CAACgC,KAAK,GAAGhC,UAAU,CAACiC,IAAI;IACjEF,SAAS,EAAE1B,UAAU,CAAC0B,SAAS;IAC/BG,iBAAiB,EAAE5B,YAAY,EAAE4B,iBAAiB,IAAI;EACxD,CAAC;EAED,IAAInB,eAAe,EAAE;IACnBG,IAAI,CAACH,eAAe,GAAGA,eAAe;EACxC;EACA,MAAMD,IAAsC,GAAGD,eAAe;EAE9D,MAAMsB,MAA0C,GAAG;IACjDC,KAAK,EAAEzB;EACT,CAAC;EAED,MAAM0B,OAA4C,GAAG;IACnDnB,IAAI;IACJJ,IAAI;IACJqB;EACF,CAAC;EAED,OAAO1B,IAAI,CAAC6B,SAAS,CAACD,OAAO,CAAC;AAChC;AAEA,OAAO,SAASrB,kBAAkBA,CAChCC,sBAA0C,EAC1CX,YAA2B,EAC6B;EACxD,IAAI,CAACA,YAAY,EAAEiC,QAAQ,EAAEC,MAAM,EAAE;IACnC,OAAOC,SAAS;EAClB;EAEA,IAAIxB,sBAAsB,KAAKwB,SAAS,EAAE;IACxC,MAAMC,gBAAgB,GAAGpC,YAAY,CAACiC,QAAQ,CAACI,IAAI,CAChDC,CAAC,IAAKA,CAAC,CAACC,aAAa,KAAK5B,sBAC7B,CAAC;IACD,IAAIyB,gBAAgB,EAAE;MACpB,OAAO;QACLG,aAAa,EAAEH,gBAAgB,CAACG,aAAa;QAC7CC,SAAS,EAAEJ,gBAAgB,CAACI;MAC9B,CAAC;IACH;EACF;;EAEA;EACA,MAAMC,YAAY,GAAGzC,YAAY,CAACiC,QAAQ,CAAC,CAAC,CAAC;EAC7C,OAAO;IACLM,aAAa,EAAEE,YAAY,CAACF,aAAa;IACzCC,SAAS,EAAEC,YAAY,CAACD;EAC1B,CAAC;AACH;AAEA,SAASlC,eAAeA,CACtBR,cAAqC,EACQ;EAC7C,MAAM+B,MAAM,GACV/B,cAAc,CAAC+B,MAAqD;EAEtE,OAAO;IACLa,IAAI,EAAEb,MAAM,CAACC,KAAK,EAAEY,IAAI,IAAI,EAAE;IAC9BC,SAAS,EAAEd,MAAM,CAACC,KAAK,EAAEa,SAAS,IAAI,CAAC;EACzC,CAAC;AACH","ignoreList":[]}
1
+ {"version":3,"file":"v1.js","names":["categoriseData","FormAdapterSubmissionSchemaVersion","FormStatus","format","context","items","model","submitResponse","formStatus","formMetadata","csvFiles","extractCsvFiles","main","v2Main","v2Data","versionMetadata","getVersionMetadata","submittedVersionNumber","meta","schemaVersion","V1","timestamp","Date","referenceNumber","formName","name","formId","id","formSlug","slug","status","isPreview","Draft","Live","notificationEmail","Object","fromEntries","entries","map","key","value","undefined","data","result","files","payload","JSON","stringify","versions","length","submittedVersion","find","v","versionNumber","createdAt","firstVersion","repeaters"],"sources":["../../../../../../src/server/plugins/engine/outputFormatters/adapter/v1.ts"],"sourcesContent":["import {\n type FormMetadata,\n type SubmitResponsePayload\n} from '@defra/forms-model'\n\nimport { type checkFormStatus } from '~/src/server/plugins/engine/helpers.js'\nimport { type FormModel } from '~/src/server/plugins/engine/models/FormModel.js'\nimport { type DetailItem } from '~/src/server/plugins/engine/models/types.js'\nimport { categoriseData } from '~/src/server/plugins/engine/outputFormatters/machine/v2.js'\nimport { FormAdapterSubmissionSchemaVersion } from '~/src/server/plugins/engine/types/enums.js'\nimport {\n type FormAdapterSubmissionMessageData,\n type FormAdapterSubmissionMessageMeta,\n type FormAdapterSubmissionMessagePayload,\n type FormAdapterSubmissionMessageResult,\n type FormContext\n} from '~/src/server/plugins/engine/types.js'\nimport { FormStatus } from '~/src/server/routes/types.js'\n\nexport function format(\n context: FormContext,\n items: DetailItem[],\n model: FormModel,\n submitResponse: SubmitResponsePayload,\n formStatus: ReturnType<typeof checkFormStatus>,\n formMetadata?: FormMetadata\n): string {\n const csvFiles = extractCsvFiles(submitResponse)\n\n const { main: v2Main, ...v2Data } = categoriseData(items)\n\n const versionMetadata = getVersionMetadata(\n context.submittedVersionNumber,\n formMetadata\n )\n\n const meta: FormAdapterSubmissionMessageMeta = {\n schemaVersion: FormAdapterSubmissionSchemaVersion.V1,\n timestamp: new Date(),\n referenceNumber: context.referenceNumber,\n formName: model.name,\n formId: formMetadata?.id ?? '',\n formSlug: formMetadata?.slug ?? '',\n status: formStatus.isPreview ? FormStatus.Draft : FormStatus.Live,\n isPreview: formStatus.isPreview,\n notificationEmail: formMetadata?.notificationEmail ?? ''\n }\n\n if (versionMetadata) {\n meta.versionMetadata = versionMetadata\n }\n\n const main = Object.fromEntries(\n Object.entries(v2Main).map(([key, value]) => {\n if (value === undefined) {\n return [key, null]\n }\n\n return [key, value]\n })\n )\n\n const data: FormAdapterSubmissionMessageData = {\n main,\n ...v2Data\n }\n\n const result: FormAdapterSubmissionMessageResult = {\n files: csvFiles\n }\n\n const payload: FormAdapterSubmissionMessagePayload = {\n meta,\n data,\n result\n }\n\n return JSON.stringify(payload)\n}\n\nexport function getVersionMetadata(\n submittedVersionNumber: number | undefined,\n formMetadata?: FormMetadata\n): { versionNumber: number; createdAt: Date } | undefined {\n if (!formMetadata?.versions?.length) {\n return undefined\n }\n\n if (submittedVersionNumber !== undefined) {\n const submittedVersion = formMetadata.versions.find(\n (v) => v.versionNumber === submittedVersionNumber\n )\n if (submittedVersion) {\n return {\n versionNumber: submittedVersion.versionNumber,\n createdAt: submittedVersion.createdAt\n }\n }\n }\n\n // fallback to first available version\n const firstVersion = formMetadata.versions[0]\n return {\n versionNumber: firstVersion.versionNumber,\n createdAt: firstVersion.createdAt\n }\n}\n\nfunction extractCsvFiles(\n submitResponse: SubmitResponsePayload\n): FormAdapterSubmissionMessageResult['files'] {\n const result =\n submitResponse.result as Partial<FormAdapterSubmissionMessageResult>\n\n return {\n main: result.files?.main ?? '',\n repeaters: result.files?.repeaters ?? {}\n }\n}\n"],"mappings":"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","ignoreList":[]}
@@ -2,5 +2,42 @@ import { type SubmitResponsePayload } from '@defra/forms-model';
2
2
  import { type checkFormStatus } from '~/src/server/plugins/engine/helpers.js';
3
3
  import { type FormModel } from '~/src/server/plugins/engine/models/index.js';
4
4
  import { type DetailItem } from '~/src/server/plugins/engine/models/types.js';
5
- import { type FormContext } from '~/src/server/plugins/engine/types.js';
5
+ import { type FormContext, type RichFormValue } from '~/src/server/plugins/engine/types.js';
6
6
  export declare function format(context: FormContext, items: DetailItem[], model: FormModel, _submitResponse: SubmitResponsePayload, _formStatus: ReturnType<typeof checkFormStatus>): string;
7
+ /**
8
+ * Categories the form submission data into the "main" body and "repeaters".
9
+ *
10
+ * {
11
+ * main: {
12
+ * componentName: 'componentValue',
13
+ * },
14
+ * repeaters: {
15
+ * repeaterName: [
16
+ * {
17
+ * textComponentName: 'componentValue'
18
+ * },
19
+ * {
20
+ * richComponentName: { foo: 'bar', 'baz': true }
21
+ * }
22
+ * ]
23
+ * },
24
+ * files: {
25
+ * fileComponentName: [
26
+ * {
27
+ * fileId: '123-456-789',
28
+ * fileName: 'example.pdf',
29
+ * userDownloadLink: 'https://forms-designer/file-download/123-456-789'
30
+ * }
31
+ * ]
32
+ * }
33
+ * }
34
+ */
35
+ export declare function categoriseData(items: DetailItem[]): {
36
+ main: Record<string, RichFormValue>;
37
+ repeaters: Record<string, Record<string, RichFormValue>[]>;
38
+ files: Record<string, {
39
+ fileId: string;
40
+ fileName: string;
41
+ userDownloadLink: string;
42
+ }[]>;
43
+ };
@@ -46,7 +46,7 @@ export function format(context, items, model, _submitResponse, _formStatus) {
46
46
  * }
47
47
  * }
48
48
  */
49
- function categoriseData(items) {
49
+ export function categoriseData(items) {
50
50
  const output = {
51
51
  main: {},
52
52
  repeaters: {},
@@ -1 +1 @@
1
- {"version":3,"file":"v2.js","names":["config","FileUploadField","designerUrl","get","format","context","items","model","_submitResponse","_formStatus","now","Date","categorisedData","categoriseData","meta","schemaVersion","timestamp","toISOString","definition","def","referenceNumber","data","body","JSON","stringify","output","main","repeaters","files","forEach","item","name","state","extractRepeaters","isFileUploadFieldItem","extractFileUploads","field","getFormValueFromState","subItems","inputRepeaterItem","outputRepeaterItem","repeaterComponent","push","fileUploadState","map","fileState","file","status","form","fileId","fileName","filename","userDownloadLink"],"sources":["../../../../../../src/server/plugins/engine/outputFormatters/machine/v2.ts"],"sourcesContent":["import { type SubmitResponsePayload } from '@defra/forms-model'\n\nimport { config } from '~/src/config/index.js'\nimport { FileUploadField } from '~/src/server/plugins/engine/components/index.js'\nimport { type checkFormStatus } from '~/src/server/plugins/engine/helpers.js'\nimport { type FormModel } from '~/src/server/plugins/engine/models/index.js'\nimport {\n type DetailItem,\n type DetailItemField,\n type DetailItemRepeat\n} from '~/src/server/plugins/engine/models/types.js'\nimport {\n type FileUploadFieldDetailitem,\n type FormAdapterFile,\n type FormContext,\n type RichFormValue\n} from '~/src/server/plugins/engine/types.js'\n\nconst designerUrl = config.get('designerUrl')\n\nexport function format(\n context: FormContext,\n items: DetailItem[],\n model: FormModel,\n _submitResponse: SubmitResponsePayload,\n _formStatus: ReturnType<typeof checkFormStatus>\n) {\n const now = new Date()\n\n const categorisedData = categoriseData(items)\n\n const meta: Record<string, unknown> = {\n schemaVersion: '2',\n timestamp: now.toISOString(),\n definition: model.def,\n referenceNumber: context.referenceNumber\n }\n\n const data = {\n meta,\n data: categorisedData\n }\n\n const body = JSON.stringify(data)\n\n return body\n}\n\n/**\n * Categories the form submission data into the \"main\" body and \"repeaters\".\n *\n * {\n * main: {\n * componentName: 'componentValue',\n * },\n * repeaters: {\n * repeaterName: [\n * {\n * textComponentName: 'componentValue'\n * },\n * {\n * richComponentName: { foo: 'bar', 'baz': true }\n * }\n * ]\n * },\n * files: {\n * fileComponentName: [\n * {\n * fileId: '123-456-789',\n * fileName: 'example.pdf',\n * userDownloadLink: 'https://forms-designer/file-download/123-456-789'\n * }\n * ]\n * }\n * }\n */\nfunction categoriseData(items: DetailItem[]) {\n const output: {\n main: Record<string, RichFormValue>\n repeaters: Record<string, Record<string, RichFormValue>[]>\n files: Record<\n string,\n { fileId: string; fileName: string; userDownloadLink: string }[]\n >\n } = { main: {}, repeaters: {}, files: {} }\n\n items.forEach((item) => {\n const { name, state } = item\n\n if ('subItems' in item) {\n output.repeaters[name] = extractRepeaters(item)\n } else if (isFileUploadFieldItem(item)) {\n output.files[name] = extractFileUploads(item)\n } else {\n output.main[name] = item.field.getFormValueFromState(state)\n }\n })\n\n return output\n}\n\n/**\n * Returns the \"repeaters\" section of the response body\n * @param item - the repeater item\n * @returns the repeater item\n */\nfunction extractRepeaters(item: DetailItemRepeat) {\n const repeaters: Record<string, RichFormValue>[] = []\n\n item.subItems.forEach((inputRepeaterItem) => {\n const outputRepeaterItem: Record<string, RichFormValue> = {}\n\n inputRepeaterItem.forEach((repeaterComponent) => {\n const { field, state } = repeaterComponent\n\n outputRepeaterItem[repeaterComponent.name] =\n field.getFormValueFromState(state)\n })\n\n repeaters.push(outputRepeaterItem)\n })\n\n return repeaters\n}\n\n/**\n * Returns the \"files\" section of the response body\n * @param item - the file upload item in the form\n * @returns the file upload data\n */\nfunction extractFileUploads(\n item: FileUploadFieldDetailitem\n): FormAdapterFile[] {\n const fileUploadState = item.field.getFormValueFromState(item.state) ?? []\n\n return fileUploadState.map((fileState) => {\n const { file } = fileState.status.form\n return {\n fileId: file.fileId,\n fileName: file.filename,\n userDownloadLink: `${designerUrl}/file-download/${file.fileId}`\n }\n })\n}\n\nfunction isFileUploadFieldItem(\n item: DetailItemField\n): item is FileUploadFieldDetailitem {\n return item.field instanceof FileUploadField\n}\n"],"mappings":"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","ignoreList":[]}
1
+ {"version":3,"file":"v2.js","names":["config","FileUploadField","designerUrl","get","format","context","items","model","_submitResponse","_formStatus","now","Date","categorisedData","categoriseData","meta","schemaVersion","timestamp","toISOString","definition","def","referenceNumber","data","body","JSON","stringify","output","main","repeaters","files","forEach","item","name","state","extractRepeaters","isFileUploadFieldItem","extractFileUploads","field","getFormValueFromState","subItems","inputRepeaterItem","outputRepeaterItem","repeaterComponent","push","fileUploadState","map","fileState","file","status","form","fileId","fileName","filename","userDownloadLink"],"sources":["../../../../../../src/server/plugins/engine/outputFormatters/machine/v2.ts"],"sourcesContent":["import { type SubmitResponsePayload } from '@defra/forms-model'\n\nimport { config } from '~/src/config/index.js'\nimport { FileUploadField } from '~/src/server/plugins/engine/components/index.js'\nimport { type checkFormStatus } from '~/src/server/plugins/engine/helpers.js'\nimport { type FormModel } from '~/src/server/plugins/engine/models/index.js'\nimport {\n type DetailItem,\n type DetailItemField,\n type DetailItemRepeat\n} from '~/src/server/plugins/engine/models/types.js'\nimport {\n type FileUploadFieldDetailitem,\n type FormAdapterFile,\n type FormContext,\n type RichFormValue\n} from '~/src/server/plugins/engine/types.js'\n\nconst designerUrl = config.get('designerUrl')\n\nexport function format(\n context: FormContext,\n items: DetailItem[],\n model: FormModel,\n _submitResponse: SubmitResponsePayload,\n _formStatus: ReturnType<typeof checkFormStatus>\n) {\n const now = new Date()\n\n const categorisedData = categoriseData(items)\n\n const meta: Record<string, unknown> = {\n schemaVersion: '2',\n timestamp: now.toISOString(),\n definition: model.def,\n referenceNumber: context.referenceNumber\n }\n\n const data = {\n meta,\n data: categorisedData\n }\n\n const body = JSON.stringify(data)\n\n return body\n}\n\n/**\n * Categories the form submission data into the \"main\" body and \"repeaters\".\n *\n * {\n * main: {\n * componentName: 'componentValue',\n * },\n * repeaters: {\n * repeaterName: [\n * {\n * textComponentName: 'componentValue'\n * },\n * {\n * richComponentName: { foo: 'bar', 'baz': true }\n * }\n * ]\n * },\n * files: {\n * fileComponentName: [\n * {\n * fileId: '123-456-789',\n * fileName: 'example.pdf',\n * userDownloadLink: 'https://forms-designer/file-download/123-456-789'\n * }\n * ]\n * }\n * }\n */\nexport function categoriseData(items: DetailItem[]) {\n const output: {\n main: Record<string, RichFormValue>\n repeaters: Record<string, Record<string, RichFormValue>[]>\n files: Record<\n string,\n { fileId: string; fileName: string; userDownloadLink: string }[]\n >\n } = { main: {}, repeaters: {}, files: {} }\n\n items.forEach((item) => {\n const { name, state } = item\n\n if ('subItems' in item) {\n output.repeaters[name] = extractRepeaters(item)\n } else if (isFileUploadFieldItem(item)) {\n output.files[name] = extractFileUploads(item)\n } else {\n output.main[name] = item.field.getFormValueFromState(state)\n }\n })\n\n return output\n}\n\n/**\n * Returns the \"repeaters\" section of the response body\n * @param item - the repeater item\n * @returns the repeater item\n */\nfunction extractRepeaters(item: DetailItemRepeat) {\n const repeaters: Record<string, RichFormValue>[] = []\n\n item.subItems.forEach((inputRepeaterItem) => {\n const outputRepeaterItem: Record<string, RichFormValue> = {}\n\n inputRepeaterItem.forEach((repeaterComponent) => {\n const { field, state } = repeaterComponent\n\n outputRepeaterItem[repeaterComponent.name] =\n field.getFormValueFromState(state)\n })\n\n repeaters.push(outputRepeaterItem)\n })\n\n return repeaters\n}\n\n/**\n * Returns the \"files\" section of the response body\n * @param item - the file upload item in the form\n * @returns the file upload data\n */\nfunction extractFileUploads(\n item: FileUploadFieldDetailitem\n): FormAdapterFile[] {\n const fileUploadState = item.field.getFormValueFromState(item.state) ?? []\n\n return fileUploadState.map((fileState) => {\n const { file } = fileState.status.form\n return {\n fileId: file.fileId,\n fileName: file.filename,\n userDownloadLink: `${designerUrl}/file-download/${file.fileId}`\n }\n })\n}\n\nfunction isFileUploadFieldItem(\n item: DetailItemField\n): item is FileUploadFieldDetailitem {\n return item.field instanceof FileUploadField\n}\n"],"mappings":"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","ignoreList":[]}
@@ -317,7 +317,7 @@ export type FileUploadFieldDetailitem = Omit<DetailItemField, 'field'> & {
317
317
  };
318
318
  export type RichFormValue = FormValue | FormPayload | DatePartsState | MonthYearState | UkAddressState;
319
319
  export interface FormAdapterSubmissionMessageData {
320
- main: Record<string, RichFormValue>;
320
+ main: Record<string, RichFormValue | null>;
321
321
  repeaters: Record<string, Record<string, RichFormValue>[]>;
322
322
  files: Record<string, FormAdapterFile[]>;
323
323
  }
@@ -1 +1 @@
1
- {"version":3,"file":"types.js","names":["FileStatus","UploadStatus"],"sources":["../../../../src/server/plugins/engine/types.ts"],"sourcesContent":["import {\n type ComponentDef,\n type Event,\n type FormDefinition,\n type FormMetadata,\n type FormVersionMetadata,\n type Item,\n type List,\n type Page\n} from '@defra/forms-model'\nimport {\n type PluginProperties,\n type Request,\n type ResponseObject\n} from '@hapi/hapi'\nimport { type JoiExpression, type ValidationErrorItem } from 'joi'\n\nimport { FormComponent } from '~/src/server/plugins/engine/components/FormComponent.js'\nimport { type UkAddressState } from '~/src/server/plugins/engine/components/UkAddressField.js'\nimport { type Component } from '~/src/server/plugins/engine/components/helpers/components.js'\nimport { type FileUploadField } from '~/src/server/plugins/engine/components/index.js'\nimport {\n type BackLink,\n type ComponentText,\n type ComponentViewModel,\n type DatePartsState,\n type MonthYearState\n} from '~/src/server/plugins/engine/components/types.js'\nimport { type FormModel } from '~/src/server/plugins/engine/models/index.js'\nimport { type DetailItemField } from '~/src/server/plugins/engine/models/types.js'\nimport { type PageController } from '~/src/server/plugins/engine/pageControllers/PageController.js'\nimport { type PageControllerClass } from '~/src/server/plugins/engine/pageControllers/helpers/pages.js'\nimport {\n type FileStatus,\n type FormAdapterSubmissionSchemaVersion,\n type UploadStatus\n} from '~/src/server/plugins/engine/types/enums.js'\nimport { type ViewContext } from '~/src/server/plugins/nunjucks/types.js'\nimport {\n type FormAction,\n type FormParams,\n type FormRequest,\n type FormRequestPayload,\n type FormResponseToolkit,\n type FormStatus\n} from '~/src/server/routes/types.js'\nimport { type CacheService } from '~/src/server/services/cacheService.js'\nimport { type RequestOptions } from '~/src/server/services/httpService.js'\nimport { type Services } from '~/src/server/types.js'\n\nexport type AnyFormRequest = FormRequest | FormRequestPayload\nexport type AnyRequest = Request | AnyFormRequest\n\n/**\n * Form submission state stores the following in Redis:\n * Props containing user's submitted values as `{ [inputId]: value }` or as `{ [sectionName]: { [inputName]: value } }`\n * a) . e.g:\n * ```ts\n * {\n * _C9PRHmsgt: 'Ben',\n * WfLk9McjzX: 'Music',\n * IK7jkUFCBL: 'Royal Academy of Music'\n * }\n * ```\n *\n * b)\n * ```ts\n * {\n * checkBeforeYouStart: { ukPassport: true },\n * applicantDetails: {\n * numberOfApplicants: 1,\n * phoneNumber: '77777777',\n * emailAddress: 'aaa@aaa.com'\n * },\n * applicantOneDetails: {\n * firstName: 'a',\n * middleName: 'a',\n * lastName: 'a',\n * address: { addressLine1: 'a', addressLine2: 'a', town: 'a', postcode: 'a' }\n * }\n * }\n * ```\n */\n\n/**\n * Form submission state\n */\nexport type FormSubmissionState = {\n upload?: Record<string, TempFileState>\n} & FormState\n\nexport interface FormSubmissionError\n extends Pick<ValidationErrorItem, 'context' | 'path'> {\n href: string // e.g: '#dateField__day'\n name: string // e.g: 'dateField__day'\n text: string // e.g: 'Date field must be a real date'\n}\n\nexport interface FormPayloadParams {\n action?: FormAction\n confirm?: true\n crumb?: string\n itemId?: string\n}\n\n/**\n * Form POST for question pages\n * (after Joi has converted value types)\n */\nexport type FormPayload = FormPayloadParams & Partial<Record<string, FormValue>>\n\nexport type FormValue =\n | Item['value']\n | Item['value'][]\n | UploadState\n | RepeatListState\n | undefined\n\nexport type FormState = Partial<Record<string, FormStateValue>>\nexport type FormStateValue = Exclude<FormValue, undefined> | null\n\nexport interface FormValidationResult<\n ValueType extends FormPayload | FormSubmissionState\n> {\n value: ValueType\n errors: FormSubmissionError[] | undefined\n}\n\nexport interface FormContext {\n /**\n * Evaluation form state only (filtered by visited paths),\n * with values formatted for condition evaluation using\n * {@link FormComponent.getContextValueFromState}\n */\n evaluationState: FormState\n\n /**\n * Relevant form state only (filtered by visited paths)\n */\n relevantState: FormState\n\n /**\n * Relevant pages only (filtered by visited paths)\n */\n relevantPages: PageControllerClass[]\n\n /**\n * Form submission payload (single page)\n */\n payload: FormPayload\n\n /**\n * Form submission state (entire form)\n */\n state: FormSubmissionState\n\n /**\n * Validation errors (entire form)\n */\n errors?: FormSubmissionError[]\n\n /**\n * Visited paths evaluated from form state\n */\n paths: string[]\n\n /**\n * Preview URL direct access is allowed\n */\n isForceAccess: boolean\n\n /**\n * Miscellaneous extra data from event responses\n */\n data: object\n\n pageDefMap: Map<string, Page>\n listDefMap: Map<string, List>\n componentDefMap: Map<string, ComponentDef>\n pageMap: Map<string, PageControllerClass>\n componentMap: Map<string, Component>\n referenceNumber: string\n submittedVersionNumber?: number\n}\n\nexport type FormContextRequest = (\n | {\n method: 'get'\n payload?: undefined\n }\n | {\n method: 'post'\n payload: FormPayload\n }\n | {\n method: FormRequest['method']\n payload?: object | undefined\n }\n) &\n Pick<\n FormRequest,\n 'app' | 'method' | 'params' | 'path' | 'query' | 'url' | 'server'\n >\n\nexport interface UploadInitiateResponse {\n uploadId: string\n uploadUrl: string\n statusUrl: string\n}\n\nexport {\n FileStatus,\n UploadStatus\n} from '~/src/server/plugins/engine/types/enums.js'\n\nexport type UploadState = FileState[]\n\nexport type FileUpload = {\n fileId: string\n filename: string\n contentLength: number\n} & (\n | {\n fileStatus: FileStatus.complete | FileStatus.rejected | FileStatus.pending\n errorMessage?: string\n }\n | {\n fileStatus: FileStatus.complete\n errorMessage?: undefined\n }\n)\n\nexport interface FileUploadMetadata {\n retrievalKey: string\n}\n\nexport type UploadStatusResponse =\n | {\n uploadStatus: UploadStatus.initiated\n metadata: FileUploadMetadata\n form: { file?: undefined }\n }\n | {\n uploadStatus: UploadStatus.pending | UploadStatus.ready\n metadata: FileUploadMetadata\n form: { file: FileUpload }\n numberOfRejectedFiles?: number\n }\n | {\n uploadStatus: UploadStatus.ready\n metadata: FileUploadMetadata\n form: { file: FileUpload }\n numberOfRejectedFiles: 0\n }\n\nexport type UploadStatusFileResponse = Exclude<\n UploadStatusResponse,\n { uploadStatus: UploadStatus.initiated }\n>\n\nexport interface FileState {\n uploadId: string\n status: UploadStatusFileResponse\n}\n\nexport interface TempFileState {\n upload?: UploadInitiateResponse\n files: UploadState\n}\n\nexport interface RepeatItemState extends FormPayload {\n itemId: string\n}\n\nexport type RepeatListState = RepeatItemState[]\n\nexport interface CheckAnswers {\n title?: ComponentText\n summaryList: SummaryList\n}\n\nexport interface SummaryList {\n classes?: string\n rows: SummaryListRow[]\n}\n\nexport interface SummaryListRow {\n key: ComponentText\n value: ComponentText\n actions?: { items: SummaryListAction[] }\n}\n\nexport type SummaryListAction = ComponentText & {\n href: string\n visuallyHiddenText: string\n}\n\nexport interface PageViewModelBase extends Partial<ViewContext> {\n page: PageController\n name?: string\n pageTitle: string\n sectionTitle?: string\n showTitle: boolean\n isStartPage: boolean\n backLink?: BackLink\n feedbackLink?: string\n serviceUrl: string\n phaseTag?: string\n}\n\nexport interface ItemDeletePageViewModel extends PageViewModelBase {\n context: FormContext\n itemTitle: string\n confirmation?: ComponentText\n buttonConfirm: ComponentText\n buttonCancel: ComponentText\n}\n\nexport interface FormPageViewModel extends PageViewModelBase {\n components: ComponentViewModel[]\n context: FormContext\n errors?: FormSubmissionError[]\n hasMissingNotificationEmail?: boolean\n allowSaveAndExit: boolean\n}\n\nexport interface RepeaterSummaryPageViewModel extends PageViewModelBase {\n context: FormContext\n errors?: FormSubmissionError[]\n checkAnswers: CheckAnswers[]\n repeatTitle: string\n allowSaveAndExit: boolean\n}\n\nexport interface FeaturedFormPageViewModel extends FormPageViewModel {\n formAction?: string\n formComponent: ComponentViewModel\n componentsBefore: ComponentViewModel[]\n uploadId: string | undefined\n proxyUrl: string | null\n}\n\nexport type PageViewModel =\n | PageViewModelBase\n | ItemDeletePageViewModel\n | FormPageViewModel\n | RepeaterSummaryPageViewModel\n | FeaturedFormPageViewModel\n\nexport type GlobalFunction = (value: unknown) => unknown\nexport type FilterFunction = (value: unknown) => unknown\nexport interface ErrorMessageTemplate {\n type: string\n template: JoiExpression\n}\n\nexport interface ErrorMessageTemplateList {\n baseErrors: ErrorMessageTemplate[]\n advancedSettingsErrors: ErrorMessageTemplate[]\n}\n\nexport type PreparePageEventRequestOptions = (\n options: RequestOptions,\n event: Event,\n page: PageControllerClass,\n context: FormContext\n) => void\n\nexport type OnRequestCallback = (\n request: AnyFormRequest,\n params: FormParams,\n definition: FormDefinition,\n metadata: FormMetadata\n) => void\n\nexport type SaveAndExitHandler = (\n request: FormRequestPayload,\n h: FormResponseToolkit,\n context: FormContext\n) => ResponseObject\n\nexport interface PluginOptions {\n model?: FormModel\n services?: Services\n controllers?: Record<string, typeof PageController>\n cache?: CacheService | string\n globals?: Record<string, GlobalFunction>\n filters?: Record<string, FilterFunction>\n saveAndExit?: SaveAndExitHandler\n pluginPath?: string\n nunjucks: {\n baseLayoutPath: string\n paths: string[]\n }\n viewContext: PluginProperties['forms-engine-plugin']['viewContext']\n preparePageEventRequestOptions?: PreparePageEventRequestOptions\n onRequest?: OnRequestCallback\n baseUrl: string // base URL of the application, protocol and hostname e.g. \"https://myapp.com\"\n}\n\nexport interface FormAdapterSubmissionMessageMeta {\n schemaVersion: FormAdapterSubmissionSchemaVersion\n timestamp: Date\n referenceNumber: string\n formName: string\n formId: string\n formSlug: string\n status: FormStatus\n isPreview: boolean\n notificationEmail: string\n versionMetadata?: FormVersionMetadata\n}\n\nexport type FormAdapterSubmissionMessageMetaSerialised = Omit<\n FormAdapterSubmissionMessageMeta,\n 'schemaVersion' | 'timestamp' | 'status'\n> & {\n schemaVersion: number\n status: string\n timestamp: string\n}\nexport interface FormAdapterFile {\n fileName: string\n fileId: string\n userDownloadLink: string\n}\n\nexport interface FormAdapterSubmissionMessageResult {\n files: {\n main: string\n repeaters: Record<string, string>\n }\n}\n\n/**\n * A detail item specifically for files\n */\nexport type FileUploadFieldDetailitem = Omit<DetailItemField, 'field'> & {\n field: FileUploadField\n}\nexport type RichFormValue =\n | FormValue\n | FormPayload\n | DatePartsState\n | MonthYearState\n | UkAddressState\n\nexport interface FormAdapterSubmissionMessageData {\n main: Record<string, RichFormValue>\n repeaters: Record<string, Record<string, RichFormValue>[]>\n files: Record<string, FormAdapterFile[]>\n}\n\nexport interface FormAdapterSubmissionMessagePayload {\n meta: FormAdapterSubmissionMessageMeta\n data: FormAdapterSubmissionMessageData\n result: FormAdapterSubmissionMessageResult\n}\n\nexport interface FormAdapterSubmissionMessage\n extends FormAdapterSubmissionMessagePayload {\n messageId: string\n recordCreatedAt: Date\n}\n\nexport interface FormAdapterSubmissionService {\n handleFormSubmission: (\n submissionMessage: FormAdapterSubmissionMessage\n ) => unknown\n}\n"],"mappings":"AAqDA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;AAEA;AACA;AACA;;AAmBA;AACA;AACA;AACA;;AAsGA,SACEA,UAAU,EACVC,YAAY;;AA8Nd;AACA;AACA","ignoreList":[]}
1
+ {"version":3,"file":"types.js","names":["FileStatus","UploadStatus"],"sources":["../../../../src/server/plugins/engine/types.ts"],"sourcesContent":["import {\n type ComponentDef,\n type Event,\n type FormDefinition,\n type FormMetadata,\n type FormVersionMetadata,\n type Item,\n type List,\n type Page\n} from '@defra/forms-model'\nimport {\n type PluginProperties,\n type Request,\n type ResponseObject\n} from '@hapi/hapi'\nimport { type JoiExpression, type ValidationErrorItem } from 'joi'\n\nimport { FormComponent } from '~/src/server/plugins/engine/components/FormComponent.js'\nimport { type UkAddressState } from '~/src/server/plugins/engine/components/UkAddressField.js'\nimport { type Component } from '~/src/server/plugins/engine/components/helpers/components.js'\nimport { type FileUploadField } from '~/src/server/plugins/engine/components/index.js'\nimport {\n type BackLink,\n type ComponentText,\n type ComponentViewModel,\n type DatePartsState,\n type MonthYearState\n} from '~/src/server/plugins/engine/components/types.js'\nimport { type FormModel } from '~/src/server/plugins/engine/models/index.js'\nimport { type DetailItemField } from '~/src/server/plugins/engine/models/types.js'\nimport { type PageController } from '~/src/server/plugins/engine/pageControllers/PageController.js'\nimport { type PageControllerClass } from '~/src/server/plugins/engine/pageControllers/helpers/pages.js'\nimport {\n type FileStatus,\n type FormAdapterSubmissionSchemaVersion,\n type UploadStatus\n} from '~/src/server/plugins/engine/types/enums.js'\nimport { type ViewContext } from '~/src/server/plugins/nunjucks/types.js'\nimport {\n type FormAction,\n type FormParams,\n type FormRequest,\n type FormRequestPayload,\n type FormResponseToolkit,\n type FormStatus\n} from '~/src/server/routes/types.js'\nimport { type CacheService } from '~/src/server/services/cacheService.js'\nimport { type RequestOptions } from '~/src/server/services/httpService.js'\nimport { type Services } from '~/src/server/types.js'\n\nexport type AnyFormRequest = FormRequest | FormRequestPayload\nexport type AnyRequest = Request | AnyFormRequest\n\n/**\n * Form submission state stores the following in Redis:\n * Props containing user's submitted values as `{ [inputId]: value }` or as `{ [sectionName]: { [inputName]: value } }`\n * a) . e.g:\n * ```ts\n * {\n * _C9PRHmsgt: 'Ben',\n * WfLk9McjzX: 'Music',\n * IK7jkUFCBL: 'Royal Academy of Music'\n * }\n * ```\n *\n * b)\n * ```ts\n * {\n * checkBeforeYouStart: { ukPassport: true },\n * applicantDetails: {\n * numberOfApplicants: 1,\n * phoneNumber: '77777777',\n * emailAddress: 'aaa@aaa.com'\n * },\n * applicantOneDetails: {\n * firstName: 'a',\n * middleName: 'a',\n * lastName: 'a',\n * address: { addressLine1: 'a', addressLine2: 'a', town: 'a', postcode: 'a' }\n * }\n * }\n * ```\n */\n\n/**\n * Form submission state\n */\nexport type FormSubmissionState = {\n upload?: Record<string, TempFileState>\n} & FormState\n\nexport interface FormSubmissionError\n extends Pick<ValidationErrorItem, 'context' | 'path'> {\n href: string // e.g: '#dateField__day'\n name: string // e.g: 'dateField__day'\n text: string // e.g: 'Date field must be a real date'\n}\n\nexport interface FormPayloadParams {\n action?: FormAction\n confirm?: true\n crumb?: string\n itemId?: string\n}\n\n/**\n * Form POST for question pages\n * (after Joi has converted value types)\n */\nexport type FormPayload = FormPayloadParams & Partial<Record<string, FormValue>>\n\nexport type FormValue =\n | Item['value']\n | Item['value'][]\n | UploadState\n | RepeatListState\n | undefined\n\nexport type FormState = Partial<Record<string, FormStateValue>>\nexport type FormStateValue = Exclude<FormValue, undefined> | null\n\nexport interface FormValidationResult<\n ValueType extends FormPayload | FormSubmissionState\n> {\n value: ValueType\n errors: FormSubmissionError[] | undefined\n}\n\nexport interface FormContext {\n /**\n * Evaluation form state only (filtered by visited paths),\n * with values formatted for condition evaluation using\n * {@link FormComponent.getContextValueFromState}\n */\n evaluationState: FormState\n\n /**\n * Relevant form state only (filtered by visited paths)\n */\n relevantState: FormState\n\n /**\n * Relevant pages only (filtered by visited paths)\n */\n relevantPages: PageControllerClass[]\n\n /**\n * Form submission payload (single page)\n */\n payload: FormPayload\n\n /**\n * Form submission state (entire form)\n */\n state: FormSubmissionState\n\n /**\n * Validation errors (entire form)\n */\n errors?: FormSubmissionError[]\n\n /**\n * Visited paths evaluated from form state\n */\n paths: string[]\n\n /**\n * Preview URL direct access is allowed\n */\n isForceAccess: boolean\n\n /**\n * Miscellaneous extra data from event responses\n */\n data: object\n\n pageDefMap: Map<string, Page>\n listDefMap: Map<string, List>\n componentDefMap: Map<string, ComponentDef>\n pageMap: Map<string, PageControllerClass>\n componentMap: Map<string, Component>\n referenceNumber: string\n submittedVersionNumber?: number\n}\n\nexport type FormContextRequest = (\n | {\n method: 'get'\n payload?: undefined\n }\n | {\n method: 'post'\n payload: FormPayload\n }\n | {\n method: FormRequest['method']\n payload?: object | undefined\n }\n) &\n Pick<\n FormRequest,\n 'app' | 'method' | 'params' | 'path' | 'query' | 'url' | 'server'\n >\n\nexport interface UploadInitiateResponse {\n uploadId: string\n uploadUrl: string\n statusUrl: string\n}\n\nexport {\n FileStatus,\n UploadStatus\n} from '~/src/server/plugins/engine/types/enums.js'\n\nexport type UploadState = FileState[]\n\nexport type FileUpload = {\n fileId: string\n filename: string\n contentLength: number\n} & (\n | {\n fileStatus: FileStatus.complete | FileStatus.rejected | FileStatus.pending\n errorMessage?: string\n }\n | {\n fileStatus: FileStatus.complete\n errorMessage?: undefined\n }\n)\n\nexport interface FileUploadMetadata {\n retrievalKey: string\n}\n\nexport type UploadStatusResponse =\n | {\n uploadStatus: UploadStatus.initiated\n metadata: FileUploadMetadata\n form: { file?: undefined }\n }\n | {\n uploadStatus: UploadStatus.pending | UploadStatus.ready\n metadata: FileUploadMetadata\n form: { file: FileUpload }\n numberOfRejectedFiles?: number\n }\n | {\n uploadStatus: UploadStatus.ready\n metadata: FileUploadMetadata\n form: { file: FileUpload }\n numberOfRejectedFiles: 0\n }\n\nexport type UploadStatusFileResponse = Exclude<\n UploadStatusResponse,\n { uploadStatus: UploadStatus.initiated }\n>\n\nexport interface FileState {\n uploadId: string\n status: UploadStatusFileResponse\n}\n\nexport interface TempFileState {\n upload?: UploadInitiateResponse\n files: UploadState\n}\n\nexport interface RepeatItemState extends FormPayload {\n itemId: string\n}\n\nexport type RepeatListState = RepeatItemState[]\n\nexport interface CheckAnswers {\n title?: ComponentText\n summaryList: SummaryList\n}\n\nexport interface SummaryList {\n classes?: string\n rows: SummaryListRow[]\n}\n\nexport interface SummaryListRow {\n key: ComponentText\n value: ComponentText\n actions?: { items: SummaryListAction[] }\n}\n\nexport type SummaryListAction = ComponentText & {\n href: string\n visuallyHiddenText: string\n}\n\nexport interface PageViewModelBase extends Partial<ViewContext> {\n page: PageController\n name?: string\n pageTitle: string\n sectionTitle?: string\n showTitle: boolean\n isStartPage: boolean\n backLink?: BackLink\n feedbackLink?: string\n serviceUrl: string\n phaseTag?: string\n}\n\nexport interface ItemDeletePageViewModel extends PageViewModelBase {\n context: FormContext\n itemTitle: string\n confirmation?: ComponentText\n buttonConfirm: ComponentText\n buttonCancel: ComponentText\n}\n\nexport interface FormPageViewModel extends PageViewModelBase {\n components: ComponentViewModel[]\n context: FormContext\n errors?: FormSubmissionError[]\n hasMissingNotificationEmail?: boolean\n allowSaveAndExit: boolean\n}\n\nexport interface RepeaterSummaryPageViewModel extends PageViewModelBase {\n context: FormContext\n errors?: FormSubmissionError[]\n checkAnswers: CheckAnswers[]\n repeatTitle: string\n allowSaveAndExit: boolean\n}\n\nexport interface FeaturedFormPageViewModel extends FormPageViewModel {\n formAction?: string\n formComponent: ComponentViewModel\n componentsBefore: ComponentViewModel[]\n uploadId: string | undefined\n proxyUrl: string | null\n}\n\nexport type PageViewModel =\n | PageViewModelBase\n | ItemDeletePageViewModel\n | FormPageViewModel\n | RepeaterSummaryPageViewModel\n | FeaturedFormPageViewModel\n\nexport type GlobalFunction = (value: unknown) => unknown\nexport type FilterFunction = (value: unknown) => unknown\nexport interface ErrorMessageTemplate {\n type: string\n template: JoiExpression\n}\n\nexport interface ErrorMessageTemplateList {\n baseErrors: ErrorMessageTemplate[]\n advancedSettingsErrors: ErrorMessageTemplate[]\n}\n\nexport type PreparePageEventRequestOptions = (\n options: RequestOptions,\n event: Event,\n page: PageControllerClass,\n context: FormContext\n) => void\n\nexport type OnRequestCallback = (\n request: AnyFormRequest,\n params: FormParams,\n definition: FormDefinition,\n metadata: FormMetadata\n) => void\n\nexport type SaveAndExitHandler = (\n request: FormRequestPayload,\n h: FormResponseToolkit,\n context: FormContext\n) => ResponseObject\n\nexport interface PluginOptions {\n model?: FormModel\n services?: Services\n controllers?: Record<string, typeof PageController>\n cache?: CacheService | string\n globals?: Record<string, GlobalFunction>\n filters?: Record<string, FilterFunction>\n saveAndExit?: SaveAndExitHandler\n pluginPath?: string\n nunjucks: {\n baseLayoutPath: string\n paths: string[]\n }\n viewContext: PluginProperties['forms-engine-plugin']['viewContext']\n preparePageEventRequestOptions?: PreparePageEventRequestOptions\n onRequest?: OnRequestCallback\n baseUrl: string // base URL of the application, protocol and hostname e.g. \"https://myapp.com\"\n}\n\nexport interface FormAdapterSubmissionMessageMeta {\n schemaVersion: FormAdapterSubmissionSchemaVersion\n timestamp: Date\n referenceNumber: string\n formName: string\n formId: string\n formSlug: string\n status: FormStatus\n isPreview: boolean\n notificationEmail: string\n versionMetadata?: FormVersionMetadata\n}\n\nexport type FormAdapterSubmissionMessageMetaSerialised = Omit<\n FormAdapterSubmissionMessageMeta,\n 'schemaVersion' | 'timestamp' | 'status'\n> & {\n schemaVersion: number\n status: string\n timestamp: string\n}\nexport interface FormAdapterFile {\n fileName: string\n fileId: string\n userDownloadLink: string\n}\n\nexport interface FormAdapterSubmissionMessageResult {\n files: {\n main: string\n repeaters: Record<string, string>\n }\n}\n\n/**\n * A detail item specifically for files\n */\nexport type FileUploadFieldDetailitem = Omit<DetailItemField, 'field'> & {\n field: FileUploadField\n}\nexport type RichFormValue =\n | FormValue\n | FormPayload\n | DatePartsState\n | MonthYearState\n | UkAddressState\n\nexport interface FormAdapterSubmissionMessageData {\n main: Record<string, RichFormValue | null>\n repeaters: Record<string, Record<string, RichFormValue>[]>\n files: Record<string, FormAdapterFile[]>\n}\n\nexport interface FormAdapterSubmissionMessagePayload {\n meta: FormAdapterSubmissionMessageMeta\n data: FormAdapterSubmissionMessageData\n result: FormAdapterSubmissionMessageResult\n}\n\nexport interface FormAdapterSubmissionMessage\n extends FormAdapterSubmissionMessagePayload {\n messageId: string\n recordCreatedAt: Date\n}\n\nexport interface FormAdapterSubmissionService {\n handleFormSubmission: (\n submissionMessage: FormAdapterSubmissionMessage\n ) => unknown\n}\n"],"mappings":"AAqDA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;AAEA;AACA;AACA;;AAmBA;AACA;AACA;AACA;;AAsGA,SACEA,UAAU,EACVC,YAAY;;AA8Nd;AACA;AACA","ignoreList":[]}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@defra/forms-engine-plugin",
3
- "version": "3.0.3",
3
+ "version": "3.0.4",
4
4
  "description": "Defra forms engine",
5
5
  "type": "module",
6
6
  "files": [
@@ -65,7 +65,9 @@ describe('SummaryViewModel', () => {
65
65
  'Pizzas',
66
66
  'Pizza'
67
67
  ],
68
- values: ['Collection', 'Not supplied']
68
+ values: ['Collection', 'Not supplied'],
69
+ answers: ['Collection', ''],
70
+ names: ['orderType', 'pizza']
69
71
  },
70
72
  {
71
73
  description: '1 item',
@@ -87,7 +89,9 @@ describe('SummaryViewModel', () => {
87
89
  'Pizzas',
88
90
  'Pizza'
89
91
  ],
90
- values: ['Delivery', 'You added 1 Pizza']
92
+ values: ['Delivery', 'You added 1 Pizza'],
93
+ answers: ['Delivery', 'You added 1 Pizza'],
94
+ names: ['orderType', 'pizza']
91
95
  },
92
96
  {
93
97
  description: '2 items',
@@ -114,142 +118,168 @@ describe('SummaryViewModel', () => {
114
118
  'Pizzas',
115
119
  'Pizza'
116
120
  ],
117
- values: ['Delivery', 'You added 2 Pizzas']
121
+ values: ['Delivery', 'You added 2 Pizzas'],
122
+ answers: ['Delivery', 'You added 2 Pizzas'],
123
+ names: ['orderType', 'pizza']
118
124
  }
119
- ])('Check answers ($description)', ({ state, keys, values }) => {
120
- beforeEach(() => {
121
- context = model.getFormContext(request, state)
122
- summaryViewModel = new SummaryViewModel(request, page, context)
123
- })
125
+ ])(
126
+ 'Check answers ($description)',
127
+ ({ state, keys, values, names, answers }) => {
128
+ beforeEach(() => {
129
+ context = model.getFormContext(request, state)
130
+ summaryViewModel = new SummaryViewModel(request, page, context)
131
+ })
124
132
 
125
- it('should add title for each section', () => {
126
- const [checkAnswers1, checkAnswers2] = summaryViewModel.checkAnswers
133
+ it('should add title for each section', () => {
134
+ const [checkAnswers1, checkAnswers2] = summaryViewModel.checkAnswers
127
135
 
128
- // 1st summary list has no title
129
- expect(checkAnswers1).toHaveProperty('title', undefined)
136
+ // 1st summary list has no title
137
+ expect(checkAnswers1).toHaveProperty('title', undefined)
130
138
 
131
- // 2nd summary list has section title
132
- expect(checkAnswers2).toHaveProperty('title', {
133
- text: 'Food'
139
+ // 2nd summary list has section title
140
+ expect(checkAnswers2).toHaveProperty('title', {
141
+ text: 'Food'
142
+ })
134
143
  })
135
- })
136
144
 
137
- it('should add summary list for each section', () => {
138
- expect(summaryViewModel.checkAnswers).toHaveLength(2)
145
+ it('should add summary list for each section', () => {
146
+ expect(summaryViewModel.checkAnswers).toHaveLength(2)
139
147
 
140
- const [checkAnswers1, checkAnswers2] = summaryViewModel.checkAnswers
148
+ const [checkAnswers1, checkAnswers2] = summaryViewModel.checkAnswers
141
149
 
142
- const { summaryList: summaryList1 } = checkAnswers1
143
- const { summaryList: summaryList2 } = checkAnswers2
150
+ const { summaryList: summaryList1 } = checkAnswers1
151
+ const { summaryList: summaryList2 } = checkAnswers2
144
152
 
145
- expect(summaryList1).toHaveProperty('rows', [
146
- {
147
- key: {
148
- text: keys[2]
149
- },
150
- value: {
151
- classes: 'app-prose-scope',
152
- html: values[0]
153
- },
154
- actions: {
155
- items: [
156
- {
157
- classes: 'govuk-link--no-visited-state',
158
- href: `${basePath}/delivery-or-collection?returnUrl=${encodeURIComponent(`${basePath}/summary`)}`,
159
- text: 'Change',
160
- visuallyHiddenText: keys[0]
161
- }
162
- ]
153
+ expect(summaryList1).toHaveProperty('rows', [
154
+ {
155
+ key: {
156
+ text: keys[2]
157
+ },
158
+ value: {
159
+ classes: 'app-prose-scope',
160
+ html: values[0]
161
+ },
162
+ actions: {
163
+ items: [
164
+ {
165
+ classes: 'govuk-link--no-visited-state',
166
+ href: `${basePath}/delivery-or-collection?returnUrl=${encodeURIComponent(`${basePath}/summary`)}`,
167
+ text: 'Change',
168
+ visuallyHiddenText: keys[0]
169
+ }
170
+ ]
171
+ }
163
172
  }
164
- }
165
- ])
173
+ ])
166
174
 
167
- expect(summaryList2).toHaveProperty('rows', [
168
- {
169
- key: {
170
- text: keys[1]
171
- },
172
- value: {
173
- classes: 'app-prose-scope',
174
- html: values[1]
175
- },
176
- actions: {
177
- items: [
178
- {
179
- classes: 'govuk-link--no-visited-state',
180
- href: `${basePath}/pizza-order/summary?returnUrl=${encodeURIComponent(`${basePath}/summary`)}`,
181
- text: 'Change',
182
- visuallyHiddenText: 'Pizza'
183
- }
184
- ]
175
+ expect(summaryList2).toHaveProperty('rows', [
176
+ {
177
+ key: {
178
+ text: keys[1]
179
+ },
180
+ value: {
181
+ classes: 'app-prose-scope',
182
+ html: values[1]
183
+ },
184
+ actions: {
185
+ items: [
186
+ {
187
+ classes: 'govuk-link--no-visited-state',
188
+ href: `${basePath}/pizza-order/summary?returnUrl=${encodeURIComponent(`${basePath}/summary`)}`,
189
+ text: 'Change',
190
+ visuallyHiddenText: 'Pizza'
191
+ }
192
+ ]
193
+ }
185
194
  }
186
- }
187
- ])
188
- })
195
+ ])
196
+ })
189
197
 
190
- it('should add summary list for each section (preview URL direct access)', () => {
191
- request.query.force = '' // Preview URL '?force'
192
- context = model.getFormContext(request, state)
193
- summaryViewModel = new SummaryViewModel(request, page, context)
198
+ it('should add summary list for each section (preview URL direct access)', () => {
199
+ request.query.force = '' // Preview URL '?force'
200
+ context = model.getFormContext(request, state)
201
+ summaryViewModel = new SummaryViewModel(request, page, context)
194
202
 
195
- expect(summaryViewModel.checkAnswers).toHaveLength(2)
203
+ expect(summaryViewModel.checkAnswers).toHaveLength(2)
196
204
 
197
- const [checkAnswers1, checkAnswers2] = summaryViewModel.checkAnswers
205
+ const [checkAnswers1, checkAnswers2] = summaryViewModel.checkAnswers
198
206
 
199
- const { summaryList: summaryList1 } = checkAnswers1
200
- const { summaryList: summaryList2 } = checkAnswers2
207
+ const { summaryList: summaryList1 } = checkAnswers1
208
+ const { summaryList: summaryList2 } = checkAnswers2
201
209
 
202
- expect(summaryList1).toHaveProperty('rows', [
203
- {
204
- key: {
205
- text: keys[2]
206
- },
207
- value: {
208
- classes: 'app-prose-scope',
209
- html: values[0]
210
- },
211
- actions: {
212
- items: []
210
+ expect(summaryList1).toHaveProperty('rows', [
211
+ {
212
+ key: {
213
+ text: keys[2]
214
+ },
215
+ value: {
216
+ classes: 'app-prose-scope',
217
+ html: values[0]
218
+ },
219
+ actions: {
220
+ items: []
221
+ }
213
222
  }
214
- }
215
- ])
223
+ ])
216
224
 
217
- expect(summaryList2).toHaveProperty('rows', [
218
- {
219
- key: {
220
- text: keys[1]
221
- },
222
- value: {
223
- classes: 'app-prose-scope',
224
- html: values[1]
225
- },
226
- actions: {
227
- items: []
225
+ expect(summaryList2).toHaveProperty('rows', [
226
+ {
227
+ key: {
228
+ text: keys[1]
229
+ },
230
+ value: {
231
+ classes: 'app-prose-scope',
232
+ html: values[1]
233
+ },
234
+ actions: {
235
+ items: []
236
+ }
228
237
  }
229
- }
230
- ])
231
- })
238
+ ])
239
+ })
232
240
 
233
- it('should use correct summary labels', () => {
234
- request.query.force = '' // Preview URL '?force'
235
- context = model.getFormContext(request, state)
236
- summaryViewModel = new SummaryViewModel(request, page, context)
241
+ it('should use correct summary labels', () => {
242
+ request.query.force = '' // Preview URL '?force'
243
+ context = model.getFormContext(request, state)
244
+ summaryViewModel = new SummaryViewModel(request, page, context)
237
245
 
238
- expect(summaryViewModel.details).toHaveLength(2)
246
+ expect(summaryViewModel.details).toHaveLength(2)
239
247
 
240
- const [details1, details2] = summaryViewModel.details
248
+ const [details1, details2] = summaryViewModel.details
241
249
 
242
- expect(details1.items[0]).toMatchObject({
243
- title: keys[2],
244
- label: keys[0]
245
- })
250
+ expect(details1.items[0]).toMatchObject({
251
+ name: names[0],
252
+ value: answers[0],
253
+ title: keys[2],
254
+ label: keys[0]
255
+ })
256
+
257
+ expect(details2.items[0]).toMatchObject({
258
+ name: names[1],
259
+ value: answers[1],
260
+ title: keys[1],
261
+ label: keys[4]
262
+ })
246
263
 
247
- expect(details2.items[0]).toMatchObject({
248
- title: keys[1],
249
- label: keys[4]
264
+ const snapshot = [
265
+ {
266
+ name: names[0],
267
+ value: answers[0],
268
+ title: keys[2],
269
+ label: keys[0]
270
+ },
271
+ {
272
+ name: names[1],
273
+ value: answers[1],
274
+ title: keys[1],
275
+ label: keys[4]
276
+ }
277
+ ]
278
+
279
+ expect(snapshot).toMatchSnapshot()
250
280
  })
251
- })
252
- })
281
+ }
282
+ )
253
283
  })
254
284
 
255
285
  describe('SummaryPageController', () => {
@@ -0,0 +1,52 @@
1
+ // Jest Snapshot v1, https://jestjs.io/docs/snapshot-testing
2
+
3
+ exports[`SummaryViewModel Check answers (0 items) should use correct summary labels 1`] = `
4
+ [
5
+ {
6
+ "label": "How would you like to receive your pizza?",
7
+ "name": "orderType",
8
+ "title": "How you would like to receive your pizza",
9
+ "value": "Collection",
10
+ },
11
+ {
12
+ "label": "Pizza",
13
+ "name": "pizza",
14
+ "title": "Pizzas",
15
+ "value": "",
16
+ },
17
+ ]
18
+ `;
19
+
20
+ exports[`SummaryViewModel Check answers (1 item) should use correct summary labels 1`] = `
21
+ [
22
+ {
23
+ "label": "How would you like to receive your pizza?",
24
+ "name": "orderType",
25
+ "title": "How you would like to receive your pizza",
26
+ "value": "Delivery",
27
+ },
28
+ {
29
+ "label": "Pizza",
30
+ "name": "pizza",
31
+ "title": "Pizza added",
32
+ "value": "You added 1 Pizza",
33
+ },
34
+ ]
35
+ `;
36
+
37
+ exports[`SummaryViewModel Check answers (2 items) should use correct summary labels 1`] = `
38
+ [
39
+ {
40
+ "label": "How would you like to receive your pizza?",
41
+ "name": "orderType",
42
+ "title": "How you would like to receive your pizza",
43
+ "value": "Delivery",
44
+ },
45
+ {
46
+ "label": "Pizza",
47
+ "name": "pizza",
48
+ "title": "Pizzas added",
49
+ "value": "You added 2 Pizzas",
50
+ },
51
+ ]
52
+ `;
@@ -723,6 +723,46 @@ describe('Adapter v1 formatter', () => {
723
723
  expect(parsedBody.meta.versionMetadata).toBeUndefined()
724
724
  })
725
725
 
726
+ it('should handle optional fields that are undefined', () => {
727
+ const formMetadata: Partial<FormMetadata> = {
728
+ id: 'form-123',
729
+ slug: 'test-form',
730
+ title: 'Test Form',
731
+ notificationEmail: 'test@example.com'
732
+ } as FormMetadata
733
+
734
+ const formStatus = {
735
+ isPreview: false,
736
+ state: FormStatus.Live
737
+ }
738
+
739
+ const dummyField: Field = {
740
+ getFormValueFromState: (_) => undefined
741
+ } as Field
742
+
743
+ const items: DetailItem[] = [
744
+ {
745
+ name: 'exampleField3',
746
+ label: 'Example Field 3',
747
+ href: '/example-field-3',
748
+ title: 'Example Field 3 Title',
749
+ field: dummyField,
750
+ value: ''
751
+ } as DetailItemField
752
+ ]
753
+
754
+ const body = format(
755
+ context,
756
+ items,
757
+ model,
758
+ submitResponse,
759
+ formStatus,
760
+ formMetadata as FormMetadata
761
+ )
762
+ const parsedBody = JSON.parse(body) as FormAdapterSubmissionMessagePayload
763
+ expect(parsedBody.data.main).toEqual({ exampleField3: null })
764
+ })
765
+
726
766
  describe('version metadata handling', () => {
727
767
  it('should include versionMetadata when context has submittedVersionNumber and formMetadata has versions', () => {
728
768
  const formMetadata: Partial<FormMetadata> = {
@@ -6,7 +6,7 @@ import {
6
6
  import { type checkFormStatus } from '~/src/server/plugins/engine/helpers.js'
7
7
  import { type FormModel } from '~/src/server/plugins/engine/models/FormModel.js'
8
8
  import { type DetailItem } from '~/src/server/plugins/engine/models/types.js'
9
- import { format as machineV2 } from '~/src/server/plugins/engine/outputFormatters/machine/v2.js'
9
+ import { categoriseData } from '~/src/server/plugins/engine/outputFormatters/machine/v2.js'
10
10
  import { FormAdapterSubmissionSchemaVersion } from '~/src/server/plugins/engine/types/enums.js'
11
11
  import {
12
12
  type FormAdapterSubmissionMessageData,
@@ -25,20 +25,9 @@ export function format(
25
25
  formStatus: ReturnType<typeof checkFormStatus>,
26
26
  formMetadata?: FormMetadata
27
27
  ): string {
28
- const v2DataString = machineV2(
29
- context,
30
- items,
31
- model,
32
- submitResponse,
33
- formStatus
34
- )
35
- const v2DataParsed = JSON.parse(v2DataString) as {
36
- data: FormAdapterSubmissionMessageData
37
- }
38
-
39
28
  const csvFiles = extractCsvFiles(submitResponse)
40
29
 
41
- const transformedData = v2DataParsed.data
30
+ const { main: v2Main, ...v2Data } = categoriseData(items)
42
31
 
43
32
  const versionMetadata = getVersionMetadata(
44
33
  context.submittedVersionNumber,
@@ -60,7 +49,21 @@ export function format(
60
49
  if (versionMetadata) {
61
50
  meta.versionMetadata = versionMetadata
62
51
  }
63
- const data: FormAdapterSubmissionMessageData = transformedData
52
+
53
+ const main = Object.fromEntries(
54
+ Object.entries(v2Main).map(([key, value]) => {
55
+ if (value === undefined) {
56
+ return [key, null]
57
+ }
58
+
59
+ return [key, value]
60
+ })
61
+ )
62
+
63
+ const data: FormAdapterSubmissionMessageData = {
64
+ main,
65
+ ...v2Data
66
+ }
64
67
 
65
68
  const result: FormAdapterSubmissionMessageResult = {
66
69
  files: csvFiles
@@ -9,6 +9,10 @@ import {
9
9
  type DetailItemRepeat
10
10
  } from '~/src/server/plugins/engine/models/types.js'
11
11
  import { format } from '~/src/server/plugins/engine/outputFormatters/machine/v2.js'
12
+ import {
13
+ SummaryPageController,
14
+ getFormSubmissionData
15
+ } from '~/src/server/plugins/engine/pageControllers/SummaryPageController.js'
12
16
  import { buildFormContextRequest } from '~/src/server/plugins/engine/pageControllers/__stubs__/request.js'
13
17
  import {
14
18
  FileStatus,
@@ -267,4 +271,41 @@ describe('getPersonalisation', () => {
267
271
  expect(parsedBody.meta.referenceNumber).toBe('foobar')
268
272
  expect(parsedBody.data).toEqual(expectedData)
269
273
  })
274
+
275
+ it('should return the machine output 2', () => {
276
+ const pageDef = definition.pages[2]
277
+ const controller = new SummaryPageController(model, pageDef)
278
+
279
+ const summaryViewModel = controller.getSummaryViewModel(request, context)
280
+
281
+ const items = getFormSubmissionData(
282
+ summaryViewModel.context,
283
+ summaryViewModel.details
284
+ )
285
+
286
+ const body = format(context, items, model, submitResponse, formStatus)
287
+
288
+ const parsedBody = JSON.parse(body)
289
+
290
+ const expectedData = {
291
+ main: {
292
+ orderType: 'delivery'
293
+ },
294
+ repeaters: {
295
+ pizza: [
296
+ {
297
+ quantity: 2,
298
+ toppings: 'Ham'
299
+ },
300
+ {
301
+ quantity: 1,
302
+ toppings: 'Pepperoni'
303
+ }
304
+ ]
305
+ },
306
+ files: {}
307
+ }
308
+
309
+ expect(parsedBody.data).toEqual(expectedData)
310
+ })
270
311
  })
@@ -74,7 +74,7 @@ export function format(
74
74
  * }
75
75
  * }
76
76
  */
77
- function categoriseData(items: DetailItem[]) {
77
+ export function categoriseData(items: DetailItem[]) {
78
78
  const output: {
79
79
  main: Record<string, RichFormValue>
80
80
  repeaters: Record<string, Record<string, RichFormValue>[]>
@@ -446,7 +446,7 @@ export type RichFormValue =
446
446
  | UkAddressState
447
447
 
448
448
  export interface FormAdapterSubmissionMessageData {
449
- main: Record<string, RichFormValue>
449
+ main: Record<string, RichFormValue | null>
450
450
  repeaters: Record<string, Record<string, RichFormValue>[]>
451
451
  files: Record<string, FormAdapterFile[]>
452
452
  }