@benwatsonuk/govuk-pages-plugin 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (60) hide show
  1. package/CHANGELOG.md +7 -0
  2. package/LICENCE.txt +21 -0
  3. package/README.md +192 -0
  4. package/dist/assets/images/arrow.svg +27 -0
  5. package/dist/assets/images/page-types/arrow-base.svg +13 -0
  6. package/dist/assets/images/page-types/arrow-head.svg +13 -0
  7. package/dist/assets/images/page-types/check-answers.svg +142 -0
  8. package/dist/assets/images/page-types/checkbox-input.svg +85 -0
  9. package/dist/assets/images/page-types/confirmation.svg +73 -0
  10. package/dist/assets/images/page-types/content-page.svg +84 -0
  11. package/dist/assets/images/page-types/email.svg +56 -0
  12. package/dist/assets/images/page-types/radio-input.svg +115 -0
  13. package/dist/assets/images/page-types/start-page.svg +101 -0
  14. package/dist/assets/images/page-types/task-list.svg +179 -0
  15. package/dist/assets/images/page-types/text-input.svg +65 -0
  16. package/dist/assets/javascripts/all.js +1 -0
  17. package/dist/assets/styles/all.scss +2 -0
  18. package/dist/assets/styles/scss/_pages.scss +169 -0
  19. package/dist/assets/styles/scss/_variables.scss +19 -0
  20. package/dist/functions/pages/getPages.js +9 -0
  21. package/dist/functions/stages/getStages.js +65 -0
  22. package/dist/index.js +39 -0
  23. package/dist/schema.json +89 -0
  24. package/dist/types.js +2 -0
  25. package/dist/validate.js +27 -0
  26. package/dist/views/page-index.html +34 -0
  27. package/dist/views/stage-index.html +41 -0
  28. package/govuk-prototype-kit.config.json +12 -0
  29. package/index.js +22 -0
  30. package/package.json +43 -0
  31. package/src/assets/images/arrow.svg +27 -0
  32. package/src/assets/images/page-types/arrow-base.svg +13 -0
  33. package/src/assets/images/page-types/arrow-head.svg +13 -0
  34. package/src/assets/images/page-types/check-answers.svg +142 -0
  35. package/src/assets/images/page-types/checkbox-input.svg +85 -0
  36. package/src/assets/images/page-types/confirmation.svg +73 -0
  37. package/src/assets/images/page-types/content-page.svg +84 -0
  38. package/src/assets/images/page-types/email.svg +56 -0
  39. package/src/assets/images/page-types/radio-input.svg +115 -0
  40. package/src/assets/images/page-types/start-page.svg +101 -0
  41. package/src/assets/images/page-types/task-list.svg +179 -0
  42. package/src/assets/images/page-types/text-input.svg +65 -0
  43. package/src/assets/javascripts/all.js +1 -0
  44. package/src/assets/styles/all.scss +2 -0
  45. package/src/assets/styles/scss/_pages.scss +169 -0
  46. package/src/assets/styles/scss/_variables.scss +19 -0
  47. package/src/functions/pages/getPages.ts +7 -0
  48. package/src/functions/stages/getStages.ts +78 -0
  49. package/src/index.ts +40 -0
  50. package/src/schema.json +89 -0
  51. package/src/types.ts +40 -0
  52. package/src/validate.ts +35 -0
  53. package/src/views/page-index.html +34 -0
  54. package/src/views/stage-index.html +41 -0
  55. package/test/data/outputs.ts +197 -0
  56. package/test/data/pages.ts +61 -0
  57. package/test/data/stages.ts +87 -0
  58. package/test/getPages.test.js +19 -0
  59. package/test/getStages.test.js +46 -0
  60. package/tsconfig.json +14 -0
@@ -0,0 +1,169 @@
1
+ .govuk-pages-plugin {
2
+
3
+ .screenPreviewContainer {
4
+ height: 400px;
5
+ overflow: hidden;
6
+ border: 1px solid #000;
7
+ .screenPreview {
8
+ width: 200%;
9
+ height: 800px;
10
+ transform: scale(0.5);
11
+ transform-origin: 0 0;
12
+ background: none;
13
+ border: 0;
14
+ }
15
+ }
16
+
17
+ .page-flow-feedback {
18
+ &--positive {
19
+ color: #00a33b;
20
+ }
21
+ &--negative {
22
+ color: #a33038;
23
+ }
24
+ }
25
+
26
+ .page-flow--stages, .user-flow--stages {
27
+ @extend .govuk-body;
28
+ padding: 0;
29
+ }
30
+ .user-flow-item--stage {
31
+ display: inline-block;
32
+ }
33
+
34
+ .user-flow-page-item {
35
+ border-top: 1px solid #333;
36
+ padding-top:1em;
37
+ //margin-bottom: 2em;
38
+ }
39
+
40
+ .page-flow-item--stage, .user-flow-item--stage {
41
+ border-top: 10px solid;
42
+ .page-flow-stage-descriptor {
43
+ padding: 2px 10px;
44
+ display: block;
45
+ //padding: 5px;
46
+ }
47
+ &--1 {
48
+ border-color: $stage-color--1;
49
+ }
50
+ &--2 {
51
+ border-color: $stage-color--2;
52
+ }
53
+ &--3 {
54
+ border-color: $stage-color--3;
55
+ }
56
+ &--4 {
57
+ border-color: $stage-color--4;
58
+ }
59
+ &--authentication {
60
+ border-color: $stage-color--authentication;
61
+ }
62
+ &--documents {
63
+ border-color: $stage-color--documents;
64
+ }
65
+ &--notifications, &--notifications {
66
+ border-color: $stage-color--notifications;
67
+ }
68
+ &:last-of-type {
69
+ .page-flow-item--page{
70
+ &:last-of-type {
71
+ &:before {
72
+ display: none;
73
+ }
74
+ }
75
+ }
76
+ }
77
+ }
78
+
79
+ .page-flow--pages, .user-flow--pages {
80
+ @extend .govuk-body;
81
+ padding: 0;
82
+ .page-flow-item--page {
83
+ display: inline-block;
84
+ font-size: 14px;
85
+ width: 130px;
86
+ //min-height: 200px;
87
+ vertical-align: top;
88
+ position: relative;
89
+ .page-flow-item-screen-icon {
90
+ width: 100px;
91
+ height: 100px;
92
+ display: block;
93
+ background-repeat: no-repeat;
94
+ &--email {
95
+ background-image: url($icons + '/email.svg');
96
+ -webkit-background-size: contain;
97
+ background-size: contain;
98
+ }
99
+ &--text-input {
100
+ background-image: url($icons + '/text-input.svg');
101
+ -webkit-background-size: contain;
102
+ background-size: contain;
103
+ }
104
+ &--dashboard, &--content-page {
105
+ background-image: url($icons + '/content-page.svg');
106
+ -webkit-background-size: contain;
107
+ background-size: contain;
108
+ }
109
+ &--radio-input {
110
+ background-image: url($icons + '/radio-input.svg');
111
+ -webkit-background-size: contain;
112
+ background-size: contain;
113
+ }
114
+ &--checkbox-input {
115
+ background-image: url($icons + '/checkbox-input.svg');
116
+ -webkit-background-size: contain;
117
+ background-size: contain;
118
+ }
119
+ &--check-answers {
120
+ background-image: url($icons + '/check-answers.svg');
121
+ -webkit-background-size: contain;
122
+ background-size: contain;
123
+ }
124
+ &--confirmation {
125
+ background-image: url($icons + '/confirmation.svg');
126
+ -webkit-background-size: contain;
127
+ background-size: contain;
128
+ }
129
+ &--task-list {
130
+ background-image: url($icons + '/task-list.svg');
131
+ -webkit-background-size: contain;
132
+ background-size: contain;
133
+ }
134
+ &--start-page {
135
+ background-image: url($icons + '/start-page.svg');
136
+ -webkit-background-size: contain;
137
+ background-size: contain;
138
+ }
139
+ &--checkbox-page {
140
+ background-image: url($icons + '/checkbox-page.svg');
141
+ -webkit-background-size: contain;
142
+ background-size: contain;
143
+ }
144
+ }
145
+ }
146
+ }
147
+ .user-flow--pages {
148
+ @extend .page-flow--pages;
149
+ .page-flow-item--page {
150
+ display: inline-block;
151
+ font-size: 14px;
152
+ width: 170px;
153
+ //min-height: 200px;
154
+ vertical-align: top;
155
+ position: relative;
156
+
157
+ &:before {
158
+ content: "";
159
+ position: absolute;
160
+ background: url($page-flow-images + '/arrow.svg') no-repeat;
161
+ background-size: contain;
162
+ width: 78px;
163
+ left: 92px;
164
+ height: 20px;
165
+ top: 35px;
166
+ }
167
+ }
168
+ }
169
+ }
@@ -0,0 +1,19 @@
1
+ // Assets - these can all be overridden
2
+ $page-flow-assets: '/extension-assets/govuk-page-flow-plugin/app/assets/';
3
+ $page-flow-images: $page-flow-assets + 'images/';
4
+ $icons: $page-flow-images + 'page-types';
5
+
6
+ // Stock colours for stages
7
+ $stage-color--1: #006c56;
8
+ $stage-color--2: #00beb7;
9
+ $stage-color--3: pink;
10
+ $stage-color--4: #f47738;
11
+ $stage-color--5: #ccc;
12
+ $stage-color--6: #005ea5;
13
+ $stage-color--7: #004432;
14
+ $stage-color--8: #f47738;
15
+ $stage-color--9: #3b1f5f;
16
+ $stage-color--10: #ffdd00;
17
+ $stage-color--documents: purple;
18
+ $stage-color--notifications: teal;
19
+ $stage-color--authentication: crimson;
@@ -0,0 +1,7 @@
1
+ import { PagesArray } from "../../types"
2
+ import { validatePagesArray } from "../../validate"
3
+
4
+ export const getPages = (pages: PagesArray) => {
5
+ const validatedPages = validatePagesArray(pages)
6
+ return validatedPages
7
+ }
@@ -0,0 +1,78 @@
1
+ import { PagesArray, StagesArray, StagesWithPagesArray } from "../../types"
2
+ import { validatePagesArray, validateStagesArray } from "../../validate"
3
+
4
+ // Get and validate stages from the provided input
5
+ export const getStages = (stages: StagesArray) => {
6
+ const validatedStages = validateStagesArray(stages)
7
+ return validatedStages
8
+ }
9
+
10
+ // Get and validate stages and relevant pages from the provided input. Return an array of Stage objects containing relevant pages.
11
+ export const getStagesWithPages = (stages: StagesArray, pages: PagesArray): StagesWithPagesArray => {
12
+ const validatedStages = validateStagesArray(stages)
13
+ const validatedPages = validatePagesArray(pages)
14
+ return mapPagesToStages(validatedStages, validatedPages)
15
+ }
16
+
17
+ // Below this point is just the helper function used internally for the above functions
18
+
19
+ // This function does the heavy lifting of mapping pages to their relevant stages
20
+ export const mapPagesToStages = (
21
+ stages: StagesArray,
22
+ pages: PagesArray
23
+ ): StagesWithPagesArray => {
24
+ let allPages = [...pages]; // Create a copy to avoid mutating the original
25
+
26
+ const stagesWithPages: StagesWithPagesArray = stages.map((stage) => {
27
+ // Filter pages that belong to this stage
28
+ const pagesForStage = pages.filter(
29
+ (page) => page.stage && page.stage.main === stage.id
30
+ )
31
+
32
+ // Only add the stage if it has pages (after all, why return empty stages?)
33
+ if (pagesForStage.length > 0) {
34
+
35
+ // Remove the filtered pages from allPages to get unused pages
36
+ allPages = allPages.filter(page => !pagesForStage.some(p => p.id === page.id));
37
+
38
+ // Map sub-stages with their relevant pages
39
+ let subStagesWithPages
40
+ if (stage.subStages) {
41
+ subStagesWithPages = stage.subStages.map((subStage) => {
42
+ // Filter pages that belong to this sub-stage
43
+ const pagesForSubStage = pagesForStage.filter(
44
+ (page) => page.stage && page.stage.subStage === subStage.id
45
+ )
46
+
47
+ if (pagesForSubStage.length > 0) {
48
+ return {
49
+ id: subStage.id,
50
+ title: subStage.title,
51
+ description: subStage.description,
52
+ pages: pagesForSubStage
53
+ }
54
+ }
55
+ }).filter(subStage => subStage !== undefined)
56
+ }
57
+
58
+ return {
59
+ id: stage.id,
60
+ title: stage.title,
61
+ description: stage.description,
62
+ subStages: subStagesWithPages,
63
+ pages: pagesForStage
64
+ }
65
+ }
66
+ }).filter(stage => stage !== undefined);
67
+
68
+ // add allPages to a misc category and bolt on to stagesWithPages
69
+ if (allPages.length > 0) {
70
+ stagesWithPages.push({
71
+ id: "unassigned",
72
+ title: "Unassigned",
73
+ description: "Pages not assigned to any stage or sub-stage",
74
+ pages: allPages
75
+ })
76
+ }
77
+ return stagesWithPages
78
+ }
package/src/index.ts ADDED
@@ -0,0 +1,40 @@
1
+ import {Router} from "express"
2
+ import { getPages } from "./functions/pages/getPages"
3
+ import { getStagesWithPages } from "./functions/stages/getStages"
4
+ import { PagesArray, StagesArray } from "./types"
5
+
6
+ /*--- UTILITIES (used by supplied routes AND made available to plugin users) ---*/
7
+
8
+ export const pageIndexData = (pages: PagesArray) => {
9
+ return getPages(pages)
10
+ }
11
+
12
+ export const pageIndex = (pages: PagesArray, pageType: string) => {
13
+ pageType = pageType || "page-index"
14
+ return (req: any, res: any) => {
15
+ res.render(pageType, { pages: pages })
16
+ }
17
+ }
18
+
19
+ export const stageIndexData = (stages: StagesArray, pages: PagesArray) => {
20
+ return getStagesWithPages(stages, pages)
21
+ }
22
+
23
+ export const stageIndex = (stages: StagesArray, pages: PagesArray, pageType?: string) => {
24
+ pageType = pageType || "stage-index"
25
+ return (req: any, res: any) => {
26
+ res.render(pageType, { stages: stageIndexData(stages, pages) })
27
+ }
28
+ }
29
+
30
+ // Add user flows, etc here later
31
+
32
+ /*--- THE MAIN USER ROUTES ---*/
33
+
34
+ export const govukPagesPlugin = (pages: PagesArray, stages?: StagesArray, pageType?: string) => {
35
+ pageType = pageType || "page-index" // Options can be 'all', 'page-index', 'stage-index' - in future could be 'user-flow-index', etc
36
+ const router = Router()
37
+ // This is the default offering from the plugin - it is expected that must users will use this. It should be robust
38
+ router.get("/", pageIndex(pageIndexData(pages), pageType))
39
+ return router
40
+ }
@@ -0,0 +1,89 @@
1
+ {
2
+ "$schema": "http://json-schema.org/draft-07/schema#",
3
+ "type": "object",
4
+ "required": ["mode"],
5
+ "properties": {
6
+ "mode": {
7
+ "type": "string",
8
+ "enum": ["pages", "stages"]
9
+ }
10
+ },
11
+ "allOf": [
12
+ {
13
+ "if": {
14
+ "properties": { "mode": { "const": "pages" } }
15
+ },
16
+ "then": { "$ref": "#/definitions/pages" },
17
+ "else": { "$ref": "#/definitions/stages" }
18
+ }
19
+ ],
20
+ "definitions": {
21
+ "pages": {
22
+ "type": "object",
23
+ "required": ["mode", "pages"],
24
+ "properties": {
25
+ "mode": { "const": "pages" },
26
+ "pages": {
27
+ "type": "array",
28
+ "items": {
29
+ "type": "object",
30
+ "required": ["id", "title", "route"],
31
+ "properties": {
32
+ "id": { "type": "integer" },
33
+ "title": { "type": "string" },
34
+ "route": { "type": "string" },
35
+ "type": { "type": ["string", "null"] },
36
+ "stage": {
37
+ "type": ["object"],
38
+ "properties": {
39
+ "main": { "type": ["string"] },
40
+ "subStage": { "type": ["integer", "string"] }
41
+ },
42
+ "additionalProperties": false
43
+ },
44
+ "description": { "type": ["string", "null"] }
45
+ },
46
+ "additionalProperties": true
47
+ }
48
+ }
49
+ },
50
+ "additionalProperties": false
51
+ },
52
+ "stages": {
53
+ "type": "object",
54
+ "required": ["mode", "stages"],
55
+ "properties": {
56
+ "mode": { "const": "stages" },
57
+ "stages": {
58
+ "type": "array",
59
+ "items": {
60
+ "type": "object",
61
+ "required": ["id", "title", "route"],
62
+ "properties": {
63
+ "id": { "type": "string" },
64
+ "title": { "type": "string" },
65
+ "route": { "type": "string" },
66
+ "description": { "type": ["string", "null"] },
67
+ "subStages": {
68
+ "type": "array",
69
+ "items": {
70
+ "type": "object",
71
+ "required": ["id", "title"],
72
+ "properties": {
73
+ "id": { "type": ["string", "integer"] },
74
+ "title": { "type": "string" },
75
+ "route": { "type": "string" },
76
+ "description": { "type": ["string", "null"] }
77
+ },
78
+ "additionalProperties": true
79
+ }
80
+ }
81
+ },
82
+ "additionalProperties": true
83
+ }
84
+ }
85
+ },
86
+ "additionalProperties": false
87
+ }
88
+ }
89
+ }
package/src/types.ts ADDED
@@ -0,0 +1,40 @@
1
+ export interface Page {
2
+ id: number
3
+ title: string
4
+ type: string
5
+ route: string
6
+ description?: string
7
+ stage?: {
8
+ main: string,
9
+ subStage?: number | string
10
+ }
11
+ }
12
+
13
+ type SubStage = Omit<Stage, 'id' | 'subStages'> & {
14
+ id: number
15
+ }
16
+
17
+ type SubStageWithPages = Omit<StageWithPages, 'id' | 'subStages'> & {
18
+ id: number | string
19
+ }
20
+
21
+ export interface Stage {
22
+ id: string
23
+ title: string
24
+ route?: string
25
+ description: string | null
26
+ subStages?: SubStage[]
27
+ }
28
+
29
+ export interface StageWithPages {
30
+ id: string
31
+ title: string
32
+ description: string | null
33
+ subStages?: SubStageWithPages[]
34
+ pages: Page[]
35
+ }
36
+
37
+
38
+ export type StagesArray = Stage[];
39
+ export type PagesArray = Page[];
40
+ export type StagesWithPagesArray = StageWithPages[] | [];
@@ -0,0 +1,35 @@
1
+ import Ajv from "ajv"
2
+ import schema from "./schema.json"
3
+ import { PagesArray, StagesArray } from "./types"
4
+
5
+ const ajv = new Ajv({ allErrors: true, strict: false })
6
+
7
+ const validate = ajv.compile(schema)
8
+
9
+ export function validatePagesArray(pages: unknown): PagesArray {
10
+ if (!validate({ mode: "pages", pages: pages })) {
11
+ const message = validate.errors
12
+ ?.map(err => `${err.instancePath || "Pages"} ${err.message}`)
13
+ .join("\n")
14
+
15
+ throw new Error(
16
+ `Invalid array of PAGES passed to govuk-pages-plugin - please check the documentation to ensure the JSON schema you are passing matches what is expected:\n${message}`
17
+ )
18
+ }
19
+
20
+ return pages as PagesArray
21
+ }
22
+
23
+ export function validateStagesArray(stages: unknown): StagesArray {
24
+ if (!validate({ mode: "stages", stages: stages })) {
25
+ const message = validate.errors
26
+ ?.map(err => `${err.instancePath || "Stages"} ${err.message}`)
27
+ .join("\n")
28
+
29
+ throw new Error(
30
+ `Invalid array of STAGES passed to govuk-pages-plugin - please check the documentation to ensure the JSON schema you are passing matches what is expected:\n${message}`
31
+ )
32
+ }
33
+
34
+ return stages as StagesArray
35
+ }
@@ -0,0 +1,34 @@
1
+ {% extends "layouts/main.html" %}
2
+
3
+ {% block pageTitle %}
4
+ Page Index
5
+ {% endblock %}
6
+
7
+ {% block beforeContent %}
8
+ {% from "govuk/components/back-link/macro.njk" import govukBackLink %}
9
+
10
+ {{ govukBackLink({
11
+ text: "Back",
12
+ href: "javascript:history.back(-1);"
13
+ }) }}
14
+ {% endblock %}
15
+
16
+ {% block content %}
17
+
18
+ <div class="page-index ">
19
+ <h1 class="govuk-heading-l">Page Index</h1>
20
+ {% from "govuk/components/details/macro.njk" import govukDetails %}
21
+
22
+ {{ govukDetails({
23
+ summaryText: "What is the 'Page Index'?",
24
+ html: "<p>This page contains links to tracked pages in this prototype. It is useful for navigating the prototype without having to go through flows.</p><p class='govuk-body-s'> Note, the prototype might contain more pages than are listed here.</p>"
25
+ }) }}
26
+
27
+ <ul class="govuk-list govuk-list--number">
28
+ {% for page in pages %}
29
+ <li><a href="{{ page.route }}">{{ page.title }}</a></li>
30
+ {% endfor %}
31
+ </ul>
32
+ </div>
33
+
34
+ {% endblock %}
@@ -0,0 +1,41 @@
1
+ {% extends "layouts/main.html" %}
2
+
3
+ {% block pageTitle %}
4
+ Stage Index
5
+ {% endblock %}
6
+
7
+ {% block beforeContent %}
8
+ {% from "govuk/components/back-link/macro.njk" import govukBackLink %}
9
+
10
+ {{ govukBackLink({
11
+ text: "Back",
12
+ href: "javascript:history.back(-1);"
13
+ }) }}
14
+ {% endblock %}
15
+
16
+ {% block content %}
17
+
18
+ <div class="page-index ">
19
+ <h1 class="govuk-heading-l">Stage Index</h1>
20
+ {% from "govuk/components/details/macro.njk" import govukDetails %}
21
+
22
+ {{ govukDetails({
23
+ summaryText: "What is the 'Stage Index'?",
24
+ html: "<p>This page contains links to tracked pages in this prototype, these pages are grouped by the 'stages' that they are assigned to. It is useful for navigating the prototype without having to go through flows.</p><p>'Stages' are defined by the plugin user but can be throught about as logical groups of pages that work to achieve a common goal, such as 'authenticating' or 'registering'.</p><p class='govuk-body-s'> Note, the prototype might contain more pages than are listed here.</p>"
25
+ }) }}
26
+ <!-- {{ stages | dump }} -->
27
+ {% for stage in stages %}
28
+ <h2>{{ stage.title }}</h2>
29
+ {% if stage.description %}
30
+ <p class="govuk-body-s">{{ stage.description }}</p>
31
+ {% endif %}
32
+ <ol>
33
+ {% for page in stage.pages %}
34
+ <li><a href="{{ page.route }}">{{ page.title }}</a>{% if page.description %} - <span class="govuk-body-s">{{ page.description }}</span> {% endif %}</li>
35
+ {% endfor %}
36
+ </ol>
37
+ <hr/>
38
+ {% endfor %}
39
+ </div>
40
+
41
+ {% endblock %}