@defra/forms-engine-plugin 0.0.4 → 0.0.6
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.server/server/index.js +0 -4
- package/.server/server/index.js.map +1 -1
- package/.server/server/plugins/engine/helpers.js +3 -0
- package/.server/server/plugins/engine/helpers.js.map +1 -1
- package/.server/server/plugins/engine/index.js +27 -1
- package/.server/server/plugins/engine/index.js.map +1 -1
- package/.server/server/plugins/engine/pageControllers/FileUploadPageController.js +2 -4
- package/.server/server/plugins/engine/pageControllers/FileUploadPageController.js.map +1 -1
- package/.server/server/plugins/engine/pageControllers/QuestionPageController.js +4 -10
- package/.server/server/plugins/engine/pageControllers/QuestionPageController.js.map +1 -1
- package/.server/server/plugins/engine/pageControllers/StatusPageController.js +2 -3
- package/.server/server/plugins/engine/pageControllers/StatusPageController.js.map +1 -1
- package/.server/server/plugins/engine/pageControllers/SummaryPageController.js +2 -4
- package/.server/server/plugins/engine/pageControllers/SummaryPageController.js.map +1 -1
- package/.server/server/plugins/engine/plugin.js +65 -6
- package/.server/server/plugins/engine/plugin.js.map +1 -1
- package/.server/server/plugins/engine/types.js.map +1 -1
- package/.server/server/{views → plugins/engine/views}/components/service-banner/template.test.js +1 -1
- package/.server/server/plugins/engine/views/components/service-banner/template.test.js.map +1 -0
- package/.server/server/{views → plugins/engine/views}/components/tag-env/template.test.js +1 -1
- package/.server/server/plugins/engine/views/components/tag-env/template.test.js.map +1 -0
- package/.server/server/services/cacheService.js +5 -2
- package/.server/server/services/cacheService.js.map +1 -1
- package/.server/typings/hapi/index.d.js.map +1 -1
- package/README.md +215 -4
- package/package.json +3 -3
- package/src/client/javascripts/application.js +87 -0
- package/src/client/javascripts/file-upload.js +386 -0
- package/src/client/stylesheets/_code.scss +33 -0
- package/src/client/stylesheets/_govuk-frontend.scss +4 -0
- package/src/client/stylesheets/_prose.scss +56 -0
- package/src/client/stylesheets/_service-banner.scss +24 -0
- package/src/client/stylesheets/_summary-list.scss +28 -0
- package/src/client/stylesheets/_tag-env.scss +24 -0
- package/src/client/stylesheets/application.scss +14 -0
- package/src/common/cookies.js +58 -0
- package/src/common/cookies.test.js +23 -0
- package/src/common/types.js +5 -0
- package/src/config/index.ts +271 -0
- package/src/index.ts +31 -0
- package/src/server/common/helpers/logging/logger-options.test.ts +50 -0
- package/src/server/common/helpers/logging/logger-options.ts +46 -0
- package/src/server/common/helpers/logging/logger.ts +7 -0
- package/src/server/common/helpers/logging/request-logger.ts +9 -0
- package/src/server/common/helpers/logging/request-tracing.js +10 -0
- package/src/server/common/helpers/redis-client.js +70 -0
- package/src/server/constants.js +1 -0
- package/src/server/forms/README.md +10 -0
- package/src/server/forms/components.json +1015 -0
- package/src/server/forms/report-a-terrorist.json +270 -0
- package/src/server/forms/runner-components-test.json +365 -0
- package/src/server/forms/test.json +581 -0
- package/src/server/index.test.ts +582 -0
- package/src/server/index.ts +135 -0
- package/src/server/plugins/blankie.test.ts +73 -0
- package/src/server/plugins/blankie.ts +48 -0
- package/src/server/plugins/crumb.ts +20 -0
- package/src/server/plugins/engine/README.md +87 -0
- package/src/server/plugins/engine/components/AutocompleteField.test.ts +294 -0
- package/src/server/plugins/engine/components/AutocompleteField.ts +49 -0
- package/src/server/plugins/engine/components/CheckboxesField.test.ts +379 -0
- package/src/server/plugins/engine/components/CheckboxesField.ts +106 -0
- package/src/server/plugins/engine/components/ComponentBase.ts +97 -0
- package/src/server/plugins/engine/components/ComponentCollection.ts +278 -0
- package/src/server/plugins/engine/components/DatePartsField.test.ts +822 -0
- package/src/server/plugins/engine/components/DatePartsField.ts +264 -0
- package/src/server/plugins/engine/components/Details.test.ts +49 -0
- package/src/server/plugins/engine/components/Details.ts +30 -0
- package/src/server/plugins/engine/components/EmailAddressField.test.ts +395 -0
- package/src/server/plugins/engine/components/EmailAddressField.ts +55 -0
- package/src/server/plugins/engine/components/FileUploadField.test.ts +778 -0
- package/src/server/plugins/engine/components/FileUploadField.ts +262 -0
- package/src/server/plugins/engine/components/FormComponent.ts +249 -0
- package/src/server/plugins/engine/components/Html.test.ts +48 -0
- package/src/server/plugins/engine/components/Html.ts +29 -0
- package/src/server/plugins/engine/components/InsetText.test.ts +48 -0
- package/src/server/plugins/engine/components/InsetText.ts +27 -0
- package/src/server/plugins/engine/components/List.test.ts +76 -0
- package/src/server/plugins/engine/components/List.ts +72 -0
- package/src/server/plugins/engine/components/ListFormComponent.ts +140 -0
- package/src/server/plugins/engine/components/MonthYearField.test.ts +567 -0
- package/src/server/plugins/engine/components/MonthYearField.ts +222 -0
- package/src/server/plugins/engine/components/MultilineTextField.test.ts +558 -0
- package/src/server/plugins/engine/components/MultilineTextField.ts +138 -0
- package/src/server/plugins/engine/components/NumberField.test.ts +701 -0
- package/src/server/plugins/engine/components/NumberField.ts +163 -0
- package/src/server/plugins/engine/components/RadiosField.test.ts +288 -0
- package/src/server/plugins/engine/components/RadiosField.ts +24 -0
- package/src/server/plugins/engine/components/SelectField.test.ts +288 -0
- package/src/server/plugins/engine/components/SelectField.ts +47 -0
- package/src/server/plugins/engine/components/SelectionControlField.ts +43 -0
- package/src/server/plugins/engine/components/TelephoneNumberField.test.ts +356 -0
- package/src/server/plugins/engine/components/TelephoneNumberField.ts +67 -0
- package/src/server/plugins/engine/components/TextField.test.ts +489 -0
- package/src/server/plugins/engine/components/TextField.ts +96 -0
- package/src/server/plugins/engine/components/UkAddressField.test.ts +623 -0
- package/src/server/plugins/engine/components/UkAddressField.ts +172 -0
- package/src/server/plugins/engine/components/YesNoField.test.ts +248 -0
- package/src/server/plugins/engine/components/YesNoField.ts +31 -0
- package/src/server/plugins/engine/components/constants.ts +1 -0
- package/src/server/plugins/engine/components/helpers.ts +330 -0
- package/src/server/plugins/engine/components/index.ts +24 -0
- package/src/server/plugins/engine/components/types.ts +117 -0
- package/src/server/plugins/engine/configureEnginePlugin.ts +47 -0
- package/src/server/plugins/engine/helpers.test.ts +791 -0
- package/src/server/plugins/engine/helpers.ts +384 -0
- package/src/server/plugins/engine/index.ts +47 -0
- package/src/server/plugins/engine/models/FormModel.test.ts +42 -0
- package/src/server/plugins/engine/models/FormModel.ts +443 -0
- package/src/server/plugins/engine/models/RepeatingSummaryViewModel.ts +0 -0
- package/src/server/plugins/engine/models/Section.ts +0 -0
- package/src/server/plugins/engine/models/SummaryViewModel.test.ts +209 -0
- package/src/server/plugins/engine/models/SummaryViewModel.ts +220 -0
- package/src/server/plugins/engine/models/index.ts +2 -0
- package/src/server/plugins/engine/models/types.ts +114 -0
- package/src/server/plugins/engine/outputFormatters/human/v1.test.ts +143 -0
- package/src/server/plugins/engine/outputFormatters/human/v1.ts +73 -0
- package/src/server/plugins/engine/outputFormatters/index.test.ts +17 -0
- package/src/server/plugins/engine/outputFormatters/index.ts +44 -0
- package/src/server/plugins/engine/outputFormatters/machine/v1.test.ts +229 -0
- package/src/server/plugins/engine/outputFormatters/machine/v1.ts +140 -0
- package/src/server/plugins/engine/outputFormatters/machine/v2.test.ts +229 -0
- package/src/server/plugins/engine/outputFormatters/machine/v2.ts +153 -0
- package/src/server/plugins/engine/pageControllers/FileUploadPageController.test.ts +1116 -0
- package/src/server/plugins/engine/pageControllers/FileUploadPageController.ts +447 -0
- package/src/server/plugins/engine/pageControllers/PageController.test.ts +205 -0
- package/src/server/plugins/engine/pageControllers/PageController.ts +176 -0
- package/src/server/plugins/engine/pageControllers/QuestionPageController.test.ts +1264 -0
- package/src/server/plugins/engine/pageControllers/QuestionPageController.ts +565 -0
- package/src/server/plugins/engine/pageControllers/README.md +28 -0
- package/src/server/plugins/engine/pageControllers/RepeatPageController.test.ts +264 -0
- package/src/server/plugins/engine/pageControllers/RepeatPageController.ts +458 -0
- package/src/server/plugins/engine/pageControllers/StartPageController.ts +18 -0
- package/src/server/plugins/engine/pageControllers/StatusPageController.ts +51 -0
- package/src/server/plugins/engine/pageControllers/SummaryPageController.ts +262 -0
- package/src/server/plugins/engine/pageControllers/TerminalController.test.ts +28 -0
- package/src/server/plugins/engine/pageControllers/TerminalPageController.ts +19 -0
- package/src/server/plugins/engine/pageControllers/helpers.test.ts +198 -0
- package/src/server/plugins/engine/pageControllers/helpers.ts +101 -0
- package/src/server/plugins/engine/pageControllers/index.ts +10 -0
- package/src/server/plugins/engine/pageControllers/validationOptions.ts +89 -0
- package/src/server/plugins/engine/plugin.ts +753 -0
- package/src/server/plugins/engine/services/formSubmissionService.js +46 -0
- package/src/server/plugins/engine/services/formsService.js +46 -0
- package/src/server/plugins/engine/services/formsService.test.js +90 -0
- package/src/server/plugins/engine/services/index.js +3 -0
- package/src/server/plugins/engine/services/notifyService.test.ts +132 -0
- package/src/server/plugins/engine/services/notifyService.ts +64 -0
- package/src/server/plugins/engine/services/uploadService.js +60 -0
- package/src/server/plugins/engine/types.ts +317 -0
- package/src/server/plugins/engine/views/components/autocompletefield.html +5 -0
- package/src/server/plugins/engine/views/components/checkboxesfield.html +5 -0
- package/src/server/plugins/engine/views/components/datepartsfield.html +5 -0
- package/src/server/plugins/engine/views/components/debug/macro.njk +3 -0
- package/src/server/plugins/engine/views/components/debug/template.njk +13 -0
- package/src/server/plugins/engine/views/components/details.html +6 -0
- package/src/server/plugins/engine/views/components/emailaddressfield.html +5 -0
- package/src/server/plugins/engine/views/components/fileuploadfield-key.html +8 -0
- package/src/server/plugins/engine/views/components/fileuploadfield-value.html +3 -0
- package/src/server/plugins/engine/views/components/fileuploadfield.html +24 -0
- package/src/server/plugins/engine/views/components/html.html +3 -0
- package/src/server/plugins/engine/views/components/insettext.html +7 -0
- package/src/server/plugins/engine/views/components/list.html +36 -0
- package/src/server/plugins/engine/views/components/monthyearfield.html +5 -0
- package/src/server/plugins/engine/views/components/multilinetextfield.html +10 -0
- package/src/server/plugins/engine/views/components/numberfield.html +5 -0
- package/src/server/plugins/engine/views/components/radiosfield.html +5 -0
- package/src/server/plugins/engine/views/components/selectfield.html +5 -0
- package/src/server/plugins/engine/views/components/service-banner/macro.njk +3 -0
- package/src/server/plugins/engine/views/components/service-banner/template.njk +20 -0
- package/src/server/plugins/engine/views/components/service-banner/template.test.js +43 -0
- package/src/server/plugins/engine/views/components/tag-env/macro.njk +3 -0
- package/src/server/plugins/engine/views/components/tag-env/template.njk +30 -0
- package/src/server/plugins/engine/views/components/tag-env/template.test.js +66 -0
- package/src/server/plugins/engine/views/components/telephonenumberfield.html +5 -0
- package/src/server/plugins/engine/views/components/textfield.html +5 -0
- package/src/server/plugins/engine/views/components/ukaddressfield.html +25 -0
- package/src/server/plugins/engine/views/components/yesnofield.html +5 -0
- package/src/server/plugins/engine/views/confirmation.html +19 -0
- package/src/server/plugins/engine/views/file-upload.html +45 -0
- package/src/server/plugins/engine/views/index.html +39 -0
- package/src/server/plugins/engine/views/item-delete.html +56 -0
- package/src/server/plugins/engine/views/layout.html +199 -0
- package/src/server/plugins/engine/views/partials/components.html +6 -0
- package/src/server/plugins/engine/views/partials/conditional-components.html +3 -0
- package/src/server/plugins/engine/views/partials/debug.html +44 -0
- package/src/server/plugins/engine/views/partials/form.html +15 -0
- package/src/server/plugins/engine/views/partials/heading.html +16 -0
- package/src/server/plugins/engine/views/partials/preview-banner.html +32 -0
- package/src/server/plugins/engine/views/partials/preview-banner.test.js +122 -0
- package/src/server/plugins/engine/views/partials/warn-missing-notification-email.html +10 -0
- package/src/server/plugins/engine/views/repeat-list-summary.html +53 -0
- package/src/server/plugins/engine/views/summary.html +50 -0
- package/src/server/plugins/errorPages.ts +58 -0
- package/src/server/plugins/nunjucks/context.js +88 -0
- package/src/server/plugins/nunjucks/context.test.js +142 -0
- package/src/server/plugins/nunjucks/enviroment.test.js +201 -0
- package/src/server/plugins/nunjucks/environment.js +116 -0
- package/src/server/plugins/nunjucks/filters/answer.js +27 -0
- package/src/server/plugins/nunjucks/filters/answer.test.js +89 -0
- package/src/server/plugins/nunjucks/filters/evaluate.js +21 -0
- package/src/server/plugins/nunjucks/filters/field.js +28 -0
- package/src/server/plugins/nunjucks/filters/field.test.js +75 -0
- package/src/server/plugins/nunjucks/filters/highlight.js +11 -0
- package/src/server/plugins/nunjucks/filters/href.js +30 -0
- package/src/server/plugins/nunjucks/filters/href.test.js +80 -0
- package/src/server/plugins/nunjucks/filters/index.js +8 -0
- package/src/server/plugins/nunjucks/filters/inspect.js +15 -0
- package/src/server/plugins/nunjucks/filters/page.js +24 -0
- package/src/server/plugins/nunjucks/filters/page.test.js +65 -0
- package/src/server/plugins/nunjucks/index.js +3 -0
- package/src/server/plugins/nunjucks/plugin.js +40 -0
- package/src/server/plugins/nunjucks/render.js +42 -0
- package/src/server/plugins/nunjucks/types.js +40 -0
- package/src/server/plugins/pulse.ts +11 -0
- package/src/server/plugins/router.ts +201 -0
- package/src/server/plugins/session.ts +28 -0
- package/src/server/routes/health.js +13 -0
- package/src/server/routes/health.test.js +35 -0
- package/src/server/routes/index.test.ts +125 -0
- package/src/server/routes/index.ts +2 -0
- package/src/server/routes/public.ts +47 -0
- package/src/server/routes/types.ts +48 -0
- package/src/server/schemas/index.ts +34 -0
- package/src/server/secure-context.js +43 -0
- package/src/server/services/cacheService.test.ts +277 -0
- package/src/server/services/cacheService.ts +138 -0
- package/src/server/services/httpService.test.js +491 -0
- package/src/server/services/httpService.ts +50 -0
- package/src/server/services/index.ts +1 -0
- package/src/server/types.ts +54 -0
- package/src/server/utils/notify.test.ts +37 -0
- package/src/server/utils/notify.ts +50 -0
- package/src/server/utils/secure-context/get-trust-store-certs.js +11 -0
- package/src/server/utils/secure-context/get-trust-store-certs.test.js +19 -0
- package/src/server/utils/utils.js +24 -0
- package/src/server/utils/utils.test.js +54 -0
- package/src/server/views/404.html +16 -0
- package/src/server/views/500.html +19 -0
- package/src/server/views/help/accessibility-statement.html +58 -0
- package/src/server/views/help/cookie-preferences.html +57 -0
- package/src/server/views/help/cookies.html +71 -0
- package/src/server/views/help/get-support.html +37 -0
- package/src/server/views/help/privacy-notice.html +68 -0
- package/src/server/views/help/terms-and-conditions.html +83 -0
- package/src/typings/hapi/index.d.ts +87 -0
- package/src/typings/hapi-tracing/index.d.ts +6 -0
- package/src/typings/index.d.ts +3 -0
- package/src/typings/joi/index.d.ts +22 -0
- package/.server/server/views/components/service-banner/template.test.js.map +0 -1
- package/.server/server/views/components/tag-env/template.test.js.map +0 -1
- /package/.server/server/{views → plugins/engine/views}/components/debug/macro.njk +0 -0
- /package/.server/server/{views → plugins/engine/views}/components/debug/template.njk +0 -0
- /package/.server/server/{views → plugins/engine/views}/components/service-banner/macro.njk +0 -0
- /package/.server/server/{views → plugins/engine/views}/components/service-banner/template.njk +0 -0
- /package/.server/server/{views → plugins/engine/views}/components/tag-env/macro.njk +0 -0
- /package/.server/server/{views → plugins/engine/views}/components/tag-env/template.njk +0 -0
- /package/.server/server/{views → plugins/engine/views}/confirmation.html +0 -0
- /package/.server/server/{views → plugins/engine/views}/layout.html +0 -0
- /package/.server/server/{views → plugins/engine/views}/summary.html +0 -0
|
@@ -0,0 +1,386 @@
|
|
|
1
|
+
export const MAX_POLLING_DURATION = 300 // 5 minutes
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Creates or updates status announcer for screen readers
|
|
5
|
+
* @param {HTMLElement | null} form - The form element
|
|
6
|
+
* @param {HTMLElement | null} fileCountP - The file count paragraph element
|
|
7
|
+
* @returns {HTMLElement} The status announcer element
|
|
8
|
+
*/
|
|
9
|
+
function createOrUpdateStatusAnnouncer(form, fileCountP) {
|
|
10
|
+
let statusAnnouncer = form?.querySelector('#statusInformation')
|
|
11
|
+
|
|
12
|
+
if (!statusAnnouncer) {
|
|
13
|
+
statusAnnouncer = document.createElement('div')
|
|
14
|
+
statusAnnouncer.id = 'statusInformation'
|
|
15
|
+
statusAnnouncer.className = 'govuk-visually-hidden'
|
|
16
|
+
statusAnnouncer.setAttribute('aria-live', 'polite')
|
|
17
|
+
|
|
18
|
+
// multiple fallbacks to ensure the status announcer is always added to the DOM
|
|
19
|
+
// this helps with cross-browser compatibility and unexpected DOM structures encountered during QA
|
|
20
|
+
try {
|
|
21
|
+
addStatusAnnouncerToDOM(
|
|
22
|
+
asHTMLElement(form),
|
|
23
|
+
asHTMLElement(fileCountP),
|
|
24
|
+
asHTMLElement(statusAnnouncer)
|
|
25
|
+
)
|
|
26
|
+
} catch {
|
|
27
|
+
try {
|
|
28
|
+
form?.appendChild(statusAnnouncer)
|
|
29
|
+
} catch {
|
|
30
|
+
document.body.appendChild(statusAnnouncer)
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
return /** @type {HTMLElement} */ (statusAnnouncer)
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* Helper function to add the status announcer to the DOM
|
|
40
|
+
* @param {HTMLElement} form - The form element
|
|
41
|
+
* @param {HTMLElement | null} fileCountP - The file count paragraph element
|
|
42
|
+
* @param {HTMLElement} statusAnnouncer - The status announcer element to add
|
|
43
|
+
*/
|
|
44
|
+
function addStatusAnnouncerToDOM(form, fileCountP, statusAnnouncer) {
|
|
45
|
+
if (fileCountP?.nextSibling && fileCountP.parentNode === form) {
|
|
46
|
+
form.insertBefore(statusAnnouncer, fileCountP.nextSibling)
|
|
47
|
+
return
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
const parentElement = fileCountP?.parentNode ?? form
|
|
51
|
+
parentElement.appendChild(statusAnnouncer)
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/**
|
|
55
|
+
* Finds an existing summary list or creates a new one
|
|
56
|
+
* @param {HTMLFormElement} form - The form element
|
|
57
|
+
* @param {HTMLElement} fileCountP - The file count paragraph element
|
|
58
|
+
* @returns {HTMLElement} The summary list element
|
|
59
|
+
*/
|
|
60
|
+
function findOrCreateSummaryList(form, fileCountP) {
|
|
61
|
+
let summaryList = form.querySelector('dl.govuk-summary-list')
|
|
62
|
+
|
|
63
|
+
if (!summaryList) {
|
|
64
|
+
summaryList = document.createElement('dl')
|
|
65
|
+
summaryList.className = 'govuk-summary-list govuk-summary-list--long-key'
|
|
66
|
+
|
|
67
|
+
const continueButton = form.querySelector('.govuk-button')
|
|
68
|
+
|
|
69
|
+
if (continueButton) {
|
|
70
|
+
form.insertBefore(summaryList, continueButton)
|
|
71
|
+
} else {
|
|
72
|
+
form.insertBefore(summaryList, fileCountP.nextSibling)
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
return /** @type {HTMLElement} */ (summaryList)
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
/**
|
|
80
|
+
* Creates a file row element for the summary list
|
|
81
|
+
* @param {File | null} selectedFile - The selected file
|
|
82
|
+
* @param {string} statusText - The status to display
|
|
83
|
+
* @returns {HTMLElement} The created row element
|
|
84
|
+
*/
|
|
85
|
+
function createFileRow(selectedFile, statusText) {
|
|
86
|
+
const row = document.createElement('div')
|
|
87
|
+
row.className = 'govuk-summary-list__row'
|
|
88
|
+
row.setAttribute('data-filename', selectedFile?.name ?? '')
|
|
89
|
+
row.innerHTML = `
|
|
90
|
+
<dt class="govuk-summary-list__key">
|
|
91
|
+
${selectedFile?.name ?? ''}
|
|
92
|
+
</dt>
|
|
93
|
+
<dd class="govuk-summary-list__value">
|
|
94
|
+
<strong class="govuk-tag govuk-tag--yellow">${statusText}</strong>
|
|
95
|
+
</dd>
|
|
96
|
+
<dd class="govuk-summary-list__actions">
|
|
97
|
+
</dd>
|
|
98
|
+
`
|
|
99
|
+
return row
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
/**
|
|
103
|
+
* Renders or updates the file summary box for the selected file
|
|
104
|
+
* @param {File | null} selectedFile - The selected file
|
|
105
|
+
* @param {string} statusText - The status to display
|
|
106
|
+
* @param {HTMLElement} form - The form element
|
|
107
|
+
*/
|
|
108
|
+
function renderSummary(selectedFile, statusText, form) {
|
|
109
|
+
const container = document.getElementById('uploadedFilesContainer')
|
|
110
|
+
const uploadForm = container ? container.closest('form') : null
|
|
111
|
+
|
|
112
|
+
if (!uploadForm || !(uploadForm instanceof HTMLFormElement)) {
|
|
113
|
+
return
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
const fileCountP = uploadForm.querySelector('p.govuk-body')
|
|
117
|
+
|
|
118
|
+
if (!fileCountP) {
|
|
119
|
+
return
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
const statusAnnouncer = createOrUpdateStatusAnnouncer(
|
|
123
|
+
/** @type {HTMLElement} */ (uploadForm),
|
|
124
|
+
/** @type {HTMLElement | null} */ (fileCountP)
|
|
125
|
+
)
|
|
126
|
+
|
|
127
|
+
const fileInput = form.querySelector('input[type="file"]')
|
|
128
|
+
|
|
129
|
+
if (fileInput) {
|
|
130
|
+
fileInput.setAttribute('aria-describedby', 'statusInformation')
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
const summaryList = findOrCreateSummaryList(
|
|
134
|
+
/** @type {HTMLFormElement} */ (uploadForm),
|
|
135
|
+
/** @type {HTMLElement} */ (fileCountP)
|
|
136
|
+
)
|
|
137
|
+
|
|
138
|
+
const existingRow = document.querySelector(
|
|
139
|
+
`[data-filename="${selectedFile?.name}"]`
|
|
140
|
+
)
|
|
141
|
+
|
|
142
|
+
if (existingRow) {
|
|
143
|
+
existingRow.remove()
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
const row = createFileRow(selectedFile, statusText)
|
|
147
|
+
summaryList.insertBefore(row, summaryList.firstChild)
|
|
148
|
+
statusAnnouncer.textContent = `${selectedFile?.name ?? ''} ${statusText}`
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
/**
|
|
152
|
+
* Shows an error message using the GOV.UK error summary component
|
|
153
|
+
* @param {string} message - The error message to display
|
|
154
|
+
* @param {HTMLElement | null} errorSummary - The error summary container
|
|
155
|
+
* @param {HTMLInputElement} fileInput - The file input element
|
|
156
|
+
* @returns {void}
|
|
157
|
+
*/
|
|
158
|
+
function showError(message, errorSummary, fileInput) {
|
|
159
|
+
if (errorSummary) {
|
|
160
|
+
errorSummary.innerHTML = `
|
|
161
|
+
<div class="govuk-error-summary" data-module="govuk-error-summary">
|
|
162
|
+
<div role="alert">
|
|
163
|
+
<h2 class="govuk-error-summary__title" id="error-summary-title">
|
|
164
|
+
There is a problem
|
|
165
|
+
</h2>
|
|
166
|
+
<div class="govuk-error-summary__body">
|
|
167
|
+
<ul class="govuk-list govuk-error-summary__list">
|
|
168
|
+
<li>
|
|
169
|
+
<a href="#file-upload">${message}</a>
|
|
170
|
+
</li>
|
|
171
|
+
</ul>
|
|
172
|
+
</div>
|
|
173
|
+
</div>
|
|
174
|
+
</div>
|
|
175
|
+
`
|
|
176
|
+
fileInput.setAttribute('aria-describedby', 'error-summary-title')
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
/**
|
|
181
|
+
* Helper to safely convert an Element to HTMLElement
|
|
182
|
+
* @param {Element | null} element - The element to convert
|
|
183
|
+
*/
|
|
184
|
+
function asHTMLElement(element) {
|
|
185
|
+
return /** @type {HTMLElement} */ (element)
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
function reloadPage() {
|
|
189
|
+
window.history.replaceState(null, '', window.location.href)
|
|
190
|
+
window.location.href = window.location.pathname
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
/**
|
|
194
|
+
* Polls the upload status endpoint until the file is ready or timeout occurs
|
|
195
|
+
* @param {string} uploadId - The upload ID to check
|
|
196
|
+
*/
|
|
197
|
+
function pollUploadStatus(uploadId) {
|
|
198
|
+
let attempts = 0
|
|
199
|
+
const interval = setInterval(() => {
|
|
200
|
+
attempts++
|
|
201
|
+
|
|
202
|
+
if (attempts >= MAX_POLLING_DURATION) {
|
|
203
|
+
clearInterval(interval)
|
|
204
|
+
reloadPage()
|
|
205
|
+
return
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
fetch(`/upload-status/${uploadId}`, {
|
|
209
|
+
headers: {
|
|
210
|
+
Accept: 'application/json'
|
|
211
|
+
}
|
|
212
|
+
})
|
|
213
|
+
.then((response) => {
|
|
214
|
+
if (!response.ok) {
|
|
215
|
+
throw new Error('Network response was not ok')
|
|
216
|
+
}
|
|
217
|
+
return response.json()
|
|
218
|
+
})
|
|
219
|
+
.then((data) => {
|
|
220
|
+
if (data.uploadStatus === 'ready') {
|
|
221
|
+
clearInterval(interval)
|
|
222
|
+
reloadPage()
|
|
223
|
+
}
|
|
224
|
+
})
|
|
225
|
+
.catch(() => {
|
|
226
|
+
clearInterval(interval)
|
|
227
|
+
reloadPage()
|
|
228
|
+
})
|
|
229
|
+
}, 1000)
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
/**
|
|
233
|
+
* Handle standard form submission for file upload
|
|
234
|
+
* @param {HTMLFormElement} formElement - The form element
|
|
235
|
+
* @param {HTMLInputElement} fileInput - The file input element
|
|
236
|
+
* @param {HTMLButtonElement} uploadButton - The upload button
|
|
237
|
+
* @param {HTMLButtonElement} continueButton - The continue button
|
|
238
|
+
* @param {File | null} selectedFile - The selected file
|
|
239
|
+
*/
|
|
240
|
+
function handleStandardFormSubmission(
|
|
241
|
+
formElement,
|
|
242
|
+
fileInput,
|
|
243
|
+
uploadButton,
|
|
244
|
+
continueButton,
|
|
245
|
+
selectedFile
|
|
246
|
+
) {
|
|
247
|
+
renderSummary(selectedFile, 'Uploading…', formElement)
|
|
248
|
+
|
|
249
|
+
fileInput.focus()
|
|
250
|
+
|
|
251
|
+
setTimeout(() => {
|
|
252
|
+
fileInput.disabled = true
|
|
253
|
+
uploadButton.disabled = true
|
|
254
|
+
continueButton.disabled = true
|
|
255
|
+
}, 100)
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
/**
|
|
259
|
+
* Handle AJAX form submission with upload ID
|
|
260
|
+
* @param {Event} event - The click event
|
|
261
|
+
* @param {HTMLFormElement} formElement - The form element
|
|
262
|
+
* @param {HTMLInputElement} fileInput - The file input element
|
|
263
|
+
* @param {HTMLButtonElement} uploadButton - The upload button
|
|
264
|
+
* @param {HTMLElement | null} errorSummary - The error summary container
|
|
265
|
+
* @param {string | undefined} uploadId - The upload ID
|
|
266
|
+
* @returns {boolean} Whether the event was handled
|
|
267
|
+
*/
|
|
268
|
+
function handleAjaxFormSubmission(
|
|
269
|
+
event,
|
|
270
|
+
formElement,
|
|
271
|
+
fileInput,
|
|
272
|
+
uploadButton,
|
|
273
|
+
errorSummary,
|
|
274
|
+
uploadId
|
|
275
|
+
) {
|
|
276
|
+
if (!formElement.action || !uploadId) {
|
|
277
|
+
return false
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
event.preventDefault()
|
|
281
|
+
|
|
282
|
+
const formData = new FormData(formElement)
|
|
283
|
+
const isLocalDev = !!formElement.dataset.proxyUrl
|
|
284
|
+
const uploadUrl = formElement.dataset.proxyUrl ?? formElement.action
|
|
285
|
+
|
|
286
|
+
const fetchOptions = /** @type {RequestInit} */ ({
|
|
287
|
+
method: 'POST',
|
|
288
|
+
body: formData,
|
|
289
|
+
redirect: isLocalDev ? 'follow' : 'manual' // follow mode if local development with the proxy
|
|
290
|
+
})
|
|
291
|
+
|
|
292
|
+
// no-cors mode if needed local development with the proxy
|
|
293
|
+
if (isLocalDev) {
|
|
294
|
+
fetchOptions.mode = 'no-cors'
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
fetch(uploadUrl, fetchOptions)
|
|
298
|
+
.then(() => {
|
|
299
|
+
pollUploadStatus(uploadId)
|
|
300
|
+
})
|
|
301
|
+
.catch(() => {
|
|
302
|
+
fileInput.disabled = false
|
|
303
|
+
uploadButton.disabled = false
|
|
304
|
+
|
|
305
|
+
showError(
|
|
306
|
+
'There was a problem uploading the file',
|
|
307
|
+
errorSummary,
|
|
308
|
+
fileInput
|
|
309
|
+
)
|
|
310
|
+
|
|
311
|
+
return null
|
|
312
|
+
})
|
|
313
|
+
|
|
314
|
+
return true
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
export function initFileUpload() {
|
|
318
|
+
const form = document.querySelector('form:has(input[type="file"])')
|
|
319
|
+
/** @type {HTMLInputElement | null} */
|
|
320
|
+
const fileInput = form ? form.querySelector('input[type="file"]') : null
|
|
321
|
+
/** @type {HTMLButtonElement | null} */
|
|
322
|
+
const uploadButton = form ? form.querySelector('.upload-file-button') : null
|
|
323
|
+
const continueButton =
|
|
324
|
+
/** @type {HTMLButtonElement} */ (
|
|
325
|
+
Array.from(document.querySelectorAll('button.govuk-button')).find(
|
|
326
|
+
(button) => button.textContent?.trim() === 'Continue'
|
|
327
|
+
)
|
|
328
|
+
) ?? null
|
|
329
|
+
|
|
330
|
+
const errorSummary = document.querySelector('.govuk-error-summary-container')
|
|
331
|
+
|
|
332
|
+
if (!form || !fileInput || !uploadButton) {
|
|
333
|
+
return
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
const formElement = /** @type {HTMLFormElement} */ (form)
|
|
337
|
+
/** @type {File | null} */
|
|
338
|
+
let selectedFile = null
|
|
339
|
+
let isSubmitting = false
|
|
340
|
+
const uploadId = formElement.dataset.uploadId
|
|
341
|
+
|
|
342
|
+
fileInput.addEventListener('change', () => {
|
|
343
|
+
if (errorSummary) {
|
|
344
|
+
errorSummary.innerHTML = ''
|
|
345
|
+
}
|
|
346
|
+
if (fileInput.files && fileInput.files.length > 0) {
|
|
347
|
+
selectedFile = fileInput.files[0]
|
|
348
|
+
}
|
|
349
|
+
})
|
|
350
|
+
|
|
351
|
+
uploadButton.addEventListener('click', (event) => {
|
|
352
|
+
if (!selectedFile) {
|
|
353
|
+
event.preventDefault()
|
|
354
|
+
showError(
|
|
355
|
+
'Select a file',
|
|
356
|
+
/** @type {HTMLElement | null} */ (errorSummary),
|
|
357
|
+
fileInput
|
|
358
|
+
)
|
|
359
|
+
return
|
|
360
|
+
}
|
|
361
|
+
|
|
362
|
+
if (isSubmitting) {
|
|
363
|
+
event.preventDefault()
|
|
364
|
+
return
|
|
365
|
+
}
|
|
366
|
+
|
|
367
|
+
isSubmitting = true
|
|
368
|
+
|
|
369
|
+
handleStandardFormSubmission(
|
|
370
|
+
formElement,
|
|
371
|
+
fileInput,
|
|
372
|
+
uploadButton,
|
|
373
|
+
continueButton,
|
|
374
|
+
selectedFile
|
|
375
|
+
)
|
|
376
|
+
|
|
377
|
+
handleAjaxFormSubmission(
|
|
378
|
+
event,
|
|
379
|
+
formElement,
|
|
380
|
+
fileInput,
|
|
381
|
+
uploadButton,
|
|
382
|
+
/** @type {HTMLElement | null} */ (errorSummary),
|
|
383
|
+
uploadId
|
|
384
|
+
)
|
|
385
|
+
})
|
|
386
|
+
}
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
@use "govuk-frontend" as *;
|
|
2
|
+
@use "pkg:highlight.js/styles/github.css";
|
|
3
|
+
|
|
4
|
+
.app-code {
|
|
5
|
+
@include govuk-responsive-margin(8, "bottom");
|
|
6
|
+
|
|
7
|
+
&__container {
|
|
8
|
+
display: block;
|
|
9
|
+
margin: 0;
|
|
10
|
+
padding: govuk-spacing(4);
|
|
11
|
+
overflow-x: auto;
|
|
12
|
+
border: $govuk-focus-width solid transparent;
|
|
13
|
+
outline: 1px solid transparent;
|
|
14
|
+
background-color: govuk-colour("light-grey");
|
|
15
|
+
@include govuk-responsive-margin(4, "bottom");
|
|
16
|
+
|
|
17
|
+
&:focus {
|
|
18
|
+
border: $govuk-focus-width solid $govuk-input-border-colour;
|
|
19
|
+
outline: $govuk-focus-width solid $govuk-focus-colour;
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
&:last-child &__container {
|
|
24
|
+
margin-bottom: 0;
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
pre,
|
|
29
|
+
code {
|
|
30
|
+
$app-code-font: ui-monospace, menlo, "Cascadia Mono", "Segoe UI Mono", consolas, "Liberation Mono", monospace;
|
|
31
|
+
@include govuk-typography-common($font-family: $app-code-font);
|
|
32
|
+
@include govuk-font-size($size: 16);
|
|
33
|
+
}
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
@use "govuk-frontend" as *;
|
|
2
|
+
|
|
3
|
+
.app-prose-scope {
|
|
4
|
+
// @extend inheritance
|
|
5
|
+
|
|
6
|
+
// Contextual heading and paragraph combinations are inherited
|
|
7
|
+
// through the use of @extend
|
|
8
|
+
|
|
9
|
+
h1 {
|
|
10
|
+
@extend %govuk-heading-xl;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
h2 {
|
|
14
|
+
@extend %govuk-heading-l;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
h3 {
|
|
18
|
+
@extend %govuk-heading-m;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
h4 {
|
|
22
|
+
@extend %govuk-heading-s;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
p {
|
|
26
|
+
@extend %govuk-body-m;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
strong,
|
|
30
|
+
b {
|
|
31
|
+
@include govuk-typography-weight-bold;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
ul,
|
|
35
|
+
ol {
|
|
36
|
+
@extend %govuk-list;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
ol {
|
|
40
|
+
@extend %govuk-list--number;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
ul {
|
|
44
|
+
@extend %govuk-list--bullet;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
a {
|
|
48
|
+
@extend %govuk-link;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
hr {
|
|
52
|
+
@extend %govuk-section-break;
|
|
53
|
+
@extend %govuk-section-break--visible;
|
|
54
|
+
@extend %govuk-section-break--xl;
|
|
55
|
+
}
|
|
56
|
+
}
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
@use "govuk-frontend" as *;
|
|
2
|
+
|
|
3
|
+
.app-service-banner {
|
|
4
|
+
display: block;
|
|
5
|
+
background: govuk-colour("yellow");
|
|
6
|
+
color: govuk-colour("black");
|
|
7
|
+
|
|
8
|
+
@include govuk-responsive-padding(2, "top");
|
|
9
|
+
@include govuk-responsive-padding(2, "bottom");
|
|
10
|
+
|
|
11
|
+
&__content {
|
|
12
|
+
margin: 0;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
.govuk-warning-text__text {
|
|
16
|
+
font-weight: $govuk-font-weight-regular;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
.govuk-skip-link:active + &,
|
|
20
|
+
.govuk-skip-link:focus + & {
|
|
21
|
+
// Ofset service banner from skip link
|
|
22
|
+
margin-top: $govuk-border-width + $govuk-focus-width;
|
|
23
|
+
}
|
|
24
|
+
}
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
@use "govuk-frontend" as *;
|
|
2
|
+
|
|
3
|
+
// Taken from https://github.com/hmrc/hmrc-frontend/blob/main/src/components/summary-list/_summary-list.scss
|
|
4
|
+
@include govuk-media-query($from: tablet) {
|
|
5
|
+
.govuk-summary-list.govuk-summary-list--long-key {
|
|
6
|
+
.govuk-summary-list__row {
|
|
7
|
+
display: flex;
|
|
8
|
+
justify-content: space-between;
|
|
9
|
+
flex-wrap: wrap;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
.govuk-summary-list__key {
|
|
13
|
+
flex-grow: 1;
|
|
14
|
+
margin-bottom: 0;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
.govuk-summary-list__actions {
|
|
18
|
+
width: auto;
|
|
19
|
+
margin-bottom: 0;
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
.govuk-summary-list.govuk-summary-list--long-actions {
|
|
24
|
+
.govuk-summary-list__actions {
|
|
25
|
+
width: auto;
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
}
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
@use "govuk-frontend" as *;
|
|
2
|
+
|
|
3
|
+
.app-tag--env {
|
|
4
|
+
margin: -6px 0 0;
|
|
5
|
+
vertical-align: top;
|
|
6
|
+
|
|
7
|
+
// Override product name font size
|
|
8
|
+
@include govuk-font-size($size: 16);
|
|
9
|
+
|
|
10
|
+
// Align with product name
|
|
11
|
+
@include govuk-media-query($from: tablet) {
|
|
12
|
+
margin: -3px 0 0;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
// Override service header alignment
|
|
16
|
+
.cross-service-header & {
|
|
17
|
+
margin: -2px 0 0 govuk-spacing(1);
|
|
18
|
+
|
|
19
|
+
// Align with product name
|
|
20
|
+
@include govuk-media-query($from: tablet) {
|
|
21
|
+
margin: 0 0 0 govuk-spacing(2);
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
@use "govuk-frontend" as *;
|
|
2
|
+
@use "pkg:accessible-autocomplete";
|
|
3
|
+
@use "code";
|
|
4
|
+
@use "prose";
|
|
5
|
+
@use "service-banner";
|
|
6
|
+
@use "summary-list";
|
|
7
|
+
@use "tag-env";
|
|
8
|
+
|
|
9
|
+
// Use default GDS Transport font for autocomplete
|
|
10
|
+
.autocomplete__hint,
|
|
11
|
+
.autocomplete__input,
|
|
12
|
+
.autocomplete__option {
|
|
13
|
+
@include govuk-typography-common;
|
|
14
|
+
}
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
/**
|
|
2
|
+
@type {CookieConsent}
|
|
3
|
+
*/
|
|
4
|
+
export const defaultConsent = {
|
|
5
|
+
analytics: null,
|
|
6
|
+
dismissed: false
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* Parses the cookie consent policy
|
|
11
|
+
* @param {string} value
|
|
12
|
+
*/
|
|
13
|
+
export function parseCookieConsent(value) {
|
|
14
|
+
/** @type {CookieConsent} */
|
|
15
|
+
let cookieConsent
|
|
16
|
+
|
|
17
|
+
try {
|
|
18
|
+
const encodedValue = decodeURIComponent(value)
|
|
19
|
+
|
|
20
|
+
// eslint-disable-next-line -- Allow JSON type 'any'
|
|
21
|
+
const decodedValue = JSON.parse(encodedValue)
|
|
22
|
+
|
|
23
|
+
if (isValidConsent(decodedValue)) {
|
|
24
|
+
cookieConsent = decodedValue
|
|
25
|
+
} else {
|
|
26
|
+
cookieConsent = defaultConsent
|
|
27
|
+
}
|
|
28
|
+
} catch {
|
|
29
|
+
cookieConsent = defaultConsent
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
return cookieConsent
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* Serialises the cookie consent policy
|
|
37
|
+
* @param {CookieConsent} consent
|
|
38
|
+
* @returns {string} cookie value
|
|
39
|
+
*/
|
|
40
|
+
export function serialiseCookieConsent(consent) {
|
|
41
|
+
return encodeURIComponent(JSON.stringify(consent))
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* @param {unknown} consent
|
|
46
|
+
* @returns {consent is CookieConsent}
|
|
47
|
+
*/
|
|
48
|
+
function isValidConsent(consent) {
|
|
49
|
+
if (consent === null || Array.isArray(consent)) {
|
|
50
|
+
return false
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
return typeof consent === 'object' && 'analytics' in consent
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
/**
|
|
57
|
+
* @import {CookieConsent} from '~/src/common/types.js'
|
|
58
|
+
*/
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import { parseCookieConsent } from '~/src/common/cookies.js'
|
|
2
|
+
|
|
3
|
+
describe('cookies', () => {
|
|
4
|
+
it('parses a valid policy', () => {
|
|
5
|
+
expect(parseCookieConsent('{"analytics":true}')).toEqual({
|
|
6
|
+
analytics: true
|
|
7
|
+
})
|
|
8
|
+
})
|
|
9
|
+
|
|
10
|
+
it.each([
|
|
11
|
+
"['not', 'an', 'object']",
|
|
12
|
+
'{{ not: "an object" }}',
|
|
13
|
+
'{ additional: AAA }',
|
|
14
|
+
'{ marketing: 100 }',
|
|
15
|
+
'',
|
|
16
|
+
'null'
|
|
17
|
+
])('converts a malformed policy to the default', (value) => {
|
|
18
|
+
expect(parseCookieConsent(value)).toEqual({
|
|
19
|
+
analytics: null,
|
|
20
|
+
dismissed: false
|
|
21
|
+
})
|
|
22
|
+
})
|
|
23
|
+
})
|