Skip to content

Commit 0709ee9

Browse files
FLS-1413: Update backlink text and logic (#209)
* FLS-1413: - Introduced shared navigationUtils for backlink and progress logic. - Updated backlink text and URL logic across form pages. - Added backlink support to the summary page. - Fixed and updated relevant tests.
1 parent d841206 commit 0709ee9

File tree

11 files changed

+186
-59
lines changed

11 files changed

+186
-59
lines changed

e2e-test/cypress/e2e/runner/getConditionEvaluationContext.feature

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@ Feature: Get Condition Evaluation Context
1313
* I enter "d'egg" for "Surname"
1414
* I continue
1515
Then I see "There Is Someone Called Applicant"
16-
When I go back to application overview
16+
When I go back to previous page
1717
And I enter "{selectAll}{backspace}Scrambled" for "First name"
1818
* I continue
1919
* I see "TestConditions"
Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import { When } from "@badeball/cypress-cucumber-preprocessor";
22

3-
When("I go back to application overview", () => {
4-
cy.findByRole("link", { name: "Go back to application overview" }).click();
3+
When("I go back to previous page", () => {
4+
cy.findByRole("link", { name: "Go back to previous page" }).click();
55
});
Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
import { When, Then } from "@badeball/cypress-cucumber-preprocessor";
22

33
Then("The back link href is selected is {string}", (href) => {
4-
cy.findByRole("link", { name: "Go back to application overview" })
4+
cy.findByRole("link", { name: "Go back to previous page" })
55
.should("have.attr", "href")
66
.and("eq", href);
77
});

runner/src/server/plugins/engine/page-controllers/PageControllerBase.ts

Lines changed: 47 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -30,8 +30,9 @@ import {AdapterFormModel} from "../models";
3030
import {ComponentCollection} from "../components";
3131
import {config} from "../../utils/AdapterConfigurationSchema";
3232
import {proceed, redirectTo} from "../util/helper";
33-
import {UtilHelper} from "../../utils/UtilHelper";
33+
import {UtilHelper, BackLinkType } from "../../utils/UtilHelper";
3434
import {validationOptions} from "./ValidationOptions";
35+
import { updateProgress, getBackLink } from '../util/navigationUtils';
3536

3637
const FORM_SCHEMA = Symbol("FORM_SCHEMA");
3738
const STATE_SCHEMA = Symbol("STATE_SCHEMA");
@@ -125,7 +126,9 @@ export class PageControllerBase {
125126
}
126127

127128
this.backLink = "";
128-
this.backLinkText = this.model.def?.backLinkText ?? UtilHelper.getBackLinkText(false, this.model.def?.metadata?.isWelsh)
129+
this.backLinkText = this.model.def?.backLinkText ??
130+
UtilHelper.getBackLinkText(BackLinkType.ApplicationOverview, this.model.def?.metadata?.isWelsh);
131+
129132

130133
this[FORM_SCHEMA] = this.components.formSchema;
131134
this[STATE_SCHEMA] = this.components.stateSchema;
@@ -596,11 +599,6 @@ export class PageControllerBase {
596599
!isStartPage &&
597600
!isInitialisedSession;
598601

599-
this.backLink = state.callback?.returnUrl ?? progress[progress.length - 2];
600-
if (state["metadata"] && state["metadata"]["has_eligibility"]) {
601-
this.backLinkText = UtilHelper.getBackLinkText(true, this.model.def?.metadata?.isWelsh);
602-
}
603-
604602
if (shouldRedirectToStartPage) {
605603
// @ts-ignore
606604
return startPage!.startsWith("http")
@@ -678,25 +676,27 @@ export class PageControllerBase {
678676
/**
679677
* used for when a user clicks the "back" link. Progress is stored in the state. This is a safer alternative to running javascript that pops the history `onclick`.
680678
*/
681-
const lastVisited = progress[progress.length - 1];
682-
if (!lastVisited || !lastVisited.startsWith(currentPath)) {
683-
if (progress[progress.length - 2] === currentPath) {
684-
progress.pop();
685-
} else {
686-
progress.push(currentPath);
687-
}
688-
}
679+
updateProgress(progress, currentPath);
680+
681+
await adapterCacheService.mergeState(request, { progress });
682+
state = await adapterCacheService.getState(request);
683+
684+
// Compute back link
685+
const { backLink, backLinkText } = getBackLink({
686+
progress,
687+
thisPath: this.path,
688+
currentPath,
689+
startPage,
690+
backLinkFallback: this.backLinkFallback,
691+
returnUrl: state.callback?.returnUrl,
692+
isWelsh: this.model.def?.metadata?.isWelsh,
693+
isEligibilityForm: state["metadata"]?.has_eligibility ?? false
694+
});
689695
//@ts-ignore
690-
await adapterCacheService.mergeState(request, {progress});
696+
this.backLink = viewModel.backLink = backLink;
691697
//@ts-ignore
692-
state = await adapterCacheService.getState(request);
698+
this.backLinkText = viewModel.backLinkText = backLinkText;
693699

694-
viewModel.backLinkText = this.backLinkText;
695-
if (state.callback?.returnUrl) {
696-
viewModel.backLink = state.callback?.returnUrl;
697-
} else {
698-
this.backLink = viewModel.backLink = progress[progress.length - 2] ?? this.backLinkFallback;
699-
}
700700
viewModel.continueButtonText = "Save and continue"
701701
request.logger.info(`[PageControllerBase][${state.metadata?.form_session_identifier}] summary value ${JSON.stringify(viewModel.components)}`);
702702
this.updatePrivacyPolicyUrlAndContactUsUrl(state, viewModel)
@@ -824,13 +824,13 @@ export class PageControllerBase {
824824
* If there are any errors, render the page with the parsed errors
825825
*/
826826
if (formResult.errors) {
827-
return this.renderWithErrors(request, h, payload, num, progress, formResult.errors);
827+
return this.renderWithErrors(request, h, payload, num, progress, formResult.errors, state);
828828
}
829829

830830
const newState = this.getStateFromValidForm(formResult.value);
831831
const stateResult = this.validateState(newState, request);
832832
if (stateResult.errors) {
833-
return this.renderWithErrors(request, h, payload, num, progress, stateResult.errors);
833+
return this.renderWithErrors(request, h, payload, num, progress, stateResult.errors, state);
834834
}
835835

836836
let update = this.getPartialMergeState(stateResult.value);
@@ -1049,10 +1049,29 @@ export class PageControllerBase {
10491049
}
10501050
}
10511051

1052-
private renderWithErrors(request, h, payload, num, progress, errors) {
1052+
private renderWithErrors(request, h, payload, num, progress, errors, state) {
10531053
const viewModel = this.getViewModel(payload, num, errors);
1054-
viewModel.backLink = progress[progress.length - 2] ?? this.backLinkFallback;
1055-
viewModel.backLinkText = this.model.def?.backLinkText ?? UtilHelper.getBackLinkText(false, this.model.def?.metadata?.isWelsh);
1054+
1055+
const previewMode = state?.previewMode || false;
1056+
1057+
if (!previewMode) {
1058+
// Compute back link
1059+
const { backLink, backLinkText } = getBackLink({
1060+
progress,
1061+
thisPath: this.path,
1062+
currentPath: `/${this.model.basePath}${this.path}${request.url.search}`,
1063+
startPage: this.model.def.startPage,
1064+
backLinkFallback: this.backLinkFallback,
1065+
returnUrl: state.callback?.returnUrl,
1066+
isWelsh: this.model.def?.metadata?.isWelsh,
1067+
isEligibilityForm: state["metadata"]?.has_eligibility ?? false
1068+
});
1069+
//@ts-ignore
1070+
this.backLink = viewModel.backLink = backLink;
1071+
//@ts-ignore
1072+
this.backLinkText = viewModel.backLinkText = backLinkText;
1073+
}
1074+
10561075
this.setPhaseTag(viewModel);
10571076
this.setFeedbackDetails(viewModel, request);
10581077

runner/src/server/plugins/engine/page-controllers/SummaryPageController.ts

Lines changed: 37 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -7,8 +7,9 @@ import {PageController} from "./PageController";
77
import {isMultipleApiKey} from "@xgovformbuilder/model";
88
import {config} from "../../utils/AdapterConfigurationSchema";
99
import {redirectTo} from "../util/helper";
10-
import {UtilHelper} from "../../utils/UtilHelper";
10+
import {UtilHelper, BackLinkType} from "../../utils/UtilHelper";
1111
import {UkAddressField} from "../components";
12+
import { updateProgress, getBackLink } from '../util/navigationUtils';
1213

1314
const LOGGER_DATA = {
1415
class: "SummaryPageController",
@@ -32,28 +33,25 @@ export class SummaryPageController extends PageController {
3233

3334
const {adapterCacheService} = request.services([]);
3435
const model = this.model;
36+
const startPage = model.def.startPage;
37+
//@ts-ignore
38+
let state = await adapterCacheService.getState(request);
39+
const progress = state.progress || [];
40+
const currentPath = `/${this.model.basePath}${this.path}${request.url.search}`;
3541

3642
// @ts-ignore - ignoring so docs can be generated. Remove when properly typed
3743
if (this.model.def.skipSummary) {
3844
return this.makePostRouteHandler()(request, h);
3945
}
40-
//@ts-ignore
41-
let state = await adapterCacheService.getState(request);
42-
if (!state.progress) {
46+
if (!progress || progress.length === 0) {
4347
const currentPath = `/${this.model.basePath}${this.path}${request.url.search}`;
44-
const progress = state.progress || [];
4548
//@ts-ignore
4649
progress.push(currentPath);
4750
//@ts-ignore
4851
await adapterCacheService.mergeState(request, {progress});
4952
//@ts-ignore
5053
state = await adapterCacheService.getState(request);
5154
}
52-
if (state["metadata"] && state["metadata"]["has_eligibility"]) {
53-
this.isEligibility = state["metadata"]["has_eligibility"];
54-
this.backLinkText = UtilHelper.getBackLinkText(true, this.model.def?.metadata?.isWelsh);
55-
this.backLink = state.callback?.returnUrl;
56-
}
5755

5856
if (state["metadata"] && state["metadata"]["is_read_only_summary"]) {
5957
this.formattingDataInTheStateToOriginal(state, model);
@@ -63,9 +61,9 @@ export class SummaryPageController extends PageController {
6361
}
6462
//@ts-ignore
6563
const viewModel = new AdapterSummaryViewModel(this.title, model, state, request, this);
66-
64+
6765
await this.handlePreviewMode(request, viewModel);
68-
66+
6967
if (viewModel.endPage) {
7068
return redirectTo(request, h, `/${model.basePath}${viewModel.endPage.path}`);
7169
}
@@ -74,11 +72,37 @@ export class SummaryPageController extends PageController {
7472
//@ts-ignore
7573
viewModel.isReadOnlySummary = true;
7674
//@ts-ignore
77-
viewModel.backLinkText = UtilHelper.getBackLinkText(true, this.model.def?.metadata?.isWelsh);
75+
viewModel.backLinkText = UtilHelper.getBackLinkText(BackLinkType.Eligibility, this.model.def?.metadata?.isWelsh);
7876
//@ts-ignore
7977
viewModel.backLink = state.callback?.returnUrl;
8078
}
8179

80+
/**
81+
* used for when a user clicks the "back" link. Progress is stored in the state. This is a safer alternative to running javascript that pops the history `onclick`.
82+
*/
83+
updateProgress(progress, currentPath);
84+
85+
await adapterCacheService.mergeState(request, { progress });
86+
state = await adapterCacheService.getState(request);
87+
88+
const isEligibilityForm = this.isEligibility = state["metadata"]?.has_eligibility ?? false
89+
90+
// Compute back link
91+
const { backLink, backLinkText } = getBackLink({
92+
progress,
93+
thisPath: this.path,
94+
currentPath,
95+
startPage,
96+
backLinkFallback: this.backLinkFallback,
97+
returnUrl: state.callback?.returnUrl,
98+
isWelsh: this.model.def?.metadata?.isWelsh,
99+
isEligibilityForm: isEligibilityForm
100+
});
101+
//@ts-ignore
102+
viewModel.backLink = backLink;
103+
//@ts-ignore
104+
viewModel.backLinkText = backLinkText;
105+
82106
/**
83107
* iterates through the errors. If there are errors, a user will be redirected to the page
84108
* with the error with returnUrl=`/${model.basePath}/<last page that shows summary details>` in the URL query parameter.
Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,59 @@
1+
import {UtilHelper, BackLinkType} from "../../utils/UtilHelper";
2+
3+
interface BackLinkParams {
4+
progress: string[];
5+
thisPath: string;
6+
currentPath: string;
7+
startPage: string;
8+
backLinkFallback?: string;
9+
returnUrl?: string;
10+
isWelsh: boolean;
11+
isEligibilityForm: boolean;
12+
}
13+
14+
export function updateProgress(progress: any, currentPath: string): void {
15+
const lastVisited = progress[progress.length - 1];
16+
if (!lastVisited || lastVisited !== currentPath) {
17+
if (progress[progress.length - 2] === currentPath) {
18+
progress.pop();
19+
} else {
20+
progress.push(currentPath);
21+
}
22+
}
23+
}
24+
25+
export function getBackLink({
26+
progress,
27+
thisPath,
28+
currentPath,
29+
startPage,
30+
backLinkFallback = '/',
31+
returnUrl,
32+
isWelsh,
33+
isEligibilityForm
34+
}: BackLinkParams) {
35+
const isFirstPage = thisPath === startPage;
36+
37+
if (isEligibilityForm && isFirstPage && returnUrl) {
38+
return {
39+
backLink: returnUrl,
40+
backLinkText: UtilHelper.getBackLinkText(BackLinkType.Eligibility, isWelsh)
41+
};
42+
}
43+
44+
if (isFirstPage && returnUrl) {
45+
return {
46+
backLink: returnUrl,
47+
backLinkText: UtilHelper.getBackLinkText(BackLinkType.ApplicationOverview, isWelsh)
48+
};
49+
}
50+
51+
const currentIndex = progress.lastIndexOf(currentPath);
52+
const previousPage = currentIndex > 0 ? progress[currentIndex - 1] : undefined;
53+
const safeBackLink = previousPage ?? backLinkFallback;
54+
55+
return {
56+
backLink: safeBackLink,
57+
backLinkText: UtilHelper.getBackLinkText(BackLinkType.PreviousPage, isWelsh)
58+
};
59+
}
Lines changed: 17 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -1,17 +1,22 @@
1+
export enum BackLinkType {
2+
Eligibility,
3+
PreviousPage,
4+
ApplicationOverview
5+
}
6+
17
export class UtilHelper {
28
// Helper class to add translations in runner
3-
public static getBackLinkText(eligibility: boolean, isWelsh: boolean) {
4-
if (eligibility) {
5-
if (isWelsh) {
6-
return "Yn ôl at eich ceisiadau";
7-
}
8-
return "Back to your applications";
9-
} else {
10-
if (isWelsh) {
11-
return "Yn ôl i'r trosolwg o'r cais";
12-
} else {
13-
return "Go back to application overview";
14-
}
9+
public static getBackLinkText(type: BackLinkType, isWelsh: boolean): string {
10+
switch (type) {
11+
case BackLinkType.Eligibility:
12+
return isWelsh ? "Yn ôl at eich ceisiadau" : "Back to your applications";
13+
case BackLinkType.PreviousPage:
14+
// TODO: Add Welsh translation for "Go back to previous page"
15+
return isWelsh ? "" : "Go back to previous page";
16+
case BackLinkType.ApplicationOverview:
17+
return isWelsh ? "Yn ôl i'r trosolwg o'r cais" : "Go back to application overview";
18+
default:
19+
return "";
1520
}
1621
}
1722
}

runner/src/server/views/layout.html

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -146,7 +146,7 @@
146146
{% endif %}
147147
{% endif %}
148148

149-
{% if page.backLink or backLink %}
149+
{% if not previewMode and (page.backLink or backLink) %}
150150
{% if page.backLink %}
151151
{{ govukBackLink({
152152
href: page.backLink,

runner/src/server/views/summary.html

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -22,10 +22,22 @@
2222
href: backLink or page.backLink,
2323
text: (backLinkText or page.backLinkText) if not isReadOnlySummary else "Back to application for funding overview"
2424
}) }}
25+
{% elif not previewMode and (page.backLink or backLink) %}
26+
{% if page.backLink %}
27+
{{ govukBackLink({
28+
href: page.backLink,
29+
text: page.backLinkText
30+
}) }}
31+
{% else %}
32+
{{ govukBackLink({
33+
href: backLink,
34+
text: backLinkText
35+
}) }}
36+
{% endif %}
2537
{% endif %}
2638
{% endblock %}
2739
{% block content %}
28-
<div class="govuk-main-wrapper">
40+
<div class="govuk-main-wrapper govuk-!-static-padding-top-0">
2941
<div class="govuk-grid-row">
3042
<div class="govuk-grid-column-full">
3143
{% set hasMultipleSections = (details and details.length > 1 and details[0].items[0] | isArray) %}

runner/test/cases/server/plugins/engine/page-controllers/ConfirmPageController.test.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -61,6 +61,7 @@ suite("ConfirmPageController", () => {
6161
// Mock adapterCacheService with the methods you need
6262
const mockAdapterCacheService: any = {
6363
getState: sinon.stub().resolves(mockState),
64+
mergeState: sinon.stub().resolves(),
6465
};
6566
// Mock request with state
6667
const request = {
@@ -70,6 +71,9 @@ suite("ConfirmPageController", () => {
7071
query: {
7172
lang: "en"
7273
},
74+
url: {
75+
search: ""
76+
},
7377
yar: {
7478
get: sinon.stub().returns("en"),
7579
flash: sinon.stub().returns("en")

0 commit comments

Comments
 (0)