@defra/forms-engine-plugin 4.4.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.
@@ -247,10 +247,13 @@ function pollUploadStatus(uploadId) {
247
247
  * @param {HTMLInputElement} fileInput - The file input element
248
248
  * @param {HTMLButtonElement} uploadButton - The upload button
249
249
  * @param {HTMLButtonElement} continueButton - The continue button
250
- * @param {File | null} selectedFile - The selected file
250
+ * @param {File[]} selectedFiles - The selected files
251
251
  */
252
- function handleStandardFormSubmission(formElement, fileInput, uploadButton, continueButton, selectedFile) {
253
- renderSummary(selectedFile, 'Uploading…', formElement);
252
+ function handleStandardFormSubmission(formElement, fileInput, uploadButton, continueButton, selectedFiles) {
253
+ // Render in reverse so first file ends up at the top of the summary list
254
+ for (let i = selectedFiles.length - 1; i >= 0; i--) {
255
+ renderSummary(selectedFiles[i], 'Uploading…', formElement);
256
+ }
254
257
  fileInput.focus();
255
258
  setTimeout(() => {
256
259
  fileInput.disabled = true;
@@ -309,8 +312,8 @@ function initUpload() {
309
312
  return;
310
313
  }
311
314
  const formElement = /** @type {HTMLFormElement} */form;
312
- /** @type {File | null} */
313
- let selectedFile = null;
315
+ /** @type {File[]} */
316
+ let selectedFiles = [];
314
317
  let isSubmitting = false;
315
318
  const uploadId = formElement.dataset.uploadId;
316
319
  fileInput.addEventListener('change', () => {
@@ -318,11 +321,11 @@ function initUpload() {
318
321
  errorSummary.innerHTML = '';
319
322
  }
320
323
  if (fileInput.files && fileInput.files.length > 0) {
321
- selectedFile = fileInput.files[0];
324
+ selectedFiles = Array.from(fileInput.files);
322
325
  }
323
326
  });
324
327
  uploadButton.addEventListener('click', event => {
325
- if (!selectedFile) {
328
+ if (selectedFiles.length === 0) {
326
329
  event.preventDefault();
327
330
  showError('Select a file', /** @type {HTMLElement | null} */errorSummary, fileInput);
328
331
  return;
@@ -332,7 +335,9 @@ function initUpload() {
332
335
  return;
333
336
  }
334
337
  isSubmitting = true;
335
- handleStandardFormSubmission(formElement, fileInput, uploadButton, continueButton, selectedFile);
338
+
339
+ // Show all selected files in the summary table
340
+ handleStandardFormSubmission(formElement, fileInput, uploadButton, continueButton, selectedFiles);
336
341
  handleAjaxFormSubmission(event, formElement, fileInput, uploadButton, /** @type {HTMLElement | null} */errorSummary, uploadId);
337
342
  });
338
343
  }
@@ -1 +1 @@
1
- {"version":3,"file":"file-upload.js","names":["MAX_POLLING_DURATION","ARIA_DESCRIBEDBY","ERROR_SUMMARY_TITLE_ID","createOrUpdateStatusAnnouncer","form","fileCountP","statusAnnouncer","querySelector","document","createElement","id","className","setAttribute","addStatusAnnouncerToDOM","asHTMLElement","appendChild","body","nextSibling","parentNode","insertBefore","parentElement","findOrCreateSummaryList","summaryList","buttonGroup","createFileRow","selectedFile","statusText","row","name","innerHTML","renderSummary","container","getElementById","uploadForm","closest","HTMLFormElement","fileInput","existingRow","remove","firstChild","textContent","showError","message","errorSummary","topErrorSummary","titleElement","removeAttribute","formGroup","classList","add","inputId","errorMessage","element","reloadPage","window","history","replaceState","location","href","pathname","buildUploadStatusUrl","uploadId","normalisedPath","replace","segments","split","filter","Boolean","prefix","length","slice","join","pollUploadStatus","attempts","interval","setInterval","clearInterval","uploadStatusUrl","fetch","headers","Accept","then","response","ok","Error","json","data","uploadStatus","catch","handleStandardFormSubmission","formElement","uploadButton","continueButton","focus","setTimeout","disabled","handleAjaxFormSubmission","event","action","preventDefault","formData","FormData","isLocalDev","dataset","proxyUrl","uploadUrl","fetchOptions","method","redirect","mode","initUpload","Array","from","querySelectorAll","find","button","trim","isSubmitting","addEventListener","files","initFileUpload","readyState"],"sources":["../../../src/client/javascripts/file-upload.js"],"sourcesContent":["export const MAX_POLLING_DURATION = 300 // 5 minutes\nconst ARIA_DESCRIBEDBY = 'aria-describedby'\nconst ERROR_SUMMARY_TITLE_ID = 'error-summary-title'\n\n/**\n * Creates or updates status announcer for screen readers\n * @param {HTMLElement | null} form - The form element\n * @param {HTMLElement | null} fileCountP - The file count paragraph element\n * @returns {HTMLElement} The status announcer element\n */\nfunction createOrUpdateStatusAnnouncer(form, fileCountP) {\n let statusAnnouncer = form?.querySelector('#statusInformation')\n\n if (!statusAnnouncer) {\n statusAnnouncer = document.createElement('div')\n statusAnnouncer.id = 'statusInformation'\n statusAnnouncer.className = 'govuk-visually-hidden'\n statusAnnouncer.setAttribute('aria-live', 'polite')\n\n // multiple fallbacks to ensure the status announcer is always added to the DOM\n // this helps with cross-browser compatibility and unexpected DOM structures encountered during QA\n try {\n addStatusAnnouncerToDOM(\n asHTMLElement(form),\n asHTMLElement(fileCountP),\n asHTMLElement(statusAnnouncer)\n )\n } catch {\n try {\n form?.appendChild(statusAnnouncer)\n } catch {\n document.body.appendChild(statusAnnouncer)\n }\n }\n }\n\n return /** @type {HTMLElement} */ (statusAnnouncer)\n}\n\n/**\n * Helper function to add the status announcer to the DOM\n * @param {HTMLElement} form - The form element\n * @param {HTMLElement | null} fileCountP - The file count paragraph element\n * @param {HTMLElement} statusAnnouncer - The status announcer element to add\n */\nfunction addStatusAnnouncerToDOM(form, fileCountP, statusAnnouncer) {\n if (fileCountP?.nextSibling && fileCountP.parentNode === form) {\n form.insertBefore(statusAnnouncer, fileCountP.nextSibling)\n return\n }\n\n const parentElement = fileCountP?.parentNode ?? form\n parentElement.appendChild(statusAnnouncer)\n}\n\n/**\n * Finds an existing summary list or creates a new one\n * @param {HTMLFormElement} form - The form element\n * @param {HTMLElement} fileCountP - The file count paragraph element\n * @returns {HTMLElement} The summary list element\n */\nfunction findOrCreateSummaryList(form, fileCountP) {\n let summaryList = form.querySelector('dl.govuk-summary-list')\n\n if (!summaryList) {\n summaryList = document.createElement('dl')\n summaryList.className = 'govuk-summary-list govuk-summary-list--long-key'\n\n const buttonGroup = form.querySelector('.govuk-button-group')\n\n if (buttonGroup) {\n form.insertBefore(summaryList, buttonGroup)\n } else {\n form.insertBefore(summaryList, fileCountP.nextSibling)\n }\n }\n\n return /** @type {HTMLElement} */ (summaryList)\n}\n\n/**\n * Creates a file row element for the summary list\n * @param {File | null} selectedFile - The selected file\n * @param {string} statusText - The status to display\n * @returns {HTMLElement} The created row element\n */\nfunction createFileRow(selectedFile, statusText) {\n const row = document.createElement('div')\n row.className = 'govuk-summary-list__row'\n row.setAttribute('data-filename', selectedFile?.name ?? '')\n row.innerHTML = `\n <dt class=\"govuk-summary-list__key\">\n ${selectedFile?.name ?? ''}\n </dt>\n <dd class=\"govuk-summary-list__value\">\n <strong class=\"govuk-tag govuk-tag--yellow\">${statusText}</strong>\n </dd>\n <dd class=\"govuk-summary-list__actions\">\n </dd>\n `\n return row\n}\n\n/**\n * Renders or updates the file summary box for the selected file\n * @param {File | null} selectedFile - The selected file\n * @param {string} statusText - The status to display\n * @param {HTMLElement} form - The form element\n */\nfunction renderSummary(selectedFile, statusText, form) {\n const container = document.getElementById('uploadedFilesContainer')\n const uploadForm = container ? container.closest('form') : null\n\n if (!uploadForm || !(uploadForm instanceof HTMLFormElement)) {\n return\n }\n\n const fileCountP = uploadForm.querySelector('p.govuk-body')\n\n if (!fileCountP) {\n return\n }\n\n const statusAnnouncer = createOrUpdateStatusAnnouncer(\n /** @type {HTMLElement} */ (uploadForm),\n /** @type {HTMLElement | null} */ (fileCountP)\n )\n\n const fileInput = form.querySelector('input[type=\"file\"]')\n\n if (fileInput) {\n fileInput.setAttribute(ARIA_DESCRIBEDBY, 'statusInformation')\n }\n\n const summaryList = findOrCreateSummaryList(\n /** @type {HTMLFormElement} */ (uploadForm),\n /** @type {HTMLElement} */ (fileCountP)\n )\n\n const existingRow = document.querySelector(\n `[data-filename=\"${selectedFile?.name}\"]`\n )\n\n if (existingRow) {\n existingRow.remove()\n }\n\n const row = createFileRow(selectedFile, statusText)\n summaryList.insertBefore(row, summaryList.firstChild)\n statusAnnouncer.textContent = `${selectedFile?.name ?? ''} ${statusText}`\n}\n\n/**\n * Shows an error message using the GOV.UK error summary component\n * and adds inline error styling to the file input\n * @param {string} message - The error message to display\n * @param {HTMLElement | null} errorSummary - The error summary container\n * @param {HTMLInputElement} fileInput - The file input element\n * @returns {void}\n */\nfunction showError(message, errorSummary, fileInput) {\n const topErrorSummary = document.querySelector('.govuk-error-summary')\n\n if (topErrorSummary) {\n const titleElement = document.getElementById(ERROR_SUMMARY_TITLE_ID)\n if (titleElement) {\n fileInput.setAttribute(ARIA_DESCRIBEDBY, ERROR_SUMMARY_TITLE_ID)\n } else {\n fileInput.removeAttribute(ARIA_DESCRIBEDBY)\n }\n return\n }\n\n if (errorSummary) {\n errorSummary.innerHTML = `\n <div class=\"govuk-error-summary\" data-module=\"govuk-error-summary\">\n <div role=\"alert\">\n <h2 class=\"govuk-error-summary__title\" id=\"${ERROR_SUMMARY_TITLE_ID}\">\n There is a problem\n </h2>\n <div class=\"govuk-error-summary__body\">\n <ul class=\"govuk-list govuk-error-summary__list\">\n <li>\n <a href=\"#file-upload\">${message}</a>\n </li>\n </ul>\n </div>\n </div>\n </div>\n `\n\n fileInput.setAttribute(ARIA_DESCRIBEDBY, ERROR_SUMMARY_TITLE_ID)\n }\n\n const formGroup = fileInput.closest('.govuk-form-group')\n if (formGroup) {\n formGroup.classList.add('govuk-form-group--error')\n fileInput.classList.add('govuk-file-upload--error')\n\n const inputId = fileInput.id\n let errorMessage = document.getElementById(`${inputId}-error`)\n\n if (!errorMessage) {\n errorMessage = document.createElement('p')\n errorMessage.id = `${inputId}-error`\n errorMessage.className = 'govuk-error-message'\n errorMessage.innerHTML = `<span class=\"govuk-visually-hidden\">Error:</span> ${message}`\n formGroup.insertBefore(errorMessage, fileInput)\n }\n\n fileInput.setAttribute(\n ARIA_DESCRIBEDBY,\n `error-summary-title ${inputId}-error`\n )\n }\n}\n\n/**\n * Helper to safely convert an Element to HTMLElement\n * @param {Element | null} element - The element to convert\n */\nfunction asHTMLElement(element) {\n return /** @type {HTMLElement} */ (element)\n}\n\nfunction reloadPage() {\n window.history.replaceState(null, '', window.location.href)\n window.location.href = window.location.pathname\n}\n\n/**\n * Build the upload status URL given the current pathname and the upload ID.\n * This only works when called on a file upload page that has a maximum depth of 1 URL segments following the slug.\n * @param {string} pathname – e.g. window.location.pathname\n * @param {string} uploadId\n * @returns {string} e.g. \"/form/upload-status/abc123\"\n */\nexport function buildUploadStatusUrl(pathname, uploadId) {\n // Remove preview markers and duplicate slashes\n const normalisedPath = pathname\n .replace(/\\/preview\\/(draft|live)/g, '')\n .replace(/\\/{2,}/g, '/')\n .replace(/\\/$/, '')\n\n const segments = normalisedPath.split('/').filter(Boolean)\n\n // The slug is always the second to last segment\n // The prefix is everything before the slug\n const prefix =\n segments.length > 2\n ? `/${segments.slice(0, segments.length - 2).join('/')}`\n : ''\n\n return `${prefix}/upload-status/${uploadId}`\n}\n\n/**\n * Polls the upload status endpoint until the file is ready or timeout occurs\n * @param {string} uploadId - The upload ID to check\n */\nfunction pollUploadStatus(uploadId) {\n let attempts = 0\n const interval = setInterval(() => {\n attempts++\n\n if (attempts >= MAX_POLLING_DURATION) {\n clearInterval(interval)\n reloadPage()\n return\n }\n\n const uploadStatusUrl = buildUploadStatusUrl(\n window.location.pathname,\n uploadId\n )\n\n fetch(uploadStatusUrl, {\n headers: {\n Accept: 'application/json'\n }\n })\n .then((response) => {\n if (!response.ok) {\n throw new Error('Network response was not ok')\n }\n return response.json()\n })\n .then((data) => {\n if (data.uploadStatus === 'ready') {\n clearInterval(interval)\n reloadPage()\n }\n })\n .catch(() => {\n clearInterval(interval)\n reloadPage()\n })\n }, 1000)\n}\n\n/**\n * Handle standard form submission for file upload\n * @param {HTMLFormElement} formElement - The form element\n * @param {HTMLInputElement} fileInput - The file input element\n * @param {HTMLButtonElement} uploadButton - The upload button\n * @param {HTMLButtonElement} continueButton - The continue button\n * @param {File | null} selectedFile - The selected file\n */\nfunction handleStandardFormSubmission(\n formElement,\n fileInput,\n uploadButton,\n continueButton,\n selectedFile\n) {\n renderSummary(selectedFile, 'Uploading…', formElement)\n\n fileInput.focus()\n\n setTimeout(() => {\n fileInput.disabled = true\n uploadButton.disabled = true\n continueButton.disabled = true\n }, 100)\n}\n\n/**\n * Handle AJAX form submission with upload ID\n * @param {Event} event - The click event\n * @param {HTMLFormElement} formElement - The form element\n * @param {HTMLInputElement} fileInput - The file input element\n * @param {HTMLButtonElement} uploadButton - The upload button\n * @param {HTMLElement | null} errorSummary - The error summary container\n * @param {string | undefined} uploadId - The upload ID\n * @returns {boolean} Whether the event was handled\n */\nfunction handleAjaxFormSubmission(\n event,\n formElement,\n fileInput,\n uploadButton,\n errorSummary,\n uploadId\n) {\n if (!formElement.action || !uploadId) {\n return false\n }\n\n event.preventDefault()\n\n const formData = new FormData(formElement)\n const isLocalDev = !!formElement.dataset.proxyUrl\n const uploadUrl = formElement.dataset.proxyUrl ?? formElement.action\n\n const fetchOptions = /** @type {RequestInit} */ ({\n method: 'POST',\n body: formData,\n redirect: isLocalDev ? 'follow' : 'manual' // follow mode if local development with the proxy\n })\n\n // no-cors mode if needed local development with the proxy\n if (isLocalDev) {\n fetchOptions.mode = 'no-cors'\n }\n\n fetch(uploadUrl, fetchOptions)\n .then(() => {\n pollUploadStatus(uploadId)\n })\n .catch(() => {\n fileInput.disabled = false\n uploadButton.disabled = false\n\n showError(\n 'There was a problem uploading the file',\n errorSummary,\n fileInput\n )\n\n return null\n })\n\n return true\n}\n\nfunction initUpload() {\n const form = document.querySelector('form:has(input[type=\"file\"])')\n /** @type {HTMLInputElement | null} */\n const fileInput = form ? form.querySelector('input[type=\"file\"]') : null\n /** @type {HTMLButtonElement | null} */\n const uploadButton = form ? form.querySelector('.upload-file-button') : null\n const continueButton =\n /** @type {HTMLButtonElement} */ (\n Array.from(document.querySelectorAll('button.govuk-button')).find(\n (button) => button.textContent.trim() === 'Continue'\n )\n ) ?? null\n\n const errorSummary = document.querySelector('.govuk-error-summary-container')\n\n if (!form || !fileInput || !uploadButton) {\n return\n }\n\n const formElement = /** @type {HTMLFormElement} */ (form)\n /** @type {File | null} */\n let selectedFile = null\n let isSubmitting = false\n const uploadId = formElement.dataset.uploadId\n\n fileInput.addEventListener('change', () => {\n if (errorSummary) {\n errorSummary.innerHTML = ''\n }\n\n if (fileInput.files && fileInput.files.length > 0) {\n selectedFile = fileInput.files[0]\n }\n })\n\n uploadButton.addEventListener('click', (event) => {\n if (!selectedFile) {\n event.preventDefault()\n showError(\n 'Select a file',\n /** @type {HTMLElement | null} */ (errorSummary),\n fileInput\n )\n return\n }\n\n if (isSubmitting) {\n event.preventDefault()\n return\n }\n\n isSubmitting = true\n\n handleStandardFormSubmission(\n formElement,\n fileInput,\n uploadButton,\n continueButton,\n selectedFile\n )\n\n handleAjaxFormSubmission(\n event,\n formElement,\n fileInput,\n uploadButton,\n /** @type {HTMLElement | null} */ (errorSummary),\n uploadId\n )\n })\n}\n\nexport function initFileUpload() {\n if (document.readyState === 'loading') {\n document.addEventListener('DOMContentLoaded', initUpload)\n } else {\n initUpload()\n }\n}\n"],"mappings":"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","ignoreList":[]}
1
+ {"version":3,"file":"file-upload.js","names":["MAX_POLLING_DURATION","ARIA_DESCRIBEDBY","ERROR_SUMMARY_TITLE_ID","createOrUpdateStatusAnnouncer","form","fileCountP","statusAnnouncer","querySelector","document","createElement","id","className","setAttribute","addStatusAnnouncerToDOM","asHTMLElement","appendChild","body","nextSibling","parentNode","insertBefore","parentElement","findOrCreateSummaryList","summaryList","buttonGroup","createFileRow","selectedFile","statusText","row","name","innerHTML","renderSummary","container","getElementById","uploadForm","closest","HTMLFormElement","fileInput","existingRow","remove","firstChild","textContent","showError","message","errorSummary","topErrorSummary","titleElement","removeAttribute","formGroup","classList","add","inputId","errorMessage","element","reloadPage","window","history","replaceState","location","href","pathname","buildUploadStatusUrl","uploadId","normalisedPath","replace","segments","split","filter","Boolean","prefix","length","slice","join","pollUploadStatus","attempts","interval","setInterval","clearInterval","uploadStatusUrl","fetch","headers","Accept","then","response","ok","Error","json","data","uploadStatus","catch","handleStandardFormSubmission","formElement","uploadButton","continueButton","selectedFiles","i","focus","setTimeout","disabled","handleAjaxFormSubmission","event","action","preventDefault","formData","FormData","isLocalDev","dataset","proxyUrl","uploadUrl","fetchOptions","method","redirect","mode","initUpload","Array","from","querySelectorAll","find","button","trim","isSubmitting","addEventListener","files","initFileUpload","readyState"],"sources":["../../../src/client/javascripts/file-upload.js"],"sourcesContent":["export const MAX_POLLING_DURATION = 300 // 5 minutes\nconst ARIA_DESCRIBEDBY = 'aria-describedby'\nconst ERROR_SUMMARY_TITLE_ID = 'error-summary-title'\n\n/**\n * Creates or updates status announcer for screen readers\n * @param {HTMLElement | null} form - The form element\n * @param {HTMLElement | null} fileCountP - The file count paragraph element\n * @returns {HTMLElement} The status announcer element\n */\nfunction createOrUpdateStatusAnnouncer(form, fileCountP) {\n let statusAnnouncer = form?.querySelector('#statusInformation')\n\n if (!statusAnnouncer) {\n statusAnnouncer = document.createElement('div')\n statusAnnouncer.id = 'statusInformation'\n statusAnnouncer.className = 'govuk-visually-hidden'\n statusAnnouncer.setAttribute('aria-live', 'polite')\n\n // multiple fallbacks to ensure the status announcer is always added to the DOM\n // this helps with cross-browser compatibility and unexpected DOM structures encountered during QA\n try {\n addStatusAnnouncerToDOM(\n asHTMLElement(form),\n asHTMLElement(fileCountP),\n asHTMLElement(statusAnnouncer)\n )\n } catch {\n try {\n form?.appendChild(statusAnnouncer)\n } catch {\n document.body.appendChild(statusAnnouncer)\n }\n }\n }\n\n return /** @type {HTMLElement} */ (statusAnnouncer)\n}\n\n/**\n * Helper function to add the status announcer to the DOM\n * @param {HTMLElement} form - The form element\n * @param {HTMLElement | null} fileCountP - The file count paragraph element\n * @param {HTMLElement} statusAnnouncer - The status announcer element to add\n */\nfunction addStatusAnnouncerToDOM(form, fileCountP, statusAnnouncer) {\n if (fileCountP?.nextSibling && fileCountP.parentNode === form) {\n form.insertBefore(statusAnnouncer, fileCountP.nextSibling)\n return\n }\n\n const parentElement = fileCountP?.parentNode ?? form\n parentElement.appendChild(statusAnnouncer)\n}\n\n/**\n * Finds an existing summary list or creates a new one\n * @param {HTMLFormElement} form - The form element\n * @param {HTMLElement} fileCountP - The file count paragraph element\n * @returns {HTMLElement} The summary list element\n */\nfunction findOrCreateSummaryList(form, fileCountP) {\n let summaryList = form.querySelector('dl.govuk-summary-list')\n\n if (!summaryList) {\n summaryList = document.createElement('dl')\n summaryList.className = 'govuk-summary-list govuk-summary-list--long-key'\n\n const buttonGroup = form.querySelector('.govuk-button-group')\n\n if (buttonGroup) {\n form.insertBefore(summaryList, buttonGroup)\n } else {\n form.insertBefore(summaryList, fileCountP.nextSibling)\n }\n }\n\n return /** @type {HTMLElement} */ (summaryList)\n}\n\n/**\n * Creates a file row element for the summary list\n * @param {File | null} selectedFile - The selected file\n * @param {string} statusText - The status to display\n * @returns {HTMLElement} The created row element\n */\nfunction createFileRow(selectedFile, statusText) {\n const row = document.createElement('div')\n row.className = 'govuk-summary-list__row'\n row.setAttribute('data-filename', selectedFile?.name ?? '')\n row.innerHTML = `\n <dt class=\"govuk-summary-list__key\">\n ${selectedFile?.name ?? ''}\n </dt>\n <dd class=\"govuk-summary-list__value\">\n <strong class=\"govuk-tag govuk-tag--yellow\">${statusText}</strong>\n </dd>\n <dd class=\"govuk-summary-list__actions\">\n </dd>\n `\n return row\n}\n\n/**\n * Renders or updates the file summary box for the selected file\n * @param {File | null} selectedFile - The selected file\n * @param {string} statusText - The status to display\n * @param {HTMLElement} form - The form element\n */\nfunction renderSummary(selectedFile, statusText, form) {\n const container = document.getElementById('uploadedFilesContainer')\n const uploadForm = container ? container.closest('form') : null\n\n if (!uploadForm || !(uploadForm instanceof HTMLFormElement)) {\n return\n }\n\n const fileCountP = uploadForm.querySelector('p.govuk-body')\n\n if (!fileCountP) {\n return\n }\n\n const statusAnnouncer = createOrUpdateStatusAnnouncer(\n /** @type {HTMLElement} */ (uploadForm),\n /** @type {HTMLElement | null} */ (fileCountP)\n )\n\n const fileInput = form.querySelector('input[type=\"file\"]')\n\n if (fileInput) {\n fileInput.setAttribute(ARIA_DESCRIBEDBY, 'statusInformation')\n }\n\n const summaryList = findOrCreateSummaryList(\n /** @type {HTMLFormElement} */ (uploadForm),\n /** @type {HTMLElement} */ (fileCountP)\n )\n\n const existingRow = document.querySelector(\n `[data-filename=\"${selectedFile?.name}\"]`\n )\n\n if (existingRow) {\n existingRow.remove()\n }\n\n const row = createFileRow(selectedFile, statusText)\n summaryList.insertBefore(row, summaryList.firstChild)\n statusAnnouncer.textContent = `${selectedFile?.name ?? ''} ${statusText}`\n}\n\n/**\n * Shows an error message using the GOV.UK error summary component\n * and adds inline error styling to the file input\n * @param {string} message - The error message to display\n * @param {HTMLElement | null} errorSummary - The error summary container\n * @param {HTMLInputElement} fileInput - The file input element\n * @returns {void}\n */\nfunction showError(message, errorSummary, fileInput) {\n const topErrorSummary = document.querySelector('.govuk-error-summary')\n\n if (topErrorSummary) {\n const titleElement = document.getElementById(ERROR_SUMMARY_TITLE_ID)\n if (titleElement) {\n fileInput.setAttribute(ARIA_DESCRIBEDBY, ERROR_SUMMARY_TITLE_ID)\n } else {\n fileInput.removeAttribute(ARIA_DESCRIBEDBY)\n }\n return\n }\n\n if (errorSummary) {\n errorSummary.innerHTML = `\n <div class=\"govuk-error-summary\" data-module=\"govuk-error-summary\">\n <div role=\"alert\">\n <h2 class=\"govuk-error-summary__title\" id=\"${ERROR_SUMMARY_TITLE_ID}\">\n There is a problem\n </h2>\n <div class=\"govuk-error-summary__body\">\n <ul class=\"govuk-list govuk-error-summary__list\">\n <li>\n <a href=\"#file-upload\">${message}</a>\n </li>\n </ul>\n </div>\n </div>\n </div>\n `\n\n fileInput.setAttribute(ARIA_DESCRIBEDBY, ERROR_SUMMARY_TITLE_ID)\n }\n\n const formGroup = fileInput.closest('.govuk-form-group')\n if (formGroup) {\n formGroup.classList.add('govuk-form-group--error')\n fileInput.classList.add('govuk-file-upload--error')\n\n const inputId = fileInput.id\n let errorMessage = document.getElementById(`${inputId}-error`)\n\n if (!errorMessage) {\n errorMessage = document.createElement('p')\n errorMessage.id = `${inputId}-error`\n errorMessage.className = 'govuk-error-message'\n errorMessage.innerHTML = `<span class=\"govuk-visually-hidden\">Error:</span> ${message}`\n formGroup.insertBefore(errorMessage, fileInput)\n }\n\n fileInput.setAttribute(\n ARIA_DESCRIBEDBY,\n `error-summary-title ${inputId}-error`\n )\n }\n}\n\n/**\n * Helper to safely convert an Element to HTMLElement\n * @param {Element | null} element - The element to convert\n */\nfunction asHTMLElement(element) {\n return /** @type {HTMLElement} */ (element)\n}\n\nfunction reloadPage() {\n window.history.replaceState(null, '', window.location.href)\n window.location.href = window.location.pathname\n}\n\n/**\n * Build the upload status URL given the current pathname and the upload ID.\n * This only works when called on a file upload page that has a maximum depth of 1 URL segments following the slug.\n * @param {string} pathname – e.g. window.location.pathname\n * @param {string} uploadId\n * @returns {string} e.g. \"/form/upload-status/abc123\"\n */\nexport function buildUploadStatusUrl(pathname, uploadId) {\n // Remove preview markers and duplicate slashes\n const normalisedPath = pathname\n .replace(/\\/preview\\/(draft|live)/g, '')\n .replace(/\\/{2,}/g, '/')\n .replace(/\\/$/, '')\n\n const segments = normalisedPath.split('/').filter(Boolean)\n\n // The slug is always the second to last segment\n // The prefix is everything before the slug\n const prefix =\n segments.length > 2\n ? `/${segments.slice(0, segments.length - 2).join('/')}`\n : ''\n\n return `${prefix}/upload-status/${uploadId}`\n}\n\n/**\n * Polls the upload status endpoint until the file is ready or timeout occurs\n * @param {string} uploadId - The upload ID to check\n */\nfunction pollUploadStatus(uploadId) {\n let attempts = 0\n const interval = setInterval(() => {\n attempts++\n\n if (attempts >= MAX_POLLING_DURATION) {\n clearInterval(interval)\n reloadPage()\n return\n }\n\n const uploadStatusUrl = buildUploadStatusUrl(\n window.location.pathname,\n uploadId\n )\n\n fetch(uploadStatusUrl, {\n headers: {\n Accept: 'application/json'\n }\n })\n .then((response) => {\n if (!response.ok) {\n throw new Error('Network response was not ok')\n }\n return response.json()\n })\n .then((data) => {\n if (data.uploadStatus === 'ready') {\n clearInterval(interval)\n reloadPage()\n }\n })\n .catch(() => {\n clearInterval(interval)\n reloadPage()\n })\n }, 1000)\n}\n\n/**\n * Handle standard form submission for file upload\n * @param {HTMLFormElement} formElement - The form element\n * @param {HTMLInputElement} fileInput - The file input element\n * @param {HTMLButtonElement} uploadButton - The upload button\n * @param {HTMLButtonElement} continueButton - The continue button\n * @param {File[]} selectedFiles - The selected files\n */\nfunction handleStandardFormSubmission(\n formElement,\n fileInput,\n uploadButton,\n continueButton,\n selectedFiles\n) {\n // Render in reverse so first file ends up at the top of the summary list\n for (let i = selectedFiles.length - 1; i >= 0; i--) {\n renderSummary(selectedFiles[i], 'Uploading…', formElement)\n }\n\n fileInput.focus()\n\n setTimeout(() => {\n fileInput.disabled = true\n uploadButton.disabled = true\n continueButton.disabled = true\n }, 100)\n}\n\n/**\n * Handle AJAX form submission with upload ID\n * @param {Event} event - The click event\n * @param {HTMLFormElement} formElement - The form element\n * @param {HTMLInputElement} fileInput - The file input element\n * @param {HTMLButtonElement} uploadButton - The upload button\n * @param {HTMLElement | null} errorSummary - The error summary container\n * @param {string | undefined} uploadId - The upload ID\n * @returns {boolean} Whether the event was handled\n */\nfunction handleAjaxFormSubmission(\n event,\n formElement,\n fileInput,\n uploadButton,\n errorSummary,\n uploadId\n) {\n if (!formElement.action || !uploadId) {\n return false\n }\n\n event.preventDefault()\n\n const formData = new FormData(formElement)\n const isLocalDev = !!formElement.dataset.proxyUrl\n const uploadUrl = formElement.dataset.proxyUrl ?? formElement.action\n\n const fetchOptions = /** @type {RequestInit} */ ({\n method: 'POST',\n body: formData,\n redirect: isLocalDev ? 'follow' : 'manual' // follow mode if local development with the proxy\n })\n\n // no-cors mode if needed local development with the proxy\n if (isLocalDev) {\n fetchOptions.mode = 'no-cors'\n }\n\n fetch(uploadUrl, fetchOptions)\n .then(() => {\n pollUploadStatus(uploadId)\n })\n .catch(() => {\n fileInput.disabled = false\n uploadButton.disabled = false\n\n showError(\n 'There was a problem uploading the file',\n errorSummary,\n fileInput\n )\n\n return null\n })\n\n return true\n}\n\nfunction initUpload() {\n const form = document.querySelector('form:has(input[type=\"file\"])')\n /** @type {HTMLInputElement | null} */\n const fileInput = form ? form.querySelector('input[type=\"file\"]') : null\n /** @type {HTMLButtonElement | null} */\n const uploadButton = form ? form.querySelector('.upload-file-button') : null\n const continueButton =\n /** @type {HTMLButtonElement} */ (\n Array.from(document.querySelectorAll('button.govuk-button')).find(\n (button) => button.textContent.trim() === 'Continue'\n )\n ) ?? null\n\n const errorSummary = document.querySelector('.govuk-error-summary-container')\n\n if (!form || !fileInput || !uploadButton) {\n return\n }\n\n const formElement = /** @type {HTMLFormElement} */ (form)\n /** @type {File[]} */\n let selectedFiles = []\n let isSubmitting = false\n const uploadId = formElement.dataset.uploadId\n\n fileInput.addEventListener('change', () => {\n if (errorSummary) {\n errorSummary.innerHTML = ''\n }\n\n if (fileInput.files && fileInput.files.length > 0) {\n selectedFiles = Array.from(fileInput.files)\n }\n })\n\n uploadButton.addEventListener('click', (event) => {\n if (selectedFiles.length === 0) {\n event.preventDefault()\n showError(\n 'Select a file',\n /** @type {HTMLElement | null} */ (errorSummary),\n fileInput\n )\n return\n }\n\n if (isSubmitting) {\n event.preventDefault()\n return\n }\n\n isSubmitting = true\n\n // Show all selected files in the summary table\n handleStandardFormSubmission(\n formElement,\n fileInput,\n uploadButton,\n continueButton,\n selectedFiles\n )\n\n handleAjaxFormSubmission(\n event,\n formElement,\n fileInput,\n uploadButton,\n /** @type {HTMLElement | null} */ (errorSummary),\n uploadId\n )\n })\n}\n\nexport function initFileUpload() {\n if (document.readyState === 'loading') {\n document.addEventListener('DOMContentLoaded', initUpload)\n } else {\n initUpload()\n }\n}\n"],"mappings":"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","ignoreList":[]}
@@ -26,12 +26,13 @@ export declare class FileUploadField extends FormComponent {
26
26
  getContextValueFromFormValue(files: UploadState | undefined): string[] | null;
27
27
  getContextValueFromState(state: FormSubmissionState): string[] | null;
28
28
  getViewModel(payload: FormPayload, errors?: FormSubmissionError[], query?: FormQuery): {
29
- value: string;
30
- name: string;
31
29
  upload: {
32
30
  count: number;
33
31
  summaryList: SummaryList;
34
32
  };
33
+ multiple?: boolean | undefined;
34
+ value: string;
35
+ name: string;
35
36
  label: {
36
37
  text: string;
37
38
  };
@@ -25,7 +25,7 @@ export const tempStatusSchema = joi.object({
25
25
  uploadStatus: joi.string().valid(UploadStatus.ready, UploadStatus.pending).required(),
26
26
  metadata: metadataSchema,
27
27
  form: joi.object().required().keys({
28
- file: tempFileSchema
28
+ file: joi.array().items(tempFileSchema).single().required()
29
29
  }),
30
30
  numberOfRejectedFiles: joi.number().optional()
31
31
  }).required();
@@ -106,7 +106,8 @@ export class FileUploadField extends FormComponent {
106
106
  getViewModel(payload, errors, query = {}) {
107
107
  const {
108
108
  options,
109
- page
109
+ page,
110
+ schema
110
111
  } = this;
111
112
 
112
113
  // Allow preview URL direct access
@@ -153,7 +154,7 @@ export class FileUploadField extends FormComponent {
153
154
 
154
155
  // Remove summary list actions from previews
155
156
  if (!isForceAccess) {
156
- const path = `/${item.uploadId}/confirm-delete`;
157
+ const path = `/${file.fileId}/confirm-delete`;
157
158
  const href = page?.getHref(`${page.path}${path}`) ?? '#';
158
159
  items.push({
159
160
  href,
@@ -182,6 +183,9 @@ export class FileUploadField extends FormComponent {
182
183
  if ('accept' in options && options.accept) {
183
184
  attributes.accept = options.accept;
184
185
  }
186
+
187
+ // Allow multiple file selection when schema permits more than 1 file
188
+ const allowsMultiple = schema.max !== 1 && schema.length !== 1;
185
189
  const summaryList = {
186
190
  classes: 'govuk-summary-list--long-key',
187
191
  rows
@@ -192,6 +196,10 @@ export class FileUploadField extends FormComponent {
192
196
  value: '',
193
197
  // Override the component name we send to CDP
194
198
  name: 'file',
199
+ // Enable multi-file selection in the file picker
200
+ ...(allowsMultiple && {
201
+ multiple: true
202
+ }),
195
203
  upload: {
196
204
  count,
197
205
  summaryList
@@ -1 +1 @@
1
- {"version":3,"file":"FileUploadField.js","names":["Boom","joi","FormComponent","isUploadState","InvalidComponentStateError","messageTemplate","FileStatus","UploadStatus","render","uploadIdSchema","string","uuid","required","fileSchema","object","fileId","filename","contentLength","number","tempFileSchema","append","fileStatus","valid","complete","rejected","pending","errorMessage","optional","formFileSchema","metadataSchema","keys","retrievalKey","email","tempStatusSchema","uploadStatus","ready","metadata","form","file","numberOfRejectedFiles","formStatusSchema","itemSchema","uploadId","tempItemSchema","status","formItemSchema","FileUploadField","constructor","def","props","options","schema","formSchema","array","label","single","length","max","min","items","stateSchema","default","allow","getFormValueFromState","state","name","getFormValue","value","isValue","undefined","getDisplayStringFromFormValue","files","unit","getDisplayStringFromState","getContextValueFromFormValue","map","getContextValueFromState","getViewModel","payload","errors","query","page","isForceAccess","viewModel","attributes","id","filtered","filter","count","rows","item","index","tag","classes","text","valueHtml","view","context","params","trim","keyHtml","path","href","getHref","push","visuallyHiddenText","key","html","actions","accept","summaryList","upload","getAllPossibleErrors","onSubmit","request","notificationEmail","Error","app","model","services","formSubmissionService","values","initiatedRetrievalKey","persistFiles","error","isBoom","output","statusCode","baseErrors","type","template","selectRequired","advancedSettingsErrors"],"sources":["../../../../../src/server/plugins/engine/components/FileUploadField.ts"],"sourcesContent":["import {\n type FileUploadFieldComponent,\n type FormMetadata\n} from '@defra/forms-model'\nimport Boom from '@hapi/boom'\nimport joi, { type ArraySchema } from 'joi'\n\nimport {\n FormComponent,\n isUploadState\n} from '~/src/server/plugins/engine/components/FormComponent.js'\nimport { InvalidComponentStateError } from '~/src/server/plugins/engine/pageControllers/errors.js'\nimport { messageTemplate } from '~/src/server/plugins/engine/pageControllers/validationOptions.js'\nimport {\n FileStatus,\n UploadStatus,\n type ErrorMessageTemplateList,\n type FileState,\n type FileUpload,\n type FileUploadMetadata,\n type FormContext,\n type FormPayload,\n type FormState,\n type FormStateValue,\n type FormSubmissionError,\n type FormSubmissionState,\n type SummaryList,\n type SummaryListAction,\n type SummaryListRow,\n type UploadState,\n type UploadStatusFileResponse,\n type UploadStatusResponse\n} from '~/src/server/plugins/engine/types.js'\nimport { render } from '~/src/server/plugins/nunjucks/index.js'\nimport {\n type FormQuery,\n type FormRequestPayload\n} from '~/src/server/routes/types.js'\n\nexport const uploadIdSchema = joi.string().uuid().required()\n\nexport const fileSchema = joi\n .object<FileUpload>({\n fileId: joi.string().uuid().required(),\n filename: joi.string().required(),\n contentLength: joi.number().required()\n })\n .required()\n\nexport const tempFileSchema = fileSchema.append({\n fileStatus: joi\n .string()\n .valid(FileStatus.complete, FileStatus.rejected, FileStatus.pending)\n .required(),\n errorMessage: joi.string().optional()\n})\n\nexport const formFileSchema = fileSchema.append({\n fileStatus: joi.string().valid(FileStatus.complete).required()\n})\n\nexport const metadataSchema = joi\n .object<FileUploadMetadata>()\n .keys({\n retrievalKey: joi.string().email().required()\n })\n .required()\n\nexport const tempStatusSchema = joi\n .object<UploadStatusFileResponse>({\n uploadStatus: joi\n .string()\n .valid(UploadStatus.ready, UploadStatus.pending)\n .required(),\n metadata: metadataSchema,\n form: joi.object().required().keys({\n file: tempFileSchema\n }),\n numberOfRejectedFiles: joi.number().optional()\n })\n .required()\n\nexport const formStatusSchema = joi\n .object<UploadStatusResponse>({\n uploadStatus: joi.string().valid(UploadStatus.ready).required(),\n metadata: metadataSchema,\n form: joi.object().required().keys({\n file: formFileSchema\n }),\n numberOfRejectedFiles: joi.number().valid(0).required()\n })\n .required()\n\nexport const itemSchema = joi.object<FileState>({\n uploadId: uploadIdSchema\n})\n\nexport const tempItemSchema = itemSchema.append({\n status: tempStatusSchema\n})\n\nexport const formItemSchema = itemSchema.append({\n status: formStatusSchema\n})\n\nexport class FileUploadField extends FormComponent {\n declare options: FileUploadFieldComponent['options']\n declare schema: FileUploadFieldComponent['schema']\n declare formSchema: ArraySchema<FileState>\n declare stateSchema: ArraySchema<FileState>\n\n constructor(\n def: FileUploadFieldComponent,\n props: ConstructorParameters<typeof FormComponent>[1]\n ) {\n super(def, props)\n\n const { options, schema } = def\n\n let formSchema = joi\n .array<FileState>()\n .label(this.label)\n .single()\n .required()\n\n if (options.required === false) {\n formSchema = formSchema.optional()\n }\n\n if (typeof schema.length !== 'number') {\n if (typeof schema.max === 'number') {\n formSchema = formSchema.max(schema.max)\n }\n\n if (typeof schema.min === 'number') {\n formSchema = formSchema.min(schema.min)\n } else if (options.required !== false) {\n formSchema = formSchema.min(1)\n }\n } else {\n formSchema = formSchema.length(schema.length)\n }\n\n this.formSchema = formSchema.items(formItemSchema)\n this.stateSchema = formSchema\n .items(formItemSchema)\n .default(null)\n .allow(null)\n\n this.options = options\n this.schema = schema\n }\n\n getFormValueFromState(state: FormSubmissionState) {\n const { name } = this\n return this.getFormValue(state[name])\n }\n\n getFormValue(value?: FormStateValue | FormState) {\n return this.isValue(value) ? value : undefined\n }\n\n getDisplayStringFromFormValue(files: FileState[] | undefined): string {\n if (!files?.length) {\n return ''\n }\n\n const unit = files.length === 1 ? 'file' : 'files'\n return `Uploaded ${files.length} ${unit}`\n }\n\n getDisplayStringFromState(state: FormSubmissionState) {\n const files = this.getFormValueFromState(state)\n\n return this.getDisplayStringFromFormValue(files)\n }\n\n getContextValueFromFormValue(\n files: UploadState | undefined\n ): string[] | null {\n return files?.map(({ status }) => status.form.file.fileId) ?? null\n }\n\n getContextValueFromState(state: FormSubmissionState) {\n const files = this.getFormValueFromState(state)\n return this.getContextValueFromFormValue(files)\n }\n\n getViewModel(\n payload: FormPayload,\n errors?: FormSubmissionError[],\n query: FormQuery = {}\n ) {\n const { options, page } = this\n\n // Allow preview URL direct access\n const isForceAccess = 'force' in query\n\n const viewModel = super.getViewModel(payload, errors)\n const { attributes, id, value } = viewModel\n\n const files = this.getFormValue(value) ?? []\n const filtered = files.filter(\n (file) => file.status.form.file.fileStatus === FileStatus.complete\n )\n const count = filtered.length\n\n const rows: SummaryListRow[] = filtered.map((item, index) => {\n const { status } = item\n const { form } = status\n const { file } = form\n\n const tag = { classes: 'govuk-tag--green', text: 'Uploaded' }\n\n const valueHtml = render\n .view('components/fileuploadfield-value.html', {\n context: { params: { tag } }\n })\n .trim()\n\n const keyHtml = render\n .view('components/fileuploadfield-key.html', {\n context: {\n params: {\n name: file.filename,\n errorMessage: errors && file.errorMessage\n }\n }\n })\n .trim()\n\n const items: SummaryListAction[] = []\n\n // Remove summary list actions from previews\n if (!isForceAccess) {\n const path = `/${item.uploadId}/confirm-delete`\n const href = page?.getHref(`${page.path}${path}`) ?? '#'\n\n items.push({\n href,\n text: 'Remove',\n classes: 'govuk-link--no-visited-state',\n attributes: { id: `${id}__${index}` },\n visuallyHiddenText: file.filename\n })\n }\n\n return {\n key: {\n html: keyHtml\n },\n value: {\n html: valueHtml\n },\n actions: {\n items\n }\n } satisfies SummaryListRow\n })\n\n // Set up the `accept` attribute\n if ('accept' in options && options.accept) {\n attributes.accept = options.accept\n }\n\n const summaryList: SummaryList = {\n classes: 'govuk-summary-list--long-key',\n rows\n }\n\n return {\n ...viewModel,\n\n // File input can't have a initial value\n value: '',\n\n // Override the component name we send to CDP\n name: 'file',\n\n upload: {\n count,\n summaryList\n }\n }\n }\n\n isValue(value?: FormStateValue | FormState): value is UploadState {\n return isUploadState(value)\n }\n\n /**\n * For error preview page that shows all possible errors on a component\n */\n getAllPossibleErrors(): ErrorMessageTemplateList {\n return FileUploadField.getAllPossibleErrors()\n }\n\n async onSubmit(\n request: FormRequestPayload,\n metadata: FormMetadata,\n context: FormContext\n ) {\n const notificationEmail = metadata.notificationEmail\n\n if (!notificationEmail) {\n // this should not happen because notificationEmail is checked further up\n // the chain in SummaryPageController before submitForm is called.\n throw new Error('Unexpected missing notificationEmail in metadata')\n }\n\n if (!request.app.model?.services.formSubmissionService) {\n throw new Error('No form submission service available in app model')\n }\n\n const { formSubmissionService } = request.app.model.services\n const values = this.getFormValueFromState(context.state) ?? []\n\n const files = values.map((value) => ({\n fileId: value.status.form.file.fileId,\n initiatedRetrievalKey: value.status.metadata.retrievalKey\n }))\n\n if (!files.length) {\n return\n }\n\n try {\n await formSubmissionService.persistFiles(files, notificationEmail)\n } catch (error) {\n if (\n Boom.isBoom(error) &&\n (error.output.statusCode === 403 || // Forbidden - retrieval key invalid\n error.output.statusCode === 410) // Gone - file expired (took to long to submit, etc)\n ) {\n // Failed to persist files. We can't recover from this, the only real way we can recover the submissions is\n // by resetting the problematic components and letting the user re-try.\n // Scenarios: file missing from S3, invalid retrieval key (timing problem), etc.\n throw new InvalidComponentStateError(\n this,\n 'There was a problem with your uploaded files. Re-upload them before submitting the form again.'\n )\n }\n\n throw error\n }\n }\n\n /**\n * Static version of getAllPossibleErrors that doesn't require a component instance.\n */\n static getAllPossibleErrors(): ErrorMessageTemplateList {\n return {\n baseErrors: [\n { type: 'selectRequired', template: messageTemplate.selectRequired },\n {\n type: 'filesMimes',\n template: 'The selected file must be a {{#limit}}'\n },\n {\n type: 'filesSize',\n template: 'The selected file must be smaller than 100MB'\n },\n { type: 'filesEmpty', template: 'The selected file is empty' },\n { type: 'filesVirus', template: 'The selected file contains a virus' },\n {\n type: 'filesPartial',\n template: 'The selected file has not fully uploaded'\n },\n {\n type: 'filesError',\n template: 'The selected file could not be uploaded – try again'\n }\n ],\n advancedSettingsErrors: [\n {\n type: 'filesMin',\n template: 'You must upload {{#limit}} files or more'\n },\n {\n type: 'filesMax',\n template: 'You can only upload {{#limit}} files or less'\n },\n {\n type: 'filesExact',\n template: 'You must upload exactly {{#limit}} files'\n }\n ]\n }\n }\n}\n"],"mappings":"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","ignoreList":[]}
1
+ {"version":3,"file":"FileUploadField.js","names":["Boom","joi","FormComponent","isUploadState","InvalidComponentStateError","messageTemplate","FileStatus","UploadStatus","render","uploadIdSchema","string","uuid","required","fileSchema","object","fileId","filename","contentLength","number","tempFileSchema","append","fileStatus","valid","complete","rejected","pending","errorMessage","optional","formFileSchema","metadataSchema","keys","retrievalKey","email","tempStatusSchema","uploadStatus","ready","metadata","form","file","array","items","single","numberOfRejectedFiles","formStatusSchema","itemSchema","uploadId","tempItemSchema","status","formItemSchema","FileUploadField","constructor","def","props","options","schema","formSchema","label","length","max","min","stateSchema","default","allow","getFormValueFromState","state","name","getFormValue","value","isValue","undefined","getDisplayStringFromFormValue","files","unit","getDisplayStringFromState","getContextValueFromFormValue","map","getContextValueFromState","getViewModel","payload","errors","query","page","isForceAccess","viewModel","attributes","id","filtered","filter","count","rows","item","index","tag","classes","text","valueHtml","view","context","params","trim","keyHtml","path","href","getHref","push","visuallyHiddenText","key","html","actions","accept","allowsMultiple","summaryList","multiple","upload","getAllPossibleErrors","onSubmit","request","notificationEmail","Error","app","model","services","formSubmissionService","values","initiatedRetrievalKey","persistFiles","error","isBoom","output","statusCode","baseErrors","type","template","selectRequired","advancedSettingsErrors"],"sources":["../../../../../src/server/plugins/engine/components/FileUploadField.ts"],"sourcesContent":["import {\n type FileUploadFieldComponent,\n type FormMetadata\n} from '@defra/forms-model'\nimport Boom from '@hapi/boom'\nimport joi, { type ArraySchema } from 'joi'\n\nimport {\n FormComponent,\n isUploadState\n} from '~/src/server/plugins/engine/components/FormComponent.js'\nimport { InvalidComponentStateError } from '~/src/server/plugins/engine/pageControllers/errors.js'\nimport { messageTemplate } from '~/src/server/plugins/engine/pageControllers/validationOptions.js'\nimport {\n FileStatus,\n UploadStatus,\n type ErrorMessageTemplateList,\n type FileState,\n type FileUpload,\n type FileUploadMetadata,\n type FormContext,\n type FormPayload,\n type FormState,\n type FormStateValue,\n type FormSubmissionError,\n type FormSubmissionState,\n type SummaryList,\n type SummaryListAction,\n type SummaryListRow,\n type UploadState,\n type UploadStatusFileResponse,\n type UploadStatusResponse\n} from '~/src/server/plugins/engine/types.js'\nimport { render } from '~/src/server/plugins/nunjucks/index.js'\nimport {\n type FormQuery,\n type FormRequestPayload\n} from '~/src/server/routes/types.js'\n\nexport const uploadIdSchema = joi.string().uuid().required()\n\nexport const fileSchema = joi\n .object<FileUpload>({\n fileId: joi.string().uuid().required(),\n filename: joi.string().required(),\n contentLength: joi.number().required()\n })\n .required()\n\nexport const tempFileSchema = fileSchema.append({\n fileStatus: joi\n .string()\n .valid(FileStatus.complete, FileStatus.rejected, FileStatus.pending)\n .required(),\n errorMessage: joi.string().optional()\n})\n\nexport const formFileSchema = fileSchema.append({\n fileStatus: joi.string().valid(FileStatus.complete).required()\n})\n\nexport const metadataSchema = joi\n .object<FileUploadMetadata>()\n .keys({\n retrievalKey: joi.string().email().required()\n })\n .required()\n\nexport const tempStatusSchema = joi\n .object<UploadStatusFileResponse>({\n uploadStatus: joi\n .string()\n .valid(UploadStatus.ready, UploadStatus.pending)\n .required(),\n metadata: metadataSchema,\n form: joi\n .object()\n .required()\n .keys({\n file: joi.array().items(tempFileSchema).single().required()\n }),\n numberOfRejectedFiles: joi.number().optional()\n })\n .required()\n\nexport const formStatusSchema = joi\n .object<UploadStatusResponse>({\n uploadStatus: joi.string().valid(UploadStatus.ready).required(),\n metadata: metadataSchema,\n form: joi.object().required().keys({\n file: formFileSchema\n }),\n numberOfRejectedFiles: joi.number().valid(0).required()\n })\n .required()\n\nexport const itemSchema = joi.object<FileState>({\n uploadId: uploadIdSchema\n})\n\nexport const tempItemSchema = itemSchema.append({\n status: tempStatusSchema\n})\n\nexport const formItemSchema = itemSchema.append({\n status: formStatusSchema\n})\n\nexport class FileUploadField extends FormComponent {\n declare options: FileUploadFieldComponent['options']\n declare schema: FileUploadFieldComponent['schema']\n declare formSchema: ArraySchema<FileState>\n declare stateSchema: ArraySchema<FileState>\n\n constructor(\n def: FileUploadFieldComponent,\n props: ConstructorParameters<typeof FormComponent>[1]\n ) {\n super(def, props)\n\n const { options, schema } = def\n\n let formSchema = joi\n .array<FileState>()\n .label(this.label)\n .single()\n .required()\n\n if (options.required === false) {\n formSchema = formSchema.optional()\n }\n\n if (typeof schema.length !== 'number') {\n if (typeof schema.max === 'number') {\n formSchema = formSchema.max(schema.max)\n }\n\n if (typeof schema.min === 'number') {\n formSchema = formSchema.min(schema.min)\n } else if (options.required !== false) {\n formSchema = formSchema.min(1)\n }\n } else {\n formSchema = formSchema.length(schema.length)\n }\n\n this.formSchema = formSchema.items(formItemSchema)\n this.stateSchema = formSchema\n .items(formItemSchema)\n .default(null)\n .allow(null)\n\n this.options = options\n this.schema = schema\n }\n\n getFormValueFromState(state: FormSubmissionState) {\n const { name } = this\n return this.getFormValue(state[name])\n }\n\n getFormValue(value?: FormStateValue | FormState) {\n return this.isValue(value) ? value : undefined\n }\n\n getDisplayStringFromFormValue(files: FileState[] | undefined): string {\n if (!files?.length) {\n return ''\n }\n\n const unit = files.length === 1 ? 'file' : 'files'\n return `Uploaded ${files.length} ${unit}`\n }\n\n getDisplayStringFromState(state: FormSubmissionState) {\n const files = this.getFormValueFromState(state)\n\n return this.getDisplayStringFromFormValue(files)\n }\n\n getContextValueFromFormValue(\n files: UploadState | undefined\n ): string[] | null {\n return files?.map(({ status }) => status.form.file.fileId) ?? null\n }\n\n getContextValueFromState(state: FormSubmissionState) {\n const files = this.getFormValueFromState(state)\n return this.getContextValueFromFormValue(files)\n }\n\n getViewModel(\n payload: FormPayload,\n errors?: FormSubmissionError[],\n query: FormQuery = {}\n ) {\n const { options, page, schema } = this\n\n // Allow preview URL direct access\n const isForceAccess = 'force' in query\n\n const viewModel = super.getViewModel(payload, errors)\n const { attributes, id, value } = viewModel\n\n const files = this.getFormValue(value) ?? []\n const filtered = files.filter(\n (file) => file.status.form.file.fileStatus === FileStatus.complete\n )\n const count = filtered.length\n\n const rows: SummaryListRow[] = filtered.map((item, index) => {\n const { status } = item\n const { form } = status\n const { file } = form\n\n const tag = { classes: 'govuk-tag--green', text: 'Uploaded' }\n\n const valueHtml = render\n .view('components/fileuploadfield-value.html', {\n context: { params: { tag } }\n })\n .trim()\n\n const keyHtml = render\n .view('components/fileuploadfield-key.html', {\n context: {\n params: {\n name: file.filename,\n errorMessage: errors && file.errorMessage\n }\n }\n })\n .trim()\n\n const items: SummaryListAction[] = []\n\n // Remove summary list actions from previews\n if (!isForceAccess) {\n const path = `/${file.fileId}/confirm-delete`\n const href = page?.getHref(`${page.path}${path}`) ?? '#'\n\n items.push({\n href,\n text: 'Remove',\n classes: 'govuk-link--no-visited-state',\n attributes: { id: `${id}__${index}` },\n visuallyHiddenText: file.filename\n })\n }\n\n return {\n key: {\n html: keyHtml\n },\n value: {\n html: valueHtml\n },\n actions: {\n items\n }\n } satisfies SummaryListRow\n })\n\n // Set up the `accept` attribute\n if ('accept' in options && options.accept) {\n attributes.accept = options.accept\n }\n\n // Allow multiple file selection when schema permits more than 1 file\n const allowsMultiple = schema.max !== 1 && schema.length !== 1\n\n const summaryList: SummaryList = {\n classes: 'govuk-summary-list--long-key',\n rows\n }\n\n return {\n ...viewModel,\n\n // File input can't have a initial value\n value: '',\n\n // Override the component name we send to CDP\n name: 'file',\n\n // Enable multi-file selection in the file picker\n ...(allowsMultiple && { multiple: true }),\n\n upload: {\n count,\n summaryList\n }\n }\n }\n\n isValue(value?: FormStateValue | FormState): value is UploadState {\n return isUploadState(value)\n }\n\n /**\n * For error preview page that shows all possible errors on a component\n */\n getAllPossibleErrors(): ErrorMessageTemplateList {\n return FileUploadField.getAllPossibleErrors()\n }\n\n async onSubmit(\n request: FormRequestPayload,\n metadata: FormMetadata,\n context: FormContext\n ) {\n const notificationEmail = metadata.notificationEmail\n\n if (!notificationEmail) {\n // this should not happen because notificationEmail is checked further up\n // the chain in SummaryPageController before submitForm is called.\n throw new Error('Unexpected missing notificationEmail in metadata')\n }\n\n if (!request.app.model?.services.formSubmissionService) {\n throw new Error('No form submission service available in app model')\n }\n\n const { formSubmissionService } = request.app.model.services\n const values = this.getFormValueFromState(context.state) ?? []\n\n const files = values.map((value) => ({\n fileId: value.status.form.file.fileId,\n initiatedRetrievalKey: value.status.metadata.retrievalKey\n }))\n\n if (!files.length) {\n return\n }\n\n try {\n await formSubmissionService.persistFiles(files, notificationEmail)\n } catch (error) {\n if (\n Boom.isBoom(error) &&\n (error.output.statusCode === 403 || // Forbidden - retrieval key invalid\n error.output.statusCode === 410) // Gone - file expired (took to long to submit, etc)\n ) {\n // Failed to persist files. We can't recover from this, the only real way we can recover the submissions is\n // by resetting the problematic components and letting the user re-try.\n // Scenarios: file missing from S3, invalid retrieval key (timing problem), etc.\n throw new InvalidComponentStateError(\n this,\n 'There was a problem with your uploaded files. Re-upload them before submitting the form again.'\n )\n }\n\n throw error\n }\n }\n\n /**\n * Static version of getAllPossibleErrors that doesn't require a component instance.\n */\n static getAllPossibleErrors(): ErrorMessageTemplateList {\n return {\n baseErrors: [\n { type: 'selectRequired', template: messageTemplate.selectRequired },\n {\n type: 'filesMimes',\n template: 'The selected file must be a {{#limit}}'\n },\n {\n type: 'filesSize',\n template: 'The selected file must be smaller than 100MB'\n },\n { type: 'filesEmpty', template: 'The selected file is empty' },\n { type: 'filesVirus', template: 'The selected file contains a virus' },\n {\n type: 'filesPartial',\n template: 'The selected file has not fully uploaded'\n },\n {\n type: 'filesError',\n template: 'The selected file could not be uploaded – try again'\n }\n ],\n advancedSettingsErrors: [\n {\n type: 'filesMin',\n template: 'You must upload {{#limit}} files or more'\n },\n {\n type: 'filesMax',\n template: 'You can only upload {{#limit}} files or less'\n },\n {\n type: 'filesExact',\n template: 'You must upload exactly {{#limit}} files'\n }\n ]\n }\n }\n}\n"],"mappings":"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","ignoreList":[]}
@@ -52,6 +52,17 @@ export declare class FileUploadPageController extends QuestionPageController {
52
52
  * @param depth - the number of retries so far
53
53
  */
54
54
  private checkUploadStatus;
55
+ /**
56
+ * Processes the uploaded files from a CDP status response.
57
+ * Complete files are added to state, rejected/pending files
58
+ * have their error messages flashed.
59
+ * @param request - the hapi request
60
+ * @param state - the form state
61
+ * @param validatedItem - the Joi-validated upload item
62
+ * @param files - the current files array from state
63
+ * @param upload - the current upload initiation response
64
+ */
65
+ private processUploadedFiles;
55
66
  /**
56
67
  * Checks the payload for a file getting removed
57
68
  * and removes it from the upload files if found
@@ -125,8 +125,8 @@ export class FileUploadPageController extends QuestionPageController {
125
125
  } = context;
126
126
  const files = this.getFilesFromState(state);
127
127
  const fileToRemove = files.find(({
128
- uploadId
129
- }) => uploadId === params.itemId);
128
+ status
129
+ }) => status.form.file.fileId === params.itemId);
130
130
  if (!fileToRemove) {
131
131
  throw Boom.notFound('File to delete not found');
132
132
  }
@@ -309,8 +309,7 @@ export class FileUploadPageController extends QuestionPageController {
309
309
 
310
310
  // Only add to files state if the file validates.
311
311
  // This secures against html tampering of the file input
312
- // by adding a 'multiple' attribute or it being
313
- // changed to a simple text field or similar.
312
+ // (e.g. changing it to a simple text field or similar).
314
313
  const validationResult = tempItemSchema.validate({
315
314
  uploadId,
316
315
  status: statusResponse
@@ -318,13 +317,69 @@ export class FileUploadPageController extends QuestionPageController {
318
317
  stripUnknown: true
319
318
  });
320
319
  const error = validationResult.error;
321
- const fileState = validationResult.value;
322
320
  if (error) {
323
321
  return this.initiateAndStoreNewUpload(request, state);
324
322
  }
325
- const file = fileState.status.form.file;
326
- if (file.fileStatus === FileStatus.complete) {
327
- files.unshift(prepareFileState(fileState));
323
+
324
+ // CDP returns form.file as a single object for one file,
325
+ // or an array for multiple files. The Joi schema normalises
326
+ // both to an array via .single().
327
+ await this.processUploadedFiles(request, state, validationResult.value, files, upload);
328
+ return this.initiateAndStoreNewUpload(request, state);
329
+ }
330
+
331
+ /**
332
+ * Processes the uploaded files from a CDP status response.
333
+ * Complete files are added to state, rejected/pending files
334
+ * have their error messages flashed.
335
+ * @param request - the hapi request
336
+ * @param state - the form state
337
+ * @param validatedItem - the Joi-validated upload item
338
+ * @param files - the current files array from state
339
+ * @param upload - the current upload initiation response
340
+ */
341
+ async processUploadedFiles(request, state, validatedItem, files, upload) {
342
+ const {
343
+ uploadId
344
+ } = validatedItem;
345
+ const validatedStatus = validatedItem.status;
346
+ const rawFile = validatedStatus.form.file;
347
+ const uploadedFiles = Array.isArray(rawFile) ? rawFile : [rawFile];
348
+ const allErrors = [];
349
+ for (const file of uploadedFiles) {
350
+ if (file.fileStatus === FileStatus.complete) {
351
+ const perFileState = {
352
+ uploadId,
353
+ status: {
354
+ ...validatedStatus,
355
+ form: {
356
+ file
357
+ }
358
+ }
359
+ };
360
+ files.unshift(prepareFileState(perFileState));
361
+ } else {
362
+ // Collect the error for rejected/pending files.
363
+ const {
364
+ fileUpload
365
+ } = this;
366
+ const name = fileUpload.name;
367
+ const text = file.errorMessage ?? 'Unknown error';
368
+ allErrors.push({
369
+ path: [name],
370
+ href: `#${name}`,
371
+ name,
372
+ text
373
+ });
374
+ }
375
+ }
376
+ if (allErrors.length) {
377
+ const cacheService = getCacheService(request.server);
378
+ cacheService.setFlash(request, {
379
+ errors: allErrors
380
+ });
381
+ }
382
+ if (uploadedFiles.some(f => f.fileStatus === FileStatus.complete)) {
328
383
  await this.mergeState(request, state, {
329
384
  upload: {
330
385
  [this.path]: {
@@ -333,25 +388,7 @@ export class FileUploadPageController extends QuestionPageController {
333
388
  }
334
389
  }
335
390
  });
336
- } else {
337
- // Flash the error message.
338
- const {
339
- fileUpload
340
- } = this;
341
- const cacheService = getCacheService(request.server);
342
- const name = fileUpload.name;
343
- const text = file.errorMessage ?? 'Unknown error';
344
- const errors = [{
345
- path: [name],
346
- href: `#${name}`,
347
- name,
348
- text
349
- }];
350
- cacheService.setFlash(request, {
351
- errors
352
- });
353
391
  }
354
- return this.initiateAndStoreNewUpload(request, state);
355
392
  }
356
393
 
357
394
  /**
@@ -371,8 +408,8 @@ export class FileUploadPageController extends QuestionPageController {
371
408
  const upload = this.getUploadFromState(state);
372
409
  const files = this.getFilesFromState(state);
373
410
  const filesUpdated = files.filter(({
374
- uploadId
375
- }) => uploadId !== params.itemId);
411
+ status
412
+ }) => status.form.file.fileId !== params.itemId);
376
413
  if (filesUpdated.length === files.length) {
377
414
  return;
378
415
  }