Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat/image-quality-playback #1161

Merged
merged 29 commits into from
Dec 1, 2023
Merged
Show file tree
Hide file tree
Changes from 22 commits
Commits
Show all changes
29 commits
Select commit Hold shift + click to select a range
41c6761
updated designer to allow users to select image quality playback option
ziggy-cyb Nov 22, 2023
446c013
Added template for upload playback
ziggy-cyb Nov 22, 2023
89a1832
Added page controllers for upload field pages
ziggy-cyb Nov 22, 2023
7199531
Added new UploadPageController to controllers list
ziggy-cyb Nov 22, 2023
784df1f
Updated UploadPageController to include form validation logic
ziggy-cyb Nov 23, 2023
2acb45d
Updated playback image page controller to run the standard post route…
ziggy-cyb Nov 23, 2023
a5905b0
Reverted PageControllerBase
ziggy-cyb Nov 23, 2023
45dc8e2
Updated upload service to process warnings in document upload responses
ziggy-cyb Nov 24, 2023
6e5be90
Moved makeRouteHandler functions to be accessible outside of getters
ziggy-cyb Nov 24, 2023
2b3cc8b
Updated UploadPageController to use new playback page route handler f…
ziggy-cyb Nov 24, 2023
8d0c004
Added unit tests for UploadPageController
ziggy-cyb Nov 24, 2023
21e7fd1
Made some content tweaks to OCR playback page
ziggy-cyb Nov 24, 2023
59ba1fd
Tidied up UploadPageController post route handler using takeover resp…
ziggy-cyb Nov 24, 2023
fe23ceb
Updates to make sure playback page is only shown when upload page con…
ziggy-cyb Nov 24, 2023
483354a
Chanhged the name of the title for the playback page
ziggy-cyb Nov 24, 2023
e509dd0
Removed innecessary test after upload files pre-handler changed
ziggy-cyb Nov 24, 2023
3bbfc78
Changed options in file upload field edit
ziggy-cyb Nov 24, 2023
ac911a7
Added documentation for the document upload api
ziggy-cyb Nov 27, 2023
6a0ab30
Removed stray console logs
ziggy-cyb Nov 27, 2023
a72660a
Cleaned up page controllers
ziggy-cyb Nov 27, 2023
03d6b51
Changed warningFromApi to just warning
ziggy-cyb Nov 27, 2023
3caecb9
Added comment explaining the getRetryUploadViewModel function
ziggy-cyb Nov 27, 2023
23769c5
Removed unnecessary back link declaration
ziggy-cyb Nov 29, 2023
d564725
Updated upload service to update file name if using the upload page c…
ziggy-cyb Nov 29, 2023
ba823fd
Updated playback upload page controller to incorporate joi
ziggy-cyb Nov 29, 2023
746da12
Updated playback page controller to mirror functionality of repeating…
ziggy-cyb Nov 29, 2023
76e796c
Updated UploadService to set state properly if the UploadPageControll…
ziggy-cyb Nov 30, 2023
e279dc4
Changed how upload service updates the file upload state
ziggy-cyb Dec 1, 2023
ab86b0b
Moved image playback response handling to upload page controller
ziggy-cyb Dec 1, 2023
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
45 changes: 39 additions & 6 deletions designer/client/file-upload-field-edit.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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.options };

return (
<details className="govuk-details">
Expand All @@ -22,21 +27,20 @@ export function FileUploadFieldEdit() {
<div className="govuk-checkboxes__item">
<input
className="govuk-checkboxes__input"
id="field-options.multiple"
id="field-options-multiple"
name="options.multiple"
type="checkbox"
checked={options.multiple === false}
checked={options.multiple}
onChange={(e) => {
e.preventDefault();
dispatch({
type: Actions.EDIT_OPTIONS_FILE_UPLOAD_MULTIPLE,
payload: !options.multiple,
payload: e.target.checked,
});
}}
/>
<label
className="govuk-label govuk-checkboxes__label"
htmlFor="field-options.multiple"
htmlFor="field-options-multiple"
>
{i18n("fileUploadFieldEditPage.multipleFilesOption.title")}
</label>
Expand All @@ -46,6 +50,35 @@ export function FileUploadFieldEdit() {
</div>
</div>

<div className="govuk-checkboxes govuk-form-group">
<div className="govuk-checkboxes__item">
<input
className="govuk-checkboxes__input"
id="field-options-imageQualityPlayback"
name="options.imageQualityPlayback"
type="checkbox"
checked={options.imageQualityPlayback}
onChange={(e) => {
dispatch({
type: Actions.EDIT_OPTIONS_IMAGE_QUALITY_PLAYBACK,
payload: e.target.checked,
});
}}
/>
<label
className="govuk-label govuk-checkboxes__label"
htmlFor="field-options.multiple"
>
{i18n("fileUploadFieldEditPage.imageQualityPlaybackOption.title")}
</label>
<span className="govuk-hint govuk-checkboxes__hint">
{i18n(
"fileUploadFieldEditPage.imageQualityPlaybackOption.helpText"
)}
</span>
</div>
</div>

<CssClasses />
</details>
);
Expand Down
4 changes: 4 additions & 0 deletions designer/client/i18n/translations/en.translation.json
Original file line number Diff line number Diff line change
Expand Up @@ -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": {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,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: {
Expand Down
1 change: 1 addition & 0 deletions designer/client/reducers/component/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
32 changes: 32 additions & 0 deletions docs/runner/document-upload.md
Original file line number Diff line number Diff line change
@@ -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.
1 change: 1 addition & 0 deletions model/src/components/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -217,6 +217,7 @@ export interface FileUploadFieldComponent {
multiple?: boolean;
classes?: string;
exposeToContext?: boolean;
imageQualityPlayback?: boolean;
};
schema: {};
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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: {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -800,6 +800,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;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

why do we need to declare twice?

this.setPhaseTag(viewModel);
this.setFeedbackDetails(viewModel, request);
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,106 @@
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 { HapiRequest, HapiResponseToolkit } from "server/types";
export class PlaybackUploadPageController extends PageController {
inputComponent: FormComponent;
standardViewModel = {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

retryUploadViewModel - standardViewModel/standard by itself doesn't convey enough information

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;
}

/**
* 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 {
errorMessage: {
text: error,
},
...this.standardViewModel,
};
}
return this.standardViewModel;
}

makeGetRouteHandler() {
return async (request: HapiRequest, h: HapiResponseToolkit) => {
const { cacheService } = request.services([]);

const state = await cacheService.getState(request);
const { progress = [] } = state;
let sectionTitle = this.section?.title;
return h.view("upload-playback", {
sectionTitle: sectionTitle,
showTitle: true,
pageTitle: "Check your image",
backLink: progress[progress.length - 1] ?? this.backLinkFallback,
radios: this.getRetryUploadViewModel(),
});
};
}

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) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

can we do a joi validation here?

const errorText = "Select if you would like to continue";
const errors = {
titleText: "Fix the following errors",
errorList: [
{
text: errorText,
href: "#retry-upload",
},
],
};
let sectionTitle = this.section?.title;
return h.view("upload-playback", {
sectionTitle: sectionTitle,
showTitle: true,
pageTitle: "Check your image",
uploadErrors: errors,
backLink: progress[progress.length - 2] ?? this.backLinkFallback,
radios: this.getRetryUploadViewModel(errorText),
});
}

if (payload.retryUpload === "true") {
return h.redirect(`/${this.model.basePath}${this.path}`);
}

delete payload.retryUpload;

return super.makePostRouteHandler()(request, h);
};
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
import { PageController } from "server/plugins/engine/pageControllers/PageController";
import { FormModel } from "server/plugins/engine/models";
import { HapiRequest, HapiResponseToolkit } from "server/types";
import { PlaybackUploadPageController } from "server/plugins/engine/pageControllers/PlaybackUploadPageController";
import { FormComponent } from "server/plugins/engine/components";

function isUploadField(component: FormComponent) {
return component.type === "FileUploadField";
}

export class UploadPageController extends PageController {
playback: PlaybackUploadPageController;
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.playback = new PlaybackUploadPageController(
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 === "playback") {
return this.playback.makeGetRouteHandler()(request, h);
}

return super.makeGetRouteHandler()(request, h);
};
}

makePostRouteHandler() {
return async (request: HapiRequest, h: HapiResponseToolkit) => {
const { query } = request;

if (query?.view === "playback") {
return this.playback.makePostRouteHandler()(request, h);
}

return super.makePostRouteHandler()(request, h);
};
}
}
2 changes: 2 additions & 0 deletions runner/src/server/plugins/engine/pageControllers/helpers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand All @@ -19,6 +20,7 @@ const PageControllers = {
SummaryPageController,
PageControllerBase,
RepeatingFieldPageController,
UploadPageController,
};

export const controllerNameFromPath = (filePath: string) => {
Expand Down
7 changes: 6 additions & 1 deletion runner/src/server/plugins/engine/plugin.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 (
Expand Down
Loading
Loading