@defra/forms-engine-plugin 1.4.1 → 2.0.0
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/README.md +2 -46
- package/.server/server/plugins/engine/configureEnginePlugin.d.ts +1 -1
- package/.server/server/plugins/engine/configureEnginePlugin.js +5 -2
- package/.server/server/plugins/engine/configureEnginePlugin.js.map +1 -1
- package/.server/server/plugins/engine/helpers.d.ts +11 -0
- package/.server/server/plugins/engine/helpers.js +7 -1
- package/.server/server/plugins/engine/helpers.js.map +1 -1
- package/.server/server/plugins/engine/models/SummaryViewModel.d.ts +1 -1
- package/.server/server/plugins/engine/models/SummaryViewModel.js +1 -1
- package/.server/server/plugins/engine/models/SummaryViewModel.js.map +1 -1
- package/.server/server/plugins/engine/options.js +5 -3
- package/.server/server/plugins/engine/options.js.map +1 -1
- package/.server/server/plugins/engine/options.test.js +18 -9
- package/.server/server/plugins/engine/options.test.js.map +1 -1
- package/.server/server/plugins/engine/pageControllers/PageController.d.ts +3 -1
- package/.server/server/plugins/engine/pageControllers/PageController.js +5 -1
- package/.server/server/plugins/engine/pageControllers/PageController.js.map +1 -1
- package/.server/server/plugins/engine/pageControllers/QuestionPageController.d.ts +1 -1
- package/.server/server/plugins/engine/pageControllers/QuestionPageController.js +2 -4
- package/.server/server/plugins/engine/pageControllers/QuestionPageController.js.map +1 -1
- package/.server/server/plugins/engine/pageControllers/StartPageController.d.ts +2 -2
- package/.server/server/plugins/engine/pageControllers/StartPageController.js +1 -3
- package/.server/server/plugins/engine/pageControllers/StartPageController.js.map +1 -1
- package/.server/server/plugins/engine/pageControllers/StatusPageController.d.ts +1 -0
- package/.server/server/plugins/engine/pageControllers/StatusPageController.js +1 -0
- package/.server/server/plugins/engine/pageControllers/StatusPageController.js.map +1 -1
- package/.server/server/plugins/engine/pageControllers/SummaryPageController.d.ts +0 -1
- package/.server/server/plugins/engine/pageControllers/SummaryPageController.js +1 -4
- package/.server/server/plugins/engine/pageControllers/SummaryPageController.js.map +1 -1
- package/.server/server/plugins/engine/pageControllers/TerminalPageController.d.ts +1 -0
- package/.server/server/plugins/engine/pageControllers/TerminalPageController.js +1 -0
- package/.server/server/plugins/engine/pageControllers/TerminalPageController.js.map +1 -1
- package/.server/server/plugins/engine/pageControllers/__stubs__/request.d.ts +4 -0
- package/.server/server/plugins/engine/pageControllers/__stubs__/request.js +14 -0
- package/.server/server/plugins/engine/pageControllers/__stubs__/request.js.map +1 -0
- package/.server/server/plugins/engine/pageControllers/__stubs__/server.d.ts +3 -0
- package/.server/server/plugins/engine/pageControllers/__stubs__/server.js +23 -0
- package/.server/server/plugins/engine/pageControllers/__stubs__/server.js.map +1 -0
- package/.server/server/plugins/engine/plugin.js +5 -6
- package/.server/server/plugins/engine/plugin.js.map +1 -1
- package/.server/server/plugins/engine/types.d.ts +7 -5
- package/.server/server/plugins/engine/types.js.map +1 -1
- package/.server/server/types.d.ts +2 -1
- package/.server/server/types.js.map +1 -1
- package/.server/typings/hapi/index.d.js.map +1 -1
- package/package.json +1 -1
- package/src/server/plugins/engine/README.md +2 -46
- package/src/server/plugins/engine/configureEnginePlugin.ts +4 -2
- package/src/server/plugins/engine/helpers.test.ts +3 -2
- package/src/server/plugins/engine/helpers.ts +9 -1
- package/src/server/plugins/engine/models/FormModel.test.ts +11 -10
- package/src/server/plugins/engine/models/SummaryViewModel.test.ts +7 -5
- package/src/server/plugins/engine/models/SummaryViewModel.ts +1 -1
- package/src/server/plugins/engine/options.js +5 -3
- package/src/server/plugins/engine/options.test.js +22 -11
- package/src/server/plugins/engine/outputFormatters/human/v1.test.ts +3 -3
- package/src/server/plugins/engine/pageControllers/FileUploadPageController.test.ts +12 -1
- package/src/server/plugins/engine/pageControllers/PageController.test.ts +9 -0
- package/src/server/plugins/engine/pageControllers/PageController.ts +10 -1
- package/src/server/plugins/engine/pageControllers/QuestionPageController.test.ts +34 -28
- package/src/server/plugins/engine/pageControllers/QuestionPageController.ts +2 -5
- package/src/server/plugins/engine/pageControllers/RepeatPageController.test.ts +19 -4
- package/src/server/plugins/engine/pageControllers/StartPageController.test.ts +32 -0
- package/src/server/plugins/engine/pageControllers/StartPageController.ts +2 -4
- package/src/server/plugins/engine/pageControllers/StatusPageController.test.ts +32 -0
- package/src/server/plugins/engine/pageControllers/StatusPageController.ts +1 -0
- package/src/server/plugins/engine/pageControllers/SummaryPageController.ts +1 -5
- package/src/server/plugins/engine/pageControllers/TerminalController.test.ts +9 -0
- package/src/server/plugins/engine/pageControllers/TerminalPageController.ts +1 -0
- package/src/server/plugins/engine/pageControllers/__stubs__/request.ts +21 -0
- package/src/server/plugins/engine/pageControllers/__stubs__/server.ts +27 -0
- package/src/server/plugins/engine/plugin.ts +6 -6
- package/src/server/plugins/engine/types.ts +14 -9
- package/src/server/types.ts +2 -0
- package/src/typings/hapi/index.d.ts +2 -0
|
@@ -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\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":[]}
|
|
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<\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 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 saveAndReturn?: {\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 }\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;;AAqGA,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":[]}
|
|
@@ -2,7 +2,7 @@ import { type FormDefinition, type FormMetadata, type SubmitPayload, type Submit
|
|
|
2
2
|
import { type FormModel } from '~/src/server/plugins/engine/models/index.js';
|
|
3
3
|
import { type DetailItem } from '~/src/server/plugins/engine/models/types.js';
|
|
4
4
|
import { type PageController } from '~/src/server/plugins/engine/pageControllers/PageController.js';
|
|
5
|
-
import { type OnRequestCallback, type PreparePageEventRequestOptions } from '~/src/server/plugins/engine/types.js';
|
|
5
|
+
import { type OnRequestCallback, type PluginOptions, type PreparePageEventRequestOptions } from '~/src/server/plugins/engine/types.js';
|
|
6
6
|
import { type FormRequestPayload, type FormStatus } from '~/src/server/routes/types.js';
|
|
7
7
|
export interface FormsService {
|
|
8
8
|
getFormMetadata: (slug: string) => Promise<FormMetadata>;
|
|
@@ -28,6 +28,7 @@ export interface RouteConfig {
|
|
|
28
28
|
controllers?: Record<string, typeof PageController>;
|
|
29
29
|
preparePageEventRequestOptions?: PreparePageEventRequestOptions;
|
|
30
30
|
onRequest?: OnRequestCallback;
|
|
31
|
+
saveAndReturn?: PluginOptions['saveAndReturn'];
|
|
31
32
|
}
|
|
32
33
|
export interface OutputService {
|
|
33
34
|
submit: (request: FormRequestPayload, model: FormModel, emailAddress: string, items: DetailItem[], submitResponse: SubmitResponsePayload) => Promise<void>;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"types.js","names":[],"sources":["../../src/server/types.ts"],"sourcesContent":["import {\n type FormDefinition,\n type FormMetadata,\n type SubmitPayload,\n type SubmitResponsePayload\n} from '@defra/forms-model'\n\nimport { type FormModel } from '~/src/server/plugins/engine/models/index.js'\nimport { type DetailItem } from '~/src/server/plugins/engine/models/types.js'\nimport { type PageController } from '~/src/server/plugins/engine/pageControllers/PageController.js'\nimport {\n type OnRequestCallback,\n type PreparePageEventRequestOptions\n} from '~/src/server/plugins/engine/types.js'\nimport {\n type FormRequestPayload,\n type FormStatus\n} from '~/src/server/routes/types.js'\n\nexport interface FormsService {\n getFormMetadata: (slug: string) => Promise<FormMetadata>\n getFormDefinition: (\n id: string,\n state: FormStatus\n ) => Promise<FormDefinition | undefined>\n}\n\nexport interface FormSubmissionService {\n persistFiles: (\n files: { fileId: string; initiatedRetrievalKey: string }[],\n persistedRetrievalKey: string\n ) => Promise<object>\n submit: (data: SubmitPayload) => Promise<SubmitResponsePayload | undefined>\n}\n\nexport interface Services {\n formsService: FormsService\n formSubmissionService: FormSubmissionService\n outputService: OutputService\n}\n\nexport interface RouteConfig {\n formFileName?: string\n formFilePath?: string\n enforceCsrf?: boolean\n services?: Services\n controllers?: Record<string, typeof PageController>\n preparePageEventRequestOptions?: PreparePageEventRequestOptions\n onRequest?: OnRequestCallback\n}\n\nexport interface OutputService {\n submit: (\n request: FormRequestPayload,\n model: FormModel,\n emailAddress: string,\n items: DetailItem[],\n submitResponse: SubmitResponsePayload\n ) => Promise<void>\n}\n"],"mappings":"","ignoreList":[]}
|
|
1
|
+
{"version":3,"file":"types.js","names":[],"sources":["../../src/server/types.ts"],"sourcesContent":["import {\n type FormDefinition,\n type FormMetadata,\n type SubmitPayload,\n type SubmitResponsePayload\n} from '@defra/forms-model'\n\nimport { type FormModel } from '~/src/server/plugins/engine/models/index.js'\nimport { type DetailItem } from '~/src/server/plugins/engine/models/types.js'\nimport { type PageController } from '~/src/server/plugins/engine/pageControllers/PageController.js'\nimport {\n type OnRequestCallback,\n type PluginOptions,\n type PreparePageEventRequestOptions\n} from '~/src/server/plugins/engine/types.js'\nimport {\n type FormRequestPayload,\n type FormStatus\n} from '~/src/server/routes/types.js'\n\nexport interface FormsService {\n getFormMetadata: (slug: string) => Promise<FormMetadata>\n getFormDefinition: (\n id: string,\n state: FormStatus\n ) => Promise<FormDefinition | undefined>\n}\n\nexport interface FormSubmissionService {\n persistFiles: (\n files: { fileId: string; initiatedRetrievalKey: string }[],\n persistedRetrievalKey: string\n ) => Promise<object>\n submit: (data: SubmitPayload) => Promise<SubmitResponsePayload | undefined>\n}\n\nexport interface Services {\n formsService: FormsService\n formSubmissionService: FormSubmissionService\n outputService: OutputService\n}\n\nexport interface RouteConfig {\n formFileName?: string\n formFilePath?: string\n enforceCsrf?: boolean\n services?: Services\n controllers?: Record<string, typeof PageController>\n preparePageEventRequestOptions?: PreparePageEventRequestOptions\n onRequest?: OnRequestCallback\n saveAndReturn?: PluginOptions['saveAndReturn']\n}\n\nexport interface OutputService {\n submit: (\n request: FormRequestPayload,\n model: FormModel,\n emailAddress: string,\n items: DetailItem[],\n submitResponse: SubmitResponsePayload\n ) => Promise<void>\n}\n"],"mappings":"","ignoreList":[]}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.js","names":[],"sources":["../../../src/typings/hapi/index.d.ts"],"sourcesContent":["/* eslint-disable @typescript-eslint/unified-signatures */\n\nimport { type Plugin } from '@hapi/hapi'\nimport { type ServerYar, type Yar } from '@hapi/yar'\nimport { type Logger } from 'pino'\n\nimport { type FormModel } from '~/src/server/plugins/engine/models/index.js'\nimport {\n type FormRequest,\n type FormRequestPayload\n} from '~/src/server/routes/types.js'\nimport { type CacheService } from '~/src/server/services/index.js'\n\ndeclare module '@hapi/hapi' {\n // Here we are decorating Hapi interface types with\n // props from plugins which doesn't export @types\n interface PluginProperties {\n crumb: {\n generate?: (request: Request | FormRequest | FormRequestPayload) => string\n }\n 'forms-engine-plugin': {\n baseLayoutPath: string\n cacheService: CacheService\n viewContext?: (\n request: FormRequest | FormRequestPayload | null\n ) => Record<string, unknown> | Promise<Record<string, unknown>>\n }\n }\n\n interface Request {\n logger: Logger\n yar: Yar\n }\n\n interface RequestApplicationState {\n model?: FormModel\n }\n\n interface Server {\n logger: Logger\n yar: ServerYar\n }\n\n interface ServerApplicationState {\n model?: FormModel\n models: Map<string, { model: FormModel; updatedAt: Date }>\n }\n}\n\ndeclare module '@hapi/scooter' {\n declare const hapiScooter: {\n plugin: Plugin\n }\n\n export = hapiScooter\n}\n\ndeclare module 'blankie' {\n declare const blankie: {\n plugin: Plugin<Record<string, boolean | string | string[]>>\n }\n\n export = blankie\n}\n\ndeclare module 'blipp' {\n declare const blipp: {\n plugin: Plugin\n }\n\n export = blipp\n}\n\ndeclare module 'hapi-pulse' {\n declare const hapiPulse: {\n plugin: Plugin<{\n timeout: number\n }>\n }\n\n export = hapiPulse\n}\n"],"mappings":"","ignoreList":[]}
|
|
1
|
+
{"version":3,"file":"index.d.js","names":[],"sources":["../../../src/typings/hapi/index.d.ts"],"sourcesContent":["/* eslint-disable @typescript-eslint/unified-signatures */\n\nimport { type Plugin } from '@hapi/hapi'\nimport { type ServerYar, type Yar } from '@hapi/yar'\nimport { type Logger } from 'pino'\n\nimport { type FormModel } from '~/src/server/plugins/engine/models/index.js'\nimport { type PluginOptions } from '~/src/server/plugins/engine/types.ts'\nimport {\n type FormRequest,\n type FormRequestPayload\n} from '~/src/server/routes/types.js'\nimport { type CacheService } from '~/src/server/services/index.js'\n\ndeclare module '@hapi/hapi' {\n // Here we are decorating Hapi interface types with\n // props from plugins which doesn't export @types\n interface PluginProperties {\n crumb: {\n generate?: (request: Request | FormRequest | FormRequestPayload) => string\n }\n 'forms-engine-plugin': {\n baseLayoutPath: string\n cacheService: CacheService\n viewContext?: (\n request: FormRequest | FormRequestPayload | null\n ) => Record<string, unknown> | Promise<Record<string, unknown>>\n saveAndReturn?: PluginOptions['saveAndReturn']\n }\n }\n\n interface Request {\n logger: Logger\n yar: Yar\n }\n\n interface RequestApplicationState {\n model?: FormModel\n }\n\n interface Server {\n logger: Logger\n yar: ServerYar\n }\n\n interface ServerApplicationState {\n model?: FormModel\n models: Map<string, { model: FormModel; updatedAt: Date }>\n }\n}\n\ndeclare module '@hapi/scooter' {\n declare const hapiScooter: {\n plugin: Plugin\n }\n\n export = hapiScooter\n}\n\ndeclare module 'blankie' {\n declare const blankie: {\n plugin: Plugin<Record<string, boolean | string | string[]>>\n }\n\n export = blankie\n}\n\ndeclare module 'blipp' {\n declare const blipp: {\n plugin: Plugin\n }\n\n export = blipp\n}\n\ndeclare module 'hapi-pulse' {\n declare const hapiPulse: {\n plugin: Plugin<{\n timeout: number\n }>\n }\n\n export = hapiPulse\n}\n"],"mappings":"","ignoreList":[]}
|
package/package.json
CHANGED
|
@@ -86,53 +86,9 @@ There are a number of `LiquidJS` filters available to you from within the templa
|
|
|
86
86
|
]
|
|
87
87
|
```
|
|
88
88
|
|
|
89
|
-
|
|
89
|
+
### Save and return
|
|
90
90
|
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
### How it works
|
|
94
|
-
|
|
95
|
-
To support session rehydration from a backend (e.g. for Save & Return), the consuming application must provide two functions when registering the DXT engine plugin:
|
|
96
|
-
|
|
97
|
-
```
|
|
98
|
-
export interface PluginOptions {
|
|
99
|
-
...
|
|
100
|
-
keyGenerator?: (request) => string
|
|
101
|
-
sessionHydrator?: (request) => Promise<FormSubmissionState | null>
|
|
102
|
-
...
|
|
103
|
-
}
|
|
104
|
-
|
|
105
|
-
```
|
|
106
|
-
|
|
107
|
-
1. `keyGenerator(request)`
|
|
108
|
-
|
|
109
|
-
This generates a stable and consistent cache key used to store and retrieve user state. It should return a string based on persistent identifiers such as userId, businessId, and grantId — i.e., something like:
|
|
110
|
-
|
|
111
|
-
```
|
|
112
|
-
const keyGenerator = request => {
|
|
113
|
-
const { userId, businessId, grantId } = request.app.userContext
|
|
114
|
-
return `${userId}:${businessId}:${grantId}`
|
|
115
|
-
}
|
|
116
|
-
```
|
|
117
|
-
|
|
118
|
-
2. `sessionHydrator(request, key)`
|
|
119
|
-
|
|
120
|
-
This function is called when no session state is found in Redis. It should fetch saved state (e.g., from an API) using the provided key and return it in the same structure expected by the form engine:
|
|
121
|
-
|
|
122
|
-
```
|
|
123
|
-
const sessionHydrator = async (request, key) => {
|
|
124
|
-
const response = await fetch(`https://backend.api/state/${key}`)
|
|
125
|
-
if (!response.ok) return null
|
|
126
|
-
return await response.json() // Must match form engine state shape
|
|
127
|
-
}
|
|
128
|
-
```
|
|
129
|
-
|
|
130
|
-
#### Session flow
|
|
131
|
-
|
|
132
|
-
- When user resumes a journey and Redis session data is missing or expired, DXT will use `keyGenerator` and `sessionHydrator` to fetch the saved state from an external API (e.g. `/state` endpoint).
|
|
133
|
-
- The fetched state is written back into Redis and used to continue the user journey.
|
|
134
|
-
- The rehydrated state must include enough information to satisfy schema validation on the current or next page.
|
|
135
|
-
- To properly resume a session, users should be redirected to the `/summary` page. This ensures the UI has all required answers preloaded and avoids invalid transitions from deep links.
|
|
91
|
+
See [our save and return feature page](/docs/features/code-based/SAVE_AND_RETURN.md).
|
|
136
92
|
|
|
137
93
|
### Additional notes
|
|
138
94
|
|
|
@@ -18,7 +18,8 @@ export const configureEnginePlugin = async ({
|
|
|
18
18
|
services,
|
|
19
19
|
controllers,
|
|
20
20
|
preparePageEventRequestOptions,
|
|
21
|
-
onRequest
|
|
21
|
+
onRequest,
|
|
22
|
+
saveAndReturn
|
|
22
23
|
}: RouteConfig = {}): Promise<{
|
|
23
24
|
plugin: typeof plugin
|
|
24
25
|
options: PluginOptions
|
|
@@ -57,7 +58,8 @@ export const configureEnginePlugin = async ({
|
|
|
57
58
|
viewContext: devtoolContext,
|
|
58
59
|
preparePageEventRequestOptions,
|
|
59
60
|
onRequest,
|
|
60
|
-
baseUrl: 'http://localhost:3009' // always runs locally
|
|
61
|
+
baseUrl: 'http://localhost:3009', // always runs locally
|
|
62
|
+
saveAndReturn
|
|
61
63
|
}
|
|
62
64
|
}
|
|
63
65
|
}
|
|
@@ -18,6 +18,7 @@ import {
|
|
|
18
18
|
} from '~/src/server/plugins/engine/helpers.js'
|
|
19
19
|
import { handleLegacyRedirect } from '~/src/server/plugins/engine/helpers.js'
|
|
20
20
|
import { FormModel } from '~/src/server/plugins/engine/models/FormModel.js'
|
|
21
|
+
import { buildFormContextRequest } from '~/src/server/plugins/engine/pageControllers/__stubs__/request.js'
|
|
21
22
|
import {
|
|
22
23
|
createPage,
|
|
23
24
|
type PageControllerClass
|
|
@@ -56,7 +57,7 @@ describe('Helpers', () => {
|
|
|
56
57
|
page = createPage(model, definition.pages[0])
|
|
57
58
|
const pageUrl = new URL(page.href, 'http://example.com')
|
|
58
59
|
|
|
59
|
-
request = {
|
|
60
|
+
request = buildFormContextRequest({
|
|
60
61
|
method: 'get',
|
|
61
62
|
url: pageUrl,
|
|
62
63
|
path: pageUrl.pathname,
|
|
@@ -66,7 +67,7 @@ describe('Helpers', () => {
|
|
|
66
67
|
},
|
|
67
68
|
query: {},
|
|
68
69
|
app: { model }
|
|
69
|
-
}
|
|
70
|
+
})
|
|
70
71
|
|
|
71
72
|
const response = {
|
|
72
73
|
code: jest.fn().mockImplementation(() => response)
|
|
@@ -377,7 +377,15 @@ export function evaluateTemplate(
|
|
|
377
377
|
}
|
|
378
378
|
|
|
379
379
|
export function getCacheService(server: Server) {
|
|
380
|
-
return server.
|
|
380
|
+
return getPluginOptions(server).cacheService
|
|
381
|
+
}
|
|
382
|
+
|
|
383
|
+
export function getSaveAndReturnHelpers(server: Server) {
|
|
384
|
+
return getPluginOptions(server).saveAndReturn
|
|
385
|
+
}
|
|
386
|
+
|
|
387
|
+
export function getPluginOptions(server: Server) {
|
|
388
|
+
return server.plugins['forms-engine-plugin']
|
|
381
389
|
}
|
|
382
390
|
|
|
383
391
|
/**
|
|
@@ -6,6 +6,7 @@ import {
|
|
|
6
6
|
|
|
7
7
|
import { todayAsDateOnly } from '~/src/server/plugins/engine/date-helper.js'
|
|
8
8
|
import { FormModel } from '~/src/server/plugins/engine/models/FormModel.js'
|
|
9
|
+
import { buildFormContextRequest } from '~/src/server/plugins/engine/pageControllers/__stubs__/request.js'
|
|
9
10
|
import { type FormContextRequest } from '~/src/server/plugins/engine/types.js'
|
|
10
11
|
import { V2 as definitionV2 } from '~/test/form/definitions/conditions-basic.js'
|
|
11
12
|
import definition from '~/test/form/definitions/conditions-escaping.js'
|
|
@@ -151,7 +152,7 @@ describe('FormModel', () => {
|
|
|
151
152
|
}
|
|
152
153
|
const pageUrl = new URL('http://example.com/components/fields-required')
|
|
153
154
|
|
|
154
|
-
const request: FormContextRequest = {
|
|
155
|
+
const request: FormContextRequest = buildFormContextRequest({
|
|
155
156
|
method: 'post',
|
|
156
157
|
payload: { crumb: 'dummyCrumb', action: 'validate' },
|
|
157
158
|
query: {},
|
|
@@ -159,7 +160,7 @@ describe('FormModel', () => {
|
|
|
159
160
|
params: { path: 'components', slug: 'fields-required' },
|
|
160
161
|
url: pageUrl,
|
|
161
162
|
app: { model: formModel }
|
|
162
|
-
}
|
|
163
|
+
})
|
|
163
164
|
|
|
164
165
|
const context = formModel.getFormContext(request, state)
|
|
165
166
|
|
|
@@ -180,7 +181,7 @@ describe('FormModel', () => {
|
|
|
180
181
|
}
|
|
181
182
|
const pageUrl = new URL('http://example.com/components/fields-required')
|
|
182
183
|
|
|
183
|
-
const request: FormContextRequest = {
|
|
184
|
+
const request: FormContextRequest = buildFormContextRequest({
|
|
184
185
|
method: 'post',
|
|
185
186
|
payload: { crumb: 'dummyCrumb', action: 'validate' },
|
|
186
187
|
query: {},
|
|
@@ -188,7 +189,7 @@ describe('FormModel', () => {
|
|
|
188
189
|
params: { path: 'components', slug: 'fields-required' },
|
|
189
190
|
url: pageUrl,
|
|
190
191
|
app: { model: formModel }
|
|
191
|
-
}
|
|
192
|
+
})
|
|
192
193
|
|
|
193
194
|
expect(() => formModel.getFormContext(request, state)).toThrow(
|
|
194
195
|
'Reference number not found in form state'
|
|
@@ -206,7 +207,7 @@ describe('FormModel', () => {
|
|
|
206
207
|
}
|
|
207
208
|
const pageUrl = new URL('http://example.com/components/fields-required')
|
|
208
209
|
|
|
209
|
-
const request: FormContextRequest = {
|
|
210
|
+
const request: FormContextRequest = buildFormContextRequest({
|
|
210
211
|
method: 'post',
|
|
211
212
|
payload: { crumb: 'dummyCrumb', action: 'validate' },
|
|
212
213
|
query: {},
|
|
@@ -214,7 +215,7 @@ describe('FormModel', () => {
|
|
|
214
215
|
params: { path: 'components', slug: 'fields-required' },
|
|
215
216
|
url: pageUrl,
|
|
216
217
|
app: { model: formModel }
|
|
217
|
-
}
|
|
218
|
+
})
|
|
218
219
|
|
|
219
220
|
expect(() => formModel.getFormContext(request, state)).toThrow(
|
|
220
221
|
'Reference number not found in form state'
|
|
@@ -236,14 +237,14 @@ describe('FormModel', () => {
|
|
|
236
237
|
'http://example.com/conditional-list-items/summary'
|
|
237
238
|
)
|
|
238
239
|
|
|
239
|
-
const request: FormContextRequest = {
|
|
240
|
+
const request: FormContextRequest = buildFormContextRequest({
|
|
240
241
|
method: 'get',
|
|
241
242
|
query: {},
|
|
242
243
|
path: pageUrl.pathname,
|
|
243
244
|
params: { path: 'summary', slug: 'conditional-list-items' },
|
|
244
245
|
url: pageUrl,
|
|
245
246
|
app: { model: formModel }
|
|
246
|
-
}
|
|
247
|
+
})
|
|
247
248
|
|
|
248
249
|
const context = formModel.getFormContext(request, state)
|
|
249
250
|
|
|
@@ -271,14 +272,14 @@ describe('FormModel', () => {
|
|
|
271
272
|
'http://example.com/conditional-list-items/summary'
|
|
272
273
|
)
|
|
273
274
|
|
|
274
|
-
const request: FormContextRequest = {
|
|
275
|
+
const request: FormContextRequest = buildFormContextRequest({
|
|
275
276
|
method: 'get',
|
|
276
277
|
query: {},
|
|
277
278
|
path: pageUrl.pathname,
|
|
278
279
|
params: { path: 'summary', slug: 'conditional-list-items' },
|
|
279
280
|
url: pageUrl,
|
|
280
281
|
app: { model: formModel }
|
|
281
|
-
}
|
|
282
|
+
})
|
|
282
283
|
|
|
283
284
|
const context = formModel.getFormContext(request, state)
|
|
284
285
|
|
|
@@ -4,6 +4,8 @@ import {
|
|
|
4
4
|
SummaryViewModel
|
|
5
5
|
} from '~/src/server/plugins/engine/models/index.js'
|
|
6
6
|
import { SummaryPageController } from '~/src/server/plugins/engine/pageControllers/SummaryPageController.js'
|
|
7
|
+
import { buildFormContextRequest } from '~/src/server/plugins/engine/pageControllers/__stubs__/request.js'
|
|
8
|
+
import { serverWithSaveAndReturn } from '~/src/server/plugins/engine/pageControllers/__stubs__/server.js'
|
|
7
9
|
import {
|
|
8
10
|
createPage,
|
|
9
11
|
type PageControllerClass
|
|
@@ -14,7 +16,6 @@ import {
|
|
|
14
16
|
type FormState
|
|
15
17
|
} from '~/src/server/plugins/engine/types.js'
|
|
16
18
|
import definition from '~/test/form/definitions/repeat-mixed.js'
|
|
17
|
-
|
|
18
19
|
const basePath = `${FORM_PREFIX}/test`
|
|
19
20
|
|
|
20
21
|
describe('SummaryViewModel', () => {
|
|
@@ -36,7 +37,7 @@ describe('SummaryViewModel', () => {
|
|
|
36
37
|
page = createPage(model, definition.pages[2])
|
|
37
38
|
pageUrl = new URL('http://example.com/repeat/pizza-order/summary')
|
|
38
39
|
|
|
39
|
-
request = {
|
|
40
|
+
request = buildFormContextRequest({
|
|
40
41
|
method: 'get',
|
|
41
42
|
url: pageUrl,
|
|
42
43
|
path: pageUrl.pathname,
|
|
@@ -46,7 +47,7 @@ describe('SummaryViewModel', () => {
|
|
|
46
47
|
},
|
|
47
48
|
query: {},
|
|
48
49
|
app: { model }
|
|
49
|
-
}
|
|
50
|
+
})
|
|
50
51
|
})
|
|
51
52
|
|
|
52
53
|
describe.each([
|
|
@@ -272,13 +273,14 @@ describe('SummaryPageController', () => {
|
|
|
272
273
|
slug: 'repeat'
|
|
273
274
|
},
|
|
274
275
|
query: {},
|
|
275
|
-
app: { model }
|
|
276
|
+
app: { model },
|
|
277
|
+
server: serverWithSaveAndReturn
|
|
276
278
|
}
|
|
277
279
|
})
|
|
278
280
|
|
|
279
281
|
describe('Save and Return functionality', () => {
|
|
280
282
|
it('should show save and return button on summary page', () => {
|
|
281
|
-
expect(controller.shouldShowSaveAndReturn()).toBe(true)
|
|
283
|
+
expect(controller.shouldShowSaveAndReturn(request.server)).toBe(true)
|
|
282
284
|
})
|
|
283
285
|
|
|
284
286
|
it('should handle save and return from summary page', () => {
|
|
@@ -20,9 +20,11 @@ const pluginRegistrationOptionsSchema = Joi.object({
|
|
|
20
20
|
preparePageEventRequestOptions: Joi.function().optional(),
|
|
21
21
|
onRequest: Joi.function().optional(),
|
|
22
22
|
baseUrl: Joi.string().uri().required(),
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
23
|
+
saveAndReturn: Joi.object({
|
|
24
|
+
keyGenerator: Joi.function(),
|
|
25
|
+
sessionHydrator: Joi.function(),
|
|
26
|
+
sessionPersister: Joi.function()
|
|
27
|
+
}).optional()
|
|
26
28
|
})
|
|
27
29
|
|
|
28
30
|
/**
|
|
@@ -2,6 +2,9 @@ import { validatePluginOptions } from '~/src/server/plugins/engine/options.js'
|
|
|
2
2
|
|
|
3
3
|
describe('validatePluginOptions', () => {
|
|
4
4
|
it('returns the validated value for valid options', () => {
|
|
5
|
+
/**
|
|
6
|
+
* @type {PluginOptions}
|
|
7
|
+
*/
|
|
5
8
|
const validOptions = {
|
|
6
9
|
nunjucks: {
|
|
7
10
|
baseLayoutPath: 'dxt-devtool-baselayout.html',
|
|
@@ -17,6 +20,9 @@ describe('validatePluginOptions', () => {
|
|
|
17
20
|
})
|
|
18
21
|
|
|
19
22
|
it('accepts optional properties keyGenerator, sessionHydrator, and sessionPersister', () => {
|
|
23
|
+
/**
|
|
24
|
+
* @type {PluginOptions}
|
|
25
|
+
*/
|
|
20
26
|
const validOptionsWithOptionals = {
|
|
21
27
|
nunjucks: {
|
|
22
28
|
baseLayoutPath: 'dxt-devtool-baselayout.html',
|
|
@@ -26,9 +32,11 @@ describe('validatePluginOptions', () => {
|
|
|
26
32
|
return { hello: 'world' }
|
|
27
33
|
},
|
|
28
34
|
baseUrl: 'http://localhost:3009',
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
35
|
+
saveAndReturn: {
|
|
36
|
+
keyGenerator: () => 'test-key',
|
|
37
|
+
sessionHydrator: () => Promise.resolve({ someState: 'value' }),
|
|
38
|
+
sessionPersister: () => Promise.resolve(undefined)
|
|
39
|
+
}
|
|
32
40
|
}
|
|
33
41
|
|
|
34
42
|
expect(validatePluginOptions(validOptionsWithOptionals)).toEqual(
|
|
@@ -40,17 +48,20 @@ describe('validatePluginOptions', () => {
|
|
|
40
48
|
* tsc would usually check compliance with the type, but given a user might be using plain JS we still want a test
|
|
41
49
|
*/
|
|
42
50
|
it('fails if a required attribute is missing', () => {
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
51
|
+
const invalidOptions =
|
|
52
|
+
/** @type {PluginOptions} testing without viewContext */ ({
|
|
53
|
+
nunjucks: {
|
|
54
|
+
baseLayoutPath: 'dxt-devtool-baselayout.html',
|
|
55
|
+
paths: ['src/server/devserver'] // custom layout to make it really clear this is not the same as the runner
|
|
56
|
+
}
|
|
57
|
+
})
|
|
50
58
|
|
|
51
|
-
// @ts-expect-error -- add a test for JS users
|
|
52
59
|
expect(() => validatePluginOptions(invalidOptions)).toThrow(
|
|
53
60
|
'Invalid plugin options'
|
|
54
61
|
)
|
|
55
62
|
})
|
|
56
63
|
})
|
|
64
|
+
|
|
65
|
+
/**
|
|
66
|
+
* @import { PluginOptions } from '~/src/server/plugins/engine/types.js'
|
|
67
|
+
*/
|
|
@@ -7,7 +7,7 @@ import {
|
|
|
7
7
|
SummaryPageController,
|
|
8
8
|
getFormSubmissionData
|
|
9
9
|
} from '~/src/server/plugins/engine/pageControllers/SummaryPageController.js'
|
|
10
|
-
import {
|
|
10
|
+
import { buildFormContextRequest } from '~/src/server/plugins/engine/pageControllers/__stubs__/request.js'
|
|
11
11
|
import { FormStatus } from '~/src/server/routes/types.js'
|
|
12
12
|
import definition from '~/test/form/definitions/repeat-mixed.js'
|
|
13
13
|
|
|
@@ -52,7 +52,7 @@ const pageUrl = new URL('http://example.com/repeat/pizza-order/summary')
|
|
|
52
52
|
|
|
53
53
|
const controller = new SummaryPageController(model, pageDef)
|
|
54
54
|
|
|
55
|
-
const request = {
|
|
55
|
+
const request = buildFormContextRequest({
|
|
56
56
|
method: 'get',
|
|
57
57
|
url: pageUrl,
|
|
58
58
|
path: pageUrl.pathname,
|
|
@@ -62,7 +62,7 @@ const request = {
|
|
|
62
62
|
},
|
|
63
63
|
query: {},
|
|
64
64
|
app: { model }
|
|
65
|
-
}
|
|
65
|
+
})
|
|
66
66
|
|
|
67
67
|
const context = model.getFormContext(request, state)
|
|
68
68
|
const summaryViewModel = controller.getSummaryViewModel(request, context)
|
|
@@ -14,6 +14,7 @@ import {
|
|
|
14
14
|
prepareStatus
|
|
15
15
|
} from '~/src/server/plugins/engine/pageControllers/FileUploadPageController.js'
|
|
16
16
|
import { QuestionPageController } from '~/src/server/plugins/engine/pageControllers/QuestionPageController.js'
|
|
17
|
+
import { serverWithSaveAndReturn } from '~/src/server/plugins/engine/pageControllers/__stubs__/server.js'
|
|
17
18
|
import * as pageHelpers from '~/src/server/plugins/engine/pageControllers/helpers.js'
|
|
18
19
|
import * as uploadService from '~/src/server/plugins/engine/services/uploadService.js'
|
|
19
20
|
import {
|
|
@@ -31,6 +32,7 @@ import {
|
|
|
31
32
|
type FormRequest,
|
|
32
33
|
type FormRequestPayload
|
|
33
34
|
} from '~/src/server/routes/types.js'
|
|
35
|
+
import { type CacheService } from '~/src/server/services/index.js'
|
|
34
36
|
import definition from '~/test/form/definitions/file-upload-basic.js'
|
|
35
37
|
|
|
36
38
|
type TestableFileUploadPageController = FileUploadPageController & {
|
|
@@ -77,12 +79,13 @@ describe('FileUploadPageController', () => {
|
|
|
77
79
|
server: {
|
|
78
80
|
plugins: {
|
|
79
81
|
'forms-engine-plugin': {
|
|
82
|
+
baseLayoutPath: '',
|
|
80
83
|
cacheService: {
|
|
81
84
|
setFlash: jest.fn(),
|
|
82
85
|
setState: jest
|
|
83
86
|
.fn()
|
|
84
87
|
.mockImplementation((req, updated) => Promise.resolve(updated))
|
|
85
|
-
}
|
|
88
|
+
} as unknown as CacheService
|
|
86
89
|
}
|
|
87
90
|
}
|
|
88
91
|
},
|
|
@@ -1115,4 +1118,12 @@ describe('FileUploadPageController', () => {
|
|
|
1115
1118
|
)
|
|
1116
1119
|
})
|
|
1117
1120
|
})
|
|
1121
|
+
|
|
1122
|
+
describe('shouldShowSaveAndReturn', () => {
|
|
1123
|
+
it('should return true when save and return is enabled', () => {
|
|
1124
|
+
expect(controller.shouldShowSaveAndReturn(serverWithSaveAndReturn)).toBe(
|
|
1125
|
+
true
|
|
1126
|
+
)
|
|
1127
|
+
})
|
|
1128
|
+
})
|
|
1118
1129
|
})
|
|
@@ -3,6 +3,7 @@ import { type ResponseToolkit } from '@hapi/hapi'
|
|
|
3
3
|
import { FORM_PREFIX } from '~/src/server/constants.js'
|
|
4
4
|
import { FormModel } from '~/src/server/plugins/engine/models/FormModel.js'
|
|
5
5
|
import { PageController } from '~/src/server/plugins/engine/pageControllers/PageController.js'
|
|
6
|
+
import { serverWithSaveAndReturn } from '~/src/server/plugins/engine/pageControllers/__stubs__/server.js'
|
|
6
7
|
import { type FormRequest } from '~/src/server/routes/types.js'
|
|
7
8
|
import definition from '~/test/form/definitions/basic.js'
|
|
8
9
|
|
|
@@ -230,4 +231,12 @@ describe('PageController', () => {
|
|
|
230
231
|
)
|
|
231
232
|
})
|
|
232
233
|
})
|
|
234
|
+
|
|
235
|
+
describe('shouldShowSaveAndReturn', () => {
|
|
236
|
+
it('should return false (PageController does not allow save and return)', () => {
|
|
237
|
+
expect(controller1.shouldShowSaveAndReturn(serverWithSaveAndReturn)).toBe(
|
|
238
|
+
false
|
|
239
|
+
)
|
|
240
|
+
})
|
|
241
|
+
})
|
|
233
242
|
})
|
|
@@ -9,12 +9,14 @@ import Boom from '@hapi/boom'
|
|
|
9
9
|
import {
|
|
10
10
|
type Lifecycle,
|
|
11
11
|
type ResponseToolkit,
|
|
12
|
-
type RouteOptions
|
|
12
|
+
type RouteOptions,
|
|
13
|
+
type Server
|
|
13
14
|
} from '@hapi/hapi'
|
|
14
15
|
|
|
15
16
|
import { type ComponentCollection } from '~/src/server/plugins/engine/components/ComponentCollection.js'
|
|
16
17
|
import {
|
|
17
18
|
encodeUrl,
|
|
19
|
+
getSaveAndReturnHelpers,
|
|
18
20
|
getStartPath,
|
|
19
21
|
normalisePath
|
|
20
22
|
} from '~/src/server/plugins/engine/helpers.js'
|
|
@@ -45,6 +47,7 @@ export class PageController {
|
|
|
45
47
|
events?: Events
|
|
46
48
|
collection?: ComponentCollection
|
|
47
49
|
viewName = 'index'
|
|
50
|
+
allowSaveAndReturn = false
|
|
48
51
|
|
|
49
52
|
constructor(model: FormModel, pageDef: Page) {
|
|
50
53
|
const { def } = model
|
|
@@ -183,4 +186,10 @@ export class PageController {
|
|
|
183
186
|
) => ReturnType<Lifecycle.Method<FormRequestPayloadRefs>> {
|
|
184
187
|
throw Boom.badRequest('Unsupported POST route handler for this page')
|
|
185
188
|
}
|
|
189
|
+
|
|
190
|
+
shouldShowSaveAndReturn(server: Server): boolean {
|
|
191
|
+
return (
|
|
192
|
+
getSaveAndReturnHelpers(server) !== undefined && this.allowSaveAndReturn
|
|
193
|
+
)
|
|
194
|
+
}
|
|
186
195
|
}
|