From b6b4220f351c5d8fdf58e837835b7fb1482ab8ca Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Dafydd=20Ll=C5=B7r=20Pearson?= Date: Tue, 10 Sep 2024 16:08:09 +0100 Subject: [PATCH 1/5] chore(editor): Bump Typescript to v5.6 (#3649) --- editor.planx.uk/package.json | 2 +- editor.planx.uk/pnpm-lock.yaml | 209 +++++++++--------- .../src/ui/shared/ReactMarkdownOrHtml.tsx | 2 +- package.json | 2 +- pnpm-lock.yaml | 8 +- 5 files changed, 111 insertions(+), 112 deletions(-) diff --git a/editor.planx.uk/package.json b/editor.planx.uk/package.json index 932dfd8f44..3aa3a6911a 100644 --- a/editor.planx.uk/package.json +++ b/editor.planx.uk/package.json @@ -162,7 +162,7 @@ "stream-browserify": "^3.0.0", "ts-jest": "^29.1.3", "tsconfig-paths-webpack-plugin": "^4.0.1", - "typescript": "^5.4.3", + "typescript": "^5.6.2", "vite-tsconfig-paths": "^4.3.2", "vitest": "^1.6.0", "vitest-axe": "1.0.0-pre.3", diff --git a/editor.planx.uk/pnpm-lock.yaml b/editor.planx.uk/pnpm-lock.yaml index ac10529f31..12aee51472 100644 --- a/editor.planx.uk/pnpm-lock.yaml +++ b/editor.planx.uk/pnpm-lock.yaml @@ -276,7 +276,7 @@ dependencies: version: 0.1.4(jest@27.5.1)(vite@5.2.11) vite-plugin-svgr: specifier: ^4.2.0 - version: 4.2.0(rollup@2.79.1)(typescript@5.4.3)(vite@5.2.11) + version: 4.2.0(rollup@2.79.1)(typescript@5.6.2)(vite@5.2.11) wkt: specifier: ^0.1.1 version: 0.1.1 @@ -335,13 +335,13 @@ devDependencies: version: 8.2.9(storybook@8.2.9) '@storybook/preset-create-react-app': specifier: ^8.2.9 - version: 8.2.9(react-refresh@0.14.0)(react-scripts@5.0.1)(storybook@8.2.9)(typescript@5.4.3)(webpack@5.91.0) + version: 8.2.9(react-refresh@0.14.0)(react-scripts@5.0.1)(storybook@8.2.9)(typescript@5.6.2)(webpack@5.91.0) '@storybook/react': specifier: ^8.2.9 - version: 8.2.9(react-dom@18.2.0)(react@18.2.0)(storybook@8.2.9)(typescript@5.4.3) + version: 8.2.9(react-dom@18.2.0)(react@18.2.0)(storybook@8.2.9)(typescript@5.6.2) '@storybook/react-vite': specifier: ^8.2.9 - version: 8.2.9(react-dom@18.2.0)(react@18.2.0)(rollup@2.79.1)(storybook@8.2.9)(typescript@5.4.3)(vite@5.2.11) + version: 8.2.9(react-dom@18.2.0)(react@18.2.0)(rollup@2.79.1)(storybook@8.2.9)(typescript@5.6.2)(vite@5.2.11) '@storybook/test': specifier: ^8.2.9 version: 8.2.9(@types/jest@27.5.2)(jest@27.5.1)(storybook@8.2.9)(vitest@1.6.0) @@ -401,10 +401,10 @@ devDependencies: version: 9.0.7 '@typescript-eslint/eslint-plugin': specifier: ^5.62.0 - version: 5.62.0(@typescript-eslint/parser@5.58.0)(eslint@8.44.0)(typescript@5.4.3) + version: 5.62.0(@typescript-eslint/parser@5.58.0)(eslint@8.44.0)(typescript@5.6.2) '@typescript-eslint/parser': specifier: ^5.58.0 - version: 5.58.0(eslint@8.44.0)(typescript@5.4.3) + version: 5.58.0(eslint@8.44.0)(typescript@5.6.2) autoprefixer: specifier: ^10.4.16 version: 10.4.16(postcss@8.4.32) @@ -434,7 +434,7 @@ devDependencies: version: 12.1.0(eslint@8.44.0) eslint-plugin-testing-library: specifier: ^5.11.1 - version: 5.11.1(eslint@8.44.0)(typescript@5.4.3) + version: 5.11.1(eslint@8.44.0)(typescript@5.6.2) husky: specifier: ^8.0.3 version: 8.0.3 @@ -479,16 +479,16 @@ devDependencies: version: 3.0.0 ts-jest: specifier: ^29.1.3 - version: 29.1.3(@babel/core@7.22.5)(esbuild@0.21.3)(jest@27.5.1)(typescript@5.4.3) + version: 29.1.3(@babel/core@7.22.5)(esbuild@0.21.3)(jest@27.5.1)(typescript@5.6.2) tsconfig-paths-webpack-plugin: specifier: ^4.0.1 version: 4.0.1 typescript: - specifier: ^5.4.3 - version: 5.4.3 + specifier: ^5.6.2 + version: 5.6.2 vite-tsconfig-paths: specifier: ^4.3.2 - version: 4.3.2(typescript@5.4.3)(vite@5.2.11) + version: 4.3.2(typescript@5.6.2)(vite@5.2.11) vitest: specifier: ^1.6.0 version: 1.6.0(@types/node@17.0.45)(sass@1.71.1) @@ -4889,7 +4889,7 @@ packages: chalk: 4.1.2 dev: true - /@joshwooding/vite-plugin-react-docgen-typescript@0.3.1(typescript@5.4.3)(vite@5.2.11): + /@joshwooding/vite-plugin-react-docgen-typescript@0.3.1(typescript@5.6.2)(vite@5.2.11): resolution: {integrity: sha512-pdoMZ9QaPnVlSM+SdU/wgg0nyD/8wQ7y90ttO2CMCyrrm7RxveYIJ5eNfjPaoMFqW41LZra7QO9j+xV4Y18Glw==} peerDependencies: typescript: '>= 4.3.x' @@ -4901,8 +4901,8 @@ packages: glob: 7.2.3 glob-promise: 4.2.2(glob@7.2.3) magic-string: 0.27.0 - react-docgen-typescript: 2.2.2(typescript@5.4.3) - typescript: 5.4.3 + react-docgen-typescript: 2.2.2(typescript@5.6.2) + typescript: 5.6.2 vite: 5.2.11(@types/node@17.0.45)(sass@1.71.1) dev: true @@ -5785,7 +5785,7 @@ packages: '@react-theming/theme-name': 1.0.3 '@react-theming/theme-swatch': 1.0.0(react@18.2.0) '@storybook/addon-devkit': 1.4.2(@storybook/addons@6.5.16)(@storybook/react@8.2.9)(react-dom@18.2.0)(react@18.2.0) - '@storybook/react': 8.2.9(react-dom@18.2.0)(react@18.2.0)(storybook@8.2.9)(typescript@5.4.3) + '@storybook/react': 8.2.9(react-dom@18.2.0)(react@18.2.0)(storybook@8.2.9)(typescript@5.6.2) '@storybook/theming': 8.2.9(storybook@8.2.9) '@usulpro/react-json-view': 2.0.1(react-dom@18.2.0)(react@18.2.0) color-string: 1.9.1 @@ -6093,7 +6093,7 @@ packages: '@reach/rect': 0.2.1(prop-types@15.8.1)(react-dom@18.2.0)(react@18.2.0) '@storybook/addons': 6.5.16(react-dom@18.2.0)(react@18.2.0) '@storybook/core-events': 6.5.16 - '@storybook/react': 8.2.9(react-dom@18.2.0)(react@18.2.0)(storybook@8.2.9)(typescript@5.4.3) + '@storybook/react': 8.2.9(react-dom@18.2.0)(react@18.2.0)(storybook@8.2.9)(typescript@5.6.2) '@storybook/theming': 6.5.16(react-dom@18.2.0)(react@18.2.0) deep-equal: 2.2.3 prop-types: 15.8.1 @@ -6285,7 +6285,7 @@ packages: util-deprecate: 1.0.2 dev: true - /@storybook/builder-vite@8.2.9(storybook@8.2.9)(typescript@5.4.3)(vite@5.2.11): + /@storybook/builder-vite@8.2.9(storybook@8.2.9)(typescript@5.6.2)(vite@5.2.11): resolution: {integrity: sha512-MHD3ezRjKkJkOl0u7CRQoQD/LKd28YMWIcaz4YrV6ygokc0c3RFTlOefICQFgboc+1RwIUowxN1CJ2kJ7p4SWw==} peerDependencies: '@preact/preset-vite': '*' @@ -6311,7 +6311,7 @@ packages: magic-string: 0.30.10 storybook: 8.2.9(@babel/preset-env@7.22.6) ts-dedent: 2.2.0 - typescript: 5.4.3 + typescript: 5.6.2 vite: 5.2.11(@types/node@17.0.45)(sass@1.71.1) transitivePeerDependencies: - supports-color @@ -6451,17 +6451,17 @@ packages: storybook: 8.2.9(@babel/preset-env@7.22.6) dev: true - /@storybook/preset-create-react-app@8.2.9(react-refresh@0.14.0)(react-scripts@5.0.1)(storybook@8.2.9)(typescript@5.4.3)(webpack@5.91.0): + /@storybook/preset-create-react-app@8.2.9(react-refresh@0.14.0)(react-scripts@5.0.1)(storybook@8.2.9)(typescript@5.6.2)(webpack@5.91.0): resolution: {integrity: sha512-ntCJ0vf9DQqkDtR3QuwNbSBH76xDudFYcd6V+2uyN3qCc3G3QpuxPngVpQzcbsct6BmTudGzf45SkLgAsnWrYw==} peerDependencies: react-scripts: '>=5.0.0' storybook: ^8.2.9 dependencies: '@pmmmwh/react-refresh-webpack-plugin': 0.5.15(react-refresh@0.14.0)(webpack@5.91.0) - '@storybook/react-docgen-typescript-plugin': 1.0.6--canary.9.0c3f3b7.0(typescript@5.4.3)(webpack@5.91.0) + '@storybook/react-docgen-typescript-plugin': 1.0.6--canary.9.0c3f3b7.0(typescript@5.6.2)(webpack@5.91.0) '@types/semver': 7.5.8 - pnp-webpack-plugin: 1.7.0(typescript@5.4.3) - react-scripts: 5.0.1(@babel/plugin-syntax-flow@7.23.3)(@babel/plugin-transform-react-jsx@7.23.4)(esbuild@0.21.3)(eslint@8.44.0)(react@18.2.0)(sass@1.71.1)(typescript@5.4.3) + pnp-webpack-plugin: 1.7.0(typescript@5.6.2) + react-scripts: 5.0.1(@babel/plugin-syntax-flow@7.23.3)(@babel/plugin-transform-react-jsx@7.23.4)(esbuild@0.21.3)(eslint@8.44.0)(react@18.2.0)(sass@1.71.1)(typescript@5.6.2) semver: 7.6.3 storybook: 8.2.9(@babel/preset-env@7.22.6) transitivePeerDependencies: @@ -6485,7 +6485,7 @@ packages: storybook: 8.2.9(@babel/preset-env@7.22.6) dev: true - /@storybook/react-docgen-typescript-plugin@1.0.6--canary.9.0c3f3b7.0(typescript@5.4.3)(webpack@5.91.0): + /@storybook/react-docgen-typescript-plugin@1.0.6--canary.9.0c3f3b7.0(typescript@5.6.2)(webpack@5.91.0): resolution: {integrity: sha512-KUqXC3oa9JuQ0kZJLBhVdS4lOneKTOopnNBK4tUAgoxWQ3u/IjzdueZjFr7gyBrXMoU6duutk3RQR9u8ZpYJ4Q==} peerDependencies: typescript: '>= 4.x' @@ -6496,9 +6496,9 @@ packages: find-cache-dir: 3.3.2 flat-cache: 3.2.0 micromatch: 4.0.7 - react-docgen-typescript: 2.2.2(typescript@5.4.3) + react-docgen-typescript: 2.2.2(typescript@5.6.2) tslib: 2.6.3 - typescript: 5.4.3 + typescript: 5.6.2 webpack: 5.91.0(esbuild@0.21.3) transitivePeerDependencies: - supports-color @@ -6528,7 +6528,7 @@ packages: storybook: 8.2.9(@babel/preset-env@7.22.6) dev: true - /@storybook/react-vite@8.2.9(react-dom@18.2.0)(react@18.2.0)(rollup@2.79.1)(storybook@8.2.9)(typescript@5.4.3)(vite@5.2.11): + /@storybook/react-vite@8.2.9(react-dom@18.2.0)(react@18.2.0)(rollup@2.79.1)(storybook@8.2.9)(typescript@5.6.2)(vite@5.2.11): resolution: {integrity: sha512-Lw6FzcAaL7jX8Y8EsDzg32Lp0NdeNJZpj0LVwX5sLOQQA6w4i3PqlFINXDY28qCGo6wqKT+w44zhgwUcU5V0Ow==} engines: {node: '>=18.0.0'} peerDependencies: @@ -6537,10 +6537,10 @@ packages: storybook: ^8.2.9 vite: ^4.0.0 || ^5.0.0 dependencies: - '@joshwooding/vite-plugin-react-docgen-typescript': 0.3.1(typescript@5.4.3)(vite@5.2.11) + '@joshwooding/vite-plugin-react-docgen-typescript': 0.3.1(typescript@5.6.2)(vite@5.2.11) '@rollup/pluginutils': 5.1.0(rollup@2.79.1) - '@storybook/builder-vite': 8.2.9(storybook@8.2.9)(typescript@5.4.3)(vite@5.2.11) - '@storybook/react': 8.2.9(react-dom@18.2.0)(react@18.2.0)(storybook@8.2.9)(typescript@5.4.3) + '@storybook/builder-vite': 8.2.9(storybook@8.2.9)(typescript@5.6.2)(vite@5.2.11) + '@storybook/react': 8.2.9(react-dom@18.2.0)(react@18.2.0)(storybook@8.2.9)(typescript@5.6.2) find-up: 5.0.0 magic-string: 0.30.10 react: 18.2.0 @@ -6558,7 +6558,7 @@ packages: - vite-plugin-glimmerx dev: true - /@storybook/react@8.2.9(react-dom@18.2.0)(react@18.2.0)(storybook@8.2.9)(typescript@5.4.3): + /@storybook/react@8.2.9(react-dom@18.2.0)(react@18.2.0)(storybook@8.2.9)(typescript@5.6.2): resolution: {integrity: sha512-F2xZcTDxxjpbqt7eP8rEHmlksiKmE/qtPusEWEY4N4jK01kN+ncxSl8gkJpUohMEmAnVC5t/1v/sU57xv1DYpg==} engines: {node: '>=18.0.0'} peerDependencies: @@ -6593,7 +6593,7 @@ packages: storybook: 8.2.9(@babel/preset-env@7.22.6) ts-dedent: 2.2.0 type-fest: 2.19.0 - typescript: 5.4.3 + typescript: 5.6.2 util-deprecate: 1.0.2 dev: true @@ -6828,14 +6828,14 @@ packages: - supports-color dev: true - /@svgr/core@8.1.0(typescript@5.4.3): + /@svgr/core@8.1.0(typescript@5.6.2): resolution: {integrity: sha512-8QqtOQT5ACVlmsvKOJNEaWmRPmcojMOzCz4Hs2BGG/toAp/K38LcsMRyLp349glq5AzJbCEeimEoxaX6v/fLrA==} engines: {node: '>=14'} dependencies: '@babel/core': 7.24.9 '@svgr/babel-preset': 8.1.0(@babel/core@7.24.9) camelcase: 6.3.0 - cosmiconfig: 8.3.6(typescript@5.4.3) + cosmiconfig: 8.3.6(typescript@5.6.2) snake-case: 3.0.4 transitivePeerDependencies: - supports-color @@ -6877,7 +6877,7 @@ packages: dependencies: '@babel/core': 7.24.9 '@svgr/babel-preset': 8.1.0(@babel/core@7.24.9) - '@svgr/core': 8.1.0(typescript@5.4.3) + '@svgr/core': 8.1.0(typescript@5.6.2) '@svgr/hast-util-to-babel-ast': 8.0.0 svg-parser: 2.0.4 transitivePeerDependencies: @@ -7994,7 +7994,7 @@ packages: '@types/yargs-parser': 21.0.3 dev: true - /@typescript-eslint/eslint-plugin@5.62.0(@typescript-eslint/parser@5.58.0)(eslint@8.44.0)(typescript@5.4.3): + /@typescript-eslint/eslint-plugin@5.62.0(@typescript-eslint/parser@5.58.0)(eslint@8.44.0)(typescript@5.6.2): resolution: {integrity: sha512-TiZzBSJja/LbhNPvk6yc0JrX9XqhQ0hdh6M2svYfsHGejaKFIAGd9MQ+ERIMzLGlN/kZoYIgdxFV0PuljTKXag==} engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} peerDependencies: @@ -8006,36 +8006,36 @@ packages: optional: true dependencies: '@eslint-community/regexpp': 4.11.0 - '@typescript-eslint/parser': 5.58.0(eslint@8.44.0)(typescript@5.4.3) + '@typescript-eslint/parser': 5.58.0(eslint@8.44.0)(typescript@5.6.2) '@typescript-eslint/scope-manager': 5.62.0 - '@typescript-eslint/type-utils': 5.62.0(eslint@8.44.0)(typescript@5.4.3) - '@typescript-eslint/utils': 5.62.0(eslint@8.44.0)(typescript@5.4.3) + '@typescript-eslint/type-utils': 5.62.0(eslint@8.44.0)(typescript@5.6.2) + '@typescript-eslint/utils': 5.62.0(eslint@8.44.0)(typescript@5.6.2) debug: 4.3.6 eslint: 8.44.0 graphemer: 1.4.0 ignore: 5.3.1 natural-compare-lite: 1.4.0 semver: 7.6.3 - tsutils: 3.21.0(typescript@5.4.3) - typescript: 5.4.3 + tsutils: 3.21.0(typescript@5.6.2) + typescript: 5.6.2 transitivePeerDependencies: - supports-color dev: true - /@typescript-eslint/experimental-utils@5.62.0(eslint@8.44.0)(typescript@5.4.3): + /@typescript-eslint/experimental-utils@5.62.0(eslint@8.44.0)(typescript@5.6.2): resolution: {integrity: sha512-RTXpeB3eMkpoclG3ZHft6vG/Z30azNHuqY6wKPBHlVMZFuEvrtlEDe8gMqDb+SO+9hjC/pLekeSCryf9vMZlCw==} engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} peerDependencies: eslint: ^6.0.0 || ^7.0.0 || ^8.0.0 dependencies: - '@typescript-eslint/utils': 5.62.0(eslint@8.44.0)(typescript@5.4.3) + '@typescript-eslint/utils': 5.62.0(eslint@8.44.0)(typescript@5.6.2) eslint: 8.44.0 transitivePeerDependencies: - supports-color - typescript dev: true - /@typescript-eslint/parser@5.58.0(eslint@8.44.0)(typescript@5.4.3): + /@typescript-eslint/parser@5.58.0(eslint@8.44.0)(typescript@5.6.2): resolution: {integrity: sha512-ixaM3gRtlfrKzP8N6lRhBbjTow1t6ztfBvQNGuRM8qH1bjFFXIJ35XY+FC0RRBKn3C6cT+7VW1y8tNm7DwPHDQ==} engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} peerDependencies: @@ -8047,10 +8047,10 @@ packages: dependencies: '@typescript-eslint/scope-manager': 5.58.0 '@typescript-eslint/types': 5.58.0 - '@typescript-eslint/typescript-estree': 5.58.0(typescript@5.4.3) + '@typescript-eslint/typescript-estree': 5.58.0(typescript@5.6.2) debug: 4.3.6 eslint: 8.44.0 - typescript: 5.4.3 + typescript: 5.6.2 transitivePeerDependencies: - supports-color dev: true @@ -8071,7 +8071,7 @@ packages: '@typescript-eslint/visitor-keys': 5.62.0 dev: true - /@typescript-eslint/type-utils@5.62.0(eslint@8.44.0)(typescript@5.4.3): + /@typescript-eslint/type-utils@5.62.0(eslint@8.44.0)(typescript@5.6.2): resolution: {integrity: sha512-xsSQreu+VnfbqQpW5vnCJdq1Z3Q0U31qiWmRhr98ONQmcp/yhiPJFPq8MXiJVLiksmOKSjIldZzkebzHuCGzew==} engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} peerDependencies: @@ -8081,12 +8081,12 @@ packages: typescript: optional: true dependencies: - '@typescript-eslint/typescript-estree': 5.62.0(typescript@5.4.3) - '@typescript-eslint/utils': 5.62.0(eslint@8.44.0)(typescript@5.4.3) + '@typescript-eslint/typescript-estree': 5.62.0(typescript@5.6.2) + '@typescript-eslint/utils': 5.62.0(eslint@8.44.0)(typescript@5.6.2) debug: 4.3.6 eslint: 8.44.0 - tsutils: 3.21.0(typescript@5.4.3) - typescript: 5.4.3 + tsutils: 3.21.0(typescript@5.6.2) + typescript: 5.6.2 transitivePeerDependencies: - supports-color dev: true @@ -8101,7 +8101,7 @@ packages: engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} dev: true - /@typescript-eslint/typescript-estree@5.58.0(typescript@5.4.3): + /@typescript-eslint/typescript-estree@5.58.0(typescript@5.6.2): resolution: {integrity: sha512-cRACvGTodA+UxnYM2uwA2KCwRL7VAzo45syNysqlMyNyjw0Z35Icc9ihPJZjIYuA5bXJYiJ2YGUB59BqlOZT1Q==} engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} peerDependencies: @@ -8116,13 +8116,13 @@ packages: globby: 11.1.0 is-glob: 4.0.3 semver: 7.6.3 - tsutils: 3.21.0(typescript@5.4.3) - typescript: 5.4.3 + tsutils: 3.21.0(typescript@5.6.2) + typescript: 5.6.2 transitivePeerDependencies: - supports-color dev: true - /@typescript-eslint/typescript-estree@5.62.0(typescript@5.4.3): + /@typescript-eslint/typescript-estree@5.62.0(typescript@5.6.2): resolution: {integrity: sha512-CmcQ6uY7b9y694lKdRB8FEel7JbU/40iSAPomu++SjLMntB+2Leay2LO6i8VnJk58MtE9/nQSFIH6jpyRWyYzA==} engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} peerDependencies: @@ -8137,13 +8137,13 @@ packages: globby: 11.1.0 is-glob: 4.0.3 semver: 7.6.3 - tsutils: 3.21.0(typescript@5.4.3) - typescript: 5.4.3 + tsutils: 3.21.0(typescript@5.6.2) + typescript: 5.6.2 transitivePeerDependencies: - supports-color dev: true - /@typescript-eslint/utils@5.62.0(eslint@8.44.0)(typescript@5.4.3): + /@typescript-eslint/utils@5.62.0(eslint@8.44.0)(typescript@5.6.2): resolution: {integrity: sha512-n8oxjeb5aIbPFEtmQxQYOLI0i9n5ySBEY/ZEHHZqKQSFnxio1rv6dthascc9dLuwrL0RC5mPCxB7vnAVGAYWAQ==} engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} peerDependencies: @@ -8154,7 +8154,7 @@ packages: '@types/semver': 7.5.8 '@typescript-eslint/scope-manager': 5.62.0 '@typescript-eslint/types': 5.62.0 - '@typescript-eslint/typescript-estree': 5.62.0(typescript@5.4.3) + '@typescript-eslint/typescript-estree': 5.62.0(typescript@5.6.2) eslint: 8.44.0 eslint-scope: 5.1.1 semver: 7.6.3 @@ -9260,7 +9260,7 @@ packages: dev: true /batch@0.6.1: - resolution: {integrity: sha1-3DQxT05nkxgJP8dgJyUl+UvyXBY=} + resolution: {integrity: sha512-x+VAiMRL6UPkx+kudNvxTl6hB2XNNCG2r+7wixVfIYwu/2HKRXimwQyaumLjMveWvT2Hkd/cAJw+QBMfJ/EKVw==} dev: true /bfj@7.1.0: @@ -9400,7 +9400,7 @@ packages: dev: true /bytes@3.0.0: - resolution: {integrity: sha1-0ygVQE1olpn4Wk6k+odV3ROpYEg=} + resolution: {integrity: sha512-pMhOfFDPiv9t5jjIXkHosWmkSyQbvsgEVNkz0ERHbuLh2T/7j4Mqqpz523Fe8MVY89KC6Sh/QfS2sM+SjgFDcw==} engines: {node: '>= 0.8'} dev: true @@ -10058,7 +10058,7 @@ packages: path-type: 4.0.0 yaml: 1.10.2 - /cosmiconfig@8.3.6(typescript@5.4.3): + /cosmiconfig@8.3.6(typescript@5.6.2): resolution: {integrity: sha512-kcZ6+W5QzcJ3P1Mt+83OUv/oHFqZHIx8DuxG6eZ5RGMERoLqp4BuGjhHLYGK+Kf5XVkQvqBSmAy/nGWN3qDgEA==} engines: {node: '>=14'} peerDependencies: @@ -10071,7 +10071,7 @@ packages: js-yaml: 4.1.0 parse-json: 5.2.0 path-type: 4.0.0 - typescript: 5.4.3 + typescript: 5.6.2 dev: false /crelt@1.0.6: @@ -11271,7 +11271,7 @@ packages: eslint: 8.44.0 dev: true - /eslint-config-react-app@7.0.1(@babel/plugin-syntax-flow@7.23.3)(@babel/plugin-transform-react-jsx@7.23.4)(eslint@8.44.0)(jest@27.5.1)(typescript@5.4.3): + /eslint-config-react-app@7.0.1(@babel/plugin-syntax-flow@7.23.3)(@babel/plugin-transform-react-jsx@7.23.4)(eslint@8.44.0)(jest@27.5.1)(typescript@5.6.2): resolution: {integrity: sha512-K6rNzvkIeHaTd8m/QEh1Zko0KI7BACWkkneSs6s9cKZC/J27X3eZR6Upt1jkmZ/4FK+XUOPPxMEN7+lbUXfSlA==} engines: {node: '>=14.0.0'} peerDependencies: @@ -11284,19 +11284,19 @@ packages: '@babel/core': 7.24.9 '@babel/eslint-parser': 7.25.1(@babel/core@7.24.9)(eslint@8.44.0) '@rushstack/eslint-patch': 1.10.4 - '@typescript-eslint/eslint-plugin': 5.62.0(@typescript-eslint/parser@5.58.0)(eslint@8.44.0)(typescript@5.4.3) - '@typescript-eslint/parser': 5.58.0(eslint@8.44.0)(typescript@5.4.3) + '@typescript-eslint/eslint-plugin': 5.62.0(@typescript-eslint/parser@5.58.0)(eslint@8.44.0)(typescript@5.6.2) + '@typescript-eslint/parser': 5.58.0(eslint@8.44.0)(typescript@5.6.2) babel-preset-react-app: 10.0.1 confusing-browser-globals: 1.0.11 eslint: 8.44.0 eslint-plugin-flowtype: 8.0.3(@babel/plugin-syntax-flow@7.23.3)(@babel/plugin-transform-react-jsx@7.23.4)(eslint@8.44.0) eslint-plugin-import: 2.29.1(@typescript-eslint/parser@5.58.0)(eslint@8.44.0) - eslint-plugin-jest: 25.7.0(@typescript-eslint/eslint-plugin@5.62.0)(eslint@8.44.0)(jest@27.5.1)(typescript@5.4.3) + eslint-plugin-jest: 25.7.0(@typescript-eslint/eslint-plugin@5.62.0)(eslint@8.44.0)(jest@27.5.1)(typescript@5.6.2) eslint-plugin-jsx-a11y: 6.7.1(eslint@8.44.0) eslint-plugin-react: 7.35.0(eslint@8.44.0) eslint-plugin-react-hooks: 4.6.0(eslint@8.44.0) - eslint-plugin-testing-library: 5.11.1(eslint@8.44.0)(typescript@5.4.3) - typescript: 5.4.3 + eslint-plugin-testing-library: 5.11.1(eslint@8.44.0)(typescript@5.6.2) + typescript: 5.6.2 transitivePeerDependencies: - '@babel/plugin-syntax-flow' - '@babel/plugin-transform-react-jsx' @@ -11337,7 +11337,7 @@ packages: eslint-import-resolver-webpack: optional: true dependencies: - '@typescript-eslint/parser': 5.58.0(eslint@8.44.0)(typescript@5.4.3) + '@typescript-eslint/parser': 5.58.0(eslint@8.44.0)(typescript@5.6.2) debug: 3.2.7 eslint: 8.44.0 eslint-import-resolver-node: 0.3.9 @@ -11370,7 +11370,7 @@ packages: '@typescript-eslint/parser': optional: true dependencies: - '@typescript-eslint/parser': 5.58.0(eslint@8.44.0)(typescript@5.4.3) + '@typescript-eslint/parser': 5.58.0(eslint@8.44.0)(typescript@5.6.2) array-includes: 3.1.8 array.prototype.findlastindex: 1.2.5 array.prototype.flat: 1.3.2 @@ -11395,7 +11395,7 @@ packages: - supports-color dev: true - /eslint-plugin-jest@25.7.0(@typescript-eslint/eslint-plugin@5.62.0)(eslint@8.44.0)(jest@27.5.1)(typescript@5.4.3): + /eslint-plugin-jest@25.7.0(@typescript-eslint/eslint-plugin@5.62.0)(eslint@8.44.0)(jest@27.5.1)(typescript@5.6.2): resolution: {integrity: sha512-PWLUEXeeF7C9QGKqvdSbzLOiLTx+bno7/HC9eefePfEb257QFHg7ye3dh80AZVkaa/RQsBB1Q/ORQvg2X7F0NQ==} engines: {node: ^12.13.0 || ^14.15.0 || >=16.0.0} peerDependencies: @@ -11408,8 +11408,8 @@ packages: jest: optional: true dependencies: - '@typescript-eslint/eslint-plugin': 5.62.0(@typescript-eslint/parser@5.58.0)(eslint@8.44.0)(typescript@5.4.3) - '@typescript-eslint/experimental-utils': 5.62.0(eslint@8.44.0)(typescript@5.4.3) + '@typescript-eslint/eslint-plugin': 5.62.0(@typescript-eslint/parser@5.58.0)(eslint@8.44.0)(typescript@5.6.2) + '@typescript-eslint/experimental-utils': 5.62.0(eslint@8.44.0)(typescript@5.6.2) eslint: 8.44.0 jest: 27.5.1 transitivePeerDependencies: @@ -11486,13 +11486,13 @@ packages: eslint: 8.44.0 dev: true - /eslint-plugin-testing-library@5.11.1(eslint@8.44.0)(typescript@5.4.3): + /eslint-plugin-testing-library@5.11.1(eslint@8.44.0)(typescript@5.6.2): resolution: {integrity: sha512-5eX9e1Kc2PqVRed3taaLnAAqPZGEX75C+M/rXzUAI3wIg/ZxzUm1OVAwfe/O+vE+6YXOLetSe9g5GKD2ecXipw==} engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0, npm: '>=6'} peerDependencies: eslint: ^7.5.0 || ^8.0.0 dependencies: - '@typescript-eslint/utils': 5.62.0(eslint@8.44.0)(typescript@5.4.3) + '@typescript-eslint/utils': 5.62.0(eslint@8.44.0)(typescript@5.6.2) eslint: 8.44.0 transitivePeerDependencies: - supports-color @@ -12128,7 +12128,7 @@ packages: cross-spawn: 7.0.3 signal-exit: 4.1.0 - /fork-ts-checker-webpack-plugin@6.5.3(eslint@8.44.0)(typescript@5.4.3)(webpack@5.91.0): + /fork-ts-checker-webpack-plugin@6.5.3(eslint@8.44.0)(typescript@5.6.2)(webpack@5.91.0): resolution: {integrity: sha512-SbH/l9ikmMWycd5puHJKTkZJKddF4iRLyW3DeZ08HTI7NGyLS38MXd/KGgeWumQO7YNQbW2u/NtPT2YowbPaGQ==} engines: {node: '>=10', yarn: '>=1.0.0'} peerDependencies: @@ -12156,7 +12156,7 @@ packages: schema-utils: 2.7.0 semver: 7.6.3 tapable: 1.1.3 - typescript: 5.4.3 + typescript: 5.6.2 webpack: 5.91.0(esbuild@0.21.3) dev: true @@ -16141,11 +16141,11 @@ packages: find-up: 3.0.0 dev: true - /pnp-webpack-plugin@1.7.0(typescript@5.4.3): + /pnp-webpack-plugin@1.7.0(typescript@5.6.2): resolution: {integrity: sha512-2Rb3vm+EXble/sMXNSu6eoBx8e79gKqhNq9F5ZWW6ERNCTE/Q0wQNne5541tE5vKjfM8hpNCYL+LGc1YTfI0dg==} engines: {node: '>=6'} dependencies: - ts-pnp: 1.2.0(typescript@5.4.3) + ts-pnp: 1.2.0(typescript@5.6.2) transitivePeerDependencies: - typescript dev: true @@ -17359,7 +17359,7 @@ packages: peerDependencies: react-scripts: '>=2.1.3' dependencies: - react-scripts: 5.0.1(@babel/plugin-syntax-flow@7.23.3)(@babel/plugin-transform-react-jsx@7.23.4)(esbuild@0.21.3)(eslint@8.44.0)(react@18.2.0)(sass@1.71.1)(typescript@5.4.3) + react-scripts: 5.0.1(@babel/plugin-syntax-flow@7.23.3)(@babel/plugin-transform-react-jsx@7.23.4)(esbuild@0.21.3)(eslint@8.44.0)(react@18.2.0)(sass@1.71.1)(typescript@5.6.2) semver: 5.7.2 dev: true @@ -17414,7 +17414,7 @@ packages: react-dom: 18.3.1(react@18.3.1) dev: true - /react-dev-utils@12.0.1(eslint@8.44.0)(typescript@5.4.3)(webpack@5.91.0): + /react-dev-utils@12.0.1(eslint@8.44.0)(typescript@5.6.2)(webpack@5.91.0): resolution: {integrity: sha512-84Ivxmr17KjUupyqzFode6xKhjwuEJDROWKJy/BthkL7Wn6NJ8h4WE6k/exAv6ImS+0oZLRRW5j/aINMHyeGeQ==} engines: {node: '>=14'} peerDependencies: @@ -17433,7 +17433,7 @@ packages: escape-string-regexp: 4.0.0 filesize: 8.0.7 find-up: 5.0.0 - fork-ts-checker-webpack-plugin: 6.5.3(eslint@8.44.0)(typescript@5.4.3)(webpack@5.91.0) + fork-ts-checker-webpack-plugin: 6.5.3(eslint@8.44.0)(typescript@5.6.2)(webpack@5.91.0) global-modules: 2.0.0 globby: 11.1.0 gzip-size: 6.0.0 @@ -17448,7 +17448,7 @@ packages: shell-quote: 1.8.1 strip-ansi: 6.0.1 text-table: 0.2.0 - typescript: 5.4.3 + typescript: 5.6.2 webpack: 5.91.0(esbuild@0.21.3) transitivePeerDependencies: - eslint @@ -17487,12 +17487,12 @@ packages: react: 18.2.0 dev: false - /react-docgen-typescript@2.2.2(typescript@5.4.3): + /react-docgen-typescript@2.2.2(typescript@5.6.2): resolution: {integrity: sha512-tvg2ZtOpOi6QDwsb3GZhOjDkkX0h8Z2gipvTg6OVMUyoYoURhEiRNePT8NZItTVCDh39JJHnLdfCOkzoLbFnTg==} peerDependencies: typescript: '>= 4.3.x' dependencies: - typescript: 5.4.3 + typescript: 5.6.2 dev: true /react-docgen@7.0.3: @@ -17715,7 +17715,7 @@ packages: engines: {node: '>=0.10.0'} dev: true - /react-scripts@5.0.1(@babel/plugin-syntax-flow@7.23.3)(@babel/plugin-transform-react-jsx@7.23.4)(esbuild@0.21.3)(eslint@8.44.0)(react@18.2.0)(sass@1.71.1)(typescript@5.4.3): + /react-scripts@5.0.1(@babel/plugin-syntax-flow@7.23.3)(@babel/plugin-transform-react-jsx@7.23.4)(esbuild@0.21.3)(eslint@8.44.0)(react@18.2.0)(sass@1.71.1)(typescript@5.6.2): resolution: {integrity: sha512-8VAmEm/ZAwQzJ+GOMLbBsTdDKOpuZh7RPs0UymvBR2vRk4iZWCskjbFnxqjrzoIvlNNRZ3QJFx6/qDSi6zSnaQ==} engines: {node: '>=14.0.0'} hasBin: true @@ -17743,7 +17743,7 @@ packages: dotenv: 10.0.0 dotenv-expand: 5.1.0 eslint: 8.44.0 - eslint-config-react-app: 7.0.1(@babel/plugin-syntax-flow@7.23.3)(@babel/plugin-transform-react-jsx@7.23.4)(eslint@8.44.0)(jest@27.5.1)(typescript@5.4.3) + eslint-config-react-app: 7.0.1(@babel/plugin-syntax-flow@7.23.3)(@babel/plugin-transform-react-jsx@7.23.4)(eslint@8.44.0)(jest@27.5.1)(typescript@5.6.2) eslint-webpack-plugin: 3.2.0(eslint@8.44.0)(webpack@5.91.0) file-loader: 6.2.0(webpack@5.91.0) fs-extra: 10.1.0 @@ -17761,7 +17761,7 @@ packages: prompts: 2.4.2 react: 18.2.0 react-app-polyfill: 3.0.0 - react-dev-utils: 12.0.1(eslint@8.44.0)(typescript@5.4.3)(webpack@5.91.0) + react-dev-utils: 12.0.1(eslint@8.44.0)(typescript@5.6.2)(webpack@5.91.0) react-refresh: 0.11.0 resolve: 1.22.8 resolve-url-loader: 4.0.0 @@ -17771,7 +17771,7 @@ packages: style-loader: 3.3.4(webpack@5.91.0) tailwindcss: 3.4.7 terser-webpack-plugin: 5.3.10(esbuild@0.21.3)(webpack@5.91.0) - typescript: 5.4.3 + typescript: 5.6.2 webpack: 5.91.0(esbuild@0.21.3) webpack-dev-server: 4.15.2(webpack@5.91.0) webpack-manifest-plugin: 4.1.1(webpack@5.91.0) @@ -19076,7 +19076,7 @@ packages: '@emotion/styled': 10.3.0(@emotion/core@10.3.1)(react@18.2.0) '@material-ui/core': 4.12.4(@types/react@18.2.45)(react-dom@18.2.0)(react@18.2.0) '@storybook/addons': 6.5.16(react-dom@18.2.0)(react@18.2.0) - '@storybook/react': 8.2.9(react-dom@18.2.0)(react@18.2.0)(storybook@8.2.9)(typescript@5.4.3) + '@storybook/react': 8.2.9(react-dom@18.2.0)(react@18.2.0)(storybook@8.2.9)(typescript@5.6.2) '@usulpro/color-picker': 1.1.4(react@18.2.0) global: 4.4.0 js-beautify: 1.15.1 @@ -19833,7 +19833,7 @@ packages: tslib: 2.6.3 dev: false - /ts-jest@29.1.3(@babel/core@7.22.5)(esbuild@0.21.3)(jest@27.5.1)(typescript@5.4.3): + /ts-jest@29.1.3(@babel/core@7.22.5)(esbuild@0.21.3)(jest@27.5.1)(typescript@5.6.2): resolution: {integrity: sha512-6L9qz3ginTd1NKhOxmkP0qU3FyKjj5CPoY+anszfVn6Pmv/RIKzhiMCsH7Yb7UvJR9I2A64rm4zQl531s2F1iw==} engines: {node: ^14.15.0 || ^16.10.0 || ^18.0.0 || >=20.0.0} hasBin: true @@ -19867,11 +19867,11 @@ packages: lodash.memoize: 4.1.2 make-error: 1.3.6 semver: 7.6.3 - typescript: 5.4.3 + typescript: 5.6.2 yargs-parser: 21.1.1 dev: true - /ts-pnp@1.2.0(typescript@5.4.3): + /ts-pnp@1.2.0(typescript@5.6.2): resolution: {integrity: sha512-csd+vJOb/gkzvcCHgTGSChYpy5f1/XKNsmvBGO4JXS+z1v2HobugDz4s1IeFXM3wZB44uczs+eazB5Q/ccdhQw==} engines: {node: '>=6'} peerDependencies: @@ -19880,14 +19880,14 @@ packages: typescript: optional: true dependencies: - typescript: 5.4.3 + typescript: 5.6.2 dev: true /ts-toolbelt@6.15.5: resolution: {integrity: sha512-FZIXf1ksVyLcfr7M317jbB67XFJhOO1YqdTcuGaq9q5jLUoTikukZ+98TPjKiP2jC5CgmYdWWYs0s2nLSU0/1A==} dev: true - /tsconfck@3.1.1(typescript@5.4.3): + /tsconfck@3.1.1(typescript@5.6.2): resolution: {integrity: sha512-00eoI6WY57SvZEVjm13stEVE90VkEdJAFGgpFLTsZbJyW/LwFQ7uQxJHWpZ2hzSWgCPKc9AnBnNP+0X7o3hAmQ==} engines: {node: ^18 || >=20} hasBin: true @@ -19897,7 +19897,7 @@ packages: typescript: optional: true dependencies: - typescript: 5.4.3 + typescript: 5.6.2 dev: true /tsconfig-paths-webpack-plugin@4.0.1: @@ -19934,14 +19934,14 @@ packages: /tslib@2.6.3: resolution: {integrity: sha512-xNvxJEOUiWPGhUuUdQgAJPKOOJfGnIyKySOc09XkKsgdUV/3E2zvwZYdejjmRgPCgcym1juLH3226yA7sEFJKQ==} - /tsutils@3.21.0(typescript@5.4.3): + /tsutils@3.21.0(typescript@5.6.2): resolution: {integrity: sha512-mHKK3iUXL+3UF6xL5k0PEhKRUBKPBCv/+RkEOpjRWxxx27KKRBmmA60A9pgOUvMi8GKhRMPEmjBRPzs2W7O1OA==} engines: {node: '>= 6'} peerDependencies: typescript: '>=2.8.0 || >= 3.2.0-dev || >= 3.3.0-dev || >= 3.4.0-dev || >= 3.5.0-dev || >= 3.6.0-dev || >= 3.6.0-beta || >= 3.7.0-dev || >= 3.7.0-beta' dependencies: tslib: 1.14.1 - typescript: 5.4.3 + typescript: 5.6.2 dev: true /type-check@0.3.2: @@ -20061,8 +20061,8 @@ packages: dependencies: is-typedarray: 1.0.0 - /typescript@5.4.3: - resolution: {integrity: sha512-KrPd3PKaCLr78MalgiwJnA25Nm8HAmdwN3mYUYZgG/wizIo9EainNVQI9/yDavtVFRN2h3k8uf3GLHuhDMgEHg==} + /typescript@5.6.2: + resolution: {integrity: sha512-NW8ByodCSNCwZeghjN3o+JX5OFH0Ojg6sadjEKY4huZ52TqbJTJnDo5+Tw98lSy63NZvi4n+ez5m2u5d4PkZyw==} engines: {node: '>=14.17'} hasBin: true @@ -20489,13 +20489,13 @@ packages: - terser dev: true - /vite-plugin-svgr@4.2.0(rollup@2.79.1)(typescript@5.4.3)(vite@5.2.11): + /vite-plugin-svgr@4.2.0(rollup@2.79.1)(typescript@5.6.2)(vite@5.2.11): resolution: {integrity: sha512-SC7+FfVtNQk7So0XMjrrtLAbEC8qjFPifyD7+fs/E6aaNdVde6umlVVh0QuwDLdOMu7vp5RiGFsB70nj5yo0XA==} peerDependencies: vite: ^2.6.0 || 3 || 4 || 5 dependencies: '@rollup/pluginutils': 5.1.0(rollup@2.79.1) - '@svgr/core': 8.1.0(typescript@5.4.3) + '@svgr/core': 8.1.0(typescript@5.6.2) '@svgr/plugin-jsx': 8.1.0(@svgr/core@8.1.0) vite: 5.2.11(@types/node@17.0.45)(sass@1.71.1) transitivePeerDependencies: @@ -20504,7 +20504,7 @@ packages: - typescript dev: false - /vite-tsconfig-paths@4.3.2(typescript@5.4.3)(vite@5.2.11): + /vite-tsconfig-paths@4.3.2(typescript@5.6.2)(vite@5.2.11): resolution: {integrity: sha512-0Vd/a6po6Q+86rPlntHye7F31zA2URZMbH8M3saAZ/xR9QoGN/L21bxEGfXdWmFdNkqPpRdxFT7nmNe12e9/uA==} peerDependencies: vite: '*' @@ -20514,7 +20514,7 @@ packages: dependencies: debug: 4.3.6 globrex: 0.1.2 - tsconfck: 3.1.1(typescript@5.4.3) + tsconfck: 3.1.1(typescript@5.6.2) vite: 5.2.11(@types/node@17.0.45)(sass@1.71.1) transitivePeerDependencies: - supports-color @@ -21079,7 +21079,6 @@ packages: /workbox-google-analytics@6.6.0: resolution: {integrity: sha512-p4DJa6OldXWd6M9zRl0H6vB9lkrmqYFkRQ2xEiNdBFp9U0LhsGO7hsBscVEyH9H2/3eZZt8c97NB2FD9U2NJ+Q==} - deprecated: It is not compatible with newer versions of GA starting with v4, as long as you are using GAv3 it should be ok, but the package is not longer being maintained dependencies: workbox-background-sync: 6.6.0 workbox-core: 6.6.0 diff --git a/editor.planx.uk/src/ui/shared/ReactMarkdownOrHtml.tsx b/editor.planx.uk/src/ui/shared/ReactMarkdownOrHtml.tsx index 0a3c0f7420..f6683b2ed9 100644 --- a/editor.planx.uk/src/ui/shared/ReactMarkdownOrHtml.tsx +++ b/editor.planx.uk/src/ui/shared/ReactMarkdownOrHtml.tsx @@ -57,7 +57,7 @@ export default function ReactMarkdownOrHtml(props: { if (typeof props.source !== "string") { return null; } - if (props.source.includes("=14.17'} hasBin: true dev: true From 80b88d3360af485e158c52446470ce619fb01bc2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Dafydd=20Ll=C5=B7r=20Pearson?= Date: Wed, 11 Sep 2024 10:45:50 +0100 Subject: [PATCH 2/5] feat: Data value search (#3646) --- editor.planx.uk/src/hooks/useSearch.ts | 22 +- .../Sidebar/Search/NodeSearchResults.tsx | 46 +- .../SearchResultCard/DataDisplayMap.test.ts | 126 ++++ .../SearchResultCard/DataDisplayMap.tsx | 116 +++ .../index.tsx} | 47 +- .../Sidebar/Search/mocks/DataDisplayMap.ts | 679 ++++++++++++++++++ .../components/Sidebar/Search/mocks/simple.ts | 2 + 7 files changed, 962 insertions(+), 76 deletions(-) create mode 100644 editor.planx.uk/src/pages/FlowEditor/components/Sidebar/Search/SearchResultCard/DataDisplayMap.test.ts create mode 100644 editor.planx.uk/src/pages/FlowEditor/components/Sidebar/Search/SearchResultCard/DataDisplayMap.tsx rename editor.planx.uk/src/pages/FlowEditor/components/Sidebar/Search/{SearchResultCard.tsx => SearchResultCard/index.tsx} (57%) create mode 100644 editor.planx.uk/src/pages/FlowEditor/components/Sidebar/Search/mocks/DataDisplayMap.ts diff --git a/editor.planx.uk/src/hooks/useSearch.ts b/editor.planx.uk/src/hooks/useSearch.ts index 16dbe22f37..a186fe707f 100644 --- a/editor.planx.uk/src/hooks/useSearch.ts +++ b/editor.planx.uk/src/hooks/useSearch.ts @@ -9,7 +9,8 @@ interface UseSearchProps { export interface SearchResult { item: T; key: string; - matchIndices?: [number, number][]; + matchIndices: [number, number][]; + refIndex: number; } export type SearchResults = SearchResult[]; @@ -39,13 +40,18 @@ export const useSearch = ({ useEffect(() => { const fuseResults = fuse.search(pattern); setResults( - fuseResults.map((result) => ({ - item: result.item, - key: result.matches?.[0].key || "", - // We only display the first match - matchIndices: - (result.matches?.[0].indices as [number, number][]) || undefined, - })), + fuseResults.map((result) => { + // Required type narrowing for FuseResult + if (!result.matches) throw Error("Matches missing from FuseResults"); + + return { + item: result.item, + key: result.matches?.[0].key || "", + // We only display the first match + matchIndices: result.matches[0].indices as [number, number][], + refIndex: result.matches[0]?.refIndex || 0, + }; + }), ); }, [pattern, fuse]); diff --git a/editor.planx.uk/src/pages/FlowEditor/components/Sidebar/Search/NodeSearchResults.tsx b/editor.planx.uk/src/pages/FlowEditor/components/Sidebar/Search/NodeSearchResults.tsx index c8dfedceaf..7672884536 100644 --- a/editor.planx.uk/src/pages/FlowEditor/components/Sidebar/Search/NodeSearchResults.tsx +++ b/editor.planx.uk/src/pages/FlowEditor/components/Sidebar/Search/NodeSearchResults.tsx @@ -2,7 +2,7 @@ import List from "@mui/material/List"; import ListItem from "@mui/material/ListItem"; import { styled } from "@mui/material/styles"; import Typography from "@mui/material/Typography"; -import { ComponentType, IndexedNode } from "@opensystemslab/planx-core/types"; +import { IndexedNode } from "@opensystemslab/planx-core/types"; import type { SearchResults } from "hooks/useSearch"; import React from "react"; @@ -17,32 +17,20 @@ export const Root = styled(List)(({ theme }) => ({ export const NodeSearchResults: React.FC<{ results: SearchResults; -}> = ({ results }) => { - /** Temporary guard function to filter out component types not yet supported by SearchResultCard */ - const isSupportedNodeType = ( - result: SearchResults[number], - ): boolean => - ![ - ComponentType.FileUploadAndLabel, - ComponentType.Calculate, - ComponentType.List, - ].includes(result.item.type); +}> = ({ results }) => ( + <> + + {!results.length && "No matches found"} + {results.length === 1 && "1 result:"} + {results.length > 1 && `${results.length} results:`} + - return ( - <> - - {!results.length && "No matches found"} - {results.length === 1 && "1 result:"} - {results.length > 1 && `${results.length} results:`} - - - - {results.filter(isSupportedNodeType).map((result) => ( - - - - ))} - - - ); -}; + + {results.map((result) => ( + + + + ))} + + +); diff --git a/editor.planx.uk/src/pages/FlowEditor/components/Sidebar/Search/SearchResultCard/DataDisplayMap.test.ts b/editor.planx.uk/src/pages/FlowEditor/components/Sidebar/Search/SearchResultCard/DataDisplayMap.test.ts new file mode 100644 index 0000000000..d8e07894e3 --- /dev/null +++ b/editor.planx.uk/src/pages/FlowEditor/components/Sidebar/Search/SearchResultCard/DataDisplayMap.test.ts @@ -0,0 +1,126 @@ +import { ComponentType } from "@opensystemslab/planx-core/types"; +import { useStore } from "pages/FlowEditor/lib/store"; + +import { + mockAnswerResult, + mockCalculateFormulaResult, + mockCalculateRootResult, + mockFileUploadAndLabelResult, + mockFlow, + mockListAnswerResult, + mockListDataResult, + mockListRootResult, + mockQuestionResult, +} from "../mocks/DataDisplayMap"; +import { getDisplayDetailsForResult } from "./DataDisplayMap"; + +type Output = ReturnType; + +// Setup flow so that it can be referenced by SearchResults (e.g. getting parent nodes) +beforeAll(() => useStore.setState({ flow: mockFlow })); + +describe("Question component", () => { + it("returns the expected display values", () => { + const output = getDisplayDetailsForResult(mockQuestionResult); + + expect(output).toStrictEqual({ + key: "Data", + iconKey: ComponentType.Question, + componentType: "Question", + title: "This is a question component", + headline: "colour", + }); + }); +}); + +describe("Answer component", () => { + it("returns the expected display values", () => { + const output = getDisplayDetailsForResult(mockAnswerResult); + + expect(output).toStrictEqual({ + key: "Option (data)", + iconKey: ComponentType.Question, + componentType: "Question", + title: "This is a question component", + headline: "red", + }); + }); +}); + +describe("List component", () => { + it("handles the root data value", () => { + const output = getDisplayDetailsForResult(mockListRootResult); + + expect(output).toStrictEqual({ + componentType: "List", + headline: "listRoot", + iconKey: ComponentType.List, + key: "Data", + title: "This is a list component", + }); + }); + + it("handles nested data variables", () => { + const output = getDisplayDetailsForResult(mockListDataResult); + + expect(output).toStrictEqual({ + componentType: "List", + headline: "tenure", + iconKey: ComponentType.List, + key: "Data", + title: "This is a list component", + }); + }); + + it("handles nested data variables in Answers", () => { + const output = getDisplayDetailsForResult(mockListAnswerResult); + + expect(output).toStrictEqual({ + componentType: "List", + headline: "selfCustomBuild", + iconKey: ComponentType.List, + key: "Option (data)", + title: "This is a list component", + }); + }); +}); + +describe("Calculate component", () => { + it("handles the output data variables", () => { + const output = getDisplayDetailsForResult(mockCalculateRootResult); + + expect(output).toStrictEqual({ + componentType: "Calculate", + headline: "calculateOutput", + iconKey: ComponentType.Calculate, + key: "Output (data)", + title: "This is a calculate component", + }); + }); + + it("handles the formula data variables", () => { + const output = getDisplayDetailsForResult(mockCalculateFormulaResult); + + expect(output).toStrictEqual({ + componentType: "Calculate", + headline: "formulaOne + formulaTwo", + iconKey: ComponentType.Calculate, + key: "Formula", + title: "This is a calculate component", + }); + }); +}); + +describe("FileUploadAndLabel component", () => { + it("handles the data variables nested in FileTypes", () => { + const output = getDisplayDetailsForResult(mockFileUploadAndLabelResult); + + expect(output).toStrictEqual({ + componentType: "File upload and label", + headline: "floorplan", + iconKey: ComponentType.FileUploadAndLabel, + key: "File type (data)", + title: "This is a FileUploadAndLabel component", + }); + }); +}); diff --git a/editor.planx.uk/src/pages/FlowEditor/components/Sidebar/Search/SearchResultCard/DataDisplayMap.tsx b/editor.planx.uk/src/pages/FlowEditor/components/Sidebar/Search/SearchResultCard/DataDisplayMap.tsx new file mode 100644 index 0000000000..ea0b2b4953 --- /dev/null +++ b/editor.planx.uk/src/pages/FlowEditor/components/Sidebar/Search/SearchResultCard/DataDisplayMap.tsx @@ -0,0 +1,116 @@ +import { ComponentType, IndexedNode } from "@opensystemslab/planx-core/types"; +import { Calculate } from "@planx/components/Calculate/model"; +import { FileUploadAndLabel } from "@planx/components/FileUploadAndLabel/model"; +import { List } from "@planx/components/List/model"; +import { SearchResult } from "hooks/useSearch"; +import { capitalize, get } from "lodash"; +import { SLUGS } from "pages/FlowEditor/data/types"; +import { useStore } from "pages/FlowEditor/lib/store"; + +interface DataDisplayValues { + displayKey: string; + getIconKey: (result: SearchResult) => ComponentType; + getTitle: (result: SearchResult) => string; + getHeadline: (result: SearchResult) => string; + getComponentType: (result: SearchResult) => string; +} + +/** + * Map of data keys to their associated display values + * Uses Partial as not all values are unique, we later apply defaults + */ +type DataKeyMap = Record>; + +/** + * Map of ComponentTypes to their associated data keys + */ +type ComponentMap = Record; + +/** + * Map of ComponentTypes which need specific overrides in order to display their data values + */ +const DISPLAY_DATA: Partial = { + // Answers are mapped to their parent questions + [ComponentType.Answer]: { + default: { + getIconKey: () => ComponentType.Question, + displayKey: "Option (data)", + getTitle: ({ item }) => { + const parentNode = useStore.getState().flow[item.parentId]; + return parentNode.data.text; + }, + getHeadline: ({ item, key }) => get(item, key)?.toString(), + }, + }, + // FileUploadAndLabel has data values nested in FileTypes + [ComponentType.FileUploadAndLabel]: { + default: { + displayKey: "File type (data)", + getHeadline: ({ item, refIndex }) => + (item["data"] as unknown as FileUploadAndLabel)["fileTypes"][refIndex][ + "fn" + ], + }, + }, + // Calculate contains both input and output data values + [ComponentType.Calculate]: { + formula: { + displayKey: "Formula", + getHeadline: ({ item }) => (item.data as unknown as Calculate).formula, + }, + "data.output": { + displayKey: "Output (data)", + getHeadline: ({ item }) => (item.data as unknown as Calculate).output, + }, + }, + // List contains data variables nested within its schema + [ComponentType.List]: { + "data.schema.fields.data.fn": { + getHeadline: ({ item, refIndex }) => + (item.data as unknown as List).schema.fields[refIndex].data.fn, + }, + "data.schema.fields.data.options.data.val": { + displayKey: "Option (data)", + getHeadline: ({ item, refIndex }) => { + // Fuse.js flattens deeply nested arrays when using refIndex + const options = (item.data as unknown as List).schema.fields + .filter((field) => field.type === "question") + .flatMap((field) => field.data.options); + return options[refIndex].data.val || ""; + }, + }, + }, +}; + +/** + * Default values for all ComponentTypes not listed in DISPLAY_DATA + */ +const DEFAULT_DISPLAY_DATA: DataDisplayValues = { + displayKey: "Data", + getIconKey: ({ item }) => item.type, + getTitle: ({ item }) => + (item.data?.title as string) || (item.data?.text as string) || "", + getHeadline: ({ item, key }) => get(item, key)?.toString() || "", + getComponentType: ({ item }) => + capitalize(SLUGS[item.type].replaceAll("-", " ")), +}; + +export const getDisplayDetailsForResult = ( + result: SearchResult, +) => { + const componentMap = DISPLAY_DATA[result.item.type]; + const keyMap = componentMap?.[result.key] || componentMap?.default || {}; + + const data: DataDisplayValues = { + ...DEFAULT_DISPLAY_DATA, + ...keyMap, + }; + + return { + iconKey: data.getIconKey(result), + componentType: data.getComponentType(result), + title: data.getTitle(result), + key: data.displayKey, + headline: data.getHeadline(result), + }; +}; diff --git a/editor.planx.uk/src/pages/FlowEditor/components/Sidebar/Search/SearchResultCard.tsx b/editor.planx.uk/src/pages/FlowEditor/components/Sidebar/Search/SearchResultCard/index.tsx similarity index 57% rename from editor.planx.uk/src/pages/FlowEditor/components/Sidebar/Search/SearchResultCard.tsx rename to editor.planx.uk/src/pages/FlowEditor/components/Sidebar/Search/SearchResultCard/index.tsx index 30f5621d2a..8478f41819 100644 --- a/editor.planx.uk/src/pages/FlowEditor/components/Sidebar/Search/SearchResultCard.tsx +++ b/editor.planx.uk/src/pages/FlowEditor/components/Sidebar/Search/SearchResultCard/index.tsx @@ -2,16 +2,14 @@ import Box from "@mui/material/Box"; import ListItemButton from "@mui/material/ListItemButton"; import { styled } from "@mui/material/styles"; import Typography from "@mui/material/Typography"; -import { ComponentType, IndexedNode } from "@opensystemslab/planx-core/types"; +import { IndexedNode } from "@opensystemslab/planx-core/types"; import { ICONS } from "@planx/components/ui"; import type { SearchResult } from "hooks/useSearch"; -import { capitalize, get } from "lodash"; -import { SLUGS } from "pages/FlowEditor/data/types"; -import { useStore } from "pages/FlowEditor/lib/store"; import React from "react"; import { FONT_WEIGHT_SEMI_BOLD } from "theme"; -import { Headline } from "./Headline"; +import { Headline } from "../Headline"; +import { getDisplayDetailsForResult } from "./DataDisplayMap"; export const Root = styled(ListItemButton)(({ theme }) => ({ padding: theme.spacing(1), @@ -22,42 +20,13 @@ export const Root = styled(ListItemButton)(({ theme }) => ({ export const SearchResultCard: React.FC<{ result: SearchResult; }> = ({ result }) => { - const getDisplayDetailsForResult = ({ - item, - key, - }: SearchResult) => { - const componentType = capitalize( - SLUGS[result.item.type].replaceAll("-", " "), - ); - let title = (item.data?.title as string) || (item.data?.text as string); - let Icon = ICONS[item.type]; // TODO: Generate display key from key - - let displayKey = "Data"; - const headline = get(item, key).toString() || ""; - - // For Answer nodes, update display values to match the parent question - if (item.type === ComponentType.Answer) { - const parentNode = useStore.getState().flow[item.parentId]; - Icon = ICONS[ComponentType.Question]; - title = parentNode!.data.text!; - displayKey = "Option (data)"; - } - - return { - Icon, - componentType, - title, - key: displayKey, - headline, - }; - }; - - const { Icon, componentType, title, key, headline } = - getDisplayDetailsForResult(result); // TODO - display portal wrapper + // TODO - display portal wrapper + const { iconKey, componentType, title, key, headline } = + getDisplayDetailsForResult(result); + const Icon = ICONS[iconKey]; const handleClick = () => { - console.log("todo!"); - console.log({ nodeId: result.item.id }); + console.log({ result }); // get path for node // generate url from path // navigate to url diff --git a/editor.planx.uk/src/pages/FlowEditor/components/Sidebar/Search/mocks/DataDisplayMap.ts b/editor.planx.uk/src/pages/FlowEditor/components/Sidebar/Search/mocks/DataDisplayMap.ts new file mode 100644 index 0000000000..44f4efeb23 --- /dev/null +++ b/editor.planx.uk/src/pages/FlowEditor/components/Sidebar/Search/mocks/DataDisplayMap.ts @@ -0,0 +1,679 @@ +import { FlowGraph, IndexedNode } from "@opensystemslab/planx-core/types"; + +import { SearchResult } from "../../../../../../hooks/useSearch"; + +/** Simple flow which contains an example of each component which has unique rules for finding data values and displaying these as search results */ +export const mockFlow: FlowGraph = { + _root: { + edges: ["UMJi4q9zud", "Xj4E14wvd6", "zryBH8H7vD", "Flfg7UnuhH"], + }, + "3W0WyymBuj": { + data: { + val: "blue", + text: "Blue", + }, + type: 200, + }, + Flfg7UnuhH: { + data: { + title: "This is a FileUploadAndLabel component", + fileTypes: [ + { + fn: "floorplan", + name: "Floorplan", + rule: { + condition: "AlwaysRequired", + }, + }, + ], + hideDropZone: false, + }, + type: 145, + }, + UMJi4q9zud: { + data: { + fn: "colour", + text: "This is a question component", + }, + type: 100, + edges: ["th2EEQ03a7", "3W0WyymBuj"], + }, + Xj4E14wvd6: { + data: { + fn: "listRoot", + title: "This is a list component", + schema: { + min: 1, + type: "Existing residential unit type", + fields: [ + { + data: { + fn: "type", + title: "What best describes the type of this unit?", + options: [ + { + id: "house", + data: { + val: "house", + text: "House", + }, + }, + { + id: "flat", + data: { + val: "flat", + text: "Flat, apartment or maisonette", + }, + }, + { + id: "sheltered", + data: { + val: "sheltered", + text: "Sheltered housing", + }, + }, + { + id: "studio", + data: { + val: "studio", + text: "Studio or bedsit", + }, + }, + { + id: "cluster", + data: { + val: "cluster", + text: "Cluster flat", + }, + }, + { + id: "other", + data: { + val: "other", + text: "Other", + }, + }, + ], + }, + type: "question", + }, + { + data: { + fn: "tenure", + title: "What best describes the tenure of this unit?", + options: [ + { + id: "MH", + data: { + val: "MH", + text: "Market housing", + }, + }, + { + id: "SAIR", + data: { + val: "SAIR", + text: "Social, affordable or interim rent", + }, + }, + { + id: "AHO", + data: { + val: "AHO", + text: "Affordable home ownership", + }, + }, + { + id: "SH", + data: { + val: "SH", + text: "Starter homes", + }, + }, + { + id: "selfCustomBuild", + data: { + val: "selfCustomBuild", + text: "Self-build and custom build", + }, + }, + { + id: "other", + data: { + val: "other", + text: "Other", + }, + }, + ], + }, + type: "question", + }, + { + data: { + fn: "bedrooms", + title: "How many bedrooms does this unit have?", + allowNegatives: false, + }, + type: "number", + }, + { + data: { + fn: "identicalUnits", + title: + "How many units of the type described above exist on the site?", + allowNegatives: false, + }, + type: "number", + }, + ], + }, + schemaName: "Residential units - Existing", + }, + type: 800, + }, + th2EEQ03a7: { + data: { + val: "red", + text: "Red", + }, + type: 200, + }, + zryBH8H7vD: { + data: { + title: "This is a calculate component", + output: "calculateOutput", + formula: "formulaOne + formulaTwo", + defaults: { + formulaOne: "1", + formulaTwo: "1", + }, + formatOutputForAutomations: false, + samples: {}, + }, + type: 700, + }, +}; + +export const mockQuestionResult: SearchResult = { + item: { + id: "UMJi4q9zud", + parentId: "_root", + type: 100, + edges: ["th2EEQ03a7", "3W0WyymBuj"], + data: { + fn: "colour", + text: "This is a question component", + }, + }, + key: "data.fn", + matchIndices: [[0, 3]], + refIndex: 0, +}; + +export const mockAnswerResult: SearchResult = { + item: { + id: "th2EEQ03a7", + parentId: "UMJi4q9zud", + type: 200, + data: { + text: "Red", + val: "red", + }, + }, + key: "data.val", + matchIndices: [[0, 2]], + refIndex: 0, +}; + +export const mockListRootResult: SearchResult = { + item: { + id: "Xj4E14wvd6", + parentId: "_root", + type: 800, + data: { + fn: "listRoot", + title: "This is a list component", + schema: { + min: 1, + type: "Tree type", + fields: [ + { + data: { + fn: "species", + type: "short", + title: "Species", + }, + type: "text", + }, + { + data: { + fn: "work", + type: "short", + title: "Proposed work", + }, + type: "text", + }, + { + data: { + fn: "justification", + type: "short", + title: "Justification", + }, + type: "text", + }, + { + data: { + fn: "urgency", + title: "Urgency", + options: [ + { + id: "low", + data: { + val: "low", + text: "Low", + }, + }, + { + id: "moderate", + data: { + val: "moderate", + text: "Moderate", + }, + }, + { + id: "high", + data: { + val: "high", + text: "High", + }, + }, + { + id: "urgent", + data: { + val: "urgent", + text: "Urgent", + }, + }, + ], + }, + type: "question", + }, + { + data: { + fn: "completionDate", + title: "Expected completion date", + }, + type: "date", + }, + { + data: { + fn: "features", + title: "Where is it? Plot as many as apply", + mapOptions: { + basemap: "MapboxSatellite", + drawMany: true, + drawType: "Point", + drawColor: "#66ff00", + }, + }, + type: "map", + }, + ], + }, + schemaName: "Trees", + }, + }, + key: "data.fn", + matchIndices: [[0, 7]], + refIndex: 0, +}; + +export const mockListDataResult: SearchResult = { + item: { + id: "Xj4E14wvd6", + parentId: "_root", + type: 800, + data: { + fn: "listRoot", + title: "This is a list component", + schema: { + min: 1, + type: "Existing residential unit type", + fields: [ + { + data: { + fn: "type", + title: "What best describes the type of this unit?", + options: [ + { + id: "house", + data: { + val: "house", + text: "House", + }, + }, + { + id: "flat", + data: { + val: "flat", + text: "Flat, apartment or maisonette", + }, + }, + { + id: "sheltered", + data: { + val: "sheltered", + text: "Sheltered housing", + }, + }, + { + id: "studio", + data: { + val: "studio", + text: "Studio or bedsit", + }, + }, + { + id: "cluster", + data: { + val: "cluster", + text: "Cluster flat", + }, + }, + { + id: "other", + data: { + val: "other", + text: "Other", + }, + }, + ], + }, + type: "question", + }, + { + data: { + fn: "tenure", + title: "What best describes the tenure of this unit?", + options: [ + { + id: "MH", + data: { + val: "MH", + text: "Market housing", + }, + }, + { + id: "SAIR", + data: { + val: "SAIR", + text: "Social, affordable or interim rent", + }, + }, + { + id: "AHO", + data: { + val: "AHO", + text: "Affordable home ownership", + }, + }, + { + id: "SH", + data: { + val: "SH", + text: "Starter homes", + }, + }, + { + id: "selfCustomBuild", + data: { + val: "selfCustomBuild", + text: "Self-build and custom build", + }, + }, + { + id: "other", + data: { + val: "other", + text: "Other", + }, + }, + ], + }, + type: "question", + }, + { + data: { + fn: "bedrooms", + title: "How many bedrooms does this unit have?", + allowNegatives: false, + }, + type: "number", + }, + { + data: { + fn: "identicalUnits", + title: + "How many units of the type described above exist on the site?", + allowNegatives: false, + }, + type: "number", + }, + ], + }, + schemaName: "Residential units - Existing", + }, + }, + key: "data.schema.fields.data.fn", + matchIndices: [[0, 5]], + refIndex: 1, +}; + +export const mockListAnswerResult: SearchResult = { + item: { + id: "Xj4E14wvd6", + parentId: "_root", + type: 800, + data: { + fn: "listRoot", + title: "This is a list component", + schema: { + min: 1, + type: "Existing residential unit type", + fields: [ + { + data: { + fn: "type", + title: "What best describes the type of this unit?", + options: [ + { + id: "house", + data: { + val: "house", + text: "House", + }, + }, + { + id: "flat", + data: { + val: "flat", + text: "Flat, apartment or maisonette", + }, + }, + { + id: "sheltered", + data: { + val: "sheltered", + text: "Sheltered housing", + }, + }, + { + id: "studio", + data: { + val: "studio", + text: "Studio or bedsit", + }, + }, + { + id: "cluster", + data: { + val: "cluster", + text: "Cluster flat", + }, + }, + { + id: "other", + data: { + val: "other", + text: "Other", + }, + }, + ], + }, + type: "question", + }, + { + data: { + fn: "tenure", + title: "What best describes the tenure of this unit?", + options: [ + { + id: "MH", + data: { + val: "MH", + text: "Market housing", + }, + }, + { + id: "SAIR", + data: { + val: "SAIR", + text: "Social, affordable or interim rent", + }, + }, + { + id: "AHO", + data: { + val: "AHO", + text: "Affordable home ownership", + }, + }, + { + id: "SH", + data: { + val: "SH", + text: "Starter homes", + }, + }, + { + id: "selfCustomBuild", + data: { + val: "selfCustomBuild", + text: "Self-build and custom build", + }, + }, + { + id: "other", + data: { + val: "other", + text: "Other", + }, + }, + ], + }, + type: "question", + }, + { + data: { + fn: "bedrooms", + title: "How many bedrooms does this unit have?", + allowNegatives: false, + }, + type: "number", + }, + { + data: { + fn: "identicalUnits", + title: + "How many units of the type described above exist on the site?", + allowNegatives: false, + }, + type: "number", + }, + ], + }, + schemaName: "Residential units - Existing", + }, + }, + key: "data.schema.fields.data.options.data.val", + matchIndices: [[0, 14]], + refIndex: 10, +}; + +export const mockCalculateRootResult: SearchResult = { + item: { + id: "zryBH8H7vD", + parentId: "_root", + type: 700, + data: { + title: "This is a calculate component", + output: "calculateOutput", + formula: "formulaOne + formulaTwo", + defaults: { + formulaOne: "1", + formulaTwo: "1", + }, + formatOutputForAutomations: false, + samples: {}, + }, + }, + key: "data.output", + matchIndices: [[0, 14]], + refIndex: 0, +}; + +export const mockCalculateFormulaResult: SearchResult = { + item: { + id: "zryBH8H7vD", + parentId: "_root", + type: 700, + data: { + title: "This is a calculate component", + output: "calculateOutput", + formula: "formulaOne + formulaTwo", + defaults: { + formulaOne: "1", + formulaTwo: "1", + }, + formatOutputForAutomations: false, + samples: {}, + }, + }, + key: "formula", + matchIndices: [[0, 6]], + refIndex: 1, +}; + +export const mockFileUploadAndLabelResult: SearchResult = { + item: { + id: "Flfg7UnuhH", + parentId: "_root", + type: 145, + data: { + title: "This is a FileUploadAndLabel component", + fileTypes: [ + { + fn: "floorplan", + name: "Floorplan", + rule: { + condition: "AlwaysRequired", + }, + }, + ], + hideDropZone: false, + }, + }, + key: "data.fileTypes.fn", + matchIndices: [[0, 8]], + refIndex: 0, +}; diff --git a/editor.planx.uk/src/pages/FlowEditor/components/Sidebar/Search/mocks/simple.ts b/editor.planx.uk/src/pages/FlowEditor/components/Sidebar/Search/mocks/simple.ts index 275c397f34..391b4bec02 100644 --- a/editor.planx.uk/src/pages/FlowEditor/components/Sidebar/Search/mocks/simple.ts +++ b/editor.planx.uk/src/pages/FlowEditor/components/Sidebar/Search/mocks/simple.ts @@ -49,6 +49,7 @@ export const results: SearchResults = [ }, key: "data.val", matchIndices: [[0, 2]], + refIndex: 0, }, { item: { @@ -62,5 +63,6 @@ export const results: SearchResults = [ }, key: "data.val", matchIndices: [[0, 2]], + refIndex: 0, }, ]; From a319b068fac217bc8572f4a42e1e7d303f1a87b9 Mon Sep 17 00:00:00 2001 From: Rory Doak <138574807+RODO94@users.noreply.github.com> Date: Wed, 11 Sep 2024 13:11:47 +0100 Subject: [PATCH 3/5] refactor: drop redundant columns from `teams` table (#3648) --- hasura.planx.uk/metadata/tables.yaml | 34 ---------------- .../down.sql | 39 +++++++++++++++++++ .../up.sql | 32 +++++++++++++++ scripts/seed-database/write/teams.sql | 24 ++---------- 4 files changed, 75 insertions(+), 54 deletions(-) create mode 100644 hasura.planx.uk/migrations/1725965297806_alter_table_public_teams_drop_column_notify_personalisation/down.sql create mode 100644 hasura.planx.uk/migrations/1725965297806_alter_table_public_teams_drop_column_notify_personalisation/up.sql diff --git a/hasura.planx.uk/metadata/tables.yaml b/hasura.planx.uk/metadata/tables.yaml index 017b9b3ac6..fd81722e10 100644 --- a/hasura.planx.uk/metadata/tables.yaml +++ b/hasura.planx.uk/metadata/tables.yaml @@ -2035,43 +2035,27 @@ table: name: team_members schema: public - computed_fields: - - name: boundary_bbox - definition: - function: - name: boundary_bbox - schema: public - comment: Bounding box of the team's full boundary insert_permissions: - role: platformAdmin permission: check: {} columns: - - boundary - created_at - domain - id - name - - notify_personalisation - - settings - slug - updated_at select_permissions: - role: api permission: columns: - - boundary - created_at - domain - id - name - - notify_personalisation - - reference_code - - settings - slug - updated_at - computed_fields: - - boundary_bbox filter: {} - role: platformAdmin permission: @@ -2080,29 +2064,18 @@ - domain - id - name - - notify_personalisation - - reference_code - - settings - slug - updated_at - computed_fields: - - boundary_bbox filter: {} - role: public permission: columns: - - boundary - created_at - domain - id - name - - notify_personalisation - - reference_code - - settings - slug - updated_at - computed_fields: - - boundary_bbox filter: {} - role: teamEditor permission: @@ -2111,13 +2084,8 @@ - domain - id - name - - notify_personalisation - - reference_code - - settings - slug - updated_at - computed_fields: - - boundary_bbox filter: {} update_permissions: - role: platformAdmin @@ -2125,8 +2093,6 @@ columns: - domain - name - - notify_personalisation - - settings - slug filter: {} check: null diff --git a/hasura.planx.uk/migrations/1725965297806_alter_table_public_teams_drop_column_notify_personalisation/down.sql b/hasura.planx.uk/migrations/1725965297806_alter_table_public_teams_drop_column_notify_personalisation/down.sql new file mode 100644 index 0000000000..9e5305ca49 --- /dev/null +++ b/hasura.planx.uk/migrations/1725965297806_alter_table_public_teams_drop_column_notify_personalisation/down.sql @@ -0,0 +1,39 @@ +alter table "public"."teams" add column "notify_personalisation" jsonb; + +alter table "public"."teams" add column "settings" jsonb; +alter table "public"."teams" alter column "settings" set default '{"externalPlanningSite": {"url": "https://www.planningportal.co.uk/", "name": "Planning Portal"}}'::jsonb; + +alter table "public"."teams" add column "reference_code" text; +comment on column "public"."teams"."reference_code" is E'Organisation reference code sourced from planning.data.gov.uk/dataset/local-authority'; + +alter table "public"."teams" add column "boundary" jsonb; +alter table "public"."teams" alter column "boundary" set default '{"type": "Feature", "geometry": {"type": "Polygon", "coordinates": [[[1.9134116, 49.528423], [1.9134116, 61.331151], [1.9134116, 61.331151], [-10.76418, 61.331151], [-10.76418, 49.528423]]]}, "properties": {}}'::jsonb; +comment on column "public"."teams"."boundary" is E'GeoJSON boundary for team provided by planning.data.gov.uk. Simplified boundary_bbox should generally by used where possible. Defaults to UK extent.'; + + +CREATE OR REPLACE VIEW "public"."teams_summary" AS + SELECT t.id, + t.name, + t.slug, + t.reference_code, + ts.homepage AS homepage, + t.domain AS subdomain, + ti.has_planning_data AS planning_data_enabled, + '@todo'::text AS article_4s_enabled, + jsonb_build_object('helpEmail', ts.help_email, 'helpPhone', ts.help_phone, 'emailReplyToId', ts.email_reply_to_id, 'helpOpeningHours', ts.help_opening_hours) AS govnotify_personalisation, + CASE + WHEN (COALESCE(ti.production_govpay_secret, ti.staging_govpay_secret) IS NOT NULL) THEN true + ELSE false + END AS govpay_enabled, + ts.submission_email AS send_to_email_address, + COALESCE(ti.production_bops_submission_url, ti.staging_bops_submission_url) AS bops_submission_url, + tt.logo, + tt.favicon, + tt.primary_colour, + tt.link_colour, + tt.action_colour + FROM (((teams t + JOIN team_integrations ti ON ((ti.team_id = t.id))) + JOIN team_themes tt ON ((tt.team_id = t.id))) + JOIN team_settings ts ON ((ts.team_id = t.id))) + ORDER BY t.name; \ No newline at end of file diff --git a/hasura.planx.uk/migrations/1725965297806_alter_table_public_teams_drop_column_notify_personalisation/up.sql b/hasura.planx.uk/migrations/1725965297806_alter_table_public_teams_drop_column_notify_personalisation/up.sql new file mode 100644 index 0000000000..ded4b05a22 --- /dev/null +++ b/hasura.planx.uk/migrations/1725965297806_alter_table_public_teams_drop_column_notify_personalisation/up.sql @@ -0,0 +1,32 @@ + +CREATE OR REPLACE VIEW "public"."teams_summary" AS + SELECT t.id, + t.name, + t.slug, + ts.reference_code, + ts.homepage AS homepage, + t.domain AS subdomain, + ti.has_planning_data AS planning_data_enabled, + '@todo'::text AS article_4s_enabled, + jsonb_build_object('helpEmail', ts.help_email, 'helpPhone', ts.help_phone, 'emailReplyToId', ts.email_reply_to_id, 'helpOpeningHours', ts.help_opening_hours) AS govnotify_personalisation, + CASE + WHEN (COALESCE(ti.production_govpay_secret, ti.staging_govpay_secret) IS NOT NULL) THEN true + ELSE false + END AS govpay_enabled, + ts.submission_email AS send_to_email_address, + COALESCE(ti.production_bops_submission_url, ti.staging_bops_submission_url) AS bops_submission_url, + tt.logo, + tt.favicon, + tt.primary_colour, + tt.link_colour, + tt.action_colour + FROM (((teams t + JOIN team_integrations ti ON ((ti.team_id = t.id))) + JOIN team_themes tt ON ((tt.team_id = t.id))) + JOIN team_settings ts ON ((ts.team_id = t.id))) + ORDER BY t.name; + +alter table "public"."teams" drop column "notify_personalisation" cascade; +alter table "public"."teams" drop column "settings" cascade; +alter table "public"."teams" drop column "reference_code" cascade; +alter table "public"."teams" drop column "boundary" cascade; \ No newline at end of file diff --git a/scripts/seed-database/write/teams.sql b/scripts/seed-database/write/teams.sql index 5a8e6bc5dc..d744bc9dd8 100644 --- a/scripts/seed-database/write/teams.sql +++ b/scripts/seed-database/write/teams.sql @@ -5,11 +5,7 @@ CREATE TEMPORARY TABLE sync_teams ( slug text, created_at timestamptz, updated_at timestamptz, - settings jsonb, - notify_personalisation jsonb, - domain text, - boundary jsonb, - reference_code text + domain text ); \copy sync_teams FROM '/tmp/teams.csv' WITH (FORMAT csv, DELIMITER ';'); @@ -17,28 +13,16 @@ CREATE TEMPORARY TABLE sync_teams ( INSERT INTO teams ( id, name, - slug, - settings, - notify_personalisation, - boundary, - reference_code + slug ) SELECT id, name, - slug, - settings, - notify_personalisation, - boundary, - reference_code + slug FROM sync_teams ON CONFLICT (id) DO UPDATE SET name = EXCLUDED.name, - slug = EXCLUDED.slug, - settings = EXCLUDED.settings, - notify_personalisation = EXCLUDED.notify_personalisation, - boundary = EXCLUDED.boundary, - reference_code = EXCLUDED.reference_code; + slug = EXCLUDED.slug; SELECT setval('teams_id_seq', max(id)) FROM teams; From 579c7bafe533a0fc0b60ec5585f07cf383603967 Mon Sep 17 00:00:00 2001 From: Rory Doak <138574807+RODO94@users.noreply.github.com> Date: Wed, 11 Sep 2024 13:58:44 +0100 Subject: [PATCH 4/5] feat[mal]: Map and Label Error and Validation Tests (#3621) --- .../MapAndLabel/Public/index.test.tsx | 157 ++++++++++++++++-- .../components/MapAndLabel/Public/index.tsx | 1 + .../MapAndLabel/test/mocks/GenericValues.ts | 7 + .../MapAndLabel/test/mocks/Trees.ts | 9 +- .../MapAndLabel/test/mocks/geojson.ts | 22 +++ .../components/MapAndLabel/test/utils.ts | 12 ++ 6 files changed, 197 insertions(+), 11 deletions(-) create mode 100644 editor.planx.uk/src/@planx/components/MapAndLabel/test/mocks/GenericValues.ts diff --git a/editor.planx.uk/src/@planx/components/MapAndLabel/Public/index.test.tsx b/editor.planx.uk/src/@planx/components/MapAndLabel/Public/index.test.tsx index 3f2e3c68d6..ee15ba20a9 100644 --- a/editor.planx.uk/src/@planx/components/MapAndLabel/Public/index.test.tsx +++ b/editor.planx.uk/src/@planx/components/MapAndLabel/Public/index.test.tsx @@ -1,14 +1,14 @@ import { MyMap } from "@opensystemslab/map"; import { Presentational as MapAndLabel } from "@planx/components/MapAndLabel/Public"; -import { waitFor } from "@testing-library/react"; +import { waitFor, within } from "@testing-library/react"; import React from "react"; import { setup } from "testUtils"; import { vi } from "vitest"; import { axe } from "vitest-axe"; -import { point1 } from "../test/mocks/geojson"; +import { point1, point2 } from "../test/mocks/geojson"; import { props } from "../test/mocks/Trees"; -import { addFeaturesToMap } from "../test/utils"; +import { addFeaturesToMap, addMultipleFeatures } from "../test/utils"; beforeAll(() => { if (!window.customElements.get("my-map")) { @@ -84,44 +84,181 @@ describe("Basic UI", () => { // Schema and field validation is handled in both List and Schema folders - here we're only testing the MapAndLabel specific error handling describe("validation and error handling", () => { - test.todo("all fields are required"); - test.todo("all fields are required, for all feature tabs"); - test.todo("an error displays if the minimum number of items is not met"); - test.todo("an error displays if the maximum number of items is exceeded"); - test.todo( - "an error state is applied to a tabpanel button, when it's associated feature is invalid", - ); + it("shows all fields are required", async () => { + const { getAllByTestId, getByTestId, getByRole, user } = setup( + , + ); + const map = getByTestId("map-and-label-map"); + expect(map).toBeInTheDocument(); + + addFeaturesToMap(map, [point1]); + + expect(getByRole("tab", { name: /Tree 1/ })).toBeInTheDocument(); + + const continueButton = getByRole("button", { name: /Continue/ }); + expect(continueButton).toBeInTheDocument(); + await user.click(continueButton); + + const errorMessages = getAllByTestId(/error-message-input/); + + // Date field has been removed so only 4 inputs + expect(errorMessages).toHaveLength(4); + + errorMessages.forEach((message) => { + expect(message).not.toBeEmptyDOMElement(); + }); + }); + + // it shows all fields are required in a tab + it("should show all fields are required, for all feature tabs", async () => { + const { getByTestId, getByRole, user, debug } = setup( + , + ); + const map = getByTestId("map-and-label-map"); + expect(map).toBeInTheDocument(); + debug(); + + addMultipleFeatures([point1, point2]); + + // vertical side tab query + const firstTab = getByRole("tab", { name: /Tree 1/ }); + const secondTab = getByRole("tab", { name: /Tree 2/ }); + + // side tab validation + expect(firstTab).toBeInTheDocument(); + expect(secondTab).toBeInTheDocument(); + + // form for each tab + const firstTabPanel = getByTestId("vertical-tabpanel-0"); + const secondTabPanel = getByTestId("vertical-tabpanel-1"); + + // default is to start on seond tab panel since we add two points + expect(firstTabPanel).not.toBeVisible(); + expect(secondTabPanel).toBeVisible(); + + // Form is generate for secondTabPanel but not the first + expect(secondTabPanel.childElementCount).toBeGreaterThan(0); + expect(firstTabPanel.childElementCount).toBe(0); + + const continueButton = getByRole("button", { name: /Continue/ }); + await user.click(continueButton); + + // error messages appear + const errorMessagesTabTwo = + within(secondTabPanel).getAllByTestId(/error-message-input/); + expect(errorMessagesTabTwo).toHaveLength(4); + + // error messages are empty but visible before error state induced + // this ensures they contain the error message text + errorMessagesTabTwo.forEach((input) => { + expect(input).not.toBeEmptyDOMElement(); + }); + + await user.click(firstTab); + + expect(firstTabPanel).toBeVisible(); + + // error messages persist + const errorMessagesTabOne = + within(firstTabPanel).getAllByTestId(/error-message-input/); + expect(errorMessagesTabOne).toHaveLength(4); + }); + + // it shows all fields are required across different tabs + it("should show an error if the minimum number of items is not met", async () => { + const { getByTestId, getByRole, user } = setup(); + const map = getByTestId("map-and-label-map"); + expect(map).toBeInTheDocument(); + + const continueButton = getByRole("button", { name: /Continue/ }); + + await user.click(continueButton); + + const errorWrapper = getByTestId(/error-wrapper/); + + const errorMessage = within(errorWrapper).getByText(/You must plot /); + + expect(errorMessage).toBeInTheDocument(); + }); + // ?? + it("an error state is applied to a tabpanel button, when it's associated feature is invalid", async () => { + const { getByTestId, getByRole, user, getAllByTestId } = setup( + , + ); + const map = getByTestId("map-and-label-map"); + expect(map).toBeInTheDocument(); + + addFeaturesToMap(map, [point1]); + + const tabOne = getByRole("tab", { name: /Tree 1/ }); + + expect(tabOne).toBeInTheDocument(); + + const continueButton = getByRole("button", { name: /Continue/ }); + expect(continueButton).toBeInTheDocument(); + await user.click(continueButton); + + const errorMessages = getAllByTestId(/error-message-input/); + + // check error messages are correct amount and contain info + expect(errorMessages).toHaveLength(4); + + errorMessages.forEach((message) => { + expect(message).not.toBeEmptyDOMElement(); + }); + + expect(tabOne).toHaveStyle("border-left: 5px solid #D4351C"); + }); + // shows the error state on a tab when it's invalid }); +test.todo("an error displays if the maximum number of items is exceeded"); describe("basic interactions - happy path", () => { test.todo("adding an item to the map adds a feature tab"); + // add feature, see a tab (one feature only) test.todo("a user can input details on a single feature and submit"); + // only one feature, fill out form, submit test.todo("adding multiple features to the map adds multiple feature tabs"); + // add more than one feature, see multiple tabs test.todo("a user can input details on multiple features and submit"); + // add details to more than one tab, submit test.todo("a user can input details on feature tabs in any order"); + // ?? }); describe("copy feature select", () => { it.todo("is disabled if only a single feature is present"); + // no copy select if only one feature it.todo("is enabled once multiple features are present"); + // copy select enabled once you add more features it.todo( "lists all other features as options (the current feature is not listed)", ); + // current tree is not an option in the copy select it.todo("copies all data from one feature to another"); + // all data fields are populated from one field to another it.todo("should not have any accessibility violations"); + // axe checks }); describe("remove feature button", () => { it.todo("removes a feature from the form"); + // click remove - feature is removed + // not tab it.todo("removes a feature from the map"); + // click remove - feature is removed + // no map icon }); describe("payload generation", () => { test.todo("a submitted payload contains a GeoJSON feature collection"); + // check payload contains GeoJSON feature collection test.todo( "the feature collection contains all geospatial data inputted by the user", ); + // feature collection matches the mocked data test.todo( "each feature's properties correspond with the details entered for that feature", ); + // feature properties contain the answers to inputs }); diff --git a/editor.planx.uk/src/@planx/components/MapAndLabel/Public/index.tsx b/editor.planx.uk/src/@planx/components/MapAndLabel/Public/index.tsx index b7825ad4ce..4b260271a7 100644 --- a/editor.planx.uk/src/@planx/components/MapAndLabel/Public/index.tsx +++ b/editor.planx.uk/src/@planx/components/MapAndLabel/Public/index.tsx @@ -128,6 +128,7 @@ const VerticalFeatureTabs: React.FC = () => { sx={{ width: "100%" }} aria-labelledby={`vertical-tab-${i}`} id={`vertical-tabpanel-${i}`} + data-testid={`vertical-tabpanel-${i}`} > field.type !== "date"), +}; + export const props: PresentationalProps = { title: "Mock title", description: "Mock description", schemaName: "Trees", fn: "MockFn", - schema: Trees, + schema: mockTreeSchema, basemap: "OSM", drawColor: "#00FF00", drawType: "Point", diff --git a/editor.planx.uk/src/@planx/components/MapAndLabel/test/mocks/geojson.ts b/editor.planx.uk/src/@planx/components/MapAndLabel/test/mocks/geojson.ts index 7a4adf8507..7aff35a382 100644 --- a/editor.planx.uk/src/@planx/components/MapAndLabel/test/mocks/geojson.ts +++ b/editor.planx.uk/src/@planx/components/MapAndLabel/test/mocks/geojson.ts @@ -10,3 +10,25 @@ export const point1: Feature = { coordinates: [-3.685929607119201, 57.15301433687542], }, }; + +export const point2: Feature = { + type: "Feature", + properties: { + label: "2", + }, + geometry: { + type: "Point", + coordinates: [-3.686529607119201, 57.15310433687542], + }, +}; + +export const point3: Feature = { + type: "Feature", + properties: { + label: "3", + }, + geometry: { + type: "Point", + coordinates: [-3.68689607119201, 57.15310833687542], + }, +}; diff --git a/editor.planx.uk/src/@planx/components/MapAndLabel/test/utils.ts b/editor.planx.uk/src/@planx/components/MapAndLabel/test/utils.ts index b335faf094..9baaeea2d1 100644 --- a/editor.planx.uk/src/@planx/components/MapAndLabel/test/utils.ts +++ b/editor.planx.uk/src/@planx/components/MapAndLabel/test/utils.ts @@ -1,3 +1,4 @@ +import { screen } from "@testing-library/react"; import { Feature, Point, Polygon } from "geojson"; import { act } from "react-dom/test-utils"; @@ -16,3 +17,14 @@ export const addFeaturesToMap = async ( }); act(() => map.dispatchEvent(mockEvent)); }; + +export const addMultipleFeatures = ( + featureArray: Feature[], +) => { + const map = screen.getByTestId("map-and-label-map"); + const pointsAddedArray: Feature[] = []; + featureArray.forEach((feature) => { + pointsAddedArray.push(feature); + addFeaturesToMap(map, pointsAddedArray); + }); +}; From ec024a8043d7f5f26c4ad2f6499b8433f5fe98da Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Dafydd=20Ll=C5=B7r=20Pearson?= Date: Wed, 11 Sep 2024 14:14:23 +0100 Subject: [PATCH 5/5] feat: Better handling of GovPay errors (#3654) --- api.planx.uk/modules/pay/controller.ts | 106 ++++++++++++++----------- api.planx.uk/modules/pay/helpers.ts | 11 +++ api.planx.uk/modules/pay/index.test.ts | 35 ++++++++ 3 files changed, 105 insertions(+), 47 deletions(-) diff --git a/api.planx.uk/modules/pay/controller.ts b/api.planx.uk/modules/pay/controller.ts index 57ae9a94b7..e8661aba8d 100644 --- a/api.planx.uk/modules/pay/controller.ts +++ b/api.planx.uk/modules/pay/controller.ts @@ -1,7 +1,7 @@ import assert from "assert"; import { Request } from "express"; import { responseInterceptor } from "http-proxy-middleware"; -import { logPaymentStatus } from "./helpers.js"; +import { handleGovPayErrors, logPaymentStatus } from "./helpers.js"; import { usePayProxy } from "./proxy.js"; import { $api } from "../../client/index.js"; import { ServerError } from "../../errors/index.js"; @@ -46,9 +46,12 @@ export const makePaymentViaProxy: PaymentProxyController = async ( pathRewrite: (path) => path.replace(/^\/pay.*$/, ""), selfHandleResponse: true, onProxyRes: responseInterceptor( - async (responseBuffer, _proxyRes, _req, _res) => { + async (responseBuffer, _proxyRes, _req, { statusCode }) => { const responseString = responseBuffer.toString("utf8"); const govUkResponse = JSON.parse(responseString); + + if (statusCode >= 400) return handleGovPayErrors(govUkResponse); + await logPaymentStatus({ sessionId, flowId, @@ -79,27 +82,32 @@ export const makeInviteToPayPaymentViaProxy: PaymentRequestProxyController = ( { pathRewrite: (path) => path.replace(/^\/pay.*$/, ""), selfHandleResponse: true, - onProxyRes: responseInterceptor(async (responseBuffer) => { - const responseString = responseBuffer.toString("utf8"); - const govUkResponse = JSON.parse(responseString); - await logPaymentStatus({ - sessionId, - flowId, - teamSlug, - govUkResponse, - }); - - try { - await addGovPayPaymentIdToPaymentRequest( - paymentRequestId, + onProxyRes: responseInterceptor( + async (responseBuffer, _proxyRes, _req, { statusCode }) => { + const responseString = responseBuffer.toString("utf8"); + const govUkResponse = JSON.parse(responseString); + + if (statusCode >= 400) return handleGovPayErrors(govUkResponse); + + await logPaymentStatus({ + sessionId, + flowId, + teamSlug, govUkResponse, - ); - } catch (error) { - throw Error(error as string); - } + }); - return responseBuffer; - }), + try { + await addGovPayPaymentIdToPaymentRequest( + paymentRequestId, + govUkResponse, + ); + } catch (error) { + throw Error(error as string); + } + + return responseBuffer; + }, + ), }, req, res, @@ -125,32 +133,36 @@ export function fetchPaymentViaProxyWithCallback( { pathRewrite: () => `/${req.params.paymentId}`, selfHandleResponse: true, - onProxyRes: responseInterceptor(async (responseBuffer) => { - const govUkResponse = JSON.parse(responseBuffer.toString("utf8")); - - await logPaymentStatus({ - sessionId, - flowId, - teamSlug, - govUkResponse, - }); - - try { - await callback(req, govUkResponse); - } catch (e) { - throw Error(e as string); - } - - // only return payment status, filter out PII - return JSON.stringify({ - payment_id: govUkResponse.payment_id, - amount: govUkResponse.amount, - state: govUkResponse.state, - _links: { - next_url: govUkResponse._links?.next_url, - }, - }); - }), + onProxyRes: responseInterceptor( + async (responseBuffer, _proxyRes, _req, { statusCode }) => { + const govUkResponse = JSON.parse(responseBuffer.toString("utf8")); + + if (statusCode >= 400) return handleGovPayErrors(govUkResponse); + + await logPaymentStatus({ + sessionId, + flowId, + teamSlug, + govUkResponse, + }); + + try { + await callback(req, govUkResponse); + } catch (e) { + throw Error(e as string); + } + + // only return payment status, filter out PII + return JSON.stringify({ + payment_id: govUkResponse.payment_id, + amount: govUkResponse.amount, + state: govUkResponse.state, + _links: { + next_url: govUkResponse._links?.next_url, + }, + }); + }, + ), }, req, res, diff --git a/api.planx.uk/modules/pay/helpers.ts b/api.planx.uk/modules/pay/helpers.ts index 76d8296c3c..f513339751 100644 --- a/api.planx.uk/modules/pay/helpers.ts +++ b/api.planx.uk/modules/pay/helpers.ts @@ -2,6 +2,17 @@ import { gql } from "graphql-request"; import airbrake from "../../airbrake.js"; import { $api } from "../../client/index.js"; +/** + * Gracefully handle GovPay errors + * Docs: https://docs.payments.service.gov.uk/api_reference/#responses + */ +export const handleGovPayErrors = (res: unknown) => + JSON.stringify({ + message: + "GovPay responded with an error when attempting to proxy to their API", + govPayResponse: res, + }); + export async function logPaymentStatus({ sessionId, flowId, diff --git a/api.planx.uk/modules/pay/index.test.ts b/api.planx.uk/modules/pay/index.test.ts index 38dff1e36c..b52f72d4fd 100644 --- a/api.planx.uk/modules/pay/index.test.ts +++ b/api.planx.uk/modules/pay/index.test.ts @@ -168,3 +168,38 @@ describe("fetching status of a GOV.UK payment", () => { }); }); }); + +test("handling GovPay error responses", async () => { + const govUKErrorResponse = { + code: "govUKErrorResponse", + description: + "Account is not fully configured. Please refer to documentation to setup your account or contact support with your error code - https://www.payments.service.gov.uk/support/ .", + }; + + nock("https://publicapi.payments.service.gov.uk/v1/payments") + .post("") + .reply(400, govUKErrorResponse); + + await supertest(app) + .post( + "/pay/southwark?flowId=7cd1c4b4-4229-424f-8d04-c9fdc958ef4e&sessionId=f2d8ca1d-a43b-43ec-b3d9-a9fec63ff19c", + ) + .send({ + amount: 100, + reference: "12343543", + description: "New application", + return_url: "https://editor.planx.uk", + metadata: { + source: "PlanX", + flow: "apply-for-a-lawful-development-certificate", + inviteToPay: false, + }, + }) + .expect(400) + .then((res) => { + expect(res.body.message).toMatch( + /GovPay responded with an error when attempting to proxy to their API/, + ); + expect(res.body.govPayResponse).toEqual(govUKErrorResponse); + }); +});