@defra/forms-engine-plugin 4.6.0 → 4.7.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.
Files changed (26) hide show
  1. package/.server/server/plugins/engine/beta/form-context.js +1 -5
  2. package/.server/server/plugins/engine/beta/form-context.js.map +1 -1
  3. package/.server/server/plugins/engine/components/CheckboxesField.d.ts +10 -1
  4. package/.server/server/plugins/engine/components/CheckboxesField.js +39 -0
  5. package/.server/server/plugins/engine/components/CheckboxesField.js.map +1 -1
  6. package/.server/server/plugins/engine/models/FormModel.d.ts +0 -2
  7. package/.server/server/plugins/engine/models/FormModel.js +1 -4
  8. package/.server/server/plugins/engine/models/FormModel.js.map +1 -1
  9. package/.server/server/plugins/engine/outputFormatters/adapter/v1.d.ts +0 -4
  10. package/.server/server/plugins/engine/outputFormatters/adapter/v1.js +1 -22
  11. package/.server/server/plugins/engine/outputFormatters/adapter/v1.js.map +1 -1
  12. package/.server/server/plugins/engine/pageControllers/validationOptions.js +4 -1
  13. package/.server/server/plugins/engine/pageControllers/validationOptions.js.map +1 -1
  14. package/.server/server/plugins/engine/types.d.ts +0 -1
  15. package/.server/server/plugins/engine/types.js.map +1 -1
  16. package/package.json +2 -2
  17. package/src/server/plugins/engine/beta/form-context.test.ts +0 -2
  18. package/src/server/plugins/engine/beta/form-context.ts +1 -8
  19. package/src/server/plugins/engine/components/CheckboxesField.test.ts +56 -1
  20. package/src/server/plugins/engine/components/CheckboxesField.ts +40 -0
  21. package/src/server/plugins/engine/models/FormModel.test.ts +0 -64
  22. package/src/server/plugins/engine/models/FormModel.ts +1 -5
  23. package/src/server/plugins/engine/outputFormatters/adapter/v1.test.ts +4 -356
  24. package/src/server/plugins/engine/outputFormatters/adapter/v1.ts +1 -31
  25. package/src/server/plugins/engine/pageControllers/validationOptions.ts +4 -1
  26. package/src/server/plugins/engine/types.ts +0 -1
@@ -1 +1 @@
1
- {"version":3,"file":"types.js","names":["FileStatus","UploadStatus"],"sources":["../../../../src/server/plugins/engine/types.ts"],"sourcesContent":["import {\n type ComponentDef,\n type Event,\n type FormVersionMetadata,\n type Item,\n type List,\n type Page,\n type PaymentFieldComponent,\n type UkAddressFieldComponent\n} from '@defra/forms-model'\nimport {\n type PluginProperties,\n type Request,\n type ResponseObject\n} from '@hapi/hapi'\nimport { type JoiExpression, type ValidationErrorItem } from 'joi'\n\nimport { FormComponent } from '~/src/server/plugins/engine/components/FormComponent.js'\nimport { type UkAddressState } from '~/src/server/plugins/engine/components/UkAddressField.js'\nimport { type Component } from '~/src/server/plugins/engine/components/helpers/components.js'\nimport {\n type FileUploadField,\n type PaymentField\n} from '~/src/server/plugins/engine/components/index.js'\nimport {\n type BackLink,\n type ComponentText,\n type ComponentViewModel,\n type DatePartsState,\n type EastingNorthingState,\n type LatLongState,\n type MonthYearState\n} from '~/src/server/plugins/engine/components/types.js'\nimport { type FormModel } from '~/src/server/plugins/engine/models/index.js'\nimport { type DetailItemField } from '~/src/server/plugins/engine/models/types.js'\nimport { type PageController } from '~/src/server/plugins/engine/pageControllers/PageController.js'\nimport { type PageControllerClass } from '~/src/server/plugins/engine/pageControllers/helpers/pages.js'\nimport { type QuestionPageController } from '~/src/server/plugins/engine/pageControllers/index.js'\nimport {\n type FileStatus,\n type FormAdapterSubmissionSchemaVersion,\n type UploadStatus\n} from '~/src/server/plugins/engine/types/enums.js'\nimport { type ViewContext } from '~/src/server/plugins/nunjucks/types.js'\nimport {\n type FormAction,\n type FormRequest,\n type FormRequestPayload,\n type FormResponseToolkit,\n type FormStatus\n} from '~/src/server/routes/types.js'\nimport { type CacheService } from '~/src/server/services/cacheService.js'\nimport { type RequestOptions } from '~/src/server/services/httpService.js'\nimport { type Services } from '~/src/server/types.js'\n\nexport type AnyFormRequest = FormRequest | FormRequestPayload\nexport type AnyRequest = Request | AnyFormRequest\n\n/**\n * Form submission state stores the following in Redis:\n * Props containing user's submitted values as `{ [inputId]: value }` or as `{ [sectionName]: { [inputName]: value } }`\n * a) . e.g:\n * ```ts\n * {\n * _C9PRHmsgt: 'Ben',\n * WfLk9McjzX: 'Music',\n * IK7jkUFCBL: 'Royal Academy of Music'\n * }\n * ```\n *\n * b)\n * ```ts\n * {\n * checkBeforeYouStart: { ukPassport: true },\n * applicantDetails: {\n * numberOfApplicants: 1,\n * phoneNumber: '77777777',\n * emailAddress: 'aaa@aaa.com'\n * },\n * applicantOneDetails: {\n * firstName: 'a',\n * middleName: 'a',\n * lastName: 'a',\n * address: { addressLine1: 'a', addressLine2: 'a', town: 'a', postcode: 'a' }\n * }\n * }\n * ```\n */\n\n/**\n * Form submission state\n */\nexport type FormSubmissionState = {\n upload?: Record<string, TempFileState>\n} & FormState\n\nexport interface FormSubmissionError extends Pick<\n ValidationErrorItem,\n 'context' | 'path'\n> {\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 FormConfirmationState {\n confirmed?: true\n formId?: string\n referenceNumber?: string\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 | GeospatialState\n | undefined\n\nexport type FormState = Partial<Record<string, FormStateValue>>\nexport type FormStateValue = Exclude<FormValue, undefined> | null\n\nexport interface FormValidationResult<\n ValueType extends FormPayload | FormSubmissionState\n> {\n value: ValueType\n errors: FormSubmissionError[] | undefined\n}\n\nexport interface FormContext {\n /**\n * Evaluation form state only (filtered by visited paths),\n * with values formatted for condition evaluation using\n * {@link FormComponent.getContextValueFromState}\n */\n evaluationState: FormState\n\n /**\n * Relevant form state only (filtered by visited paths)\n */\n relevantState: FormState\n\n /**\n * Relevant pages only (filtered by visited paths)\n */\n relevantPages: PageControllerClass[]\n\n /**\n * Form submission payload (single page)\n */\n payload: FormPayload\n\n /**\n * Form submission state (entire form)\n */\n state: FormSubmissionState\n\n /**\n * Validation errors (entire form)\n */\n errors?: FormSubmissionError[]\n\n /**\n * Visited paths evaluated from form state\n */\n paths: string[]\n\n /**\n * Preview URL direct access is allowed\n */\n isForceAccess: boolean\n\n /**\n * Miscellaneous extra data from event responses\n */\n data: object\n\n pageDefMap: Map<string, Page>\n listDefMap: Map<string, List>\n componentDefMap: Map<string, ComponentDef>\n pageMap: Map<string, PageControllerClass>\n componentMap: Map<string, Component>\n referenceNumber: string\n submittedVersionNumber?: number\n}\n\nexport type FormContextRequest = (\n | {\n method: 'get'\n payload?: undefined\n }\n | {\n method: 'post'\n payload: FormPayload\n }\n | {\n method: FormRequest['method']\n payload?: object | undefined\n }\n) &\n Pick<\n FormRequest,\n 'app' | 'method' | 'params' | 'path' | 'query' | 'url' | 'server'\n >\n\nexport interface UploadInitiateResponse {\n uploadId: string\n uploadUrl: string\n statusUrl: string\n}\n\nexport {\n FileStatus,\n UploadStatus\n} from '~/src/server/plugins/engine/types/enums.js'\n\nexport type UploadState = FileState[]\n\nexport type FileUpload = {\n fileId: string\n filename: string\n contentLength: number\n} & (\n | {\n fileStatus: FileStatus.complete | FileStatus.rejected | FileStatus.pending\n errorMessage?: string\n }\n | {\n fileStatus: FileStatus.complete\n errorMessage?: undefined\n }\n)\n\nexport interface FileUploadMetadata {\n retrievalKey: string\n}\n\nexport type UploadStatusResponse =\n | {\n uploadStatus: UploadStatus.initiated\n metadata: FileUploadMetadata\n form: { file?: undefined }\n }\n | {\n uploadStatus: UploadStatus.pending | UploadStatus.ready\n metadata: FileUploadMetadata\n form: { file: FileUpload }\n numberOfRejectedFiles?: number\n }\n | {\n uploadStatus: UploadStatus.ready\n metadata: FileUploadMetadata\n form: { file: FileUpload }\n numberOfRejectedFiles: 0\n }\n\nexport type UploadStatusFileResponse = Exclude<\n UploadStatusResponse,\n { uploadStatus: UploadStatus.initiated }\n>\n\nexport interface FileState {\n uploadId: string\n status: UploadStatusFileResponse\n}\n\nexport interface TempFileState {\n upload?: UploadInitiateResponse\n files: UploadState\n}\n\nexport interface RepeatItemState extends FormPayload {\n itemId: string\n}\n\nexport type RepeatListState = RepeatItemState[]\n\n/**\n * A longitude/latitude coordinate pair in WGS84 format\n * Format: [longitude, latitude]\n */\nexport type Coordinates = [longitude: number, latitude: number]\n\n/**\n * GeoJSON Point geometry\n */\nexport interface PointGeometry {\n type: 'Point'\n coordinates: Coordinates\n}\n\n/**\n * GeoJSON LineString geometry\n */\nexport interface LineStringGeometry {\n type: 'LineString'\n coordinates: Coordinates[]\n}\n\n/**\n * GeoJSON Polygon geometry\n */\nexport interface PolygonGeometry {\n type: 'Polygon'\n coordinates: Coordinates[][]\n}\n\n/**\n * Supported geometry types\n */\nexport type Geometry = PointGeometry | LineStringGeometry | PolygonGeometry\n\n/**\n * Feature metadata\n */\nexport interface FeatureProperties {\n /**\n * Human-readable description of the feature\n */\n description: string\n /**\n * The OS grid reference of the first coordinate of the feature\n */\n coordinateGridReference?: string\n /**\n * The OS grid reference of the centroid of the feature\n */\n centroidGridReference?: string\n}\n\n/**\n * A single GeoJSON Feature\n */\nexport interface Feature {\n id: string\n type: 'Feature'\n properties: FeatureProperties\n geometry: Geometry\n}\n\n/**\n * A GeoJSON FeatureCollection\n */\nexport type FeatureCollection = Feature[]\n\nexport type GeospatialState = FeatureCollection\n\nexport interface CheckAnswers {\n title?: ComponentText\n summaryList: SummaryList\n}\n\nexport interface SummaryList {\n classes?: string\n rows: SummaryListRow[]\n}\n\nexport interface SummaryListRow {\n key: ComponentText\n value: ComponentText\n actions?: { items: SummaryListAction[] }\n}\n\nexport type SummaryListAction = ComponentText & {\n href: string\n visuallyHiddenText: string\n}\n\nexport interface PageViewModelBase extends Partial<ViewContext> {\n page: PageController\n name?: string\n pageTitle: string\n sectionTitle?: string\n showTitle: boolean\n isStartPage: boolean\n backLink?: BackLink\n feedbackLink?: string\n serviceUrl: string\n phaseTag?: string\n}\n\nexport interface ItemDeletePageViewModel extends PageViewModelBase {\n context: FormContext\n itemTitle: string\n confirmation?: ComponentText\n buttonConfirm: ComponentText\n buttonCancel: ComponentText\n}\n\nexport interface FormPageViewModel extends PageViewModelBase {\n components: ComponentViewModel[]\n context: FormContext\n errors?: FormSubmissionError[]\n hasMissingNotificationEmail?: boolean\n allowSaveAndExit: boolean\n showSubmitButton?: boolean\n showPaymentExpiredNotification?: boolean\n}\n\nexport interface RepeaterSummaryPageViewModel extends PageViewModelBase {\n context: FormContext\n errors?: FormSubmissionError[]\n checkAnswers: CheckAnswers[]\n repeatTitle: string\n allowSaveAndExit: boolean\n}\n\nexport interface FeaturedFormPageViewModel extends FormPageViewModel {\n formAction?: string\n formComponent: ComponentViewModel\n componentsBefore: ComponentViewModel[]\n uploadId: string | undefined\n proxyUrl: string | null\n}\n\nexport type PageViewModel =\n | PageViewModelBase\n | ItemDeletePageViewModel\n | FormPageViewModel\n | RepeaterSummaryPageViewModel\n | FeaturedFormPageViewModel\n\nexport type GlobalFunction = (value: unknown) => unknown\nexport type FilterFunction = (value: unknown) => unknown\nexport interface ErrorMessageTemplate {\n type: string\n template: JoiExpression\n}\n\nexport interface ErrorMessageTemplateList {\n baseErrors: ErrorMessageTemplate[]\n advancedSettingsErrors: ErrorMessageTemplate[]\n}\n\nexport type PreparePageEventRequestOptions = (\n options: RequestOptions,\n event: Event,\n page: PageControllerClass,\n context: FormContext\n) => void\n\nexport type OnRequestCallback = (\n request: AnyFormRequest,\n h: FormResponseToolkit,\n context: FormContext\n) =>\n | ResponseObject\n | FormResponseToolkit['continue']\n | Promise<ResponseObject | FormResponseToolkit['continue']>\n\nexport type SaveAndExitHandler = (\n request: FormRequestPayload,\n h: FormResponseToolkit,\n context: FormContext\n) => ResponseObject\n\nexport interface ExternalArgs {\n component: ComponentDef\n controller: QuestionPageController\n sourceUrl: string\n actionArgs?: Record<string, string>\n isLive: boolean\n isPreview: boolean\n}\n\nexport interface PostcodeLookupExternalArgs extends ExternalArgs {\n component: UkAddressFieldComponent\n actionArgs: { step: string }\n}\n\nexport interface PaymentExternalArgs extends ExternalArgs {\n component: PaymentFieldComponent\n}\n\nexport interface ExternalStateAppendage {\n component: string\n data: FormStateValue | FormState\n}\n\nexport interface PluginOptions {\n model?: FormModel\n services?: Services\n controllers?: Record<string, typeof PageController>\n cache?: CacheService | string\n globals?: Record<string, GlobalFunction>\n filters?: Record<string, FilterFunction>\n saveAndExit?: SaveAndExitHandler\n pluginPath?: string\n nunjucks: {\n baseLayoutPath: string\n paths: string[]\n }\n viewContext: PluginProperties['forms-engine-plugin']['viewContext']\n preparePageEventRequestOptions?: PreparePageEventRequestOptions\n onRequest?: OnRequestCallback\n baseUrl: string // base URL of the application, protocol and hostname e.g. \"https://myapp.com\"\n ordnanceSurveyApiKey?: string\n ordnanceSurveyApiSecret?: string\n}\n\nexport interface FormAdapterSubmissionMessageMeta {\n schemaVersion: FormAdapterSubmissionSchemaVersion\n timestamp: Date\n referenceNumber: string\n formName: string\n formId: string\n formSlug: string\n status: FormStatus\n isPreview: boolean\n notificationEmail: string\n versionMetadata?: FormVersionMetadata\n custom?: Record<string, unknown>\n}\n\nexport type FormAdapterSubmissionMessageMetaSerialised = Omit<\n FormAdapterSubmissionMessageMeta,\n 'schemaVersion' | 'timestamp' | 'status'\n> & {\n schemaVersion: number\n status: string\n timestamp: string\n}\nexport interface FormAdapterFile {\n fileName: string\n fileId: string\n userDownloadLink: string\n}\n\nexport interface FormAdapterPayment {\n paymentId: string\n reference: string\n amount: number\n description: string\n createdAt: string\n}\n\nexport interface FormAdapterSubmissionMessageResult {\n files: {\n main: string\n repeaters: Record<string, string>\n }\n}\n\n/**\n * A detail item specifically for files\n */\nexport type FileUploadFieldDetailitem = Omit<DetailItemField, 'field'> & {\n field: FileUploadField\n}\n\n/**\n * A detail item specifically for payments\n */\nexport type PaymentFieldDetailItem = Omit<DetailItemField, 'field'> & {\n field: PaymentField\n}\nexport type RichFormValue =\n | FormValue\n | FormPayload\n | DatePartsState\n | MonthYearState\n | UkAddressState\n | EastingNorthingState\n | LatLongState\n | GeospatialState\n\nexport interface FormAdapterSubmissionMessageData {\n main: Record<string, RichFormValue | null>\n repeaters: Record<string, Record<string, RichFormValue>[]>\n files: Record<string, FormAdapterFile[]>\n payment?: FormAdapterPayment\n}\n\nexport interface FormAdapterSubmissionMessagePayload {\n meta: FormAdapterSubmissionMessageMeta\n data: FormAdapterSubmissionMessageData\n result: FormAdapterSubmissionMessageResult\n}\n\nexport interface FormAdapterSubmissionMessage extends FormAdapterSubmissionMessagePayload {\n messageId: string\n recordCreatedAt: Date\n}\n\nexport interface FormAdapterSubmissionService {\n handleFormSubmission: (\n submissionMessage: FormAdapterSubmissionMessage\n ) => unknown\n}\n"],"mappings":"AA0DA;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;;AA2BA;AACA;AACA;AACA;;AAuGA,SACEA,UAAU,EACVC,YAAY;;AAgEd;AACA;AACA;AACA;;AAGA;AACA;AACA;;AAMA;AACA;AACA;;AAMA;AACA;AACA;;AAMA;AACA;AACA;;AAGA;AACA;AACA;;AAgBA;AACA;AACA;;AAQA;AACA;AACA;;AAyMA;AACA;AACA;;AAKA;AACA;AACA","ignoreList":[]}
1
+ {"version":3,"file":"types.js","names":["FileStatus","UploadStatus"],"sources":["../../../../src/server/plugins/engine/types.ts"],"sourcesContent":["import {\n type ComponentDef,\n type Event,\n type FormVersionMetadata,\n type Item,\n type List,\n type Page,\n type PaymentFieldComponent,\n type UkAddressFieldComponent\n} from '@defra/forms-model'\nimport {\n type PluginProperties,\n type Request,\n type ResponseObject\n} from '@hapi/hapi'\nimport { type JoiExpression, type ValidationErrorItem } from 'joi'\n\nimport { FormComponent } from '~/src/server/plugins/engine/components/FormComponent.js'\nimport { type UkAddressState } from '~/src/server/plugins/engine/components/UkAddressField.js'\nimport { type Component } from '~/src/server/plugins/engine/components/helpers/components.js'\nimport {\n type FileUploadField,\n type PaymentField\n} from '~/src/server/plugins/engine/components/index.js'\nimport {\n type BackLink,\n type ComponentText,\n type ComponentViewModel,\n type DatePartsState,\n type EastingNorthingState,\n type LatLongState,\n type MonthYearState\n} from '~/src/server/plugins/engine/components/types.js'\nimport { type FormModel } from '~/src/server/plugins/engine/models/index.js'\nimport { type DetailItemField } from '~/src/server/plugins/engine/models/types.js'\nimport { type PageController } from '~/src/server/plugins/engine/pageControllers/PageController.js'\nimport { type PageControllerClass } from '~/src/server/plugins/engine/pageControllers/helpers/pages.js'\nimport { type QuestionPageController } from '~/src/server/plugins/engine/pageControllers/index.js'\nimport {\n type FileStatus,\n type FormAdapterSubmissionSchemaVersion,\n type UploadStatus\n} from '~/src/server/plugins/engine/types/enums.js'\nimport { type ViewContext } from '~/src/server/plugins/nunjucks/types.js'\nimport {\n type FormAction,\n type FormRequest,\n type FormRequestPayload,\n type FormResponseToolkit,\n type FormStatus\n} from '~/src/server/routes/types.js'\nimport { type CacheService } from '~/src/server/services/cacheService.js'\nimport { type RequestOptions } from '~/src/server/services/httpService.js'\nimport { type Services } from '~/src/server/types.js'\n\nexport type AnyFormRequest = FormRequest | FormRequestPayload\nexport type AnyRequest = Request | AnyFormRequest\n\n/**\n * Form submission state stores the following in Redis:\n * Props containing user's submitted values as `{ [inputId]: value }` or as `{ [sectionName]: { [inputName]: value } }`\n * a) . e.g:\n * ```ts\n * {\n * _C9PRHmsgt: 'Ben',\n * WfLk9McjzX: 'Music',\n * IK7jkUFCBL: 'Royal Academy of Music'\n * }\n * ```\n *\n * b)\n * ```ts\n * {\n * checkBeforeYouStart: { ukPassport: true },\n * applicantDetails: {\n * numberOfApplicants: 1,\n * phoneNumber: '77777777',\n * emailAddress: 'aaa@aaa.com'\n * },\n * applicantOneDetails: {\n * firstName: 'a',\n * middleName: 'a',\n * lastName: 'a',\n * address: { addressLine1: 'a', addressLine2: 'a', town: 'a', postcode: 'a' }\n * }\n * }\n * ```\n */\n\n/**\n * Form submission state\n */\nexport type FormSubmissionState = {\n upload?: Record<string, TempFileState>\n} & FormState\n\nexport interface FormSubmissionError extends Pick<\n ValidationErrorItem,\n 'context' | 'path'\n> {\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 FormConfirmationState {\n confirmed?: true\n formId?: string\n referenceNumber?: string\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 | GeospatialState\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 {\n FileStatus,\n UploadStatus\n} from '~/src/server/plugins/engine/types/enums.js'\n\nexport type UploadState = FileState[]\n\nexport type FileUpload = {\n fileId: string\n filename: string\n contentLength: number\n} & (\n | {\n fileStatus: FileStatus.complete | FileStatus.rejected | FileStatus.pending\n errorMessage?: string\n }\n | {\n fileStatus: FileStatus.complete\n errorMessage?: undefined\n }\n)\n\nexport interface FileUploadMetadata {\n retrievalKey: string\n}\n\nexport type UploadStatusResponse =\n | {\n uploadStatus: UploadStatus.initiated\n metadata: FileUploadMetadata\n form: { file?: undefined }\n }\n | {\n uploadStatus: UploadStatus.pending | UploadStatus.ready\n metadata: FileUploadMetadata\n form: { file: FileUpload }\n numberOfRejectedFiles?: number\n }\n | {\n uploadStatus: UploadStatus.ready\n metadata: FileUploadMetadata\n form: { file: FileUpload }\n numberOfRejectedFiles: 0\n }\n\nexport type UploadStatusFileResponse = Exclude<\n UploadStatusResponse,\n { uploadStatus: UploadStatus.initiated }\n>\n\nexport interface FileState {\n uploadId: string\n status: UploadStatusFileResponse\n}\n\nexport interface TempFileState {\n upload?: UploadInitiateResponse\n files: UploadState\n}\n\nexport interface RepeatItemState extends FormPayload {\n itemId: string\n}\n\nexport type RepeatListState = RepeatItemState[]\n\n/**\n * A longitude/latitude coordinate pair in WGS84 format\n * Format: [longitude, latitude]\n */\nexport type Coordinates = [longitude: number, latitude: number]\n\n/**\n * GeoJSON Point geometry\n */\nexport interface PointGeometry {\n type: 'Point'\n coordinates: Coordinates\n}\n\n/**\n * GeoJSON LineString geometry\n */\nexport interface LineStringGeometry {\n type: 'LineString'\n coordinates: Coordinates[]\n}\n\n/**\n * GeoJSON Polygon geometry\n */\nexport interface PolygonGeometry {\n type: 'Polygon'\n coordinates: Coordinates[][]\n}\n\n/**\n * Supported geometry types\n */\nexport type Geometry = PointGeometry | LineStringGeometry | PolygonGeometry\n\n/**\n * Feature metadata\n */\nexport interface FeatureProperties {\n /**\n * Human-readable description of the feature\n */\n description: string\n /**\n * The OS grid reference of the first coordinate of the feature\n */\n coordinateGridReference?: string\n /**\n * The OS grid reference of the centroid of the feature\n */\n centroidGridReference?: string\n}\n\n/**\n * A single GeoJSON Feature\n */\nexport interface Feature {\n id: string\n type: 'Feature'\n properties: FeatureProperties\n geometry: Geometry\n}\n\n/**\n * A GeoJSON FeatureCollection\n */\nexport type FeatureCollection = Feature[]\n\nexport type GeospatialState = FeatureCollection\n\nexport interface CheckAnswers {\n title?: ComponentText\n summaryList: SummaryList\n}\n\nexport interface SummaryList {\n classes?: string\n rows: SummaryListRow[]\n}\n\nexport interface SummaryListRow {\n key: ComponentText\n value: ComponentText\n actions?: { items: SummaryListAction[] }\n}\n\nexport type SummaryListAction = ComponentText & {\n href: string\n visuallyHiddenText: string\n}\n\nexport interface PageViewModelBase extends Partial<ViewContext> {\n page: PageController\n name?: string\n pageTitle: string\n sectionTitle?: string\n showTitle: boolean\n isStartPage: boolean\n backLink?: BackLink\n feedbackLink?: string\n serviceUrl: string\n phaseTag?: string\n}\n\nexport interface ItemDeletePageViewModel extends PageViewModelBase {\n context: FormContext\n itemTitle: string\n confirmation?: ComponentText\n buttonConfirm: ComponentText\n buttonCancel: ComponentText\n}\n\nexport interface FormPageViewModel extends PageViewModelBase {\n components: ComponentViewModel[]\n context: FormContext\n errors?: FormSubmissionError[]\n hasMissingNotificationEmail?: boolean\n allowSaveAndExit: boolean\n showSubmitButton?: boolean\n showPaymentExpiredNotification?: boolean\n}\n\nexport interface RepeaterSummaryPageViewModel extends PageViewModelBase {\n context: FormContext\n errors?: FormSubmissionError[]\n checkAnswers: CheckAnswers[]\n repeatTitle: string\n allowSaveAndExit: boolean\n}\n\nexport interface FeaturedFormPageViewModel extends FormPageViewModel {\n formAction?: string\n formComponent: ComponentViewModel\n componentsBefore: ComponentViewModel[]\n uploadId: string | undefined\n proxyUrl: string | null\n}\n\nexport type PageViewModel =\n | PageViewModelBase\n | ItemDeletePageViewModel\n | FormPageViewModel\n | RepeaterSummaryPageViewModel\n | FeaturedFormPageViewModel\n\nexport type GlobalFunction = (value: unknown) => unknown\nexport type FilterFunction = (value: unknown) => unknown\nexport interface ErrorMessageTemplate {\n type: string\n template: JoiExpression\n}\n\nexport interface ErrorMessageTemplateList {\n baseErrors: ErrorMessageTemplate[]\n advancedSettingsErrors: ErrorMessageTemplate[]\n}\n\nexport type PreparePageEventRequestOptions = (\n options: RequestOptions,\n event: Event,\n page: PageControllerClass,\n context: FormContext\n) => void\n\nexport type OnRequestCallback = (\n request: AnyFormRequest,\n h: FormResponseToolkit,\n context: FormContext\n) =>\n | ResponseObject\n | FormResponseToolkit['continue']\n | Promise<ResponseObject | FormResponseToolkit['continue']>\n\nexport type SaveAndExitHandler = (\n request: FormRequestPayload,\n h: FormResponseToolkit,\n context: FormContext\n) => ResponseObject\n\nexport interface ExternalArgs {\n component: ComponentDef\n controller: QuestionPageController\n sourceUrl: string\n actionArgs?: Record<string, string>\n isLive: boolean\n isPreview: boolean\n}\n\nexport interface PostcodeLookupExternalArgs extends ExternalArgs {\n component: UkAddressFieldComponent\n actionArgs: { step: string }\n}\n\nexport interface PaymentExternalArgs extends ExternalArgs {\n component: PaymentFieldComponent\n}\n\nexport interface ExternalStateAppendage {\n component: string\n data: FormStateValue | FormState\n}\n\nexport interface PluginOptions {\n model?: FormModel\n services?: Services\n controllers?: Record<string, typeof PageController>\n cache?: CacheService | string\n globals?: Record<string, GlobalFunction>\n filters?: Record<string, FilterFunction>\n saveAndExit?: SaveAndExitHandler\n pluginPath?: string\n nunjucks: {\n baseLayoutPath: string\n paths: string[]\n }\n viewContext: PluginProperties['forms-engine-plugin']['viewContext']\n preparePageEventRequestOptions?: PreparePageEventRequestOptions\n onRequest?: OnRequestCallback\n baseUrl: string // base URL of the application, protocol and hostname e.g. \"https://myapp.com\"\n ordnanceSurveyApiKey?: string\n ordnanceSurveyApiSecret?: string\n}\n\nexport interface FormAdapterSubmissionMessageMeta {\n schemaVersion: FormAdapterSubmissionSchemaVersion\n timestamp: Date\n referenceNumber: string\n formName: string\n formId: string\n formSlug: string\n status: FormStatus\n isPreview: boolean\n notificationEmail: string\n versionMetadata?: FormVersionMetadata\n custom?: Record<string, unknown>\n}\n\nexport type FormAdapterSubmissionMessageMetaSerialised = Omit<\n FormAdapterSubmissionMessageMeta,\n 'schemaVersion' | 'timestamp' | 'status'\n> & {\n schemaVersion: number\n status: string\n timestamp: string\n}\nexport interface FormAdapterFile {\n fileName: string\n fileId: string\n userDownloadLink: string\n}\n\nexport interface FormAdapterPayment {\n paymentId: string\n reference: string\n amount: number\n description: string\n createdAt: string\n}\n\nexport interface FormAdapterSubmissionMessageResult {\n files: {\n main: string\n repeaters: Record<string, string>\n }\n}\n\n/**\n * A detail item specifically for files\n */\nexport type FileUploadFieldDetailitem = Omit<DetailItemField, 'field'> & {\n field: FileUploadField\n}\n\n/**\n * A detail item specifically for payments\n */\nexport type PaymentFieldDetailItem = Omit<DetailItemField, 'field'> & {\n field: PaymentField\n}\nexport type RichFormValue =\n | FormValue\n | FormPayload\n | DatePartsState\n | MonthYearState\n | UkAddressState\n | EastingNorthingState\n | LatLongState\n | GeospatialState\n\nexport interface FormAdapterSubmissionMessageData {\n main: Record<string, RichFormValue | null>\n repeaters: Record<string, Record<string, RichFormValue>[]>\n files: Record<string, FormAdapterFile[]>\n payment?: FormAdapterPayment\n}\n\nexport interface FormAdapterSubmissionMessagePayload {\n meta: FormAdapterSubmissionMessageMeta\n data: FormAdapterSubmissionMessageData\n result: FormAdapterSubmissionMessageResult\n}\n\nexport interface FormAdapterSubmissionMessage extends FormAdapterSubmissionMessagePayload {\n messageId: string\n recordCreatedAt: Date\n}\n\nexport interface FormAdapterSubmissionService {\n handleFormSubmission: (\n submissionMessage: FormAdapterSubmissionMessage\n ) => unknown\n}\n"],"mappings":"AA0DA;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;;AA2BA;AACA;AACA;AACA;;AAsGA,SACEA,UAAU,EACVC,YAAY;;AAgEd;AACA;AACA;AACA;;AAGA;AACA;AACA;;AAMA;AACA;AACA;;AAMA;AACA;AACA;;AAMA;AACA;AACA;;AAGA;AACA;AACA;;AAgBA;AACA;AACA;;AAQA;AACA;AACA;;AAyMA;AACA;AACA;;AAKA;AACA;AACA","ignoreList":[]}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@defra/forms-engine-plugin",
3
- "version": "4.6.0",
3
+ "version": "4.7.0",
4
4
  "description": "Defra forms engine",
5
5
  "type": "module",
6
6
  "files": [
@@ -83,7 +83,7 @@
83
83
  },
84
84
  "license": "SEE LICENSE IN LICENSE",
85
85
  "dependencies": {
86
- "@defra/forms-model": "^3.0.644",
86
+ "@defra/forms-model": "^3.0.647",
87
87
  "@defra/hapi-tracing": "^1.29.0",
88
88
  "@defra/interactive-map": "^0.0.17-alpha",
89
89
  "@elastic/ecs-pino-format": "^1.5.0",
@@ -183,7 +183,6 @@ describe('getFormModel helper', () => {
183
183
  definition,
184
184
  {
185
185
  basePath: slug,
186
- versionNumber: 17,
187
186
  ordnanceSurveyApiKey: undefined,
188
187
  formId: metadata.id
189
188
  },
@@ -288,7 +287,6 @@ describe('resolveFormModel helper', () => {
288
287
  definition,
289
288
  expect.objectContaining({
290
289
  basePath: 'forms/preview/live/tb-origin',
291
- versionNumber: 9,
292
290
  ordnanceSurveyApiKey: 'os-api-key',
293
291
  formId: metadata.id
294
292
  }),
@@ -5,8 +5,7 @@ import { isEqual } from 'date-fns'
5
5
  import { PREVIEW_PATH_PREFIX } from '~/src/server/constants.js'
6
6
  import {
7
7
  checkEmailAddressForLiveFormSubmission,
8
- getCacheService,
9
- getFormVersion
8
+ getCacheService
10
9
  } from '~/src/server/plugins/engine/helpers.js'
11
10
  import { FormModel } from '~/src/server/plugins/engine/models/index.js'
12
11
  import { type PageController } from '~/src/server/plugins/engine/pageControllers/PageController.js'
@@ -65,15 +64,12 @@ export async function getFormModel(
65
64
  )
66
65
  }
67
66
 
68
- const versionNumber = getFormVersion(definition)?.versionNumber
69
-
70
67
  return new FormModel(
71
68
  definition,
72
69
  {
73
70
  basePath:
74
71
  options.basePath ??
75
72
  buildBasePath(options.routePrefix ?? '', slug, formState, isPreview),
76
- versionNumber,
77
73
  ordnanceSurveyApiKey: options.ordnanceSurveyApiKey,
78
74
  formId: options.formId ?? metadata.id
79
75
  },
@@ -182,15 +178,12 @@ export async function resolveFormModel(
182
178
  const routePrefix =
183
179
  options.routePrefix ?? server.realm.modifiers.route.prefix
184
180
 
185
- const versionNumber = getFormVersion(definition)?.versionNumber
186
-
187
181
  const model = new FormModel(
188
182
  definition,
189
183
  {
190
184
  basePath:
191
185
  options.basePath ??
192
186
  buildBasePath(routePrefix, slug, formState, isPreview),
193
- versionNumber,
194
187
  ordnanceSurveyApiKey: options.ordnanceSurveyApiKey,
195
188
  formId: options.formId ?? metadata.id
196
189
  },
@@ -173,6 +173,61 @@ describe.each([
173
173
  )
174
174
  })
175
175
 
176
+ it('is configured with min/max items', () => {
177
+ const collectionLimited = new ComponentCollection(
178
+ [{ ...def, schema: { min: 2, max: 4 } }],
179
+ { model }
180
+ )
181
+ const { formSchema } = collectionLimited
182
+ const { keys } = formSchema.describe()
183
+
184
+ expect(keys).toHaveProperty(
185
+ 'myComponent',
186
+ expect.objectContaining({
187
+ items: [
188
+ {
189
+ allow: options.allow,
190
+ flags: {
191
+ label: def.shortDescription,
192
+ only: true
193
+ },
194
+ type: options.list.type
195
+ }
196
+ ],
197
+ rules: [
198
+ { args: { limit: 2 }, name: 'min' },
199
+ { args: { limit: 4 }, name: 'max' }
200
+ ]
201
+ })
202
+ )
203
+ })
204
+
205
+ it('is configured with length items', () => {
206
+ const collectionLimited = new ComponentCollection(
207
+ [{ ...def, schema: { length: 3 } }],
208
+ { model }
209
+ )
210
+ const { formSchema } = collectionLimited
211
+ const { keys } = formSchema.describe()
212
+
213
+ expect(keys).toHaveProperty(
214
+ 'myComponent',
215
+ expect.objectContaining({
216
+ items: [
217
+ {
218
+ allow: options.allow,
219
+ flags: {
220
+ label: def.shortDescription,
221
+ only: true
222
+ },
223
+ type: options.list.type
224
+ }
225
+ ],
226
+ rules: [{ args: { limit: 3 }, name: 'length' }]
227
+ })
228
+ )
229
+ })
230
+
176
231
  it('adds errors for empty value', () => {
177
232
  const result = collection.validate(getFormData())
178
233
 
@@ -386,7 +441,7 @@ describe.each([
386
441
  it('should return errors', () => {
387
442
  const errors = field.getAllPossibleErrors()
388
443
  expect(errors.baseErrors).not.toBeEmpty()
389
- expect(errors.advancedSettingsErrors).toBeEmpty()
444
+ expect(errors.advancedSettingsErrors).not.toBeEmpty()
390
445
  })
391
446
  })
392
447
 
@@ -5,7 +5,9 @@ import { isFormValue } from '~/src/server/plugins/engine/components/FormComponen
5
5
  import { SelectionControlField } from '~/src/server/plugins/engine/components/SelectionControlField.js'
6
6
  import { type FormModel } from '~/src/server/plugins/engine/models/FormModel.js'
7
7
  import { type QuestionPageController } from '~/src/server/plugins/engine/pageControllers/QuestionPageController.js'
8
+ import { messageTemplate } from '~/src/server/plugins/engine/pageControllers/validationOptions.js'
8
9
  import {
10
+ type ErrorMessageTemplateList,
9
11
  type FormState,
10
12
  type FormStateValue,
11
13
  type FormSubmissionState
@@ -13,6 +15,7 @@ import {
13
15
 
14
16
  export class CheckboxesField extends SelectionControlField {
15
17
  declare options: CheckboxesFieldComponent['options']
18
+ declare schema: CheckboxesFieldComponent['schema']
16
19
  declare formSchema: ArraySchema<string> | ArraySchema<number>
17
20
  declare stateSchema: ArraySchema<string> | ArraySchema<number>
18
21
 
@@ -24,6 +27,7 @@ export class CheckboxesField extends SelectionControlField {
24
27
 
25
28
  const { listType: type } = this
26
29
  const { options } = def
30
+ const schema = 'schema' in def ? def.schema : {}
27
31
 
28
32
  let formSchema =
29
33
  type === 'string' ? joi.array<string>() : joi.array<number>()
@@ -42,6 +46,18 @@ export class CheckboxesField extends SelectionControlField {
42
46
  formSchema = formSchema.optional()
43
47
  }
44
48
 
49
+ if (typeof schema?.length === 'number') {
50
+ formSchema = formSchema.length(schema.length)
51
+ } else {
52
+ if (typeof schema?.min === 'number') {
53
+ formSchema = formSchema.min(schema.min)
54
+ }
55
+
56
+ if (typeof schema?.max === 'number') {
57
+ formSchema = formSchema.max(schema.max)
58
+ }
59
+ }
60
+
45
61
  this.formSchema = formSchema.default([])
46
62
  this.stateSchema = formSchema.default(null).allow(null)
47
63
  this.options = options
@@ -112,6 +128,30 @@ export class CheckboxesField extends SelectionControlField {
112
128
  return this.getContextValueFromFormValue(values)
113
129
  }
114
130
 
131
+ /**
132
+ * For error preview page that shows all possible errors on a component
133
+ */
134
+ getAllPossibleErrors(): ErrorMessageTemplateList {
135
+ return CheckboxesField.getAllPossibleErrors()
136
+ }
137
+
138
+ /**
139
+ * Static version of getAllPossibleErrors that doesn't require a component instance.
140
+ */
141
+ static getAllPossibleErrors(): ErrorMessageTemplateList {
142
+ const parentErrors = SelectionControlField.getAllPossibleErrors()
143
+
144
+ return {
145
+ ...parentErrors,
146
+ advancedSettingsErrors: [
147
+ ...parentErrors.advancedSettingsErrors,
148
+ { type: 'array.min', template: messageTemplate.arrayMin },
149
+ { type: 'array.max', template: messageTemplate.arrayMax },
150
+ { type: 'array.length', template: messageTemplate.arrayLength }
151
+ ]
152
+ }
153
+ }
154
+
115
155
  isValue(value?: FormStateValue | FormState): value is Item['value'][] {
116
156
  if (!Array.isArray(value)) {
117
157
  return false
@@ -141,21 +141,6 @@ describe('FormModel', () => {
141
141
  expect(model.schemaVersion).toBe(SchemaVersion.V1)
142
142
  })
143
143
 
144
- it('sets versionNumber from options', () => {
145
- const model = new FormModel(definition, {
146
- basePath: 'test',
147
- versionNumber: 42
148
- })
149
-
150
- expect(model.versionNumber).toBe(42)
151
- })
152
-
153
- it('sets versionNumber to undefined when not provided', () => {
154
- const model = new FormModel(definition, { basePath: 'test' })
155
-
156
- expect(model.versionNumber).toBeUndefined()
157
- })
158
-
159
144
  it.each([
160
145
  {
161
146
  input: undefined,
@@ -344,55 +329,6 @@ describe('FormModel', () => {
344
329
  )
345
330
  })
346
331
 
347
- it('includes submittedVersionNumber in context when versionNumber is set', () => {
348
- const formModel = new FormModel(fieldsRequiredDefinition, {
349
- basePath: '/components',
350
- versionNumber: 123
351
- })
352
-
353
- const state = {
354
- $$__referenceNumber: 'foobar'
355
- }
356
- const pageUrl = new URL('http://example.com/components/fields-required')
357
-
358
- const request: FormContextRequest = buildFormContextRequest({
359
- method: 'get',
360
- query: {},
361
- path: pageUrl.pathname,
362
- params: { path: 'components', slug: 'fields-required' },
363
- url: pageUrl,
364
- app: { model: formModel }
365
- })
366
-
367
- const context = formModel.getFormContext(request, state)
368
-
369
- expect(context.submittedVersionNumber).toBe(123)
370
- })
371
-
372
- it('sets submittedVersionNumber to undefined when versionNumber is not set', () => {
373
- const formModel = new FormModel(fieldsRequiredDefinition, {
374
- basePath: '/components'
375
- })
376
-
377
- const state = {
378
- $$__referenceNumber: 'foobar'
379
- }
380
- const pageUrl = new URL('http://example.com/components/fields-required')
381
-
382
- const request: FormContextRequest = buildFormContextRequest({
383
- method: 'get',
384
- query: {},
385
- path: pageUrl.pathname,
386
- params: { path: 'components', slug: 'fields-required' },
387
- url: pageUrl,
388
- app: { model: formModel }
389
- })
390
-
391
- const context = formModel.getFormContext(request, state)
392
-
393
- expect(context.submittedVersionNumber).toBeUndefined()
394
- })
395
-
396
332
  it('redirects to the page if the list field (radio) is invalidated due to list item conditions', () => {
397
333
  const formModel = new FormModel(conditionsListDefinition, {
398
334
  basePath: '/conditional-list-items'
@@ -78,7 +78,6 @@ export class FormModel {
78
78
  formId: string
79
79
  values: FormDefinition
80
80
  basePath: string
81
- versionNumber?: number
82
81
  ordnanceSurveyApiKey?: string
83
82
  conditions: Partial<Record<string, ExecutableCondition>>
84
83
  pages: PageControllerClass[]
@@ -100,7 +99,6 @@ export class FormModel {
100
99
  def: typeof this.def,
101
100
  options: {
102
101
  basePath: string
103
- versionNumber?: number
104
102
  ordnanceSurveyApiKey?: string
105
103
  formId?: string
106
104
  },
@@ -158,7 +156,6 @@ export class FormModel {
158
156
  this.formId = options.formId ?? ''
159
157
  this.values = result.value
160
158
  this.basePath = options.basePath
161
- this.versionNumber = options.versionNumber
162
159
  this.ordnanceSurveyApiKey = options.ordnanceSurveyApiKey
163
160
  this.conditions = {}
164
161
  this.services = services
@@ -362,8 +359,7 @@ export class FormModel {
362
359
  componentDefMap: this.componentDefMap,
363
360
  pageMap: this.pageMap,
364
361
  componentMap: this.componentMap,
365
- referenceNumber: getReferenceNumber(state),
366
- submittedVersionNumber: this.versionNumber
362
+ referenceNumber: getReferenceNumber(state)
367
363
  }
368
364
 
369
365
  // Validate current page