From 2f85695c7636a8184f770d453ca8a6afe3a4a214 Mon Sep 17 00:00:00 2001 From: Ynda Jas Date: Wed, 30 Aug 2023 14:22:08 +0100 Subject: [PATCH 1/3] Add referral task list util method This returns task list items in the format needed for an MOJ task list. Currently the referral model doesn't have the fields on it to make this a particularly dynamic method, so the return value is mostly hardcoded, but it will act as a template that we can update once we have the required model fields We're using the `moj-task-list__task-completed` class on tags here even when the status is not completed. This class actually just floats the tags to the right of the task list row and adds margin. We could recreate this class in our own stylesheets and give it a more accurate name, but I've favoured using a shared class here. The same is done in [the Interventions UI][1] There's some logic here, similar to Interventions, to allow task list items not to have links. Currently we don't want the "Confirm personal details" task to have a link, since it will be completed en route to this page. A link may be added later to reshow the personal details (but not allow updates) Explanation of why we're building this manually instead of using the [Nunjucks macro][2] will come in the next commit [1]: https://github.com/ministryofjustice/hmpps-interventions-ui/pull/34/files#diff-03f5907738cb33527fe8f34fbe0d5b1b7bb6cef5bb8cdec11da3d40731b28ba2R7 [2]: https://design-patterns.service.justice.gov.uk/patterns/task-list --- server/@types/ui/index.d.ts | 22 ++++++++++++ server/utils/index.ts | 13 ++++++- server/utils/referralUtils.test.ts | 55 ++++++++++++++++++++++++++++++ server/utils/referralUtils.ts | 49 ++++++++++++++++++++++++++ 4 files changed, 138 insertions(+), 1 deletion(-) create mode 100644 server/utils/referralUtils.test.ts create mode 100644 server/utils/referralUtils.ts diff --git a/server/@types/ui/index.d.ts b/server/@types/ui/index.d.ts index afbcdca0a..b749555fe 100644 --- a/server/@types/ui/index.d.ts +++ b/server/@types/ui/index.d.ts @@ -27,6 +27,25 @@ type OrganisationWithOfferingEmailsPresenter = Organisation & { summaryListRows: Array } +type ReferralTaskListStatusText = 'completed' | 'not started' | 'cannot start yet' + +// these are `GovukFrontendTag`s with spefic values +type ReferralTaskListStatusTag = + | { classes: 'govuk-tag moj-task-list__task-completed'; text: 'completed' } + | { classes: 'govuk-tag govuk-tag--grey moj-task-list__task-completed'; text: 'not started' } + | { classes: 'govuk-tag govuk-tag--grey moj-task-list__task-completed'; text: 'cannot start yet' } + +type ReferralTaskListItem = { + statusTag: ReferralTaskListStatusTag + text: string + url?: string +} + +type ReferralTaskListSection = { + heading: string + items: Array +} + export type { CoursePresenter, GovukFrontendSummaryListRowWithValue, @@ -35,5 +54,8 @@ export type { OrganisationWithOfferingEmailsPresenter, OrganisationWithOfferingEmailsSummaryListRows, OrganisationWithOfferingId, + ReferralTaskListSection, + ReferralTaskListStatusTag, + ReferralTaskListStatusText, TagColour, } diff --git a/server/utils/index.ts b/server/utils/index.ts index 909e1b365..2f14e8f58 100644 --- a/server/utils/index.ts +++ b/server/utils/index.ts @@ -3,8 +3,19 @@ import DateUtils from './dateUtils' import nunjucksSetup from './nunjucksSetup' import OrganisationUtils from './organisationUtils' import PersonUtils from './personUtils' +import ReferralUtils from './referralUtils' import RouteUtils from './routeUtils' import StringUtils from './stringUtils' import TypeUtils from './typeUtils' -export { CourseUtils, DateUtils, OrganisationUtils, PersonUtils, RouteUtils, StringUtils, TypeUtils, nunjucksSetup } +export { + CourseUtils, + DateUtils, + OrganisationUtils, + PersonUtils, + ReferralUtils, + RouteUtils, + StringUtils, + TypeUtils, + nunjucksSetup, +} diff --git a/server/utils/referralUtils.test.ts b/server/utils/referralUtils.test.ts new file mode 100644 index 000000000..f845e2176 --- /dev/null +++ b/server/utils/referralUtils.test.ts @@ -0,0 +1,55 @@ +import ReferralUtils from './referralUtils' +import { referralFactory } from '../testutils/factories' + +describe('ReferralUtils', () => { + describe('taskListSections', () => { + it('returns task list sections for a given referral', () => { + const referral = referralFactory.build() + + expect(ReferralUtils.taskListSections(referral)).toEqual([ + { + heading: 'Personal details', + items: [ + { + statusTag: { classes: 'govuk-tag moj-task-list__task-completed', text: 'completed' }, + text: 'Confirm personal details', + }, + ], + }, + { + heading: 'Referral information', + items: [ + { + statusTag: { classes: 'govuk-tag govuk-tag--grey moj-task-list__task-completed', text: 'not started' }, + text: 'Add Accredited Programme history', + url: '#', + }, + { + statusTag: { classes: 'govuk-tag govuk-tag--grey moj-task-list__task-completed', text: 'not started' }, + text: 'Confirm the OASys information', + url: '#', + }, + { + statusTag: { classes: 'govuk-tag govuk-tag--grey moj-task-list__task-completed', text: 'not started' }, + text: 'Add reason for referral and any additional information', + url: '#', + }, + ], + }, + { + heading: 'Check answers and submit', + items: [ + { + statusTag: { + classes: 'govuk-tag govuk-tag--grey moj-task-list__task-completed', + text: 'cannot start yet', + }, + text: 'Check answers and submit', + url: '#', + }, + ], + }, + ]) + }) + }) +}) diff --git a/server/utils/referralUtils.ts b/server/utils/referralUtils.ts new file mode 100644 index 000000000..9f8748463 --- /dev/null +++ b/server/utils/referralUtils.ts @@ -0,0 +1,49 @@ +import type { Referral } from '@accredited-programmes/models' +import type { + ReferralTaskListSection, + ReferralTaskListStatusTag, + ReferralTaskListStatusText, +} from '@accredited-programmes/ui' + +export default class ReferralUtils { + // eslint-disable-next-line no-unused-vars, @typescript-eslint/no-unused-vars + static taskListSections(referral: Referral): Array { + return [ + { + heading: 'Personal details', + items: [{ statusTag: ReferralUtils.taskListStatus('completed'), text: 'Confirm personal details' }], + }, + { + heading: 'Referral information', + items: [ + { + statusTag: ReferralUtils.taskListStatus('not started'), + text: 'Add Accredited Programme history', + url: '#', + }, + { statusTag: ReferralUtils.taskListStatus('not started'), text: 'Confirm the OASys information', url: '#' }, + { + statusTag: ReferralUtils.taskListStatus('not started'), + text: 'Add reason for referral and any additional information', + url: '#', + }, + ], + }, + { + heading: 'Check answers and submit', + items: [ + { statusTag: ReferralUtils.taskListStatus('cannot start yet'), text: 'Check answers and submit', url: '#' }, + ], + }, + ] + } + + private static taskListStatus(text: ReferralTaskListStatusText): ReferralTaskListStatusTag { + const classes = + text === 'completed' + ? 'govuk-tag moj-task-list__task-completed' + : 'govuk-tag govuk-tag--grey moj-task-list__task-completed' + + return { classes, text } as ReferralTaskListStatusTag + } +} From 9e8f2d4bf60565de09bc33f86f962c23e19a318d Mon Sep 17 00:00:00 2001 From: Ynda Jas Date: Wed, 30 Aug 2023 16:44:19 +0100 Subject: [PATCH 2/3] Add task list to task list page This adds a task list to the task list page While an [MOJ Nunjucks macro][1] exists to do this, its options are quite limited and don't allow us to follow the designs: the macro has numbered sections, compulsory item links and no tags other than "completed" This loosely follows [a pattern from Interventions][2], who also built a task list using a mixture of MOJ Pattern Library classes, GOV.UK Design System classes and custom HTML The `shouldContainOrganisationAndCourseHeading` integration test method is updated to avoid a matcher that could now be ambiguous [1]: https://design-patterns.service.justice.gov.uk/patterns/task-list [2]: https://github.com/ministryofjustice/hmpps-interventions-ui/pull/34 --- assets/scss/application.scss | 1 + assets/scss/components/_task-list.scss | 3 +++ integration_tests/pages/page.ts | 2 +- .../refer/referralsController.test.ts | 6 +++++- .../controllers/refer/referralsController.ts | 3 ++- server/views/referrals/_taskListSection.njk | 19 +++++++++++++++++++ server/views/referrals/show.njk | 7 +++++++ 7 files changed, 38 insertions(+), 3 deletions(-) create mode 100644 assets/scss/components/_task-list.scss create mode 100644 server/views/referrals/_taskListSection.njk diff --git a/assets/scss/application.scss b/assets/scss/application.scss index 044050a11..704e41004 100755 --- a/assets/scss/application.scss +++ b/assets/scss/application.scss @@ -11,4 +11,5 @@ $govuk-page-width: $moj-page-width; @import './components/header-bar'; @import './components/navigation'; @import './components/person-banner'; +@import './components/task-list'; @import './local'; diff --git a/assets/scss/components/_task-list.scss b/assets/scss/components/_task-list.scss new file mode 100644 index 000000000..3aa219904 --- /dev/null +++ b/assets/scss/components/_task-list.scss @@ -0,0 +1,3 @@ +.moj-task-list__items { + padding-left: 0; +} diff --git a/integration_tests/pages/page.ts b/integration_tests/pages/page.ts index 55df1ef27..b164d32d5 100644 --- a/integration_tests/pages/page.ts +++ b/integration_tests/pages/page.ts @@ -86,7 +86,7 @@ export default abstract class Page { }): void { const { course, organisation } = pageWithOrganisationAndCoursePresenter - cy.get('h2:nth-of-type(1)').then(organisationAndCourseHeading => { + cy.get('.govuk-grid-column-two-thirds > h2:first-child').then(organisationAndCourseHeading => { const expectedText = `${organisation.name} | ${course.nameAndAlternateName}` const { actual, expected } = Helpers.parseHtml(organisationAndCourseHeading, expectedText) expect(actual).to.equal(expected) diff --git a/server/controllers/refer/referralsController.test.ts b/server/controllers/refer/referralsController.test.ts index f84517f4d..b809f7433 100644 --- a/server/controllers/refer/referralsController.test.ts +++ b/server/controllers/refer/referralsController.test.ts @@ -13,7 +13,7 @@ import { personFactory, referralFactory, } from '../../testutils/factories' -import { CourseUtils, TypeUtils } from '../../utils' +import { CourseUtils, ReferralUtils, TypeUtils } from '../../utils' import type { CoursePresenter } from '@accredited-programmes/ui' jest.mock('../../utils/courseUtils') @@ -127,6 +127,9 @@ describe('ReferralsController', () => { const person = personFactory.build() personService.getPerson.mockResolvedValue(person) + const referral = referralFactory.build({ offeringId: courseOffering.id, prisonNumber: person.prisonNumber }) + referralService.getReferral.mockResolvedValue(referral) + const requestHandler = referralsController.show() await requestHandler(request, response, next) @@ -138,6 +141,7 @@ describe('ReferralsController', () => { organisation, pageHeading: 'Make a referral', person, + taskListSections: ReferralUtils.taskListSections(referral), }) }) }) diff --git a/server/controllers/refer/referralsController.ts b/server/controllers/refer/referralsController.ts index be5588554..ad85c1a74 100644 --- a/server/controllers/refer/referralsController.ts +++ b/server/controllers/refer/referralsController.ts @@ -3,7 +3,7 @@ import createError from 'http-errors' import { referPaths } from '../../paths' import type { CourseService, OrganisationService, PersonService, ReferralService } from '../../services' -import { CourseUtils, TypeUtils } from '../../utils' +import { CourseUtils, ReferralUtils, TypeUtils } from '../../utils' import type { CreatedReferralResponse } from '@accredited-programmes/models' export default class ReferralsController { @@ -69,6 +69,7 @@ export default class ReferralsController { organisation, pageHeading: 'Make a referral', person, + taskListSections: ReferralUtils.taskListSections(referral), }) } } diff --git a/server/views/referrals/_taskListSection.njk b/server/views/referrals/_taskListSection.njk new file mode 100644 index 000000000..2761e9177 --- /dev/null +++ b/server/views/referrals/_taskListSection.njk @@ -0,0 +1,19 @@ +{% from "govuk/components/tag/macro.njk" import govukTag %} + +{% macro taskListSection(section) %} +
  • +

    {{ section.heading }}

    +
      + {% for item in section.items %} +
    • + {% if item.url %} + {{ item.text }} + {% else %} + {{ item.text }} + {% endif %} + {{ govukTag(item.statusTag) }} +
    • + {% endfor %} +
    +
  • +{% endmacro %} diff --git a/server/views/referrals/show.njk b/server/views/referrals/show.njk index 68702d9d5..90dee1e22 100644 --- a/server/views/referrals/show.njk +++ b/server/views/referrals/show.njk @@ -1,5 +1,6 @@ {% from "govuk/components/back-link/macro.njk" import govukBackLink %} +{% from "./_taskListSection.njk" import taskListSection %} {% from "../partials/audienceTags.njk" import audienceTags %} {% extends "../partials/layout.njk" %} @@ -23,6 +24,12 @@ {% include "../partials/organisationAndCourse.njk" %} {{ audienceTags(course.audienceTags) }} + +
      + {% for section in taskListSections %} + {{ taskListSection(section) }} + {% endfor %} +
    {% endblock content %} From a2988a40b037456f4f2e00fb54b45dde40449915 Mon Sep 17 00:00:00 2001 From: Ynda Jas Date: Thu, 31 Aug 2023 11:13:47 +0100 Subject: [PATCH 3/3] Check task list in integration tests --- integration_tests/e2e/refer.cy.ts | 5 +-- integration_tests/pages/refer/taskList.ts | 40 ++++++++++++++++++++--- 2 files changed, 39 insertions(+), 6 deletions(-) diff --git a/integration_tests/e2e/refer.cy.ts b/integration_tests/e2e/refer.cy.ts index 7e731850a..6448e1f89 100644 --- a/integration_tests/e2e/refer.cy.ts +++ b/integration_tests/e2e/refer.cy.ts @@ -130,7 +130,7 @@ context('Refer', () => { const confirmPersonPage = Page.verifyOnPage(ConfirmPersonPage, { course, courseOffering, person }) confirmPersonPage.confirmPerson() - Page.verifyOnPage(TaskListPage, { course, courseOffering, organisation }) + Page.verifyOnPage(TaskListPage, { course, courseOffering, organisation, referral }) }) it('Shows the in-progress referral task list', () => { @@ -169,11 +169,12 @@ context('Refer', () => { const path = referPaths.show({ referralId: referral.id }) cy.visit(path) - const taskListPage = Page.verifyOnPage(TaskListPage, { course, courseOffering, organisation }) + const taskListPage = Page.verifyOnPage(TaskListPage, { course, courseOffering, organisation, referral }) taskListPage.shouldHavePersonDetails(person) taskListPage.shouldContainNavigation(path) taskListPage.shouldContainBackLink('#') taskListPage.shouldContainOrganisationAndCourseHeading(taskListPage) taskListPage.shouldContainAudienceTags(taskListPage.course.audienceTags) + taskListPage.shouldContainTaskList() }) }) diff --git a/integration_tests/pages/refer/taskList.ts b/integration_tests/pages/refer/taskList.ts index 2ba4db7f1..b9cde2734 100644 --- a/integration_tests/pages/refer/taskList.ts +++ b/integration_tests/pages/refer/taskList.ts @@ -1,6 +1,6 @@ -import { CourseUtils } from '../../../server/utils' +import { CourseUtils, ReferralUtils } from '../../../server/utils' import Page from '../page' -import type { Course, CourseOffering, Organisation } from '@accredited-programmes/models' +import type { Course, CourseOffering, Organisation, Referral } from '@accredited-programmes/models' import type { CoursePresenter } from '@accredited-programmes/ui' export default class TaskListPage extends Page { @@ -10,12 +10,44 @@ export default class TaskListPage extends Page { organisation: Organisation - constructor(args: { course: Course; courseOffering: CourseOffering; organisation: Organisation }) { + referral: Referral + + constructor(args: { + course: Course + courseOffering: CourseOffering + organisation: Organisation + referral: Referral + }) { super('Make a referral') - const { course, courseOffering, organisation } = args + const { course, courseOffering, organisation, referral } = args this.course = CourseUtils.presentCourse(course) this.courseOffering = courseOffering this.organisation = organisation + this.referral = referral + } + + shouldContainTaskList() { + const taskListSections = ReferralUtils.taskListSections(this.referral) + + taskListSections.forEach((section, sectionIndex) => { + cy.get(`.moj-task-list > li:nth-child(${sectionIndex + 1})`).within(() => { + cy.get('.moj-task-list__section').should('have.text', section.heading) + + section.items.forEach((item, itemIndex) => { + cy.get(`.moj-task-list__item:nth-child(${itemIndex + 1})`).within(listItemElement => { + cy.get('.moj-task-list__task-name').then(taskNameElement => { + cy.wrap(taskNameElement).should('have.text', item.text) + + if (item.url) { + cy.wrap(taskNameElement).should('have.attr', 'href', item.url) + } + }) + + this.shouldContainTags([item.statusTag], listItemElement) + }) + }) + }) + }) } }