@defra/forms-engine-plugin 4.3.0 → 4.5.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 (97) hide show
  1. package/.public/javascripts/application.min.js +1 -1
  2. package/.public/javascripts/application.min.js.map +1 -1
  3. package/.public/javascripts/shared.min.js +1 -1
  4. package/.public/javascripts/shared.min.js.map +1 -1
  5. package/.public/javascripts/vendor/accessible-autocomplete.min.js.map +1 -1
  6. package/.public/stylesheets/application.min.css +1 -1
  7. package/.public/stylesheets/application.min.css.map +1 -1
  8. package/.server/client/javascripts/file-upload.js +13 -8
  9. package/.server/client/javascripts/file-upload.js.map +1 -1
  10. package/.server/client/javascripts/geospatial-map.d.ts +189 -0
  11. package/.server/client/javascripts/geospatial-map.js +1068 -0
  12. package/.server/client/javascripts/geospatial-map.js.map +1 -0
  13. package/.server/client/javascripts/location-map.d.ts +6 -91
  14. package/.server/client/javascripts/location-map.js +78 -385
  15. package/.server/client/javascripts/location-map.js.map +1 -1
  16. package/.server/client/javascripts/map.d.ts +199 -0
  17. package/.server/client/javascripts/map.js +384 -0
  18. package/.server/client/javascripts/map.js.map +1 -0
  19. package/.server/client/javascripts/shared.d.ts +3 -1
  20. package/.server/client/javascripts/shared.js +3 -1
  21. package/.server/client/javascripts/shared.js.map +1 -1
  22. package/.server/client/stylesheets/shared.scss +7 -0
  23. package/.server/server/plugins/engine/components/ComponentBase.d.ts +1 -0
  24. package/.server/server/plugins/engine/components/ComponentBase.js +2 -0
  25. package/.server/server/plugins/engine/components/ComponentBase.js.map +1 -1
  26. package/.server/server/plugins/engine/components/FileUploadField.d.ts +3 -2
  27. package/.server/server/plugins/engine/components/FileUploadField.js +11 -3
  28. package/.server/server/plugins/engine/components/FileUploadField.js.map +1 -1
  29. package/.server/server/plugins/engine/components/FormComponent.d.ts +9 -1
  30. package/.server/server/plugins/engine/components/FormComponent.js +22 -0
  31. package/.server/server/plugins/engine/components/FormComponent.js.map +1 -1
  32. package/.server/server/plugins/engine/components/GeospatialField.d.ts +77 -0
  33. package/.server/server/plugins/engine/components/GeospatialField.js +102 -0
  34. package/.server/server/plugins/engine/components/GeospatialField.js.map +1 -0
  35. package/.server/server/plugins/engine/components/helpers/__stubs__/geospatial.d.ts +3 -0
  36. package/.server/server/plugins/engine/components/helpers/__stubs__/geospatial.js +63 -0
  37. package/.server/server/plugins/engine/components/helpers/__stubs__/geospatial.js.map +1 -0
  38. package/.server/server/plugins/engine/components/helpers/components.d.ts +1 -1
  39. package/.server/server/plugins/engine/components/helpers/components.js +7 -0
  40. package/.server/server/plugins/engine/components/helpers/components.js.map +1 -1
  41. package/.server/server/plugins/engine/components/helpers/geospatial.d.ts +6 -0
  42. package/.server/server/plugins/engine/components/helpers/geospatial.js +71 -0
  43. package/.server/server/plugins/engine/components/helpers/geospatial.js.map +1 -0
  44. package/.server/server/plugins/engine/components/helpers/geospatial.test.js +42 -0
  45. package/.server/server/plugins/engine/components/helpers/geospatial.test.js.map +1 -0
  46. package/.server/server/plugins/engine/components/index.d.ts +1 -0
  47. package/.server/server/plugins/engine/components/index.js +1 -0
  48. package/.server/server/plugins/engine/components/index.js.map +1 -1
  49. package/.server/server/plugins/engine/pageControllers/FileUploadPageController.d.ts +11 -0
  50. package/.server/server/plugins/engine/pageControllers/FileUploadPageController.js +65 -28
  51. package/.server/server/plugins/engine/pageControllers/FileUploadPageController.js.map +1 -1
  52. package/.server/server/plugins/engine/pageControllers/PageController.d.ts +1 -0
  53. package/.server/server/plugins/engine/pageControllers/PageController.js +2 -0
  54. package/.server/server/plugins/engine/pageControllers/PageController.js.map +1 -1
  55. package/.server/server/plugins/engine/pageControllers/helpers/submission.js +13 -1
  56. package/.server/server/plugins/engine/pageControllers/helpers/submission.js.map +1 -1
  57. package/.server/server/plugins/engine/pageControllers/validationOptions.js +2 -1
  58. package/.server/server/plugins/engine/pageControllers/validationOptions.js.map +1 -1
  59. package/.server/server/plugins/engine/types.d.ts +63 -2
  60. package/.server/server/plugins/engine/types.js +33 -0
  61. package/.server/server/plugins/engine/types.js.map +1 -1
  62. package/.server/server/plugins/engine/views/components/geospatialfield.html +7 -0
  63. package/.server/server/plugins/nunjucks/context.test.js.map +1 -1
  64. package/.server/server/plugins/nunjucks/filters/field.d.ts +1 -1
  65. package/.server/server/routes/types.js.map +1 -1
  66. package/.server/server/services/cacheService.js +3 -0
  67. package/.server/server/services/cacheService.js.map +1 -1
  68. package/package.json +9 -5
  69. package/src/client/javascripts/file-upload.js +12 -8
  70. package/src/client/javascripts/geospatial-map.js +1023 -0
  71. package/src/client/javascripts/location-map.js +94 -390
  72. package/src/client/javascripts/map.js +389 -0
  73. package/src/client/javascripts/shared.js +3 -1
  74. package/src/client/stylesheets/shared.scss +7 -0
  75. package/src/server/plugins/engine/components/ComponentBase.ts +2 -0
  76. package/src/server/plugins/engine/components/FileUploadField.test.ts +11 -8
  77. package/src/server/plugins/engine/components/FileUploadField.ts +14 -5
  78. package/src/server/plugins/engine/components/FormComponent.ts +29 -0
  79. package/src/server/plugins/engine/components/GeospatialField.test.ts +380 -0
  80. package/src/server/plugins/engine/components/GeospatialField.ts +145 -0
  81. package/src/server/plugins/engine/components/helpers/__stubs__/geospatial.ts +85 -0
  82. package/src/server/plugins/engine/components/helpers/components.test.ts +44 -0
  83. package/src/server/plugins/engine/components/helpers/components.ts +10 -0
  84. package/src/server/plugins/engine/components/helpers/geospatial.test.js +55 -0
  85. package/src/server/plugins/engine/components/helpers/geospatial.ts +93 -0
  86. package/src/server/plugins/engine/components/index.ts +1 -0
  87. package/src/server/plugins/engine/pageControllers/FileUploadPageController.test.ts +109 -5
  88. package/src/server/plugins/engine/pageControllers/FileUploadPageController.ts +69 -21
  89. package/src/server/plugins/engine/pageControllers/PageController.ts +2 -0
  90. package/src/server/plugins/engine/pageControllers/helpers/submission.test.ts +74 -0
  91. package/src/server/plugins/engine/pageControllers/helpers/submission.ts +17 -1
  92. package/src/server/plugins/engine/pageControllers/validationOptions.ts +3 -1
  93. package/src/server/plugins/engine/types.ts +77 -4
  94. package/src/server/plugins/engine/views/components/geospatialfield.html +7 -0
  95. package/src/server/plugins/nunjucks/context.test.js +2 -3
  96. package/src/server/routes/types.ts +4 -2
  97. package/src/server/services/cacheService.ts +2 -0
@@ -1 +1 @@
1
- {"version":3,"file":"FileUploadPageController.js","names":["ComponentType","Boom","wait","StatusCodes","FileUploadField","tempItemSchema","getCacheService","getError","getExponentialBackoffDelay","QuestionPageController","getProxyUrlForLocalDevelopment","getUploadStatus","initiateUpload","FileStatus","UploadStatus","MAX_UPLOADS","CDP_UPLOAD_TIMEOUT_MS","prepareStatus","status","file","form","isPending","fileStatus","pending","errorMessage","prepareFileState","fileState","FileUploadPageController","fileUpload","fileDeleteViewName","constructor","model","pageDef","collection","fileUploads","fields","filter","field","type","at","length","badImplementation","path","indexOf","name","viewName","getStateKeys","component","pagePath","page","getFormDataFromState","request","state","payload","files","getFilesFromState","undefined","getState","refreshUpload","uploadState","upload","getUploadFromState","makeGetItemDeleteRouteHandler","context","h","viewModel","params","fileToRemove","find","uploadId","itemId","notFound","filename","view","backLink","getBackLink","pageTitle","itemTitle","confirmation","text","buttonConfirm","buttonCancel","makePostItemDeleteRouteHandler","confirm","getFormParams","checkRemovedFiles","proceed","getErrors","details","errors","forEach","error","isUploadError","isUploadRootError","push","value","href","getViewModel","components","formComponent","id","index","proxyUrl","uploadUrl","formAction","componentsBefore","slice","checkUploadStatus","depth","initiateAndStoreNewUpload","statusResponse","err","isBoom","output","statusCode","NOT_FOUND","valueOf","badRequest","uploadStatus","initiated","Error","logger","gatewayTimeout","toFixed","delay","info","validationResult","validate","stripUnknown","complete","unshift","mergeState","cacheService","server","setFlash","filesUpdated","options","schema","getFormMetadata","services","formsService","max","Math","min","formMetadata","slug","notificationEmail","newUpload","accept"],"sources":["../../../../../src/server/plugins/engine/pageControllers/FileUploadPageController.ts"],"sourcesContent":["import { ComponentType, type PageFileUpload } from '@defra/forms-model'\nimport Boom from '@hapi/boom'\nimport { wait } from '@hapi/hoek'\nimport { StatusCodes } from 'http-status-codes'\nimport { type ValidationErrorItem } from 'joi'\n\nimport {\n FileUploadField,\n tempItemSchema\n} from '~/src/server/plugins/engine/components/FileUploadField.js'\nimport { type FormComponent } from '~/src/server/plugins/engine/components/FormComponent.js'\nimport {\n getCacheService,\n getError,\n getExponentialBackoffDelay\n} from '~/src/server/plugins/engine/helpers.js'\nimport { type FormModel } from '~/src/server/plugins/engine/models/index.js'\nimport { QuestionPageController } from '~/src/server/plugins/engine/pageControllers/QuestionPageController.js'\nimport { getProxyUrlForLocalDevelopment } from '~/src/server/plugins/engine/pageControllers/helpers/index.js'\nimport {\n getUploadStatus,\n initiateUpload\n} from '~/src/server/plugins/engine/services/uploadService.js'\nimport {\n FileStatus,\n UploadStatus,\n type AnyFormRequest,\n type FeaturedFormPageViewModel,\n type FileState,\n type FormContext,\n type FormContextRequest,\n type FormSubmissionError,\n type FormSubmissionState,\n type ItemDeletePageViewModel,\n type UploadInitiateResponse,\n type UploadStatusFileResponse\n} from '~/src/server/plugins/engine/types.js'\nimport {\n type FormRequest,\n type FormRequestPayload,\n type FormResponseToolkit\n} from '~/src/server/routes/types.js'\n\nconst MAX_UPLOADS = 25\nconst CDP_UPLOAD_TIMEOUT_MS = 60000 // 1 minute\n\nexport function prepareStatus(status: UploadStatusFileResponse) {\n const file = status.form.file\n const isPending = file.fileStatus === FileStatus.pending\n\n if (!file.errorMessage && isPending) {\n file.errorMessage = 'The selected file has not fully uploaded'\n }\n\n return status\n}\n\nfunction prepareFileState(fileState: FileState) {\n prepareStatus(fileState.status)\n\n return fileState\n}\n\nexport class FileUploadPageController extends QuestionPageController {\n declare pageDef: PageFileUpload\n\n fileUpload: FileUploadField\n fileDeleteViewName = 'item-delete'\n\n constructor(model: FormModel, pageDef: PageFileUpload) {\n super(model, pageDef)\n\n const { collection } = this\n\n // Get the file upload fields from the collection\n const fileUploads = collection.fields.filter(\n (field): field is FileUploadField =>\n field.type === ComponentType.FileUploadField\n )\n\n const fileUpload = fileUploads.at(0)\n\n // Assert we have exactly 1 file upload component\n if (!fileUpload || fileUploads.length > 1) {\n throw Boom.badImplementation(\n `Expected 1 FileUploadFieldComponent in FileUploadPageController '${pageDef.path}'`\n )\n }\n\n // Assert the file upload component is the first form component\n if (collection.fields.indexOf(fileUpload) !== 0) {\n throw Boom.badImplementation(\n `Expected '${fileUpload.name}' to be the first form component in FileUploadPageController '${pageDef.path}'`\n )\n }\n\n // Assign the file upload component to the controller\n this.fileUpload = fileUpload\n this.viewName = 'file-upload'\n }\n\n /**\n * Get supplementary state keys for clearing file upload state.\n * Returns the nested upload path for FileUploadField components only.\n * @param component - The component to get supplementary state keys for\n * @returns Array containing the nested upload path, e.g., [\"upload['/page-path']\"]\n * or ['upload'] if no page path is available. Returns empty array for non-FileUploadField components.\n */\n getStateKeys(component: FormComponent): string[] {\n // Only return upload keys for FileUploadField components\n if (!(component instanceof FileUploadField)) {\n return []\n }\n\n const pagePath = component.page?.path\n return pagePath ? [`upload['${pagePath}']`] : ['upload']\n }\n\n getFormDataFromState(\n request: FormContextRequest | undefined,\n state: FormSubmissionState\n ) {\n const { fileUpload } = this\n\n const payload = super.getFormDataFromState(request, state)\n const files = this.getFilesFromState(state)\n\n // Append the files to the payload\n payload[fileUpload.name] = files.length ? files : undefined\n\n return payload\n }\n\n async getState(request: AnyFormRequest) {\n const { fileUpload } = this\n\n // Get the actual state\n const state = await super.getState(request)\n const files = this.getFilesFromState(state)\n\n // Overwrite the files with those in the upload state\n state[fileUpload.name] = files\n\n return this.refreshUpload(request, state)\n }\n\n /**\n * Get the uploaded files from state.\n */\n getFilesFromState(state: FormSubmissionState) {\n const { path } = this\n\n const uploadState = state.upload?.[path]\n return uploadState?.files ?? []\n }\n\n /**\n * Get the initiated upload from state.\n */\n getUploadFromState(state: FormSubmissionState) {\n const { path } = this\n\n const uploadState = state.upload?.[path]\n return uploadState?.upload\n }\n\n makeGetItemDeleteRouteHandler() {\n return (\n request: FormRequest,\n context: FormContext,\n h: FormResponseToolkit\n ) => {\n const { viewModel } = this\n const { params } = request\n const { state } = context\n\n const files = this.getFilesFromState(state)\n\n const fileToRemove = files.find(\n ({ uploadId }) => uploadId === params.itemId\n )\n\n if (!fileToRemove) {\n throw Boom.notFound('File to delete not found')\n }\n\n const { filename } = fileToRemove.status.form.file\n\n return h.view(this.fileDeleteViewName, {\n ...viewModel,\n context,\n backLink: this.getBackLink(request, context),\n pageTitle: `Are you sure you want to remove this file?`,\n itemTitle: filename,\n confirmation: { text: 'You cannot recover removed files.' },\n buttonConfirm: { text: 'Remove file' },\n buttonCancel: { text: 'Cancel' }\n } satisfies ItemDeletePageViewModel)\n }\n }\n\n makePostItemDeleteRouteHandler() {\n return async (\n request: FormRequestPayload,\n context: FormContext,\n h: FormResponseToolkit\n ) => {\n const { path } = this\n const { state } = context\n\n const { confirm } = this.getFormParams(request)\n\n // Check for any removed files in the POST payload\n if (confirm) {\n await this.checkRemovedFiles(request, state)\n return this.proceed(request, h, path)\n }\n\n return this.proceed(request, h)\n }\n }\n\n getErrors(details?: ValidationErrorItem[]) {\n const { fileUpload } = this\n\n if (details) {\n const errors: FormSubmissionError[] = []\n\n details.forEach((error) => {\n const isUploadError = error.path[0] === fileUpload.name\n const isUploadRootError = isUploadError && error.path.length === 1\n\n if (!isUploadError || isUploadRootError) {\n // The error is for the root of the upload or another\n // field on the page so defer to the getError helper\n errors.push(getError(error))\n } else {\n const { context, path, type } = error\n\n if (type === 'object.unknown' && path.at(-1) === 'errorMessage') {\n const value = context?.value as string | undefined\n\n if (value) {\n const name = fileUpload.name\n const text = typeof value === 'string' ? value : 'Unknown error'\n const href = `#${name}`\n\n errors.push({ path, href, name, text })\n }\n }\n }\n })\n\n return errors\n }\n }\n\n getViewModel(\n request: FormContextRequest,\n context: FormContext\n ): FeaturedFormPageViewModel {\n const { fileUpload } = this\n const { state } = context\n\n const upload = this.getUploadFromState(state)\n\n const viewModel = super.getViewModel(request, context)\n const { components } = viewModel\n\n // Featured form component\n const [formComponent] = components.filter(\n ({ model }) => model.id === fileUpload.name\n )\n\n const index = components.indexOf(formComponent)\n\n const proxyUrl = getProxyUrlForLocalDevelopment(upload?.uploadUrl)\n\n return {\n ...viewModel,\n formAction: upload?.uploadUrl,\n uploadId: upload?.uploadId,\n formComponent,\n\n // Split out components before/after\n componentsBefore: components.slice(0, index),\n components: components.slice(index),\n proxyUrl\n }\n }\n\n /**\n * Refreshes the CDP upload and files in the\n * state and checks for any removed files.\n *\n * If an upload exists and hasn't been consumed\n * it gets re-used, otherwise we initiate a new one.\n * @param request - the hapi request\n * @param state - the form state\n */\n private async refreshUpload(\n request: AnyFormRequest,\n state: FormSubmissionState\n ) {\n state = await this.checkUploadStatus(request, state)\n\n return state\n }\n\n /**\n * If an upload exists and hasn't been consumed\n * it gets re-used, otherwise a new one is initiated.\n * @param request - the hapi request\n * @param state - the form state\n * @param depth - the number of retries so far\n */\n private async checkUploadStatus(\n request: AnyFormRequest,\n state: FormSubmissionState,\n depth = 1\n ): Promise<FormSubmissionState> {\n const upload = this.getUploadFromState(state)\n const files = this.getFilesFromState(state)\n\n // If no upload exists, initiate a new one.\n if (!upload?.uploadId) {\n return this.initiateAndStoreNewUpload(request, state)\n }\n\n const uploadId = upload.uploadId\n\n let statusResponse\n\n try {\n statusResponse = await getUploadStatus(uploadId)\n } catch (err) {\n // if the user loads a file upload page and queries the cached upload, after the upload has\n // expired in CDP, we will get a 404 from the getUploadStatus endpoint.\n // In this case we want to initiate a new upload and return that state, so the form\n // doesn't blow up for the end user.\n if (\n Boom.isBoom(err) &&\n err.output.statusCode === StatusCodes.NOT_FOUND.valueOf()\n ) {\n return this.initiateAndStoreNewUpload(request, state)\n }\n throw err\n }\n\n if (!statusResponse) {\n throw Boom.badRequest(\n `Unexpected empty response from getUploadStatus for ${uploadId}`\n )\n }\n\n // Re-use the upload if it is still in the \"initiated\" state.\n if (statusResponse.uploadStatus === UploadStatus.initiated) {\n return state\n }\n\n if (statusResponse.uploadStatus === UploadStatus.pending) {\n // Using exponential backoff delays:\n // Depth 1: 2000ms, Depth 2: 4000ms, Depth 3: 8000ms, Depth 4: 16000ms, Depth 5+: 30000ms (capped)\n // A depth of 5 (or more) implies cumulative delays roughly reaching 55 seconds.\n if (depth >= 5) {\n const err = new Error(\n `Exceeded cumulative retry delay for ${uploadId} (depth: ${depth}). Re-initiating a new upload.`\n )\n request.logger.error(\n err,\n `[uploadTimeout] Exceeded cumulative retry delay for uploadId: ${uploadId} at depth: ${depth} - re-initiating new upload`\n )\n await this.initiateAndStoreNewUpload(request, state)\n throw Boom.gatewayTimeout(\n `Timed out waiting for ${uploadId} after cumulative retries exceeding ${((CDP_UPLOAD_TIMEOUT_MS - 5000) / 1000).toFixed(0)} seconds`\n )\n }\n const delay = getExponentialBackoffDelay(depth)\n request.logger.info(\n `[uploadRetry] Waiting ${delay / 1000} seconds for uploadId: ${uploadId} to complete (retry depth: ${depth})`\n )\n await wait(delay)\n return this.checkUploadStatus(request, state, depth + 1)\n }\n\n // Only add to files state if the file validates.\n // This secures against html tampering of the file input\n // by adding a 'multiple' attribute or it being\n // changed to a simple text field or similar.\n const validationResult = tempItemSchema.validate(\n { uploadId, status: statusResponse },\n { stripUnknown: true }\n )\n const error = validationResult.error\n const fileState = validationResult.value as FileState\n\n if (error) {\n return this.initiateAndStoreNewUpload(request, state)\n }\n\n const file = fileState.status.form.file\n if (file.fileStatus === FileStatus.complete) {\n files.unshift(prepareFileState(fileState))\n await this.mergeState(request, state, {\n upload: { [this.path]: { files, upload } }\n })\n } else {\n // Flash the error message.\n const { fileUpload } = this\n const cacheService = getCacheService(request.server)\n\n const name = fileUpload.name\n const text = file.errorMessage ?? 'Unknown error'\n const errors: FormSubmissionError[] = [\n { path: [name], href: `#${name}`, name, text }\n ]\n cacheService.setFlash(request, { errors })\n }\n\n return this.initiateAndStoreNewUpload(request, state)\n }\n\n /**\n * Checks the payload for a file getting removed\n * and removes it from the upload files if found\n * @param request - the hapi request\n * @param state - the form state\n * @returns updated state if any files have been removed\n */\n private async checkRemovedFiles(\n request: FormRequestPayload,\n state: FormSubmissionState\n ) {\n const { path } = this\n const { params } = request\n\n const upload = this.getUploadFromState(state)\n const files = this.getFilesFromState(state)\n\n const filesUpdated = files.filter(\n ({ uploadId }) => uploadId !== params.itemId\n )\n\n if (filesUpdated.length === files.length) {\n return\n }\n\n await this.mergeState(request, state, {\n upload: { [path]: { files: filesUpdated, upload } }\n })\n }\n\n /**\n * Initiates a CDP file upload and stores in the upload state\n * @param request - the hapi request\n * @param state - the form state\n */\n private async initiateAndStoreNewUpload(\n request: AnyFormRequest,\n state: FormSubmissionState\n ) {\n const { fileUpload, href, path } = this\n const { options, schema } = fileUpload\n const { getFormMetadata } = this.model.services.formsService\n\n const files = this.getFilesFromState(state)\n\n // Reset the upload in state\n let upload: UploadInitiateResponse | undefined\n\n // Don't initiate anymore after minimum of `schema.max` or MAX_UPLOADS\n const max = Math.min(schema.max ?? MAX_UPLOADS, MAX_UPLOADS)\n\n if (files.length < max) {\n const formMetadata = await getFormMetadata(request.params.slug)\n const notificationEmail =\n formMetadata.notificationEmail ?? 'defraforms@defra.gov.uk'\n\n const newUpload = await initiateUpload(\n href,\n notificationEmail,\n options.accept\n )\n\n if (newUpload === undefined) {\n throw Boom.badRequest('Unexpected empty response from initiateUpload')\n }\n\n upload = newUpload\n }\n\n return this.mergeState(request, state, {\n upload: { [path]: { files, upload } }\n })\n }\n}\n"],"mappings":"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","ignoreList":[]}
1
+ {"version":3,"file":"FileUploadPageController.js","names":["ComponentType","Boom","wait","StatusCodes","FileUploadField","tempItemSchema","getCacheService","getError","getExponentialBackoffDelay","QuestionPageController","getProxyUrlForLocalDevelopment","getUploadStatus","initiateUpload","FileStatus","UploadStatus","MAX_UPLOADS","CDP_UPLOAD_TIMEOUT_MS","prepareStatus","status","file","form","isPending","fileStatus","pending","errorMessage","prepareFileState","fileState","FileUploadPageController","fileUpload","fileDeleteViewName","constructor","model","pageDef","collection","fileUploads","fields","filter","field","type","at","length","badImplementation","path","indexOf","name","viewName","getStateKeys","component","pagePath","page","getFormDataFromState","request","state","payload","files","getFilesFromState","undefined","getState","refreshUpload","uploadState","upload","getUploadFromState","makeGetItemDeleteRouteHandler","context","h","viewModel","params","fileToRemove","find","fileId","itemId","notFound","filename","view","backLink","getBackLink","pageTitle","itemTitle","confirmation","text","buttonConfirm","buttonCancel","makePostItemDeleteRouteHandler","confirm","getFormParams","checkRemovedFiles","proceed","getErrors","details","errors","forEach","error","isUploadError","isUploadRootError","push","value","href","getViewModel","components","formComponent","id","index","proxyUrl","uploadUrl","formAction","uploadId","componentsBefore","slice","checkUploadStatus","depth","initiateAndStoreNewUpload","statusResponse","err","isBoom","output","statusCode","NOT_FOUND","valueOf","badRequest","uploadStatus","initiated","Error","logger","gatewayTimeout","toFixed","delay","info","validationResult","validate","stripUnknown","processUploadedFiles","validatedItem","validatedStatus","rawFile","uploadedFiles","Array","isArray","allErrors","complete","perFileState","unshift","cacheService","server","setFlash","some","f","mergeState","filesUpdated","options","schema","getFormMetadata","services","formsService","max","Math","min","formMetadata","slug","notificationEmail","newUpload","accept"],"sources":["../../../../../src/server/plugins/engine/pageControllers/FileUploadPageController.ts"],"sourcesContent":["import { ComponentType, type PageFileUpload } from '@defra/forms-model'\nimport Boom from '@hapi/boom'\nimport { wait } from '@hapi/hoek'\nimport { StatusCodes } from 'http-status-codes'\nimport { type ValidationErrorItem } from 'joi'\n\nimport {\n FileUploadField,\n tempItemSchema\n} from '~/src/server/plugins/engine/components/FileUploadField.js'\nimport { type FormComponent } from '~/src/server/plugins/engine/components/FormComponent.js'\nimport {\n getCacheService,\n getError,\n getExponentialBackoffDelay\n} from '~/src/server/plugins/engine/helpers.js'\nimport { type FormModel } from '~/src/server/plugins/engine/models/index.js'\nimport { QuestionPageController } from '~/src/server/plugins/engine/pageControllers/QuestionPageController.js'\nimport { getProxyUrlForLocalDevelopment } from '~/src/server/plugins/engine/pageControllers/helpers/index.js'\nimport {\n getUploadStatus,\n initiateUpload\n} from '~/src/server/plugins/engine/services/uploadService.js'\nimport {\n FileStatus,\n UploadStatus,\n type AnyFormRequest,\n type FeaturedFormPageViewModel,\n type FileState,\n type FileUpload,\n type FormContext,\n type FormContextRequest,\n type FormSubmissionError,\n type FormSubmissionState,\n type ItemDeletePageViewModel,\n type UploadInitiateResponse,\n type UploadStatusFileResponse\n} from '~/src/server/plugins/engine/types.js'\nimport {\n type FormRequest,\n type FormRequestPayload,\n type FormResponseToolkit\n} from '~/src/server/routes/types.js'\n\nconst MAX_UPLOADS = 25\nconst CDP_UPLOAD_TIMEOUT_MS = 60000 // 1 minute\n\nexport function prepareStatus(status: UploadStatusFileResponse) {\n const file = status.form.file\n const isPending = file.fileStatus === FileStatus.pending\n\n if (!file.errorMessage && isPending) {\n file.errorMessage = 'The selected file has not fully uploaded'\n }\n\n return status\n}\n\nfunction prepareFileState(fileState: FileState) {\n prepareStatus(fileState.status)\n\n return fileState\n}\n\nexport class FileUploadPageController extends QuestionPageController {\n declare pageDef: PageFileUpload\n\n fileUpload: FileUploadField\n fileDeleteViewName = 'item-delete'\n\n constructor(model: FormModel, pageDef: PageFileUpload) {\n super(model, pageDef)\n\n const { collection } = this\n\n // Get the file upload fields from the collection\n const fileUploads = collection.fields.filter(\n (field): field is FileUploadField =>\n field.type === ComponentType.FileUploadField\n )\n\n const fileUpload = fileUploads.at(0)\n\n // Assert we have exactly 1 file upload component\n if (!fileUpload || fileUploads.length > 1) {\n throw Boom.badImplementation(\n `Expected 1 FileUploadFieldComponent in FileUploadPageController '${pageDef.path}'`\n )\n }\n\n // Assert the file upload component is the first form component\n if (collection.fields.indexOf(fileUpload) !== 0) {\n throw Boom.badImplementation(\n `Expected '${fileUpload.name}' to be the first form component in FileUploadPageController '${pageDef.path}'`\n )\n }\n\n // Assign the file upload component to the controller\n this.fileUpload = fileUpload\n this.viewName = 'file-upload'\n }\n\n /**\n * Get supplementary state keys for clearing file upload state.\n * Returns the nested upload path for FileUploadField components only.\n * @param component - The component to get supplementary state keys for\n * @returns Array containing the nested upload path, e.g., [\"upload['/page-path']\"]\n * or ['upload'] if no page path is available. Returns empty array for non-FileUploadField components.\n */\n getStateKeys(component: FormComponent): string[] {\n // Only return upload keys for FileUploadField components\n if (!(component instanceof FileUploadField)) {\n return []\n }\n\n const pagePath = component.page?.path\n return pagePath ? [`upload['${pagePath}']`] : ['upload']\n }\n\n getFormDataFromState(\n request: FormContextRequest | undefined,\n state: FormSubmissionState\n ) {\n const { fileUpload } = this\n\n const payload = super.getFormDataFromState(request, state)\n const files = this.getFilesFromState(state)\n\n // Append the files to the payload\n payload[fileUpload.name] = files.length ? files : undefined\n\n return payload\n }\n\n async getState(request: AnyFormRequest) {\n const { fileUpload } = this\n\n // Get the actual state\n const state = await super.getState(request)\n const files = this.getFilesFromState(state)\n\n // Overwrite the files with those in the upload state\n state[fileUpload.name] = files\n\n return this.refreshUpload(request, state)\n }\n\n /**\n * Get the uploaded files from state.\n */\n getFilesFromState(state: FormSubmissionState) {\n const { path } = this\n\n const uploadState = state.upload?.[path]\n return uploadState?.files ?? []\n }\n\n /**\n * Get the initiated upload from state.\n */\n getUploadFromState(state: FormSubmissionState) {\n const { path } = this\n\n const uploadState = state.upload?.[path]\n return uploadState?.upload\n }\n\n makeGetItemDeleteRouteHandler() {\n return (\n request: FormRequest,\n context: FormContext,\n h: FormResponseToolkit\n ) => {\n const { viewModel } = this\n const { params } = request\n const { state } = context\n\n const files = this.getFilesFromState(state)\n\n const fileToRemove = files.find(\n ({ status }) => status.form.file.fileId === params.itemId\n )\n\n if (!fileToRemove) {\n throw Boom.notFound('File to delete not found')\n }\n\n const { filename } = fileToRemove.status.form.file\n\n return h.view(this.fileDeleteViewName, {\n ...viewModel,\n context,\n backLink: this.getBackLink(request, context),\n pageTitle: `Are you sure you want to remove this file?`,\n itemTitle: filename,\n confirmation: { text: 'You cannot recover removed files.' },\n buttonConfirm: { text: 'Remove file' },\n buttonCancel: { text: 'Cancel' }\n } satisfies ItemDeletePageViewModel)\n }\n }\n\n makePostItemDeleteRouteHandler() {\n return async (\n request: FormRequestPayload,\n context: FormContext,\n h: FormResponseToolkit\n ) => {\n const { path } = this\n const { state } = context\n\n const { confirm } = this.getFormParams(request)\n\n // Check for any removed files in the POST payload\n if (confirm) {\n await this.checkRemovedFiles(request, state)\n return this.proceed(request, h, path)\n }\n\n return this.proceed(request, h)\n }\n }\n\n getErrors(details?: ValidationErrorItem[]) {\n const { fileUpload } = this\n\n if (details) {\n const errors: FormSubmissionError[] = []\n\n details.forEach((error) => {\n const isUploadError = error.path[0] === fileUpload.name\n const isUploadRootError = isUploadError && error.path.length === 1\n\n if (!isUploadError || isUploadRootError) {\n // The error is for the root of the upload or another\n // field on the page so defer to the getError helper\n errors.push(getError(error))\n } else {\n const { context, path, type } = error\n\n if (type === 'object.unknown' && path.at(-1) === 'errorMessage') {\n const value = context?.value as string | undefined\n\n if (value) {\n const name = fileUpload.name\n const text = typeof value === 'string' ? value : 'Unknown error'\n const href = `#${name}`\n\n errors.push({ path, href, name, text })\n }\n }\n }\n })\n\n return errors\n }\n }\n\n getViewModel(\n request: FormContextRequest,\n context: FormContext\n ): FeaturedFormPageViewModel {\n const { fileUpload } = this\n const { state } = context\n\n const upload = this.getUploadFromState(state)\n\n const viewModel = super.getViewModel(request, context)\n const { components } = viewModel\n\n // Featured form component\n const [formComponent] = components.filter(\n ({ model }) => model.id === fileUpload.name\n )\n\n const index = components.indexOf(formComponent)\n\n const proxyUrl = getProxyUrlForLocalDevelopment(upload?.uploadUrl)\n\n return {\n ...viewModel,\n formAction: upload?.uploadUrl,\n uploadId: upload?.uploadId,\n formComponent,\n\n // Split out components before/after\n componentsBefore: components.slice(0, index),\n components: components.slice(index),\n proxyUrl\n }\n }\n\n /**\n * Refreshes the CDP upload and files in the\n * state and checks for any removed files.\n *\n * If an upload exists and hasn't been consumed\n * it gets re-used, otherwise we initiate a new one.\n * @param request - the hapi request\n * @param state - the form state\n */\n private async refreshUpload(\n request: AnyFormRequest,\n state: FormSubmissionState\n ) {\n state = await this.checkUploadStatus(request, state)\n\n return state\n }\n\n /**\n * If an upload exists and hasn't been consumed\n * it gets re-used, otherwise a new one is initiated.\n * @param request - the hapi request\n * @param state - the form state\n * @param depth - the number of retries so far\n */\n private async checkUploadStatus(\n request: AnyFormRequest,\n state: FormSubmissionState,\n depth = 1\n ): Promise<FormSubmissionState> {\n const upload = this.getUploadFromState(state)\n const files = this.getFilesFromState(state)\n\n // If no upload exists, initiate a new one.\n if (!upload?.uploadId) {\n return this.initiateAndStoreNewUpload(request, state)\n }\n\n const uploadId = upload.uploadId\n\n let statusResponse\n\n try {\n statusResponse = await getUploadStatus(uploadId)\n } catch (err) {\n // if the user loads a file upload page and queries the cached upload, after the upload has\n // expired in CDP, we will get a 404 from the getUploadStatus endpoint.\n // In this case we want to initiate a new upload and return that state, so the form\n // doesn't blow up for the end user.\n if (\n Boom.isBoom(err) &&\n err.output.statusCode === StatusCodes.NOT_FOUND.valueOf()\n ) {\n return this.initiateAndStoreNewUpload(request, state)\n }\n throw err\n }\n\n if (!statusResponse) {\n throw Boom.badRequest(\n `Unexpected empty response from getUploadStatus for ${uploadId}`\n )\n }\n\n // Re-use the upload if it is still in the \"initiated\" state.\n if (statusResponse.uploadStatus === UploadStatus.initiated) {\n return state\n }\n\n if (statusResponse.uploadStatus === UploadStatus.pending) {\n // Using exponential backoff delays:\n // Depth 1: 2000ms, Depth 2: 4000ms, Depth 3: 8000ms, Depth 4: 16000ms, Depth 5+: 30000ms (capped)\n // A depth of 5 (or more) implies cumulative delays roughly reaching 55 seconds.\n if (depth >= 5) {\n const err = new Error(\n `Exceeded cumulative retry delay for ${uploadId} (depth: ${depth}). Re-initiating a new upload.`\n )\n request.logger.error(\n err,\n `[uploadTimeout] Exceeded cumulative retry delay for uploadId: ${uploadId} at depth: ${depth} - re-initiating new upload`\n )\n await this.initiateAndStoreNewUpload(request, state)\n throw Boom.gatewayTimeout(\n `Timed out waiting for ${uploadId} after cumulative retries exceeding ${((CDP_UPLOAD_TIMEOUT_MS - 5000) / 1000).toFixed(0)} seconds`\n )\n }\n const delay = getExponentialBackoffDelay(depth)\n request.logger.info(\n `[uploadRetry] Waiting ${delay / 1000} seconds for uploadId: ${uploadId} to complete (retry depth: ${depth})`\n )\n await wait(delay)\n return this.checkUploadStatus(request, state, depth + 1)\n }\n\n // Only add to files state if the file validates.\n // This secures against html tampering of the file input\n // (e.g. changing it to a simple text field or similar).\n const validationResult = tempItemSchema.validate(\n { uploadId, status: statusResponse },\n { stripUnknown: true }\n )\n const error = validationResult.error\n\n if (error) {\n return this.initiateAndStoreNewUpload(request, state)\n }\n\n // CDP returns form.file as a single object for one file,\n // or an array for multiple files. The Joi schema normalises\n // both to an array via .single().\n await this.processUploadedFiles(\n request,\n state,\n validationResult.value,\n files,\n upload\n )\n\n return this.initiateAndStoreNewUpload(request, state)\n }\n\n /**\n * Processes the uploaded files from a CDP status response.\n * Complete files are added to state, rejected/pending files\n * have their error messages flashed.\n * @param request - the hapi request\n * @param state - the form state\n * @param validatedItem - the Joi-validated upload item\n * @param files - the current files array from state\n * @param upload - the current upload initiation response\n */\n private async processUploadedFiles(\n request: AnyFormRequest,\n state: FormSubmissionState,\n validatedItem: FileState,\n files: FileState[],\n upload: UploadInitiateResponse | undefined\n ) {\n const { uploadId } = validatedItem\n const validatedStatus = validatedItem.status\n const rawFile = validatedStatus.form.file as unknown as\n | FileUpload\n | FileUpload[]\n const uploadedFiles = Array.isArray(rawFile) ? rawFile : [rawFile]\n\n const allErrors: FormSubmissionError[] = []\n\n for (const file of uploadedFiles) {\n if (file.fileStatus === FileStatus.complete) {\n const perFileState: FileState = {\n uploadId,\n status: {\n ...validatedStatus,\n form: { file }\n } as FileState['status']\n }\n files.unshift(prepareFileState(perFileState))\n } else {\n // Collect the error for rejected/pending files.\n const { fileUpload } = this\n const name = fileUpload.name\n const text = file.errorMessage ?? 'Unknown error'\n allErrors.push({ path: [name], href: `#${name}`, name, text })\n }\n }\n\n if (allErrors.length) {\n const cacheService = getCacheService(request.server)\n cacheService.setFlash(request, { errors: allErrors })\n }\n\n if (uploadedFiles.some((f) => f.fileStatus === FileStatus.complete)) {\n await this.mergeState(request, state, {\n upload: { [this.path]: { files, upload } }\n })\n }\n }\n\n /**\n * Checks the payload for a file getting removed\n * and removes it from the upload files if found\n * @param request - the hapi request\n * @param state - the form state\n * @returns updated state if any files have been removed\n */\n private async checkRemovedFiles(\n request: FormRequestPayload,\n state: FormSubmissionState\n ) {\n const { path } = this\n const { params } = request\n\n const upload = this.getUploadFromState(state)\n const files = this.getFilesFromState(state)\n\n const filesUpdated = files.filter(\n ({ status }) => status.form.file.fileId !== params.itemId\n )\n\n if (filesUpdated.length === files.length) {\n return\n }\n\n await this.mergeState(request, state, {\n upload: { [path]: { files: filesUpdated, upload } }\n })\n }\n\n /**\n * Initiates a CDP file upload and stores in the upload state\n * @param request - the hapi request\n * @param state - the form state\n */\n private async initiateAndStoreNewUpload(\n request: AnyFormRequest,\n state: FormSubmissionState\n ) {\n const { fileUpload, href, path } = this\n const { options, schema } = fileUpload\n const { getFormMetadata } = this.model.services.formsService\n\n const files = this.getFilesFromState(state)\n\n // Reset the upload in state\n let upload: UploadInitiateResponse | undefined\n\n // Don't initiate anymore after minimum of `schema.max` or MAX_UPLOADS\n const max = Math.min(schema.max ?? MAX_UPLOADS, MAX_UPLOADS)\n\n if (files.length < max) {\n const formMetadata = await getFormMetadata(request.params.slug)\n const notificationEmail =\n formMetadata.notificationEmail ?? 'defraforms@defra.gov.uk'\n\n const newUpload = await initiateUpload(\n href,\n notificationEmail,\n options.accept\n )\n\n if (newUpload === undefined) {\n throw Boom.badRequest('Unexpected empty response from initiateUpload')\n }\n\n upload = newUpload\n }\n\n return this.mergeState(request, state, {\n upload: { [path]: { files, upload } }\n })\n }\n}\n"],"mappings":"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","ignoreList":[]}
@@ -14,6 +14,7 @@ export declare class PageController {
14
14
  name?: string;
15
15
  model: FormModel;
16
16
  pageDef: Page;
17
+ id?: string;
17
18
  title: string;
18
19
  section?: Section;
19
20
  condition?: ExecutableCondition;
@@ -9,6 +9,7 @@ export class PageController {
9
9
  name;
10
10
  model;
11
11
  pageDef;
12
+ id;
12
13
  title;
13
14
  section;
14
15
  condition;
@@ -24,6 +25,7 @@ export class PageController {
24
25
  this.name = def.name;
25
26
  this.model = model;
26
27
  this.pageDef = pageDef;
28
+ this.id = pageDef.id;
27
29
  this.title = pageDef.title;
28
30
  this.events = pageDef.events;
29
31
 
@@ -1 +1 @@
1
- {"version":3,"file":"PageController.js","names":["ControllerPath","Boom","getSaveAndExitHelpers","getStartPath","normalisePath","PageController","def","name","model","pageDef","title","section","condition","events","collection","viewName","allowSaveAndExit","constructor","getSection","conditions","view","path","href","getHref","keys","getRouteOptions","postRouteOptions","viewModel","showTitle","pageTitle","sectionTitle","hideTitle","page","isStartPage","serviceUrl","feedbackLink","phaseTag","options","disableUserFeedback","undefined","formId","phaseBanner","phase","basePath","relativeTargetPath","startsWith","substring","finalPath","replace","getSummaryPath","Summary","valueOf","getStatusPath","Status","makeGetRouteHandler","request","context","h","makePostRouteHandler","badRequest","getStateKeys","_component","shouldShowSaveAndExit","server"],"sources":["../../../../../src/server/plugins/engine/pageControllers/PageController.ts"],"sourcesContent":["import {\n ControllerPath,\n type Events,\n type FormDefinition,\n type Page,\n type Section\n} from '@defra/forms-model'\nimport Boom from '@hapi/boom'\nimport { type Lifecycle, type RouteOptions, type Server } from '@hapi/hapi'\n\nimport { type ComponentCollection } from '~/src/server/plugins/engine/components/ComponentCollection.js'\nimport { type FormComponent } from '~/src/server/plugins/engine/components/FormComponent.js'\nimport {\n getSaveAndExitHelpers,\n getStartPath,\n normalisePath\n} from '~/src/server/plugins/engine/helpers.js'\nimport { type FormModel } from '~/src/server/plugins/engine/models/index.js'\nimport { type ExecutableCondition } from '~/src/server/plugins/engine/models/types.js'\nimport {\n type FormContext,\n type PageViewModelBase\n} from '~/src/server/plugins/engine/types.js'\nimport {\n type FormRequest,\n type FormRequestPayload,\n type FormRequestPayloadRefs,\n type FormRequestRefs,\n type FormResponseToolkit\n} from '~/src/server/routes/types.js'\n\nexport class PageController {\n /**\n * The base class for all page controllers. Page controllers are responsible for generating the get and post route handlers when a user navigates to `/{id}/{path*}`.\n */\n def: FormDefinition\n name?: string\n model: FormModel\n pageDef: Page\n title: string\n section?: Section\n condition?: ExecutableCondition\n events?: Events\n collection?: ComponentCollection\n viewName = 'index'\n allowSaveAndExit = false\n\n constructor(model: FormModel, pageDef: Page) {\n const { def } = model\n\n this.def = def\n this.name = def.name\n this.model = model\n this.pageDef = pageDef\n this.title = pageDef.title\n this.events = pageDef.events\n\n // Resolve section\n if (pageDef.section) {\n this.section = model.getSection(pageDef.section)\n }\n\n // Resolve condition\n if (pageDef.condition) {\n this.condition = model.conditions[pageDef.condition]\n }\n\n // Override view name\n if (pageDef.view) {\n this.viewName = pageDef.view\n }\n }\n\n get path() {\n return this.pageDef.path\n }\n\n get href() {\n const { path } = this\n return this.getHref(`/${normalisePath(path)}`)\n }\n\n get keys() {\n return this.collection?.keys ?? []\n }\n\n /**\n * {@link https://hapi.dev/api/?v=20.1.2#route-options}\n */\n get getRouteOptions(): RouteOptions<FormRequestRefs> {\n return {}\n }\n\n /**\n * {@link https://hapi.dev/api/?v=20.1.2#route-options}\n */\n get postRouteOptions(): RouteOptions<FormRequestPayloadRefs> {\n return {}\n }\n\n get viewModel(): PageViewModelBase {\n const { name, section, title } = this\n\n const showTitle = true\n const pageTitle = title\n const sectionTitle = section?.hideTitle !== true ? section?.title : ''\n\n return {\n name,\n page: this,\n pageTitle,\n sectionTitle,\n showTitle,\n isStartPage: false,\n serviceUrl: this.getHref('/'),\n feedbackLink: this.feedbackLink,\n phaseTag: this.phaseTag\n }\n }\n\n get feedbackLink() {\n return this.def.options?.disableUserFeedback\n ? undefined\n : `/form/feedback?formId=${this.model.formId}`\n }\n\n get phaseTag() {\n const { def } = this\n return def.phaseBanner?.phase\n }\n\n getHref(path: string): string {\n const basePath = this.model.basePath\n\n if (path === '/') {\n return `/${basePath}`\n }\n\n // if ever the path is not prefixed with a slash, add it\n const relativeTargetPath = path.startsWith('/') ? path.substring(1) : path\n let finalPath = `/${basePath}`\n if (relativeTargetPath) {\n finalPath += `/${relativeTargetPath}`\n }\n finalPath = finalPath.replace(/\\/{2,}/g, '/')\n\n return finalPath\n }\n\n getStartPath() {\n return getStartPath(this.model)\n }\n\n getSummaryPath() {\n return ControllerPath.Summary.valueOf()\n }\n\n getStatusPath() {\n return ControllerPath.Status.valueOf()\n }\n\n makeGetRouteHandler(): (\n request: FormRequest,\n context: FormContext,\n h: FormResponseToolkit\n ) => ReturnType<Lifecycle.Method<FormRequestRefs>> {\n return (request, context, h) => {\n const { viewModel, viewName } = this\n return h.view(viewName, viewModel)\n }\n }\n\n makePostRouteHandler(): (\n request: FormRequestPayload,\n context: FormContext,\n h: FormResponseToolkit\n ) => ReturnType<Lifecycle.Method<FormRequestPayloadRefs>> {\n throw Boom.badRequest('Unsupported POST route handler for this page')\n }\n\n /**\n * Get supplementary state keys for clearing component state.\n *\n * This method returns page controller-level state keys only. The core component's\n * state key (the component's name) is managed separately by the framework and should\n * NOT be included in the returned array.\n *\n * Returns an empty array by default. Override in subclasses to provide\n * page-specific supplementary state keys (e.g., upload state, cached data).\n * @param _component - The component to get supplementary state keys for (optional)\n * @returns Array of supplementary state keys to clear (excluding the component name itself)\n */\n getStateKeys(_component?: FormComponent): string[] {\n return []\n }\n\n shouldShowSaveAndExit(server: Server): boolean {\n return getSaveAndExitHelpers(server) !== undefined && this.allowSaveAndExit\n }\n}\n"],"mappings":"AAAA,SACEA,cAAc,QAKT,oBAAoB;AAC3B,OAAOC,IAAI,MAAM,YAAY;AAK7B,SACEC,qBAAqB,EACrBC,YAAY,EACZC,aAAa;AAgBf,OAAO,MAAMC,cAAc,CAAC;EAC1B;AACF;AACA;EACEC,GAAG;EACHC,IAAI;EACJC,KAAK;EACLC,OAAO;EACPC,KAAK;EACLC,OAAO;EACPC,SAAS;EACTC,MAAM;EACNC,UAAU;EACVC,QAAQ,GAAG,OAAO;EAClBC,gBAAgB,GAAG,KAAK;EAExBC,WAAWA,CAACT,KAAgB,EAAEC,OAAa,EAAE;IAC3C,MAAM;MAAEH;IAAI,CAAC,GAAGE,KAAK;IAErB,IAAI,CAACF,GAAG,GAAGA,GAAG;IACd,IAAI,CAACC,IAAI,GAAGD,GAAG,CAACC,IAAI;IACpB,IAAI,CAACC,KAAK,GAAGA,KAAK;IAClB,IAAI,CAACC,OAAO,GAAGA,OAAO;IACtB,IAAI,CAACC,KAAK,GAAGD,OAAO,CAACC,KAAK;IAC1B,IAAI,CAACG,MAAM,GAAGJ,OAAO,CAACI,MAAM;;IAE5B;IACA,IAAIJ,OAAO,CAACE,OAAO,EAAE;MACnB,IAAI,CAACA,OAAO,GAAGH,KAAK,CAACU,UAAU,CAACT,OAAO,CAACE,OAAO,CAAC;IAClD;;IAEA;IACA,IAAIF,OAAO,CAACG,SAAS,EAAE;MACrB,IAAI,CAACA,SAAS,GAAGJ,KAAK,CAACW,UAAU,CAACV,OAAO,CAACG,SAAS,CAAC;IACtD;;IAEA;IACA,IAAIH,OAAO,CAACW,IAAI,EAAE;MAChB,IAAI,CAACL,QAAQ,GAAGN,OAAO,CAACW,IAAI;IAC9B;EACF;EAEA,IAAIC,IAAIA,CAAA,EAAG;IACT,OAAO,IAAI,CAACZ,OAAO,CAACY,IAAI;EAC1B;EAEA,IAAIC,IAAIA,CAAA,EAAG;IACT,MAAM;MAAED;IAAK,CAAC,GAAG,IAAI;IACrB,OAAO,IAAI,CAACE,OAAO,CAAC,IAAInB,aAAa,CAACiB,IAAI,CAAC,EAAE,CAAC;EAChD;EAEA,IAAIG,IAAIA,CAAA,EAAG;IACT,OAAO,IAAI,CAACV,UAAU,EAAEU,IAAI,IAAI,EAAE;EACpC;;EAEA;AACF;AACA;EACE,IAAIC,eAAeA,CAAA,EAAkC;IACnD,OAAO,CAAC,CAAC;EACX;;EAEA;AACF;AACA;EACE,IAAIC,gBAAgBA,CAAA,EAAyC;IAC3D,OAAO,CAAC,CAAC;EACX;EAEA,IAAIC,SAASA,CAAA,EAAsB;IACjC,MAAM;MAAEpB,IAAI;MAAEI,OAAO;MAAED;IAAM,CAAC,GAAG,IAAI;IAErC,MAAMkB,SAAS,GAAG,IAAI;IACtB,MAAMC,SAAS,GAAGnB,KAAK;IACvB,MAAMoB,YAAY,GAAGnB,OAAO,EAAEoB,SAAS,KAAK,IAAI,GAAGpB,OAAO,EAAED,KAAK,GAAG,EAAE;IAEtE,OAAO;MACLH,IAAI;MACJyB,IAAI,EAAE,IAAI;MACVH,SAAS;MACTC,YAAY;MACZF,SAAS;MACTK,WAAW,EAAE,KAAK;MAClBC,UAAU,EAAE,IAAI,CAACX,OAAO,CAAC,GAAG,CAAC;MAC7BY,YAAY,EAAE,IAAI,CAACA,YAAY;MAC/BC,QAAQ,EAAE,IAAI,CAACA;IACjB,CAAC;EACH;EAEA,IAAID,YAAYA,CAAA,EAAG;IACjB,OAAO,IAAI,CAAC7B,GAAG,CAAC+B,OAAO,EAAEC,mBAAmB,GACxCC,SAAS,GACT,yBAAyB,IAAI,CAAC/B,KAAK,CAACgC,MAAM,EAAE;EAClD;EAEA,IAAIJ,QAAQA,CAAA,EAAG;IACb,MAAM;MAAE9B;IAAI,CAAC,GAAG,IAAI;IACpB,OAAOA,GAAG,CAACmC,WAAW,EAAEC,KAAK;EAC/B;EAEAnB,OAAOA,CAACF,IAAY,EAAU;IAC5B,MAAMsB,QAAQ,GAAG,IAAI,CAACnC,KAAK,CAACmC,QAAQ;IAEpC,IAAItB,IAAI,KAAK,GAAG,EAAE;MAChB,OAAO,IAAIsB,QAAQ,EAAE;IACvB;;IAEA;IACA,MAAMC,kBAAkB,GAAGvB,IAAI,CAACwB,UAAU,CAAC,GAAG,CAAC,GAAGxB,IAAI,CAACyB,SAAS,CAAC,CAAC,CAAC,GAAGzB,IAAI;IAC1E,IAAI0B,SAAS,GAAG,IAAIJ,QAAQ,EAAE;IAC9B,IAAIC,kBAAkB,EAAE;MACtBG,SAAS,IAAI,IAAIH,kBAAkB,EAAE;IACvC;IACAG,SAAS,GAAGA,SAAS,CAACC,OAAO,CAAC,SAAS,EAAE,GAAG,CAAC;IAE7C,OAAOD,SAAS;EAClB;EAEA5C,YAAYA,CAAA,EAAG;IACb,OAAOA,YAAY,CAAC,IAAI,CAACK,KAAK,CAAC;EACjC;EAEAyC,cAAcA,CAAA,EAAG;IACf,OAAOjD,cAAc,CAACkD,OAAO,CAACC,OAAO,CAAC,CAAC;EACzC;EAEAC,aAAaA,CAAA,EAAG;IACd,OAAOpD,cAAc,CAACqD,MAAM,CAACF,OAAO,CAAC,CAAC;EACxC;EAEAG,mBAAmBA,CAAA,EAIgC;IACjD,OAAO,CAACC,OAAO,EAAEC,OAAO,EAAEC,CAAC,KAAK;MAC9B,MAAM;QAAE9B,SAAS;QAAEZ;MAAS,CAAC,GAAG,IAAI;MACpC,OAAO0C,CAAC,CAACrC,IAAI,CAACL,QAAQ,EAAEY,SAAS,CAAC;IACpC,CAAC;EACH;EAEA+B,oBAAoBA,CAAA,EAIsC;IACxD,MAAMzD,IAAI,CAAC0D,UAAU,CAAC,8CAA8C,CAAC;EACvE;;EAEA;AACF;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;EACEC,YAAYA,CAACC,UAA0B,EAAY;IACjD,OAAO,EAAE;EACX;EAEAC,qBAAqBA,CAACC,MAAc,EAAW;IAC7C,OAAO7D,qBAAqB,CAAC6D,MAAM,CAAC,KAAKxB,SAAS,IAAI,IAAI,CAACvB,gBAAgB;EAC7E;AACF","ignoreList":[]}
1
+ {"version":3,"file":"PageController.js","names":["ControllerPath","Boom","getSaveAndExitHelpers","getStartPath","normalisePath","PageController","def","name","model","pageDef","id","title","section","condition","events","collection","viewName","allowSaveAndExit","constructor","getSection","conditions","view","path","href","getHref","keys","getRouteOptions","postRouteOptions","viewModel","showTitle","pageTitle","sectionTitle","hideTitle","page","isStartPage","serviceUrl","feedbackLink","phaseTag","options","disableUserFeedback","undefined","formId","phaseBanner","phase","basePath","relativeTargetPath","startsWith","substring","finalPath","replace","getSummaryPath","Summary","valueOf","getStatusPath","Status","makeGetRouteHandler","request","context","h","makePostRouteHandler","badRequest","getStateKeys","_component","shouldShowSaveAndExit","server"],"sources":["../../../../../src/server/plugins/engine/pageControllers/PageController.ts"],"sourcesContent":["import {\n ControllerPath,\n type Events,\n type FormDefinition,\n type Page,\n type Section\n} from '@defra/forms-model'\nimport Boom from '@hapi/boom'\nimport { type Lifecycle, type RouteOptions, type Server } from '@hapi/hapi'\n\nimport { type ComponentCollection } from '~/src/server/plugins/engine/components/ComponentCollection.js'\nimport { type FormComponent } from '~/src/server/plugins/engine/components/FormComponent.js'\nimport {\n getSaveAndExitHelpers,\n getStartPath,\n normalisePath\n} from '~/src/server/plugins/engine/helpers.js'\nimport { type FormModel } from '~/src/server/plugins/engine/models/index.js'\nimport { type ExecutableCondition } from '~/src/server/plugins/engine/models/types.js'\nimport {\n type FormContext,\n type PageViewModelBase\n} from '~/src/server/plugins/engine/types.js'\nimport {\n type FormRequest,\n type FormRequestPayload,\n type FormRequestPayloadRefs,\n type FormRequestRefs,\n type FormResponseToolkit\n} from '~/src/server/routes/types.js'\n\nexport class PageController {\n /**\n * The base class for all page controllers. Page controllers are responsible for generating the get and post route handlers when a user navigates to `/{id}/{path*}`.\n */\n def: FormDefinition\n name?: string\n model: FormModel\n pageDef: Page\n id?: string\n title: string\n section?: Section\n condition?: ExecutableCondition\n events?: Events\n collection?: ComponentCollection\n viewName = 'index'\n allowSaveAndExit = false\n\n constructor(model: FormModel, pageDef: Page) {\n const { def } = model\n\n this.def = def\n this.name = def.name\n this.model = model\n this.pageDef = pageDef\n this.id = pageDef.id\n this.title = pageDef.title\n this.events = pageDef.events\n\n // Resolve section\n if (pageDef.section) {\n this.section = model.getSection(pageDef.section)\n }\n\n // Resolve condition\n if (pageDef.condition) {\n this.condition = model.conditions[pageDef.condition]\n }\n\n // Override view name\n if (pageDef.view) {\n this.viewName = pageDef.view\n }\n }\n\n get path() {\n return this.pageDef.path\n }\n\n get href() {\n const { path } = this\n return this.getHref(`/${normalisePath(path)}`)\n }\n\n get keys() {\n return this.collection?.keys ?? []\n }\n\n /**\n * {@link https://hapi.dev/api/?v=20.1.2#route-options}\n */\n get getRouteOptions(): RouteOptions<FormRequestRefs> {\n return {}\n }\n\n /**\n * {@link https://hapi.dev/api/?v=20.1.2#route-options}\n */\n get postRouteOptions(): RouteOptions<FormRequestPayloadRefs> {\n return {}\n }\n\n get viewModel(): PageViewModelBase {\n const { name, section, title } = this\n\n const showTitle = true\n const pageTitle = title\n const sectionTitle = section?.hideTitle !== true ? section?.title : ''\n\n return {\n name,\n page: this,\n pageTitle,\n sectionTitle,\n showTitle,\n isStartPage: false,\n serviceUrl: this.getHref('/'),\n feedbackLink: this.feedbackLink,\n phaseTag: this.phaseTag\n }\n }\n\n get feedbackLink() {\n return this.def.options?.disableUserFeedback\n ? undefined\n : `/form/feedback?formId=${this.model.formId}`\n }\n\n get phaseTag() {\n const { def } = this\n return def.phaseBanner?.phase\n }\n\n getHref(path: string): string {\n const basePath = this.model.basePath\n\n if (path === '/') {\n return `/${basePath}`\n }\n\n // if ever the path is not prefixed with a slash, add it\n const relativeTargetPath = path.startsWith('/') ? path.substring(1) : path\n let finalPath = `/${basePath}`\n if (relativeTargetPath) {\n finalPath += `/${relativeTargetPath}`\n }\n finalPath = finalPath.replace(/\\/{2,}/g, '/')\n\n return finalPath\n }\n\n getStartPath() {\n return getStartPath(this.model)\n }\n\n getSummaryPath() {\n return ControllerPath.Summary.valueOf()\n }\n\n getStatusPath() {\n return ControllerPath.Status.valueOf()\n }\n\n makeGetRouteHandler(): (\n request: FormRequest,\n context: FormContext,\n h: FormResponseToolkit\n ) => ReturnType<Lifecycle.Method<FormRequestRefs>> {\n return (request, context, h) => {\n const { viewModel, viewName } = this\n return h.view(viewName, viewModel)\n }\n }\n\n makePostRouteHandler(): (\n request: FormRequestPayload,\n context: FormContext,\n h: FormResponseToolkit\n ) => ReturnType<Lifecycle.Method<FormRequestPayloadRefs>> {\n throw Boom.badRequest('Unsupported POST route handler for this page')\n }\n\n /**\n * Get supplementary state keys for clearing component state.\n *\n * This method returns page controller-level state keys only. The core component's\n * state key (the component's name) is managed separately by the framework and should\n * NOT be included in the returned array.\n *\n * Returns an empty array by default. Override in subclasses to provide\n * page-specific supplementary state keys (e.g., upload state, cached data).\n * @param _component - The component to get supplementary state keys for (optional)\n * @returns Array of supplementary state keys to clear (excluding the component name itself)\n */\n getStateKeys(_component?: FormComponent): string[] {\n return []\n }\n\n shouldShowSaveAndExit(server: Server): boolean {\n return getSaveAndExitHelpers(server) !== undefined && this.allowSaveAndExit\n }\n}\n"],"mappings":"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","ignoreList":[]}
@@ -1,3 +1,4 @@
1
+ import { GeospatialField } from "../../components/GeospatialField.js";
1
2
  import { PaymentField } from "../../components/PaymentField.js";
2
3
  import { getAnswer } from "../../components/helpers/components.js";
3
4
  import { formatCurrency, formatPaymentDate } from "../../../payment/helper.js";
@@ -12,6 +13,14 @@ export function buildMainRecords(items) {
12
13
  for (const item of fieldItems) {
13
14
  if (item.field instanceof PaymentField) {
14
15
  records.push(...buildPaymentRecords(item));
16
+ } else if (item.field instanceof GeospatialField) {
17
+ // Stringify of GeoJSON is done here rather than inside `getContextValueFromState`
18
+ // so we don't incur the overhead of JSON.stringify on every request when building context
19
+ records.push({
20
+ name: item.name,
21
+ title: item.label,
22
+ value: JSON.stringify(item.field.getFormValueFromState(item.state))
23
+ });
15
24
  } else {
16
25
  records.push({
17
26
  name: item.name,
@@ -68,7 +77,10 @@ export function buildRepeaterRecords(items) {
68
77
  value: item.subItems.map(detailItems => detailItems.map(subItem => ({
69
78
  name: subItem.name,
70
79
  title: subItem.label,
71
- value: getAnswer(subItem.field, subItem.state, {
80
+ value:
81
+ // Stringify of GeoJSON is done here rather than inside `getContextValueFromState`
82
+ // so we don't incur the overhead of JSON.stringify on every request when building context
83
+ subItem.field instanceof GeospatialField ? JSON.stringify(subItem.field.getFormValueFromState(subItem.state)) : getAnswer(subItem.field, subItem.state, {
72
84
  format: 'data'
73
85
  })
74
86
  })))
@@ -1 +1 @@
1
- {"version":3,"file":"submission.js","names":["PaymentField","getAnswer","formatCurrency","formatPaymentDate","buildMainRecords","items","fieldItems","filter","item","records","field","push","buildPaymentRecords","name","title","label","value","state","format","paymentState","getPaymentStateFromState","description","amount","reference","preAuth","createdAt","buildRepeaterRecords","map","subItems","detailItems","subItem"],"sources":["../../../../../../src/server/plugins/engine/pageControllers/helpers/submission.ts"],"sourcesContent":["import { type SubmitPayload } from '@defra/forms-model'\n\nimport { PaymentField } from '~/src/server/plugins/engine/components/PaymentField.js'\nimport { getAnswer } from '~/src/server/plugins/engine/components/helpers/components.js'\nimport {\n type DetailItem,\n type DetailItemField\n} from '~/src/server/plugins/engine/models/types.js'\nimport {\n formatCurrency,\n formatPaymentDate\n} from '~/src/server/plugins/payment/helper.js'\n\nexport interface SubmitRecord {\n name: string\n title: string\n value: string\n}\n\n/**\n * Builds the main submission records from field items.\n * Regular fields are converted to single records, while PaymentField\n * components are expanded into four separate records.\n */\nexport function buildMainRecords(items: DetailItem[]): SubmitRecord[] {\n const fieldItems = items.filter(\n (item): item is DetailItemField => 'field' in item\n )\n\n const records: SubmitRecord[] = []\n\n for (const item of fieldItems) {\n if (item.field instanceof PaymentField) {\n records.push(...buildPaymentRecords(item))\n } else {\n records.push({\n name: item.name,\n title: item.label,\n value: getAnswer(item.field, item.state, { format: 'data' })\n })\n }\n }\n\n return records\n}\n\n/**\n * Expands a PaymentField into four submission records:\n * - Payment description\n * - Payment amount (formatted with currency symbol)\n * - Payment reference\n * - Payment date (formatted date/time)\n *\n * Returns an empty array if no payment state exists.\n */\nexport function buildPaymentRecords(item: DetailItemField): SubmitRecord[] {\n const paymentState = (item.field as PaymentField).getPaymentStateFromState(\n item.state\n )\n\n if (!paymentState) {\n return []\n }\n\n return [\n {\n name: `${item.name}_paymentDescription`,\n title: 'Payment description',\n value: paymentState.description\n },\n {\n name: `${item.name}_paymentAmount`,\n title: 'Payment amount',\n value: formatCurrency(paymentState.amount)\n },\n {\n name: `${item.name}_paymentReference`,\n title: 'Payment reference',\n value: paymentState.reference\n },\n {\n name: `${item.name}_paymentDate`,\n title: 'Payment date',\n value: paymentState.preAuth?.createdAt\n ? formatPaymentDate(paymentState.preAuth.createdAt)\n : ''\n }\n ]\n}\n\n/**\n * Builds the repeater submission records from repeater items.\n */\nexport function buildRepeaterRecords(\n items: DetailItem[]\n): SubmitPayload['repeaters'] {\n return items\n .filter((item) => 'subItems' in item)\n .map((item) => ({\n name: item.name,\n title: item.label,\n value: item.subItems.map((detailItems) =>\n detailItems.map((subItem) => ({\n name: subItem.name,\n title: subItem.label,\n value: getAnswer(subItem.field, subItem.state, { format: 'data' })\n }))\n )\n }))\n}\n"],"mappings":"AAEA,SAASA,YAAY;AACrB,SAASC,SAAS;AAKlB,SACEC,cAAc,EACdC,iBAAiB;AASnB;AACA;AACA;AACA;AACA;AACA,OAAO,SAASC,gBAAgBA,CAACC,KAAmB,EAAkB;EACpE,MAAMC,UAAU,GAAGD,KAAK,CAACE,MAAM,CAC5BC,IAAI,IAA8B,OAAO,IAAIA,IAChD,CAAC;EAED,MAAMC,OAAuB,GAAG,EAAE;EAElC,KAAK,MAAMD,IAAI,IAAIF,UAAU,EAAE;IAC7B,IAAIE,IAAI,CAACE,KAAK,YAAYV,YAAY,EAAE;MACtCS,OAAO,CAACE,IAAI,CAAC,GAAGC,mBAAmB,CAACJ,IAAI,CAAC,CAAC;IAC5C,CAAC,MAAM;MACLC,OAAO,CAACE,IAAI,CAAC;QACXE,IAAI,EAAEL,IAAI,CAACK,IAAI;QACfC,KAAK,EAAEN,IAAI,CAACO,KAAK;QACjBC,KAAK,EAAEf,SAAS,CAACO,IAAI,CAACE,KAAK,EAAEF,IAAI,CAACS,KAAK,EAAE;UAAEC,MAAM,EAAE;QAAO,CAAC;MAC7D,CAAC,CAAC;IACJ;EACF;EAEA,OAAOT,OAAO;AAChB;;AAEA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA,OAAO,SAASG,mBAAmBA,CAACJ,IAAqB,EAAkB;EACzE,MAAMW,YAAY,GAAIX,IAAI,CAACE,KAAK,CAAkBU,wBAAwB,CACxEZ,IAAI,CAACS,KACP,CAAC;EAED,IAAI,CAACE,YAAY,EAAE;IACjB,OAAO,EAAE;EACX;EAEA,OAAO,CACL;IACEN,IAAI,EAAE,GAAGL,IAAI,CAACK,IAAI,qBAAqB;IACvCC,KAAK,EAAE,qBAAqB;IAC5BE,KAAK,EAAEG,YAAY,CAACE;EACtB,CAAC,EACD;IACER,IAAI,EAAE,GAAGL,IAAI,CAACK,IAAI,gBAAgB;IAClCC,KAAK,EAAE,gBAAgB;IACvBE,KAAK,EAAEd,cAAc,CAACiB,YAAY,CAACG,MAAM;EAC3C,CAAC,EACD;IACET,IAAI,EAAE,GAAGL,IAAI,CAACK,IAAI,mBAAmB;IACrCC,KAAK,EAAE,mBAAmB;IAC1BE,KAAK,EAAEG,YAAY,CAACI;EACtB,CAAC,EACD;IACEV,IAAI,EAAE,GAAGL,IAAI,CAACK,IAAI,cAAc;IAChCC,KAAK,EAAE,cAAc;IACrBE,KAAK,EAAEG,YAAY,CAACK,OAAO,EAAEC,SAAS,GAClCtB,iBAAiB,CAACgB,YAAY,CAACK,OAAO,CAACC,SAAS,CAAC,GACjD;EACN,CAAC,CACF;AACH;;AAEA;AACA;AACA;AACA,OAAO,SAASC,oBAAoBA,CAClCrB,KAAmB,EACS;EAC5B,OAAOA,KAAK,CACTE,MAAM,CAAEC,IAAI,IAAK,UAAU,IAAIA,IAAI,CAAC,CACpCmB,GAAG,CAAEnB,IAAI,KAAM;IACdK,IAAI,EAAEL,IAAI,CAACK,IAAI;IACfC,KAAK,EAAEN,IAAI,CAACO,KAAK;IACjBC,KAAK,EAAER,IAAI,CAACoB,QAAQ,CAACD,GAAG,CAAEE,WAAW,IACnCA,WAAW,CAACF,GAAG,CAAEG,OAAO,KAAM;MAC5BjB,IAAI,EAAEiB,OAAO,CAACjB,IAAI;MAClBC,KAAK,EAAEgB,OAAO,CAACf,KAAK;MACpBC,KAAK,EAAEf,SAAS,CAAC6B,OAAO,CAACpB,KAAK,EAAEoB,OAAO,CAACb,KAAK,EAAE;QAAEC,MAAM,EAAE;MAAO,CAAC;IACnE,CAAC,CAAC,CACJ;EACF,CAAC,CAAC,CAAC;AACP","ignoreList":[]}
1
+ {"version":3,"file":"submission.js","names":["GeospatialField","PaymentField","getAnswer","formatCurrency","formatPaymentDate","buildMainRecords","items","fieldItems","filter","item","records","field","push","buildPaymentRecords","name","title","label","value","JSON","stringify","getFormValueFromState","state","format","paymentState","getPaymentStateFromState","description","amount","reference","preAuth","createdAt","buildRepeaterRecords","map","subItems","detailItems","subItem"],"sources":["../../../../../../src/server/plugins/engine/pageControllers/helpers/submission.ts"],"sourcesContent":["import { type SubmitPayload } from '@defra/forms-model'\n\nimport { GeospatialField } from '~/src/server/plugins/engine/components/GeospatialField.js'\nimport { PaymentField } from '~/src/server/plugins/engine/components/PaymentField.js'\nimport { getAnswer } from '~/src/server/plugins/engine/components/helpers/components.js'\nimport {\n type DetailItem,\n type DetailItemField\n} from '~/src/server/plugins/engine/models/types.js'\nimport {\n formatCurrency,\n formatPaymentDate\n} from '~/src/server/plugins/payment/helper.js'\n\nexport interface SubmitRecord {\n name: string\n title: string\n value: string\n}\n\n/**\n * Builds the main submission records from field items.\n * Regular fields are converted to single records, while PaymentField\n * components are expanded into four separate records.\n */\nexport function buildMainRecords(items: DetailItem[]): SubmitRecord[] {\n const fieldItems = items.filter(\n (item): item is DetailItemField => 'field' in item\n )\n\n const records: SubmitRecord[] = []\n\n for (const item of fieldItems) {\n if (item.field instanceof PaymentField) {\n records.push(...buildPaymentRecords(item))\n } else if (item.field instanceof GeospatialField) {\n // Stringify of GeoJSON is done here rather than inside `getContextValueFromState`\n // so we don't incur the overhead of JSON.stringify on every request when building context\n records.push({\n name: item.name,\n title: item.label,\n value: JSON.stringify(item.field.getFormValueFromState(item.state))\n })\n } else {\n records.push({\n name: item.name,\n title: item.label,\n value: getAnswer(item.field, item.state, { format: 'data' })\n })\n }\n }\n\n return records\n}\n\n/**\n * Expands a PaymentField into four submission records:\n * - Payment description\n * - Payment amount (formatted with currency symbol)\n * - Payment reference\n * - Payment date (formatted date/time)\n *\n * Returns an empty array if no payment state exists.\n */\nexport function buildPaymentRecords(item: DetailItemField): SubmitRecord[] {\n const paymentState = (item.field as PaymentField).getPaymentStateFromState(\n item.state\n )\n\n if (!paymentState) {\n return []\n }\n\n return [\n {\n name: `${item.name}_paymentDescription`,\n title: 'Payment description',\n value: paymentState.description\n },\n {\n name: `${item.name}_paymentAmount`,\n title: 'Payment amount',\n value: formatCurrency(paymentState.amount)\n },\n {\n name: `${item.name}_paymentReference`,\n title: 'Payment reference',\n value: paymentState.reference\n },\n {\n name: `${item.name}_paymentDate`,\n title: 'Payment date',\n value: paymentState.preAuth?.createdAt\n ? formatPaymentDate(paymentState.preAuth.createdAt)\n : ''\n }\n ]\n}\n\n/**\n * Builds the repeater submission records from repeater items.\n */\nexport function buildRepeaterRecords(\n items: DetailItem[]\n): SubmitPayload['repeaters'] {\n return items\n .filter((item) => 'subItems' in item)\n .map((item) => ({\n name: item.name,\n title: item.label,\n value: item.subItems.map((detailItems) =>\n detailItems.map((subItem) => ({\n name: subItem.name,\n title: subItem.label,\n value:\n // Stringify of GeoJSON is done here rather than inside `getContextValueFromState`\n // so we don't incur the overhead of JSON.stringify on every request when building context\n subItem.field instanceof GeospatialField\n ? JSON.stringify(\n subItem.field.getFormValueFromState(subItem.state)\n )\n : getAnswer(subItem.field, subItem.state, { format: 'data' })\n }))\n )\n }))\n}\n"],"mappings":"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","ignoreList":[]}
@@ -57,7 +57,8 @@ export const messages = {
57
57
  'date.base': messageTemplate.dateFormat,
58
58
  'date.format': messageTemplate.dateFormat,
59
59
  'date.min': messageTemplate.dateMin,
60
- 'date.max': messageTemplate.dateMax
60
+ 'date.max': messageTemplate.dateMax,
61
+ 'object.invalidjson': messageTemplate.format
61
62
  };
62
63
  export const messagesPre = messages;
63
64
  export const validationOptions = {
@@ -1 +1 @@
1
- {"version":3,"file":"validationOptions.js","names":["joi","lowerFirstPreserveProperNouns","opts","functions","lowerFirst","messageTemplate","declarationRequired","expression","required","selectRequired","selectYesNoRequired","max","min","minMax","pattern","format","number","numberPrecision","numberInteger","numberMin","numberMax","maxWords","objectRequired","objectMissing","dateFormat","dateMin","dateMax","messages","messagesPre","validationOptions","abortEarly","errors","wrap","array","label"],"sources":["../../../../../src/server/plugins/engine/pageControllers/validationOptions.ts"],"sourcesContent":["// Declaration above is needed for: https://github.com/hapijs/joi/issues/3064\n\nimport joi, {\n type JoiExpression,\n type LanguageMessages,\n type LanguageMessagesExt,\n type ReferenceOptions,\n type ValidationOptions\n} from 'joi'\n\nimport { lowerFirstPreserveProperNouns } from '~/src/server/plugins/engine/components/helpers/index.js'\n\nconst opts = {\n functions: {\n lowerFirst: lowerFirstPreserveProperNouns\n }\n} as ReferenceOptions\n\n/**\n * see @link https://joi.dev/api/?v=17.4.2#template-syntax for template syntax\n */\nexport const messageTemplate: Record<string, JoiExpression> = {\n declarationRequired: joi.expression(\n 'You must confirm you understand and agree with the {{lowerFirst(#label)}} to continue',\n opts\n ) as JoiExpression,\n required: joi.expression(\n 'Enter {{lowerFirst(#label)}}',\n opts\n ) as JoiExpression,\n selectRequired: joi.expression(\n 'Select {{lowerFirst(#label)}}',\n opts\n ) as JoiExpression,\n selectYesNoRequired: '{{#label}} - select yes or no',\n max: '{{#label}} must be {{#limit}} characters or less',\n min: '{{#label}} must be {{#limit}} characters or more',\n minMax: '{{#label}} must be between {{#min}} and {{#max}} characters',\n pattern: joi.expression(\n 'Enter a valid {{lowerFirst(#label)}}',\n opts\n ) as JoiExpression,\n format: joi.expression(\n 'Enter {{lowerFirst(#label)}} in the correct format',\n opts\n ) as JoiExpression,\n number: '{{#label}} must be a number',\n numberPrecision: '{{#label}} must have {{#limit}} or fewer decimal places',\n numberInteger: '{{#label}} must be a whole number',\n numberMin: '{{#label}} must be {{#limit}} or higher',\n numberMax: '{{#label}} must be {{#limit}} or lower',\n maxWords: '{{#label}} must be {{#limit}} words or fewer',\n\n // Nested fields use component title\n\n objectRequired: joi.expression('Enter {{#label}}', opts) as JoiExpression,\n objectMissing: joi.expression(\n '{{#title}} must include a {{lowerFirst(#label)}}',\n opts\n ) as JoiExpression,\n dateFormat: '{{#title}} must be a real date',\n dateMin: '{{#title}} must be the same as or after {{#limit}}',\n dateMax: '{{#title}} must be the same as or before {{#limit}}'\n}\n\nexport const messages: LanguageMessagesExt = {\n 'string.base': messageTemplate.required,\n 'string.min': messageTemplate.min,\n 'string.empty': messageTemplate.required,\n 'string.max': messageTemplate.max,\n 'string.email': messageTemplate.format,\n 'string.pattern.base': messageTemplate.pattern,\n 'string.maxWords': messageTemplate.maxWords,\n\n 'number.base': messageTemplate.number,\n 'number.precision': messageTemplate.numberPrecision,\n 'number.integer': messageTemplate.numberInteger,\n 'number.unsafe': messageTemplate.format,\n 'number.min': messageTemplate.numberMin,\n 'number.max': messageTemplate.numberMax,\n\n 'object.required': messageTemplate.objectRequired,\n 'object.and': messageTemplate.objectMissing,\n\n 'any.only': messageTemplate.selectRequired,\n 'any.required': messageTemplate.selectRequired,\n 'any.empty': messageTemplate.required,\n\n 'date.base': messageTemplate.dateFormat,\n 'date.format': messageTemplate.dateFormat,\n 'date.min': messageTemplate.dateMin,\n 'date.max': messageTemplate.dateMax\n}\n\nexport const messagesPre: LanguageMessages =\n messages as unknown as LanguageMessages\n\nexport const validationOptions: ValidationOptions = {\n abortEarly: false,\n messages: messagesPre,\n errors: {\n wrap: {\n array: false,\n label: false\n }\n }\n}\n"],"mappings":"AAAA;;AAEA,OAAOA,GAAG,MAMH,KAAK;AAEZ,SAASC,6BAA6B;AAEtC,MAAMC,IAAI,GAAG;EACXC,SAAS,EAAE;IACTC,UAAU,EAAEH;EACd;AACF,CAAqB;;AAErB;AACA;AACA;AACA,OAAO,MAAMI,eAA8C,GAAG;EAC5DC,mBAAmB,EAAEN,GAAG,CAACO,UAAU,CACjC,uFAAuF,EACvFL,IACF,CAAkB;EAClBM,QAAQ,EAAER,GAAG,CAACO,UAAU,CACtB,8BAA8B,EAC9BL,IACF,CAAkB;EAClBO,cAAc,EAAET,GAAG,CAACO,UAAU,CAC5B,+BAA+B,EAC/BL,IACF,CAAkB;EAClBQ,mBAAmB,EAAE,+BAA+B;EACpDC,GAAG,EAAE,kDAAkD;EACvDC,GAAG,EAAE,kDAAkD;EACvDC,MAAM,EAAE,6DAA6D;EACrEC,OAAO,EAAEd,GAAG,CAACO,UAAU,CACrB,sCAAsC,EACtCL,IACF,CAAkB;EAClBa,MAAM,EAAEf,GAAG,CAACO,UAAU,CACpB,oDAAoD,EACpDL,IACF,CAAkB;EAClBc,MAAM,EAAE,6BAA6B;EACrCC,eAAe,EAAE,yDAAyD;EAC1EC,aAAa,EAAE,mCAAmC;EAClDC,SAAS,EAAE,yCAAyC;EACpDC,SAAS,EAAE,wCAAwC;EACnDC,QAAQ,EAAE,8CAA8C;EAExD;;EAEAC,cAAc,EAAEtB,GAAG,CAACO,UAAU,CAAC,kBAAkB,EAAEL,IAAI,CAAkB;EACzEqB,aAAa,EAAEvB,GAAG,CAACO,UAAU,CAC3B,kDAAkD,EAClDL,IACF,CAAkB;EAClBsB,UAAU,EAAE,gCAAgC;EAC5CC,OAAO,EAAE,oDAAoD;EAC7DC,OAAO,EAAE;AACX,CAAC;AAED,OAAO,MAAMC,QAA6B,GAAG;EAC3C,aAAa,EAAEtB,eAAe,CAACG,QAAQ;EACvC,YAAY,EAAEH,eAAe,CAACO,GAAG;EACjC,cAAc,EAAEP,eAAe,CAACG,QAAQ;EACxC,YAAY,EAAEH,eAAe,CAACM,GAAG;EACjC,cAAc,EAAEN,eAAe,CAACU,MAAM;EACtC,qBAAqB,EAAEV,eAAe,CAACS,OAAO;EAC9C,iBAAiB,EAAET,eAAe,CAACgB,QAAQ;EAE3C,aAAa,EAAEhB,eAAe,CAACW,MAAM;EACrC,kBAAkB,EAAEX,eAAe,CAACY,eAAe;EACnD,gBAAgB,EAAEZ,eAAe,CAACa,aAAa;EAC/C,eAAe,EAAEb,eAAe,CAACU,MAAM;EACvC,YAAY,EAAEV,eAAe,CAACc,SAAS;EACvC,YAAY,EAAEd,eAAe,CAACe,SAAS;EAEvC,iBAAiB,EAAEf,eAAe,CAACiB,cAAc;EACjD,YAAY,EAAEjB,eAAe,CAACkB,aAAa;EAE3C,UAAU,EAAElB,eAAe,CAACI,cAAc;EAC1C,cAAc,EAAEJ,eAAe,CAACI,cAAc;EAC9C,WAAW,EAAEJ,eAAe,CAACG,QAAQ;EAErC,WAAW,EAAEH,eAAe,CAACmB,UAAU;EACvC,aAAa,EAAEnB,eAAe,CAACmB,UAAU;EACzC,UAAU,EAAEnB,eAAe,CAACoB,OAAO;EACnC,UAAU,EAAEpB,eAAe,CAACqB;AAC9B,CAAC;AAED,OAAO,MAAME,WAA6B,GACxCD,QAAuC;AAEzC,OAAO,MAAME,iBAAoC,GAAG;EAClDC,UAAU,EAAE,KAAK;EACjBH,QAAQ,EAAEC,WAAW;EACrBG,MAAM,EAAE;IACNC,IAAI,EAAE;MACJC,KAAK,EAAE,KAAK;MACZC,KAAK,EAAE;IACT;EACF;AACF,CAAC","ignoreList":[]}
1
+ {"version":3,"file":"validationOptions.js","names":["joi","lowerFirstPreserveProperNouns","opts","functions","lowerFirst","messageTemplate","declarationRequired","expression","required","selectRequired","selectYesNoRequired","max","min","minMax","pattern","format","number","numberPrecision","numberInteger","numberMin","numberMax","maxWords","objectRequired","objectMissing","dateFormat","dateMin","dateMax","messages","messagesPre","validationOptions","abortEarly","errors","wrap","array","label"],"sources":["../../../../../src/server/plugins/engine/pageControllers/validationOptions.ts"],"sourcesContent":["// Declaration above is needed for: https://github.com/hapijs/joi/issues/3064\n\nimport joi, {\n type JoiExpression,\n type LanguageMessages,\n type LanguageMessagesExt,\n type ReferenceOptions,\n type ValidationOptions\n} from 'joi'\n\nimport { lowerFirstPreserveProperNouns } from '~/src/server/plugins/engine/components/helpers/index.js'\n\nconst opts = {\n functions: {\n lowerFirst: lowerFirstPreserveProperNouns\n }\n} as ReferenceOptions\n\n/**\n * see @link https://joi.dev/api/?v=17.4.2#template-syntax for template syntax\n */\nexport const messageTemplate: Record<string, JoiExpression> = {\n declarationRequired: joi.expression(\n 'You must confirm you understand and agree with the {{lowerFirst(#label)}} to continue',\n opts\n ) as JoiExpression,\n required: joi.expression(\n 'Enter {{lowerFirst(#label)}}',\n opts\n ) as JoiExpression,\n selectRequired: joi.expression(\n 'Select {{lowerFirst(#label)}}',\n opts\n ) as JoiExpression,\n selectYesNoRequired: '{{#label}} - select yes or no',\n max: '{{#label}} must be {{#limit}} characters or less',\n min: '{{#label}} must be {{#limit}} characters or more',\n minMax: '{{#label}} must be between {{#min}} and {{#max}} characters',\n pattern: joi.expression(\n 'Enter a valid {{lowerFirst(#label)}}',\n opts\n ) as JoiExpression,\n format: joi.expression(\n 'Enter {{lowerFirst(#label)}} in the correct format',\n opts\n ) as JoiExpression,\n number: '{{#label}} must be a number',\n numberPrecision: '{{#label}} must have {{#limit}} or fewer decimal places',\n numberInteger: '{{#label}} must be a whole number',\n numberMin: '{{#label}} must be {{#limit}} or higher',\n numberMax: '{{#label}} must be {{#limit}} or lower',\n maxWords: '{{#label}} must be {{#limit}} words or fewer',\n\n // Nested fields use component title\n\n objectRequired: joi.expression('Enter {{#label}}', opts) as JoiExpression,\n objectMissing: joi.expression(\n '{{#title}} must include a {{lowerFirst(#label)}}',\n opts\n ) as JoiExpression,\n dateFormat: '{{#title}} must be a real date',\n dateMin: '{{#title}} must be the same as or after {{#limit}}',\n dateMax: '{{#title}} must be the same as or before {{#limit}}'\n}\n\nexport const messages: LanguageMessagesExt = {\n 'string.base': messageTemplate.required,\n 'string.min': messageTemplate.min,\n 'string.empty': messageTemplate.required,\n 'string.max': messageTemplate.max,\n 'string.email': messageTemplate.format,\n 'string.pattern.base': messageTemplate.pattern,\n 'string.maxWords': messageTemplate.maxWords,\n\n 'number.base': messageTemplate.number,\n 'number.precision': messageTemplate.numberPrecision,\n 'number.integer': messageTemplate.numberInteger,\n 'number.unsafe': messageTemplate.format,\n 'number.min': messageTemplate.numberMin,\n 'number.max': messageTemplate.numberMax,\n\n 'object.required': messageTemplate.objectRequired,\n 'object.and': messageTemplate.objectMissing,\n\n 'any.only': messageTemplate.selectRequired,\n 'any.required': messageTemplate.selectRequired,\n 'any.empty': messageTemplate.required,\n\n 'date.base': messageTemplate.dateFormat,\n 'date.format': messageTemplate.dateFormat,\n 'date.min': messageTemplate.dateMin,\n 'date.max': messageTemplate.dateMax,\n\n 'object.invalidjson': messageTemplate.format\n}\n\nexport const messagesPre: LanguageMessages =\n messages as unknown as LanguageMessages\n\nexport const validationOptions: ValidationOptions = {\n abortEarly: false,\n messages: messagesPre,\n errors: {\n wrap: {\n array: false,\n label: false\n }\n }\n}\n"],"mappings":"AAAA;;AAEA,OAAOA,GAAG,MAMH,KAAK;AAEZ,SAASC,6BAA6B;AAEtC,MAAMC,IAAI,GAAG;EACXC,SAAS,EAAE;IACTC,UAAU,EAAEH;EACd;AACF,CAAqB;;AAErB;AACA;AACA;AACA,OAAO,MAAMI,eAA8C,GAAG;EAC5DC,mBAAmB,EAAEN,GAAG,CAACO,UAAU,CACjC,uFAAuF,EACvFL,IACF,CAAkB;EAClBM,QAAQ,EAAER,GAAG,CAACO,UAAU,CACtB,8BAA8B,EAC9BL,IACF,CAAkB;EAClBO,cAAc,EAAET,GAAG,CAACO,UAAU,CAC5B,+BAA+B,EAC/BL,IACF,CAAkB;EAClBQ,mBAAmB,EAAE,+BAA+B;EACpDC,GAAG,EAAE,kDAAkD;EACvDC,GAAG,EAAE,kDAAkD;EACvDC,MAAM,EAAE,6DAA6D;EACrEC,OAAO,EAAEd,GAAG,CAACO,UAAU,CACrB,sCAAsC,EACtCL,IACF,CAAkB;EAClBa,MAAM,EAAEf,GAAG,CAACO,UAAU,CACpB,oDAAoD,EACpDL,IACF,CAAkB;EAClBc,MAAM,EAAE,6BAA6B;EACrCC,eAAe,EAAE,yDAAyD;EAC1EC,aAAa,EAAE,mCAAmC;EAClDC,SAAS,EAAE,yCAAyC;EACpDC,SAAS,EAAE,wCAAwC;EACnDC,QAAQ,EAAE,8CAA8C;EAExD;;EAEAC,cAAc,EAAEtB,GAAG,CAACO,UAAU,CAAC,kBAAkB,EAAEL,IAAI,CAAkB;EACzEqB,aAAa,EAAEvB,GAAG,CAACO,UAAU,CAC3B,kDAAkD,EAClDL,IACF,CAAkB;EAClBsB,UAAU,EAAE,gCAAgC;EAC5CC,OAAO,EAAE,oDAAoD;EAC7DC,OAAO,EAAE;AACX,CAAC;AAED,OAAO,MAAMC,QAA6B,GAAG;EAC3C,aAAa,EAAEtB,eAAe,CAACG,QAAQ;EACvC,YAAY,EAAEH,eAAe,CAACO,GAAG;EACjC,cAAc,EAAEP,eAAe,CAACG,QAAQ;EACxC,YAAY,EAAEH,eAAe,CAACM,GAAG;EACjC,cAAc,EAAEN,eAAe,CAACU,MAAM;EACtC,qBAAqB,EAAEV,eAAe,CAACS,OAAO;EAC9C,iBAAiB,EAAET,eAAe,CAACgB,QAAQ;EAE3C,aAAa,EAAEhB,eAAe,CAACW,MAAM;EACrC,kBAAkB,EAAEX,eAAe,CAACY,eAAe;EACnD,gBAAgB,EAAEZ,eAAe,CAACa,aAAa;EAC/C,eAAe,EAAEb,eAAe,CAACU,MAAM;EACvC,YAAY,EAAEV,eAAe,CAACc,SAAS;EACvC,YAAY,EAAEd,eAAe,CAACe,SAAS;EAEvC,iBAAiB,EAAEf,eAAe,CAACiB,cAAc;EACjD,YAAY,EAAEjB,eAAe,CAACkB,aAAa;EAE3C,UAAU,EAAElB,eAAe,CAACI,cAAc;EAC1C,cAAc,EAAEJ,eAAe,CAACI,cAAc;EAC9C,WAAW,EAAEJ,eAAe,CAACG,QAAQ;EAErC,WAAW,EAAEH,eAAe,CAACmB,UAAU;EACvC,aAAa,EAAEnB,eAAe,CAACmB,UAAU;EACzC,UAAU,EAAEnB,eAAe,CAACoB,OAAO;EACnC,UAAU,EAAEpB,eAAe,CAACqB,OAAO;EAEnC,oBAAoB,EAAErB,eAAe,CAACU;AACxC,CAAC;AAED,OAAO,MAAMa,WAA6B,GACxCD,QAAuC;AAEzC,OAAO,MAAME,iBAAoC,GAAG;EAClDC,UAAU,EAAE,KAAK;EACjBH,QAAQ,EAAEC,WAAW;EACrBG,MAAM,EAAE;IACNC,IAAI,EAAE;MACJC,KAAK,EAAE,KAAK;MACZC,KAAK,EAAE;IACT;EACF;AACF,CAAC","ignoreList":[]}
@@ -75,7 +75,7 @@ export interface FormPayloadParams {
75
75
  * (after Joi has converted value types)
76
76
  */
77
77
  export type FormPayload = FormPayloadParams & Partial<Record<string, FormValue>>;
78
- export type FormValue = Item['value'] | Item['value'][] | UploadState | RepeatListState | undefined;
78
+ export type FormValue = Item['value'] | Item['value'][] | UploadState | RepeatListState | GeospatialState | undefined;
79
79
  export type FormState = Partial<Record<string, FormStateValue>>;
80
80
  export type FormStateValue = Exclude<FormValue, undefined> | null;
81
81
  export interface FormValidationResult<ValueType extends FormPayload | FormSubmissionState> {
@@ -196,6 +196,67 @@ export interface RepeatItemState extends FormPayload {
196
196
  itemId: string;
197
197
  }
198
198
  export type RepeatListState = RepeatItemState[];
199
+ /**
200
+ * A longitude/latitude coordinate pair in WGS84 format
201
+ * Format: [longitude, latitude]
202
+ */
203
+ export type Coordinates = [longitude: number, latitude: number];
204
+ /**
205
+ * GeoJSON Point geometry
206
+ */
207
+ export interface PointGeometry {
208
+ type: 'Point';
209
+ coordinates: Coordinates;
210
+ }
211
+ /**
212
+ * GeoJSON LineString geometry
213
+ */
214
+ export interface LineStringGeometry {
215
+ type: 'LineString';
216
+ coordinates: Coordinates[];
217
+ }
218
+ /**
219
+ * GeoJSON Polygon geometry
220
+ */
221
+ export interface PolygonGeometry {
222
+ type: 'Polygon';
223
+ coordinates: Coordinates[][];
224
+ }
225
+ /**
226
+ * Supported geometry types
227
+ */
228
+ export type Geometry = PointGeometry | LineStringGeometry | PolygonGeometry;
229
+ /**
230
+ * Feature metadata
231
+ */
232
+ export interface FeatureProperties {
233
+ /**
234
+ * Human-readable description of the feature
235
+ */
236
+ description: string;
237
+ /**
238
+ * The OS grid reference of the first coordinate of the feature
239
+ */
240
+ coordinateGridReference?: string;
241
+ /**
242
+ * The OS grid reference of the centroid of the feature
243
+ */
244
+ centroidGridReference?: string;
245
+ }
246
+ /**
247
+ * A single GeoJSON Feature
248
+ */
249
+ export interface Feature {
250
+ id: string;
251
+ type: 'Feature';
252
+ properties: FeatureProperties;
253
+ geometry: Geometry;
254
+ }
255
+ /**
256
+ * A GeoJSON FeatureCollection
257
+ */
258
+ export type FeatureCollection = Feature[];
259
+ export type GeospatialState = FeatureCollection;
199
260
  export interface CheckAnswers {
200
261
  title?: ComponentText;
201
262
  summaryList: SummaryList;
@@ -360,7 +421,7 @@ export type FileUploadFieldDetailitem = Omit<DetailItemField, 'field'> & {
360
421
  export type PaymentFieldDetailItem = Omit<DetailItemField, 'field'> & {
361
422
  field: PaymentField;
362
423
  };
363
- export type RichFormValue = FormValue | FormPayload | DatePartsState | MonthYearState | UkAddressState | EastingNorthingState | LatLongState;
424
+ export type RichFormValue = FormValue | FormPayload | DatePartsState | MonthYearState | UkAddressState | EastingNorthingState | LatLongState | GeospatialState;
364
425
  export interface FormAdapterSubmissionMessageData {
365
426
  main: Record<string, RichFormValue | null>;
366
427
  repeaters: Record<string, Record<string, RichFormValue>[]>;
@@ -40,6 +40,39 @@
40
40
 
41
41
  export { FileStatus, UploadStatus } from "./types/enums.js";
42
42
 
43
+ /**
44
+ * A longitude/latitude coordinate pair in WGS84 format
45
+ * Format: [longitude, latitude]
46
+ */
47
+
48
+ /**
49
+ * GeoJSON Point geometry
50
+ */
51
+
52
+ /**
53
+ * GeoJSON LineString geometry
54
+ */
55
+
56
+ /**
57
+ * GeoJSON Polygon geometry
58
+ */
59
+
60
+ /**
61
+ * Supported geometry types
62
+ */
63
+
64
+ /**
65
+ * Feature metadata
66
+ */
67
+
68
+ /**
69
+ * A single GeoJSON Feature
70
+ */
71
+
72
+ /**
73
+ * A GeoJSON FeatureCollection
74
+ */
75
+
43
76
  /**
44
77
  * A detail item specifically for files
45
78
  */
@@ -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\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 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 | undefined\n\nexport type FormState = Partial<Record<string, FormStateValue>>\nexport type FormStateValue = Exclude<FormValue, undefined> | null\n\nexport interface FormValidationResult<\n ValueType extends FormPayload | FormSubmissionState\n> {\n value: ValueType\n errors: FormSubmissionError[] | undefined\n}\n\nexport interface FormContext {\n /**\n * Evaluation form state only (filtered by visited paths),\n * with values formatted for condition evaluation using\n * {@link FormComponent.getContextValueFromState}\n */\n evaluationState: FormState\n\n /**\n * Relevant form state only (filtered by visited paths)\n */\n relevantState: FormState\n\n /**\n * Relevant pages only (filtered by visited paths)\n */\n relevantPages: PageControllerClass[]\n\n /**\n * Form submission payload (single page)\n */\n payload: FormPayload\n\n /**\n * Form submission state (entire form)\n */\n state: FormSubmissionState\n\n /**\n * Validation errors (entire form)\n */\n errors?: FormSubmissionError[]\n\n /**\n * Visited paths evaluated from form state\n */\n paths: string[]\n\n /**\n * Preview URL direct access is allowed\n */\n isForceAccess: boolean\n\n /**\n * Miscellaneous extra data from event responses\n */\n data: object\n\n pageDefMap: Map<string, Page>\n listDefMap: Map<string, List>\n componentDefMap: Map<string, ComponentDef>\n pageMap: Map<string, PageControllerClass>\n componentMap: Map<string, Component>\n referenceNumber: string\n submittedVersionNumber?: number\n}\n\nexport type FormContextRequest = (\n | {\n method: 'get'\n payload?: undefined\n }\n | {\n method: 'post'\n payload: FormPayload\n }\n | {\n method: FormRequest['method']\n payload?: object | undefined\n }\n) &\n Pick<\n FormRequest,\n 'app' | 'method' | 'params' | 'path' | 'query' | 'url' | 'server'\n >\n\nexport interface UploadInitiateResponse {\n uploadId: string\n uploadUrl: string\n statusUrl: string\n}\n\nexport {\n FileStatus,\n UploadStatus\n} from '~/src/server/plugins/engine/types/enums.js'\n\nexport type UploadState = FileState[]\n\nexport type FileUpload = {\n fileId: string\n filename: string\n contentLength: number\n} & (\n | {\n fileStatus: FileStatus.complete | FileStatus.rejected | FileStatus.pending\n errorMessage?: string\n }\n | {\n fileStatus: FileStatus.complete\n errorMessage?: undefined\n }\n)\n\nexport interface FileUploadMetadata {\n retrievalKey: string\n}\n\nexport type UploadStatusResponse =\n | {\n uploadStatus: UploadStatus.initiated\n metadata: FileUploadMetadata\n form: { file?: undefined }\n }\n | {\n uploadStatus: UploadStatus.pending | UploadStatus.ready\n metadata: FileUploadMetadata\n form: { file: FileUpload }\n numberOfRejectedFiles?: number\n }\n | {\n uploadStatus: UploadStatus.ready\n metadata: FileUploadMetadata\n form: { file: FileUpload }\n numberOfRejectedFiles: 0\n }\n\nexport type UploadStatusFileResponse = Exclude<\n UploadStatusResponse,\n { uploadStatus: UploadStatus.initiated }\n>\n\nexport interface FileState {\n uploadId: string\n status: UploadStatusFileResponse\n}\n\nexport interface TempFileState {\n upload?: UploadInitiateResponse\n files: UploadState\n}\n\nexport interface RepeatItemState extends FormPayload {\n itemId: string\n}\n\nexport type RepeatListState = RepeatItemState[]\n\nexport interface CheckAnswers {\n title?: ComponentText\n summaryList: SummaryList\n}\n\nexport interface SummaryList {\n classes?: string\n rows: SummaryListRow[]\n}\n\nexport interface SummaryListRow {\n key: ComponentText\n value: ComponentText\n actions?: { items: SummaryListAction[] }\n}\n\nexport type SummaryListAction = ComponentText & {\n href: string\n visuallyHiddenText: string\n}\n\nexport interface PageViewModelBase extends Partial<ViewContext> {\n page: PageController\n name?: string\n pageTitle: string\n sectionTitle?: string\n showTitle: boolean\n isStartPage: boolean\n backLink?: BackLink\n feedbackLink?: string\n serviceUrl: string\n phaseTag?: string\n}\n\nexport interface ItemDeletePageViewModel extends PageViewModelBase {\n context: FormContext\n itemTitle: string\n confirmation?: ComponentText\n buttonConfirm: ComponentText\n buttonCancel: ComponentText\n}\n\nexport interface FormPageViewModel extends PageViewModelBase {\n components: ComponentViewModel[]\n context: FormContext\n errors?: FormSubmissionError[]\n hasMissingNotificationEmail?: boolean\n allowSaveAndExit: boolean\n 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\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\n 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;;AAyBA;AACA;AACA;AACA;;AAsGA,SACEA,UAAU,EACVC,YAAY;;AAoQd;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 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":[]}
@@ -0,0 +1,7 @@
1
+ {% from "govuk/components/textarea/macro.njk" import govukTextarea %}
2
+
3
+ {% macro GeospatialField(component) %}
4
+ <div class="app-geospatial-field">
5
+ {{ govukTextarea(component.model) }}
6
+ </div>
7
+ {% endmacro %}
@@ -1 +1 @@
1
- {"version":3,"file":"context.test.js","names":["tmpdir","context","devtoolContext","describe","beforeEach","jest","resetModules","it","assetPath","expect","toBe","getDxtAssetPath","isolateModulesAsync","config","set","rejects","toThrow","malformedRequest","server","plugins","crumb","generate","fn","baseLayoutPath","route","settings","path","url","search","yar","flash","mockReturnValue","commit","toBeUndefined","not","toHaveBeenCalled","mockCrumb","validRequest","state","toHaveBeenCalledWith"],"sources":["../../../../src/server/plugins/nunjucks/context.test.js"],"sourcesContent":["import { tmpdir } from 'node:os'\n\nimport {\n context,\n devtoolContext\n} from '~/src/server/plugins/nunjucks/context.js'\n\ndescribe('Nunjucks context', () => {\n beforeEach(() => jest.resetModules())\n\n describe('Asset path', () => {\n it(\"should include 'assetPath' for GOV.UK Frontend icons\", () => {\n const { assetPath } = devtoolContext(null)\n expect(assetPath).toBe('/assets')\n })\n })\n\n describe('Asset helper', () => {\n it(\"should locate 'assets-manifest.json' assets\", () => {\n const { getDxtAssetPath } = devtoolContext(null)\n\n expect(getDxtAssetPath('example.scss')).toBe(\n '/stylesheets/example.xxxxxxx.min.css'\n )\n\n expect(getDxtAssetPath('example.mjs')).toBe(\n '/javascripts/example.xxxxxxx.min.js'\n )\n })\n\n it(\"should return path when 'assets-manifest.json' is missing\", async () => {\n await jest.isolateModulesAsync(async () => {\n const { config } = await import('~/src/config/index.js')\n\n // Import when isolated to avoid cache\n const { devtoolContext } = await import(\n '~/src/server/plugins/nunjucks/context.js'\n )\n\n // Update config for missing manifest\n config.set('publicDir', tmpdir())\n const { getDxtAssetPath } = devtoolContext(null)\n\n // Uses original paths when missing\n expect(getDxtAssetPath('example.scss')).toBe('/example.scss')\n expect(getDxtAssetPath('example.mjs')).toBe('/example.mjs')\n })\n })\n\n it('should return path to unknown assets', () => {\n const { getDxtAssetPath } = devtoolContext(null)\n\n expect(getDxtAssetPath('')).toBe('/')\n expect(getDxtAssetPath('example.jpg')).toBe('/example.jpg')\n expect(getDxtAssetPath('example.gif')).toBe('/example.gif')\n })\n })\n\n describe('Config', () => {\n it('should include environment, phase tag and service info', async () => {\n await expect(context(null)).rejects.toThrow(\n 'context called before plugin registered'\n )\n })\n })\n\n describe('Crumb', () => {\n it('should handle malformed requests with missing state', async () => {\n // While state should always exist in a valid Hapi request (it holds cookies),\n // we've seen malformed requests in production where it's missing\n const malformedRequest = /** @type {FormRequest} */ (\n /** @type {unknown} */ ({\n server: {\n plugins: {\n crumb: {\n generate: jest.fn()\n },\n 'forms-engine-plugin': {\n baseLayoutPath: 'randomValue'\n }\n }\n },\n plugins: {},\n route: {\n settings: {\n plugins: {}\n }\n },\n path: '/test',\n url: { search: '' },\n yar: {\n flash: jest.fn().mockReturnValue([]),\n commit: jest.fn()\n }\n // state intentionally omitted to test real malformed requests\n })\n )\n\n const { crumb } = await context(malformedRequest)\n expect(crumb).toBeUndefined()\n expect(\n malformedRequest.server.plugins.crumb.generate\n ).not.toHaveBeenCalled()\n })\n\n it('should generate crumb when state exists', async () => {\n const mockCrumb = 'generated-crumb-value'\n const validRequest = /** @type {FormRequest} */ (\n /** @type {unknown} */ ({\n server: {\n plugins: {\n crumb: {\n generate: jest.fn().mockReturnValue(mockCrumb)\n },\n 'forms-engine-plugin': {\n baseLayoutPath: 'randomValue'\n }\n }\n },\n plugins: {},\n route: {\n settings: {\n plugins: {}\n }\n },\n path: '/test',\n url: { search: '' },\n state: {},\n yar: {\n flash: jest.fn().mockReturnValue([]),\n commit: jest.fn()\n }\n })\n )\n\n const { crumb } = await context(validRequest)\n expect(crumb).toBe(mockCrumb)\n expect(validRequest.server.plugins.crumb.generate).toHaveBeenCalledWith(\n validRequest\n )\n })\n })\n})\n\n/**\n * @import { FormRequest } from '~/src/server/routes/types.js'\n */\n"],"mappings":"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","ignoreList":[]}
1
+ {"version":3,"file":"context.test.js","names":["tmpdir","context","devtoolContext","describe","beforeEach","jest","resetModules","it","assetPath","expect","toBe","getDxtAssetPath","isolateModulesAsync","config","set","rejects","toThrow","malformedRequest","server","plugins","crumb","generate","fn","baseLayoutPath","route","settings","path","url","search","yar","flash","mockReturnValue","commit","toBeUndefined","not","toHaveBeenCalled","mockCrumb","validRequest","state","toHaveBeenCalledWith"],"sources":["../../../../src/server/plugins/nunjucks/context.test.js"],"sourcesContent":["import { tmpdir } from 'node:os'\n\nimport {\n context,\n devtoolContext\n} from '~/src/server/plugins/nunjucks/context.js'\n\ndescribe('Nunjucks context', () => {\n beforeEach(() => jest.resetModules())\n\n describe('Asset path', () => {\n it(\"should include 'assetPath' for GOV.UK Frontend icons\", () => {\n const { assetPath } = devtoolContext(null)\n expect(assetPath).toBe('/assets')\n })\n })\n\n describe('Asset helper', () => {\n it(\"should locate 'assets-manifest.json' assets\", () => {\n const { getDxtAssetPath } = devtoolContext(null)\n\n expect(getDxtAssetPath('example.scss')).toBe(\n '/stylesheets/example.xxxxxxx.min.css'\n )\n\n expect(getDxtAssetPath('example.mjs')).toBe(\n '/javascripts/example.xxxxxxx.min.js'\n )\n })\n\n it(\"should return path when 'assets-manifest.json' is missing\", async () => {\n await jest.isolateModulesAsync(async () => {\n const { config } = await import('~/src/config/index.js')\n\n // Import when isolated to avoid cache\n const { devtoolContext } =\n await import('~/src/server/plugins/nunjucks/context.js')\n\n // Update config for missing manifest\n config.set('publicDir', tmpdir())\n const { getDxtAssetPath } = devtoolContext(null)\n\n // Uses original paths when missing\n expect(getDxtAssetPath('example.scss')).toBe('/example.scss')\n expect(getDxtAssetPath('example.mjs')).toBe('/example.mjs')\n })\n })\n\n it('should return path to unknown assets', () => {\n const { getDxtAssetPath } = devtoolContext(null)\n\n expect(getDxtAssetPath('')).toBe('/')\n expect(getDxtAssetPath('example.jpg')).toBe('/example.jpg')\n expect(getDxtAssetPath('example.gif')).toBe('/example.gif')\n })\n })\n\n describe('Config', () => {\n it('should include environment, phase tag and service info', async () => {\n await expect(context(null)).rejects.toThrow(\n 'context called before plugin registered'\n )\n })\n })\n\n describe('Crumb', () => {\n it('should handle malformed requests with missing state', async () => {\n // While state should always exist in a valid Hapi request (it holds cookies),\n // we've seen malformed requests in production where it's missing\n const malformedRequest = /** @type {FormRequest} */ (\n /** @type {unknown} */ ({\n server: {\n plugins: {\n crumb: {\n generate: jest.fn()\n },\n 'forms-engine-plugin': {\n baseLayoutPath: 'randomValue'\n }\n }\n },\n plugins: {},\n route: {\n settings: {\n plugins: {}\n }\n },\n path: '/test',\n url: { search: '' },\n yar: {\n flash: jest.fn().mockReturnValue([]),\n commit: jest.fn()\n }\n // state intentionally omitted to test real malformed requests\n })\n )\n\n const { crumb } = await context(malformedRequest)\n expect(crumb).toBeUndefined()\n expect(\n malformedRequest.server.plugins.crumb.generate\n ).not.toHaveBeenCalled()\n })\n\n it('should generate crumb when state exists', async () => {\n const mockCrumb = 'generated-crumb-value'\n const validRequest = /** @type {FormRequest} */ (\n /** @type {unknown} */ ({\n server: {\n plugins: {\n crumb: {\n generate: jest.fn().mockReturnValue(mockCrumb)\n },\n 'forms-engine-plugin': {\n baseLayoutPath: 'randomValue'\n }\n }\n },\n plugins: {},\n route: {\n settings: {\n plugins: {}\n }\n },\n path: '/test',\n url: { search: '' },\n state: {},\n yar: {\n flash: jest.fn().mockReturnValue([]),\n commit: jest.fn()\n }\n })\n )\n\n const { crumb } = await context(validRequest)\n expect(crumb).toBe(mockCrumb)\n expect(validRequest.server.plugins.crumb.generate).toHaveBeenCalledWith(\n validRequest\n )\n })\n })\n})\n\n/**\n * @import { FormRequest } from '~/src/server/routes/types.js'\n */\n"],"mappings":"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","ignoreList":[]}