From 45a66f544338d876a77dd5ec1284c181d0328310 Mon Sep 17 00:00:00 2001 From: helennickols Date: Mon, 12 May 2025 09:38:32 +0100 Subject: [PATCH 01/18] docs: updates --- docs/contribute.md | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/docs/contribute.md b/docs/contribute.md index c08a885c0..406cf1a35 100644 --- a/docs/contribute.md +++ b/docs/contribute.md @@ -34,6 +34,14 @@ The component will be given an 'experimental' status. This is to show that it's [Add a component to the MoJ Design System](/contribute/add-new-component/start) +## Adding a pattern or page + +
+Later in 2025 you'll also be able to add a pattern or page. +
+ +These building blocks will also have an experimental status. + ## Joining discussions After an experimental building block is live, contributions can be added to a Github discussion thread. These might include feedback, images, user research findings or accessibility insights. From 459ad13768d03c1c7fc3c6ab3f521cf6ce1a6c57 Mon Sep 17 00:00:00 2001 From: Chris Pymm Date: Fri, 2 May 2025 10:00:43 +0100 Subject: [PATCH 02/18] docs: add sentry error logging --- package-lock.json | 148 ++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 148 insertions(+) diff --git a/package-lock.json b/package-lock.json index da27aac13..398675ef3 100644 --- a/package-lock.json +++ b/package-lock.json @@ -2204,6 +2204,8 @@ }, "node_modules/@es-joy/jsdoccomment": { "version": "0.50.2", + "resolved": "https://registry.npmjs.org/@es-joy/jsdoccomment/-/jsdoccomment-0.50.2.tgz", + "integrity": "sha512-YAdE/IJSpwbOTiaURNCKECdAwqrJuFiZhylmesBcIRawtYKnBR2wxPhoIewMg+Yu+QuYvHfJNReWpoxGBKOChA==", "dev": true, "license": "MIT", "dependencies": { @@ -3880,6 +3882,8 @@ }, "node_modules/@puppeteer/browsers": { "version": "2.10.5", + "resolved": "https://registry.npmjs.org/@puppeteer/browsers/-/browsers-2.10.5.tgz", + "integrity": "sha512-eifa0o+i8dERnngJwKrfp3dEq7ia5XFyoqB17S4gK8GhsQE4/P8nxOfQSE0zQHxzzLo/cmF+7+ywEQ7wK7Fb+w==", "dev": true, "license": "Apache-2.0", "dependencies": { @@ -5574,6 +5578,8 @@ }, "node_modules/@tootallnate/quickjs-emscripten": { "version": "0.23.0", + "resolved": "https://registry.npmjs.org/@tootallnate/quickjs-emscripten/-/quickjs-emscripten-0.23.0.tgz", + "integrity": "sha512-C5Mc6rdnsaJDjO3UpGW/CQTHtCKaYlScZTly4JIu97Jxo/odCiH0ITnDXSJPTOrEKk/ycSZ0AOgTmkDtkOsvIA==", "dev": true, "license": "MIT" }, @@ -6106,6 +6112,8 @@ }, "node_modules/@types/yauzl": { "version": "2.10.3", + "resolved": "https://registry.npmjs.org/@types/yauzl/-/yauzl-2.10.3.tgz", + "integrity": "sha512-oJoftv0LSuaDZE3Le4DbKX+KS9G36NzOeSap90UIK0yMA/NhKJhqlSGtNDORNRaIbQfzjXDrQa0ytJ6mNRGz/Q==", "dev": true, "license": "MIT", "optional": true, @@ -6353,6 +6361,8 @@ }, "node_modules/acorn": { "version": "8.14.1", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.14.1.tgz", + "integrity": "sha512-OvQ/2pUDKmgfCg++xsTX1wGxfTaszcHVcTctW4UJB4hibJx2HXxxO5UmVgyjMa+ZDsiaf5wWLXYpRWMmBI0QHg==", "license": "MIT", "bin": { "acorn": "bin/acorn" @@ -6612,6 +6622,8 @@ }, "node_modules/array-differ": { "version": "1.0.0", + "resolved": "https://registry.npmjs.org/array-differ/-/array-differ-1.0.0.tgz", + "integrity": "sha512-LeZY+DZDRnvP7eMuQ6LHfCzUGxAAIViUBliK24P3hWXL6y4SortgR6Nim6xrkfSLlmH0+k+9NYNwVC2s53ZrYQ==", "dev": true, "license": "MIT", "engines": { @@ -6664,6 +6676,8 @@ }, "node_modules/array-union": { "version": "1.0.2", + "resolved": "https://registry.npmjs.org/array-union/-/array-union-1.0.2.tgz", + "integrity": "sha512-Dxr6QJj/RdU/hCaBjOfxW+q6lyuVE6JFWIrAUpuOOhoJJoQ99cUn3igRaHVB5P9WrgFVN0FfArM3x0cueOU8ng==", "dev": true, "license": "MIT", "dependencies": { @@ -6675,6 +6689,8 @@ }, "node_modules/array-uniq": { "version": "1.0.3", + "resolved": "https://registry.npmjs.org/array-uniq/-/array-uniq-1.0.3.tgz", + "integrity": "sha512-MNha4BWQ6JbwhFhj03YK552f7cb3AzoE8SzeljgChvL1dl3IcvggXVz1DilzySZkCja+CXuZbdW7yATchWn8/Q==", "dev": true, "license": "MIT", "engines": { @@ -6756,6 +6772,8 @@ }, "node_modules/arrify": { "version": "1.0.1", + "resolved": "https://registry.npmjs.org/arrify/-/arrify-1.0.1.tgz", + "integrity": "sha512-3CYzex9M9FGQjCGMGyi6/31c8GJbgb0qGyrx5HWxPd0aCwh4cB2YjMb2Xf9UuoogrMrlO9cTqnB5rI5GHZTcUA==", "dev": true, "license": "MIT", "engines": { @@ -6776,6 +6794,8 @@ }, "node_modules/ast-types": { "version": "0.13.4", + "resolved": "https://registry.npmjs.org/ast-types/-/ast-types-0.13.4.tgz", + "integrity": "sha512-x1FCFnFifvYDDzTaLII71vG5uvDwgtmDTEVWAxrgeiR8VjMONcCXJx7E+USjDtHlwFmt9MysbqgF9b9Vjr6w+w==", "dev": true, "license": "MIT", "dependencies": { @@ -7112,6 +7132,8 @@ }, "node_modules/bare-fs": { "version": "4.1.5", + "resolved": "https://registry.npmjs.org/bare-fs/-/bare-fs-4.1.5.tgz", + "integrity": "sha512-1zccWBMypln0jEE05LzZt+V/8y8AQsQQqxtklqaIyg5nu6OAYFhZxPXinJTSG+kU5qyNmeLgcn9AW7eHiCHVLA==", "dev": true, "license": "Apache-2.0", "optional": true, @@ -7134,6 +7156,8 @@ }, "node_modules/bare-os": { "version": "3.6.1", + "resolved": "https://registry.npmjs.org/bare-os/-/bare-os-3.6.1.tgz", + "integrity": "sha512-uaIjxokhFidJP+bmmvKSgiMzj2sV5GPHaZVAIktcxcpCyBFFWO+YlikVAdhmUo2vYFvFhOXIAlldqV29L8126g==", "dev": true, "license": "Apache-2.0", "optional": true, @@ -7143,6 +7167,8 @@ }, "node_modules/bare-path": { "version": "3.0.0", + "resolved": "https://registry.npmjs.org/bare-path/-/bare-path-3.0.0.tgz", + "integrity": "sha512-tyfW2cQcB5NN8Saijrhqn0Zh7AnFNsnczRcuWODH0eYAXBsJ5gVxAUuNr7tsHSC6IZ77cA0SitzT+s47kot8Mw==", "dev": true, "license": "Apache-2.0", "optional": true, @@ -7152,6 +7178,8 @@ }, "node_modules/bare-stream": { "version": "2.6.5", + "resolved": "https://registry.npmjs.org/bare-stream/-/bare-stream-2.6.5.tgz", + "integrity": "sha512-jSmxKJNJmHySi6hC42zlZnq00rga4jjxcgNZjY9N5WlOe/iOoGRtdwGsHzQv2RlH2KOYMwGUXhf2zXd32BA9RA==", "dev": true, "license": "Apache-2.0", "optional": true, @@ -7200,6 +7228,8 @@ }, "node_modules/basic-ftp": { "version": "5.0.5", + "resolved": "https://registry.npmjs.org/basic-ftp/-/basic-ftp-5.0.5.tgz", + "integrity": "sha512-4Bcg1P8xhUuqcii/S0Z9wiHIrQVPMermM1any+MX5GeGD7faD3/msQUDGLol9wOcz4/jbg/WJnGqoJF6LiBdtg==", "dev": true, "license": "MIT", "engines": { @@ -8652,6 +8682,8 @@ }, "node_modules/chromium-bidi/node_modules/mitt": { "version": "3.0.1", + "resolved": "https://registry.npmjs.org/mitt/-/mitt-3.0.1.tgz", + "integrity": "sha512-vKivATfr97l2/QBCYAkXYDbrIWPM2IIKEl7YPhjCvKlG3kE2gm+uBo6nEXK3M5/Ffh/FLpKExzOQ3JJoJGFKBw==", "dev": true, "license": "MIT" }, @@ -8984,6 +9016,8 @@ }, "node_modules/comment-parser": { "version": "1.4.1", + "resolved": "https://registry.npmjs.org/comment-parser/-/comment-parser-1.4.1.tgz", + "integrity": "sha512-buhp5kePrmda3vhc5B9t7pUQXAb2Tnd0qgpkIhPhkHXxJpiPJ11H0ZEU0oBpJ2QztSbzG/ZxMj/CHsYJqRHmyg==", "dev": true, "license": "MIT", "engines": { @@ -9076,6 +9110,8 @@ }, "node_modules/component-emitter": { "version": "1.3.1", + "resolved": "https://registry.npmjs.org/component-emitter/-/component-emitter-1.3.1.tgz", + "integrity": "sha512-T0+barUSQRTUQASh8bx02dl+DhF54GtIDY13Y3m9oWTklKbb3Wv974meRpeZ3lp1JpLVECWWNHC4vaG2XHXouQ==", "dev": true, "license": "MIT", "funding": { @@ -9443,6 +9479,8 @@ }, "node_modules/cookiejar": { "version": "2.1.4", + "resolved": "https://registry.npmjs.org/cookiejar/-/cookiejar-2.1.4.tgz", + "integrity": "sha512-LDx6oHrK+PhzLKJU9j5S7/Y3jM/mUHvD/DeI1WQmJn652iPC5Y4TBzC9l+5OMOXlyTTA+SmVUPm0HQUwpD5Jqw==", "dev": true, "license": "MIT" }, @@ -9634,6 +9672,8 @@ }, "node_modules/css-declaration-sorter": { "version": "7.2.0", + "resolved": "https://registry.npmjs.org/css-declaration-sorter/-/css-declaration-sorter-7.2.0.tgz", + "integrity": "sha512-h70rUM+3PNFuaBDTLe8wF/cdWu+dOZmb7pJt8Z2sedYbAcQVQV/tEchueg3GWxwqS0cxtbxmaHEdkNACqcvsow==", "dev": true, "license": "ISC", "engines": { @@ -9981,6 +10021,8 @@ }, "node_modules/data-uri-to-buffer": { "version": "6.0.2", + "resolved": "https://registry.npmjs.org/data-uri-to-buffer/-/data-uri-to-buffer-6.0.2.tgz", + "integrity": "sha512-7hvf7/GW8e86rW0ptuwS3OcBGDjIi6SZva7hCyWC0yYry2cOPmLIjXAUHI6DK2HsnwJd9ifmt57i8eV2n4YNpw==", "dev": true, "license": "MIT", "engines": { @@ -10055,6 +10097,8 @@ }, "node_modules/debug": { "version": "4.4.1", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.1.tgz", + "integrity": "sha512-KcKCqiftBJcZr++7ykoDIEwSa3XWowTfNPo92BYxjXiyYEVrUQh2aLyhxBCwww+heortUFxEJYcRzosstTEBYQ==", "license": "MIT", "dependencies": { "ms": "^2.1.3" @@ -10390,6 +10434,8 @@ }, "node_modules/degenerator": { "version": "5.0.1", + "resolved": "https://registry.npmjs.org/degenerator/-/degenerator-5.0.1.tgz", + "integrity": "sha512-TllpMR/t0M5sqCXfj85i4XaAzxmS5tVA16dqvdkMwGmzI+dXLXnw3J+3Vdv7VKw+ThlTMboK6i9rnZ6Nntj5CQ==", "dev": true, "license": "MIT", "dependencies": { @@ -10459,6 +10505,8 @@ }, "node_modules/dependency-graph": { "version": "1.0.0", + "resolved": "https://registry.npmjs.org/dependency-graph/-/dependency-graph-1.0.0.tgz", + "integrity": "sha512-cW3gggJ28HZ/LExwxP2B++aiKxhJXMSIt9K48FOXQkm+vuG5gyatXnLsONRJdzO/7VfjDIiaOOa/bs4l464Lwg==", "dev": true, "license": "MIT", "engines": { @@ -12775,6 +12823,8 @@ }, "node_modules/extract-zip": { "version": "2.0.1", + "resolved": "https://registry.npmjs.org/extract-zip/-/extract-zip-2.0.1.tgz", + "integrity": "sha512-GDhU9ntwuKyGXdZBUgTIe+vXnWj0fppUEtMDL0+idd5Sta8TGpHssn/eusA9mrPr9qNDym6SxAYZjNvCn/9RBg==", "dev": true, "license": "BSD-2-Clause", "dependencies": { @@ -12794,6 +12844,8 @@ }, "node_modules/extract-zip/node_modules/get-stream": { "version": "5.2.0", + "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-5.2.0.tgz", + "integrity": "sha512-nBF+F1rAZVCu/p7rjzgA+Yb4lfYXrpl7a6VmJrU8wF9I1CKvP/QwPNZHnOlwbTkY6dvtFIzFMSyQXbLoTQPRpA==", "dev": true, "license": "MIT", "dependencies": { @@ -12866,6 +12918,8 @@ }, "node_modules/fast-safe-stringify": { "version": "2.1.1", + "resolved": "https://registry.npmjs.org/fast-safe-stringify/-/fast-safe-stringify-2.1.1.tgz", + "integrity": "sha512-W+KJc2dmILlPplD/H4K9l9LcAHAfPtP6BY84uVLXQ6Evcz9Lcg33Y2z1IVblT6xdY54PXYVHEv+0Wpq8Io6zkA==", "dev": true, "license": "MIT" }, @@ -13669,6 +13723,8 @@ }, "node_modules/get-uri": { "version": "6.0.4", + "resolved": "https://registry.npmjs.org/get-uri/-/get-uri-6.0.4.tgz", + "integrity": "sha512-E1b1lFFLvLgak2whF2xDBcOy6NLVGZBqqjJjsIhvopKfWWEi64pLVTWWehV8KlLerZkfNTA95sTe2OdJKm1OzQ==", "dev": true, "license": "MIT", "dependencies": { @@ -15063,6 +15119,8 @@ }, "node_modules/htmlparser2": { "version": "7.2.0", + "resolved": "https://registry.npmjs.org/htmlparser2/-/htmlparser2-7.2.0.tgz", + "integrity": "sha512-H7MImA4MS6cw7nbyURtLPO1Tms7C5H602LRETv95z1MxO/7CP7rDVROehUYeYBUYEON94NXXDEPmZuq+hX4sog==", "dev": true, "funding": [ "https://github.com/fb55/htmlparser2?sponsor=1", @@ -15081,6 +15139,8 @@ }, "node_modules/htmlparser2/node_modules/entities": { "version": "3.0.1", + "resolved": "https://registry.npmjs.org/entities/-/entities-3.0.1.tgz", + "integrity": "sha512-WiyBqoomrwMdFG1e0kqvASYfnlb0lp8M5o5Fw2OFq1hNZxxcNk8Ik0Xm7LxzBhuidnZB/UtBqVCgUz3kBOP51Q==", "dev": true, "license": "BSD-2-Clause", "engines": { @@ -15833,6 +15893,8 @@ }, "node_modules/ip-address": { "version": "9.0.5", + "resolved": "https://registry.npmjs.org/ip-address/-/ip-address-9.0.5.tgz", + "integrity": "sha512-zHtQzGojZXTwZTHQqra+ETKd4Sn3vgi7uBmlPoXVWZqYvuKmtI0l/VZTjqGmJY9x88GGOaZ9+G9ES8hC4T4X8g==", "dev": true, "license": "MIT", "dependencies": { @@ -15845,6 +15907,8 @@ }, "node_modules/ip-address/node_modules/sprintf-js": { "version": "1.1.3", + "resolved": "https://registry.npmjs.org/sprintf-js/-/sprintf-js-1.1.3.tgz", + "integrity": "sha512-Oo+0REFV59/rz3gfJNKQiBlwfHaSESl1pcGyABQsnnIfWOFt6JNj5gCog2U6MLZ//IGYD+nA8nI+mTShREReaA==", "dev": true, "license": "BSD-3-Clause" }, @@ -16206,6 +16270,8 @@ }, "node_modules/is-json": { "version": "2.0.1", + "resolved": "https://registry.npmjs.org/is-json/-/is-json-2.0.1.tgz", + "integrity": "sha512-6BEnpVn1rcf3ngfmViLM6vjUjGErbdrL4rwlv+u1NO1XO8kqT4YGL8+19Q+Z/bas8tY90BTWMk2+fW1g6hQjbA==", "dev": true, "license": "ISC" }, @@ -18228,11 +18294,15 @@ }, "node_modules/jsbn": { "version": "1.1.0", + "resolved": "https://registry.npmjs.org/jsbn/-/jsbn-1.1.0.tgz", + "integrity": "sha512-4bYVV3aAMtDTTu4+xsDYa6sy9GyJ69/amsu9sYF2zqjiEoZA5xJi3BrfX3uY+/IekIu7MwdObdbDWpoZdBv3/A==", "dev": true, "license": "MIT" }, "node_modules/jsdoc-type-pratt-parser": { "version": "4.1.0", + "resolved": "https://registry.npmjs.org/jsdoc-type-pratt-parser/-/jsdoc-type-pratt-parser-4.1.0.tgz", + "integrity": "sha512-Hicd6JK5Njt2QB6XYFS7ok9e37O8AYk3jTcppG4YVQnYjOemymvTcmc7OWsmq/Qqj5TdRFO5/x/tIPmBeRtGHg==", "dev": true, "license": "MIT", "engines": { @@ -18986,6 +19056,8 @@ }, "node_modules/maximatch": { "version": "0.1.0", + "resolved": "https://registry.npmjs.org/maximatch/-/maximatch-0.1.0.tgz", + "integrity": "sha512-9ORVtDUFk4u/NFfo0vG/ND/z7UQCVZBL539YW0+U1I7H1BkZwizcPx5foFv7LCPcBnm2U6RjFnQOsIvN4/Vm2A==", "dev": true, "license": "MIT", "dependencies": { @@ -19054,6 +19126,8 @@ }, "node_modules/meow": { "version": "12.1.1", + "resolved": "https://registry.npmjs.org/meow/-/meow-12.1.1.tgz", + "integrity": "sha512-BhXM0Au22RwUneMPwSCnyhTOizdWoIEPU9sp0Aqa1PnDMR5Wv2FGXYDjuzJEIX+Eo2Rb8xuYe5jrnm5QowQFkw==", "dev": true, "license": "MIT", "engines": { @@ -19151,6 +19225,8 @@ }, "node_modules/mime": { "version": "3.0.0", + "resolved": "https://registry.npmjs.org/mime/-/mime-3.0.0.tgz", + "integrity": "sha512-jSCU7/VB1loIWBZe14aEYHU/+1UMEHoaO7qxCOVJOw9GgH72VAWppxNcjU+x9a2k3GSIBXNKxXQFqRvvZ7vr3A==", "dev": true, "license": "MIT", "bin": { @@ -19162,6 +19238,8 @@ }, "node_modules/mime-db": { "version": "1.54.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.54.0.tgz", + "integrity": "sha512-aU5EJuIN2WDemCcAp2vFBfp/m4EAhWJnUNSSw0ixs7/kXbd6Pg64EmwJkNdFhB8aWt1sH2CTXrLxo/iAGV3oPQ==", "dev": true, "license": "MIT", "engines": { @@ -19278,6 +19356,8 @@ }, "node_modules/morphdom": { "version": "2.7.5", + "resolved": "https://registry.npmjs.org/morphdom/-/morphdom-2.7.5.tgz", + "integrity": "sha512-z6bfWFMra7kBqDjQGHud1LSXtq5JJC060viEkQFMBX6baIecpkNr2Ywrn2OQfWP3rXiNFQRPoFjD8/TvJcWcDg==", "dev": true, "license": "MIT" }, @@ -19451,6 +19531,8 @@ }, "node_modules/netmask": { "version": "2.0.2", + "resolved": "https://registry.npmjs.org/netmask/-/netmask-2.0.2.tgz", + "integrity": "sha512-dBpDMdxv9Irdq66304OLfEmQ9tbNRFnFTuZiLo+bD+r332bBmMJ8GBLXklIXXgxd3+v9+KUnZaUR5PJMa75Gsg==", "dev": true, "license": "MIT", "engines": { @@ -23082,6 +23164,8 @@ }, "node_modules/pac-proxy-agent": { "version": "7.2.0", + "resolved": "https://registry.npmjs.org/pac-proxy-agent/-/pac-proxy-agent-7.2.0.tgz", + "integrity": "sha512-TEB8ESquiLMc0lV8vcd5Ql/JAKAoyzHFXaStwjkzpOpC5Yv+pIzLfHvjTSdf3vpa2bMiUQrg9i6276yn8666aA==", "dev": true, "license": "MIT", "dependencies": { @@ -23100,6 +23184,8 @@ }, "node_modules/pac-proxy-agent/node_modules/agent-base": { "version": "7.1.3", + "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-7.1.3.tgz", + "integrity": "sha512-jRR5wdylq8CkOe6hei19GGZnxM6rBGwFl3Bg0YItGDimvjGtAvdZk4Pu6Cl4u4Igsws4a1fd1Vq3ezrhn4KmFw==", "dev": true, "license": "MIT", "engines": { @@ -23108,6 +23194,8 @@ }, "node_modules/pac-proxy-agent/node_modules/http-proxy-agent": { "version": "7.0.2", + "resolved": "https://registry.npmjs.org/http-proxy-agent/-/http-proxy-agent-7.0.2.tgz", + "integrity": "sha512-T1gkAiYYDWYx3V5Bmyu7HcfcvL7mUrTWiM6yOfa3PIphViJ/gFPbvidQ+veqSOHci/PxBcDabeUNCzpOODJZig==", "dev": true, "license": "MIT", "dependencies": { @@ -23120,6 +23208,8 @@ }, "node_modules/pac-proxy-agent/node_modules/https-proxy-agent": { "version": "7.0.6", + "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-7.0.6.tgz", + "integrity": "sha512-vK9P5/iUfdl95AI+JVyUuIcVtd4ofvtrOr3HNtM2yxC9bnMbEdp3x01OhQNnjb8IJYi38VlTE3mBXwcfvywuSw==", "dev": true, "license": "MIT", "dependencies": { @@ -23132,6 +23222,8 @@ }, "node_modules/pac-resolver": { "version": "7.0.1", + "resolved": "https://registry.npmjs.org/pac-resolver/-/pac-resolver-7.0.1.tgz", + "integrity": "sha512-5NPgf87AT2STgwa2ntRMr45jTKrYBGkVU36yT0ig/n/GMAa3oPqhZfIQ2kMEimReg0+t9kZViDVZ83qfVUlckg==", "dev": true, "license": "MIT", "dependencies": { @@ -23637,6 +23729,8 @@ }, "node_modules/please-upgrade-node": { "version": "3.2.0", + "resolved": "https://registry.npmjs.org/please-upgrade-node/-/please-upgrade-node-3.2.0.tgz", + "integrity": "sha512-gQR3WpIgNIKwBMVLkpMUeR3e1/E1y42bqDQZfql+kDeXd8COYfM8PQA4X6y7a8u9Ua9FHmsrrmirW2vHs45hWg==", "dev": true, "license": "MIT", "dependencies": { @@ -23728,6 +23822,8 @@ }, "node_modules/postcss-calc": { "version": "10.1.1", + "resolved": "https://registry.npmjs.org/postcss-calc/-/postcss-calc-10.1.1.tgz", + "integrity": "sha512-NYEsLHh8DgG/PRH2+G9BTuUdtf9ViS+vdoQ0YA5OQdGsfN4ztiwtDWNtBl9EKeqNMFnIu8IKZ0cLxEQ5r5KVMw==", "dev": true, "license": "MIT", "dependencies": { @@ -24274,6 +24370,8 @@ }, "node_modules/posthtml": { "version": "0.16.6", + "resolved": "https://registry.npmjs.org/posthtml/-/posthtml-0.16.6.tgz", + "integrity": "sha512-JcEmHlyLK/o0uGAlj65vgg+7LIms0xKXe60lcDOTU7oVX/3LuEuLwrQpW3VJ7de5TaFKiW4kWkaIpJL42FEgxQ==", "dev": true, "license": "MIT", "dependencies": { @@ -24286,6 +24384,8 @@ }, "node_modules/posthtml-match-helper": { "version": "2.0.3", + "resolved": "https://registry.npmjs.org/posthtml-match-helper/-/posthtml-match-helper-2.0.3.tgz", + "integrity": "sha512-p9oJgTdMF2dyd7WE54QI1LvpBIkNkbSiiECKezNnDVYhGhD1AaOnAkw0Uh0y5TW+OHO8iBdSqnd8Wkpb6iUqmw==", "dev": true, "license": "MIT", "engines": { @@ -24297,6 +24397,8 @@ }, "node_modules/posthtml-parser": { "version": "0.11.0", + "resolved": "https://registry.npmjs.org/posthtml-parser/-/posthtml-parser-0.11.0.tgz", + "integrity": "sha512-QecJtfLekJbWVo/dMAA+OSwY79wpRmbqS5TeXvXSX+f0c6pW4/SE6inzZ2qkU7oAMCPqIDkZDvd/bQsSFUnKyw==", "dev": true, "license": "MIT", "dependencies": { @@ -24308,6 +24410,8 @@ }, "node_modules/posthtml-render": { "version": "3.0.0", + "resolved": "https://registry.npmjs.org/posthtml-render/-/posthtml-render-3.0.0.tgz", + "integrity": "sha512-z+16RoxK3fUPgwaIgH9NGnK1HKY9XIDpydky5eQGgAFVXTCSezalv9U2jQuNV+Z9qV1fDWNzldcw4eK0SSbqKA==", "dev": true, "license": "MIT", "dependencies": { @@ -24446,6 +24550,8 @@ }, "node_modules/proxy-agent": { "version": "6.5.0", + "resolved": "https://registry.npmjs.org/proxy-agent/-/proxy-agent-6.5.0.tgz", + "integrity": "sha512-TmatMXdr2KlRiA2CyDu8GqR8EjahTG3aY3nXjdzFyoZbmB8hrBsTyMezhULIXKnC0jpfjlmiZ3+EaCzoInSu/A==", "dev": true, "license": "MIT", "dependencies": { @@ -24464,6 +24570,8 @@ }, "node_modules/proxy-agent/node_modules/agent-base": { "version": "7.1.3", + "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-7.1.3.tgz", + "integrity": "sha512-jRR5wdylq8CkOe6hei19GGZnxM6rBGwFl3Bg0YItGDimvjGtAvdZk4Pu6Cl4u4Igsws4a1fd1Vq3ezrhn4KmFw==", "dev": true, "license": "MIT", "engines": { @@ -24472,6 +24580,8 @@ }, "node_modules/proxy-agent/node_modules/http-proxy-agent": { "version": "7.0.2", + "resolved": "https://registry.npmjs.org/http-proxy-agent/-/http-proxy-agent-7.0.2.tgz", + "integrity": "sha512-T1gkAiYYDWYx3V5Bmyu7HcfcvL7mUrTWiM6yOfa3PIphViJ/gFPbvidQ+veqSOHci/PxBcDabeUNCzpOODJZig==", "dev": true, "license": "MIT", "dependencies": { @@ -24484,6 +24594,8 @@ }, "node_modules/proxy-agent/node_modules/https-proxy-agent": { "version": "7.0.6", + "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-7.0.6.tgz", + "integrity": "sha512-vK9P5/iUfdl95AI+JVyUuIcVtd4ofvtrOr3HNtM2yxC9bnMbEdp3x01OhQNnjb8IJYi38VlTE3mBXwcfvywuSw==", "dev": true, "license": "MIT", "dependencies": { @@ -24496,6 +24608,8 @@ }, "node_modules/proxy-agent/node_modules/lru-cache": { "version": "7.18.3", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-7.18.3.tgz", + "integrity": "sha512-jumlc0BIUrS3qJGgIkWZsyfAM7NCWiBcCDhnd+3NNM5KbBmLTgHVfWBcg6W+rLUsIpzpERPsvwUP7CckAQSOoA==", "dev": true, "license": "ISC", "engines": { @@ -24560,6 +24674,8 @@ }, "node_modules/puppeteer": { "version": "24.9.0", + "resolved": "https://registry.npmjs.org/puppeteer/-/puppeteer-24.9.0.tgz", + "integrity": "sha512-L0pOtALIx8rgDt24Y+COm8X52v78gNtBOW6EmUcEPci0TYD72SAuaXKqasRIx4JXxmg2Tkw5ySKcpPOwN8xXnQ==", "dev": true, "hasInstallScript": true, "license": "Apache-2.0", @@ -24580,6 +24696,8 @@ }, "node_modules/puppeteer-core": { "version": "24.9.0", + "resolved": "https://registry.npmjs.org/puppeteer-core/-/puppeteer-core-24.9.0.tgz", + "integrity": "sha512-HFdCeH/wx6QPz8EncafbCqJBqaCG1ENW75xg3cLFMRUoqZDgByT6HSueiumetT2uClZxwqj0qS4qMVZwLHRHHw==", "dev": true, "license": "Apache-2.0", "dependencies": { @@ -26249,6 +26367,8 @@ }, "node_modules/semantic-release/node_modules/marked": { "version": "15.0.12", + "resolved": "https://registry.npmjs.org/marked/-/marked-15.0.12.tgz", + "integrity": "sha512-8dD6FusOQSrpv9Z1rdNMdlSgQOIP880DHqnohobOmYLElGEqAL/JvxvuxZO16r4HtjTlfPRDC1hbvxC9dPN2nA==", "dev": true, "license": "MIT", "bin": { @@ -26351,6 +26471,8 @@ }, "node_modules/semver-compare": { "version": "1.0.0", + "resolved": "https://registry.npmjs.org/semver-compare/-/semver-compare-1.0.0.tgz", + "integrity": "sha512-YM3/ITh2MJ5MtzaM429anh+x2jiLVjqILF4m4oyQB18W7Ggea7BfqdH/wGMK7dDiMghv/6WG7znWMwUDzJiXow==", "dev": true, "license": "MIT" }, @@ -26411,6 +26533,8 @@ }, "node_modules/send": { "version": "1.2.0", + "resolved": "https://registry.npmjs.org/send/-/send-1.2.0.tgz", + "integrity": "sha512-uaW0WwXKpL9blXE2o0bRhoL2EGXIrZxQ2ZQ4mgcfoBxdFmQold+qWsD2jLrfZ0trjKL6vOw0j//eAwcALFjKSw==", "dev": true, "license": "MIT", "dependencies": { @@ -26432,6 +26556,8 @@ }, "node_modules/send/node_modules/fresh": { "version": "2.0.0", + "resolved": "https://registry.npmjs.org/fresh/-/fresh-2.0.0.tgz", + "integrity": "sha512-Rx/WycZ60HOaqLKAi6cHRKKI7zxWbJ31MhntmtwMoaTeF7XFH9hhBp8vITaMidfljRQ6eYWCKkaTK+ykVJHP2A==", "dev": true, "license": "MIT", "engines": { @@ -26440,6 +26566,8 @@ }, "node_modules/send/node_modules/mime-types": { "version": "3.0.1", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-3.0.1.tgz", + "integrity": "sha512-xRc4oEhT6eaBpU1XF7AjpOFD+xQmXNB5OVKwp4tqCuBpHLS/ZbBDrc07mYTDqVMg6PfxUjjNp85O6Cd2Z/5HWA==", "dev": true, "license": "MIT", "dependencies": { @@ -26933,6 +27061,8 @@ }, "node_modules/smart-buffer": { "version": "4.2.0", + "resolved": "https://registry.npmjs.org/smart-buffer/-/smart-buffer-4.2.0.tgz", + "integrity": "sha512-94hK0Hh8rPqQl2xXc3HsaBoOXKV20MToPkcXvwbISWLEs+64sBq5kFgn2kJDHb1Pry9yrP0dxrCI9RRci7RXKg==", "dev": true, "license": "MIT", "engines": { @@ -27083,6 +27213,8 @@ }, "node_modules/socks": { "version": "2.8.4", + "resolved": "https://registry.npmjs.org/socks/-/socks-2.8.4.tgz", + "integrity": "sha512-D3YaD0aRxR3mEcqnidIs7ReYJFVzWdd6fXJYUM8ixcQcJRGTka/b3saV0KflYhyVJXKhb947GndU35SxYNResQ==", "dev": true, "license": "MIT", "dependencies": { @@ -27096,6 +27228,8 @@ }, "node_modules/socks-proxy-agent": { "version": "8.0.5", + "resolved": "https://registry.npmjs.org/socks-proxy-agent/-/socks-proxy-agent-8.0.5.tgz", + "integrity": "sha512-HehCEsotFqbPW9sJ8WVYB6UbmIMv7kUUORIF2Nncq4VQvBfNBLibW9YZR5dlYCSUhwcD628pRllm7n+E+YTzJw==", "dev": true, "license": "MIT", "dependencies": { @@ -27109,6 +27243,8 @@ }, "node_modules/socks-proxy-agent/node_modules/agent-base": { "version": "7.1.3", + "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-7.1.3.tgz", + "integrity": "sha512-jRR5wdylq8CkOe6hei19GGZnxM6rBGwFl3Bg0YItGDimvjGtAvdZk4Pu6Cl4u4Igsws4a1fd1Vq3ezrhn4KmFw==", "dev": true, "license": "MIT", "engines": { @@ -27240,6 +27376,8 @@ }, "node_modules/split2": { "version": "4.2.0", + "resolved": "https://registry.npmjs.org/split2/-/split2-4.2.0.tgz", + "integrity": "sha512-UcjcJOWknrNkF6PLX83qcHM6KHgVKNkV62Y8a5uYDVv9ydGQVwAHMKqHdJje1VTWpljG0WYpCDhrCdAOYH4TWg==", "dev": true, "license": "ISC", "engines": { @@ -27253,6 +27391,8 @@ }, "node_modules/ssri": { "version": "11.0.0", + "resolved": "https://registry.npmjs.org/ssri/-/ssri-11.0.0.tgz", + "integrity": "sha512-aZpUoMN/Jj2MqA4vMCeiKGnc/8SuSyHbGSBdgFbZxP8OJGF/lFkIuElzPxsN0q8TQQ+prw3P4EDfB3TBHHgfXw==", "dev": true, "license": "ISC", "dependencies": { @@ -28095,6 +28235,8 @@ }, "node_modules/superagent/node_modules/mime": { "version": "2.6.0", + "resolved": "https://registry.npmjs.org/mime/-/mime-2.6.0.tgz", + "integrity": "sha512-USPkMeET31rOMiarsBNIHZKLGgvKc/LrjofAnBlOttf5ajRvqiRA8QsenbcooctK6d6Ts6aqZXBA+XbkKthiQg==", "dev": true, "license": "MIT", "bin": { @@ -28284,6 +28426,8 @@ }, "node_modules/tar-fs": { "version": "3.0.8", + "resolved": "https://registry.npmjs.org/tar-fs/-/tar-fs-3.0.8.tgz", + "integrity": "sha512-ZoROL70jptorGAlgAYiLoBLItEKw/fUxg9BSYK/dF/GAGYFJOJJJMvjPAKDJraCXFwadD456FCuvLWgfhMsPwg==", "dev": true, "license": "MIT", "dependencies": { @@ -29307,6 +29451,8 @@ }, "node_modules/urlpattern-polyfill": { "version": "10.1.0", + "resolved": "https://registry.npmjs.org/urlpattern-polyfill/-/urlpattern-polyfill-10.1.0.tgz", + "integrity": "sha512-IGjKp/o0NL3Bso1PymYURCJxMPNAf/ILOpendP9f5B6e1rTJgdgiOvgfoT8VxCAdY+Wisb9uhGaJJf3yZ2V9nw==", "dev": true, "license": "MIT" }, @@ -30021,6 +30167,8 @@ }, "node_modules/zod": { "version": "3.25.7", + "resolved": "https://registry.npmjs.org/zod/-/zod-3.25.7.tgz", + "integrity": "sha512-YGdT1cVRmKkOg6Sq7vY7IkxdphySKnXhaUmFI4r4FcuFVNgpCb9tZfNwXbT6BPjD5oz0nubFsoo9pIqKrDcCvg==", "dev": true, "license": "MIT", "funding": { From de063c8167f6547edd92e4cff7f653493c38d7a5 Mon Sep 17 00:00:00 2001 From: helennickols Date: Tue, 13 May 2025 09:54:54 +0100 Subject: [PATCH 03/18] docs: content updates --- docs/contribute.md | 8 -------- 1 file changed, 8 deletions(-) diff --git a/docs/contribute.md b/docs/contribute.md index 406cf1a35..c08a885c0 100644 --- a/docs/contribute.md +++ b/docs/contribute.md @@ -34,14 +34,6 @@ The component will be given an 'experimental' status. This is to show that it's [Add a component to the MoJ Design System](/contribute/add-new-component/start) -## Adding a pattern or page - -
-Later in 2025 you'll also be able to add a pattern or page. -
- -These building blocks will also have an experimental status. - ## Joining discussions After an experimental building block is live, contributions can be added to a Github discussion thread. These might include feedback, images, user research findings or accessibility insights. From 1113f8ff6eb1d22713a6caf0b4ee2cc64ee7df3b Mon Sep 17 00:00:00 2001 From: Chris Pymm Date: Thu, 29 May 2025 16:01:41 +0100 Subject: [PATCH 04/18] docs(contributions): use 11ty generated nav in community space (#1504) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * docs(contributions): use 11ty generated nav in community space * docs(contributions): updates to allow docs site and contributions app… (#1505) * docs(contributions): updates to allow docs site and contributions app to share a base layout 11ty copies across the base.njk layout along with the header and footer partials Also update the generation of the side nav to be a njk file and place in views/common/partials/side-navigation.njk in order for the base layout to have an include that works both for the docs site and the contributions app * docs: expose staging env to templates --- app.js | 2 ++ 1 file changed, 2 insertions(+) diff --git a/app.js b/app.js index 7d1980a5c..9a2b076c8 100644 --- a/app.js +++ b/app.js @@ -22,6 +22,8 @@ const ApplicationError = require('./helpers/application-error') const rev = require('./filters/rev') +const rev = require('./filters/rev') + const { APP_PORT, ENV, REDIS_URL, SESSION_SECRET } = require('./config') const addComponentRoutes = require('./routes/add-component') From 1a7a28501a6d5f91ace0b1ab60c65e25107ff107 Mon Sep 17 00:00:00 2001 From: Chris Pymm Date: Tue, 3 Jun 2025 10:44:35 +0100 Subject: [PATCH 05/18] docs(contributions): clear data when changing a yes to a no (#1508) * docs(contributions): clear data when changing a yes to a no * Ensure dependent questions are answered (#1509) * docs(contributions): ensure dependent questions are answered * docs(contributions): rework data removal and routing after cya amendments --- config.js | 1 + middleware/component-session.js | 7 +++++++ 2 files changed, 8 insertions(+) diff --git a/config.js b/config.js index 4fb6ee709..dddba5ed5 100644 --- a/config.js +++ b/config.js @@ -234,4 +234,5 @@ const config = { } } + module.exports = config diff --git a/middleware/component-session.js b/middleware/component-session.js index 8ba125a2d..268763fb2 100644 --- a/middleware/component-session.js +++ b/middleware/component-session.js @@ -45,6 +45,12 @@ const transformErrorsToErrorList = (errors) => { })) } +const setCurrentFormPages = (req, res, next) => { + const { url, session } = req + req.formPages = getCurrentFormPages(url, session) + next() +} + const setNextPage = (req, res, next) => { console.log('setting nextPage') const amendingAnswers = req?.session?.checkYourAnswers @@ -378,6 +384,7 @@ const saveFileToRedis = async (req, res, next) => { module.exports = { setNextPage, + setCurrentFormPages, validateFormData, saveSession, getFormDataFromSession, From afbb3c6130527cd24c32f2c53c0f6ec8bad97a7d Mon Sep 17 00:00:00 2001 From: Chris Pymm Date: Tue, 24 Jun 2025 10:07:44 +0100 Subject: [PATCH 06/18] docs(contributions): CYA page updates (#1545) * docs: swicth CYA to use summary cards * docs: updated cya page structure * docs: cya tweaks * docs: allow overrides of cya section titles as well as field labels * docs: handle empty sections and fix change/remove links * docs: updates to the remove page --- app.js | 2 +- config.js | 1 - 2 files changed, 1 insertion(+), 2 deletions(-) diff --git a/app.js b/app.js index 9a2b076c8..193e6735a 100644 --- a/app.js +++ b/app.js @@ -89,7 +89,7 @@ app.set('view engine', 'njk') const njk = expressNunjucks(app, { watch: isDev, noCache: false, - loader: nunjucks.FileSystemLoader, + loader: nunjucks.FileSystemLoader }) njk.env.addFilter('rev', rev) diff --git a/config.js b/config.js index dddba5ed5..4fb6ee709 100644 --- a/config.js +++ b/config.js @@ -234,5 +234,4 @@ const config = { } } - module.exports = config From b5d72a6c14f9379ca01af999e44a245eb8f4ee09 Mon Sep 17 00:00:00 2001 From: Chris Pymm Date: Thu, 26 Jun 2025 12:19:11 +0100 Subject: [PATCH 07/18] docs: wip --- config.js | 10 +++++ routes/add-component.js | 33 +++++++++++++++- schema/email.schema.js | 15 ++++++++ views/community/pages/check-your-email.njk | 17 +++++++++ views/community/pages/email.njk | 44 ++++++++++++++++++++++ 5 files changed, 118 insertions(+), 1 deletion(-) create mode 100644 schema/email.schema.js create mode 100644 views/community/pages/check-your-email.njk create mode 100644 views/community/pages/email.njk diff --git a/config.js b/config.js index 4fb6ee709..dde378325 100644 --- a/config.js +++ b/config.js @@ -18,6 +18,16 @@ const config = { SESSION_SECRET: process.env.SESSION_SECRET || 'your-secret-key', ENV: process.env.ENV || 'development', COMPONENT_FORM_PAGES: { + 'email': { + title: 'Verify your email address', + fields: { + emailAddress: { + label: 'Email address' + } + }, + showOnCya: false, + removable: false + }, 'component-details': { title: 'Component details', fields: { diff --git a/routes/add-component.js b/routes/add-component.js index 9b01e4a82..3efe2c93d 100644 --- a/routes/add-component.js +++ b/routes/add-component.js @@ -147,7 +147,38 @@ router.get('/confirmation', (req, res) => { router.all('*', sessionStarted) // Check that we have a session in progress router.post('/start', verifyCsrf, (req, res) => { - res.redirect('/contribute/add-new-component/component-details') + res.redirect('/contribute/add-new-component/email') +}) + +router.get('/email', (req,res) => { + res.render('email', { + page: COMPONENT_FORM_PAGES.email, + csrfToken: req?.session?.csrfToken + }) +}) + +router.post('/email', xss(), + verifyCsrf, + validatePageParams, + validateFormData, + saveSession, + (req, res, next) => { + res.redirect('/contribute/add-new-component/check-your-email') + }) + +router.get('/check-your-email', (req,res) => { + console.log(req.session) + res.render('check-your-email', { + page: { + title: 'Check your email', + email: req?.session?.['/email']?.emailAddress + }, + csrfToken: req?.session?.csrfToken + }) +}) + +router.get('/email/verify/:token', (req,res) => { + }) // Remove form page diff --git a/schema/email.schema.js b/schema/email.schema.js new file mode 100644 index 000000000..79a9bdb78 --- /dev/null +++ b/schema/email.schema.js @@ -0,0 +1,15 @@ +const Joi = require('joi') + +const schema = Joi.object({ + emailAddress: Joi.string() + .email({ tlds: { allow: false } }) + .required() + .messages({ + 'string.email': + 'Enter an email address in the correct format, like name@example.com', + 'string.empty': 'Enter your email address', + 'any.required': 'Enter your email address' + }) +}) + +module.exports = schema diff --git a/views/community/pages/check-your-email.njk b/views/community/pages/check-your-email.njk new file mode 100644 index 000000000..d8ea98ba5 --- /dev/null +++ b/views/community/pages/check-your-email.njk @@ -0,0 +1,17 @@ +{% from "govuk/components/back-link/macro.njk" import govukBackLink %} +{% extends "base.njk" %} +{% block content %} +
+ {% if backLink %} + {{ govukBackLink({ + text: "Back", + href: backLink + }) }} + {% endif %} + Submit a component +

{{ page.title }}

+

For security, we need to check if you have access to {{page.email}}.

+

We’ve sent you a link that you can use to continue submitting a component. The link will open in a new browser window, so you can close this one

+

Not received an email?

+
+{% endblock %} diff --git a/views/community/pages/email.njk b/views/community/pages/email.njk new file mode 100644 index 000000000..94d89f66e --- /dev/null +++ b/views/community/pages/email.njk @@ -0,0 +1,44 @@ +{% from "govuk/components/input/macro.njk" import govukInput %} +{% from "govuk/components/button/macro.njk" import govukButton %} +{% from "govuk/components/error-summary/macro.njk" import govukErrorSummary %} +{% from "govuk/components/back-link/macro.njk" import govukBackLink %} +{% extends "base.njk" %} +{% block content %} +
+ {% if backLink %} + {{ govukBackLink({ + text: "Back", + href: backLink + }) }} + {% endif %} + {% if errorList %} + {{ govukErrorSummary({ + titleText: "There is a problem", + errorList: errorList + }) }} + {% endif %} +
+ Submit a component +

{{page.title}}

+

At the moment, contributions to the MoJ Design System are only accepted from MoJ staff.

+

Enter your email address to receive a link to verify that you work for the MoJ before you continue.

+ {{ govukInput({ + label: { + text: page.fields.emailAddress.label, + classes: "govuk-label--m" + }, + id: "email-address", + name: "emailAddress", + errorMessage: formErrors.emailAddress, + value: formData.emailAddress + }) }} + + +
+ {{ govukButton({ + text: 'Continue' + }) }} +
+
+
+{% endblock %} From 855a2413a0605613a788c7c86a004f04ee9c3c6c Mon Sep 17 00:00:00 2001 From: Chris Pymm Date: Fri, 27 Jun 2025 10:35:05 +0100 Subject: [PATCH 08/18] docs: eip --- config.js | 1 + middleware/component-session.js | 17 ++++++++++++++++- routes/add-component.js | 19 +++++++++++++++---- 3 files changed, 32 insertions(+), 5 deletions(-) diff --git a/config.js b/config.js index dde378325..4a299c4c5 100644 --- a/config.js +++ b/config.js @@ -17,6 +17,7 @@ const config = { REDIS_PORT: parseInt(process.env.REDIS_PORT, 10) || 6379, SESSION_SECRET: process.env.SESSION_SECRET || 'your-secret-key', ENV: process.env.ENV || 'development', + ALLOWED_EMAIL_DOMAINS: ['justice.gov.uk'], COMPONENT_FORM_PAGES: { 'email': { title: 'Verify your email address', diff --git a/middleware/component-session.js b/middleware/component-session.js index 268763fb2..7ddcf4966 100644 --- a/middleware/component-session.js +++ b/middleware/component-session.js @@ -5,6 +5,7 @@ const sanitize = require('sanitize-filename') const { MAX_ADD_ANOTHER: maxAddAnother, ADD_NEW_COMPONENT_ROUTE, + ALLOWED_EMAIL_DOMAINS: allowedDomains, COMPONENT_FORM_PAGES: formPages } = require('../config') const { getAnswersForSection } = require('../helpers/check-your-answers') @@ -38,6 +39,19 @@ const getPageData = (req) => { return formPages[pageData] } +const checkEmail = (req, res, next) => { + let allowed = false + const email = req?.session?.['/email']?.emailAddress + if (email) { + const domain = email.split('@').at(-1) + if (allowedDomains.includes(domain)) { + allowed = true + } + } + req.emailAllowed = allowed + next() +} + const transformErrorsToErrorList = (errors) => { return errors.map((error) => ({ text: error.message, @@ -397,5 +411,6 @@ module.exports = { validateFormDataFileUpload, validateComponentImagePage, saveFileToRedis, - clearSkippedPageData + clearSkippedPageData, + checkEmail } diff --git a/routes/add-component.js b/routes/add-component.js index 3efe2c93d..4ddaa3742 100644 --- a/routes/add-component.js +++ b/routes/add-component.js @@ -144,6 +144,15 @@ router.get('/confirmation', (req, res) => { }) }) +router.get('/email/verify/:token', + // we may not have a session + // if no session - token expored + // if session check token + // + (req,res) => { + +}) + router.all('*', sessionStarted) // Check that we have a session in progress router.post('/start', verifyCsrf, (req, res) => { @@ -159,9 +168,13 @@ router.get('/email', (req,res) => { router.post('/email', xss(), verifyCsrf, - validatePageParams, validateFormData, - saveSession, + // check email domain + // if valid + saveSession, + // send email + // else + // redirect (req, res, next) => { res.redirect('/contribute/add-new-component/check-your-email') }) @@ -177,9 +190,7 @@ router.get('/check-your-email', (req,res) => { }) }) -router.get('/email/verify/:token', (req,res) => { -}) // Remove form page router.get( From 1f1b7212b6b0a269eccf6f1fc1f2db5e7ba7ac3b Mon Sep 17 00:00:00 2001 From: Chris Pymm Date: Mon, 30 Jun 2025 23:10:03 +0100 Subject: [PATCH 09/18] docs: email verifiaction feature --- app.js | 2 +- config.js | 7 +- middleware/component-session.js | 23 ++- middleware/notify-email.js | 59 ++++---- routes/add-component.js | 152 +++++++++++++++----- schema/email.schema.js | 2 +- views/community/pages/check-your-email.njk | 17 --- views/community/pages/component-details.njk | 10 ++ views/community/pages/email-check.njk | 20 +++ views/community/pages/email-not-allowed.njk | 16 +++ views/community/pages/email-resend.njk | 35 +++++ views/community/pages/email.njk | 32 +++-- 12 files changed, 284 insertions(+), 91 deletions(-) delete mode 100644 views/community/pages/check-your-email.njk create mode 100644 views/community/pages/email-check.njk create mode 100644 views/community/pages/email-not-allowed.njk create mode 100644 views/community/pages/email-resend.njk diff --git a/app.js b/app.js index 193e6735a..28fd6c89b 100644 --- a/app.js +++ b/app.js @@ -57,7 +57,7 @@ const sessionOptions = { secret: SESSION_SECRET, resave: true, saveUninitialized: true, - cookie: { secure: !isDev, maxAge: 24 * 60 * 60 * 1000 } + cookie: { secure: !isDev, maxAge: 60 * 1000 * 3 /* 24 * 60 * 60 * 1000 */ } } if (REDIS_URL) { diff --git a/config.js b/config.js index 4a299c4c5..51dbacd17 100644 --- a/config.js +++ b/config.js @@ -5,12 +5,14 @@ const config = { GITHUB_REPO_NAME: process.env.GITHUB_REPO_NAME || 'your-default-repo-name', NOTIFY_PR_TEMPLATE: process.env.NOTIFY_PR_TEMPLATE || '', NOTIFY_SUBMISSION_TEMPLATE: process.env.NOTIFY_SUBMISSION_TEMPLATE || '', + NOTIFY_VERIFICATION_TEMPLATE: process.env.NOTIFY_VERIFICATION_TEMPLATE || '', NOTIFY_EMAIL: process.env.NOTIFY_EMAIL || 'your-email', NOTIFY_TOKEN: process.env.NOTIFY_TOKEN || 'your-default-repo-token', NOTIFY_EMAIL_RETRY_MS: parseInt(process.env.NOTIFY_EMAIL_RETRY_MS, 10) || 5000, NOTIFY_EMAIL_MAX_RETRIES: parseInt(process.env.NOTIFY_EMAIL_MAX_RETRIES, 10) || 5, + APP_URL: process.env.APP_URL, APP_PORT: parseInt(process.env.APP_PORT, 10) || 3001, REDIS_URL: process.env.REDIS_URL, REDIS_AUTH_TOKEN: process.env.REDIS_AUTH_TOKEN, @@ -20,10 +22,11 @@ const config = { ALLOWED_EMAIL_DOMAINS: ['justice.gov.uk'], COMPONENT_FORM_PAGES: { 'email': { - title: 'Verify your email address', + title: 'Enter your email address', fields: { emailAddress: { - label: 'Email address' + label: 'Email address', + hint: 'Enter an email address ending @justice.gov.uk' } }, showOnCya: false, diff --git a/middleware/component-session.js b/middleware/component-session.js index 7ddcf4966..a85499762 100644 --- a/middleware/component-session.js +++ b/middleware/component-session.js @@ -39,16 +39,19 @@ const getPageData = (req) => { return formPages[pageData] } -const checkEmail = (req, res, next) => { +const checkEmailDomain = (req, res, next) => { let allowed = false - const email = req?.session?.['/email']?.emailAddress + const email = req?.body?.emailAddress + console.log({email}) if (email) { const domain = email.split('@').at(-1) + console.log({domain}) if (allowedDomains.includes(domain)) { allowed = true } } - req.emailAllowed = allowed + console.log({allowed}) + req.emailDomainAllowed = allowed next() } @@ -100,7 +103,7 @@ const clearSkippedPageData = (req, res, next) => { // Delete page and subpage data for (const sessionPage of Object.keys(req.session)) { if ( - !['started', 'cookie', 'csrfToken', 'checkYourAnswers'].includes( + !['started', 'cookie', 'csrfToken', 'checkYourAnswers', 'emailToken','emailDomainAllowed','verified'].includes( sessionPage ) ) { @@ -210,6 +213,7 @@ const validateFormData = (req, res, next) => { } const saveSession = (req, res, next) => { + console.log('saving session') if (!req.session) req.session = {} const { _csrf, ...body } = req.body @@ -355,6 +359,14 @@ const sessionStarted = (req, res, next) => { next() } +const sessionVerified = (req, res, next) => { + if (!req?.session?.verified) { + console.error('No verified email for session') + return res.redirect(`${ADD_NEW_COMPONENT_ROUTE}/start`) + } + next() +} + const validateComponentImagePage = (req, res, next) => { if (req.params.page !== 'component-image') { const error = new ApplicationError('Invalid page', 400) @@ -408,9 +420,10 @@ module.exports = { getFormSummaryListForRemove, removeFromSession, sessionStarted, + sessionVerified, validateFormDataFileUpload, validateComponentImagePage, saveFileToRedis, clearSkippedPageData, - checkEmail + checkEmailDomain } diff --git a/middleware/notify-email.js b/middleware/notify-email.js index e98b92978..4a9e347a8 100644 --- a/middleware/notify-email.js +++ b/middleware/notify-email.js @@ -4,9 +4,11 @@ const { NOTIFY_TOKEN, NOTIFY_PR_TEMPLATE, NOTIFY_SUBMISSION_TEMPLATE, + NOTIFY_VERIFICATION_TEMPLATE, NOTIFY_EMAIL, NOTIFY_EMAIL_RETRY_MS, - NOTIFY_EMAIL_MAX_RETRIES + NOTIFY_EMAIL_MAX_RETRIES, + APP_URL } = require('../config') const notifyClient = new NotifyClient(NOTIFY_TOKEN) @@ -14,29 +16,11 @@ const emailAddress = NOTIFY_EMAIL const sendEmail = async ( templateId, - prLink = null, - previewLink = null, - fileBuffer = null, - markdown = null, + email, + personalisation = {}, retries = NOTIFY_EMAIL_MAX_RETRIES, backoff = NOTIFY_EMAIL_RETRY_MS ) => { - const personalisation = {} - - if (prLink) { - personalisation.pr_link = prLink - } - if (previewLink) { - personalisation.preview_link = previewLink - } - - if (fileBuffer) { - personalisation.link_to_file = notifyClient.prepareUpload(fileBuffer) - } - - if (markdown) { - personalisation.markdown = markdown - } for (let attempt = 1; attempt <= retries; attempt++) { try { @@ -63,12 +47,36 @@ const sendEmail = async ( } const sendSubmissionEmail = async (fileBuffer = null, markdown = null) => { - return sendEmail(NOTIFY_SUBMISSION_TEMPLATE, null, null, fileBuffer, markdown) + const personalisation = {} + + if (fileBuffer) { + personalisation.link_to_file = notifyClient.prepareUpload(fileBuffer) + } + + if (markdown) { + personalisation.markdown = markdown + } + + return sendEmail(NOTIFY_SUBMISSION_TEMPLATE, NOTIFY_EMAIL, personalisation) } const sendPrEmail = async ({ url, number }) => { - const previewUrl = `https://moj-frontend-pr-${number}.apps.live.cloud-platform.service.justice.gov.uk` - return sendEmail(NOTIFY_PR_TEMPLATE, url, previewUrl) + const personalisation = {} + + if (url) { + personalisation.pr_link = url + } + if (number) { + personalisation.preview_link = `https://moj-frontend-pr-${number}.apps.live.cloud-platform.service.justice.gov.uk` + } + + return sendEmail(NOTIFY_PR_TEMPLATE, NOTIFY_EMAIL, personalisation) +} + +const sendVerificationEmail = async (email, token) => { + const personalisation = {} + personalisation.token_link = `${APP_URL}/contribute/add-new-component/email/verify/${token}` + return sendEmail(NOTIFY_VERIFICATION_TEMPLATE, email, personalisation) } const handleEmailError = (error) => { @@ -87,5 +95,6 @@ const handleEmailError = (error) => { module.exports = { sendSubmissionEmail, - sendPrEmail + sendPrEmail, + sendVerificationEmail } diff --git a/routes/add-component.js b/routes/add-component.js index 4ddaa3742..6bf95a7ee 100644 --- a/routes/add-component.js +++ b/routes/add-component.js @@ -21,15 +21,18 @@ const { getFormSummaryListForRemove, removeFromSession, sessionStarted, + sessionVerified, validateFormDataFileUpload, saveFileToRedis, - clearSkippedPageData + clearSkippedPageData, + checkEmailDomain } = require('../middleware/component-session') const { generateMarkdown } = require('../middleware/generate-documentation') const { pushToGitHub, createPullRequest } = require('../middleware/github-api') const { sendSubmissionEmail, - sendPrEmail + sendPrEmail, + sendVerificationEmail } = require('../middleware/notify-email') const { processSubmissionData, @@ -41,6 +44,7 @@ const upload = multer({ limits: { fileSize: 10 * 1024 * 1024 } // 10MB }) const router = express.Router() +const checkYourAnswersPath = 'check-your-answers' const validatePageParams = (req, res, next) => { const validPages = Object.keys(COMPONENT_FORM_PAGES) @@ -64,8 +68,6 @@ const validatePageParams = (req, res, next) => { } } -const checkYourAnswersPath = 'check-your-answers' - const setCsrfToken = (req, res, next) => { if (req?.session) { if (!req?.session?.csrfToken) { @@ -144,53 +146,141 @@ router.get('/confirmation', (req, res) => { }) }) -router.get('/email/verify/:token', - // we may not have a session - // if no session - token expored - // if session check token - // - (req,res) => { - +router.get('/email/verify/:token', (req, res) => { + if (!req?.session?.emailToken) { + // session expired + req.session.sessionFlash = { + type: 'error', + title: 'Email verification link expired', + text: `Enter your email address below to receive a new verification link.` + } + res.redirect(`${ADD_NEW_COMPONENT_ROUTE}/email`) + } else { + const tokenInSession = req.session.emailToken + const tokenFromParams = req.params.token + if (tokenFromParams === tokenInSession) { + // verified + req.session.verified = true + req.session.sessionFlash = { + type: 'success', + title: 'Your email address has been verified', + text: 'You can now submit a new component' + } + res.redirect(`${ADD_NEW_COMPONENT_ROUTE}/component-details`) + } else { + // token invalid + req.session.sessionFlash = { + type: 'error', + title: 'Email verification link invalid', + text: `Enter your email address below to receive a new verification link.` + } + res.redirect(`${ADD_NEW_COMPONENT_ROUTE}/email`) + } + } }) -router.all('*', sessionStarted) // Check that we have a session in progress +router.get('/email', (req, res) => { + req.session.started = true -router.post('/start', verifyCsrf, (req, res) => { - res.redirect('/contribute/add-new-component/email') -}) + if (req.query.reset === 'true') { + // eslint-disable-next-line @typescript-eslint/no-dynamic-delete + delete req.session['/email'] + // eslint-disable-next-line @typescript-eslint/no-dynamic-delete + delete req.session.emailToken + } -router.get('/email', (req,res) => { res.render('email', { page: COMPONENT_FORM_PAGES.email, + submitUrl: req.originalUrl, csrfToken: req?.session?.csrfToken }) }) -router.post('/email', xss(), +// For all following routed we must have a session in progress +router.all('*', sessionStarted) + +router.post('/start', verifyCsrf, (req, res) => { + res.redirect('/contribute/add-new-component/email') +}) + +router.post( + '/email', + xss(), verifyCsrf, validateFormData, - // check email domain - // if valid - saveSession, - // send email - // else - // redirect + checkEmailDomain, (req, res, next) => { - res.redirect('/contribute/add-new-component/check-your-email') - }) + if (req.emailDomainAllowed) { + saveSession(req, res, next) + } else { + res.redirect('/contribute/add-new-component/email/not-allowed') + } + }, + // generate token + (req, res, next) => { + req.session.emailToken = crypto.randomBytes(32).toString('hex') + next() + }, + // send email + async (req, res, next) => { + if (req.emailDomainAllowed) { + res.redirect('/contribute/add-new-component/email/check') + const token = req?.session?.emailToken + const email = req?.session?.['/email']?.emailAddress + if (token && email) { + try { + await sendVerificationEmail(email, token) + } catch (error) { + console.error('Error sending verification email') + } + } + } + } +) -router.get('/check-your-email', (req,res) => { - console.log(req.session) - res.render('check-your-email', { +router.get('/email/check', (req, res) => { + res.render('email-check', { page: { title: 'Check your email', email: req?.session?.['/email']?.emailAddress - }, - csrfToken: req?.session?.csrfToken + } + }) +}) + +router.get('/email/resend', (req, res) => { + res.render('email-resend', { + submitUrl: req.originalUrl, + csrfToken: req?.session?.csrfToken, + page: { + title: 'If you’re having problems with the email', + email: req?.session?.['/email']?.emailAddress + } }) }) +router.post('/email/resend', xss(), verifyCsrf, async (req, res, next) => { + res.redirect('/contribute/add-new-component/email/check') + const token = req?.session?.emailToken + const email = req?.session?.['/email']?.emailAddress + if (token && email) { + try { + await sendVerificationEmail(email, token) + } catch (error) { + console.error('Error sending verification email') + } + } +}) + +router.get('/email/not-allowed', (req, res) => { + res.render('email-not-allowed', { + page: { + title: 'You cannot submit a component' + } + }) +}) +// For all following routed we must have verified an email address +router.all('*', sessionVerified) // Remove form page router.get( @@ -266,8 +356,6 @@ router.get( } ) - - // "Check Your Answers" form submission router.post( `/${checkYourAnswersPath}`, diff --git a/schema/email.schema.js b/schema/email.schema.js index 79a9bdb78..46aaa0a70 100644 --- a/schema/email.schema.js +++ b/schema/email.schema.js @@ -6,7 +6,7 @@ const schema = Joi.object({ .required() .messages({ 'string.email': - 'Enter an email address in the correct format, like name@example.com', + 'Enter an email address ending @justice.gov.uk', 'string.empty': 'Enter your email address', 'any.required': 'Enter your email address' }) diff --git a/views/community/pages/check-your-email.njk b/views/community/pages/check-your-email.njk deleted file mode 100644 index d8ea98ba5..000000000 --- a/views/community/pages/check-your-email.njk +++ /dev/null @@ -1,17 +0,0 @@ -{% from "govuk/components/back-link/macro.njk" import govukBackLink %} -{% extends "base.njk" %} -{% block content %} -
- {% if backLink %} - {{ govukBackLink({ - text: "Back", - href: backLink - }) }} - {% endif %} - Submit a component -

{{ page.title }}

-

For security, we need to check if you have access to {{page.email}}.

-

We’ve sent you a link that you can use to continue submitting a component. The link will open in a new browser window, so you can close this one

-

Not received an email?

-
-{% endblock %} diff --git a/views/community/pages/component-details.njk b/views/community/pages/component-details.njk index 811ec9ee9..97293a25a 100644 --- a/views/community/pages/component-details.njk +++ b/views/community/pages/component-details.njk @@ -3,6 +3,7 @@ {% from "govuk/components/button/macro.njk" import govukButton %} {% from "govuk/components/error-summary/macro.njk" import govukErrorSummary %} {% from "govuk/components/back-link/macro.njk" import govukBackLink %} +{% from "moj/components/alert/macro.njk" import mojAlert %} {% extends "base.njk" %} {% block content %}
@@ -18,6 +19,15 @@ errorList: errorList }) }} {% endif %} + {% if sessionFlash.text %} + {{ mojAlert({ + variant: sessionFlash.type, + title: sessionFlash.title, + text: sessionFlash.text, + showTitleAsHeading: true, + role: "alert" + })}} + {% endif %}
Submit a new component

Add details about the component

diff --git a/views/community/pages/email-check.njk b/views/community/pages/email-check.njk new file mode 100644 index 000000000..3a2c0ede3 --- /dev/null +++ b/views/community/pages/email-check.njk @@ -0,0 +1,20 @@ +{% from "govuk/components/back-link/macro.njk" import govukBackLink %} +{% from "govuk/components/details/macro.njk" import govukDetails %} +{% extends "base.njk" %} +{% block content %} +
+ {% if backLink %} + {{ govukBackLink({ + text: "Back", + href: backLink + }) }} + {% endif %} + Submit a component +

{{ page.title }}

+

We've sent an email to: {{page.email}}.

+

Select the link in the email to continue submitting a component. The link will open in a new window, so you can close this one.

+ {% call govukDetails({ summaryText: "Problems with the email?" }) %} + We can send the email again or you can use a different email address. + {% endcall %} +
+{% endblock %} diff --git a/views/community/pages/email-not-allowed.njk b/views/community/pages/email-not-allowed.njk new file mode 100644 index 000000000..174c7df22 --- /dev/null +++ b/views/community/pages/email-not-allowed.njk @@ -0,0 +1,16 @@ +{% from "govuk/components/back-link/macro.njk" import govukBackLink %} +{% extends "base.njk" %} +{% block content %} +
+ {% if backLink %} + {{ govukBackLink({ + text: "Back", + href: backLink + }) }} + {% endif %} + Submit a component +

{{ page.title }}

+

You need to work at MoJ to contribute to the MoJ Design System. Contributions will be open to everyone in 2026.

+

Go back to the MoJ Design System

+
+{% endblock %} diff --git a/views/community/pages/email-resend.njk b/views/community/pages/email-resend.njk new file mode 100644 index 000000000..187f9471c --- /dev/null +++ b/views/community/pages/email-resend.njk @@ -0,0 +1,35 @@ +{% from "govuk/components/back-link/macro.njk" import govukBackLink %} +{% from "govuk/components/button/macro.njk" import govukButton %} +{% extends "base.njk" %} +{% block content %} +
+ {% if backLink %} + {{ govukBackLink({ + text: "Back", + href: backLink + }) }} + {% endif %} + + Submit a component +

{{ page.title }}

+
    +
  1. Check that you entered your email address correctly and that it ends justice.gov.uk
  2. +
  3. Check your junk or spam folder
  4. +
+

If you’ve still not received the verification email after step 2 you can either:

+
    +
  • resend the verification email
  • +
  • enter a different email address, for example if the original ended @digital.justice.gov.uk
  • +
+ + + +
+ {{ govukButton({ + text: 'Resend verification email' + }) }} +
+

If none of this works, contact us +

+
+{% endblock %} diff --git a/views/community/pages/email.njk b/views/community/pages/email.njk index 94d89f66e..cc5f7f9ba 100644 --- a/views/community/pages/email.njk +++ b/views/community/pages/email.njk @@ -2,6 +2,7 @@ {% from "govuk/components/button/macro.njk" import govukButton %} {% from "govuk/components/error-summary/macro.njk" import govukErrorSummary %} {% from "govuk/components/back-link/macro.njk" import govukBackLink %} +{% from "moj/components/alert/macro.njk" import mojAlert %} {% extends "base.njk" %} {% block content %}
@@ -17,21 +18,36 @@ errorList: errorList }) }} {% endif %} + {% if sessionFlash.text %} + {{ mojAlert({ + variant: sessionFlash.type, + title: sessionFlash.title, + text: sessionFlash.text, + showTitleAsHeading: true, + role: "alert" + })}} + {% endif %}
Submit a component -

{{page.title}}

-

At the moment, contributions to the MoJ Design System are only accepted from MoJ staff.

-

Enter your email address to receive a link to verify that you work for the MoJ before you continue.

+

{{page.title}}

+

You need to work for MoJ to submit a new component to the Design System.

{{ govukInput({ - label: { - text: page.fields.emailAddress.label, - classes: "govuk-label--m" - }, + id: "email-address", name: "emailAddress", + hint: { + text: page.fields.emailAddress.hint + }, errorMessage: formErrors.emailAddress, - value: formData.emailAddress + value: formData.emailAddress, + attributes: { + 'aria-labelledby': 'page-title' + }, + formGroup: { + classes: 'govuk-!-margin-top-0' + } }) }} +

You’ll be sent an email with a verification link.

From 54cda80e0cadd4811afe1372ce3321262e158654 Mon Sep 17 00:00:00 2001 From: Chris Pymm Date: Tue, 1 Jul 2025 11:50:19 +0100 Subject: [PATCH 10/18] docs: tweaks to email verification --- app.js | 7 +- config.js | 29 +++++++ middleware/component-session.js | 43 +++++++++-- routes/add-component.js | 101 ++++++++----------------- views/community/pages/email-resend.njk | 7 +- 5 files changed, 107 insertions(+), 80 deletions(-) diff --git a/app.js b/app.js index 28fd6c89b..e29bd076b 100644 --- a/app.js +++ b/app.js @@ -109,9 +109,10 @@ app.use(xss()) app.use('/contribute/add-new-component', addComponentRoutes) // Fallback route to 404 -app.get('*', (req, res, next) => { - const error = new ApplicationError('Page not found', 404) - next(error) +app.use((req, res) => { + res.status(404).render('404', { + title: 'Page not found' + }) }) // The error handler must be registered before any other error middleware and after all controllers diff --git a/config.js b/config.js index 51dbacd17..f68871f77 100644 --- a/config.js +++ b/config.js @@ -245,6 +245,35 @@ const config = { SHARE_YOUR_DETAILS: { addNameToComponentPage: 'add my name to the component page', addTeamNameToComponentPage: 'add my team name to the component page' + }, + MESSAGES: { + emailVerificationExpired: { + type: 'error', + title: 'Email verification link expired', + text: `Enter your email address below to receive a new verification link.` + }, + emailVerificationSuccess: { + type: 'success', + title: 'Your email address has been verified', + text: 'You can now submit a new component' + }, + emailVerificationInvalidToken: { + type: 'error', + title: 'Email verification link invalid', + text: `Enter your email address below to receive a new verification link.` + }, + componentImageUploaded: (filename) => { + return { + type: 'success', + message: `File ‘${filename}’ has been uploaded.` + } + }, + componentImageRemoved: (filename) => { + return { + type: 'success', + message: `File ‘${filename}’ has been removed.` + } + } } } diff --git a/middleware/component-session.js b/middleware/component-session.js index a85499762..08f35fd1b 100644 --- a/middleware/component-session.js +++ b/middleware/component-session.js @@ -6,7 +6,8 @@ const { MAX_ADD_ANOTHER: maxAddAnother, ADD_NEW_COMPONENT_ROUTE, ALLOWED_EMAIL_DOMAINS: allowedDomains, - COMPONENT_FORM_PAGES: formPages + COMPONENT_FORM_PAGES: formPages, + MESSAGES } = require('../config') const { getAnswersForSection } = require('../helpers/check-your-answers') const ApplicationError = require('../helpers/application-error') @@ -322,10 +323,7 @@ const removeFromSession = (req, res, next) => { const filename = req.session[url]?.componentImage?.originalname console.log(filename) if (filename) { - req.session.sessionFlash = { - type: 'success', - message: `File ‘${filename}’ has been removed.` - } + req.session.sessionFlash = MESSAGES.componentImageRemoved(filename) } } @@ -408,6 +406,37 @@ const saveFileToRedis = async (req, res, next) => { next() } +const validatePageParams = (req, res, next) => { + const validPages = Object.keys(formPages) + let valid = true + + if (req.params.page) { + // if page is present it must be in allowlist of configured pages + valid = validPages.includes(req.params.page) + } + + if (req.params.subpage) { + // if subpage is present it must always be a number + valid = /^\d+$/.test(req.params.subpage) + } + + if (valid) { + next() + } else { + const error = new ApplicationError('Page not found', 404) + next(error) + } +} + +const setCsrfToken = (req, res, next) => { + if (req?.session) { + if (!req?.session?.csrfToken) { + req.session.csrfToken = crypto.randomBytes(32).toString('hex') + } + } + next() +} + module.exports = { setNextPage, setCurrentFormPages, @@ -425,5 +454,7 @@ module.exports = { validateComponentImagePage, saveFileToRedis, clearSkippedPageData, - checkEmailDomain + checkEmailDomain, + validatePageParams, + setCsrfToken } diff --git a/routes/add-component.js b/routes/add-component.js index 6bf95a7ee..5c052daf1 100644 --- a/routes/add-component.js +++ b/routes/add-component.js @@ -4,7 +4,11 @@ const express = require('express') const { xss } = require('express-xss-sanitizer') const multer = require('multer') -const { COMPONENT_FORM_PAGES, ADD_NEW_COMPONENT_ROUTE } = require('../config') +const { + COMPONENT_FORM_PAGES, + ADD_NEW_COMPONENT_ROUTE, + MESSAGES +} = require('../config') const ApplicationError = require('../helpers/application-error') const { checkYourAnswers } = require('../helpers/check-your-answers') const getPrTitleAndDescription = require('../helpers/get-pr-title-and-description') @@ -25,7 +29,9 @@ const { validateFormDataFileUpload, saveFileToRedis, clearSkippedPageData, - checkEmailDomain + checkEmailDomain, + validatePageParams, + setCsrfToken } = require('../middleware/component-session') const { generateMarkdown } = require('../middleware/generate-documentation') const { pushToGitHub, createPullRequest } = require('../middleware/github-api') @@ -46,41 +52,11 @@ const upload = multer({ const router = express.Router() const checkYourAnswersPath = 'check-your-answers' -const validatePageParams = (req, res, next) => { - const validPages = Object.keys(COMPONENT_FORM_PAGES) - let valid = true - - if (req.params.page) { - // if page is present it must be in allowlist of configured pages - valid = validPages.includes(req.params.page) - } - - if (req.params.subpage) { - // if subpage is present it must always be a number - valid = /^\d+$/.test(req.params.subpage) - } - - if (valid) { - next() - } else { - const error = new ApplicationError('Page not found', 404) - next(error) - } -} - -const setCsrfToken = (req, res, next) => { - if (req?.session) { - if (!req?.session?.csrfToken) { - req.session.csrfToken = crypto.randomBytes(32).toString('hex') - } - } - next() -} - router.all('*', setCsrfToken) // TODO: Why is this a get * ? Can it not just be set in the get /checkYourAnswersPath route below? router.get('*', (req, res, next) => { + console.log('setting cya visited') if (req?.session) { if (req?.url.endsWith(checkYourAnswersPath)) { console.log('visited checkYourAnswersPath') @@ -91,11 +67,6 @@ router.get('*', (req, res, next) => { next() }) -router.get('/error', (req, res, next) => { - const error = new ApplicationError('Uh oh!', 500) - next(error) -}) - // Check your answers page router.get( `/${checkYourAnswersPath}`, @@ -130,6 +101,8 @@ if (process.env.DEV_DUMMY_DATA) { // Start router.get('/start', (req, res) => { + console.log('get start') + delete req.session.checkYourAnswers req.session.started = true console.log('Start session') @@ -137,6 +110,7 @@ router.get('/start', (req, res) => { title: 'Submit a component', csrfToken: req?.session?.csrfToken }) + console.log('after render') }) // Confirmation page @@ -149,31 +123,17 @@ router.get('/confirmation', (req, res) => { router.get('/email/verify/:token', (req, res) => { if (!req?.session?.emailToken) { // session expired - req.session.sessionFlash = { - type: 'error', - title: 'Email verification link expired', - text: `Enter your email address below to receive a new verification link.` - } + req.session.sessionFlash = MESSAGES.emailVerificationExpired res.redirect(`${ADD_NEW_COMPONENT_ROUTE}/email`) } else { - const tokenInSession = req.session.emailToken - const tokenFromParams = req.params.token - if (tokenFromParams === tokenInSession) { + if (req.params.token === req.session.emailToken) { // verified req.session.verified = true - req.session.sessionFlash = { - type: 'success', - title: 'Your email address has been verified', - text: 'You can now submit a new component' - } + req.session.sessionFlash = MESSAGES.emailVerificationSuccess res.redirect(`${ADD_NEW_COMPONENT_ROUTE}/component-details`) } else { // token invalid - req.session.sessionFlash = { - type: 'error', - title: 'Email verification link invalid', - text: `Enter your email address below to receive a new verification link.` - } + req.session.sessionFlash = MESSAGES.emailVerificationInvalidToken res.redirect(`${ADD_NEW_COMPONENT_ROUTE}/email`) } } @@ -200,7 +160,14 @@ router.get('/email', (req, res) => { router.all('*', sessionStarted) router.post('/start', verifyCsrf, (req, res) => { - res.redirect('/contribute/add-new-component/email') + if(process.env.SKIP_VERIFICATION === 'true' && process.env.DEV_VERIFIED_EMAIL) { + req.session['/email'] = { emailAddress: process.env.DEV_VERIFIED_EMAIL} + req.session.emailDomainAllowed = true + req.session.verified = true + res.redirect(`${ADD_NEW_COMPONENT_ROUTE}/component-details`) + } else { + res.redirect(`${ADD_NEW_COMPONENT_ROUTE}/email`) + } }) router.post( @@ -213,7 +180,7 @@ router.post( if (req.emailDomainAllowed) { saveSession(req, res, next) } else { - res.redirect('/contribute/add-new-component/email/not-allowed') + res.redirect(`${ADD_NEW_COMPONENT_ROUTE}/email/not-allowed`) } }, // generate token @@ -222,16 +189,16 @@ router.post( next() }, // send email - async (req, res, next) => { + async (req, res) => { if (req.emailDomainAllowed) { - res.redirect('/contribute/add-new-component/email/check') + res.redirect(`${ADD_NEW_COMPONENT_ROUTE}/email/check`) const token = req?.session?.emailToken const email = req?.session?.['/email']?.emailAddress if (token && email) { try { await sendVerificationEmail(email, token) } catch (error) { - console.error('Error sending verification email') + console.error(`Error sending verification email: ${error}`) } } } @@ -258,15 +225,15 @@ router.get('/email/resend', (req, res) => { }) }) -router.post('/email/resend', xss(), verifyCsrf, async (req, res, next) => { - res.redirect('/contribute/add-new-component/email/check') +router.post('/email/resend', xss(), verifyCsrf, async (req, res) => { + res.redirect(`${ADD_NEW_COMPONENT_ROUTE}/email/check`) const token = req?.session?.emailToken const email = req?.session?.['/email']?.emailAddress if (token && email) { try { await sendVerificationEmail(email, token) } catch (error) { - console.error('Error sending verification email') + console.error(`Error sending verification email: ${error}`) } } }) @@ -341,6 +308,7 @@ router.get( canAddAnother, getBackLink, (req, res) => { + console.log('get page/subpage') console.log(`CYA: ${req?.session?.checkYourAnswers}`) res.render(`${req.params.page}`, { page: COMPONENT_FORM_PAGES[req.params.page], @@ -413,10 +381,7 @@ router.post( validateFormData, (req, res, next) => { if (req.file) { - req.session.sessionFlash = { - type: 'success', - message: `File ‘${req.file.originalname}’ has been uploaded.` - } + req.session.sessionFlash = MESSAGES.componentImageUploaded(req.file.originalname) saveSession(req, res, next) } else { // Skipping saving as no new file uploaded diff --git a/views/community/pages/email-resend.njk b/views/community/pages/email-resend.njk index 187f9471c..dcd3c402e 100644 --- a/views/community/pages/email-resend.njk +++ b/views/community/pages/email-resend.njk @@ -13,13 +13,14 @@ Submit a component

{{ page.title }}

    -
  1. Check that you entered your email address correctly and that it ends justice.gov.uk
  2. -
  3. Check your junk or spam folder
  4. +
  5. Check that you entered your email address correctly: {{page.email}}
  6. +
  7. Check your junk or spam folder.
  8. +
  9. Wait 5 minutes for it arrive.

If you’ve still not received the verification email after step 2 you can either:

  • resend the verification email
  • -
  • enter a different email address, for example if the original ended @digital.justice.gov.uk
  • +
  • enter a different email address
From 7b06597b43756e93815d03689bbd6cb7fe5be5c9 Mon Sep 17 00:00:00 2001 From: Chris Pymm Date: Wed, 2 Jul 2025 14:34:01 +0100 Subject: [PATCH 11/18] Secrets (#1557) * docs: enable sentry in prod using dsn from env * docs: add new secrets into k8s deploy template * docs: pass app url into env --- app.js | 26 ++++++++++++++++++-------- config.js | 1 + kubernetes-deploy-preview.tpl | 14 +++++++++++++- kubernetes-deploy-production.tpl | 13 ++++++++++++- routes/add-component.js | 5 +++++ 5 files changed, 49 insertions(+), 10 deletions(-) diff --git a/app.js b/app.js index e29bd076b..38f5b873f 100644 --- a/app.js +++ b/app.js @@ -1,11 +1,22 @@ /* eslint import/order: "off" */ /* eslint n/no-unpublished-require: "off" */ const Sentry = require('@sentry/node') +const { + APP_PORT, + ENV, + REDIS_URL, + SESSION_SECRET, + SENTRY_DSN +} = require('./config') -Sentry.init({ - dsn: 'https://304866cea16570b04e2090537ae9ac77@o345774.ingest.us.sentry.io/4509252675371008', - sendDefaultPii: true -}) +const isDev = ENV === 'development' +if (!isDev) { + Sentry.init({ + dsn: SENTRY_DSN, + sendDefaultPii: false, + environment: ENV + }) +} const path = require('path') const redisClient = require('./helpers/redis-client') @@ -28,7 +39,6 @@ const { APP_PORT, ENV, REDIS_URL, SESSION_SECRET } = require('./config') const addComponentRoutes = require('./routes/add-component') const app = express() -const isDev = ENV === 'development' if (!isDev) { // Only trust single proxy (Nginx) @@ -110,9 +120,9 @@ app.use('/contribute/add-new-component', addComponentRoutes) // Fallback route to 404 app.use((req, res) => { - res.status(404).render('404', { - title: 'Page not found' - }) + res.status(404).render('404', { + title: 'Page not found' + }) }) // The error handler must be registered before any other error middleware and after all controllers diff --git a/config.js b/config.js index f68871f77..07d444483 100644 --- a/config.js +++ b/config.js @@ -19,6 +19,7 @@ const config = { REDIS_PORT: parseInt(process.env.REDIS_PORT, 10) || 6379, SESSION_SECRET: process.env.SESSION_SECRET || 'your-secret-key', ENV: process.env.ENV || 'development', + SENTRY_DSN: process.env.SENTRY_DSN, ALLOWED_EMAIL_DOMAINS: ['justice.gov.uk'], COMPONENT_FORM_PAGES: { 'email': { diff --git a/kubernetes-deploy-preview.tpl b/kubernetes-deploy-preview.tpl index df0a8637f..11df5a9fd 100644 --- a/kubernetes-deploy-preview.tpl +++ b/kubernetes-deploy-preview.tpl @@ -63,6 +63,11 @@ spec: secretKeyRef: name: notify-submission-template key: notify-submission-template + - name: NOTIFY_VERIFICATION_TEMPLATE + valueFrom: + secretKeyRef: + name: notify-verification-template + key: notify-verification-template - name: NOTIFY_EMAIL valueFrom: secretKeyRef: @@ -83,8 +88,15 @@ spec: secretKeyRef: name: notify-email-max-retries key: notify-email-max-retries + - name: SENTRY_DSN + valueFrom: + secretKeyRef: + name: sentry-dsn + key: sentry-dsn - name: BRANCH value: ${BRANCH} + - name: APP_URL + value: https://moj-frontend-${BRANCH}.apps.live.cloud-platform.service.justice.gov.uk - name: REDIS_URL valueFrom: secretKeyRef: @@ -150,4 +162,4 @@ spec: service: name: moj-frontend-service-${BRANCH} port: - number: 3000 \ No newline at end of file + number: 3000 diff --git a/kubernetes-deploy-production.tpl b/kubernetes-deploy-production.tpl index 153f475b4..1ae24ee3b 100644 --- a/kubernetes-deploy-production.tpl +++ b/kubernetes-deploy-production.tpl @@ -50,6 +50,11 @@ spec: secretKeyRef: name: notify-submission-template key: notify-submission-template + - name: NOTIFY_VERIFICATION_TEMPLATE + valueFrom: + secretKeyRef: + name: notify-verification-template + key: notify-verification-template - name: NOTIFY_EMAIL valueFrom: secretKeyRef: @@ -70,9 +75,15 @@ spec: secretKeyRef: name: notify-email-max-retries key: notify-email-max-retries + - name: SENTRY_DSN + valueFrom: + secretKeyRef: + name: sentry-dsn + key: sentry-dsn - name: BRANCH value: ${BRANCH} - - name: REDIS_URL + - name: APP_URL + value: https://design-patterns.service.justice.gov.uk - name: REDIS_URL valueFrom: secretKeyRef: name: moj-frontend-ec-cluster-output diff --git a/routes/add-component.js b/routes/add-component.js index 5c052daf1..e303aabd5 100644 --- a/routes/add-component.js +++ b/routes/add-component.js @@ -52,6 +52,11 @@ const upload = multer({ const router = express.Router() const checkYourAnswersPath = 'check-your-answers' +router.get('/error', (req,res, next) => { + const error = new ApplicationError('ohno') + next(error) +}) + router.all('*', setCsrfToken) // TODO: Why is this a get * ? Can it not just be set in the get /checkYourAnswersPath route below? From 464f2b9ecd55eaa84eccb547656f93e53f2ac985 Mon Sep 17 00:00:00 2001 From: Chris Pymm Date: Wed, 2 Jul 2025 14:47:49 +0100 Subject: [PATCH 12/18] docs: small email content tweaks --- views/community/pages/email-check.njk | 2 +- views/community/pages/email-not-allowed.njk | 2 +- views/community/pages/email-resend.njk | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/views/community/pages/email-check.njk b/views/community/pages/email-check.njk index 3a2c0ede3..04c3cf5f2 100644 --- a/views/community/pages/email-check.njk +++ b/views/community/pages/email-check.njk @@ -11,7 +11,7 @@ {% endif %} Submit a component

{{ page.title }}

-

We've sent an email to: {{page.email}}.

+

We've sent an email to: {{page.email}}

Select the link in the email to continue submitting a component. The link will open in a new window, so you can close this one.

{% call govukDetails({ summaryText: "Problems with the email?" }) %} We can send the email again or you can use a different email address. diff --git a/views/community/pages/email-not-allowed.njk b/views/community/pages/email-not-allowed.njk index 174c7df22..fa6b7c426 100644 --- a/views/community/pages/email-not-allowed.njk +++ b/views/community/pages/email-not-allowed.njk @@ -10,7 +10,7 @@ {% endif %} Submit a component

{{ page.title }}

-

You need to work at MoJ to contribute to the MoJ Design System. Contributions will be open to everyone in 2026.

+

You need to work at MoJ to contribute to the MoJ Design System.

Go back to the MoJ Design System

{% endblock %} diff --git a/views/community/pages/email-resend.njk b/views/community/pages/email-resend.njk index dcd3c402e..145c9f310 100644 --- a/views/community/pages/email-resend.njk +++ b/views/community/pages/email-resend.njk @@ -15,7 +15,7 @@
  1. Check that you entered your email address correctly: {{page.email}}
  2. Check your junk or spam folder.
  3. -
  4. Wait 5 minutes for it arrive.
  5. +
  6. Wait 5 minutes for it to arrive.

If you’ve still not received the verification email after step 2 you can either:

    From 8c410314f677874c5babab99ce07431dc8de96ba Mon Sep 17 00:00:00 2001 From: Chris Pymm Date: Wed, 2 Jul 2025 20:11:50 +0100 Subject: [PATCH 13/18] docs: use user email for verification emails --- middleware/notify-email.js | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/middleware/notify-email.js b/middleware/notify-email.js index 4a9e347a8..9ece1dfd5 100644 --- a/middleware/notify-email.js +++ b/middleware/notify-email.js @@ -12,7 +12,7 @@ const { } = require('../config') const notifyClient = new NotifyClient(NOTIFY_TOKEN) -const emailAddress = NOTIFY_EMAIL +const dsTeamEmail = NOTIFY_EMAIL const sendEmail = async ( templateId, @@ -25,9 +25,9 @@ const sendEmail = async ( for (let attempt = 1; attempt <= retries; attempt++) { try { console.log( - `Sending email to ${emailAddress} using template ${templateId}, attempt ${attempt}` + `Sending email to ${email} using template ${templateId}, attempt ${attempt}` ) - const response = await notifyClient.sendEmail(templateId, emailAddress, { + const response = await notifyClient.sendEmail(templateId, email, { personalisation }) console.log('Email sent successfully.') @@ -57,7 +57,7 @@ const sendSubmissionEmail = async (fileBuffer = null, markdown = null) => { personalisation.markdown = markdown } - return sendEmail(NOTIFY_SUBMISSION_TEMPLATE, NOTIFY_EMAIL, personalisation) + return sendEmail(NOTIFY_SUBMISSION_TEMPLATE, dsTeamEmail, personalisation) } const sendPrEmail = async ({ url, number }) => { @@ -70,7 +70,7 @@ const sendPrEmail = async ({ url, number }) => { personalisation.preview_link = `https://moj-frontend-pr-${number}.apps.live.cloud-platform.service.justice.gov.uk` } - return sendEmail(NOTIFY_PR_TEMPLATE, NOTIFY_EMAIL, personalisation) + return sendEmail(NOTIFY_PR_TEMPLATE, dsTeamEmail, personalisation) } const sendVerificationEmail = async (email, token) => { From 2d81a86773b31be4c97668e8b152707be371a2cd Mon Sep 17 00:00:00 2001 From: Chris Pymm Date: Fri, 4 Jul 2025 10:24:28 +0100 Subject: [PATCH 14/18] View refactoring (#1558) * refactor(wip): refactor views in the contributions app * docs: refactor all standard views * docs: refactor remaining views --- config.js | 2 +- docs/_includes/layouts/base.njk | 2 +- views/common/base.njk | 2 +- views/common/contributions.njk | 49 +++++ views/common/macros/form.njk | 8 + views/common/macros/page-title.njk | 6 + .../pages/accessibility-findings-more.njk | 46 ---- .../pages/accessibility-findings.njk | 200 ++++++++---------- views/community/pages/add-assistive-tech.njk | 136 +++++------- views/community/pages/add-external-audit.njk | 152 ++++++------- views/community/pages/add-internal-audit.njk | 157 ++++++-------- views/community/pages/check-your-answers.njk | 71 ++++--- .../pages/component-code-details.njk | 188 ++++++++-------- views/community/pages/component-code.njk | 91 ++++---- views/community/pages/component-details.njk | 116 ++++------ views/community/pages/component-image.njk | 147 ++++++------- views/community/pages/confirmation.njk | 62 +++--- views/community/pages/email-check.njk | 25 +-- views/community/pages/email-not-allowed.njk | 19 +- views/community/pages/email-resend.njk | 40 ++-- views/community/pages/email.njk | 87 +++----- views/community/pages/figma-link.njk | 101 ++++----- views/community/pages/figma.njk | 91 ++++---- views/community/pages/prototype-url.njk | 77 ------- views/community/pages/prototype.njk | 64 ------ views/community/pages/remove.njk | 61 +++--- views/community/pages/start.njk | 61 +++--- views/community/pages/your-details.njk | 158 +++++++------- 28 files changed, 933 insertions(+), 1286 deletions(-) create mode 100644 views/common/contributions.njk create mode 100644 views/common/macros/form.njk create mode 100644 views/common/macros/page-title.njk delete mode 100644 views/community/pages/accessibility-findings-more.njk delete mode 100644 views/community/pages/prototype-url.njk delete mode 100644 views/community/pages/prototype.njk diff --git a/config.js b/config.js index 07d444483..9be15e7ff 100644 --- a/config.js +++ b/config.js @@ -266,7 +266,7 @@ const config = { componentImageUploaded: (filename) => { return { type: 'success', - message: `File ‘${filename}’ has been uploaded.` + text: `File ‘${filename}’ has been uploaded.` } }, componentImageRemoved: (filename) => { diff --git a/docs/_includes/layouts/base.njk b/docs/_includes/layouts/base.njk index 939032e99..59fe855f6 100644 --- a/docs/_includes/layouts/base.njk +++ b/docs/_includes/layouts/base.njk @@ -40,7 +40,7 @@
{% block beforeContent %}{% endblock %} -
+
{% block content %} {{ content | safe }} {% endblock %} diff --git a/views/common/base.njk b/views/common/base.njk index 607007095..6f9c8d183 100644 --- a/views/common/base.njk +++ b/views/common/base.njk @@ -40,7 +40,7 @@
{% block beforeContent %}{% endblock %} -
+
{% block content %} {{ content | safe }} {% endblock %} diff --git a/views/common/contributions.njk b/views/common/contributions.njk new file mode 100644 index 000000000..de170c33d --- /dev/null +++ b/views/common/contributions.njk @@ -0,0 +1,49 @@ +{% from "govuk/components/error-summary/macro.njk" import govukErrorSummary %} +{% from "govuk/components/back-link/macro.njk" import govukBackLink %} +{% from "moj/components/alert/macro.njk" import mojAlert %}{% extends "base.njk" %} +{% from "govuk/components/button/macro.njk" import govukButton %} +{% from "macros/page-title.njk" import pageTitle %} +{% from "macros/form.njk" import form %} + +{% block beforeContent %} + +{% endblock %} + +{% block content %} +
+ {% if errorList %} + {{ govukErrorSummary({ + titleText: "There is a problem", + errorList: errorList + }) }} + {% endif %} + + {% if sessionFlash.text %} + {{ mojAlert({ + variant: sessionFlash.type, + title: sessionFlash.title, + text: sessionFlash.text, + showTitleAsHeading: true, + role: "alert" + })}} + {% endif %} + + {% block pageTitle %} + {{ pageTitle({ + caption: 'Submit a new component', + title: page.title + })}} + {% endblock %} + + {% block form %}{% endblock %} + +
+ +{% endblock %} diff --git a/views/common/macros/form.njk b/views/common/macros/form.njk new file mode 100644 index 000000000..67f1cad6a --- /dev/null +++ b/views/common/macros/form.njk @@ -0,0 +1,8 @@ +{% from "govuk/macros/attributes.njk" import govukAttributes %} +{% macro form(params) %} + + + + {{ caller()}} + +{% endmacro %} diff --git a/views/common/macros/page-title.njk b/views/common/macros/page-title.njk new file mode 100644 index 000000000..63781f75b --- /dev/null +++ b/views/common/macros/page-title.njk @@ -0,0 +1,6 @@ +{% macro pageTitle(params)%} + {% if params.title %} + {{ params.caption }} + {% endif %} +

{{ params.title }}

+{% endmacro %} diff --git a/views/community/pages/accessibility-findings-more.njk b/views/community/pages/accessibility-findings-more.njk deleted file mode 100644 index 0b5bd5126..000000000 --- a/views/community/pages/accessibility-findings-more.njk +++ /dev/null @@ -1,46 +0,0 @@ -{% from "govuk/components/character-count/macro.njk" import govukCharacterCount %} -{% from "govuk/components/button/macro.njk" import govukButton %} -{% from "govuk/components/error-summary/macro.njk" import govukErrorSummary %} -{% from "govuk/components/back-link/macro.njk" import govukBackLink %} -{% extends "base.njk" %} -{% block content %} -
- {% if backLink %} - {{ govukBackLink({ - text: "Back", - href: backLink - }) }} - {% endif %} - {% if errorList %} - {{ govukErrorSummary({ - titleText: "There is a problem", - errorList: errorList - }) }} - {% endif %} -
- Submit a new component -

Accessibility findings

- {{ govukCharacterCount({ - name: "accessibilityTellUsMore", - id: "accessibility-tell-us-more", - maxwords: 500, - label: { - text: "Tell us more about how it has been accessibility tested and the findings", - classes: "govuk-label--m", - isPageHeading: true - }, - errorMessage: formErrors.accessibilityTellUsMore, - value: formData.accessibilityTellUsMore - }) }} - - - -
- {{ govukButton({ - text: 'Continue' - }) }} -
-
-
- -{% endblock %} diff --git a/views/community/pages/accessibility-findings.njk b/views/community/pages/accessibility-findings.njk index a336462d2..8402d85e2 100644 --- a/views/community/pages/accessibility-findings.njk +++ b/views/community/pages/accessibility-findings.njk @@ -1,120 +1,104 @@ {% from "govuk/components/radios/macro.njk" import govukRadios %} -{% from "govuk/components/button/macro.njk" import govukButton %} -{% from "govuk/components/error-summary/macro.njk" import govukErrorSummary %} -{% from "govuk/components/back-link/macro.njk" import govukBackLink %} -{% extends "base.njk" %} -{% block content %} -
- {% if backLink %} - {{ govukBackLink({ - text: "Back", - href: backLink - }) }} - {% endif %} - {% if errorList %} - {{ govukErrorSummary({ - titleText: "There is a problem", - errorList: errorList - }) }} - {% endif %} -
- Submit new component -

Accessibility findings

-

Sharing accessibility findings helps people understand how to use the component. It also helps the Design System team to review it.

- {{ govukRadios({ - name: "hasComponentBeenTestedExternalAccessibility", - hint: { - text: page.fields.hasComponentBeenTestedExternalAccessibility.hint +{% extends "contributions.njk" %} + +{% block form %} + {% call form({ + action: submitUrl, + csrfToken: csrfToken + }) %} + +

Sharing accessibility findings helps people understand how to use the component. It also helps the Design System team to review it.

+ + {{ govukRadios({ + name: "hasComponentBeenTestedExternalAccessibility", + hint: { + text: page.fields.hasComponentBeenTestedExternalAccessibility.hint + }, + fieldset: { + attributes: { + id: "has-component-been-tested-external-accessibility" }, - fieldset: { - attributes: { - id: "has-component-been-tested-external-accessibility" - }, - legend: { - text: page.fields.hasComponentBeenTestedExternalAccessibility.label, - classes: "govuk-fieldset__legend--m" - } + legend: { + text: page.fields.hasComponentBeenTestedExternalAccessibility.label, + classes: "govuk-fieldset__legend--m" + } + }, + items: [ + { + value: "yes", + text: "Yes" }, - items: [ - { - value: "yes", - text: "Yes" - }, - { - value: "no", - text: "No" - } - ], - errorMessage: formErrors.hasComponentBeenTestedExternalAccessibility, - value: formData.hasComponentBeenTestedExternalAccessibility - }) }} + { + value: "no", + text: "No" + } + ], + errorMessage: formErrors.hasComponentBeenTestedExternalAccessibility, + value: formData.hasComponentBeenTestedExternalAccessibility + }) }} - {{ govukRadios({ - name: "hasComponentBeenTestedInternalAudit", - hint: { - text: page.fields.hasComponentBeenTestedInternalAudit.hint + {{ govukRadios({ + name: "hasComponentBeenTestedInternalAudit", + hint: { + text: page.fields.hasComponentBeenTestedInternalAudit.hint + }, + fieldset: { + attributes: { + id: "has-component-been-tested-internal-audit" }, - fieldset: { - attributes: { - id: "has-component-been-tested-internal-audit" - }, - legend: { - text: page.fields.hasComponentBeenTestedInternalAudit.label, - classes: "govuk-fieldset__legend--m" - } + legend: { + text: page.fields.hasComponentBeenTestedInternalAudit.label, + classes: "govuk-fieldset__legend--m" + } + }, + items: [ + { + value: "yes", + text: "Yes" }, - items: [ - { - value: "yes", - text: "Yes" - }, - { - value: "no", - text: "No" - } - ], - errorMessage: formErrors.hasComponentBeenTestedInternalAudit, - value: formData.hasComponentBeenTestedInternalAudit - }) }} + { + value: "no", + text: "No" + } + ], + errorMessage: formErrors.hasComponentBeenTestedInternalAudit, + value: formData.hasComponentBeenTestedInternalAudit + }) }} - {{ govukRadios({ - name: "hasComponentBeenTestedUsingAssistiveTechnology", - hint: { - text: page.fields.hasComponentBeenTestedUsingAssistiveTechnology.hint + {{ govukRadios({ + name: "hasComponentBeenTestedUsingAssistiveTechnology", + hint: { + text: page.fields.hasComponentBeenTestedUsingAssistiveTechnology.hint + }, + fieldset: { + attributes: { + id: "has-component-been-tested-using-assistive-technology" }, - fieldset: { - attributes: { - id: "has-component-been-tested-using-assistive-technology" - }, - legend: { - text: page.fields.hasComponentBeenTestedUsingAssistiveTechnology.label, - classes: "govuk-fieldset__legend--m" - } + legend: { + text: page.fields.hasComponentBeenTestedUsingAssistiveTechnology.label, + classes: "govuk-fieldset__legend--m" + } + }, + items: [ + { + value: "yes", + text: "Yes" }, - items: [ - { - value: "yes", - text: "Yes" - }, - { - value: "no", - text: "No" - } - ], - errorMessage: formErrors.hasComponentBeenTestedUsingAssistiveTechnology, - value: formData.hasComponentBeenTestedUsingAssistiveTechnology - }) }} - - - -
- {{ govukButton({ - text: 'Continue', - preventDoubleClick: true - }) }} -
-
-
+ { + value: "no", + text: "No" + } + ], + errorMessage: formErrors.hasComponentBeenTestedUsingAssistiveTechnology, + value: formData.hasComponentBeenTestedUsingAssistiveTechnology + }) }} +
+ {{ govukButton({ + text: 'Continue', + preventDoubleClick: true + }) }} +
+ {% endcall %} {% endblock %} diff --git a/views/community/pages/add-assistive-tech.njk b/views/community/pages/add-assistive-tech.njk index 41d009e03..ee143083d 100644 --- a/views/community/pages/add-assistive-tech.njk +++ b/views/community/pages/add-assistive-tech.njk @@ -2,88 +2,68 @@ {% from "govuk/components/date-input/macro.njk" import govukDateInput %} {% from "govuk/components/file-upload/macro.njk" import govukFileUpload %} {% from "govuk/components/character-count/macro.njk" import govukCharacterCount %} -{% from "govuk/components/button/macro.njk" import govukButton %} -{% from "govuk/components/error-summary/macro.njk" import govukErrorSummary %} -{% from "govuk/components/back-link/macro.njk" import govukBackLink %} -{% from "govuk/components/summary-list/macro.njk" import govukSummaryList %} -{% from "govuk/components/tag/macro.njk" import govukTag %} -{% extends "base.njk" %} -{% block content %} -
- {% if backLink %} - {{ govukBackLink({ - text: "Back", - href: backLink - }) }} - {% endif %} - {% if errorList %} - {{ govukErrorSummary({ - titleText: "There is a problem", - errorList: errorList - }) }} - {% endif %} -
- Submit a new component -

Testing with assistive technology

+{% extends "contributions.njk" %} - {{ govukDateInput({ - id: "testing-date", - namePrefix: "testingDate", - name: "testing-date", - errorMessage: formErrors.testingDate, - value: formData.testingDate, - items: [ - { - classes: (formErrorStyles['testingDate-day'] | default('')) + " govuk-input--width-2", - name: "day", - value: formData['testingDate-day'] - }, - { - classes: (formErrorStyles['testingDate-month'] | default('')) + " govuk-input--width-2", - name: "month", - value: formData['testingDate-month'] - }, - { - classes: (formErrorStyles['testingDate-year'] | default('')) + " govuk-input--width-4", - name: "year", - value: formData['testingDate-year'] - } - ], - fieldset: { - legend: { - text: page.fields.testingDate.label, - isPageHeading: true, - classes: "govuk-fieldset__legend--m" - } - }, - hint: { - text: page.fields.testingDate.hint - } - }) }} +{% block form %} + {% call form({ + action: submitUrl, + csrfToken: csrfToken + }) %} - {{ govukCharacterCount({ - id: "issues-discovered", - name: "issuesDiscovered", - maxwords: 250, - label: { - text: page.fields.issuesDiscovered.label, - classes: "govuk-label--m", - isPageHeading: true + {{ govukDateInput({ + id: "testing-date", + namePrefix: "testingDate", + name: "testing-date", + errorMessage: formErrors.testingDate, + value: formData.testingDate, + items: [ + { + classes: (formErrorStyles['testingDate-day'] | default('')) + " govuk-input--width-2", + name: "day", + value: formData['testingDate-day'] }, - errorMessage: formErrors.issuesDiscovered, - value: formData.issuesDiscovered - }) }} - - + { + classes: (formErrorStyles['testingDate-month'] | default('')) + " govuk-input--width-2", + name: "month", + value: formData['testingDate-month'] + }, + { + classes: (formErrorStyles['testingDate-year'] | default('')) + " govuk-input--width-4", + name: "year", + value: formData['testingDate-year'] + } + ], + fieldset: { + legend: { + text: page.fields.testingDate.label, + isPageHeading: true, + classes: "govuk-fieldset__legend--m" + } + }, + hint: { + text: page.fields.testingDate.hint + } + }) }} -
- {{ govukButton({ - text: 'Continue', - preventDoubleClick: true - }) }} -
-
-
+ {{ govukCharacterCount({ + id: "issues-discovered", + name: "issuesDiscovered", + maxwords: 250, + label: { + text: page.fields.issuesDiscovered.label, + classes: "govuk-label--m", + isPageHeading: true + }, + errorMessage: formErrors.issuesDiscovered, + value: formData.issuesDiscovered + }) }} +
+ {{ govukButton({ + text: 'Continue', + preventDoubleClick: true + }) }} +
+ {% endcall %} {% endblock %} diff --git a/views/community/pages/add-external-audit.njk b/views/community/pages/add-external-audit.njk index 01e157faf..a1d14e892 100644 --- a/views/community/pages/add-external-audit.njk +++ b/views/community/pages/add-external-audit.njk @@ -1,98 +1,78 @@ {% from "govuk/components/input/macro.njk" import govukInput %} {% from "govuk/components/date-input/macro.njk" import govukDateInput %} -{% from "govuk/components/file-upload/macro.njk" import govukFileUpload %} {% from "govuk/components/character-count/macro.njk" import govukCharacterCount %} -{% from "govuk/components/button/macro.njk" import govukButton %} -{% from "govuk/components/error-summary/macro.njk" import govukErrorSummary %} -{% from "govuk/components/back-link/macro.njk" import govukBackLink %} -{% from "govuk/components/summary-list/macro.njk" import govukSummaryList %} -{% from "govuk/components/tag/macro.njk" import govukTag %} -{% extends "base.njk" %} -{% block content %} -
- {% if backLink %} - {{ govukBackLink({ - text: "Back", - href: backLink - }) }} - {% endif %} - {% if errorList %} - {{ govukErrorSummary({ - titleText: "There is a problem", - errorList: errorList - }) }} - {% endif %} -
- Submit a new component -

External accessibility audit

+{% extends "contributions.njk" %} - {{ govukInput({ - label: { - text: page.fields.externalOrganisation.label, - classes: "govuk-label--m" - }, - id: "external-organisation", - name: "externalOrganisation", - errorMessage: formErrors.externalOrganisation, - value: formData.externalOrganisation - }) }} +{% block form %} + {% call form({ + action: submitUrl, + csrfToken: csrfToken + }) %} - {{ govukDateInput({ - id: "audit-date", - namePrefix: "auditDate", - name: "audit-date", - errorMessage: formErrors.auditDate, - value: formData.auditDate, - items: [ - { - classes: (formErrorStyles['auditDate-day'] | default('')) + " govuk-input--width-2", - name: "day", - value: formData['auditDate-day'] - }, - { - classes: (formErrorStyles['auditDate-month'] | default('')) + " govuk-input--width-2", - name: "month", - value: formData['auditDate-month'] - }, - { - classes: (formErrorStyles['auditDate-year'] | default('')) + " govuk-input--width-4", - name: "year", - value: formData['auditDate-year'] - } - ], - fieldset: { - legend: { - text: page.fields.auditDate.label, - classes: "govuk-fieldset__legend--m" - } - }, - hint: { - text: page.fields.auditDate.hint - } - }) }} + {{ govukInput({ + label: { + text: page.fields.externalOrganisation.label, + classes: "govuk-label--m" + }, + id: "external-organisation", + name: "externalOrganisation", + errorMessage: formErrors.externalOrganisation, + value: formData.externalOrganisation + }) }} - {{ govukCharacterCount({ - id: "issues-discovered", - name: "issuesDiscovered", - maxwords: 250, - label: { - text: page.fields.issuesDiscovered.label, - classes: "govuk-label--m" + {{ govukDateInput({ + id: "audit-date", + namePrefix: "auditDate", + name: "audit-date", + errorMessage: formErrors.auditDate, + value: formData.auditDate, + items: [ + { + classes: (formErrorStyles['auditDate-day'] | default('')) + " govuk-input--width-2", + name: "day", + value: formData['auditDate-day'] }, - errorMessage: formErrors.issuesDiscovered, - value: formData.issuesDiscovered - }) }} + { + classes: (formErrorStyles['auditDate-month'] | default('')) + " govuk-input--width-2", + name: "month", + value: formData['auditDate-month'] + }, + { + classes: (formErrorStyles['auditDate-year'] | default('')) + " govuk-input--width-4", + name: "year", + value: formData['auditDate-year'] + } + ], + fieldset: { + legend: { + text: page.fields.auditDate.label, + classes: "govuk-fieldset__legend--m" + } + }, + hint: { + text: page.fields.auditDate.hint + } + }) }} - + {{ govukCharacterCount({ + id: "issues-discovered", + name: "issuesDiscovered", + maxwords: 250, + label: { + text: page.fields.issuesDiscovered.label, + classes: "govuk-label--m" + }, + errorMessage: formErrors.issuesDiscovered, + value: formData.issuesDiscovered + }) }} -
- {{ govukButton({ - text: 'Continue', - preventDoubleClick: true - }) }} -
-
-
+
+ {{ govukButton({ + text: 'Continue', + preventDoubleClick: true + }) }} +
+ {% endcall %} {% endblock %} diff --git a/views/community/pages/add-internal-audit.njk b/views/community/pages/add-internal-audit.njk index 739bc1c1c..1886659de 100644 --- a/views/community/pages/add-internal-audit.njk +++ b/views/community/pages/add-internal-audit.njk @@ -1,100 +1,79 @@ {% from "govuk/components/input/macro.njk" import govukInput %} {% from "govuk/components/date-input/macro.njk" import govukDateInput %} -{% from "govuk/components/file-upload/macro.njk" import govukFileUpload %} {% from "govuk/components/character-count/macro.njk" import govukCharacterCount %} -{% from "govuk/components/button/macro.njk" import govukButton %} -{% from "govuk/components/error-summary/macro.njk" import govukErrorSummary %} -{% from "govuk/components/back-link/macro.njk" import govukBackLink %} -{% from "govuk/components/summary-list/macro.njk" import govukSummaryList %} -{% from "govuk/components/tag/macro.njk" import govukTag %} -{% extends "base.njk" %} -{% block content %} -
- {% if backLink %} - {{ govukBackLink({ - text: "Back", - href: backLink - }) }} - {% endif %} - {% if errorList %} - {{ govukErrorSummary({ - titleText: "There is a problem", - errorList: errorList - }) }} - {% endif %} -
- Submit a new component -

Internal accessibility review

+{% extends "contributions.njk" %} - {{ govukInput({ - label: { - text: page.fields.internalOrganisation.label, - classes: "govuk-label--m" - }, - id: "internal-organisation", - name: "internalOrganisation", - errorMessage: formErrors.internalOrganisation, - value: formData.internalOrganisation - }) }} +{% block form %} + {% call form({ + action: submitUrl, + csrfToken: csrfToken + }) %} - {{ govukDateInput({ - id: "audit-date", - namePrefix: "auditDate", - name: "audit-date", - errorMessage: formErrors.auditDate, - value: formData.auditDate, - items: [ - { - classes: (formErrorStyles['auditDate-day'] | default('')) + " govuk-input--width-2", - name: "day", - value: formData['auditDate-day'] - }, - { - classes: (formErrorStyles['auditDate-month'] | default('')) + " govuk-input--width-2", - name: "month", - value: formData['auditDate-month'] - }, - { - classes: (formErrorStyles['auditDate-year'] | default('')) + " govuk-input--width-4", - name: "year", - value: formData['auditDate-year'] - } - ], - fieldset: { - legend: { - text: page.fields.auditDate.label, - isPageHeading: true, - classes: "govuk-fieldset__legend--m" - } - }, - hint: { - text: page.fields.auditDate.hint - } - }) }} + {{ govukInput({ + label: { + text: page.fields.internalOrganisation.label, + classes: "govuk-label--m" + }, + id: "internal-organisation", + name: "internalOrganisation", + errorMessage: formErrors.internalOrganisation, + value: formData.internalOrganisation + }) }} - {{ govukCharacterCount({ - id: "issues-discovered", - name: "issuesDiscovered", - maxwords: 250, - label: { - text: page.fields.issuesDiscovered.label, - classes: "govuk-label--m", - isPageHeading: true + {{ govukDateInput({ + id: "audit-date", + namePrefix: "auditDate", + name: "audit-date", + errorMessage: formErrors.auditDate, + value: formData.auditDate, + items: [ + { + classes: (formErrorStyles['auditDate-day'] | default('')) + " govuk-input--width-2", + name: "day", + value: formData['auditDate-day'] }, - errorMessage: formErrors.issuesDiscovered, - value: formData.issuesDiscovered - }) }} - - + { + classes: (formErrorStyles['auditDate-month'] | default('')) + " govuk-input--width-2", + name: "month", + value: formData['auditDate-month'] + }, + { + classes: (formErrorStyles['auditDate-year'] | default('')) + " govuk-input--width-4", + name: "year", + value: formData['auditDate-year'] + } + ], + fieldset: { + legend: { + text: page.fields.auditDate.label, + isPageHeading: true, + classes: "govuk-fieldset__legend--m" + } + }, + hint: { + text: page.fields.auditDate.hint + } + }) }} -
- {{ govukButton({ - text: 'Continue', - preventDoubleClick: true - }) }} -
-
-
+ {{ govukCharacterCount({ + id: "issues-discovered", + name: "issuesDiscovered", + maxwords: 250, + label: { + text: page.fields.issuesDiscovered.label, + classes: "govuk-label--m", + isPageHeading: true + }, + errorMessage: formErrors.issuesDiscovered, + value: formData.issuesDiscovered + }) }} +
+ {{ govukButton({ + text: 'Continue', + preventDoubleClick: true + }) }} +
+ {% endcall %} {% endblock %} diff --git a/views/community/pages/check-your-answers.njk b/views/community/pages/check-your-answers.njk index 16dfc7cf9..28595cf54 100644 --- a/views/community/pages/check-your-answers.njk +++ b/views/community/pages/check-your-answers.njk @@ -1,42 +1,61 @@ {% from "govuk/components/summary-list/macro.njk" import govukSummaryList %} -{% from "macros/summary-card.njk" import summaryCard %} {% from "govuk/components/button/macro.njk" import govukButton %} +{% from "macros/summary-card.njk" import summaryCard %} +{% from "macros/page-title.njk" import pageTitle %} +{% from "macros/form.njk" import form %} + {% extends "base.njk" %} + +{% block beforeContent %} + +{% endblock %} + {% block content %}
-
- Submit a new component -

Check your answers

+ {{ pageTitle({ + caption: 'Submit a new component', + title: 'Check your answers' + })}} - {% for section in sections %} - {% set cardParams = { - title: { - text: section.title - }, - actions: { - items: section.actions - } + {% for section in sections %} + {% set cardParams = { + title: { + text: section.title + }, + actions: { + items: section.actions } - %} - {% if section.answerRows.length > 0 %} - {{ govukSummaryList({ - card:cardParams, - rows: section.answerRows - }) }} - {% else %} - {% call summaryCard(cardParams) -%} -

None

- {%- endcall %} - {% endif %} - {% endfor %} + } + %} - + {% if section.answerRows.length > 0 %} + {{ govukSummaryList({ + card:cardParams, + rows: section.answerRows + }) }} + {% else %} + {% call summaryCard(cardParams) -%} +

None

+ {%- endcall %} + {% endif %} + {% endfor %} + {% call form({ + csrfToken: csrfToken, + action: submitUrl + }) %}
{{ govukButton({ text: 'Accept and send' }) }}
-
+ {% endcall %}
{% endblock %} diff --git a/views/community/pages/component-code-details.njk b/views/community/pages/component-code-details.njk index 641df4f21..4443bbc30 100644 --- a/views/community/pages/component-code-details.njk +++ b/views/community/pages/component-code-details.njk @@ -1,29 +1,18 @@ {% from "govuk/components/input/macro.njk" import govukInput %} {% from "govuk/components/radios/macro.njk" import govukRadios %} {% from "govuk/components/character-count/macro.njk" import govukCharacterCount %} -{% from "govuk/components/button/macro.njk" import govukButton %} -{% from "govuk/components/error-summary/macro.njk" import govukErrorSummary %} -{% from "govuk/components/back-link/macro.njk" import govukBackLink %} -{% extends "base.njk" %} -{% block content %} -
- {% if backLink %} - {{ govukBackLink({ - text: "Back", - href: backLink - }) }} - {% endif %} - {% if errorList %} - {{ govukErrorSummary({ - titleText: "There is a problem", - errorList: errorList - }) }} - {% endif %} -
- Submit a new component -

Component code

-

You should only send the component code. It should not contain any personally identifiable or sensitive information. You can add more than one code example.

- {% set otherHtml %} + +{% extends "contributions.njk" %} + +{% block form %} + {% call form({ + action: submitUrl, + csrfToken: csrfToken + }) %} + +

You should only send the component code. It should not contain any personally identifiable or sensitive information. You can add more than one code example.

+ + {% set otherHtml %} {{ govukInput({ id: "component-code-language-other", name: "componentCodeLanguageOther", @@ -35,94 +24,91 @@ text: page.fields.componentCodeLanguageOther.label } }) }} - {% endset -%} - {{ govukRadios({ - id: "component-code-language", - name: "componentCodeLanguage", - errorMessage: formErrors.componentCodeLanguage, - value: formData.componentCodeLanguage, - hint: { - text: page.fields.componentCodeLanguage.hint - }, - fieldset: { - legend: { - text: page.fields.componentCodeLanguage.label, - isPageHeading: true, - classes: "govuk-fieldset__legend--l" - } - }, - items: [ - { - value: "html", - text: "HTML" - }, - { - value: "nunjucks", - text: "Nunjucks" - }, - { - value: "css", - text: "CSS" - }, - { - value: "javascript", - text: "JavaScript" - }, - { - value: "other", - text: "Other", - conditional: { - html: otherHtml - } - } - ] - }) }} + {% endset -%} - {{ govukCharacterCount({ - name: "componentCode", - id: "component-code", - maxlength: 10000, - classes: 'font-monospace', - rows: 30, - label: { - text: "Add the code", - classes: "govuk-label--m" + {{ govukRadios({ + id: "component-code-language", + name: "componentCodeLanguage", + errorMessage: formErrors.componentCodeLanguage, + value: formData.componentCodeLanguage, + hint: { + text: page.fields.componentCodeLanguage.hint + }, + fieldset: { + legend: { + text: page.fields.componentCodeLanguage.label, + isPageHeading: true, + classes: "govuk-fieldset__legend--l" + } + }, + items: [ + { + value: "html", + text: "HTML" }, - errorMessage: formErrors.componentCode, - value: formData.componentCode - }) }} - - {{ govukCharacterCount({ - name: "componentCodeUsage", - id: "component-code-usage", - maxwords: 250, - label: { - text: page.fields.componentCodeUsage.label, - classes: "govuk-label--m" + { + value: "nunjucks", + text: "Nunjucks" }, - hint: { - text: page.fields.componentCodeUsage.hint + { + value: "css", + text: "CSS" }, - errorMessage: formErcomponentCodeLanguages.componentCodeUsage, - value: formData.componentCodeUsage - }) }} + { + value: "javascript", + text: "JavaScript" + }, + { + value: "other", + text: "Other", + conditional: { + html: otherHtml + } + } + ] + }) }} - + {{ govukCharacterCount({ + name: "componentCode", + id: "component-code", + maxlength: 10000, + classes: 'font-monospace', + rows: 30, + label: { + text: "Add the code", + classes: "govuk-label--m" + }, + errorMessage: formErrors.componentCode, + value: formData.componentCode + }) }} -
- {{ govukButton({ - text: 'Continue', - preventDoubleClick: true - }) }} - {% if showAddAnother %} + {{ govukCharacterCount({ + name: "componentCodeUsage", + id: "component-code-usage", + maxwords: 250, + label: { + text: page.fields.componentCodeUsage.label, + classes: "govuk-label--m" + }, + hint: { + text: page.fields.componentCodeUsage.hint + }, + errorMessage: formErcomponentCodeLanguages.componentCodeUsage, + value: formData.componentCodeUsage + }) }} +
+ {{ govukButton({ + text: 'Continue', + preventDoubleClick: true + }) }} + {% if showAddAnother %} {{ govukButton({ text: "Add another code example", classes: "govuk-button--secondary", name: "addAnother" }) }} - {% endif %} -
- -
+ {% endif %} +
+ {% endcall %} {% endblock %} diff --git a/views/community/pages/component-code.njk b/views/community/pages/component-code.njk index f045a0ec4..355036a8c 100644 --- a/views/community/pages/component-code.njk +++ b/views/community/pages/component-code.njk @@ -1,60 +1,43 @@ {% from "govuk/components/radios/macro.njk" import govukRadios %} -{% from "govuk/components/button/macro.njk" import govukButton %} -{% from "govuk/components/error-summary/macro.njk" import govukErrorSummary %} -{% from "govuk/components/back-link/macro.njk" import govukBackLink %} -{% extends "base.njk" %} -{% block content %} -
- {% if backLink %} - {{ govukBackLink({ - text: "Back", - href: backLink - }) }} - {% endif %} - {% if errorList %} - {{ govukErrorSummary({ - titleText: "There is a problem", - errorList: errorList - }) }} - {% endif %} -
- Submit a new component -

Component code

-

You should only send the component code and not the full page of code. This ensures it's easy for others to understand and use. The code should not contain any personally identifiable or sensitive information.

- {{ govukRadios({ - name: "componentCodeAvailable", - fieldset: { - attributes: { - id: "component-code-available" - }, - legend: { - text: page.fields.componentCodeAvailable.label, - classes: "govuk-fieldset__legend--m" - } - }, - items: [ - { - value: "yes", - text: "Yes" - }, - { - value: "no", - text: "No" - } - ], - errorMessage: formErrors.componentCodeAvailable, - value: formData.componentCodeAvailable - }) }} - +{% extends "contributions.njk" %} -
- {{ govukButton({ - text: 'Continue', - preventDoubleClick: true - }) }} +{% block form %} + {% call form({ + action: submitUrl, + csrfToken: csrfToken + }) %} - -
+

You should only send the component code and not the full page of code. This ensures it's easy for others to understand and use. The code should not contain any personally identifiable or sensitive information.

+ {{ govukRadios({ + name: "componentCodeAvailable", + fieldset: { + attributes: { + id: "component-code-available" + }, + legend: { + text: page.fields.componentCodeAvailable.label, + classes: "govuk-fieldset__legend--m" + } + }, + items: [ + { + value: "yes", + text: "Yes" + }, + { + value: "no", + text: "No" + } + ], + errorMessage: formErrors.componentCodeAvailable, + value: formData.componentCodeAvailable + }) }} +
+ {{ govukButton({ + text: 'Continue', + preventDoubleClick: true + }) }} + {% endcall %} {% endblock %} diff --git a/views/community/pages/component-details.njk b/views/community/pages/component-details.njk index 97293a25a..b20761ab0 100644 --- a/views/community/pages/component-details.njk +++ b/views/community/pages/component-details.njk @@ -1,81 +1,55 @@ {% from "govuk/components/input/macro.njk" import govukInput %} {% from "govuk/components/character-count/macro.njk" import govukCharacterCount %} -{% from "govuk/components/button/macro.njk" import govukButton %} -{% from "govuk/components/error-summary/macro.njk" import govukErrorSummary %} -{% from "govuk/components/back-link/macro.njk" import govukBackLink %} -{% from "moj/components/alert/macro.njk" import mojAlert %} -{% extends "base.njk" %} -{% block content %} -
- {% if backLink %} - {{ govukBackLink({ - text: "Back", - href: backLink - }) }} - {% endif %} - {% if errorList %} - {{ govukErrorSummary({ - titleText: "There is a problem", - errorList: errorList - }) }} - {% endif %} - {% if sessionFlash.text %} - {{ mojAlert({ - variant: sessionFlash.type, - title: sessionFlash.title, - text: sessionFlash.text, - showTitleAsHeading: true, - role: "alert" - })}} - {% endif %} -
- Submit a new component -

Add details about the component

- {{ govukInput({ - label: { - text: page.fields.componentName.label, - classes: "govuk-label--m" - }, - id: "component-name", - name: "componentName", - errorMessage: formErrors.componentName, - value: formData.componentName - }) }} +{% extends "contributions.njk" %} - {{ govukCharacterCount({ - name: "componentOverview", - id: "component-overview", - maxwords: 250, - label: { - text: page.fields.componentOverview.label, - classes: "govuk-label--m" - }, - errorMessage: formErrors.componentOverview, - value: formData.componentOverview - }) }} +{% block form %} + {% call form({ + action: submitUrl, + csrfToken: csrfToken + }) %} - {{ govukCharacterCount({ - name: "howIsTheComponentUsed", - id: "how-is-the-component-used", - maxwords: 250, - label: { - text: page.fields.howIsTheComponentUsed.label, - classes: "govuk-label--m" - }, + {{ govukInput({ + label: { + text: page.fields.componentName.label, + classes: "govuk-label--m" + }, + id: "component-name", + name: "componentName", + errorMessage: formErrors.componentName, + value: formData.componentName + }) }} - errorMessage: formErrors.howIsTheComponentUsed, - value: formData.howIsTheComponentUsed - }) }} + {{ govukCharacterCount({ + name: "componentOverview", + id: "component-overview", + maxwords: 250, + label: { + text: page.fields.componentOverview.label, + classes: "govuk-label--m" + }, + errorMessage: formErrors.componentOverview, + value: formData.componentOverview + }) }} - + {{ govukCharacterCount({ + name: "howIsTheComponentUsed", + id: "how-is-the-component-used", + maxwords: 250, + label: { + text: page.fields.howIsTheComponentUsed.label, + classes: "govuk-label--m" + }, -
- {{ govukButton({ - text: 'Continue', - preventDoubleClick: true - }) }} - -
+ errorMessage: formErrors.howIsTheComponentUsed, + value: formData.howIsTheComponentUsed + }) }} +
+ {{ govukButton({ + text: 'Continue', + preventDoubleClick: true + }) }} +
+ {% endcall %} {% endblock %} diff --git a/views/community/pages/component-image.njk b/views/community/pages/component-image.njk index 7f641fdb4..fbdd3b52e 100644 --- a/views/community/pages/component-image.njk +++ b/views/community/pages/component-image.njk @@ -1,101 +1,76 @@ {% from "govuk/components/file-upload/macro.njk" import govukFileUpload %} -{% from "govuk/components/button/macro.njk" import govukButton %} -{% from "govuk/components/error-summary/macro.njk" import govukErrorSummary %} -{% from "govuk/components/back-link/macro.njk" import govukBackLink %} {% from "govuk/components/summary-list/macro.njk" import govukSummaryList %} {% from "govuk/components/tag/macro.njk" import govukTag %} -{% from "moj/components/alert/macro.njk" import mojAlert %} -{% extends "base.njk" %} +{% extends "contributions.njk" %} {% set uploadedFileName = formData.componentImage.originalname %} -{% block content %} -
- {% if backLink %} - {{ govukBackLink({ - text: "Back", - href: backLink - }) }} - {% endif %} - {% if errorList %} - {{ govukErrorSummary({ - titleText: "There is a problem", - errorList: errorList - }) }} - {% endif %} - {% if sessionFlash.message%} - {{ mojAlert({ - variant: sessionFlash.type, - title: sessionFlash.message, - role: "alert" - })}} - {% endif %} -
- Submit a new component -

Upload an image of the component

-

Ensure that the component is clear from the image. For example, remove other parts of the page and ensure it's on a white background.

+{% block form %} + {% call form({ + action: submitUrl, + csrfToken: csrfToken, + attributes: { + enctype: "multipart/form-data" + } + }) %} +

Ensure that the component is clear from the image. For example, remove other parts of the page and ensure it's on a white background.

+ {% if uploadedFileName %} + + {{ govukSummaryList({ + classes: 'govuk-summary-list--long-key', + rows: [ + { + key: { + text: "File" + }, + value: { + text: uploadedFileName + }, + actions: { + items: [ + { + href: changeUrl, + text: "Remove", + visuallyHiddenText: uploadedFileName + } + ] + } + } + ] + }) }} + {% else %} + {{ govukFileUpload({ + id: "component-image", + name: "componentImage", + label: { + text: page.fields.componentImage.label, + classes: "govuk-label--m" + }, + hint: { + text: page.fields.componentImage.hint + }, + attributes: { + accept: ".jpg, .jpeg, .bmp, .png, .tif, .tiff, .pdf" + }, + errorMessage: formErrors.componentImage + }) }} + {% endif %} +
{% if uploadedFileName %} - - {{ govukSummaryList({ - classes: 'govuk-summary-list--long-key', - rows: [ - { - key: { - text: "File" - }, - value: { - text: uploadedFileName - }, - actions: { - items: [ - { - href: changeUrl, - text: "Remove", - visuallyHiddenText: uploadedFileName - } - ] - } - } - ] - }) }} + {{ govukButton({ + text: 'Continue', + preventDoubleClick: true + }) }} {% else %} - {{ govukFileUpload({ - id: "component-image", - name: "componentImage", - label: { - text: page.fields.componentImage.label, - classes: "govuk-label--m" - }, - hint: { - text: page.fields.componentImage.hint - }, - attributes: { - accept: ".jpg, .jpeg, .bmp, .png, .tif, .tiff, .pdf" - }, - errorMessage: formErrors.componentImage + {{ govukButton({ + text: 'Upload', + preventDoubleClick: true }) }} {% endif %} - - - - -
- {% if uploadedFileName %} - {{ govukButton({ - text: 'Continue', - preventDoubleClick: true - }) }} - {% else %} - {{ govukButton({ - text: 'Upload', - preventDoubleClick: true - }) }} - {% endif %} -
- -
+
+ {% endcall %} {% endblock %} diff --git a/views/community/pages/confirmation.njk b/views/community/pages/confirmation.njk index 5b4f29639..3f09e3f3f 100644 --- a/views/community/pages/confirmation.njk +++ b/views/community/pages/confirmation.njk @@ -1,42 +1,46 @@ {% from "govuk/components/button/macro.njk" import govukButton %} {% from "govuk/components/notification-banner/macro.njk" import govukNotificationBanner %} -{% extends "base.njk" %} -{% block content %} -
-
+{% extends "base.njk" %} -
- Submit a new component +{% block beforeContent %} + +{% endblock %} - {% set html %} -

- You've submitted a component -

- {% endset %} +{% block content %} +
+ Submit a new component - {{ govukNotificationBanner({ - classes: "govuk-!-margin-top-5", - html: html, - titleText: "Component received", - type: "success" - }) }} + {% set html %} +

+ You've submitted a component +

+ {% endset %} -

What happens next

+ {{ govukNotificationBanner({ + classes: "govuk-!-margin-top-5", + html: html, + titleText: "Component received", + type: "success" + }) }} -

The Design System team reviews the component

-

The team will give the component a sense check.

-

If there are no queries, it'll be added to the Design System within 10 working days. It will have an experimental status, which means that it's early in development. You can read more about the MoJ Design System statuses.

-

You'll get an email when the component is live.

+

What happens next

-

Collaborating on the component

-

Each community component will have a Github discussion. This enables it to continue to develop through discussions and contributions. Later in 2025, people in MoJ and UK government will be able to use the documentation site to add to the component.

+

The Design System team reviews the component

+

The team will give the component a sense check.

+

If there are no queries, it'll be added to the Design System within 10 working days. It will have an experimental status, which means that it's early in development. You can read more about the MoJ Design System statuses.

+

You'll get an email when the component is live.

-

- Back to the Design System homepage -

-
+

Collaborating on the component

+

Each community component will have a Github discussion. This enables it to continue to develop through discussions and contributions. Later in 2025, people in MoJ and UK government will be able to use the documentation site to add to the component.

-
+

Back to the Design System homepage

{% endblock %} diff --git a/views/community/pages/email-check.njk b/views/community/pages/email-check.njk index 04c3cf5f2..ea0f4515e 100644 --- a/views/community/pages/email-check.njk +++ b/views/community/pages/email-check.njk @@ -1,20 +1,11 @@ -{% from "govuk/components/back-link/macro.njk" import govukBackLink %} {% from "govuk/components/details/macro.njk" import govukDetails %} -{% extends "base.njk" %} -{% block content %} -
- {% if backLink %} - {{ govukBackLink({ - text: "Back", - href: backLink - }) }} - {% endif %} - Submit a component -

{{ page.title }}

-

We've sent an email to: {{page.email}}

-

Select the link in the email to continue submitting a component. The link will open in a new window, so you can close this one.

- {% call govukDetails({ summaryText: "Problems with the email?" }) %} + +{% extends "contributions.njk" %} + +{% block form %} +

We've sent an email to: {{page.email}}

+

Select the link in the email to continue submitting a component. The link will open in a new window, so you can close this one.

+ {% call govukDetails({ summaryText: "Problems with the email?" }) %} We can send the email again or you can use a different email address. - {% endcall %} -
+ {% endcall %} {% endblock %} diff --git a/views/community/pages/email-not-allowed.njk b/views/community/pages/email-not-allowed.njk index fa6b7c426..c377a9632 100644 --- a/views/community/pages/email-not-allowed.njk +++ b/views/community/pages/email-not-allowed.njk @@ -1,16 +1,5 @@ -{% from "govuk/components/back-link/macro.njk" import govukBackLink %} -{% extends "base.njk" %} -{% block content %} -
- {% if backLink %} - {{ govukBackLink({ - text: "Back", - href: backLink - }) }} - {% endif %} - Submit a component -

{{ page.title }}

-

You need to work at MoJ to contribute to the MoJ Design System.

-

Go back to the MoJ Design System

-
+{% extends "contributions.njk" %} +{% block form %} +

You need to work at MoJ to contribute to the MoJ Design System.

+

Go back to the MoJ Design System

{% endblock %} diff --git a/views/community/pages/email-resend.njk b/views/community/pages/email-resend.njk index 145c9f310..05ea94376 100644 --- a/views/community/pages/email-resend.njk +++ b/views/community/pages/email-resend.njk @@ -1,36 +1,26 @@ -{% from "govuk/components/back-link/macro.njk" import govukBackLink %} -{% from "govuk/components/button/macro.njk" import govukButton %} -{% extends "base.njk" %} -{% block content %} -
- {% if backLink %} - {{ govukBackLink({ - text: "Back", - href: backLink - }) }} - {% endif %} -
- Submit a component -

{{ page.title }}

-
    +{% extends "contributions.njk" %} + +{% block form %} + {% call form({ + action: submitUrl, + csrfToken: csrfToken + }) %} +
    1. Check that you entered your email address correctly: {{page.email}}
    2. Check your junk or spam folder.
    3. Wait 5 minutes for it to arrive.

    If you’ve still not received the verification email after step 2 you can either:

    -
      + - - -
      - {{ govukButton({ - text: 'Resend verification email' - }) }} -
      +
      + {{ govukButton({ + text: 'Resend verification email' + }) }} +

      If none of this works, contact us -

      -
+ {% endcall %} {% endblock %} diff --git a/views/community/pages/email.njk b/views/community/pages/email.njk index cc5f7f9ba..b2fa6070f 100644 --- a/views/community/pages/email.njk +++ b/views/community/pages/email.njk @@ -1,60 +1,37 @@ {% from "govuk/components/input/macro.njk" import govukInput %} -{% from "govuk/components/button/macro.njk" import govukButton %} -{% from "govuk/components/error-summary/macro.njk" import govukErrorSummary %} -{% from "govuk/components/back-link/macro.njk" import govukBackLink %} -{% from "moj/components/alert/macro.njk" import mojAlert %} -{% extends "base.njk" %} -{% block content %} -
- {% if backLink %} - {{ govukBackLink({ - text: "Back", - href: backLink - }) }} - {% endif %} - {% if errorList %} - {{ govukErrorSummary({ - titleText: "There is a problem", - errorList: errorList - }) }} - {% endif %} - {% if sessionFlash.text %} - {{ mojAlert({ - variant: sessionFlash.type, - title: sessionFlash.title, - text: sessionFlash.text, - showTitleAsHeading: true, - role: "alert" - })}} - {% endif %} -
- Submit a component -

{{page.title}}

+ +{% extends "contributions.njk" %} + +{% block form %} + {% call form({ + action: submitUrl, + csrfToken: csrfToken + }) %} +

You need to work for MoJ to submit a new component to the Design System.

- {{ govukInput({ - id: "email-address", - name: "emailAddress", - hint: { - text: page.fields.emailAddress.hint - }, - errorMessage: formErrors.emailAddress, - value: formData.emailAddress, - attributes: { - 'aria-labelledby': 'page-title' - }, - formGroup: { - classes: 'govuk-!-margin-top-0' - } - }) }} -

You’ll be sent an email with a verification link.

- + {{ govukInput({ + id: "email-address", + name: "emailAddress", + hint: { + text: page.fields.emailAddress.hint + }, + errorMessage: formErrors.emailAddress, + value: formData.emailAddress, + attributes: { + 'aria-labelledby': 'page-title' + }, + formGroup: { + classes: 'govuk-!-margin-top-0' + } + }) }} -
- {{ govukButton({ - text: 'Continue' - }) }} -
-
-
+

You’ll be sent an email with a verification link.

+ +
+ {{ govukButton({ + text: 'Continue' + }) }} +
+ {% endcall %} {% endblock %} diff --git a/views/community/pages/figma-link.njk b/views/community/pages/figma-link.njk index 3670e7c38..72e12e338 100644 --- a/views/community/pages/figma-link.njk +++ b/views/community/pages/figma-link.njk @@ -1,64 +1,49 @@ {% from "govuk/components/input/macro.njk" import govukInput %} {% from "govuk/components/character-count/macro.njk" import govukCharacterCount %} -{% from "govuk/components/button/macro.njk" import govukButton %} -{% from "govuk/components/error-summary/macro.njk" import govukErrorSummary %} -{% from "govuk/components/back-link/macro.njk" import govukBackLink %} -{% extends "base.njk" %} -{% block content %} -
- {% if backLink %} - {{ govukBackLink({ - text: "Back", - href: backLink - }) }} - {% endif %} - {% if errorList %} - {{ govukErrorSummary({ - titleText: "There is a problem", - errorList: errorList - }) }} - {% endif %} -
- Submit a new component -

Figma design

- {{ govukInput({ - hint: { - text: page.fields.figmaLink.hint - }, - label: { - text: page.fields.figmaLink.label, - classes: "govuk-label--m" - }, - id: "figma-link", - name: "figmaLink", - errorMessage: formErrors.figmaLink, - value: formData.figmaLink, - autocapitalize: false - }) }} - {{ govukCharacterCount({ - name: "figmaLinkAdditionalInformation", - id: "figmaLink-additional-information", - maxwords: 250, - hint: { - text: page.fields.figmaLinkAdditionalInformation.hint - }, - label: { - text: page.fields.figmaLinkAdditionalInformation.label, - classes: "govuk-label--m" - }, - errorMessage: formErrors.figmaLinkAdditionalInformation, - value: formData.figmaLinkAdditionalInformation - }) }} +{% extends "contributions.njk" %} - +{% block form %} + {% call form({ + action: submitUrl, + csrfToken: csrfToken + }) %} -
- {{ govukButton({ - text: 'Continue', - preventDoubleClick: true - }) }} -
-
-
+ {{ govukInput({ + hint: { + text: page.fields.figmaLink.hint + }, + label: { + text: page.fields.figmaLink.label, + classes: "govuk-label--m" + }, + id: "figma-link", + name: "figmaLink", + errorMessage: formErrors.figmaLink, + value: formData.figmaLink, + autocapitalize: false + }) }} + + {{ govukCharacterCount({ + name: "figmaLinkAdditionalInformation", + id: "figmaLink-additional-information", + maxwords: 250, + hint: { + text: page.fields.figmaLinkAdditionalInformation.hint + }, + label: { + text: page.fields.figmaLinkAdditionalInformation.label, + classes: "govuk-label--m" + }, + errorMessage: formErrors.figmaLinkAdditionalInformation, + value: formData.figmaLinkAdditionalInformation + }) }} + +
+ {{ govukButton({ + text: 'Continue', + preventDoubleClick: true + }) }} +
+ {% endcall %} {% endblock %} diff --git a/views/community/pages/figma.njk b/views/community/pages/figma.njk index 44649210d..43579076b 100644 --- a/views/community/pages/figma.njk +++ b/views/community/pages/figma.njk @@ -1,58 +1,43 @@ {% from "govuk/components/radios/macro.njk" import govukRadios %} -{% from "govuk/components/button/macro.njk" import govukButton %} -{% from "govuk/components/error-summary/macro.njk" import govukErrorSummary %} -{% from "govuk/components/back-link/macro.njk" import govukBackLink %} -{% extends "base.njk" %} -{% block content %} -
- {% if backLink %} - {{ govukBackLink({ - text: "Back", - href: backLink - }) }} - {% endif %} - {% if errorList %} - {{ govukErrorSummary({ - titleText: "There is a problem", - errorList: errorList - }) }} - {% endif %} -
- Submit a new component -

Figma design

- {{ govukRadios({ - name: "figmaUrl", - fieldset: { - attributes: { - id: "figma-url" - }, - legend: { - text: page.fields.figmaUrl.label, - classes: "govuk-fieldset__legend--m" - } - }, - items: [ - { - value: "yes", - text: "Yes" - }, - { - value: "no", - text: "No" - } - ], - errorMessage: formErrors.figmaUrl, - value: formData.figmaUrl - }) }} - +{% extends "contributions.njk" %} -
- {{ govukButton({ - text: 'Continue', - preventDoubleClick: true - }) }} +{% block form %} + {% call form({ + action: submitUrl, + csrfToken: csrfToken + }) %} - -
+ {{ govukRadios({ + name: "figmaUrl", + fieldset: { + attributes: { + id: "figma-url" + }, + legend: { + text: page.fields.figmaUrl.label, + classes: "govuk-fieldset__legend--m" + } + }, + items: [ + { + value: "yes", + text: "Yes" + }, + { + value: "no", + text: "No" + } + ], + errorMessage: formErrors.figmaUrl, + value: formData.figmaUrl + }) }} + +
+ {{ govukButton({ + text: 'Continue', + preventDoubleClick: true + }) }} +
+ {% endcall %} {% endblock %} diff --git a/views/community/pages/prototype-url.njk b/views/community/pages/prototype-url.njk deleted file mode 100644 index de5e8f83e..000000000 --- a/views/community/pages/prototype-url.njk +++ /dev/null @@ -1,77 +0,0 @@ -{% from "govuk/components/input/macro.njk" import govukInput %} -{% from "govuk/components/character-count/macro.njk" import govukCharacterCount %} -{% from "govuk/components/button/macro.njk" import govukButton %} -{% from "govuk/components/error-summary/macro.njk" import govukErrorSummary %} -{% from "govuk/components/back-link/macro.njk" import govukBackLink %} -{% extends "base.njk" %} -{% block content %} -
- {% if backLink %} - {{ govukBackLink({ - text: "Back", - href: backLink - }) }} - {% endif %} - {% if errorList %} - {{ govukErrorSummary({ - titleText: "There is a problem", - errorList: errorList - }) }} - {% endif %} -
- Submit a new component -

Prototype designs

-

Before sharing a prototype you need to:

-
    -
  • make the link shareable
  • -
  • remove any personally identifiable or sensitive information
  • -
- - {{ govukInput({ - label: { - text: page.fields.prototypeUrl.label, - classes: "govuk-label--m" - }, - id: "prototype-url", - name: "prototypeUrl", - errorMessage: formErrors.prototypeUrl, - value: formData.prototypeUrl, - autocapitalize: false, - hint: { - text: page.fields.prototypeUrl.hint - } - }) }} - - {{ govukCharacterCount({ - name: "prototypeUrlAdditionalInformation", - id: "prototypeUrl-additional-information", - maxwords: 250, - hint: { - text: page.fields.prototypeUrlAdditionalInformation.hint - }, - label: { - text: page.fields.prototypeUrlAdditionalInformation.label, - classes: "govuk-label--m" - }, - errorMessage: formErrors.prototypeUrlAdditionalInformation, - value: formData.prototypeUrlAdditionalInformation - }) }} - - - -
- {{ govukButton({ - text: 'Continue', - preventDoubleClick: true - }) }} - {% if showAddAnother %} - {{ govukButton({ - text: "Add another link", - classes: "govuk-button--secondary", - name: "addAnother" - }) }} - {% endif %} -
-
-
-{% endblock %} diff --git a/views/community/pages/prototype.njk b/views/community/pages/prototype.njk deleted file mode 100644 index 5137c56b2..000000000 --- a/views/community/pages/prototype.njk +++ /dev/null @@ -1,64 +0,0 @@ -{% from "govuk/components/radios/macro.njk" import govukRadios %} -{% from "govuk/components/button/macro.njk" import govukButton %} -{% from "govuk/components/error-summary/macro.njk" import govukErrorSummary %} -{% from "govuk/components/back-link/macro.njk" import govukBackLink %} -{% extends "base.njk" %} -{% block content %} -
- {% if backLink %} - {{ govukBackLink({ - text: "Back", - href: backLink - }) }} - {% endif %} - {% if errorList %} - {{ govukErrorSummary({ - titleText: "There is a problem", - errorList: errorList - }) }} - {% endif %} -
- Submit a new component -

Add a prototype

-

A prototype showing the component helps the Design System team to review the component.

- - {{ govukRadios({ - name: "componentPrototypeUrl", - hint: { - text: page.fields.componentPrototypeUrl.hint - }, - fieldset: { - attributes: { - id: "component-prototype-url" - }, - legend: { - text: page.fields.componentPrototypeUrl.label, - isPageHeading: true, - classes: "govuk-fieldset__legend--m" - } - }, - items: [ - { - value: "yes", - text: "Yes" - }, - { - value: "no", - text: "No" - } - ], - errorMessage: formErrors.componentPrototypeUrl, - value: formData.componentPrototypeUrl - }) }} - - - -
- {{ govukButton({ - text: 'Continue', - preventDoubleClick: true - }) }} - - -
-{% endblock %} diff --git a/views/community/pages/remove.njk b/views/community/pages/remove.njk index d5e6b172e..4a4ece462 100644 --- a/views/community/pages/remove.njk +++ b/views/community/pages/remove.njk @@ -1,37 +1,36 @@ -{% from "govuk/components/button/macro.njk" import govukButton %} {% from "govuk/components/summary-list/macro.njk" import govukSummaryList %} -{% from "govuk/components/back-link/macro.njk" import govukBackLink %} -{% extends "base.njk" %} -{% block content %} -
- {% if backLink %} - {{ govukBackLink({ - text: "Back", - href: backLink - }) }} - {% endif %} -
- Submit a new component -

Are you sure you want to remove this information?

- {{ govukSummaryList({ - rows: summaryRows - }) }} +{% extends "contributions.njk" %} + +{% block pageTitle %} + {{ pageTitle({ + caption: 'Submit a new component', + title: "Are you sure you want to remove this information?" + })}} +{% endblock %} - +{% block form %} + {% call form({ + action: submitUrl, + csrfToken: csrfToken + }) %} -
- {{ govukButton({ - text: 'Delete answers', - preventDoubleClick: true, - classes: "govuk-button--warning" - }) }} + {{ govukSummaryList({ + rows: summaryRows + }) }} - {{ govukButton({ - text: "Cancel", - classes: "govuk-button--secondary", - href: backLink - }) }} - -
+
+ {{ govukButton({ + text: 'Delete answers', + preventDoubleClick: true, + classes: "govuk-button--warning" + }) }} + + {{ govukButton({ + text: "Cancel", + classes: "govuk-button--secondary", + href: backLink + }) }} +
+ {% endcall %} {% endblock %} diff --git a/views/community/pages/start.njk b/views/community/pages/start.njk index 2d5c209af..0c1223801 100644 --- a/views/community/pages/start.njk +++ b/views/community/pages/start.njk @@ -1,31 +1,41 @@ {% from "govuk/components/button/macro.njk" import govukButton %} +{% from "macros/page-title.njk" import pageTitle %} +{% from "macros/form.njk" import form %} + {% extends "base.njk" %} +{% block beforeContent %} + +{% endblock %} {% block content %}
- Submit a new component -

Before you start

- -

You can now add a component to the MoJ Design System. You might want to do this to: + {{ pageTitle({ + caption: 'Submit a new component', + title: 'Before you start' + })}} +

You can now add a component to the MoJ Design System. You might want to do this to:

-
    - -
  • make it easier for teams to find your work
  • -
  • get contributions from other people to help the component develop
  • -
  • help your team meet the Government Design Principles
  • -
-

+
    +
  • make it easier for teams to find your work
  • +
  • get contributions from other people to help the component develop
  • +
  • help your team meet the Government Design Principles
  • +
-
-Later in 2025 you'll also be able to add a pattern or page to the MoJ Design System. -
+
Later in 2025 you'll also be able to add a pattern or page to the MoJ Design System.
-

You can add a component that has not been thoroughly developed or tested. If you have an image and you can briefly describe it, it’s ready to be added.

+

You can add a component that has not been thoroughly developed or tested. If you have an image and you can briefly describe it, it’s ready to be added.

What you'll need

-

To submit a component to the Design System you'll need: +

To submit a component to the Design System you'll need:

  • the component name
  • @@ -40,17 +50,14 @@ Later in 2025 you'll also be able to add a pattern or page to the MoJ Design Sys

    After you submit a component

    -

    The MoJ Design System team will briefly check the component and then it'll be live within 10 working days. People may add comments to the component's Github discussion. -

    - -
    - +

    The MoJ Design System team will briefly check the component and then it'll be live within 10 working days. People may add comments to the component's Github discussion.

    -
    - {{ govukButton({ - text: 'Continue' - }) }} -
    -
    + {% call form({csrfToken: csrfToken}) %} +
    + {{ govukButton({ + text: 'Continue' + }) }} +
    + {% endcall %}
{% endblock %} diff --git a/views/community/pages/your-details.njk b/views/community/pages/your-details.njk index 26b595d6c..c37c2b8d9 100644 --- a/views/community/pages/your-details.njk +++ b/views/community/pages/your-details.njk @@ -1,9 +1,7 @@ {% from "govuk/components/input/macro.njk" import govukInput %} {% from "govuk/components/checkboxes/macro.njk" import govukCheckboxes %} -{% from "govuk/components/button/macro.njk" import govukButton %} -{% from "govuk/components/error-summary/macro.njk" import govukErrorSummary %} -{% from "govuk/components/back-link/macro.njk" import govukBackLink %} -{% extends "base.njk" %} + +{% extends "contributions.njk" %} {% set shareYourDetails = [] %} @@ -18,96 +16,82 @@ {% set addNameToComponentPage = "addNameToComponentPage" in shareYourDetails %} {% set addTeamNameToComponentPage = "addTeamNameToComponentPage" in shareYourDetails %} -{% block content %} +{% block form %} + {% call form({ + action: submitUrl, + csrfToken: csrfToken + }) %} -
- {% if backLink %} - {{ govukBackLink({ - text: "Back", - href: backLink - }) }} - {% endif %} - {% if errorList %} - {{ govukErrorSummary({ - titleText: "There is a problem", - errorList: errorList - }) }} - {% endif %} -
- Submit a new component -

Your details

-

Add your details to enable the MoJ Design System team to let you know when the component is live. They might also need to contact you with questions about the component.

-

You can request for your details to be added to the component page. This is to show that you added it to the Design System.

- {{ govukInput({ - label: { - text: page.fields.fullName.label, - classes: "govuk-label--m" - }, - id: "full-name", - name: "fullName", - errorMessage: formErrors.fullName, - value: formData.fullName - }) }} +

Add your details to enable the MoJ Design System team to let you know when the component is live. They might also need to contact you with questions about the component.

+

You can request for your details to be added to the component page. This is to show that you added it to the Design System.

- {{ govukInput({ - label: { - text: page.fields.emailAddress.label, - classes: "govuk-label--m" - }, - id: "email-address", - name: "emailAddress", - errorMessage: formErrors.emailAddress, - value: formData.emailAddress - }) }} + {{ govukInput({ + label: { + text: page.fields.fullName.label, + classes: "govuk-label--m" + }, + id: "full-name", + name: "fullName", + errorMessage: formErrors.fullName, + value: formData.fullName + }) }} - {{ govukInput({ + {{ govukInput({ label: { - text: page.fields.teamName.label, - classes: "govuk-label--m" + text: page.fields.emailAddress.label, + classes: "govuk-label--m" }, - id: "team-name", - name: "teamName", - errorMessage: formErrors.teamName, - value: formData.teamName - }) }} + id: "email-address", + name: "emailAddress", + errorMessage: formErrors.emailAddress, + value: formData.emailAddress + }) }} - + {{ govukInput({ + label: { + text: page.fields.teamName.label, + classes: "govuk-label--m" + }, + id: "team-name", + name: "teamName", + errorMessage: formErrors.teamName, + value: formData.teamName + }) }} - {{ govukCheckboxes({ - name: "shareYourDetails", - id: 'share-your-details', - fieldset: { - legend: { - text: page.fields.shareYourDetails.label, - classes: "govuk-fieldset__legend--m" - } - }, - hint: { - text: page.fields.shareYourDetails.hint - }, - items: [ - { - value: "addNameToComponentPage", - text: "Add my name to the component page", - checked: addNameToComponentPage - }, - { - value: "addTeamNameToComponentPage", - text: "Add my team name to the component page", - checked: addTeamNameToComponentPage - } - ], - errorMessage: formErrors.shareYourDetails, - value: formData.shareYourDetails - }) }} + - + {{ govukCheckboxes({ + name: "shareYourDetails", + id: 'share-your-details', + fieldset: { + legend: { + text: page.fields.shareYourDetails.label, + classes: "govuk-fieldset__legend--m" + } + }, + hint: { + text: page.fields.shareYourDetails.hint + }, + items: [ + { + value: "addNameToComponentPage", + text: "Add my name to the component page", + checked: addNameToComponentPage + }, + { + value: "addTeamNameToComponentPage", + text: "Add my team name to the component page", + checked: addTeamNameToComponentPage + } + ], + errorMessage: formErrors.shareYourDetails, + value: formData.shareYourDetails + }) }} -
- {{ govukButton({ - text: 'Continue' - }) }} -
-
-
+
+ {{ govukButton({ + text: 'Continue' + }) }} +
+ {% endcall %} {% endblock %} From e14505e7cc8d718f4e68153e22c84e8b7f82333d Mon Sep 17 00:00:00 2001 From: Chris Pymm Date: Mon, 7 Jul 2025 11:13:08 +0100 Subject: [PATCH 15/18] docs: tweaks to email verification screens --- app.js | 2 +- docs/stylesheets/components/_layout.scss | 15 ++++++++++++++- middleware/component-session.js | 20 +++++++++++++------- views/common/contributions.njk | 2 +- views/community/pages/email-check.njk | 2 +- views/community/pages/email-resend.njk | 4 ++-- views/community/pages/start.njk | 2 +- 7 files changed, 33 insertions(+), 14 deletions(-) diff --git a/app.js b/app.js index 38f5b873f..501bb1774 100644 --- a/app.js +++ b/app.js @@ -67,7 +67,7 @@ const sessionOptions = { secret: SESSION_SECRET, resave: true, saveUninitialized: true, - cookie: { secure: !isDev, maxAge: 60 * 1000 * 3 /* 24 * 60 * 60 * 1000 */ } + cookie: { secure: !isDev, maxAge: 24 * 60 * 60 * 1000 } } if (REDIS_URL) { diff --git a/docs/stylesheets/components/_layout.scss b/docs/stylesheets/components/_layout.scss index cb3869b0b..dbf760206 100644 --- a/docs/stylesheets/components/_layout.scss +++ b/docs/stylesheets/components/_layout.scss @@ -40,6 +40,19 @@ @include govuk-responsive-padding(6); @include govuk-media-query(app.$breakpoint) { - @include govuk-responsive-padding(9); + @include govuk-responsive-padding(9, 'top'); + @include govuk-responsive-padding(9, 'right'); + @include govuk-responsive-padding(9, 'left'); + } +} + +.app-layout__back-link { + position: absolute; + @include govuk-responsive-padding(6, 'right'); + @include govuk-responsive-padding(6, 'left'); + + @include govuk-media-query(app.$breakpoint) { + @include govuk-responsive-padding(9, 'right'); + @include govuk-responsive-padding(9, 'left'); } } diff --git a/middleware/component-session.js b/middleware/component-session.js index 08f35fd1b..a4225dad9 100644 --- a/middleware/component-session.js +++ b/middleware/component-session.js @@ -43,15 +43,15 @@ const getPageData = (req) => { const checkEmailDomain = (req, res, next) => { let allowed = false const email = req?.body?.emailAddress - console.log({email}) + console.log({ email }) if (email) { const domain = email.split('@').at(-1) - console.log({domain}) + console.log({ domain }) if (allowedDomains.includes(domain)) { allowed = true } } - console.log({allowed}) + console.log({ allowed }) req.emailDomainAllowed = allowed next() } @@ -104,9 +104,15 @@ const clearSkippedPageData = (req, res, next) => { // Delete page and subpage data for (const sessionPage of Object.keys(req.session)) { if ( - !['started', 'cookie', 'csrfToken', 'checkYourAnswers', 'emailToken','emailDomainAllowed','verified'].includes( - sessionPage - ) + ![ + 'started', + 'cookie', + 'csrfToken', + 'checkYourAnswers', + 'emailToken', + 'emailDomainAllowed', + 'verified' + ].includes(sessionPage) ) { console.log(sessionPage) const parentPage = `/${sessionPage.split('/')[1]}` @@ -162,7 +168,7 @@ const validateFormDataFileUpload = (err, req, res, next) => { const validateFormData = (req, res, next) => { console.log('validate form data') - const schemaName = req.url.split('/')[1] + const schemaName = req.url.split('/').at(1).split('?').at(0) const schema = require(`../schema/${schemaName}.schema`) const body = extractBody(req?.url, { ...req.body }) delete body._csrf diff --git a/views/common/contributions.njk b/views/common/contributions.njk index de170c33d..29d5d59d7 100644 --- a/views/common/contributions.njk +++ b/views/common/contributions.njk @@ -37,7 +37,7 @@ {% block pageTitle %} {{ pageTitle({ - caption: 'Submit a new component', + caption: 'Submit a component', title: page.title })}} {% endblock %} diff --git a/views/community/pages/email-check.njk b/views/community/pages/email-check.njk index ea0f4515e..ef77f104f 100644 --- a/views/community/pages/email-check.njk +++ b/views/community/pages/email-check.njk @@ -6,6 +6,6 @@

We've sent an email to: {{page.email}}

Select the link in the email to continue submitting a component. The link will open in a new window, so you can close this one.

{% call govukDetails({ summaryText: "Problems with the email?" }) %} - We can send the email again or you can use a different email address. + We can send the email again or you can use a different email address. {% endcall %} {% endblock %} diff --git a/views/community/pages/email-resend.njk b/views/community/pages/email-resend.njk index 05ea94376..3b5753eb9 100644 --- a/views/community/pages/email-resend.njk +++ b/views/community/pages/email-resend.njk @@ -13,7 +13,7 @@

If you’ve still not received the verification email after step 2 you can either:

@@ -21,6 +21,6 @@ text: 'Resend verification email' }) }}
-

If none of this works, contact us +

If none of this works, contact us {% endcall %} {% endblock %} diff --git a/views/community/pages/start.njk b/views/community/pages/start.njk index 0c1223801..d4adc6ef9 100644 --- a/views/community/pages/start.njk +++ b/views/community/pages/start.njk @@ -26,7 +26,7 @@

Later in 2025 you'll also be able to add a pattern or page to the MoJ Design System.
From 7f55863805de009bcef7a3e07f5f5222d04cc13c Mon Sep 17 00:00:00 2001 From: Chris Pymm Date: Mon, 7 Jul 2025 13:30:14 +0100 Subject: [PATCH 16/18] docs: merge changes from main --- app.js | 4 ---- views/common/base.njk | 2 +- views/common/partials/side-navigation.njk | 7 ------- 3 files changed, 1 insertion(+), 12 deletions(-) diff --git a/app.js b/app.js index 501bb1774..13015579b 100644 --- a/app.js +++ b/app.js @@ -29,13 +29,9 @@ const RedisStore = require('connect-redis')(session) const helmet = require('helmet') const nunjucks = require('nunjucks') const { xss } = require('express-xss-sanitizer') -const ApplicationError = require('./helpers/application-error') const rev = require('./filters/rev') -const rev = require('./filters/rev') - -const { APP_PORT, ENV, REDIS_URL, SESSION_SECRET } = require('./config') const addComponentRoutes = require('./routes/add-component') const app = express() diff --git a/views/common/base.njk b/views/common/base.njk index 6f9c8d183..59fe855f6 100644 --- a/views/common/base.njk +++ b/views/common/base.njk @@ -38,7 +38,7 @@ -
+
{% block beforeContent %}{% endblock %}
{% block content %} diff --git a/views/common/partials/side-navigation.njk b/views/common/partials/side-navigation.njk index 0bbc1c3b4..5f2af32ae 100644 --- a/views/common/partials/side-navigation.njk +++ b/views/common/partials/side-navigation.njk @@ -406,13 +406,6 @@ -
  • - Setting up coded prototypes
  • - - - - -
  • Deploying coded prototypes
  • From 992ba048909eeca6b243ddeef8a43e4531cafbd4 Mon Sep 17 00:00:00 2001 From: Helen Nickols <94117270+helennickols@users.noreply.github.com> Date: Wed, 9 Jul 2025 11:36:53 +0100 Subject: [PATCH 17/18] docs: email changes (#1568) Updates to the contributions start page --- views/community/pages/start.njk | 10 ++++------ 1 file changed, 4 insertions(+), 6 deletions(-) diff --git a/views/community/pages/start.njk b/views/community/pages/start.njk index d4adc6ef9..1dbcf94df 100644 --- a/views/community/pages/start.njk +++ b/views/community/pages/start.njk @@ -21,16 +21,14 @@ caption: 'Submit a new component', title: 'Before you start' })}} -

    You can now add a component to the MoJ Design System. You might want to do this to:

    +

    Anyone at MoJ can now add a component to the MoJ Design System. You might want to do this to:

    • make it easier for teams to find your work
    • -
    • get contributions from other people to help the component develop
    • -
    • help your team meet the Government Design Principles
    • +
    • get contributions from other people across government to help the component develop
    • +
    • help your team meet the Government Design Principles
    -
    Later in 2025 you'll also be able to add a pattern or page to the MoJ Design System.
    -

    You can add a component that has not been thoroughly developed or tested. If you have an image and you can briefly describe it, it’s ready to be added.

    What you'll need

    @@ -46,7 +44,7 @@

    This helps people to understand what the component is and how to use it.

    -

    You’ll be asked for your details. This is so that you can be contacted when the component is live or if there queries. You can ask for your name or team name to be added to the documentation, but this is optional.

    +

    You’ll be asked for your email address to verify that you work for MoJ. We'll also use this to contact you when the component is live, or if there queries. You can ask for your name or team name to be added to the documentation, but this is optional.

    After you submit a component

    From e9449b31280d65330aba25c008cc946a0cc334b6 Mon Sep 17 00:00:00 2001 From: Chris Pymm Date: Thu, 10 Jul 2025 14:16:13 +0100 Subject: [PATCH 18/18] Experimental component page (#1569) * docs: wip - submitted component markdown generation * docs: generate correctly formatted component guidance page * docs: fix linebreaks in accessibility tab for generated component page * docs: refactor processing to exclude personal info from component page --------- Co-authored-by: Murray Lippiatt --- docs/_data/statusInfo.json | 2 +- .../partials/building-block-header.njk | 4 +- .../layouts/partials/page-header.njk | 18 +- docs/_includes/macros/status-label/macro.njk | 4 +- .../macros/status-label/template.njk | 34 +- helpers/mockSessionData/sessionData.js | 237 +++++++++++++- middleware/generate-documentation.js | 290 ++++++++++-------- middleware/process-subission-data.js | 86 ++++-- routes/add-component.js | 120 +++++--- views/community/pages/check-your-answers.njk | 9 +- 10 files changed, 566 insertions(+), 238 deletions(-) diff --git a/docs/_data/statusInfo.json b/docs/_data/statusInfo.json index 705bbfd68..1c6bf8d23 100644 --- a/docs/_data/statusInfo.json +++ b/docs/_data/statusInfo.json @@ -12,7 +12,7 @@ "Experimental": { "link": "How to use ‘experimental’ {{ types }}", "linkContent": "Anyone can add an ‘experimental’ status {{ type }} to the MoJ Design System. They’re early in development and can be used as a starting point.", - "statusMessage": "Created" + "statusMessage": "Added" }, "Archived": { "link": "About ‘archived’ {{ types }}", diff --git a/docs/_includes/layouts/partials/building-block-header.njk b/docs/_includes/layouts/partials/building-block-header.njk index 941eac92a..24a29982a 100644 --- a/docs/_includes/layouts/partials/building-block-header.njk +++ b/docs/_includes/layouts/partials/building-block-header.njk @@ -11,8 +11,8 @@ status, statusInfo[status], statusDate, - creatorName, - creatorTeam + contributorName, + contributorTeam ) if statusInfo[status] }}
    diff --git a/docs/_includes/layouts/partials/page-header.njk b/docs/_includes/layouts/partials/page-header.njk index 7aa9a6427..96a6cb9ea 100644 --- a/docs/_includes/layouts/partials/page-header.njk +++ b/docs/_includes/layouts/partials/page-header.njk @@ -16,16 +16,16 @@ text: title, caption: subsection, statusTag: { text: status } if status - }) }} + }) }} - {{ statusLabel( - type, - status, - statusInfo[status], - statusDate, - creatorName, - creatorTeam - ) if statusInfo[status] }} + {{ statusLabel({ + type: type, + status: status, + statusData: statusInfo[status], + statusDate: statusDate, + contributorName: contributorName, + contributorTeam: contributorTeam + }) if statusInfo[status] }} {% if lede %} {{ lede | safe }} diff --git a/docs/_includes/macros/status-label/macro.njk b/docs/_includes/macros/status-label/macro.njk index 009397720..fe403603c 100644 --- a/docs/_includes/macros/status-label/macro.njk +++ b/docs/_includes/macros/status-label/macro.njk @@ -1,3 +1,3 @@ -{% macro statusLabel(type, status, statusData, statusDate, creatorName, creatorTeam) %} - {%- include "./template.njk" -%} +{% macro statusLabel(params) %} + {% include "./template.njk" %} {% endmacro %} diff --git a/docs/_includes/macros/status-label/template.njk b/docs/_includes/macros/status-label/template.njk index 75e19d3c3..a291a138f 100644 --- a/docs/_includes/macros/status-label/template.njk +++ b/docs/_includes/macros/status-label/template.njk @@ -1,36 +1,30 @@ {% from "govuk/components/details/macro.njk" import govukDetails %} - - {% set statusAttribution -%} - {%- if status == "Experimental" %} - by {{ creatorName | default("someone") }} in {{ creatorTeam | default("the community") }} - {%- endif %} - {%- if statusDate %} - in {{ statusDate }} + {%- if params.status == "Experimental" %} + {%- if params.contributorTeam %} + by {{ params.contributorName | default("someone") }} in {{ params.contributorTeam }} + {%- elif params.contributorName %} + by {{ contributorName }} + {% endif %} + {% endif %} + {%- if params.statusDate %} + in {{ params.statusDate }} {%- endif %}. {%- endset %} -{% set typeSingular = type -%} -{% set typePlural = type + "s" -%} +{% set typeSingular = params.type -%} +{% set typePlural = params.type + "s" -%} {% set linkText -%} - {{ statusData.link | renderString({ + {{ params.statusData.link | renderString({ types: typePlural, type: typeSingular }) | upperFirst }} {%- endset %} {% set linkHtml -%} - {{ statusData.linkContent | renderString({ + {{ params.statusData.linkContent | renderString({ types: typePlural, type: typeSingular }) | upperFirst }} @@ -38,7 +32,7 @@ Arguments to be passed: {%- endset %}

    - {{ statusData.statusMessage }} {{ statusAttribution }} + {{ params.statusData.statusMessage }} {{ statusAttribution }}

    {{ govukDetails({ diff --git a/helpers/mockSessionData/sessionData.js b/helpers/mockSessionData/sessionData.js index 28f54acd0..c0f04eb1a 100644 --- a/helpers/mockSessionData/sessionData.js +++ b/helpers/mockSessionData/sessionData.js @@ -2,9 +2,9 @@ module.exports = { '/component-details': { componentName: 'Duis', componentOverview: - 'Lorem ipsum dolor sit amet, consectetur adipiscing elit. Aliquam porttitor, turpis eu congue semper, felis purus blandit purus, eu finibus dui orci et augue. Vestibulum nec dignissim ante. Sed vehicula sagittis nunc, sed iaculis lorem. Quisque quis lorem non lorem ornare venenatis. Morbi luctus, enim et tincidunt pellentesque, ante justo venenatis nisl, rhoncus lobortis quam nunc in lectus. Nam consectetur sapien sem, quis laoreet mi dapibus sit amet. Nullam consectetur erat ut diam luctus posuere. Duis vulputate turpis vitae magna commodo pharetra. Sed varius pulvinar sapien, nec tempus ante pulvinar sit amet. Sed mauris tortor, dictum tristique aliquam vestibulum, porta et justo.\r\n\r\n', + 'Lorem ipsum dolor sit amet, consectetur adipiscing elit.\r\n\r\nAliquam porttitor, turpis eu congue semper, felis purus blandit purus, eu finibus dui orci et augue. Vestibulum nec dignissim ante.\r\n\r\n Sed vehicula sagittis nunc, sed iaculis lorem. Quisque quis lorem non lorem ornare venenatis. Morbi luctus, enim et tincidunt pellentesque, ante justo venenatis nisl, rhoncus lobortis quam nunc in lectus. Nam consectetur sapien sem, quis laoreet mi dapibus sit amet. Nullam consectetur erat ut diam luctus posuere. Duis vulputate turpis vitae magna commodo pharetra. Sed varius pulvinar sapien, nec tempus ante pulvinar sit amet. Sed mauris tortor, dictum tristique aliquam vestibulum, porta et justo.\r\n\r\n', howIsTheComponentUsed: - 'Lorem ipsum dolor sit amet, consectetur adipiscing elit. Aliquam porttitor, turpis eu congue semper, felis purus blandit purus, eu finibus dui orci et augue. Vestibulum nec dignissim ante. Sed vehicula sagittis nunc, sed iaculis lorem. Quisque quis lorem non lorem ornare venenatis. Morbi luctus, enim et tincidunt pellentesque, ante justo venenatis nisl, rhoncus lobortis quam nunc in lectus. Nam consectetur sapien sem, quis laoreet mi dapibus sit amet. Nullam consectetur erat ut diam luctus posuere. Duis vulputate turpis vitae magna commodo pharetra. Sed varius pulvinar sapien, nec tempus ante pulvinar sit amet. Sed mauris tortor, dictum tristique aliquam vestibulum, porta et justo.\r\n\r\n' + 'Lorem ipsum dolor sit amet, consectetur adipiscing elit. Aliquam porttitor, turpis eu congue semper, felis purus blandit purus, eu finibus dui orci et augue.\r\n\r\nVestibulum nec dignissim ante. Sed vehicula sagittis nunc, sed iaculis lorem. Quisque quis lorem non lorem ornare venenatis. Morbi luctus, enim et tincidunt pellentesque, ante justo venenatis nisl, rhoncus lobortis quam nunc in lectus. Nam consectetur sapien sem, quis laoreet mi dapibus sit amet. Nullam consectetur erat ut diam luctus posuere. Duis vulputate turpis vitae magna commodo pharetra. Sed varius pulvinar sapien, nec tempus ante pulvinar sit amet. Sed mauris tortor, dictum tristique aliquam vestibulum, porta et justo.\r\n\r\n' }, '/component-image': {}, '/accessibility-findings': { @@ -18,7 +18,7 @@ module.exports = { 'auditDate-month': '1', 'auditDate-year': '2020', issuesDiscovered: - 'In lacus ipsum, molestie nec sapien vitae, tincidunt tristique nisi. Integer a lacus quis nisl mollis fringilla. Nullam blandit imperdiet mauris, ac feugiat ante. Vestibulum nec semper nulla, ut ultricies massa. Curabitur lacinia tortor augue, sed varius leo viverra non. Fusce vitae libero ac orci elementum vestibulum eu et libero. In rhoncus laoreet nisi, sit amet rutrum tellus. Ut ut dui in metus sodales molestie eu ut purus.\r\n\r\n' + 'In lacus ipsum, molestie nec sapien vitae, tincidunt tristique nisi.\r\n\r\n Integer a lacus quis nisl mollis fringilla. Nullam blandit imperdiet mauris, ac feugiat ante.\r\n\r\n* lklsdkfjl\r\n* sdflkjsdflkjsdflk sdflkjlk lsdfkjlksdfj\r\n\r\nVestibulum nec semper nulla, ut ultricies massa. Curabitur lacinia tortor augue, sed varius leo viverra non. Fusce vitae libero ac orci elementum vestibulum eu et libero.\r\n\r\n In rhoncus laoreet nisi, sit amet rutrum tellus. Ut ut dui in metus sodales molestie eu ut purus.\r\n\r\n' }, '/add-internal-audit': { internalOrganisation: 'Curabitur', @@ -26,7 +26,7 @@ module.exports = { 'auditDate-month': '1', 'auditDate-year': '2020', issuesDiscovered: - 'In lacus ipsum, molestie nec sapien vitae, tincidunt tristique nisi. Integer a lacus quis nisl mollis fringilla. Nullam blandit imperdiet mauris, ac feugiat ante. Vestibulum nec semper nulla, ut ultricies massa. Curabitur lacinia tortor augue, sed varius leo viverra non. Fusce vitae libero ac orci elementum vestibulum eu et libero. In rhoncus laoreet nisi, sit amet rutrum tellus. Ut ut dui in metus sodales molestie eu ut purus.\r\n\r\n' + 'In lacus ipsum, molestie nec sapien vitae, tincidunt tristique nisi. Integer a lacus quis nisl mollis fringilla\r\n\r\nNullam blandit imperdiet mauris, ac feugiat ante. Vestibulum nec semper nulla, ut ultricies massa. Curabitur lacinia tortor augue, sed varius leo viverra non. Fusce vitae libero ac orci elementum vestibulum eu et libero. In rhoncus laoreet nisi, sit amet rutrum tellus. Ut ut dui in metus sodales molestie eu ut purus.\r\n\r\n' }, '/add-assistive-tech': { 'testingDate-day': '1', @@ -49,11 +49,21 @@ module.exports = { componentCodeAvailable: 'yes' }, '/component-code-details': { - componentCodeLanguage: 'other', - componentCodeLanguageOther: 'Nunjucks', + componentCodeLanguage: 'nunjucks', componentCodeUsage: 'In lacus ipsum, molestie nec sapien vitae, tincidunt tristique nisi. Integer a lacus quis nisl mollis fringilla. Nullam blandit imperdiet mauris, ac feugiat ante. Vestibulum nec semper nulla, ut ultricies massa. Curabitur lacinia tortor augue, sed varius leo viverra non. Fusce vitae libero ac orci elementum vestibulum eu et libero. In rhoncus laoreet nisi, sit amet rutrum tellus. Ut ut dui in metus sodales molestie eu ut purus.\r\n\r\n', - componentCode: '

    test

    ' + componentCode: `{% from "govuk/components/notification-banner/macro.njk" import govukNotificationBanner %} + +{% set html %} +

    + You have 7 days left to send your application. + View application. +

    +{% endset %} + +{{ govukNotificationBanner({ + html: html +}) }}` }, '/your-details': { fullName: 'test test', @@ -70,8 +80,219 @@ module.exports = { figmaLinkAdditionalInformation: 'only link' }, '/component-code-details/1': { + componentCodeLanguage: 'html', + componentCodeUsage: 'Some HTML', + componentCode: `
    +
    +

    + Important +

    +
    +
    +

    + You have 7 days left to send your application. + View application. +

    +
    +
    ` + }, + '/component-code-details/2': { componentCodeLanguage: 'css', componentCodeUsage: 'Some CSS', - componentCode: 'style' + componentCode: `.govuk-notification-banner { + font-family: GDS Transport,arial,sans-serif; + -webkit-font-smoothing: antialiased; + -moz-osx-font-smoothing: grayscale; + font-weight: 400; + font-size: 1rem; + line-height: 1.25; + margin-bottom: 30px; + border: 5px solid #1d70b8; + background-color: #1d70b8 +} + +@media print { + .govuk-notification-banner { + font-family: sans-serif + } +} + +@media (min-width: 40.0625em) { + .govuk-notification-banner { + font-size:1.1875rem; + line-height: 1.3157894737 + } +} + +@media print { + .govuk-notification-banner { + font-size: 14pt; + line-height: 1.15 + } +} + +@media (min-width: 40.0625em) { + .govuk-notification-banner { + margin-bottom:50px + } +} + +.govuk-notification-banner:focus { + outline: 3px solid #fd0 +} + +.govuk-notification-banner__header { + padding: 2px 15px 5px; + border-bottom: 1px solid transparent +} + +@media (min-width: 40.0625em) { + .govuk-notification-banner__header { + padding:2px 20px 5px + } +} + +.govuk-notification-banner__title { + font-size: 1rem; + line-height: 1.25; + font-weight: 700; + margin: 0; + padding: 0; + color: #fff +} + +@media (min-width: 40.0625em) { + .govuk-notification-banner__title { + font-size:1.1875rem; + line-height: 1.3157894737 + } +} + +@media print { + .govuk-notification-banner__title { + font-size: 14pt; + line-height: 1.15 + } +} + +.govuk-notification-banner__content { + color: #0b0c0c; + padding: 15px; + background-color: #fff +} + +@media print { + .govuk-notification-banner__content { + color: #000 + } +} + +@media (min-width: 40.0625em) { + .govuk-notification-banner__content { + padding:20px + } +} + +.govuk-notification-banner__content>* { + box-sizing: border-box; + max-width: 605px +} + +.govuk-notification-banner__content>:last-child { + margin-bottom: 0 +} + +.govuk-notification-banner__heading { + font-size: 1.125rem; + line-height: 1.1111111111; + font-weight: 700; + margin: 0 0 15px; + padding: 0 +} + +@media (min-width: 40.0625em) { + .govuk-notification-banner__heading { + font-size:1.5rem; + line-height: 1.25 + } +} + +@media print { + .govuk-notification-banner__heading { + font-size: 18pt; + line-height: 1.15 + } +} + +.govuk-notification-banner__link { + font-family: GDS Transport,arial,sans-serif; + -webkit-font-smoothing: antialiased; + -moz-osx-font-smoothing: grayscale; + text-decoration: underline; + text-decoration-thickness: max(1px,.0625rem); + text-underline-offset: .1578em +} + +@media print { + .govuk-notification-banner__link { + font-family: sans-serif + } +} + +.govuk-notification-banner__link:hover { + text-decoration-thickness: max(3px,.1875rem,.12em); + -webkit-text-decoration-skip-ink: none; + text-decoration-skip-ink: none; + -webkit-text-decoration-skip: none; + text-decoration-skip: none +} + +.govuk-notification-banner__link:focus { + outline: 3px solid transparent; + background-color: #fd0; + box-shadow: 0 -2px #fd0,0 4px #0b0c0c; + text-decoration: none +} + +@supports not (text-wrap: balance) { + .govuk-notification-banner__link:focus { + -webkit-box-decoration-break:clone; + box-decoration-break: clone + } +} + +.govuk-notification-banner__link:link,.govuk-notification-banner__link:visited { + color: #1d70b8 +} + +.govuk-notification-banner__link:hover { + color: #003078 +} + +.govuk-notification-banner__link:active,.govuk-notification-banner__link:focus { + color: #0b0c0c +} + +.govuk-notification-banner--success { + border-color: #00703c; + background-color: #00703c +} + +.govuk-notification-banner--success .govuk-notification-banner__link:link,.govuk-notification-banner--success .govuk-notification-banner__link:visited { + color: #00703c +} + +.govuk-notification-banner--success .govuk-notification-banner__link:hover { + color: #004e2a +} + +.govuk-notification-banner--success .govuk-notification-banner__link:active { + color: #00703c +} + +.govuk-notification-banner--success .govuk-notification-banner__link:focus { + color: #0b0c0c +}` } + } diff --git a/middleware/generate-documentation.js b/middleware/generate-documentation.js index 1fc62a78b..697267210 100644 --- a/middleware/generate-documentation.js +++ b/middleware/generate-documentation.js @@ -1,110 +1,100 @@ /* eslint-disable prefer-template */ const moment = require('moment') +const { MAX_ADD_ANOTHER } = require('../config.js') +const { ucFirst } = require('../helpers/text-helper.js') const generateMarkdown = (data, files) => { + console.log(data) const { '/component-details': details } = data - const documentationDirectory = 'docs/components' const componentName = details?.componentName || 'unknown-component' - const sanitizedComponentName = componentName - .toLowerCase() - .replace(/[^a-z0-9-]/g, '-') - .replace(/-+/g, '-') - const filename = `${documentationDirectory}/${sanitizedComponentName}.md` - const generateLinksSection = (data) => { - const noLinks = - 'No prototypes or design files have been added for this component. If you have used this component in your service and you have a prototype you can share it here.\n' + const sanitizedComponentName = ucFirst( + componentName + .toLowerCase() + .replace(/[^a-z0-9-]/g, '-') + .replace(/-+/g, '-') + ) + const filename = `${sanitizedComponentName}.md` + + const githubDiscussionLink = (componentName = '') => { + return `[${componentName ? `${componentName} ` : ''}Github discussion]({{ githuburl }})` + } + + const generateDesignsTabContent = (data) => { let content = '' - let prototypeContent = '' - let figmaContent = '' - let n = 0 - // Process prototype data - while (data[`/prototype-url${n > 0 ? `/${n}` : ''}`]) { - const prototype = data[`/prototype-url${n > 0 ? `/${n}` : ''}`] - prototypeContent += ` -${prototype?.prototypeUrlAdditionalInformation || ''} + if (data['/figma-link']) { + content += `A Figma link has been added for this component. There may be more links and resources in the ${githubDiscussionLink(sanitizedComponentName)}.\r\n -Prototype link (opens in a new tab) -` - n++ - } +### Figma link - // Process figma data - n = 0 - while (data[`/figma-link${n > 0 ? `/${n}` : ''}`]) { - const figma = data[`/figma-link${n > 0 ? `/${n}` : ''}`] - figmaContent += ` -${figma?.figmaLinkAdditionalInformation || ''} + [View the ${sanitizedComponentName} in Figma (opens in a new tab)](${data['/figma-link']?.figmaUrl || ''})\r\n\r\n` -Figma link (opens in a new tab) -` - n++ - } + content += `${data['/figma-link']?.figmaLinkAdditionalInformation || ''}\r\n` - if (prototypeContent.length) { - content += '### Prototype\n' + prototypeContent - } - if (figmaContent.length) { - content += '\n### Figma\n' + figmaContent + content += `### Contribute prototypes and Figma links + + If you have design files that are relevant to this component you can add them to the ${githubDiscussionLink()}. This helps other people to use it in their service.` + } else { + content = `A Figma link was not included when this component was added. + + There may be more information in the ${githubDiscussionLink(sanitizedComponentName)}. You can also view the component image in the overview. + +## Contribute a Figma link + + If you have a Figma link for this component (or a component like it) you can add it to ${githubDiscussionLink()}. This helps other people to use it in their service.` } - return content.length ? content : noLinks + return content } - const generateAccessibilityReportSection = (data, files) => { - const contentHeading = - 'If you have had an accessibility audit or tested with users with access needs then you could contribute to this component.\n' - const noContent = - 'No accessibility findings have been added for this component.\n' - let content = '' - const externalAudit = data['/add-external-audit'] - const externalAuditFile = files?.['/add-external-audit'] - const internalAudit = data['/add-internal-audit'] - const internalAuditFile = files?.['/add-internal-audit'] - const assistiveTech = data['/add-assistive-tech'] - const assistiveTechReport = files?.['/add-assistive-tech'] - + const generateAccessibilityTabContent = (data) => { const formatDate = (day, month, year) => { if (day && month && year) { - return moment(`${year}-${month}-${day}`, 'YYYY-M-D').format('MMMM YYYY') + return moment(`${year}-${month}-${day}`, 'YYYY-M-D').format( + 'D MMMM YYYY' + ) } return null } - if (externalAudit && externalAudit.externalOrganisation) { - const auditDate = formatDate( + let content = '' + const externalAudit = data['/add-external-audit'] + const internalAudit = data['/add-internal-audit'] + const assistiveTech = data['/add-assistive-tech'] + + if (externalAudit) { + const externalAuditDate = formatDate( externalAudit['auditDate-day'], externalAudit['auditDate-month'], externalAudit['auditDate-year'] ) - if (auditDate) { - content += `### External audit (${externalAudit.externalOrganisation}) - ${auditDate}\n` - if (externalAudit.issuesDiscovered) { - content += externalAudit.issuesDiscovered + '\n\n' - } - if (externalAudit.accessibilityReport && externalAuditFile) { - content += `[Download the accessibility report](/${externalAuditFile.path})\n` - } - } + + content += `### External audit + +* Conducted by: ${externalAudit.externalOrganisation} +* Date: ${externalAuditDate} + +#### Audit findings + +${externalAudit.issuesDiscovered}\r\n` } - if (internalAudit && internalAudit.internalOrganisation) { - const auditDate = formatDate( + if (internalAudit) { + const internalAuditDate = formatDate( internalAudit['auditDate-day'], internalAudit['auditDate-month'], internalAudit['auditDate-year'] ) - if (auditDate) { - content += `### Internal audit (${internalAudit.internalOrganisation}) - ${auditDate}\n` - if (internalAudit.issuesDiscovered) { - content += internalAudit.issuesDiscovered + '\n\n' - } - if (internalAudit.accessibilityReport && internalAuditFile) { - content += `[Download the accessibility report](/${internalAuditFile.path})\n` - } - } + content += `### Internal review + +* By: ${internalAudit.internalOrganisation} +* Date: ${internalAuditDate} + +#### Review findings + +${internalAudit.issuesDiscovered}\r\n` } if (assistiveTech) { @@ -113,94 +103,130 @@ ${figma?.figmaLinkAdditionalInformation || ''} assistiveTech['testingDate-month'], assistiveTech['testingDate-year'] ) - if (testingDate) { - content += `### Assistive Technology audit - ${testingDate}\n` - if (assistiveTech.issuesDiscovered) { - content += assistiveTech.issuesDiscovered + '\n\n' - } - if (assistiveTech.accessibilityReport && assistiveTechReport) { - content += `[Download the accessibility report](/${assistiveTechReport.path})\n` - } - } + content += `### Assistive Technology testing + +Date: ${testingDate} + +#### Testing details + +${assistiveTech.issuesDiscovered}\r\n` } - return content ? contentHeading + content : noContent - } + if (externalAudit || internalAudit || assistiveTech) { + content = `Accessibility findings have been added for this component. There may be more findings in the ${githubDiscussionLink(sanitizedComponentName)}.\r\n + +${content}\r\n` + } else { + content = `No accessibility findings were included when this component was added. There may be more information in the ${githubDiscussionLink(sanitizedComponentName)}.\r\n` + } + + content += `## Contribute accessibility findings - const generateComponentCodeSection = (data) => { - const noCode = - 'No code has been added for this component. If you have examples of how you have used this component in your service then you could help the community. Most users are looking for HTML, Nunjucks, Javascript and CSS or SASS.\n' + If you have accessibility findings that are relevant to this component you can add them to the ${githubDiscussionLink()}. This helps other people to use it in their service.` + return content + } + + const generateCodeTabContent = (data) => { + const hljsLang = (lang) => { + switch(lang.toLowerCase()) { + case 'html': + case 'css': + case 'scss': + case 'javascript': + case 'typescript': + return lang + case 'nunjucks': + return 'njk' + case 'sass': + return 'scss' + case 'react': + return 'javascript' + default: + return '' + } + } let content = '' - let n = 1 - while (data[`/component-code${n > 1 ? `-${n}` : ''}`]) { - const componentCodeDetails = - data[`/component-code-details${n > 1 ? `-${n}` : ''}`] + for(let i=0;i<=MAX_ADD_ANOTHER;i++) { + const code = data[`/component-code-details${i === 0 ? '' : `/${i}`}`] + if(!code) { + break + } + const language = + code?.componentCodeLanguage === 'other' + ? code?.componentCodeLanguageOther + : code?.componentCodeLanguage + + content += ` +### Example ${i + 1}: ${language} -### ${componentCodeDetails?.componentCodeLanguage || ''} +\`\`\`${hljsLang(language)} +{% raw %} +${code?.componentCode || ''} +{% endraw %} +\`\`\` -### ${componentCodeDetails?.componentCodeLanguageOther || ''} +${code?.componentCodeUsage || ''}\r\n\r\n` + } -
    + if (data['/component-code-details']) { + content = `Code has been added for this component. There may be other code examples in the ${githubDiscussionLink(sanitizedComponentName)}. -\`\`\`html -${componentCodeDetails?.componentCode || ''} -\`\`\` +${content} -
    -${componentCodeDetails?.componentCodeUsage || ''} -` +## Contribute code for this component - n++ +If you have code that is relevant to this component you can add it to the ${githubDiscussionLink()}. This helps other people to use it in their service.` + } else { + content = `No code was included when this contribution was added. + +You can use the ${githubDiscussionLink(sanitizedComponentName)} to: + +* view other code examples +* add relevant code` } - return content.length ? content : noCode + + console.log(content) + return content } - const content = `--- -layout: layouts/tabbed-component.njk -title: ${componentName} -type: component +const content = `--- +title: ${ucFirst(componentName)} +tabs: true status: Experimental statusDate: ${moment().format('MMMM YYYY')} -eleventyNavigation: - key: ${componentName} - parent: Components - excerpt: "${details?.briefDescription || ''}" +excerpt: "${details?.briefDescription || ''}" +lede: "${details?.briefDescription || ''}" +githuburl: https://github.com/ministryofjustice/moj-frontend/discussions/xxx +${data['/your-details']?.fullName === 'Not shared' ? '' : `contributorName: ${data['/your-details']?.fullName}`} +${data['/your-details']?.teamName === 'Not shared' ? '' : `contributorTeam: ${data['/your-details']?.teamName}`} --- -{% tabs "Contents" %} - +{% tabs "paginate" %} {% tab "Overview" %} -## Overview -
    - ${files?.['/component-image'] ? '![' + componentName + '](/' + files['/component-image'].path + ')' : ''} + ${componentName}
    +## Overview ${details?.componentOverview || ''} -## How the component is currently used +### How the component is currently used ${details?.howIsTheComponentUsed || ''} -{% endtab %} - -{% tab "Code" %} - -## Help develop existing building blocks in GitHub - -After a new building block is published in the design system, you, and other users, have the chance to continue enhancing it. This is done with users adding more information and resources to the component via GitHub. +### Contribute to this component +You can help develop this component by adding information to the Github discussion. This helps other people to use it in their service. -To do this you should: +{% endtab %} -- go to the GitHub conversation -- add your comments, information and resources about the building block +{% tab "Designs" %} -## Code +## Designs -${generateComponentCodeSection(data)} +${generateDesignsTabContent(data)} {% endtab %} @@ -208,15 +234,15 @@ ${generateComponentCodeSection(data)} ## Accessibility -${generateAccessibilityReportSection(data, files)} +${generateAccessibilityTabContent(data)} {% endtab %} -{% tab "Links" %} +{% tab "Code" %} -## Links +## Code -${generateLinksSection(data)} +${generateCodeTabContent(data)} {% endtab %} diff --git a/middleware/process-subission-data.js b/middleware/process-subission-data.js index 534177e4b..db56dcb54 100644 --- a/middleware/process-subission-data.js +++ b/middleware/process-subission-data.js @@ -1,3 +1,5 @@ +const { generateMarkdown } = require('../middleware/generate-documentation') + const redis = require('../helpers/redis-client') const imageDirectory = 'assets/images' @@ -48,13 +50,16 @@ const getUniqueFilename = (originalName, existingFilenames) => { return uniqueName } -const processSubmissionFiles = async (sessionData, submissionRef) => { +const processSubmissionFiles = async (req) => { + console.log('processing files') + console.log(req) + const { session, submissionRef } = req const submissionFiles = {} const existingFilenames = new Set() - for (const key of Object.keys(sessionData)) { + for (const key of Object.keys(session)) { if (!['cookie', 'csrfToken'].includes(key)) { - const fileData = sessionData[key] + const fileData = session[key] if ( fileData?.componentImage?.redisKey || fileData?.accessibilityReport?.redisKey @@ -82,7 +87,11 @@ const processSubmissionFiles = async (sessionData, submissionRef) => { return submissionFiles } -const processSubmissionData = (sessionData, submissionFiles, submissionRef) => { +const processSubmissionData = (req, res, next) => { + console.log('processing submission data') + const sessionData = { ...req.session, ...req.markdown } + console.log(sessionData) + const { submissionFiles, submissionRef } = req const submissionData = {} for (const key in sessionData) { @@ -94,30 +103,9 @@ const processSubmissionData = (sessionData, submissionFiles, submissionRef) => { const filename = extractFilename(key) if (filename.endsWith('.md')) { // Documentation should be outside of the submission folder - submissionData[filename] = sessionData[key] + submissionData[`docs/components/${filename}`] = sessionData[key] } else { const data = Object.assign({}, sessionData[key]) - if (key === '/your-details') { - // Remove personal data - data.fullName = 'Not shared' - data.teamName = 'Not shared' - - // Add back in personal details if permission is given - if ( - sessionData[key]?.shareYourDetails?.includes( - 'addNameToComponentPage' - ) - ) { - data.fullName = sessionData[key].fullName - } - if ( - sessionData[key]?.shareYourDetails?.includes( - 'addTeamNameToComponentPage' - ) - ) { - data.teamName = sessionData[key].teamName - } - } if (key.startsWith('/component-code-details')) { const exampleNum = key.split('/').at(2) ? `-${key.split('/').at(2)}` @@ -147,7 +135,49 @@ const processSubmissionData = (sessionData, submissionFiles, submissionRef) => { } } - return submissionData + req.submissionData = submissionData + next() } -module.exports = { processSubmissionData, processSubmissionFiles } +const processPersonalData = (req, res, next) => { + console.log('excluding personal data') + const personalData = req.session['/your-details'] + const data = Object.assign({}, personalData) + // Remove personal data + data.fullName = 'Not shared' + data.teamName = 'Not shared' + + // Add back in personal details if permission is given + if (personalData?.shareYourDetails?.includes('addNameToComponentPage')) { + data.fullName = personalData.fullName + } + if (personalData?.shareYourDetails?.includes('addTeamNameToComponentPage')) { + data.teamName = personalData.teamName + } + req.session['/your-details'] = data + next() +} + +const buildComponentPage = (req, res, next) => { + console.log('building component page') + const { filename: markdownFilename, content: markdownContent } = + generateMarkdown(req.session, req.submissionFiles) + req.markdown = {} + req.markdownFilename = markdownFilename + req.markdownContent = markdownContent + req.markdown[markdownFilename] = markdownContent + next() +} + +const generateSubmissionRef = (req,res,next) => { + req.submissionRef = `submission-${Date.now()}` + next() +} + +module.exports = { + processSubmissionData, + processSubmissionFiles, + processPersonalData, + buildComponentPage, + generateSubmissionRef +} diff --git a/routes/add-component.js b/routes/add-component.js index e303aabd5..d643b13e9 100644 --- a/routes/add-component.js +++ b/routes/add-component.js @@ -1,4 +1,5 @@ const crypto = require('crypto') +const fs = require('fs') const express = require('express') const { xss } = require('express-xss-sanitizer') @@ -7,12 +8,13 @@ const multer = require('multer') const { COMPONENT_FORM_PAGES, ADD_NEW_COMPONENT_ROUTE, - MESSAGES + MESSAGES, + ENV } = require('../config') const ApplicationError = require('../helpers/application-error') const { checkYourAnswers } = require('../helpers/check-your-answers') const getPrTitleAndDescription = require('../helpers/get-pr-title-and-description') -const sessionData = require('../helpers/mockSessionData/sessionData.js') +const mockSessionData = require('../helpers/mockSessionData/sessionData.js') const { urlToTitleCase } = require('../helpers/text-helper') const { validateFormData, @@ -41,8 +43,11 @@ const { sendVerificationEmail } = require('../middleware/notify-email') const { + processPersonalData, processSubmissionData, - processSubmissionFiles + processSubmissionFiles, + buildComponentPage, + generateSubmissionRef } = require('../middleware/process-subission-data') const verifyCsrf = require('../middleware/verify-csrf') const upload = multer({ @@ -52,7 +57,7 @@ const upload = multer({ const router = express.Router() const checkYourAnswersPath = 'check-your-answers' -router.get('/error', (req,res, next) => { +router.get('/error', (req, res, next) => { const error = new ApplicationError('ohno') next(error) }) @@ -82,7 +87,9 @@ router.get( res.render(checkYourAnswersPath, { submitUrl: req.originalUrl, sections, - csrfToken: req?.session?.csrfToken + csrfToken: req?.session?.csrfToken, + isDev: ENV === 'development', + generateLink: `${ADD_NEW_COMPONENT_ROUTE}/generate-markdown` }) } ) @@ -94,7 +101,7 @@ if (process.env.DEV_DUMMY_DATA) { return next(new Error('Session not available')) } - Object.assign(req.session, sessionData) + Object.assign(req.session, mockSessionData) req.session.save((err) => { if (err) { return next(err) @@ -104,6 +111,45 @@ if (process.env.DEV_DUMMY_DATA) { }) } +if (ENV === 'development') { + router.get('/generate-markdown', (req, res, next) => { + if (!req.session) { + return next(new Error('Session not available')) + } + + if (!req.session.checkYourAnswers) { + Object.assign(req.session, mockSessionData) + req.session.save((err) => { + if (err) { + return next(err) + } + }) + } + next() + }, + processPersonalData, + generateSubmissionRef, + async (req, res, next) => { + console.log('processing files') + req.submissionFiles = await processSubmissionFiles(req) + next() + }, + buildComponentPage, + async (req,res) => { + const { markdownFilename: filename, markdownContent: content} = req + fs.writeFile(`docs/components/${filename}`, content, (err) => { + if (err) { + console.error(err) + } else { + // file written successfully + setTimeout(() => { + res.redirect(`/components/${filename.split('.').at(0).toLowerCase()}`) + }, 2000) + } + }) + }) +} + // Start router.get('/start', (req, res) => { console.log('get start') @@ -165,8 +211,11 @@ router.get('/email', (req, res) => { router.all('*', sessionStarted) router.post('/start', verifyCsrf, (req, res) => { - if(process.env.SKIP_VERIFICATION === 'true' && process.env.DEV_VERIFIED_EMAIL) { - req.session['/email'] = { emailAddress: process.env.DEV_VERIFIED_EMAIL} + if ( + process.env.SKIP_VERIFICATION === 'true' && + process.env.DEV_VERIFIED_EMAIL + ) { + req.session['/email'] = { emailAddress: process.env.DEV_VERIFIED_EMAIL } req.session.emailDomainAllowed = true req.session.verified = true res.redirect(`${ADD_NEW_COMPONENT_ROUTE}/component-details`) @@ -231,16 +280,16 @@ router.get('/email/resend', (req, res) => { }) router.post('/email/resend', xss(), verifyCsrf, async (req, res) => { - res.redirect(`${ADD_NEW_COMPONENT_ROUTE}/email/check`) - const token = req?.session?.emailToken - const email = req?.session?.['/email']?.emailAddress - if (token && email) { - try { - await sendVerificationEmail(email, token) - } catch (error) { - console.error(`Error sending verification email: ${error}`) - } + res.redirect(`${ADD_NEW_COMPONENT_ROUTE}/email/check`) + const token = req?.session?.emailToken + const email = req?.session?.['/email']?.emailAddress + if (token && email) { + try { + await sendVerificationEmail(email, token) + } catch (error) { + console.error(`Error sending verification email: ${error}`) } + } }) router.get('/email/not-allowed', (req, res) => { @@ -334,24 +383,23 @@ router.post( `/${checkYourAnswersPath}`, verifyCsrf, getRawSessionText, + processPersonalData, + generateSubmissionRef, + async (req, res, next) => { + console.log('processing files') + req.submissionFiles = await processSubmissionFiles(req) + next() + }, + buildComponentPage, + processSubmissionData, async (req, res) => { - const submissionRef = `submission-${Date.now()}` - const submissionFiles = await processSubmissionFiles( - req.session, - submissionRef - ) - // console.log(submissionFiles) - const { filename: markdownFilename, content: markdownContent } = - generateMarkdown(req.session, submissionFiles) - const markdown = {} - markdown[markdownFilename] = markdownContent - const { sessionText } = req - const session = { ...req.session, ...markdown } - const sessionData = processSubmissionData( + const { session, - submissionFiles, - submissionRef - ) + sessionText, + submissionData, + submissionRef, + markdownContent + } = req req.session.regenerate((err) => { if (err) { @@ -361,7 +409,7 @@ router.post( }) try { - const branchName = await pushToGitHub(sessionData, submissionRef) + const branchName = await pushToGitHub(submissionData, submissionRef) const { title, description } = getPrTitleAndDescription(session) const pr = await createPullRequest(branchName, title, description) await sendPrEmail(pr) @@ -386,7 +434,9 @@ router.post( validateFormData, (req, res, next) => { if (req.file) { - req.session.sessionFlash = MESSAGES.componentImageUploaded(req.file.originalname) + req.session.sessionFlash = MESSAGES.componentImageUploaded( + req.file.originalname + ) saveSession(req, res, next) } else { // Skipping saving as no new file uploaded diff --git a/views/community/pages/check-your-answers.njk b/views/community/pages/check-your-answers.njk index 28595cf54..afedcd928 100644 --- a/views/community/pages/check-your-answers.njk +++ b/views/community/pages/check-your-answers.njk @@ -51,10 +51,17 @@ csrfToken: csrfToken, action: submitUrl }) %} -
    +
    {{ govukButton({ text: 'Accept and send' }) }} + {% if isDev %} + {{ govukButton({ + text: "Generate markdown", + classes: "govuk-button--secondary", + href: generateLink + }) }} + {% endif %}
    {% endcall %}