@defra/forms-engine-plugin 1.3.1 → 1.4.1
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.
- package/.server/server/plugins/engine/index.js +1 -1
- package/.server/server/plugins/engine/index.js.map +1 -1
- package/.server/server/plugins/engine/models/FormModel.js +2 -2
- package/.server/server/plugins/engine/models/FormModel.js.map +1 -1
- package/.server/server/plugins/engine/models/SummaryViewModel.d.ts +1 -0
- package/.server/server/plugins/engine/models/SummaryViewModel.js +1 -0
- package/.server/server/plugins/engine/models/SummaryViewModel.js.map +1 -1
- package/.server/server/plugins/engine/options.js +4 -1
- package/.server/server/plugins/engine/options.js.map +1 -1
- package/.server/server/plugins/engine/options.test.js +20 -0
- package/.server/server/plugins/engine/options.test.js.map +1 -1
- package/.server/server/plugins/engine/pageControllers/QuestionPageController.d.ts +5 -0
- package/.server/server/plugins/engine/pageControllers/QuestionPageController.js +27 -1
- package/.server/server/plugins/engine/pageControllers/QuestionPageController.js.map +1 -1
- package/.server/server/plugins/engine/pageControllers/StartPageController.d.ts +2 -0
- package/.server/server/plugins/engine/pageControllers/StartPageController.js +3 -0
- package/.server/server/plugins/engine/pageControllers/StartPageController.js.map +1 -1
- package/.server/server/plugins/engine/pageControllers/SummaryPageController.d.ts +1 -0
- package/.server/server/plugins/engine/pageControllers/SummaryPageController.js +4 -0
- package/.server/server/plugins/engine/pageControllers/SummaryPageController.js.map +1 -1
- package/.server/server/plugins/engine/plugin.js +5 -2
- package/.server/server/plugins/engine/plugin.js.map +1 -1
- package/.server/server/plugins/engine/routes/exit.d.ts +46 -0
- package/.server/server/plugins/engine/routes/exit.js +36 -0
- package/.server/server/plugins/engine/routes/exit.js.map +1 -0
- package/.server/server/plugins/engine/types.d.ts +6 -2
- package/.server/server/plugins/engine/types.js.map +1 -1
- package/.server/server/plugins/engine/views/exit.html +31 -0
- package/.server/server/plugins/engine/views/partials/form.html +17 -6
- package/.server/server/routes/types.d.ts +2 -1
- package/.server/server/routes/types.js +1 -0
- package/.server/server/routes/types.js.map +1 -1
- package/.server/server/schemas/index.js +1 -1
- package/.server/server/schemas/index.js.map +1 -1
- package/.server/server/services/cacheService.d.ts +2 -0
- package/.server/server/services/cacheService.js +9 -5
- package/.server/server/services/cacheService.js.map +1 -1
- package/package.json +1 -1
- package/src/server/index.test.ts +39 -0
- package/src/server/plugins/engine/components/helpers.test.ts +31 -0
- package/src/server/plugins/engine/index.ts +1 -3
- package/src/server/plugins/engine/models/FormModel.test.ts +85 -11
- package/src/server/plugins/engine/models/FormModel.ts +5 -2
- package/src/server/plugins/engine/models/SummaryViewModel.test.ts +59 -0
- package/src/server/plugins/engine/models/SummaryViewModel.ts +1 -0
- package/src/server/plugins/engine/options.js +4 -1
- package/src/server/plugins/engine/options.test.js +20 -0
- package/src/server/plugins/engine/pageControllers/PageController.test.ts +25 -0
- package/src/server/plugins/engine/pageControllers/QuestionPageController.test.ts +178 -1
- package/src/server/plugins/engine/pageControllers/QuestionPageController.ts +28 -1
- package/src/server/plugins/engine/pageControllers/StartPageController.ts +4 -0
- package/src/server/plugins/engine/pageControllers/SummaryPageController.ts +5 -0
- package/src/server/plugins/engine/plugin.ts +5 -1
- package/src/server/plugins/engine/routes/exit.ts +47 -0
- package/src/server/plugins/engine/types.ts +10 -4
- package/src/server/plugins/engine/views/exit.html +31 -0
- package/src/server/plugins/engine/views/partials/form.html +17 -6
- package/src/server/routes/types.ts +2 -1
- package/src/server/schemas/index.ts +2 -1
- package/src/server/services/cacheService.test.ts +45 -0
- package/src/server/services/cacheService.ts +20 -9
|
@@ -13,6 +13,7 @@ export declare class StartPageController extends QuestionPageController {
|
|
|
13
13
|
context: FormContext;
|
|
14
14
|
errors?: import("~/src/server/plugins/engine/types.js").FormSubmissionError[];
|
|
15
15
|
hasMissingNotificationEmail?: boolean;
|
|
16
|
+
allowSaveAndReturn?: boolean;
|
|
16
17
|
page: import("./PageController.js").PageController;
|
|
17
18
|
name?: string;
|
|
18
19
|
pageTitle: string;
|
|
@@ -47,4 +48,5 @@ export declare class StartPageController extends QuestionPageController {
|
|
|
47
48
|
*/
|
|
48
49
|
slug?: string | undefined;
|
|
49
50
|
};
|
|
51
|
+
shouldShowSaveAndReturn(): boolean;
|
|
50
52
|
}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"StartPageController.js","names":["QuestionPageController","StartPageController","getViewModel","request","context","isStartPage"],"sources":["../../../../../src/server/plugins/engine/pageControllers/StartPageController.ts"],"sourcesContent":["import { QuestionPageController } from '~/src/server/plugins/engine/pageControllers/QuestionPageController.js'\nimport { type FormContext } from '~/src/server/plugins/engine/types.js'\nimport { type FormRequest } from '~/src/server/routes/types.js'\n\nexport class StartPageController extends QuestionPageController {\n /**\n * The controller which is used when Page[\"controller\"] is defined as \"./pages/start.js\"\n * This page should not be used in production. This page is helpful for prototyping start pages within the app,\n * but start pages should really live on gov.uk (whitehall publisher) so a user can be properly signposted.\n */\n\n getViewModel(request: FormRequest, context: FormContext) {\n return {\n ...super.getViewModel(request, context),\n isStartPage: true\n }\n }\n}\n"],"mappings":"AAAA,SAASA,sBAAsB;AAI/B,OAAO,MAAMC,mBAAmB,SAASD,sBAAsB,CAAC;EAC9D;AACF;AACA;AACA;AACA;;EAEEE,YAAYA,CAACC,OAAoB,EAAEC,OAAoB,EAAE;IACvD,OAAO;MACL,GAAG,KAAK,CAACF,YAAY,CAACC,OAAO,EAAEC,OAAO,CAAC;MACvCC,WAAW,EAAE;IACf,CAAC;EACH;AACF","ignoreList":[]}
|
|
1
|
+
{"version":3,"file":"StartPageController.js","names":["QuestionPageController","StartPageController","getViewModel","request","context","isStartPage","shouldShowSaveAndReturn"],"sources":["../../../../../src/server/plugins/engine/pageControllers/StartPageController.ts"],"sourcesContent":["import { QuestionPageController } from '~/src/server/plugins/engine/pageControllers/QuestionPageController.js'\nimport { type FormContext } from '~/src/server/plugins/engine/types.js'\nimport { type FormRequest } from '~/src/server/routes/types.js'\n\nexport class StartPageController extends QuestionPageController {\n /**\n * The controller which is used when Page[\"controller\"] is defined as \"./pages/start.js\"\n * This page should not be used in production. This page is helpful for prototyping start pages within the app,\n * but start pages should really live on gov.uk (whitehall publisher) so a user can be properly signposted.\n */\n\n getViewModel(request: FormRequest, context: FormContext) {\n return {\n ...super.getViewModel(request, context),\n isStartPage: true\n }\n }\n\n shouldShowSaveAndReturn(): boolean {\n return false\n }\n}\n"],"mappings":"AAAA,SAASA,sBAAsB;AAI/B,OAAO,MAAMC,mBAAmB,SAASD,sBAAsB,CAAC;EAC9D;AACF;AACA;AACA;AACA;;EAEEE,YAAYA,CAACC,OAAoB,EAAEC,OAAoB,EAAE;IACvD,OAAO;MACL,GAAG,KAAK,CAACF,YAAY,CAACC,OAAO,EAAEC,OAAO,CAAC;MACvCC,WAAW,EAAE;IACf,CAAC;EACH;EAEAC,uBAAuBA,CAAA,EAAY;IACjC,OAAO,KAAK;EACd;AACF","ignoreList":[]}
|
|
@@ -22,5 +22,6 @@ export declare class SummaryPageController extends QuestionPageController {
|
|
|
22
22
|
*/
|
|
23
23
|
makePostRouteHandler(): (request: FormRequestPayload, context: FormContext, h: Pick<ResponseToolkit, "redirect" | "view">) => Promise<import("@hapi/hapi").ResponseObject>;
|
|
24
24
|
get postRouteOptions(): RouteOptions<FormRequestPayloadRefs>;
|
|
25
|
+
shouldShowSaveAndReturn(): boolean;
|
|
25
26
|
}
|
|
26
27
|
export declare function getFormSubmissionData(context: FormContext, details: Detail[]): DetailItem[];
|
|
@@ -38,6 +38,7 @@ export class SummaryPageController extends QuestionPageController {
|
|
|
38
38
|
viewModel.feedbackLink = this.feedbackLink;
|
|
39
39
|
viewModel.phaseTag = this.phaseTag;
|
|
40
40
|
viewModel.components = components;
|
|
41
|
+
viewModel.allowSaveAndReturn = this.shouldShowSaveAndReturn();
|
|
41
42
|
return viewModel;
|
|
42
43
|
}
|
|
43
44
|
|
|
@@ -113,6 +114,9 @@ export class SummaryPageController extends QuestionPageController {
|
|
|
113
114
|
}
|
|
114
115
|
};
|
|
115
116
|
}
|
|
117
|
+
shouldShowSaveAndReturn() {
|
|
118
|
+
return true;
|
|
119
|
+
}
|
|
116
120
|
}
|
|
117
121
|
async function submitForm(request, summaryViewModel, model, state, emailAddress) {
|
|
118
122
|
await extendFileRetention(model, state, emailAddress);
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"SummaryPageController.js","names":["hasComponentsEvenIfNoNext","Boom","ComponentCollection","FileUploadField","getAnswer","checkEmailAddressForLiveFormSubmission","checkFormStatus","getCacheService","SummaryViewModel","QuestionPageController","SummaryPageController","constructor","model","pageDef","viewName","collection","components","page","getSummaryViewModel","request","context","viewModel","query","payload","errors","getViewModel","backLink","getBackLink","feedbackLink","phaseTag","makeGetRouteHandler","h","hasMissingNotificationEmail","view","makePostRouteHandler","params","state","cacheService","server","formsService","services","getFormMetadata","notificationEmail","slug","isPreview","emailAddress","def","outputEmail","submitForm","setConfirmationState","confirmed","clearState","proceed","getStatusPath","postRouteOptions","ext","onPreHandler","method","continue","summaryViewModel","extendFileRetention","formStatus","logTags","logger","info","items","getFormSubmissionData","details","submitResponse","submitData","yar","id","undefined","badRequest","outputService","submit","updatedRetrievalKey","formSubmissionService","persistFiles","files","pages","forEach","fileUploadComponents","fields","filter","component","values","getFormValueFromState","length","push","map","status","fileId","form","file","initiatedRetrievalKey","metadata","retrievalKey","sessionId","main","item","name","title","label","value","field","format","repeaters","subItems","detailItems","subItem","relevantPages","href","flatMap","flat"],"sources":["../../../../../src/server/plugins/engine/pageControllers/SummaryPageController.ts"],"sourcesContent":["import {\n hasComponentsEvenIfNoNext,\n type Page,\n type SubmitPayload\n} from '@defra/forms-model'\nimport Boom from '@hapi/boom'\nimport { type ResponseToolkit, type RouteOptions } from '@hapi/hapi'\n\nimport { ComponentCollection } from '~/src/server/plugins/engine/components/ComponentCollection.js'\nimport { FileUploadField } from '~/src/server/plugins/engine/components/FileUploadField.js'\nimport { getAnswer } from '~/src/server/plugins/engine/components/helpers.js'\nimport {\n checkEmailAddressForLiveFormSubmission,\n checkFormStatus,\n getCacheService\n} from '~/src/server/plugins/engine/helpers.js'\nimport {\n SummaryViewModel,\n type FormModel\n} from '~/src/server/plugins/engine/models/index.js'\nimport {\n type Detail,\n type DetailItem\n} from '~/src/server/plugins/engine/models/types.js'\nimport { QuestionPageController } from '~/src/server/plugins/engine/pageControllers/QuestionPageController.js'\nimport {\n type FormContext,\n type FormContextRequest,\n type FormSubmissionState\n} from '~/src/server/plugins/engine/types.js'\nimport {\n type FormRequest,\n type FormRequestPayload,\n type FormRequestPayloadRefs\n} from '~/src/server/routes/types.js'\n\nexport class SummaryPageController extends QuestionPageController {\n declare pageDef: Page\n\n /**\n * The controller which is used when Page[\"controller\"] is defined as \"./pages/summary.js\"\n */\n\n constructor(model: FormModel, pageDef: Page) {\n super(model, pageDef)\n this.viewName = 'summary'\n\n // Components collection\n this.collection = new ComponentCollection(\n hasComponentsEvenIfNoNext(pageDef) ? pageDef.components : [],\n { model, page: this }\n )\n }\n\n getSummaryViewModel(\n request: FormContextRequest,\n context: FormContext\n ): SummaryViewModel {\n const viewModel = new SummaryViewModel(request, this, context)\n\n const { query } = request\n const { payload, errors } = context\n const components = this.collection.getViewModel(payload, errors, query)\n\n // We already figure these out in the base page controller. Take them and apply them to our page-specific model.\n // This is a stop-gap until we can add proper inheritance in place.\n viewModel.backLink = this.getBackLink(request, context)\n viewModel.feedbackLink = this.feedbackLink\n viewModel.phaseTag = this.phaseTag\n viewModel.components = components\n\n return viewModel\n }\n\n /**\n * Returns an async function. This is called in plugin.ts when there is a GET request at `/{id}/{path*}`,\n */\n makeGetRouteHandler() {\n return async (\n request: FormRequest,\n context: FormContext,\n h: Pick<ResponseToolkit, 'redirect' | 'view'>\n ) => {\n const { viewName } = this\n\n const viewModel = this.getSummaryViewModel(request, context)\n\n viewModel.hasMissingNotificationEmail =\n await this.hasMissingNotificationEmail(request, context)\n\n return h.view(viewName, viewModel)\n }\n }\n\n /**\n * Returns an async function. This is called in plugin.ts when there is a POST request at `/{id}/{path*}`.\n * If a form is incomplete, a user will be redirected to the start page.\n */\n makePostRouteHandler() {\n return async (\n request: FormRequestPayload,\n context: FormContext,\n h: Pick<ResponseToolkit, 'redirect' | 'view'>\n ) => {\n const { model } = this\n const { params } = request\n const { state } = context\n const cacheService = getCacheService(request.server)\n\n const { formsService } = this.model.services\n const { getFormMetadata } = formsService\n\n // Get the form metadata using the `slug` param\n const { notificationEmail } = await getFormMetadata(params.slug)\n const { isPreview } = checkFormStatus(request.params)\n const emailAddress = notificationEmail ?? this.model.def.outputEmail\n\n checkEmailAddressForLiveFormSubmission(emailAddress, isPreview)\n\n // Send submission email\n if (emailAddress) {\n const viewModel = this.getSummaryViewModel(request, context)\n await submitForm(request, viewModel, model, state, emailAddress)\n }\n\n await cacheService.setConfirmationState(request, { confirmed: true })\n\n // Clear all form data\n await cacheService.clearState(request)\n\n return this.proceed(request, h, this.getStatusPath())\n }\n }\n\n get postRouteOptions(): RouteOptions<FormRequestPayloadRefs> {\n return {\n ext: {\n onPreHandler: {\n method(request, h) {\n return h.continue\n }\n }\n }\n }\n }\n}\n\nasync function submitForm(\n request: FormRequestPayload,\n summaryViewModel: SummaryViewModel,\n model: FormModel,\n state: FormSubmissionState,\n emailAddress: string\n) {\n await extendFileRetention(model, state, emailAddress)\n\n const formStatus = checkFormStatus(request.params)\n const logTags = ['submit', 'submissionApi']\n\n request.logger.info(logTags, 'Preparing email', formStatus)\n\n // Get detail items\n const items = getFormSubmissionData(\n summaryViewModel.context,\n summaryViewModel.details\n )\n\n // Submit data\n request.logger.info(logTags, 'Submitting data')\n const submitResponse = await submitData(\n model,\n items,\n emailAddress,\n request.yar.id\n )\n\n if (submitResponse === undefined) {\n throw Boom.badRequest('Unexpected empty response from submit api')\n }\n\n return model.services.outputService.submit(\n request,\n model,\n emailAddress,\n items,\n submitResponse\n )\n}\n\nasync function extendFileRetention(\n model: FormModel,\n state: FormSubmissionState,\n updatedRetrievalKey: string\n) {\n const { formSubmissionService } = model.services\n const { persistFiles } = formSubmissionService\n const files: { fileId: string; initiatedRetrievalKey: string }[] = []\n\n // For each file upload component with files in\n // state, add the files to the batch getting persisted\n model.pages.forEach((page) => {\n const fileUploadComponents = page.collection.fields.filter(\n (component) => component instanceof FileUploadField\n )\n\n fileUploadComponents.forEach((component) => {\n const values = component.getFormValueFromState(state)\n if (!values?.length) {\n return\n }\n\n files.push(\n ...values.map(({ status }) => ({\n fileId: status.form.file.fileId,\n initiatedRetrievalKey: status.metadata.retrievalKey\n }))\n )\n })\n })\n\n if (files.length) {\n return persistFiles(files, updatedRetrievalKey)\n }\n}\n\nfunction submitData(\n model: FormModel,\n items: DetailItem[],\n retrievalKey: string,\n sessionId: string\n) {\n const { formSubmissionService } = model.services\n const { submit } = formSubmissionService\n\n const payload: SubmitPayload = {\n sessionId,\n retrievalKey,\n\n // Main form answers\n main: items\n .filter((item) => 'field' in item)\n .map((item) => ({\n name: item.name,\n title: item.label,\n value: getAnswer(item.field, item.state, { format: 'data' })\n })),\n\n // Repeater form answers\n repeaters: items\n .filter((item) => 'subItems' in item)\n .map((item) => ({\n name: item.name,\n title: item.label,\n\n // Repeater item values\n value: item.subItems.map((detailItems) =>\n detailItems.map((subItem) => ({\n name: subItem.name,\n title: subItem.label,\n value: getAnswer(subItem.field, subItem.state, { format: 'data' })\n }))\n )\n }))\n }\n\n return submit(payload)\n}\n\nexport function getFormSubmissionData(context: FormContext, details: Detail[]) {\n return context.relevantPages\n .map(({ href }) =>\n details.flatMap(({ items }) =>\n items.filter(({ page }) => page.href === href)\n )\n )\n .flat()\n}\n"],"mappings":"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","ignoreList":[]}
|
|
1
|
+
{"version":3,"file":"SummaryPageController.js","names":["hasComponentsEvenIfNoNext","Boom","ComponentCollection","FileUploadField","getAnswer","checkEmailAddressForLiveFormSubmission","checkFormStatus","getCacheService","SummaryViewModel","QuestionPageController","SummaryPageController","constructor","model","pageDef","viewName","collection","components","page","getSummaryViewModel","request","context","viewModel","query","payload","errors","getViewModel","backLink","getBackLink","feedbackLink","phaseTag","allowSaveAndReturn","shouldShowSaveAndReturn","makeGetRouteHandler","h","hasMissingNotificationEmail","view","makePostRouteHandler","params","state","cacheService","server","formsService","services","getFormMetadata","notificationEmail","slug","isPreview","emailAddress","def","outputEmail","submitForm","setConfirmationState","confirmed","clearState","proceed","getStatusPath","postRouteOptions","ext","onPreHandler","method","continue","summaryViewModel","extendFileRetention","formStatus","logTags","logger","info","items","getFormSubmissionData","details","submitResponse","submitData","yar","id","undefined","badRequest","outputService","submit","updatedRetrievalKey","formSubmissionService","persistFiles","files","pages","forEach","fileUploadComponents","fields","filter","component","values","getFormValueFromState","length","push","map","status","fileId","form","file","initiatedRetrievalKey","metadata","retrievalKey","sessionId","main","item","name","title","label","value","field","format","repeaters","subItems","detailItems","subItem","relevantPages","href","flatMap","flat"],"sources":["../../../../../src/server/plugins/engine/pageControllers/SummaryPageController.ts"],"sourcesContent":["import {\n hasComponentsEvenIfNoNext,\n type Page,\n type SubmitPayload\n} from '@defra/forms-model'\nimport Boom from '@hapi/boom'\nimport { type ResponseToolkit, type RouteOptions } from '@hapi/hapi'\n\nimport { ComponentCollection } from '~/src/server/plugins/engine/components/ComponentCollection.js'\nimport { FileUploadField } from '~/src/server/plugins/engine/components/FileUploadField.js'\nimport { getAnswer } from '~/src/server/plugins/engine/components/helpers.js'\nimport {\n checkEmailAddressForLiveFormSubmission,\n checkFormStatus,\n getCacheService\n} from '~/src/server/plugins/engine/helpers.js'\nimport {\n SummaryViewModel,\n type FormModel\n} from '~/src/server/plugins/engine/models/index.js'\nimport {\n type Detail,\n type DetailItem\n} from '~/src/server/plugins/engine/models/types.js'\nimport { QuestionPageController } from '~/src/server/plugins/engine/pageControllers/QuestionPageController.js'\nimport {\n type FormContext,\n type FormContextRequest,\n type FormSubmissionState\n} from '~/src/server/plugins/engine/types.js'\nimport {\n type FormRequest,\n type FormRequestPayload,\n type FormRequestPayloadRefs\n} from '~/src/server/routes/types.js'\n\nexport class SummaryPageController extends QuestionPageController {\n declare pageDef: Page\n\n /**\n * The controller which is used when Page[\"controller\"] is defined as \"./pages/summary.js\"\n */\n\n constructor(model: FormModel, pageDef: Page) {\n super(model, pageDef)\n this.viewName = 'summary'\n\n // Components collection\n this.collection = new ComponentCollection(\n hasComponentsEvenIfNoNext(pageDef) ? pageDef.components : [],\n { model, page: this }\n )\n }\n\n getSummaryViewModel(\n request: FormContextRequest,\n context: FormContext\n ): SummaryViewModel {\n const viewModel = new SummaryViewModel(request, this, context)\n\n const { query } = request\n const { payload, errors } = context\n const components = this.collection.getViewModel(payload, errors, query)\n\n // We already figure these out in the base page controller. Take them and apply them to our page-specific model.\n // This is a stop-gap until we can add proper inheritance in place.\n viewModel.backLink = this.getBackLink(request, context)\n viewModel.feedbackLink = this.feedbackLink\n viewModel.phaseTag = this.phaseTag\n viewModel.components = components\n viewModel.allowSaveAndReturn = this.shouldShowSaveAndReturn()\n\n return viewModel\n }\n\n /**\n * Returns an async function. This is called in plugin.ts when there is a GET request at `/{id}/{path*}`,\n */\n makeGetRouteHandler() {\n return async (\n request: FormRequest,\n context: FormContext,\n h: Pick<ResponseToolkit, 'redirect' | 'view'>\n ) => {\n const { viewName } = this\n\n const viewModel = this.getSummaryViewModel(request, context)\n\n viewModel.hasMissingNotificationEmail =\n await this.hasMissingNotificationEmail(request, context)\n\n return h.view(viewName, viewModel)\n }\n }\n\n /**\n * Returns an async function. This is called in plugin.ts when there is a POST request at `/{id}/{path*}`.\n * If a form is incomplete, a user will be redirected to the start page.\n */\n makePostRouteHandler() {\n return async (\n request: FormRequestPayload,\n context: FormContext,\n h: Pick<ResponseToolkit, 'redirect' | 'view'>\n ) => {\n const { model } = this\n const { params } = request\n const { state } = context\n const cacheService = getCacheService(request.server)\n\n const { formsService } = this.model.services\n const { getFormMetadata } = formsService\n\n // Get the form metadata using the `slug` param\n const { notificationEmail } = await getFormMetadata(params.slug)\n const { isPreview } = checkFormStatus(request.params)\n const emailAddress = notificationEmail ?? this.model.def.outputEmail\n\n checkEmailAddressForLiveFormSubmission(emailAddress, isPreview)\n\n // Send submission email\n if (emailAddress) {\n const viewModel = this.getSummaryViewModel(request, context)\n await submitForm(request, viewModel, model, state, emailAddress)\n }\n\n await cacheService.setConfirmationState(request, { confirmed: true })\n\n // Clear all form data\n await cacheService.clearState(request)\n\n return this.proceed(request, h, this.getStatusPath())\n }\n }\n\n get postRouteOptions(): RouteOptions<FormRequestPayloadRefs> {\n return {\n ext: {\n onPreHandler: {\n method(request, h) {\n return h.continue\n }\n }\n }\n }\n }\n\n shouldShowSaveAndReturn(): boolean {\n return true\n }\n}\n\nasync function submitForm(\n request: FormRequestPayload,\n summaryViewModel: SummaryViewModel,\n model: FormModel,\n state: FormSubmissionState,\n emailAddress: string\n) {\n await extendFileRetention(model, state, emailAddress)\n\n const formStatus = checkFormStatus(request.params)\n const logTags = ['submit', 'submissionApi']\n\n request.logger.info(logTags, 'Preparing email', formStatus)\n\n // Get detail items\n const items = getFormSubmissionData(\n summaryViewModel.context,\n summaryViewModel.details\n )\n\n // Submit data\n request.logger.info(logTags, 'Submitting data')\n const submitResponse = await submitData(\n model,\n items,\n emailAddress,\n request.yar.id\n )\n\n if (submitResponse === undefined) {\n throw Boom.badRequest('Unexpected empty response from submit api')\n }\n\n return model.services.outputService.submit(\n request,\n model,\n emailAddress,\n items,\n submitResponse\n )\n}\n\nasync function extendFileRetention(\n model: FormModel,\n state: FormSubmissionState,\n updatedRetrievalKey: string\n) {\n const { formSubmissionService } = model.services\n const { persistFiles } = formSubmissionService\n const files: { fileId: string; initiatedRetrievalKey: string }[] = []\n\n // For each file upload component with files in\n // state, add the files to the batch getting persisted\n model.pages.forEach((page) => {\n const fileUploadComponents = page.collection.fields.filter(\n (component) => component instanceof FileUploadField\n )\n\n fileUploadComponents.forEach((component) => {\n const values = component.getFormValueFromState(state)\n if (!values?.length) {\n return\n }\n\n files.push(\n ...values.map(({ status }) => ({\n fileId: status.form.file.fileId,\n initiatedRetrievalKey: status.metadata.retrievalKey\n }))\n )\n })\n })\n\n if (files.length) {\n return persistFiles(files, updatedRetrievalKey)\n }\n}\n\nfunction submitData(\n model: FormModel,\n items: DetailItem[],\n retrievalKey: string,\n sessionId: string\n) {\n const { formSubmissionService } = model.services\n const { submit } = formSubmissionService\n\n const payload: SubmitPayload = {\n sessionId,\n retrievalKey,\n\n // Main form answers\n main: items\n .filter((item) => 'field' in item)\n .map((item) => ({\n name: item.name,\n title: item.label,\n value: getAnswer(item.field, item.state, { format: 'data' })\n })),\n\n // Repeater form answers\n repeaters: items\n .filter((item) => 'subItems' in item)\n .map((item) => ({\n name: item.name,\n title: item.label,\n\n // Repeater item values\n value: item.subItems.map((detailItems) =>\n detailItems.map((subItem) => ({\n name: subItem.name,\n title: subItem.label,\n value: getAnswer(subItem.field, subItem.state, { format: 'data' })\n }))\n )\n }))\n }\n\n return submit(payload)\n}\n\nexport function getFormSubmissionData(context: FormContext, details: Detail[]) {\n return context.relevantPages\n .map(({ href }) =>\n details.flatMap(({ items }) =>\n items.filter(({ page }) => page.href === href)\n )\n )\n .flat()\n}\n"],"mappings":"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","ignoreList":[]}
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { validatePluginOptions } from "./options.js";
|
|
2
|
+
import { getRoutes as getSaveAndReturnExitRoutes } from "./routes/exit.js";
|
|
2
3
|
import { getRoutes as getFileUploadStatusRoutes } from "./routes/file-upload.js";
|
|
3
4
|
import { makeLoadFormPreHandler } from "./routes/index.js";
|
|
4
5
|
import { getRoutes as getQuestionRoutes } from "./routes/questions.js";
|
|
@@ -17,6 +18,7 @@ export const plugin = {
|
|
|
17
18
|
cacheName,
|
|
18
19
|
keyGenerator,
|
|
19
20
|
sessionHydrator,
|
|
21
|
+
sessionPersister,
|
|
20
22
|
nunjucks: nunjucksOptions,
|
|
21
23
|
viewContext,
|
|
22
24
|
preparePageEventRequestOptions
|
|
@@ -26,7 +28,8 @@ export const plugin = {
|
|
|
26
28
|
cacheName,
|
|
27
29
|
options: {
|
|
28
30
|
keyGenerator,
|
|
29
|
-
sessionHydrator
|
|
31
|
+
sessionHydrator,
|
|
32
|
+
sessionPersister
|
|
30
33
|
}
|
|
31
34
|
});
|
|
32
35
|
await registerVision(server, options);
|
|
@@ -53,7 +56,7 @@ export const plugin = {
|
|
|
53
56
|
method: loadFormPreHandler
|
|
54
57
|
}]
|
|
55
58
|
};
|
|
56
|
-
const routes = [...getQuestionRoutes(getRouteOptions, postRouteOptions, preparePageEventRequestOptions), ...getRepeaterSummaryRoutes(getRouteOptions, postRouteOptions), ...getRepeaterItemDeleteRoutes(getRouteOptions, postRouteOptions), ...getFileUploadStatusRoutes()];
|
|
59
|
+
const routes = [...getQuestionRoutes(getRouteOptions, postRouteOptions, preparePageEventRequestOptions), ...getRepeaterSummaryRoutes(getRouteOptions, postRouteOptions), ...getRepeaterItemDeleteRoutes(getRouteOptions, postRouteOptions), ...getSaveAndReturnExitRoutes(getRouteOptions), ...getFileUploadStatusRoutes()];
|
|
57
60
|
server.route(routes); // TODO
|
|
58
61
|
}
|
|
59
62
|
};
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"plugin.js","names":["validatePluginOptions","getRoutes","getFileUploadStatusRoutes","makeLoadFormPreHandler","getQuestionRoutes","getRepeaterItemDeleteRoutes","getRepeaterSummaryRoutes","registerVision","CacheService","plugin","name","dependencies","multiple","register","server","options","model","cacheName","keyGenerator","sessionHydrator","nunjucks","nunjucksOptions","viewContext","preparePageEventRequestOptions","cacheService","expose","baseLayoutPath","app","itemCache","Map","models","loadFormPreHandler","getRouteOptions","pre","method","postRouteOptions","payload","parse","routes","route"],"sources":["../../../../src/server/plugins/engine/plugin.ts"],"sourcesContent":["import {\n type Lifecycle,\n type Plugin,\n type RouteOptions,\n type Server,\n type ServerRoute\n} from '@hapi/hapi'\n\nimport { type FormModel } from '~/src/server/plugins/engine/models/index.js'\nimport { validatePluginOptions } from '~/src/server/plugins/engine/options.js'\nimport { getRoutes as getFileUploadStatusRoutes } from '~/src/server/plugins/engine/routes/file-upload.js'\nimport { makeLoadFormPreHandler } from '~/src/server/plugins/engine/routes/index.js'\nimport { getRoutes as getQuestionRoutes } from '~/src/server/plugins/engine/routes/questions.js'\nimport { getRoutes as getRepeaterItemDeleteRoutes } from '~/src/server/plugins/engine/routes/repeaters/item-delete.js'\nimport { getRoutes as getRepeaterSummaryRoutes } from '~/src/server/plugins/engine/routes/repeaters/summary.js'\nimport { type PluginOptions } from '~/src/server/plugins/engine/types.js'\nimport { registerVision } from '~/src/server/plugins/engine/vision.js'\nimport {\n type FormRequestPayloadRefs,\n type FormRequestRefs\n} from '~/src/server/routes/types.js'\nimport { CacheService } from '~/src/server/services/index.js'\n\nexport const plugin = {\n name: '@defra/forms-engine-plugin',\n dependencies: ['@hapi/crumb', '@hapi/yar', 'hapi-pino'],\n multiple: true,\n async register(server: Server, options: PluginOptions) {\n options = validatePluginOptions(options)\n\n const {\n model,\n cacheName,\n keyGenerator,\n sessionHydrator,\n nunjucks: nunjucksOptions,\n viewContext,\n preparePageEventRequestOptions\n } = options\n const cacheService = new CacheService({\n server,\n cacheName,\n options: {\n keyGenerator,\n sessionHydrator\n }\n })\n\n await registerVision(server, options)\n\n server.expose('baseLayoutPath', nunjucksOptions.baseLayoutPath)\n server.expose('viewContext', viewContext)\n server.expose('cacheService', cacheService)\n\n server.app.model = model\n\n // In-memory cache of FormModel items, exposed\n // (for testing purposes) through `server.app.models`\n const itemCache = new Map<string, { model: FormModel; updatedAt: Date }>()\n server.app.models = itemCache\n\n const loadFormPreHandler = makeLoadFormPreHandler(server, options)\n\n const getRouteOptions: RouteOptions<FormRequestRefs> = {\n pre: [\n {\n method:\n loadFormPreHandler as unknown as Lifecycle.Method<FormRequestRefs>\n }\n ]\n }\n\n const postRouteOptions: RouteOptions<FormRequestPayloadRefs> = {\n payload: {\n parse: true\n },\n pre: [\n {\n method:\n loadFormPreHandler as unknown as Lifecycle.Method<FormRequestPayloadRefs>\n }\n ]\n }\n\n const routes = [\n ...getQuestionRoutes(\n getRouteOptions,\n postRouteOptions,\n preparePageEventRequestOptions\n ),\n ...getRepeaterSummaryRoutes(getRouteOptions, postRouteOptions),\n ...getRepeaterItemDeleteRoutes(getRouteOptions, postRouteOptions),\n ...getFileUploadStatusRoutes()\n ]\n\n server.route(routes as unknown as ServerRoute[]) // TODO\n }\n} satisfies Plugin<PluginOptions>\n"],"mappings":"AASA,SAASA,qBAAqB;AAC9B,SAASC,SAAS,IAAIC,yBAAyB;AAC/C,SAASC,sBAAsB;AAC/B,
|
|
1
|
+
{"version":3,"file":"plugin.js","names":["validatePluginOptions","getRoutes","getSaveAndReturnExitRoutes","getFileUploadStatusRoutes","makeLoadFormPreHandler","getQuestionRoutes","getRepeaterItemDeleteRoutes","getRepeaterSummaryRoutes","registerVision","CacheService","plugin","name","dependencies","multiple","register","server","options","model","cacheName","keyGenerator","sessionHydrator","sessionPersister","nunjucks","nunjucksOptions","viewContext","preparePageEventRequestOptions","cacheService","expose","baseLayoutPath","app","itemCache","Map","models","loadFormPreHandler","getRouteOptions","pre","method","postRouteOptions","payload","parse","routes","route"],"sources":["../../../../src/server/plugins/engine/plugin.ts"],"sourcesContent":["import {\n type Lifecycle,\n type Plugin,\n type RouteOptions,\n type Server,\n type ServerRoute\n} from '@hapi/hapi'\n\nimport { type FormModel } from '~/src/server/plugins/engine/models/index.js'\nimport { validatePluginOptions } from '~/src/server/plugins/engine/options.js'\nimport { getRoutes as getSaveAndReturnExitRoutes } from '~/src/server/plugins/engine/routes/exit.js'\nimport { getRoutes as getFileUploadStatusRoutes } from '~/src/server/plugins/engine/routes/file-upload.js'\nimport { makeLoadFormPreHandler } from '~/src/server/plugins/engine/routes/index.js'\nimport { getRoutes as getQuestionRoutes } from '~/src/server/plugins/engine/routes/questions.js'\nimport { getRoutes as getRepeaterItemDeleteRoutes } from '~/src/server/plugins/engine/routes/repeaters/item-delete.js'\nimport { getRoutes as getRepeaterSummaryRoutes } from '~/src/server/plugins/engine/routes/repeaters/summary.js'\nimport { type PluginOptions } from '~/src/server/plugins/engine/types.js'\nimport { registerVision } from '~/src/server/plugins/engine/vision.js'\nimport {\n type FormRequestPayloadRefs,\n type FormRequestRefs\n} from '~/src/server/routes/types.js'\nimport { CacheService } from '~/src/server/services/index.js'\n\nexport const plugin = {\n name: '@defra/forms-engine-plugin',\n dependencies: ['@hapi/crumb', '@hapi/yar', 'hapi-pino'],\n multiple: true,\n async register(server: Server, options: PluginOptions) {\n options = validatePluginOptions(options)\n\n const {\n model,\n cacheName,\n keyGenerator,\n sessionHydrator,\n sessionPersister,\n nunjucks: nunjucksOptions,\n viewContext,\n preparePageEventRequestOptions\n } = options\n const cacheService = new CacheService({\n server,\n cacheName,\n options: {\n keyGenerator,\n sessionHydrator,\n sessionPersister\n }\n })\n\n await registerVision(server, options)\n\n server.expose('baseLayoutPath', nunjucksOptions.baseLayoutPath)\n server.expose('viewContext', viewContext)\n server.expose('cacheService', cacheService)\n\n server.app.model = model\n\n // In-memory cache of FormModel items, exposed\n // (for testing purposes) through `server.app.models`\n const itemCache = new Map<string, { model: FormModel; updatedAt: Date }>()\n server.app.models = itemCache\n\n const loadFormPreHandler = makeLoadFormPreHandler(server, options)\n\n const getRouteOptions: RouteOptions<FormRequestRefs> = {\n pre: [\n {\n method:\n loadFormPreHandler as unknown as Lifecycle.Method<FormRequestRefs>\n }\n ]\n }\n\n const postRouteOptions: RouteOptions<FormRequestPayloadRefs> = {\n payload: {\n parse: true\n },\n pre: [\n {\n method:\n loadFormPreHandler as unknown as Lifecycle.Method<FormRequestPayloadRefs>\n }\n ]\n }\n\n const routes = [\n ...getQuestionRoutes(\n getRouteOptions,\n postRouteOptions,\n preparePageEventRequestOptions\n ),\n ...getRepeaterSummaryRoutes(getRouteOptions, postRouteOptions),\n ...getRepeaterItemDeleteRoutes(getRouteOptions, postRouteOptions),\n ...getSaveAndReturnExitRoutes(getRouteOptions),\n ...getFileUploadStatusRoutes()\n ]\n\n server.route(routes as unknown as ServerRoute[]) // TODO\n }\n} satisfies Plugin<PluginOptions>\n"],"mappings":"AASA,SAASA,qBAAqB;AAC9B,SAASC,SAAS,IAAIC,0BAA0B;AAChD,SAASD,SAAS,IAAIE,yBAAyB;AAC/C,SAASC,sBAAsB;AAC/B,SAASH,SAAS,IAAII,iBAAiB;AACvC,SAASJ,SAAS,IAAIK,2BAA2B;AACjD,SAASL,SAAS,IAAIM,wBAAwB;AAE9C,SAASC,cAAc;AAKvB,SAASC,YAAY;AAErB,OAAO,MAAMC,MAAM,GAAG;EACpBC,IAAI,EAAE,4BAA4B;EAClCC,YAAY,EAAE,CAAC,aAAa,EAAE,WAAW,EAAE,WAAW,CAAC;EACvDC,QAAQ,EAAE,IAAI;EACd,MAAMC,QAAQA,CAACC,MAAc,EAAEC,OAAsB,EAAE;IACrDA,OAAO,GAAGhB,qBAAqB,CAACgB,OAAO,CAAC;IAExC,MAAM;MACJC,KAAK;MACLC,SAAS;MACTC,YAAY;MACZC,eAAe;MACfC,gBAAgB;MAChBC,QAAQ,EAAEC,eAAe;MACzBC,WAAW;MACXC;IACF,CAAC,GAAGT,OAAO;IACX,MAAMU,YAAY,GAAG,IAAIjB,YAAY,CAAC;MACpCM,MAAM;MACNG,SAAS;MACTF,OAAO,EAAE;QACPG,YAAY;QACZC,eAAe;QACfC;MACF;IACF,CAAC,CAAC;IAEF,MAAMb,cAAc,CAACO,MAAM,EAAEC,OAAO,CAAC;IAErCD,MAAM,CAACY,MAAM,CAAC,gBAAgB,EAAEJ,eAAe,CAACK,cAAc,CAAC;IAC/Db,MAAM,CAACY,MAAM,CAAC,aAAa,EAAEH,WAAW,CAAC;IACzCT,MAAM,CAACY,MAAM,CAAC,cAAc,EAAED,YAAY,CAAC;IAE3CX,MAAM,CAACc,GAAG,CAACZ,KAAK,GAAGA,KAAK;;IAExB;IACA;IACA,MAAMa,SAAS,GAAG,IAAIC,GAAG,CAAgD,CAAC;IAC1EhB,MAAM,CAACc,GAAG,CAACG,MAAM,GAAGF,SAAS;IAE7B,MAAMG,kBAAkB,GAAG7B,sBAAsB,CAACW,MAAM,EAAEC,OAAO,CAAC;IAElE,MAAMkB,eAA8C,GAAG;MACrDC,GAAG,EAAE,CACH;QACEC,MAAM,EACJH;MACJ,CAAC;IAEL,CAAC;IAED,MAAMI,gBAAsD,GAAG;MAC7DC,OAAO,EAAE;QACPC,KAAK,EAAE;MACT,CAAC;MACDJ,GAAG,EAAE,CACH;QACEC,MAAM,EACJH;MACJ,CAAC;IAEL,CAAC;IAED,MAAMO,MAAM,GAAG,CACb,GAAGnC,iBAAiB,CAClB6B,eAAe,EACfG,gBAAgB,EAChBZ,8BACF,CAAC,EACD,GAAGlB,wBAAwB,CAAC2B,eAAe,EAAEG,gBAAgB,CAAC,EAC9D,GAAG/B,2BAA2B,CAAC4B,eAAe,EAAEG,gBAAgB,CAAC,EACjE,GAAGnC,0BAA0B,CAACgC,eAAe,CAAC,EAC9C,GAAG/B,yBAAyB,CAAC,CAAC,CAC/B;IAEDY,MAAM,CAAC0B,KAAK,CAACD,MAAkC,CAAC,EAAC;EACnD;AACF,CAAiC","ignoreList":[]}
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
import { type ResponseToolkit, type RouteOptions } from '@hapi/hapi';
|
|
2
|
+
import Joi from 'joi';
|
|
3
|
+
import { type FormRequest, type FormRequestRefs } from '~/src/server/routes/types.js';
|
|
4
|
+
export declare function getRoutes(getRouteOptions: RouteOptions<FormRequestRefs>): {
|
|
5
|
+
method: string;
|
|
6
|
+
path: string;
|
|
7
|
+
handler: (request: FormRequest, h: Pick<ResponseToolkit, "redirect" | "view">) => import("@hapi/hapi").ResponseObject;
|
|
8
|
+
options: {
|
|
9
|
+
validate: {
|
|
10
|
+
params: Joi.ObjectSchema<any>;
|
|
11
|
+
};
|
|
12
|
+
auth?: false | string | import("@hapi/hapi").RouteOptionsAccess | undefined;
|
|
13
|
+
app?: import("@hapi/hapi").RouteOptionsApp | undefined;
|
|
14
|
+
bind?: object | null | undefined;
|
|
15
|
+
cache?: false | import("@hapi/hapi").RouteOptionsCache | undefined;
|
|
16
|
+
compression?: { [P in keyof import("@hapi/hapi").ContentEncoders]?: Parameters<import("@hapi/hapi").ContentEncoders[P]>[0]; } | undefined;
|
|
17
|
+
cors?: boolean | import("@hapi/hapi").RouteOptionsCors | undefined;
|
|
18
|
+
description?: string | undefined;
|
|
19
|
+
ext?: { [key in import("@hapi/hapi").RouteRequestExtType]?: import("@hapi/hapi").RouteExtObject | import("@hapi/hapi").RouteExtObject[] | undefined; } | undefined;
|
|
20
|
+
files?: {
|
|
21
|
+
relativeTo: string;
|
|
22
|
+
} | undefined;
|
|
23
|
+
handler?: object | import("@hapi/hapi").Lifecycle.Method<FormRequestRefs, import("@hapi/hapi").Lifecycle.ReturnValue<FormRequestRefs>> | undefined;
|
|
24
|
+
id?: string | undefined;
|
|
25
|
+
isInternal?: boolean | undefined;
|
|
26
|
+
json?: import("@hapi/hapi").Json.StringifyArguments | undefined;
|
|
27
|
+
log?: {
|
|
28
|
+
collect: boolean;
|
|
29
|
+
} | undefined;
|
|
30
|
+
notes?: string | string[] | undefined;
|
|
31
|
+
payload?: import("@hapi/hapi").RouteOptionsPayload | undefined;
|
|
32
|
+
plugins?: import("@hapi/hapi").PluginSpecificConfiguration | undefined;
|
|
33
|
+
pre?: import("@hapi/hapi").RouteOptionsPreArray<FormRequestRefs> | undefined;
|
|
34
|
+
response?: import("@hapi/hapi").RouteOptionsResponse | undefined;
|
|
35
|
+
security?: import("@hapi/hapi").RouteOptionsSecure | undefined;
|
|
36
|
+
state?: {
|
|
37
|
+
parse?: boolean | undefined;
|
|
38
|
+
failAction?: import("@hapi/hapi").Lifecycle.FailAction | undefined;
|
|
39
|
+
} | undefined;
|
|
40
|
+
tags?: string[] | undefined;
|
|
41
|
+
timeout?: {
|
|
42
|
+
server?: boolean | number | undefined;
|
|
43
|
+
socket?: boolean | number | undefined;
|
|
44
|
+
} | undefined;
|
|
45
|
+
};
|
|
46
|
+
}[];
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import { slugSchema } from '@defra/forms-model';
|
|
2
|
+
import Boom from '@hapi/boom';
|
|
3
|
+
import Joi from 'joi';
|
|
4
|
+
export function getRoutes(getRouteOptions) {
|
|
5
|
+
return [{
|
|
6
|
+
method: 'get',
|
|
7
|
+
path: '/{slug}/exit',
|
|
8
|
+
handler: (request, h) => {
|
|
9
|
+
const {
|
|
10
|
+
app
|
|
11
|
+
} = request;
|
|
12
|
+
const {
|
|
13
|
+
model
|
|
14
|
+
} = app;
|
|
15
|
+
if (!model) {
|
|
16
|
+
throw Boom.notFound('No model found for exit page');
|
|
17
|
+
}
|
|
18
|
+
const returnUrl = request.query.returnUrl;
|
|
19
|
+
const exitViewModel = {
|
|
20
|
+
pageTitle: 'Your progress has been saved',
|
|
21
|
+
phaseTag: model.def.phaseBanner?.phase,
|
|
22
|
+
returnUrl
|
|
23
|
+
};
|
|
24
|
+
return h.view('exit', exitViewModel);
|
|
25
|
+
},
|
|
26
|
+
options: {
|
|
27
|
+
...getRouteOptions,
|
|
28
|
+
validate: {
|
|
29
|
+
params: Joi.object().keys({
|
|
30
|
+
slug: slugSchema
|
|
31
|
+
})
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
}];
|
|
35
|
+
}
|
|
36
|
+
//# sourceMappingURL=exit.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"exit.js","names":["slugSchema","Boom","Joi","getRoutes","getRouteOptions","method","path","handler","request","h","app","model","notFound","returnUrl","query","exitViewModel","pageTitle","phaseTag","def","phaseBanner","phase","view","options","validate","params","object","keys","slug"],"sources":["../../../../../src/server/plugins/engine/routes/exit.ts"],"sourcesContent":["import { slugSchema } from '@defra/forms-model'\nimport Boom from '@hapi/boom'\nimport { type ResponseToolkit, type RouteOptions } from '@hapi/hapi'\nimport Joi from 'joi'\n\nimport {\n type FormRequest,\n type FormRequestRefs\n} from '~/src/server/routes/types.js'\n\nexport function getRoutes(getRouteOptions: RouteOptions<FormRequestRefs>) {\n return [\n {\n method: 'get',\n path: '/{slug}/exit',\n handler: (\n request: FormRequest,\n h: Pick<ResponseToolkit, 'redirect' | 'view'>\n ) => {\n const { app } = request\n const { model } = app\n\n if (!model) {\n throw Boom.notFound('No model found for exit page')\n }\n\n const returnUrl = request.query.returnUrl\n\n const exitViewModel = {\n pageTitle: 'Your progress has been saved',\n phaseTag: model.def.phaseBanner?.phase,\n returnUrl\n }\n\n return h.view('exit', exitViewModel)\n },\n options: {\n ...getRouteOptions,\n validate: {\n params: Joi.object().keys({\n slug: slugSchema\n })\n }\n }\n }\n ]\n}\n"],"mappings":"AAAA,SAASA,UAAU,QAAQ,oBAAoB;AAC/C,OAAOC,IAAI,MAAM,YAAY;AAE7B,OAAOC,GAAG,MAAM,KAAK;AAOrB,OAAO,SAASC,SAASA,CAACC,eAA8C,EAAE;EACxE,OAAO,CACL;IACEC,MAAM,EAAE,KAAK;IACbC,IAAI,EAAE,cAAc;IACpBC,OAAO,EAAEA,CACPC,OAAoB,EACpBC,CAA6C,KAC1C;MACH,MAAM;QAAEC;MAAI,CAAC,GAAGF,OAAO;MACvB,MAAM;QAAEG;MAAM,CAAC,GAAGD,GAAG;MAErB,IAAI,CAACC,KAAK,EAAE;QACV,MAAMV,IAAI,CAACW,QAAQ,CAAC,8BAA8B,CAAC;MACrD;MAEA,MAAMC,SAAS,GAAGL,OAAO,CAACM,KAAK,CAACD,SAAS;MAEzC,MAAME,aAAa,GAAG;QACpBC,SAAS,EAAE,8BAA8B;QACzCC,QAAQ,EAAEN,KAAK,CAACO,GAAG,CAACC,WAAW,EAAEC,KAAK;QACtCP;MACF,CAAC;MAED,OAAOJ,CAAC,CAACY,IAAI,CAAC,MAAM,EAAEN,aAAa,CAAC;IACtC,CAAC;IACDO,OAAO,EAAE;MACP,GAAGlB,eAAe;MAClBmB,QAAQ,EAAE;QACRC,MAAM,EAAEtB,GAAG,CAACuB,MAAM,CAAC,CAAC,CAACC,IAAI,CAAC;UACxBC,IAAI,EAAE3B;QACR,CAAC;MACH;IACF;EACF,CAAC,CACF;AACH","ignoreList":[]}
|
|
@@ -10,6 +10,7 @@ import { type ViewContext } from '~/src/server/plugins/nunjucks/types.js';
|
|
|
10
10
|
import { type FormAction, type FormParams, type FormRequest, type FormRequestPayload } from '~/src/server/routes/types.js';
|
|
11
11
|
import { type RequestOptions } from '~/src/server/services/httpService.js';
|
|
12
12
|
import { type Services } from '~/src/server/types.js';
|
|
13
|
+
type RequestType = Request | FormRequest | FormRequestPayload;
|
|
13
14
|
/**
|
|
14
15
|
* Form submission state stores the following in Redis:
|
|
15
16
|
* Props containing user's submitted values as `{ [inputId]: value }` or as `{ [sectionName]: { [inputName]: value } }`
|
|
@@ -234,6 +235,7 @@ export interface FormPageViewModel extends PageViewModelBase {
|
|
|
234
235
|
context: FormContext;
|
|
235
236
|
errors?: FormSubmissionError[];
|
|
236
237
|
hasMissingNotificationEmail?: boolean;
|
|
238
|
+
allowSaveAndReturn?: boolean;
|
|
237
239
|
}
|
|
238
240
|
export interface RepeaterSummaryPageViewModel extends PageViewModelBase {
|
|
239
241
|
context: FormContext;
|
|
@@ -268,8 +270,9 @@ export interface PluginOptions {
|
|
|
268
270
|
cacheName?: string;
|
|
269
271
|
globals?: Record<string, GlobalFunction>;
|
|
270
272
|
filters?: Record<string, FilterFunction>;
|
|
271
|
-
keyGenerator?: (request:
|
|
272
|
-
sessionHydrator?: (request:
|
|
273
|
+
keyGenerator?: (request: RequestType) => string;
|
|
274
|
+
sessionHydrator?: (request: RequestType) => Promise<FormSubmissionState>;
|
|
275
|
+
sessionPersister?: (key: string, state: FormSubmissionState, request: RequestType) => Promise<void>;
|
|
273
276
|
pluginPath?: string;
|
|
274
277
|
nunjucks: {
|
|
275
278
|
baseLayoutPath: string;
|
|
@@ -280,3 +283,4 @@ export interface PluginOptions {
|
|
|
280
283
|
onRequest?: OnRequestCallback;
|
|
281
284
|
baseUrl: string;
|
|
282
285
|
}
|
|
286
|
+
export {};
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"types.js","names":["UploadStatus","FileStatus"],"sources":["../../../../src/server/plugins/engine/types.ts"],"sourcesContent":["import {\n type ComponentDef,\n type Event,\n type FormDefinition,\n type FormMetadata,\n type Item,\n type List,\n type Page\n} from '@defra/forms-model'\nimport { type PluginProperties, type Request } from '@hapi/hapi'\nimport { type JoiExpression, type ValidationErrorItem } from 'joi'\n\nimport { FormComponent } from '~/src/server/plugins/engine/components/FormComponent.js'\nimport { type Component } from '~/src/server/plugins/engine/components/helpers.js'\nimport {\n type BackLink,\n type ComponentText,\n type ComponentViewModel\n} from '~/src/server/plugins/engine/components/types.js'\nimport { type FormModel } from '~/src/server/plugins/engine/models/index.js'\nimport { type PageController } from '~/src/server/plugins/engine/pageControllers/PageController.js'\nimport { type PageControllerClass } from '~/src/server/plugins/engine/pageControllers/helpers.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} from '~/src/server/routes/types.js'\nimport { type RequestOptions } from '~/src/server/services/httpService.js'\nimport { type Services } from '~/src/server/types.js'\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}\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<FormRequest, 'app' | 'method' | 'params' | 'path' | 'query' | 'url'>\n\nexport interface UploadInitiateResponse {\n uploadId: string\n uploadUrl: string\n statusUrl: string\n}\n\nexport enum UploadStatus {\n initiated = 'initiated',\n pending = 'pending',\n ready = 'ready'\n}\n\nexport enum FileStatus {\n complete = 'complete',\n rejected = 'rejected',\n pending = 'pending'\n}\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}\n\nexport interface RepeaterSummaryPageViewModel extends PageViewModelBase {\n context: FormContext\n errors?: FormSubmissionError[]\n checkAnswers: CheckAnswers[]\n repeatTitle: string\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: FormRequest | FormRequestPayload,\n params: FormParams,\n definition: FormDefinition,\n metadata: FormMetadata\n) => void\n\nexport interface PluginOptions {\n model?: FormModel\n services?: Services\n controllers?: Record<string, typeof PageController>\n cacheName?: string\n globals?: Record<string, GlobalFunction>\n filters?: Record<string, FilterFunction>\n keyGenerator?: (request: Request | FormRequest | FormRequestPayload) => string\n sessionHydrator?: (\n request: Request | FormRequest | FormRequestPayload\n ) => Promise<FormSubmissionState>\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"],"mappings":"AAgCA;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;;AAkGA,WAAYA,YAAY,0BAAZA,YAAY;EAAZA,YAAY;EAAZA,YAAY;EAAZA,YAAY;EAAA,OAAZA,YAAY;AAAA;AAMxB,WAAYC,UAAU,0BAAVA,UAAU;EAAVA,UAAU;EAAVA,UAAU;EAAVA,UAAU;EAAA,OAAVA,UAAU;AAAA","ignoreList":[]}
|
|
1
|
+
{"version":3,"file":"types.js","names":["UploadStatus","FileStatus"],"sources":["../../../../src/server/plugins/engine/types.ts"],"sourcesContent":["import {\n type ComponentDef,\n type Event,\n type FormDefinition,\n type FormMetadata,\n type Item,\n type List,\n type Page\n} from '@defra/forms-model'\nimport { type PluginProperties, type Request } from '@hapi/hapi'\nimport { type JoiExpression, type ValidationErrorItem } from 'joi'\n\nimport { FormComponent } from '~/src/server/plugins/engine/components/FormComponent.js'\nimport { type Component } from '~/src/server/plugins/engine/components/helpers.js'\nimport {\n type BackLink,\n type ComponentText,\n type ComponentViewModel\n} from '~/src/server/plugins/engine/components/types.js'\nimport { type FormModel } from '~/src/server/plugins/engine/models/index.js'\nimport { type PageController } from '~/src/server/plugins/engine/pageControllers/PageController.js'\nimport { type PageControllerClass } from '~/src/server/plugins/engine/pageControllers/helpers.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} from '~/src/server/routes/types.js'\nimport { type RequestOptions } from '~/src/server/services/httpService.js'\nimport { type Services } from '~/src/server/types.js'\n\ntype RequestType = Request | FormRequest | FormRequestPayload\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}\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<FormRequest, 'app' | 'method' | 'params' | 'path' | 'query' | 'url'>\n\nexport interface UploadInitiateResponse {\n uploadId: string\n uploadUrl: string\n statusUrl: string\n}\n\nexport enum UploadStatus {\n initiated = 'initiated',\n pending = 'pending',\n ready = 'ready'\n}\n\nexport enum FileStatus {\n complete = 'complete',\n rejected = 'rejected',\n pending = 'pending'\n}\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 allowSaveAndReturn?: boolean\n}\n\nexport interface RepeaterSummaryPageViewModel extends PageViewModelBase {\n context: FormContext\n errors?: FormSubmissionError[]\n checkAnswers: CheckAnswers[]\n repeatTitle: string\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: FormRequest | FormRequestPayload,\n params: FormParams,\n definition: FormDefinition,\n metadata: FormMetadata\n) => void\n\nexport interface PluginOptions {\n model?: FormModel\n services?: Services\n controllers?: Record<string, typeof PageController>\n cacheName?: string\n globals?: Record<string, GlobalFunction>\n filters?: Record<string, FilterFunction>\n keyGenerator?: (request: RequestType) => string\n sessionHydrator?: (request: RequestType) => Promise<FormSubmissionState>\n sessionPersister?: (\n key: string,\n state: FormSubmissionState,\n request: RequestType\n ) => Promise<void>\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"],"mappings":"AAkCA;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;;AAkGA,WAAYA,YAAY,0BAAZA,YAAY;EAAZA,YAAY;EAAZA,YAAY;EAAZA,YAAY;EAAA,OAAZA,YAAY;AAAA;AAMxB,WAAYC,UAAU,0BAAVA,UAAU;EAAVA,UAAU;EAAVA,UAAU;EAAVA,UAAU;EAAA,OAAVA,UAAU;AAAA","ignoreList":[]}
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
{% extends baseLayoutPath %}
|
|
2
|
+
|
|
3
|
+
{% from "govuk/components/panel/macro.njk" import govukPanel %}
|
|
4
|
+
{% from "govuk/components/button/macro.njk" import govukButton %}
|
|
5
|
+
|
|
6
|
+
{% set mainClasses = "govuk-main-wrapper--l" %}
|
|
7
|
+
|
|
8
|
+
{% block content %}
|
|
9
|
+
<div class="govuk-grid-row">
|
|
10
|
+
<div class="govuk-grid-column-two-thirds">
|
|
11
|
+
{{ govukPanel({
|
|
12
|
+
titleText: pageTitle or "Your progress has been saved"
|
|
13
|
+
}) }}
|
|
14
|
+
|
|
15
|
+
<h2 class="govuk-heading-m">What happens next</h2>
|
|
16
|
+
<div class="app-prose-scope">
|
|
17
|
+
<p class="govuk-body">Your form progress has been saved. You can return to complete your application at any time using the link provided.</p>
|
|
18
|
+
|
|
19
|
+
{% if returnUrl %}
|
|
20
|
+
<p class="govuk-body">
|
|
21
|
+
{{ govukButton({
|
|
22
|
+
text: "Return to application",
|
|
23
|
+
href: returnUrl,
|
|
24
|
+
classes: "govuk-button--secondary"
|
|
25
|
+
}) }}
|
|
26
|
+
</p>
|
|
27
|
+
{% endif %}
|
|
28
|
+
</div>
|
|
29
|
+
</div>
|
|
30
|
+
</div>
|
|
31
|
+
{% endblock %}
|
|
@@ -3,13 +3,24 @@
|
|
|
3
3
|
|
|
4
4
|
<form method="post" novalidate>
|
|
5
5
|
<input type="hidden" name="crumb" value="{{ crumb }}">
|
|
6
|
-
<input type="hidden" name="action" value="validate">
|
|
7
6
|
|
|
8
7
|
{{ componentList(components) }}
|
|
9
8
|
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
9
|
+
<div class="govuk-button-group">
|
|
10
|
+
{{ govukButton({
|
|
11
|
+
text: "Start now" if isStartPage else "Continue",
|
|
12
|
+
isStartButton: isStartPage,
|
|
13
|
+
preventDoubleClick: true
|
|
14
|
+
}) }}
|
|
15
|
+
|
|
16
|
+
{% if allowSaveAndReturn %}
|
|
17
|
+
{{ govukButton({
|
|
18
|
+
text: "Save and return",
|
|
19
|
+
classes: "govuk-button--secondary",
|
|
20
|
+
name: "action",
|
|
21
|
+
value: "save-and-return",
|
|
22
|
+
preventDoubleClick: true
|
|
23
|
+
}) }}
|
|
24
|
+
{% endif %}
|
|
25
|
+
</div>
|
|
15
26
|
</form>
|
|
@@ -4,6 +4,7 @@ export let FormAction = /*#__PURE__*/function (FormAction) {
|
|
|
4
4
|
FormAction["Delete"] = "delete";
|
|
5
5
|
FormAction["AddAnother"] = "add-another";
|
|
6
6
|
FormAction["Send"] = "send";
|
|
7
|
+
FormAction["SaveAndReturn"] = "save-and-return";
|
|
7
8
|
return FormAction;
|
|
8
9
|
}({});
|
|
9
10
|
export let FormStatus = /*#__PURE__*/function (FormStatus) {
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"types.js","names":["FormAction","FormStatus"],"sources":["../../../src/server/routes/types.ts"],"sourcesContent":["import { type ReqRefDefaults, type Request } from '@hapi/hapi'\n\nimport { type FormPayload } from '~/src/server/plugins/engine/types.js'\n\nexport interface FormQuery extends Partial<Record<string, string>> {\n /**\n * Allow preview URL direct access without relevant page checks\n */\n force?: string\n\n /**\n * Redirect location after 'continue' form action\n */\n returnUrl?: string\n}\n\nexport interface FormParams extends Partial<Record<string, string>> {\n path: string\n slug: string\n state?: FormStatus\n}\n\nexport interface FormRequestRefs\n extends Omit<ReqRefDefaults, 'Params' | 'Payload' | 'Query'> {\n Params: FormParams\n Payload: object | undefined\n Query: FormQuery\n}\n\nexport interface FormRequestPayloadRefs extends FormRequestRefs {\n Payload: FormPayload\n}\n\nexport type FormRequest = Request<FormRequestRefs>\nexport type FormRequestPayload = Request<FormRequestPayloadRefs>\n\nexport enum FormAction {\n Continue = 'continue',\n Validate = 'validate',\n Delete = 'delete',\n AddAnother = 'add-another',\n Send = 'send'\n}\n\nexport enum FormStatus {\n Draft = 'draft',\n Live = 'live'\n}\n"],"mappings":"AAoCA,WAAYA,UAAU,0BAAVA,UAAU;EAAVA,UAAU;EAAVA,UAAU;EAAVA,UAAU;EAAVA,UAAU;EAAVA,UAAU;EAAA,OAAVA,UAAU;AAAA;
|
|
1
|
+
{"version":3,"file":"types.js","names":["FormAction","FormStatus"],"sources":["../../../src/server/routes/types.ts"],"sourcesContent":["import { type ReqRefDefaults, type Request } from '@hapi/hapi'\n\nimport { type FormPayload } from '~/src/server/plugins/engine/types.js'\n\nexport interface FormQuery extends Partial<Record<string, string>> {\n /**\n * Allow preview URL direct access without relevant page checks\n */\n force?: string\n\n /**\n * Redirect location after 'continue' form action\n */\n returnUrl?: string\n}\n\nexport interface FormParams extends Partial<Record<string, string>> {\n path: string\n slug: string\n state?: FormStatus\n}\n\nexport interface FormRequestRefs\n extends Omit<ReqRefDefaults, 'Params' | 'Payload' | 'Query'> {\n Params: FormParams\n Payload: object | undefined\n Query: FormQuery\n}\n\nexport interface FormRequestPayloadRefs extends FormRequestRefs {\n Payload: FormPayload\n}\n\nexport type FormRequest = Request<FormRequestRefs>\nexport type FormRequestPayload = Request<FormRequestPayloadRefs>\n\nexport enum FormAction {\n Continue = 'continue',\n Validate = 'validate',\n Delete = 'delete',\n AddAnother = 'add-another',\n Send = 'send',\n SaveAndReturn = 'save-and-return'\n}\n\nexport enum FormStatus {\n Draft = 'draft',\n Live = 'live'\n}\n"],"mappings":"AAoCA,WAAYA,UAAU,0BAAVA,UAAU;EAAVA,UAAU;EAAVA,UAAU;EAAVA,UAAU;EAAVA,UAAU;EAAVA,UAAU;EAAVA,UAAU;EAAA,OAAVA,UAAU;AAAA;AAStB,WAAYC,UAAU,0BAAVA,UAAU;EAAVA,UAAU;EAAVA,UAAU;EAAA,OAAVA,UAAU;AAAA","ignoreList":[]}
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import Joi from 'joi';
|
|
2
2
|
import { FormAction, FormStatus } from "../routes/types.js";
|
|
3
3
|
export const stateSchema = Joi.string().valid(FormStatus.Draft, FormStatus.Live).required();
|
|
4
|
-
export const actionSchema = Joi.string().valid(FormAction.Continue, FormAction.Validate, FormAction.Delete, FormAction.AddAnother, FormAction.Send).default(FormAction.Validate).optional();
|
|
4
|
+
export const actionSchema = Joi.string().valid(FormAction.Continue, FormAction.Validate, FormAction.Delete, FormAction.AddAnother, FormAction.Send, FormAction.SaveAndReturn).default(FormAction.Validate).optional();
|
|
5
5
|
export const pathSchema = Joi.string().required();
|
|
6
6
|
export const itemIdSchema = Joi.string().uuid().required();
|
|
7
7
|
export const crumbSchema = Joi.string().optional().allow('');
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.js","names":["Joi","FormAction","FormStatus","stateSchema","string","valid","Draft","Live","required","actionSchema","Continue","Validate","Delete","AddAnother","Send","default","optional","pathSchema","itemIdSchema","uuid","crumbSchema","allow","confirmSchema","boolean","empty","paramsSchema","object","keys","action","confirm","crumb","itemId"],"sources":["../../../src/server/schemas/index.ts"],"sourcesContent":["import Joi from 'joi'\n\nimport { type FormPayloadParams } from '~/src/server/plugins/engine/types.js'\nimport { FormAction, FormStatus } from '~/src/server/routes/types.js'\n\nexport const stateSchema = Joi.string<FormStatus>()\n .valid(FormStatus.Draft, FormStatus.Live)\n .required()\n\nexport const actionSchema = Joi.string<FormAction>()\n .valid(\n FormAction.Continue,\n FormAction.Validate,\n FormAction.Delete,\n FormAction.AddAnother,\n FormAction.Send\n )\n .default(FormAction.Validate)\n .optional()\n\nexport const pathSchema = Joi.string().required()\nexport const itemIdSchema = Joi.string().uuid().required()\nexport const crumbSchema = Joi.string().optional().allow('')\nexport const confirmSchema = Joi.boolean().empty(false)\n\nexport const paramsSchema = Joi.object<FormPayloadParams>()\n .keys({\n action: actionSchema,\n confirm: confirmSchema,\n crumb: crumbSchema,\n itemId: itemIdSchema.optional()\n })\n .default({})\n .optional()\n"],"mappings":"AAAA,OAAOA,GAAG,MAAM,KAAK;AAGrB,SAASC,UAAU,EAAEC,UAAU;AAE/B,OAAO,MAAMC,WAAW,GAAGH,GAAG,CAACI,MAAM,CAAa,CAAC,CAChDC,KAAK,CAACH,UAAU,CAACI,KAAK,EAAEJ,UAAU,CAACK,IAAI,CAAC,CACxCC,QAAQ,CAAC,CAAC;AAEb,OAAO,MAAMC,YAAY,GAAGT,GAAG,CAACI,MAAM,CAAa,CAAC,CACjDC,KAAK,CACJJ,UAAU,CAACS,QAAQ,EACnBT,UAAU,CAACU,QAAQ,EACnBV,UAAU,CAACW,MAAM,EACjBX,UAAU,CAACY,UAAU,EACrBZ,UAAU,CAACa,
|
|
1
|
+
{"version":3,"file":"index.js","names":["Joi","FormAction","FormStatus","stateSchema","string","valid","Draft","Live","required","actionSchema","Continue","Validate","Delete","AddAnother","Send","SaveAndReturn","default","optional","pathSchema","itemIdSchema","uuid","crumbSchema","allow","confirmSchema","boolean","empty","paramsSchema","object","keys","action","confirm","crumb","itemId"],"sources":["../../../src/server/schemas/index.ts"],"sourcesContent":["import Joi from 'joi'\n\nimport { type FormPayloadParams } from '~/src/server/plugins/engine/types.js'\nimport { FormAction, FormStatus } from '~/src/server/routes/types.js'\n\nexport const stateSchema = Joi.string<FormStatus>()\n .valid(FormStatus.Draft, FormStatus.Live)\n .required()\n\nexport const actionSchema = Joi.string<FormAction>()\n .valid(\n FormAction.Continue,\n FormAction.Validate,\n FormAction.Delete,\n FormAction.AddAnother,\n FormAction.Send,\n FormAction.SaveAndReturn\n )\n .default(FormAction.Validate)\n .optional()\n\nexport const pathSchema = Joi.string().required()\nexport const itemIdSchema = Joi.string().uuid().required()\nexport const crumbSchema = Joi.string().optional().allow('')\nexport const confirmSchema = Joi.boolean().empty(false)\n\nexport const paramsSchema = Joi.object<FormPayloadParams>()\n .keys({\n action: actionSchema,\n confirm: confirmSchema,\n crumb: crumbSchema,\n itemId: itemIdSchema.optional()\n })\n .default({})\n .optional()\n"],"mappings":"AAAA,OAAOA,GAAG,MAAM,KAAK;AAGrB,SAASC,UAAU,EAAEC,UAAU;AAE/B,OAAO,MAAMC,WAAW,GAAGH,GAAG,CAACI,MAAM,CAAa,CAAC,CAChDC,KAAK,CAACH,UAAU,CAACI,KAAK,EAAEJ,UAAU,CAACK,IAAI,CAAC,CACxCC,QAAQ,CAAC,CAAC;AAEb,OAAO,MAAMC,YAAY,GAAGT,GAAG,CAACI,MAAM,CAAa,CAAC,CACjDC,KAAK,CACJJ,UAAU,CAACS,QAAQ,EACnBT,UAAU,CAACU,QAAQ,EACnBV,UAAU,CAACW,MAAM,EACjBX,UAAU,CAACY,UAAU,EACrBZ,UAAU,CAACa,IAAI,EACfb,UAAU,CAACc,aACb,CAAC,CACAC,OAAO,CAACf,UAAU,CAACU,QAAQ,CAAC,CAC5BM,QAAQ,CAAC,CAAC;AAEb,OAAO,MAAMC,UAAU,GAAGlB,GAAG,CAACI,MAAM,CAAC,CAAC,CAACI,QAAQ,CAAC,CAAC;AACjD,OAAO,MAAMW,YAAY,GAAGnB,GAAG,CAACI,MAAM,CAAC,CAAC,CAACgB,IAAI,CAAC,CAAC,CAACZ,QAAQ,CAAC,CAAC;AAC1D,OAAO,MAAMa,WAAW,GAAGrB,GAAG,CAACI,MAAM,CAAC,CAAC,CAACa,QAAQ,CAAC,CAAC,CAACK,KAAK,CAAC,EAAE,CAAC;AAC5D,OAAO,MAAMC,aAAa,GAAGvB,GAAG,CAACwB,OAAO,CAAC,CAAC,CAACC,KAAK,CAAC,KAAK,CAAC;AAEvD,OAAO,MAAMC,YAAY,GAAG1B,GAAG,CAAC2B,MAAM,CAAoB,CAAC,CACxDC,IAAI,CAAC;EACJC,MAAM,EAAEpB,YAAY;EACpBqB,OAAO,EAAEP,aAAa;EACtBQ,KAAK,EAAEV,WAAW;EAClBW,MAAM,EAAEb,YAAY,CAACF,QAAQ,CAAC;AAChC,CAAC,CAAC,CACDD,OAAO,CAAC,CAAC,CAAC,CAAC,CACXC,QAAQ,CAAC,CAAC","ignoreList":[]}
|
|
@@ -14,6 +14,7 @@ export declare class CacheService {
|
|
|
14
14
|
}>;
|
|
15
15
|
generateKey?: (request: Request | FormRequest | FormRequestPayload) => string;
|
|
16
16
|
customFetcher?: (request: Request | FormRequest | FormRequestPayload) => Promise<FormSubmissionState | null>;
|
|
17
|
+
customPersister?: (key: string, state: FormSubmissionState, request: Request | FormRequest | FormRequestPayload) => Promise<void>;
|
|
17
18
|
logger: Server['logger'];
|
|
18
19
|
constructor({ server, cacheName, options }: {
|
|
19
20
|
server: Server;
|
|
@@ -21,6 +22,7 @@ export declare class CacheService {
|
|
|
21
22
|
options?: {
|
|
22
23
|
keyGenerator?: (request: Request | FormRequest | FormRequestPayload) => string;
|
|
23
24
|
sessionHydrator?: (request: Request | FormRequest | FormRequestPayload) => Promise<FormSubmissionState | null>;
|
|
25
|
+
sessionPersister?: (key: string, state: FormSubmissionState, request: Request | FormRequest | FormRequestPayload) => Promise<void>;
|
|
24
26
|
};
|
|
25
27
|
});
|
|
26
28
|
getState(request: Request | FormRequest | FormRequestPayload): Promise<FormSubmissionState>;
|
|
@@ -12,6 +12,7 @@ export class CacheService {
|
|
|
12
12
|
cache;
|
|
13
13
|
generateKey;
|
|
14
14
|
customFetcher;
|
|
15
|
+
customPersister;
|
|
15
16
|
logger;
|
|
16
17
|
constructor({
|
|
17
18
|
server,
|
|
@@ -20,13 +21,15 @@ export class CacheService {
|
|
|
20
21
|
}) {
|
|
21
22
|
const {
|
|
22
23
|
keyGenerator,
|
|
23
|
-
sessionHydrator
|
|
24
|
+
sessionHydrator,
|
|
25
|
+
sessionPersister
|
|
24
26
|
} = options ?? {};
|
|
25
27
|
if (!cacheName) {
|
|
26
28
|
server.log('warn', 'You are using the default hapi cache. Please provide a cache name in plugin registration options.');
|
|
27
29
|
}
|
|
28
30
|
this.generateKey = keyGenerator ?? this.defaultKeyGenerator.bind(this);
|
|
29
31
|
this.customFetcher = sessionHydrator ?? undefined;
|
|
32
|
+
this.customPersister = sessionPersister ?? undefined;
|
|
30
33
|
this.cache = server.cache({
|
|
31
34
|
cache: cacheName,
|
|
32
35
|
segment: 'formSubmission'
|
|
@@ -34,13 +37,14 @@ export class CacheService {
|
|
|
34
37
|
this.logger = server.logger;
|
|
35
38
|
}
|
|
36
39
|
async getState(request) {
|
|
37
|
-
|
|
40
|
+
const key = this.Key(request);
|
|
41
|
+
let cached = await this.cache.get(key);
|
|
38
42
|
|
|
39
43
|
// If nothing in Redis, attempt to rehydrate from backend DB
|
|
40
44
|
if (!cached && this.customFetcher) {
|
|
41
45
|
const rehydrated = await this.customFetcher(request);
|
|
42
46
|
if (rehydrated != null) {
|
|
43
|
-
await this.cache.set(
|
|
47
|
+
await this.cache.set(key, rehydrated, config.get('sessionTimeout'));
|
|
44
48
|
cached = await this.getState(request);
|
|
45
49
|
}
|
|
46
50
|
}
|
|
@@ -82,8 +86,8 @@ export class CacheService {
|
|
|
82
86
|
if (!request.yar.id) {
|
|
83
87
|
throw new Error('No session ID found');
|
|
84
88
|
}
|
|
85
|
-
const state = request.params.state
|
|
86
|
-
const slug = request.params.slug
|
|
89
|
+
const state = request.params.state || '';
|
|
90
|
+
const slug = request.params.slug || '';
|
|
87
91
|
return `${request.yar.id}:${state}:${slug}:`;
|
|
88
92
|
}
|
|
89
93
|
|