From 41c67614603607e78b11c3fcfd9e7cdc3144f7eb Mon Sep 17 00:00:00 2001 From: Luke Zigler Date: Wed, 22 Nov 2023 07:47:20 +0000 Subject: [PATCH 01/29] updated designer to allow users to select image quality playback option --- designer/client/file-upload-field-edit.tsx | 45 ++++++++++++++++--- .../i18n/translations/en.translation.json | 4 ++ .../component/componentReducer.options.ts | 9 ++++ designer/client/reducers/component/types.ts | 1 + model/src/components/types.ts | 1 + 5 files changed, 54 insertions(+), 6 deletions(-) diff --git a/designer/client/file-upload-field-edit.tsx b/designer/client/file-upload-field-edit.tsx index 7be3f57af3..020fd44483 100644 --- a/designer/client/file-upload-field-edit.tsx +++ b/designer/client/file-upload-field-edit.tsx @@ -5,10 +5,15 @@ import { Actions } from "./reducers/component/types"; import { CssClasses } from "./components/CssClasses"; import { i18n } from "./i18n"; +const defaultOptions = { + multiple: false, + imageQualityPlayback: false, +}; + export function FileUploadFieldEdit() { const { state, dispatch } = useContext(ComponentContext); const { selectedComponent } = state; - const { options = {} } = selectedComponent; + const { options = defaultOptions } = selectedComponent; return (
@@ -22,21 +27,20 @@ export function FileUploadFieldEdit() {
{ - e.preventDefault(); dispatch({ type: Actions.EDIT_OPTIONS_FILE_UPLOAD_MULTIPLE, - payload: !options.multiple, + payload: e.target.checked, }); }} /> @@ -46,6 +50,35 @@ export function FileUploadFieldEdit() {
+
+
+ { + dispatch({ + type: Actions.EDIT_OPTIONS_IMAGE_QUALITY_PLAYBACK, + payload: e.target.checked, + }); + }} + /> + + + {i18n( + "fileUploadFieldEditPage.imageQualityPlaybackOption.helpText" + )} + +
+
+
); diff --git a/designer/client/i18n/translations/en.translation.json b/designer/client/i18n/translations/en.translation.json index b6e13fffe5..1657fcf028 100644 --- a/designer/client/i18n/translations/en.translation.json +++ b/designer/client/i18n/translations/en.translation.json @@ -208,6 +208,10 @@ "multipleFilesOption": { "helpText": "Tick this box to enable users to upload multiple files", "title": "Allow multiple file upload" + }, + "imageQualityPlaybackOption": { + "helpText": "If your document upload API assesses the quality of uploaded images, and you want to inform the user but don't want to stop them from continuing, check this box.", + "title": "Enable playback page for image quality checking" } }, "formDetails": { diff --git a/designer/client/reducers/component/componentReducer.options.ts b/designer/client/reducers/component/componentReducer.options.ts index 8b555af45b..0e0d14a49a 100644 --- a/designer/client/reducers/component/componentReducer.options.ts +++ b/designer/client/reducers/component/componentReducer.options.ts @@ -16,6 +16,8 @@ export function optionsReducer(state, action: OptionsActions) { const { type, payload } = action; const { selectedComponent } = state; const { options } = selectedComponent; + console.log("reducer type: ", type); + console.log("reducer payload: ", payload); switch (type) { case Options.EDIT_OPTIONS_HIDE_TITLE: return { @@ -53,6 +55,13 @@ export function optionsReducer(state, action: OptionsActions) { options: { ...options, multiple: payload }, }, }; + case Options.EDIT_OPTIONS_IMAGE_QUALITY_PLAYBACK: + return { + selectedComponent: { + ...selectedComponent, + options: { ...options, imageQualityPlayback: payload }, + }, + }; case Options.EDIT_OPTIONS_CLASSES: return { selectedComponent: { diff --git a/designer/client/reducers/component/types.ts b/designer/client/reducers/component/types.ts index b3c2c9f08f..a1e14657e3 100644 --- a/designer/client/reducers/component/types.ts +++ b/designer/client/reducers/component/types.ts @@ -40,6 +40,7 @@ export enum Options { EDIT_OPTIONS_REQUIRED = "EDIT_OPTIONS_REQUIRED", EDIT_OPTIONS_HIDE_OPTIONAL = "EDIT_OPTIONS_HIDE_OPTIONAL", EDIT_OPTIONS_FILE_UPLOAD_MULTIPLE = "EDIT_OPTIONS_FILE_UPLOAD_MULTIPLE", + EDIT_OPTIONS_IMAGE_QUALITY_PLAYBACK = "EDIT_OPTIONS_IMAGE_QUALITY_PLAYBACK", EDIT_OPTIONS_CLASSES = "EDIT_OPTIONS_CLASSES", EDIT_OPTIONS_MAX_DAYS_IN_FUTURE = "EDIT_OPTIONS_MAX_DAYS_IN_FUTURE", EDIT_OPTIONS_MAX_DAYS_IN_PAST = "EDIT_OPTIONS_MAX_DAYS_IN_PAST", diff --git a/model/src/components/types.ts b/model/src/components/types.ts index 3121a5e1d4..fcb283b0aa 100644 --- a/model/src/components/types.ts +++ b/model/src/components/types.ts @@ -217,6 +217,7 @@ export interface FileUploadFieldComponent { multiple?: boolean; classes?: string; exposeToContext?: boolean; + imageQualityPlayback?: boolean; }; schema: {}; } From 446c013dd3c8299806f4e5bfb95e23718fcfefc9 Mon Sep 17 00:00:00 2001 From: Luke Zigler Date: Wed, 22 Nov 2023 15:23:28 +0000 Subject: [PATCH 02/29] Added template for upload playback --- runner/src/server/views/upload-playback.html | 57 ++++++++++++++++++++ 1 file changed, 57 insertions(+) create mode 100644 runner/src/server/views/upload-playback.html diff --git a/runner/src/server/views/upload-playback.html b/runner/src/server/views/upload-playback.html new file mode 100644 index 0000000000..6a8d30259c --- /dev/null +++ b/runner/src/server/views/upload-playback.html @@ -0,0 +1,57 @@ +{% from "error-summary/macro.njk" import govukErrorSummary %} +{% from "notification-banner/macro.njk" import govukNotificationBanner %} +{% from "details/macro.njk" import govukDetails %} +{% from "radios/macro.njk" import govukRadios %} +{% from "button/macro.njk" import govukButton %} + +{% extends "layout.html" %} + +{% set infoHtml %} +

+ It looks like your birth certificate image is too blurred. Check your image and upload another one if needed. +

+{% endset %} + +{% set takePhotoHtml %} +

+ +{% endset %} + +{% block content %} +
+
+

+ Check your {{ fieldName }} +

+ {% if uploadErrors %} + {{ govukErrorSummary(uploadErrors) }} + {% endif %} + + {% include "partials/heading.html" %} +
+ + {{ govukNotificationBanner({ + html: infoHtml + }) + }} + +

All the information in the image must be readable. If not, you may be asked to send another copy, which could delay your application.

+ + {{ govukDetails({ + summaryText: "How to take a good photograph", + html: takePhotoHtml + }) + }} + + {{ govukRadios(radios) }} + {{ govukButton({ attributes: { id: "submit" }, text: "Continue" }) }} +
+
+
+{% endblock %} + From 89a1832404796f7c646fb36c4aeeec5e329af996 Mon Sep 17 00:00:00 2001 From: Luke Zigler Date: Wed, 22 Nov 2023 16:24:27 +0000 Subject: [PATCH 03/29] Added page controllers for upload field pages --- .../SummaryUploadPageController.ts | 112 ++++++++++++++++++ .../pageControllers/UploadPageController.ts | 62 ++++++++++ 2 files changed, 174 insertions(+) create mode 100644 runner/src/server/plugins/engine/pageControllers/SummaryUploadPageController.ts create mode 100644 runner/src/server/plugins/engine/pageControllers/UploadPageController.ts diff --git a/runner/src/server/plugins/engine/pageControllers/SummaryUploadPageController.ts b/runner/src/server/plugins/engine/pageControllers/SummaryUploadPageController.ts new file mode 100644 index 0000000000..dc737aa84e --- /dev/null +++ b/runner/src/server/plugins/engine/pageControllers/SummaryUploadPageController.ts @@ -0,0 +1,112 @@ +import { PageController } from "server/plugins/engine/pageControllers/PageController"; +import { FormModel } from "server/plugins/engine/models"; +import { Page } from "@xgovformbuilder/model"; +import { FormComponent } from "server/plugins/engine/components"; +import { + HapiLifecycleMethod, + HapiRequest, + HapiResponseToolkit, +} from "server/types"; +import { FormData } from "../types"; + +export class SummaryUploadPageController extends PageController { + inputComponent: FormComponent; + private getRoute!: HapiLifecycleMethod; + private postRoute!: HapiLifecycleMethod; + + constructor(model: FormModel, pageDef: Page, inputComponent: FormComponent) { + super(model, pageDef); + this.inputComponent = inputComponent; + } + + get getRouteHandler() { + this.getRoute ??= this.makeGetRouteHandler(); + return this.getRoute; + } + get postRouteHandler() { + this.postRoute ??= this.makePostRouteHandler(); + return this.postRoute; + } + + buildRadioViewModel(error?: string) { + const standardViewModel = { + name: "retryUpload", + fieldset: { + legend: { + text: "Would you like to upload a new image?", + isPageHeading: false, + classes: "govuk-fieldset__legend--s", + }, + }, + items: [ + { + value: "true", + text: "Yes - I would like to upload a new image", + }, + { + value: "false", + text: "No - I am happy with the image", + }, + ], + }; + if (error) { + return { + errorMessage: { + text: error, + }, + ...standardViewModel, + }; + } + return standardViewModel; + } + + makeGetRouteHandler() { + return async (request: HapiRequest, h: HapiResponseToolkit) => { + const { cacheService } = request.services([]); + + const state = await cacheService.getState(request); + const { progress = [] } = state; + return h.view("upload-playback", { + fieldName: this.inputComponent.title, + backLink: progress[progress.length - 1] ?? this.backLinkFallback, + radios: this.buildRadioViewModel(), + }); + }; + } + + makePostRouteHandler() { + return async (request: HapiRequest, h: HapiResponseToolkit) => { + const { cacheService } = request.services([]); + + const state = await cacheService.getState(request); + const { progress = [] } = state; + const payload = request.payload; + if (!payload.retryUpload) { + const errorText = "Select if you would like to continue"; + const errors = { + titleText: "Fix the following errors", + errorList: [ + { + text: errorText, + href: "#retry-upload", + }, + ], + }; + return h.view("upload-playback", { + uploadErrors: errors, + backLink: progress[progress.length - 2] ?? this.backLinkFallback, + radios: this.buildRadioViewModel(errorText), + fieldName: this.inputComponent.title, + }); + } + + if (payload.retryUpload === "true") { + return h.redirect(`/${this.model.basePath}${this.path}`); + } + + delete payload.retryUpload; + + return h.redirect(this.getNext(payload)); + }; + } +} diff --git a/runner/src/server/plugins/engine/pageControllers/UploadPageController.ts b/runner/src/server/plugins/engine/pageControllers/UploadPageController.ts new file mode 100644 index 0000000000..429814e09b --- /dev/null +++ b/runner/src/server/plugins/engine/pageControllers/UploadPageController.ts @@ -0,0 +1,62 @@ +import { PageController } from "server/plugins/engine/pageControllers/PageController"; +import { FormModel } from "server/plugins/engine/models"; +import { HapiRequest, HapiResponseToolkit } from "server/types"; +import { SummaryUploadPageController } from "server/plugins/engine/pageControllers/SummaryUploadPageController"; +import { FormComponent } from "server/plugins/engine/components"; + +function isUploadField(component: FormComponent) { + return component.type === "FileUploadField"; +} + +export class UploadPageController extends PageController { + summary: SummaryUploadPageController; + inputComponent: FormComponent; + constructor(model: FormModel, pageDef: any) { + super(model, pageDef); + const inputComponent = this.components?.items?.find(isUploadField); + if (!inputComponent) { + throw Error( + "UploadPageController initialisation failed, no file upload component was found" + ); + } + this.summary = new SummaryUploadPageController( + model, + pageDef, + inputComponent as FormComponent + ); + this.inputComponent = inputComponent as FormComponent; + } + + makeGetRouteHandler() { + return async (request: HapiRequest, h: HapiResponseToolkit) => { + const { query } = request; + const { view } = query; + + if (view === "summary") { + return this.summary.getRouteHandler(request, h); + } + + return super.makeGetRouteHandler()(request, h); + }; + } + + makePostRouteHandler() { + return async (request: HapiRequest, h: HapiResponseToolkit) => { + const { query } = request; + + if (query.view === "summary") { + return this.summary.postRouteHandler(request, h); + } + if (request?.pre?.errors) { + if ( + request?.pre?.errorCode === "qualityError" && + this.inputComponent.options?.imageQualityPlayback + ) { + return h.redirect(`?view=summary`); + } + } + + return super.makePostRouteHandler()(request, h); + }; + } +} From 7199531e59468281661ef70f6630bfac1865bdfd Mon Sep 17 00:00:00 2001 From: Luke Zigler Date: Wed, 22 Nov 2023 16:24:59 +0000 Subject: [PATCH 04/29] Added new UploadPageController to controllers list --- runner/src/server/plugins/engine/pageControllers/helpers.ts | 2 ++ 1 file changed, 2 insertions(+) diff --git a/runner/src/server/plugins/engine/pageControllers/helpers.ts b/runner/src/server/plugins/engine/pageControllers/helpers.ts index 74b48293fb..5659d700ad 100644 --- a/runner/src/server/plugins/engine/pageControllers/helpers.ts +++ b/runner/src/server/plugins/engine/pageControllers/helpers.ts @@ -9,6 +9,7 @@ import { SummaryPageController } from "./SummaryPageController"; import { PageControllerBase } from "./PageControllerBase"; import { RepeatingFieldPageController } from "./RepeatingFieldPageController"; import { Page } from "@xgovformbuilder/model"; +import { UploadPageController } from "server/plugins/engine/pageControllers/UploadPageController"; const PageControllers = { DobPageController, @@ -19,6 +20,7 @@ const PageControllers = { SummaryPageController, PageControllerBase, RepeatingFieldPageController, + UploadPageController, }; export const controllerNameFromPath = (filePath: string) => { From 784df1f31555f741cae01f7db05a7c180d3bf8b6 Mon Sep 17 00:00:00 2001 From: Luke Zigler Date: Thu, 23 Nov 2023 17:03:57 +0000 Subject: [PATCH 05/29] Updated UploadPageController to include form validation logic --- .../pageControllers/PageControllerBase.ts | 73 ++++++++++++------- ...ler.ts => PlaybackUploadPageController.ts} | 0 .../pageControllers/UploadPageController.ts | 38 ++++++---- 3 files changed, 70 insertions(+), 41 deletions(-) rename runner/src/server/plugins/engine/pageControllers/{SummaryUploadPageController.ts => PlaybackUploadPageController.ts} (100%) diff --git a/runner/src/server/plugins/engine/pageControllers/PageControllerBase.ts b/runner/src/server/plugins/engine/pageControllers/PageControllerBase.ts index f92e8a932c..2bde40269d 100644 --- a/runner/src/server/plugins/engine/pageControllers/PageControllerBase.ts +++ b/runner/src/server/plugins/engine/pageControllers/PageControllerBase.ts @@ -21,7 +21,7 @@ import { FormSubmissionErrors, FormSubmissionState, } from "../types"; -import { ComponentCollectionViewModel } from "../components/types"; +import { ComponentCollectionViewModel, ViewModel } from "../components/types"; import { format, parseISO } from "date-fns"; import config from "server/config"; import nunjucks from "nunjucks"; @@ -506,37 +506,29 @@ export class PageControllerBase { }; } - /** - * deals with parsing errors and saving answers to state - */ - async handlePostRequest( + processUploads( + state: FormSubmissionState, request: HapiRequest, - h: HapiResponseToolkit, - mergeOptions: { - nullOverride?: boolean; - arrayMerge?: boolean; - /** - * if you wish to modify the value just before it is added to the user's session (i.e. after validation and error parsing), use the modifyUpdate method. - * pass in a function, that takes in the update value. Make sure that this returns the modified value. - */ - modifyUpdate?: (value: T) => any; - } = {} + formResult: any ) { - const { cacheService } = request.services([]); - const hasFilesizeError = request.payload === null; - const preHandlerErrors = request.pre.errors; - const payload = (request.payload || {}) as FormData; - const formResult: any = this.validateForm(payload); - const state = await cacheService.getState(request); const originalFilenames = (state || {}).originalFilenames || {}; const fileFields = this.getViewModel(formResult) .components.filter((component) => component.type === "FileUploadField") .map((component) => component.model); - const progress = state.progress || []; - const { num } = request.query; + formResult = this.validateUploads(request, fileFields, formResult); + if (formResult.errors) { + return formResult; + } + this.replaceFilenames(request.payload, originalFilenames); + return formResult; + } - // TODO:- Refactor this into a validation method - if (hasFilesizeError) { + validateUploads( + request: HapiRequest, + fileFields: ViewModel[], + formResult: any + ) { + if (request.payload === null) { const reformattedErrors = fileFields.map((field) => { return { path: field.name, @@ -551,7 +543,7 @@ export class PageControllerBase { : formResult.errors; formResult.errors.errorList = reformattedErrors; } - + const preHandlerErrors = request.pre.errors; /** * other file related errors.. assuming file fields will be on their own page. This will replace all other errors from the page if not.. */ @@ -578,12 +570,41 @@ export class PageControllerBase { : formResult.errors; formResult.errors.errorList = reformattedErrors; } + return formResult; + } + replaceFilenames(payload: object, originalFilenames: string[]) { Object.entries(payload).forEach(([key, value]) => { if (value && value === (originalFilenames[key] || {}).location) { payload[key] = originalFilenames[key].originalFilename; } }); + } + + /** + * deals with parsing errors and saving answers to state + */ + async handlePostRequest( + request: HapiRequest, + h: HapiResponseToolkit, + mergeOptions: { + nullOverride?: boolean; + arrayMerge?: boolean; + /** + * if you wish to modify the value just before it is added to the user's session (i.e. after validation and error parsing), use the modifyUpdate method. + * pass in a function, that takes in the update value. Make sure that this returns the modified value. + */ + modifyUpdate?: (value: T) => any; + } = {} + ) { + const { cacheService } = request.services([]); + const payload = (request.payload || {}) as FormData; + let formResult: any = this.validateForm(payload); + const state = await cacheService.getState(request); + const progress = state.progress || []; + const { num } = request.query; + + formResult = this.processUploads(state, request, formResult); /** * If there are any errors, render the page with the parsed errors diff --git a/runner/src/server/plugins/engine/pageControllers/SummaryUploadPageController.ts b/runner/src/server/plugins/engine/pageControllers/PlaybackUploadPageController.ts similarity index 100% rename from runner/src/server/plugins/engine/pageControllers/SummaryUploadPageController.ts rename to runner/src/server/plugins/engine/pageControllers/PlaybackUploadPageController.ts diff --git a/runner/src/server/plugins/engine/pageControllers/UploadPageController.ts b/runner/src/server/plugins/engine/pageControllers/UploadPageController.ts index 429814e09b..6782b571c2 100644 --- a/runner/src/server/plugins/engine/pageControllers/UploadPageController.ts +++ b/runner/src/server/plugins/engine/pageControllers/UploadPageController.ts @@ -1,7 +1,7 @@ import { PageController } from "server/plugins/engine/pageControllers/PageController"; import { FormModel } from "server/plugins/engine/models"; import { HapiRequest, HapiResponseToolkit } from "server/types"; -import { SummaryUploadPageController } from "server/plugins/engine/pageControllers/SummaryUploadPageController"; +import { PlaybackUploadPageController } from "server/plugins/engine/pageControllers/PlaybackUploadPageController"; import { FormComponent } from "server/plugins/engine/components"; function isUploadField(component: FormComponent) { @@ -9,7 +9,7 @@ function isUploadField(component: FormComponent) { } export class UploadPageController extends PageController { - summary: SummaryUploadPageController; + playback: PlaybackUploadPageController; inputComponent: FormComponent; constructor(model: FormModel, pageDef: any) { super(model, pageDef); @@ -19,7 +19,7 @@ export class UploadPageController extends PageController { "UploadPageController initialisation failed, no file upload component was found" ); } - this.summary = new SummaryUploadPageController( + this.playback = new PlaybackUploadPageController( model, pageDef, inputComponent as FormComponent @@ -32,8 +32,8 @@ export class UploadPageController extends PageController { const { query } = request; const { view } = query; - if (view === "summary") { - return this.summary.getRouteHandler(request, h); + if (view === "playback") { + return this.playback.getRouteHandler(request, h); } return super.makeGetRouteHandler()(request, h); @@ -44,19 +44,27 @@ export class UploadPageController extends PageController { return async (request: HapiRequest, h: HapiResponseToolkit) => { const { query } = request; - if (query.view === "summary") { - return this.summary.postRouteHandler(request, h); + if (query.view === "playback") { + return this.playback.postRouteHandler(request, h); } - if (request?.pre?.errors) { - if ( - request?.pre?.errorCode === "qualityError" && - this.inputComponent.options?.imageQualityPlayback - ) { - return h.redirect(`?view=summary`); - } + + const response = await this.handlePostRequest(request, h); + if (response?.source?.context?.errors) { + return response; + } + const { cacheService } = request.services([]); + const savedState = await cacheService.getState(request); + //This is required to ensure we don't navigate to an incorrect page based on stale state values + let relevantState = this.getConditionEvaluationContext( + this.model, + savedState + ); + + if (request?.pre?.warningFromApi === "qualityWarning") { + return h.redirect(`?view=playback`); } - return super.makePostRouteHandler()(request, h); + return this.proceed(request, h, relevantState); }; } } From 2acb45d79fbbe6da115693e3c91b7f35b83451ba Mon Sep 17 00:00:00 2001 From: Luke Zigler Date: Thu, 23 Nov 2023 17:05:09 +0000 Subject: [PATCH 06/29] Updated playback image page controller to run the standard post route handler after checking whether a redirect is needed --- .../engine/pageControllers/PlaybackUploadPageController.ts | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/runner/src/server/plugins/engine/pageControllers/PlaybackUploadPageController.ts b/runner/src/server/plugins/engine/pageControllers/PlaybackUploadPageController.ts index dc737aa84e..2e0154843a 100644 --- a/runner/src/server/plugins/engine/pageControllers/PlaybackUploadPageController.ts +++ b/runner/src/server/plugins/engine/pageControllers/PlaybackUploadPageController.ts @@ -7,9 +7,8 @@ import { HapiRequest, HapiResponseToolkit, } from "server/types"; -import { FormData } from "../types"; -export class SummaryUploadPageController extends PageController { +export class PlaybackUploadPageController extends PageController { inputComponent: FormComponent; private getRoute!: HapiLifecycleMethod; private postRoute!: HapiLifecycleMethod; @@ -106,7 +105,7 @@ export class SummaryUploadPageController extends PageController { delete payload.retryUpload; - return h.redirect(this.getNext(payload)); + return super.makePostRouteHandler()(request, h); }; } } From a5905b0b621166298a588530b071311e6665fa23 Mon Sep 17 00:00:00 2001 From: Luke Zigler Date: Thu, 23 Nov 2023 17:08:05 +0000 Subject: [PATCH 07/29] Reverted PageControllerBase --- .../pageControllers/PageControllerBase.ts | 75 +++++++------------ 1 file changed, 28 insertions(+), 47 deletions(-) diff --git a/runner/src/server/plugins/engine/pageControllers/PageControllerBase.ts b/runner/src/server/plugins/engine/pageControllers/PageControllerBase.ts index 2bde40269d..e2944dda93 100644 --- a/runner/src/server/plugins/engine/pageControllers/PageControllerBase.ts +++ b/runner/src/server/plugins/engine/pageControllers/PageControllerBase.ts @@ -21,7 +21,7 @@ import { FormSubmissionErrors, FormSubmissionState, } from "../types"; -import { ComponentCollectionViewModel, ViewModel } from "../components/types"; +import { ComponentCollectionViewModel } from "../components/types"; import { format, parseISO } from "date-fns"; import config from "server/config"; import nunjucks from "nunjucks"; @@ -500,35 +500,44 @@ export class PageControllerBase { await cacheService.mergeState(request, { progress }); + viewModel.backLink = progress[progress.length - 2]; viewModel.backLink = progress[progress.length - 2] ?? this.backLinkFallback; return h.view(this.viewName, viewModel); }; } - processUploads( - state: FormSubmissionState, + /** + * deals with parsing errors and saving answers to state + */ + async handlePostRequest( request: HapiRequest, - formResult: any + h: HapiResponseToolkit, + mergeOptions: { + nullOverride?: boolean; + arrayMerge?: boolean; + /** + * if you wish to modify the value just before it is added to the user's session (i.e. after validation and error parsing), use the modifyUpdate method. + * pass in a function, that takes in the update value. Make sure that this returns the modified value. + */ + modifyUpdate?: (value: T) => any; + } = {} ) { + const { cacheService } = request.services([]); + const hasFilesizeError = request.payload === null; + const preHandlerErrors = request.pre.errors; + const payload = (request.payload || {}) as FormData; + const formResult: any = this.validateForm(payload); + const state = await cacheService.getState(request); const originalFilenames = (state || {}).originalFilenames || {}; const fileFields = this.getViewModel(formResult) .components.filter((component) => component.type === "FileUploadField") .map((component) => component.model); - formResult = this.validateUploads(request, fileFields, formResult); - if (formResult.errors) { - return formResult; - } - this.replaceFilenames(request.payload, originalFilenames); - return formResult; - } + const progress = state.progress || []; + const { num } = request.query; - validateUploads( - request: HapiRequest, - fileFields: ViewModel[], - formResult: any - ) { - if (request.payload === null) { + // TODO:- Refactor this into a validation method + if (hasFilesizeError) { const reformattedErrors = fileFields.map((field) => { return { path: field.name, @@ -543,7 +552,7 @@ export class PageControllerBase { : formResult.errors; formResult.errors.errorList = reformattedErrors; } - const preHandlerErrors = request.pre.errors; + /** * other file related errors.. assuming file fields will be on their own page. This will replace all other errors from the page if not.. */ @@ -570,41 +579,12 @@ export class PageControllerBase { : formResult.errors; formResult.errors.errorList = reformattedErrors; } - return formResult; - } - replaceFilenames(payload: object, originalFilenames: string[]) { Object.entries(payload).forEach(([key, value]) => { if (value && value === (originalFilenames[key] || {}).location) { payload[key] = originalFilenames[key].originalFilename; } }); - } - - /** - * deals with parsing errors and saving answers to state - */ - async handlePostRequest( - request: HapiRequest, - h: HapiResponseToolkit, - mergeOptions: { - nullOverride?: boolean; - arrayMerge?: boolean; - /** - * if you wish to modify the value just before it is added to the user's session (i.e. after validation and error parsing), use the modifyUpdate method. - * pass in a function, that takes in the update value. Make sure that this returns the modified value. - */ - modifyUpdate?: (value: T) => any; - } = {} - ) { - const { cacheService } = request.services([]); - const payload = (request.payload || {}) as FormData; - let formResult: any = this.validateForm(payload); - const state = await cacheService.getState(request); - const progress = state.progress || []; - const { num } = request.query; - - formResult = this.processUploads(state, request, formResult); /** * If there are any errors, render the page with the parsed errors @@ -821,6 +801,7 @@ export class PageControllerBase { private renderWithErrors(request, h, payload, num, progress, errors) { const viewModel = this.getViewModel(payload, num, errors); + viewModel.backLink = progress[progress.length - 2]; viewModel.backLink = progress[progress.length - 2] ?? this.backLinkFallback; this.setPhaseTag(viewModel); this.setFeedbackDetails(viewModel, request); From 45dc8e2f09bfdf26c1e066d4af54976c0ec95d62 Mon Sep 17 00:00:00 2001 From: Luke Zigler Date: Fri, 24 Nov 2023 10:16:39 +0000 Subject: [PATCH 08/29] Updated upload service to process warnings in document upload responses --- runner/src/server/services/uploadService.ts | 14 ++++++++++---- 1 file changed, 10 insertions(+), 4 deletions(-) diff --git a/runner/src/server/services/uploadService.ts b/runner/src/server/services/uploadService.ts index 299ba6f446..126fc80a0c 100644 --- a/runner/src/server/services/uploadService.ts +++ b/runner/src/server/services/uploadService.ts @@ -19,7 +19,6 @@ const ERRORS = { fileSizeError: 'The selected file for "%s" is too large', fileTypeError: "Invalid file type. Upload a PNG, JPG or PDF", virusError: 'The selected file for "%s" contained a virus', - qualityError: 'The selected file for "%s" was too blurry', default: "There was an error uploading your file", }; @@ -72,9 +71,10 @@ export class UploadService { } parsedDocumentUploadResponse({ res, payload }) { - const errorCodeFromApi = payload?.toString?.(); + const warningFromApi = payload?.toString?.(); let error: string | undefined; let location: string | undefined; + console.log("This is the payload", warningFromApi); switch (res.statusCode) { case 201: location = res.headers.location; @@ -86,7 +86,7 @@ export class UploadService { error = ERRORS.fileSizeError; break; case 422: - error = ERRORS[errorCodeFromApi] ?? ERRORS.virusError; + error = ERRORS.virusError; break; default: error = ERRORS.default; @@ -95,6 +95,7 @@ export class UploadService { return { location, error, + warningFromApi, }; } @@ -181,10 +182,15 @@ export class UploadService { if (validFiles.length === values.length) { try { - const { error, location } = await this.uploadDocuments(validFiles); + const { + error, + location, + warningFromApi, + } = await this.uploadDocuments(validFiles); if (location) { originalFilenames[key] = { location }; request.payload[key] = location; + request.pre.warningFromApi = warningFromApi; } if (error) { request.pre.errors = [ From 6e5be90313128f5ee6789cb1e95bbe82b8d11b0c Mon Sep 17 00:00:00 2001 From: Luke Zigler Date: Fri, 24 Nov 2023 12:52:53 +0000 Subject: [PATCH 09/29] Moved makeRouteHandler functions to be accessible outside of getters --- .../PlaybackUploadPageController.ts | 17 +---------------- 1 file changed, 1 insertion(+), 16 deletions(-) diff --git a/runner/src/server/plugins/engine/pageControllers/PlaybackUploadPageController.ts b/runner/src/server/plugins/engine/pageControllers/PlaybackUploadPageController.ts index 2e0154843a..a29158f9cc 100644 --- a/runner/src/server/plugins/engine/pageControllers/PlaybackUploadPageController.ts +++ b/runner/src/server/plugins/engine/pageControllers/PlaybackUploadPageController.ts @@ -2,31 +2,16 @@ import { PageController } from "server/plugins/engine/pageControllers/PageContro import { FormModel } from "server/plugins/engine/models"; import { Page } from "@xgovformbuilder/model"; import { FormComponent } from "server/plugins/engine/components"; -import { - HapiLifecycleMethod, - HapiRequest, - HapiResponseToolkit, -} from "server/types"; +import { HapiRequest, HapiResponseToolkit } from "server/types"; export class PlaybackUploadPageController extends PageController { inputComponent: FormComponent; - private getRoute!: HapiLifecycleMethod; - private postRoute!: HapiLifecycleMethod; constructor(model: FormModel, pageDef: Page, inputComponent: FormComponent) { super(model, pageDef); this.inputComponent = inputComponent; } - get getRouteHandler() { - this.getRoute ??= this.makeGetRouteHandler(); - return this.getRoute; - } - get postRouteHandler() { - this.postRoute ??= this.makePostRouteHandler(); - return this.postRoute; - } - buildRadioViewModel(error?: string) { const standardViewModel = { name: "retryUpload", From 2b3cc8b8747b678d3d214a692ab78f7d0aff1d03 Mon Sep 17 00:00:00 2001 From: Luke Zigler Date: Fri, 24 Nov 2023 12:53:48 +0000 Subject: [PATCH 10/29] Updated UploadPageController to use new playback page route handler functionality --- .../engine/pageControllers/UploadPageController.ts | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/runner/src/server/plugins/engine/pageControllers/UploadPageController.ts b/runner/src/server/plugins/engine/pageControllers/UploadPageController.ts index 6782b571c2..3b6858e8fb 100644 --- a/runner/src/server/plugins/engine/pageControllers/UploadPageController.ts +++ b/runner/src/server/plugins/engine/pageControllers/UploadPageController.ts @@ -33,7 +33,7 @@ export class UploadPageController extends PageController { const { view } = query; if (view === "playback") { - return this.playback.getRouteHandler(request, h); + return this.playback.makeGetRouteHandler()(request, h); } return super.makeGetRouteHandler()(request, h); @@ -44,14 +44,18 @@ export class UploadPageController extends PageController { return async (request: HapiRequest, h: HapiResponseToolkit) => { const { query } = request; - if (query.view === "playback") { - return this.playback.postRouteHandler(request, h); + if (query?.view === "playback") { + return this.playback.makePostRouteHandler()(request, h); } const response = await this.handlePostRequest(request, h); if (response?.source?.context?.errors) { return response; } + if (request?.pre?.warningFromApi === "qualityWarning") { + return h.redirect(`?view=playback`); + } + const { cacheService } = request.services([]); const savedState = await cacheService.getState(request); //This is required to ensure we don't navigate to an incorrect page based on stale state values @@ -60,10 +64,6 @@ export class UploadPageController extends PageController { savedState ); - if (request?.pre?.warningFromApi === "qualityWarning") { - return h.redirect(`?view=playback`); - } - return this.proceed(request, h, relevantState); }; } From 8d0c00480efedaab6ce89157cb99d4fb29eb6a88 Mon Sep 17 00:00:00 2001 From: Luke Zigler Date: Fri, 24 Nov 2023 12:54:15 +0000 Subject: [PATCH 11/29] Added unit tests for UploadPageController --- .../UploadPageController.test.ts | 114 ++++++++++++++++++ 1 file changed, 114 insertions(+) create mode 100644 runner/test/cases/server/plugins/engine/pageControllers/UploadPageController.test.ts diff --git a/runner/test/cases/server/plugins/engine/pageControllers/UploadPageController.test.ts b/runner/test/cases/server/plugins/engine/pageControllers/UploadPageController.test.ts new file mode 100644 index 0000000000..8392ee32dc --- /dev/null +++ b/runner/test/cases/server/plugins/engine/pageControllers/UploadPageController.test.ts @@ -0,0 +1,114 @@ +import * as Code from "@hapi/code"; +import * as Lab from "@hapi/lab"; +import { UploadPageController } from "../../../../../../src/server/plugins/engine/pageControllers/UploadPageController"; +import { FormModel } from "../../../../../../src/server/plugins/engine/models"; +import * as sinon from "sinon"; +import * as PlaybackUploadPageController from "../../../../../../src/server/plugins/engine/pageControllers/PlaybackUploadPageController"; +import { Page } from "@xgovformbuilder/model"; +import { FormComponent } from "../../../../../../src/server/plugins/engine/components"; + +const lab = Lab.script(); +exports.lab = lab; +const { expect } = Code; +const { suite, test } = lab; + +const def = { + title: "Your birth certificate", + path: "/your-birth-certificate", + name: "", + components: [ + { + name: "imageUpload", + options: { + required: true, + }, + type: "FileUploadField", + title: "Birth certificate", + schema: {}, + }, + ], + next: [ + { + path: "/second-page", + }, + ], + controller: "UploadPageController", +}; + +const model = new FormModel( + { + pages: [], + startPage: "/start", + sections: [], + lists: [], + conditions: [], + }, + {} +); + +suite("UploadPageController", () => { + lab.before(() => { + class mockPlaybackPageController { + constructor( + _model: FormModel, + _pageDef: Page, + _inputComponent: FormComponent + ) {} + makePostRouteHandler() { + return sinon.stub().returns(true); + } + makeGetRouteHandler() { + return sinon.stub().returns(true); + } + } + sinon + .stub(PlaybackUploadPageController, "PlaybackUploadPageController") + .callsFake((model, pageDef, inputComponent) => { + return new mockPlaybackPageController(model, pageDef, inputComponent); + }); + }); + + test("Redirects post handler to the playback page post handler when view=playback", async () => { + const pageController = new UploadPageController(model, def); + const request = { + query: { + view: "playback", + }, + }; + const result = await pageController.makePostRouteHandler()(request, {}); + expect(result).to.be.true(); + }); + test("Redirects get handler to the playback page get handler when view=playback", async () => { + const pageController = new UploadPageController(model, def); + const request = { + query: { + view: "playback", + }, + }; + const result = await pageController.makeGetRouteHandler()(request, {}); + expect(result).to.be.true(); + }); + test("Redirects to ?view=playback if a quality warning is passed through", async () => { + const pageController = new UploadPageController(model, def); + const request = { + query: {}, + pre: { + warningFromApi: "qualityWarning", + }, + services: () => ({ + cacheService: { + getState: (_request) => ({}), + }, + }), + }; + const responseObj = { + redirect: (string: string) => string, + view: (_tpl, _options) => true, + }; + const result = await pageController.makePostRouteHandler()( + request, + responseObj + ); + expect(result).to.equal("?view=playback"); + }); +}); From 21e7fd1cb45f6d2f4ef6ca02a08d797a1a9f92c0 Mon Sep 17 00:00:00 2001 From: Luke Zigler Date: Fri, 24 Nov 2023 13:53:53 +0000 Subject: [PATCH 12/29] Made some content tweaks to OCR playback page --- .../pageControllers/PlaybackUploadPageController.ts | 11 ++++++++++- runner/src/server/views/upload-playback.html | 8 ++------ 2 files changed, 12 insertions(+), 7 deletions(-) diff --git a/runner/src/server/plugins/engine/pageControllers/PlaybackUploadPageController.ts b/runner/src/server/plugins/engine/pageControllers/PlaybackUploadPageController.ts index a29158f9cc..dca8d23364 100644 --- a/runner/src/server/plugins/engine/pageControllers/PlaybackUploadPageController.ts +++ b/runner/src/server/plugins/engine/pageControllers/PlaybackUploadPageController.ts @@ -50,8 +50,12 @@ export class PlaybackUploadPageController extends PageController { const state = await cacheService.getState(request); const { progress = [] } = state; + let sectionTitle = this.section?.title; + let pageTitle = `Check your ${this.inputComponent.title} image`; return h.view("upload-playback", { - fieldName: this.inputComponent.title, + sectionTitle: sectionTitle, + showTitle: true, + pageTitle: pageTitle, backLink: progress[progress.length - 1] ?? this.backLinkFallback, radios: this.buildRadioViewModel(), }); @@ -76,7 +80,12 @@ export class PlaybackUploadPageController extends PageController { }, ], }; + let sectionTitle = this.section?.title; + let pageTitle = `Check your ${this.inputComponent.title} image`; return h.view("upload-playback", { + sectionTitle: sectionTitle, + showTitle: true, + pageTitle: pageTitle, uploadErrors: errors, backLink: progress[progress.length - 2] ?? this.backLinkFallback, radios: this.buildRadioViewModel(errorText), diff --git a/runner/src/server/views/upload-playback.html b/runner/src/server/views/upload-playback.html index 6a8d30259c..7c15d404ca 100644 --- a/runner/src/server/views/upload-playback.html +++ b/runner/src/server/views/upload-playback.html @@ -8,7 +8,8 @@ {% set infoHtml %}

- It looks like your birth certificate image is too blurred. Check your image and upload another one if needed. + It looks like your image is too blurred. Upload a new one or if you're happy with it you can still submit it. + It will be checked by consular staff. If they cannot read it they will contact you - this could delay your application.

{% endset %} @@ -25,9 +26,6 @@ {% block content %}
-

- Check your {{ fieldName }} -

{% if uploadErrors %} {{ govukErrorSummary(uploadErrors) }} {% endif %} @@ -40,8 +38,6 @@

}) }} -

All the information in the image must be readable. If not, you may be asked to send another copy, which could delay your application.

- {{ govukDetails({ summaryText: "How to take a good photograph", html: takePhotoHtml From 59ba1fd2917a2d19f66cd70f870f4b1da783d951 Mon Sep 17 00:00:00 2001 From: Luke Zigler Date: Fri, 24 Nov 2023 14:26:46 +0000 Subject: [PATCH 13/29] Tidied up UploadPageController post route handler using takeover responses in upload service --- .../pageControllers/UploadPageController.ts | 18 +----------------- runner/src/server/services/uploadService.ts | 4 ++++ 2 files changed, 5 insertions(+), 17 deletions(-) diff --git a/runner/src/server/plugins/engine/pageControllers/UploadPageController.ts b/runner/src/server/plugins/engine/pageControllers/UploadPageController.ts index 3b6858e8fb..dfc64c02df 100644 --- a/runner/src/server/plugins/engine/pageControllers/UploadPageController.ts +++ b/runner/src/server/plugins/engine/pageControllers/UploadPageController.ts @@ -48,23 +48,7 @@ export class UploadPageController extends PageController { return this.playback.makePostRouteHandler()(request, h); } - const response = await this.handlePostRequest(request, h); - if (response?.source?.context?.errors) { - return response; - } - if (request?.pre?.warningFromApi === "qualityWarning") { - return h.redirect(`?view=playback`); - } - - const { cacheService } = request.services([]); - const savedState = await cacheService.getState(request); - //This is required to ensure we don't navigate to an incorrect page based on stale state values - let relevantState = this.getConditionEvaluationContext( - this.model, - savedState - ); - - return this.proceed(request, h, relevantState); + return super.makePostRouteHandler()(request, h); }; } } diff --git a/runner/src/server/services/uploadService.ts b/runner/src/server/services/uploadService.ts index 126fc80a0c..e848ca9549 100644 --- a/runner/src/server/services/uploadService.ts +++ b/runner/src/server/services/uploadService.ts @@ -234,6 +234,10 @@ export class UploadService { await cacheService.mergeState(request, { originalFilenames }); + if (request.pre?.warningFromApi) { + return h.redirect("?view=playback").takeover(); + } + return h.continue; } From fe23ceb1f43f58957f3decac26b6758924a13c36 Mon Sep 17 00:00:00 2001 From: Luke Zigler Date: Fri, 24 Nov 2023 15:10:22 +0000 Subject: [PATCH 14/29] Updates to make sure playback page is only shown when upload page controller is being used --- .../plugins/engine/pageControllers/PageController.ts | 2 +- runner/src/server/plugins/engine/plugin.ts | 7 ++++++- runner/src/server/services/uploadService.ts | 11 +++++++++-- 3 files changed, 16 insertions(+), 4 deletions(-) diff --git a/runner/src/server/plugins/engine/pageControllers/PageController.ts b/runner/src/server/plugins/engine/pageControllers/PageController.ts index 78bd0a0f6c..ccb871b005 100644 --- a/runner/src/server/plugins/engine/pageControllers/PageController.ts +++ b/runner/src/server/plugins/engine/pageControllers/PageController.ts @@ -40,7 +40,7 @@ export class PageController extends PageControllerBase { onPreHandler: { method: async (request: HapiRequest, h: HapiResponseToolkit) => { const { uploadService } = request.services([]); - return uploadService.handleUploadRequest(request, h); + return uploadService.handleUploadRequest(request, h, this.pageDef); }, }, onPostHandler: { diff --git a/runner/src/server/plugins/engine/plugin.ts b/runner/src/server/plugins/engine/plugin.ts index dc6a4abb47..375b273231 100644 --- a/runner/src/server/plugins/engine/plugin.ts +++ b/runner/src/server/plugins/engine/plugin.ts @@ -229,7 +229,12 @@ export const plugin = { const { uploadService } = server.services([]); const handleFiles = (request: HapiRequest, h: HapiResponseToolkit) => { - return uploadService.handleUploadRequest(request, h); + const { path, id } = request.params; + const model = forms[id]; + const page = model?.pages.find( + (page) => normalisePath(page.path) === normalisePath(path) + ); + return uploadService.handleUploadRequest(request, h, page.pageDef); }; const postHandler = async ( diff --git a/runner/src/server/services/uploadService.ts b/runner/src/server/services/uploadService.ts index e848ca9549..9c144a735b 100644 --- a/runner/src/server/services/uploadService.ts +++ b/runner/src/server/services/uploadService.ts @@ -104,7 +104,11 @@ export class UploadService { return h.continue; } - async handleUploadRequest(request: HapiRequest, h: HapiResponseToolkit) { + async handleUploadRequest( + request: HapiRequest, + h: HapiResponseToolkit, + page: any + ) { const { cacheService } = request.services([]); const state = await cacheService.getState(request); const originalFilenames = state?.originalFilenames ?? {}; @@ -234,7 +238,10 @@ export class UploadService { await cacheService.mergeState(request, { originalFilenames }); - if (request.pre?.warningFromApi) { + if ( + request.pre?.warningFromApi && + page?.controller === "UploadPageController" + ) { return h.redirect("?view=playback").takeover(); } From 483354a253c50f3b3d5d01f35b13f0f0a9e428cb Mon Sep 17 00:00:00 2001 From: Luke Zigler Date: Fri, 24 Nov 2023 15:50:55 +0000 Subject: [PATCH 15/29] Chanhged the name of the title for the playback page --- .../engine/pageControllers/PlaybackUploadPageController.ts | 7 ++----- 1 file changed, 2 insertions(+), 5 deletions(-) diff --git a/runner/src/server/plugins/engine/pageControllers/PlaybackUploadPageController.ts b/runner/src/server/plugins/engine/pageControllers/PlaybackUploadPageController.ts index dca8d23364..e921d9b87b 100644 --- a/runner/src/server/plugins/engine/pageControllers/PlaybackUploadPageController.ts +++ b/runner/src/server/plugins/engine/pageControllers/PlaybackUploadPageController.ts @@ -51,11 +51,10 @@ export class PlaybackUploadPageController extends PageController { const state = await cacheService.getState(request); const { progress = [] } = state; let sectionTitle = this.section?.title; - let pageTitle = `Check your ${this.inputComponent.title} image`; return h.view("upload-playback", { sectionTitle: sectionTitle, showTitle: true, - pageTitle: pageTitle, + pageTitle: "Check your image", backLink: progress[progress.length - 1] ?? this.backLinkFallback, radios: this.buildRadioViewModel(), }); @@ -81,15 +80,13 @@ export class PlaybackUploadPageController extends PageController { ], }; let sectionTitle = this.section?.title; - let pageTitle = `Check your ${this.inputComponent.title} image`; return h.view("upload-playback", { sectionTitle: sectionTitle, showTitle: true, - pageTitle: pageTitle, + pageTitle: "Check your image", uploadErrors: errors, backLink: progress[progress.length - 2] ?? this.backLinkFallback, radios: this.buildRadioViewModel(errorText), - fieldName: this.inputComponent.title, }); } From e509dd0f9b72966fb41cf1408acf1c19f0037383 Mon Sep 17 00:00:00 2001 From: Luke Zigler Date: Fri, 24 Nov 2023 15:54:20 +0000 Subject: [PATCH 16/29] Removed innecessary test after upload files pre-handler changed --- .../UploadPageController.test.ts | 23 ------------------- 1 file changed, 23 deletions(-) diff --git a/runner/test/cases/server/plugins/engine/pageControllers/UploadPageController.test.ts b/runner/test/cases/server/plugins/engine/pageControllers/UploadPageController.test.ts index 8392ee32dc..8d14b91737 100644 --- a/runner/test/cases/server/plugins/engine/pageControllers/UploadPageController.test.ts +++ b/runner/test/cases/server/plugins/engine/pageControllers/UploadPageController.test.ts @@ -88,27 +88,4 @@ suite("UploadPageController", () => { const result = await pageController.makeGetRouteHandler()(request, {}); expect(result).to.be.true(); }); - test("Redirects to ?view=playback if a quality warning is passed through", async () => { - const pageController = new UploadPageController(model, def); - const request = { - query: {}, - pre: { - warningFromApi: "qualityWarning", - }, - services: () => ({ - cacheService: { - getState: (_request) => ({}), - }, - }), - }; - const responseObj = { - redirect: (string: string) => string, - view: (_tpl, _options) => true, - }; - const result = await pageController.makePostRouteHandler()( - request, - responseObj - ); - expect(result).to.equal("?view=playback"); - }); }); From 3bbfc78ea6a47ef0c22c95c548821b33cbebca69 Mon Sep 17 00:00:00 2001 From: Luke Zigler Date: Fri, 24 Nov 2023 15:55:17 +0000 Subject: [PATCH 17/29] Changed options in file upload field edit --- designer/client/file-upload-field-edit.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/designer/client/file-upload-field-edit.tsx b/designer/client/file-upload-field-edit.tsx index 020fd44483..bd64e80957 100644 --- a/designer/client/file-upload-field-edit.tsx +++ b/designer/client/file-upload-field-edit.tsx @@ -13,7 +13,7 @@ const defaultOptions = { export function FileUploadFieldEdit() { const { state, dispatch } = useContext(ComponentContext); const { selectedComponent } = state; - const { options = defaultOptions } = selectedComponent; + const options = { ...defaultOptions, ...selectedComponent.options }; return (
From ac911a7de4118493640d478c8b52e61175da92d0 Mon Sep 17 00:00:00 2001 From: Luke Zigler Date: Mon, 27 Nov 2023 08:50:24 +0000 Subject: [PATCH 18/29] Added documentation for the document upload api --- docs/runner/document-upload.md | 32 ++++++++++++++++++++++++++++++++ 1 file changed, 32 insertions(+) create mode 100644 docs/runner/document-upload.md diff --git a/docs/runner/document-upload.md b/docs/runner/document-upload.md new file mode 100644 index 0000000000..78c887e0ac --- /dev/null +++ b/docs/runner/document-upload.md @@ -0,0 +1,32 @@ +# Document upload + +the form builder supports the use of an external document upload service. This allows users to upload files, but gives developers the flexibility to decide how they want to process the files. + +## Setup + +In order to start using file upload files in your form, you will need to specify an endpoint to send your files to. This can be done by setting the following environment variables: + +| Variable name | Definition | example | +| ----------------------- | ------------------------------------------------------ | ------------------------------- | +| DOCUMENT_UPLOAD_API_URL | the root endpoint of service used to upload your files | https://document-upload-api.com | + +The service you're using for your document upload api will need an endpoint of /files that accepts POST requests with a file in the body. Currently, there is no support for authenticating against this endpoint, so this endpoint will need to be open. + +### Responses + +The upload service which handles communication with the api can handle the following responses: + +| Code | Payload | Handled by | +| ---- | ---------------- | ------------------------------------------------------------------------------------------------------------------------------------------------- | +| 201 | | Updating the value of the upload field to the location url returned | +| 201 | `qualityWarning` | Updating the upload field as above, as well as routing to the upload playback page if using the UploadPageController. See below for more details. | +| 400 | | Redirects back to the upload field page displaying a file type error | +| 413 | | Redirects back to the upload field page displaying a file size error | +| 422 | | Redirects back to the upload field page displaying a file virus error | + +#### UploadPageController + +We have introduced a specific UploadPageController, which can be used if you want to integrate image quality checking into your document upload api. +By adding the property `controller: UploadPageController` to the page in your form json, if a successful response is returned from the api but with the payload "qualityWarning", the user will be presented a playback page. +This page will strongly suggest the user upload a new image, and give the user the option to continue anyway or upload a new image. +If the UploadPageController is not specified on the page, the quality warning will be ignored, and the user will be routed to the next page in the form as normal. From 6a0ab30931e83e26cd1358d098edaf3136c0af19 Mon Sep 17 00:00:00 2001 From: Luke Zigler Date: Mon, 27 Nov 2023 15:22:00 +0000 Subject: [PATCH 19/29] Removed stray console logs --- designer/client/reducers/component/componentReducer.options.ts | 2 -- runner/src/server/services/uploadService.ts | 1 - 2 files changed, 3 deletions(-) diff --git a/designer/client/reducers/component/componentReducer.options.ts b/designer/client/reducers/component/componentReducer.options.ts index 0e0d14a49a..b3046945d6 100644 --- a/designer/client/reducers/component/componentReducer.options.ts +++ b/designer/client/reducers/component/componentReducer.options.ts @@ -16,8 +16,6 @@ export function optionsReducer(state, action: OptionsActions) { const { type, payload } = action; const { selectedComponent } = state; const { options } = selectedComponent; - console.log("reducer type: ", type); - console.log("reducer payload: ", payload); switch (type) { case Options.EDIT_OPTIONS_HIDE_TITLE: return { diff --git a/runner/src/server/services/uploadService.ts b/runner/src/server/services/uploadService.ts index 9c144a735b..34eadeb7bf 100644 --- a/runner/src/server/services/uploadService.ts +++ b/runner/src/server/services/uploadService.ts @@ -74,7 +74,6 @@ export class UploadService { const warningFromApi = payload?.toString?.(); let error: string | undefined; let location: string | undefined; - console.log("This is the payload", warningFromApi); switch (res.statusCode) { case 201: location = res.headers.location; From a72660aca837a3b7b0024ea3f7fbd11898780202 Mon Sep 17 00:00:00 2001 From: Luke Zigler Date: Mon, 27 Nov 2023 15:52:35 +0000 Subject: [PATCH 20/29] Cleaned up page controllers --- .../pageControllers/PageControllerBase.ts | 1 - .../PlaybackUploadPageController.ts | 51 +++++++++---------- 2 files changed, 25 insertions(+), 27 deletions(-) diff --git a/runner/src/server/plugins/engine/pageControllers/PageControllerBase.ts b/runner/src/server/plugins/engine/pageControllers/PageControllerBase.ts index e2944dda93..fe3e1a52f8 100644 --- a/runner/src/server/plugins/engine/pageControllers/PageControllerBase.ts +++ b/runner/src/server/plugins/engine/pageControllers/PageControllerBase.ts @@ -500,7 +500,6 @@ export class PageControllerBase { await cacheService.mergeState(request, { progress }); - viewModel.backLink = progress[progress.length - 2]; viewModel.backLink = progress[progress.length - 2] ?? this.backLinkFallback; return h.view(this.viewName, viewModel); diff --git a/runner/src/server/plugins/engine/pageControllers/PlaybackUploadPageController.ts b/runner/src/server/plugins/engine/pageControllers/PlaybackUploadPageController.ts index e921d9b87b..608ce09c1c 100644 --- a/runner/src/server/plugins/engine/pageControllers/PlaybackUploadPageController.ts +++ b/runner/src/server/plugins/engine/pageControllers/PlaybackUploadPageController.ts @@ -3,45 +3,44 @@ import { FormModel } from "server/plugins/engine/models"; import { Page } from "@xgovformbuilder/model"; import { FormComponent } from "server/plugins/engine/components"; import { HapiRequest, HapiResponseToolkit } from "server/types"; - export class PlaybackUploadPageController extends PageController { inputComponent: FormComponent; + standardViewModel = { + name: "retryUpload", + fieldset: { + legend: { + text: "Would you like to upload a new image?", + isPageHeading: false, + classes: "govuk-fieldset__legend--s", + }, + }, + items: [ + { + value: "true", + text: "Yes - I would like to upload a new image", + }, + { + value: "false", + text: "No - I am happy with the image", + }, + ], + }; constructor(model: FormModel, pageDef: Page, inputComponent: FormComponent) { super(model, pageDef); this.inputComponent = inputComponent; } - buildRadioViewModel(error?: string) { - const standardViewModel = { - name: "retryUpload", - fieldset: { - legend: { - text: "Would you like to upload a new image?", - isPageHeading: false, - classes: "govuk-fieldset__legend--s", - }, - }, - items: [ - { - value: "true", - text: "Yes - I would like to upload a new image", - }, - { - value: "false", - text: "No - I am happy with the image", - }, - ], - }; + getRetryUploadViewModel(error?: string) { if (error) { return { errorMessage: { text: error, }, - ...standardViewModel, + ...this.standardViewModel, }; } - return standardViewModel; + return this.standardViewModel; } makeGetRouteHandler() { @@ -56,7 +55,7 @@ export class PlaybackUploadPageController extends PageController { showTitle: true, pageTitle: "Check your image", backLink: progress[progress.length - 1] ?? this.backLinkFallback, - radios: this.buildRadioViewModel(), + radios: this.getRetryUploadViewModel(), }); }; } @@ -86,7 +85,7 @@ export class PlaybackUploadPageController extends PageController { pageTitle: "Check your image", uploadErrors: errors, backLink: progress[progress.length - 2] ?? this.backLinkFallback, - radios: this.buildRadioViewModel(errorText), + radios: this.getRetryUploadViewModel(errorText), }); } From 03d6b5100e8db7beb99bc3a4f2b4660cedf02807 Mon Sep 17 00:00:00 2001 From: Luke Zigler Date: Mon, 27 Nov 2023 15:54:40 +0000 Subject: [PATCH 21/29] Changed warningFromApi to just warning --- runner/src/server/services/uploadService.ts | 19 +++++++------------ 1 file changed, 7 insertions(+), 12 deletions(-) diff --git a/runner/src/server/services/uploadService.ts b/runner/src/server/services/uploadService.ts index 34eadeb7bf..1439ee0475 100644 --- a/runner/src/server/services/uploadService.ts +++ b/runner/src/server/services/uploadService.ts @@ -71,7 +71,7 @@ export class UploadService { } parsedDocumentUploadResponse({ res, payload }) { - const warningFromApi = payload?.toString?.(); + const warning = payload?.toString?.(); let error: string | undefined; let location: string | undefined; switch (res.statusCode) { @@ -94,7 +94,7 @@ export class UploadService { return { location, error, - warningFromApi, + warning, }; } @@ -185,15 +185,13 @@ export class UploadService { if (validFiles.length === values.length) { try { - const { - error, - location, - warningFromApi, - } = await this.uploadDocuments(validFiles); + const { error, location, warning } = await this.uploadDocuments( + validFiles + ); if (location) { originalFilenames[key] = { location }; request.payload[key] = location; - request.pre.warningFromApi = warningFromApi; + request.pre.warning = warning; } if (error) { request.pre.errors = [ @@ -237,10 +235,7 @@ export class UploadService { await cacheService.mergeState(request, { originalFilenames }); - if ( - request.pre?.warningFromApi && - page?.controller === "UploadPageController" - ) { + if (request.pre?.warning && page?.controller === "UploadPageController") { return h.redirect("?view=playback").takeover(); } From 3caecb9deb5b622661947993f53c51104527e4fa Mon Sep 17 00:00:00 2001 From: Luke Zigler Date: Mon, 27 Nov 2023 15:59:48 +0000 Subject: [PATCH 22/29] Added comment explaining the getRetryUploadViewModel function --- .../engine/pageControllers/PlaybackUploadPageController.ts | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/runner/src/server/plugins/engine/pageControllers/PlaybackUploadPageController.ts b/runner/src/server/plugins/engine/pageControllers/PlaybackUploadPageController.ts index 608ce09c1c..59e1e7661b 100644 --- a/runner/src/server/plugins/engine/pageControllers/PlaybackUploadPageController.ts +++ b/runner/src/server/plugins/engine/pageControllers/PlaybackUploadPageController.ts @@ -31,6 +31,11 @@ export class PlaybackUploadPageController extends PageController { this.inputComponent = inputComponent; } + /** + * Gets the radio button view model for the "Would you like to upload a new image?" question + * @param error - if the user hasn't chosen an option and tries to continue, add the required field error to the field + * @returns the view model for the radio button component + * */ getRetryUploadViewModel(error?: string) { if (error) { return { From 23769c567922e6e68b73a217e4f1c2e36fef9641 Mon Sep 17 00:00:00 2001 From: Luke Zigler Date: Wed, 29 Nov 2023 15:07:33 +0000 Subject: [PATCH 23/29] Removed unnecessary back link declaration --- .../server/plugins/engine/pageControllers/PageControllerBase.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/runner/src/server/plugins/engine/pageControllers/PageControllerBase.ts b/runner/src/server/plugins/engine/pageControllers/PageControllerBase.ts index fe3e1a52f8..f92e8a932c 100644 --- a/runner/src/server/plugins/engine/pageControllers/PageControllerBase.ts +++ b/runner/src/server/plugins/engine/pageControllers/PageControllerBase.ts @@ -800,7 +800,6 @@ export class PageControllerBase { private renderWithErrors(request, h, payload, num, progress, errors) { const viewModel = this.getViewModel(payload, num, errors); - viewModel.backLink = progress[progress.length - 2]; viewModel.backLink = progress[progress.length - 2] ?? this.backLinkFallback; this.setPhaseTag(viewModel); this.setFeedbackDetails(viewModel, request); From d564725b608e0bd55dd1fe284207ffbdaee1a6e2 Mon Sep 17 00:00:00 2001 From: Luke Zigler Date: Wed, 29 Nov 2023 15:08:52 +0000 Subject: [PATCH 24/29] Updated upload service to update file name if using the upload page controller --- runner/src/server/services/uploadService.ts | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/runner/src/server/services/uploadService.ts b/runner/src/server/services/uploadService.ts index 1439ee0475..ec11364b5d 100644 --- a/runner/src/server/services/uploadService.ts +++ b/runner/src/server/services/uploadService.ts @@ -236,6 +236,17 @@ export class UploadService { await cacheService.mergeState(request, { originalFilenames }); if (request.pre?.warning && page?.controller === "UploadPageController") { + const update = Object.entries(originalFilenames).reduce( + (acc, [key, value]) => { + return { + ...acc, + [key]: value.originalFilename, + }; + }, + {} + ); + await cacheService.mergeState(request, update); + return h.redirect("?view=playback").takeover(); } From ba823fd177bdc98e6094097324db57fe3f75e7c1 Mon Sep 17 00:00:00 2001 From: Luke Zigler Date: Wed, 29 Nov 2023 15:09:29 +0000 Subject: [PATCH 25/29] Updated playback upload page controller to incorporate joi --- .../PlaybackUploadPageController.ts | 66 +++++++++++-------- 1 file changed, 37 insertions(+), 29 deletions(-) diff --git a/runner/src/server/plugins/engine/pageControllers/PlaybackUploadPageController.ts b/runner/src/server/plugins/engine/pageControllers/PlaybackUploadPageController.ts index 59e1e7661b..2501eccd85 100644 --- a/runner/src/server/plugins/engine/pageControllers/PlaybackUploadPageController.ts +++ b/runner/src/server/plugins/engine/pageControllers/PlaybackUploadPageController.ts @@ -1,12 +1,20 @@ import { PageController } from "server/plugins/engine/pageControllers/PageController"; import { FormModel } from "server/plugins/engine/models"; import { Page } from "@xgovformbuilder/model"; -import { FormComponent } from "server/plugins/engine/components"; +import { + ComponentCollection, + FormComponent, +} from "server/plugins/engine/components"; import { HapiRequest, HapiResponseToolkit } from "server/types"; +import joi from "joi"; +import { FormSubmissionErrors } from "../types"; export class PlaybackUploadPageController extends PageController { inputComponent: FormComponent; - standardViewModel = { + retryUploadViewModel = { name: "retryUpload", + type: "RadiosField", + options: {}, + schema: {}, fieldset: { legend: { text: "Would you like to upload a new image?", @@ -16,11 +24,11 @@ export class PlaybackUploadPageController extends PageController { }, items: [ { - value: "true", + value: true, text: "Yes - I would like to upload a new image", }, { - value: "false", + value: false, text: "No - I am happy with the image", }, ], @@ -29,6 +37,16 @@ export class PlaybackUploadPageController extends PageController { constructor(model: FormModel, pageDef: Page, inputComponent: FormComponent) { super(model, pageDef); this.inputComponent = inputComponent; + this.formSchema = joi.object({ + crumb: joi.string(), + retryUpload: joi + .string() + .required() + .allow("true", "false") + .label("if you would like to upload a new image"), + }); + this.stateSchema = joi.object(); + this.components = new ComponentCollection([], this.model); } /** @@ -36,16 +54,16 @@ export class PlaybackUploadPageController extends PageController { * @param error - if the user hasn't chosen an option and tries to continue, add the required field error to the field * @returns the view model for the radio button component * */ - getRetryUploadViewModel(error?: string) { - if (error) { - return { - errorMessage: { - text: error, - }, - ...this.standardViewModel, - }; - } - return this.standardViewModel; + getRetryUploadViewModel(errors?: FormSubmissionErrors) { + let viewModel = { ...this.retryUploadViewModel }; + errors?.errorList?.forEach((err) => { + if (err.name === viewModel.name) { + viewModel.errorMessage = { + text: err.text, + }; + } + }); + return viewModel; } makeGetRouteHandler() { @@ -71,18 +89,10 @@ export class PlaybackUploadPageController extends PageController { const state = await cacheService.getState(request); const { progress = [] } = state; - const payload = request.payload; - if (!payload.retryUpload) { - const errorText = "Select if you would like to continue"; - const errors = { - titleText: "Fix the following errors", - errorList: [ - { - text: errorText, - href: "#retry-upload", - }, - ], - }; + const { payload } = request; + const result = this.formSchema.validate(payload, this.validationOptions); + if (result.error) { + const errors = this.getErrors(result); let sectionTitle = this.section?.title; return h.view("upload-playback", { sectionTitle: sectionTitle, @@ -90,7 +100,7 @@ export class PlaybackUploadPageController extends PageController { pageTitle: "Check your image", uploadErrors: errors, backLink: progress[progress.length - 2] ?? this.backLinkFallback, - radios: this.getRetryUploadViewModel(errorText), + radios: this.getRetryUploadViewModel(errors), }); } @@ -98,8 +108,6 @@ export class PlaybackUploadPageController extends PageController { return h.redirect(`/${this.model.basePath}${this.path}`); } - delete payload.retryUpload; - return super.makePostRouteHandler()(request, h); }; } From 746da127ab62a90e55c732d67b232f604aea8c1d Mon Sep 17 00:00:00 2001 From: Luke Zigler Date: Wed, 29 Nov 2023 16:02:01 +0000 Subject: [PATCH 26/29] Updated playback page controller to mirror functionality of repeating summary page controller --- .../pageControllers/PlaybackUploadPageController.ts | 9 ++------- 1 file changed, 2 insertions(+), 7 deletions(-) diff --git a/runner/src/server/plugins/engine/pageControllers/PlaybackUploadPageController.ts b/runner/src/server/plugins/engine/pageControllers/PlaybackUploadPageController.ts index 2501eccd85..b011a4fbc0 100644 --- a/runner/src/server/plugins/engine/pageControllers/PlaybackUploadPageController.ts +++ b/runner/src/server/plugins/engine/pageControllers/PlaybackUploadPageController.ts @@ -1,10 +1,7 @@ import { PageController } from "server/plugins/engine/pageControllers/PageController"; import { FormModel } from "server/plugins/engine/models"; import { Page } from "@xgovformbuilder/model"; -import { - ComponentCollection, - FormComponent, -} from "server/plugins/engine/components"; +import { FormComponent } from "server/plugins/engine/components"; import { HapiRequest, HapiResponseToolkit } from "server/types"; import joi from "joi"; import { FormSubmissionErrors } from "../types"; @@ -45,8 +42,6 @@ export class PlaybackUploadPageController extends PageController { .allow("true", "false") .label("if you would like to upload a new image"), }); - this.stateSchema = joi.object(); - this.components = new ComponentCollection([], this.model); } /** @@ -108,7 +103,7 @@ export class PlaybackUploadPageController extends PageController { return h.redirect(`/${this.model.basePath}${this.path}`); } - return super.makePostRouteHandler()(request, h); + return h.redirect(this.getNext(request.payload)); }; } } From 76e796cf010d8298704b6e8ef32de0b6d68f5faf Mon Sep 17 00:00:00 2001 From: Luke Zigler Date: Thu, 30 Nov 2023 12:32:38 +0000 Subject: [PATCH 27/29] Updated UploadService to set state properly if the UploadPageController is being used --- runner/src/server/services/uploadService.ts | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/runner/src/server/services/uploadService.ts b/runner/src/server/services/uploadService.ts index ec11364b5d..86e148b38b 100644 --- a/runner/src/server/services/uploadService.ts +++ b/runner/src/server/services/uploadService.ts @@ -238,9 +238,17 @@ export class UploadService { if (request.pre?.warning && page?.controller === "UploadPageController") { const update = Object.entries(originalFilenames).reduce( (acc, [key, value]) => { + if (page?.section) { + return { + ...acc, + [page.section]: { + [key]: value.location, + }, + }; + } return { ...acc, - [key]: value.originalFilename, + [key]: value.location, }; }, {} From e279dc4f339faf7ded378a7181e2e9208a3c2067 Mon Sep 17 00:00:00 2001 From: Luke Zigler Date: Fri, 1 Dec 2023 11:22:45 +0000 Subject: [PATCH 28/29] Changed how upload service updates the file upload state --- runner/src/server/services/uploadService.ts | 32 ++++++++++----------- 1 file changed, 16 insertions(+), 16 deletions(-) diff --git a/runner/src/server/services/uploadService.ts b/runner/src/server/services/uploadService.ts index 86e148b38b..64d7b924eb 100644 --- a/runner/src/server/services/uploadService.ts +++ b/runner/src/server/services/uploadService.ts @@ -236,24 +236,24 @@ export class UploadService { await cacheService.mergeState(request, { originalFilenames }); if (request.pre?.warning && page?.controller === "UploadPageController") { - const update = Object.entries(originalFilenames).reduce( - (acc, [key, value]) => { - if (page?.section) { - return { - ...acc, - [page.section]: { - [key]: value.location, - }, - }; - } - return { - ...acc, - [key]: value.location, - }; - }, + let update; + const updatedFilenames = files.reduce( + (acc, [key, _file]) => ({ + ...acc, + [key]: originalFilenames[key].location, + }), {} ); - await cacheService.mergeState(request, update); + if (page?.section) { + update = { + [page.section]: { + ...(state[page.section] ?? {}), + ...updatedFilenames, + }, + }; + } + + await cacheService.mergeState(request, update ?? updatedFilenames); return h.redirect("?view=playback").takeover(); } From ab86b0b8c3dd33aceeaff30d70f0c0fd70d927a1 Mon Sep 17 00:00:00 2001 From: Luke Zigler Date: Fri, 1 Dec 2023 12:36:40 +0000 Subject: [PATCH 29/29] Moved image playback response handling to upload page controller --- .../pageControllers/UploadPageController.ts | 8 ++++++- runner/src/server/services/uploadService.ts | 23 ------------------- 2 files changed, 7 insertions(+), 24 deletions(-) diff --git a/runner/src/server/plugins/engine/pageControllers/UploadPageController.ts b/runner/src/server/plugins/engine/pageControllers/UploadPageController.ts index dfc64c02df..09b0c8b217 100644 --- a/runner/src/server/plugins/engine/pageControllers/UploadPageController.ts +++ b/runner/src/server/plugins/engine/pageControllers/UploadPageController.ts @@ -48,7 +48,13 @@ export class UploadPageController extends PageController { return this.playback.makePostRouteHandler()(request, h); } - return super.makePostRouteHandler()(request, h); + const defaultRes = super.makePostRouteHandler()(request, h); + + if (request.pre?.warning) { + return h.redirect("?view=playback"); + } + + return defaultRes; }; } } diff --git a/runner/src/server/services/uploadService.ts b/runner/src/server/services/uploadService.ts index 64d7b924eb..53331efbd2 100644 --- a/runner/src/server/services/uploadService.ts +++ b/runner/src/server/services/uploadService.ts @@ -235,29 +235,6 @@ export class UploadService { await cacheService.mergeState(request, { originalFilenames }); - if (request.pre?.warning && page?.controller === "UploadPageController") { - let update; - const updatedFilenames = files.reduce( - (acc, [key, _file]) => ({ - ...acc, - [key]: originalFilenames[key].location, - }), - {} - ); - if (page?.section) { - update = { - [page.section]: { - ...(state[page.section] ?? {}), - ...updatedFilenames, - }, - }; - } - - await cacheService.mergeState(request, update ?? updatedFilenames); - - return h.redirect("?view=playback").takeover(); - } - return h.continue; }