diff --git a/api.planx.uk/modules/send/downloadApplicationFiles/index.ts b/api.planx.uk/modules/send/downloadApplicationFiles/index.ts index d44a73dd40..aede2c8369 100644 --- a/api.planx.uk/modules/send/downloadApplicationFiles/index.ts +++ b/api.planx.uk/modules/send/downloadApplicationFiles/index.ts @@ -17,10 +17,10 @@ export async function downloadApplicationFiles( try { // Confirm that the provided email matches the stored team settings for the provided localAuthority - const { sendToEmail } = await getTeamEmailSettings( + const { notifyPersonalisation } = await getTeamEmailSettings( req.query.localAuthority as string, ); - if (sendToEmail !== req.query.email) { + if (notifyPersonalisation.sendToEmail !== req.query.email) { return next({ status: 403, message: diff --git a/api.planx.uk/modules/send/email/index.test.ts b/api.planx.uk/modules/send/email/index.test.ts index 6c19f24386..5338507a11 100644 --- a/api.planx.uk/modules/send/email/index.test.ts +++ b/api.planx.uk/modules/send/email/index.test.ts @@ -49,8 +49,10 @@ describe(`sending an application by email to a planning office`, () => { data: { teams: [ { - sendToEmail: "planning.office.example@council.gov.uk", - settings: { emailReplyToId: "abc123" }, + notifyPersonalisation: { + emailReplyToId: "abc123", + sendToEmail: "planning.office.example@council.gov.uk", + }, }, ], }, @@ -145,15 +147,17 @@ describe(`sending an application by email to a planning office`, () => { }); }); - it("errors if this team does not have a 'submission_email' configured in teams", async () => { + it("errors if this team does not have a 'submission_email' configured in team settings", async () => { queryMock.mockQuery({ name: "GetTeamEmailSettings", matchOnVariables: false, data: { teams: [ { - sendToEmail: null, - settings: { emailReplyToId: "abc123" }, + notifyPersonalisation: { + emailReplyToId: "abc123", + sendToEmail: null, + }, }, ], }, @@ -200,7 +204,13 @@ describe(`downloading application data received by email`, () => { name: "GetTeamEmailSettings", matchOnVariables: false, data: { - teams: [{ sendToEmail: "planning.office.example@council.gov.uk" }], + teams: [ + { + notifyPersonalisation: { + sendToEmail: "planning.office.example@council.gov.uk", + }, + }, + ], }, variables: { slug: "southwark" }, }); diff --git a/api.planx.uk/modules/send/email/index.ts b/api.planx.uk/modules/send/email/index.ts index 20dfc83fd3..d1784564c7 100644 --- a/api.planx.uk/modules/send/email/index.ts +++ b/api.planx.uk/modules/send/email/index.ts @@ -27,10 +27,11 @@ export async function sendToEmail( } try { - // Confirm this local authority (aka team) has an email configured in teams.submission_email - const { sendToEmail, notifyPersonalisation } = + // Confirm this local authority (aka team) has an email configured in team_settings.submission_email + const { notifyPersonalisation } = await getTeamEmailSettings(localAuthority); - if (!sendToEmail) { + + if (!notifyPersonalisation.sendToEmail) { return next({ status: 400, message: `Send to email is not enabled for this local authority (${localAuthority})`, @@ -47,13 +48,17 @@ export async function sendToEmail( serviceName: flowName, sessionId: payload.sessionId, applicantEmail: email, - downloadLink: `${process.env.API_URL_EXT}/download-application-files/${payload.sessionId}?email=${sendToEmail}&localAuthority=${localAuthority}`, + downloadLink: `${process.env.API_URL_EXT}/download-application-files/${payload.sessionId}?email=${notifyPersonalisation.sendToEmail}&localAuthority=${localAuthority}`, ...notifyPersonalisation, }, }; // Send the email - const response = await sendEmail("submit", sendToEmail, config); + const response = await sendEmail( + "submit", + notifyPersonalisation.sendToEmail, + config, + ); // Mark session as submitted so that reminder and expiry emails are not triggered markSessionAsSubmitted(payload.sessionId); @@ -62,14 +67,14 @@ export async function sendToEmail( insertAuditEntry( payload.sessionId, localAuthority, - sendToEmail, + notifyPersonalisation.sendToEmail, config, response, ); return res.status(200).send({ message: `Successfully sent to email`, - inbox: sendToEmail, + inbox: notifyPersonalisation.sendToEmail, govuk_notify_template: "Submit", }); } catch (error) { diff --git a/api.planx.uk/modules/send/email/service.ts b/api.planx.uk/modules/send/email/service.ts index e5b1776be4..2f8660db8d 100644 --- a/api.planx.uk/modules/send/email/service.ts +++ b/api.planx.uk/modules/send/email/service.ts @@ -8,8 +8,7 @@ import { EmailSubmissionNotifyConfig } from "../../../types.js"; interface GetTeamEmailSettings { teams: { - sendToEmail: string; - notifyPersonalisation: NotifyPersonalisation; + notifyPersonalisation: NotifyPersonalisation & { sendToEmail: string }; }[]; } @@ -18,12 +17,12 @@ export async function getTeamEmailSettings(localAuthority: string) { gql` query GetTeamEmailSettings($slug: String) { teams(where: { slug: { _eq: $slug } }) { - sendToEmail: submission_email notifyPersonalisation: team_settings { helpEmail: help_email helpPhone: help_phone emailReplyToId: email_reply_to_id helpOpeningHours: help_opening_hours + sendToEmail: submission_email } } } @@ -32,7 +31,6 @@ export async function getTeamEmailSettings(localAuthority: string) { slug: localAuthority, }, ); - return response?.teams[0]; } diff --git a/e2e/tests/api-driven/src/globalHelpers.ts b/e2e/tests/api-driven/src/globalHelpers.ts index 88a0cafcb9..ee54a6e9f9 100644 --- a/e2e/tests/api-driven/src/globalHelpers.ts +++ b/e2e/tests/api-driven/src/globalHelpers.ts @@ -8,9 +8,10 @@ export function createTeam( $admin.team.create({ name: "E2E Test Team", slug: "E2E", - submissionEmail: TEST_EMAIL, + settings: { homepage: "http://www.planx.uk", + submissionEmail: TEST_EMAIL, }, ...args, }), diff --git a/e2e/tests/ui-driven/src/context.ts b/e2e/tests/ui-driven/src/context.ts index 7f08ccf25f..3ef2a4f3cf 100644 --- a/e2e/tests/ui-driven/src/context.ts +++ b/e2e/tests/ui-driven/src/context.ts @@ -41,8 +41,8 @@ export const contextDefaults: Context = { }, settings: { homepage: "planx.uk", + submissionEmail: "simulate-delivered@notifications.service.gov.uk", }, - submissionEmail: "simulate-delivered@notifications.service.gov.uk", }, }; @@ -58,9 +58,9 @@ export async function setUpTestContext( context.team.id = await $admin.team.create({ slug: context.team.slug, name: context.team.name, - submissionEmail: context.team.submissionEmail, settings: { homepage: context.team.settings?.homepage, + submissionEmail: context.team.submissionEmail, }, }); } diff --git a/editor.planx.uk/package.json b/editor.planx.uk/package.json index 39bacd8869..eb0250223d 100644 --- a/editor.planx.uk/package.json +++ b/editor.planx.uk/package.json @@ -8,12 +8,13 @@ "@ctrl/tinycolor": "^4.0.2", "@emotion/react": "^11.11.1", "@emotion/styled": "^11.11.0", + "@microlink/react-json-view": "^1.23.1", "@mui/base": "5.0.0-beta.40", "@mui/icons-material": "^5.15.10", "@mui/lab": "5.0.0-alpha.170", "@mui/material": "^5.15.10", "@mui/utils": "^5.15.11", - "@opensystemslab/map": "1.0.0-alpha.0", + "@opensystemslab/map": "1.0.0-alpha.1", "@opensystemslab/planx-core": "git+https://github.com/theopensystemslab/planx-core#d46df8d", "@tiptap/core": "^2.4.0", "@tiptap/extension-bold": "^2.0.3", @@ -169,7 +170,7 @@ }, "packageManager": "pnpm@8.6.6", "scripts": { - "start": "vite", + "start": "vite --open", "build": "tsc && vite build", "serve": "vite preview", "test": "vitest", diff --git a/editor.planx.uk/pnpm-lock.yaml b/editor.planx.uk/pnpm-lock.yaml index b64ea68854..d5100d2f72 100644 --- a/editor.planx.uk/pnpm-lock.yaml +++ b/editor.planx.uk/pnpm-lock.yaml @@ -25,6 +25,9 @@ dependencies: '@emotion/styled': specifier: ^11.11.0 version: 11.11.0(@emotion/react@11.11.1)(@types/react@18.2.45)(react@18.2.0) + '@microlink/react-json-view': + specifier: ^1.23.1 + version: 1.23.1(@types/react@18.2.45)(react-dom@18.2.0)(react@18.2.0) '@mui/base': specifier: 5.0.0-beta.40 version: 5.0.0-beta.40(@types/react@18.2.45)(react-dom@18.2.0)(react@18.2.0) @@ -41,8 +44,8 @@ dependencies: specifier: ^5.15.11 version: 5.15.11(@types/react@18.2.45)(react@18.2.0) '@opensystemslab/map': - specifier: 1.0.0-alpha.0 - version: 1.0.0-alpha.0 + specifier: 1.0.0-alpha.1 + version: 1.0.0-alpha.1 '@opensystemslab/planx-core': specifier: git+https://github.com/theopensystemslab/planx-core#d46df8d version: github.com/theopensystemslab/planx-core/d46df8d(@types/react@18.2.45) @@ -5114,6 +5117,23 @@ packages: react: 18.3.1 dev: true + /@microlink/react-json-view@1.23.1(@types/react@18.2.45)(react-dom@18.2.0)(react@18.2.0): + resolution: {integrity: sha512-Iz1uA2Y4x7Xr+O4BP2Brk16UpL6rh9GDv2A19OOb9pymkUE8eXHq1GzhJte4269wMDc8Esm2jXsTqIPLgqq7kg==} + peerDependencies: + react: '>= 15' + react-dom: '>= 15' + dependencies: + flux: 4.0.4(react@18.2.0) + react: 18.2.0 + react-base16-styling: 0.6.0 + react-dom: 18.2.0(react@18.2.0) + react-lifecycles-compat: 3.0.4 + react-textarea-autosize: 8.3.4(@types/react@18.2.45)(react@18.2.0) + transitivePeerDependencies: + - '@types/react' + - encoding + dev: false + /@mui/base@5.0.0-beta.36(@types/react@18.2.45)(react-dom@18.2.0)(react@18.2.0): resolution: {integrity: sha512-6A8fYiXgjqTO6pgj31Hc8wm1M3rFYCxDRh09dBVk0L0W4cb2lnurRJa3cAyic6hHY+we1S58OdGYRbKmOsDpGQ==} engines: {node: '>=12.0.0'} @@ -5590,8 +5610,8 @@ packages: resolution: {integrity: sha512-XuySG1E38YScSJoMlqovLru4KTUNSjgVTIjyh7qMX6aNN5HY5Ct5LhRJdxO79JtTzKfzV/bnWpz+zquYrISsvw==} dev: true - /@opensystemslab/map@1.0.0-alpha.0: - resolution: {integrity: sha512-zOAo9mz4g8VOqd6Kp4Fxf4a1uCTPBYqGLLhdD/7QJjRBZN5kWxYMhymiteFPE0V5afEIPyI4nnWS2Q2RB9JnDQ==} + /@opensystemslab/map@1.0.0-alpha.1: + resolution: {integrity: sha512-T8foDyKS18algLun1uV4o4j0VL+TfsvNL0YnqKj26E8m6x6RMx6gbo5njjRH0bRnj14E8IsLaRN546fGoZhrWw==} dependencies: '@turf/union': 7.0.0 accessible-autocomplete: 2.0.4 @@ -8787,7 +8807,6 @@ packages: /asap@2.0.6: resolution: {integrity: sha512-BSHWgDSAiKs50o2Re8ppvp3seVHXSRM44cdSsT9FfNEUUZLOGWVCsiWaRPWM1Znn+mqZ1OfVZ3z3DWEzSp7hRA==} - dev: true /assertion-error@1.1.0: resolution: {integrity: sha512-jgsaNduz+ndvGyFt3uSuWqvy4lCnIJiovtouQN5JZHOKCS2QuhEdbcQHFhVksz2N2U9hXJo8odG7ETyWlEeuDw==} @@ -9216,7 +9235,6 @@ packages: /base16@1.0.0: resolution: {integrity: sha512-pNdYkNPiJUnEhnfXV56+sQy8+AaPcG3POZAUnwr4EeqCUZFz4u2PePbo3e5Gj4ziYPCWGUZT9RHisvJKnwFuBQ==} - dev: true /base64-arraybuffer@1.0.2: resolution: {integrity: sha512-I3yl4r9QB5ZRY3XuJVEPfc2XhZO6YweFPI+UovAzn+8/hb3oJ6lnysaFcjVpkCPfVWFUDvoZ8kmVDP7WyRtYtQ==} @@ -9242,7 +9260,7 @@ packages: dev: true /batch@0.6.1: - resolution: {integrity: sha512-x+VAiMRL6UPkx+kudNvxTl6hB2XNNCG2r+7wixVfIYwu/2HKRXimwQyaumLjMveWvT2Hkd/cAJw+QBMfJ/EKVw==} + resolution: {integrity: sha1-3DQxT05nkxgJP8dgJyUl+UvyXBY=} dev: true /bfj@7.1.0: @@ -9382,7 +9400,7 @@ packages: dev: true /bytes@3.0.0: - resolution: {integrity: sha512-pMhOfFDPiv9t5jjIXkHosWmkSyQbvsgEVNkz0ERHbuLh2T/7j4Mqqpz523Fe8MVY89KC6Sh/QfS2sM+SjgFDcw==} + resolution: {integrity: sha1-0ygVQE1olpn4Wk6k+odV3ROpYEg=} engines: {node: '>= 0.8'} dev: true @@ -11923,11 +11941,9 @@ packages: fbjs: 3.0.5 transitivePeerDependencies: - encoding - dev: true /fbjs-css-vars@1.0.2: resolution: {integrity: sha512-b2XGFAFdWZWg0phtAWLHCk836A1Xann+I+Dgd3Gk64MHKZO44FfoD1KxyvbSh0qZsIoXQGGlVztIY+oitJPpRQ==} - dev: true /fbjs@3.0.5: resolution: {integrity: sha512-ztsSx77JBtkuMrEypfhgc3cI0+0h+svqeie7xHbh1k/IKdcydnvadp/mUaGgjAOXQmQSxsqgaRhS3q9fy+1kxg==} @@ -11941,7 +11957,6 @@ packages: ua-parser-js: 1.0.38 transitivePeerDependencies: - encoding - dev: true /fd-package-json@1.2.0: resolution: {integrity: sha512-45LSPmWf+gC5tdCQMNH4s9Sr00bIkiD9aN7dc5hqkrEw1geRYyDQS1v1oMHAW3ysfxfndqGsrDREHHjNNbKUfA==} @@ -12086,7 +12101,6 @@ packages: react: 18.2.0 transitivePeerDependencies: - encoding - dev: true /follow-redirects@1.15.6: resolution: {integrity: sha512-wWN62YITEaOpSK584EZXJafH1AGpO8RVgElfkuXbTOrPX4fIfOyEpW/CsiNd8JdYrAoOvafRTOEnvsO++qCqFA==} @@ -14747,7 +14761,6 @@ packages: /lodash.curry@4.1.1: resolution: {integrity: sha512-/u14pXGviLaweY5JI0IUzgzF2J6Ne8INyzAZjImcryjgkZ+ebruBxy2/JaOOkTqScddcYtakjhSaeemV8lR0tA==} - dev: true /lodash.debounce@4.0.8: resolution: {integrity: sha512-FT1yDzDYEoYWhnSGnpE/4Kj1fLZkDFyqRb7fNt6FdYOSxlUWAtp42Eh6Wb0rGIv/m9Bgo7x4GhQbm5Ys4SG5ow==} @@ -14755,7 +14768,6 @@ packages: /lodash.flow@3.5.0: resolution: {integrity: sha512-ff3BX/tSioo+XojX4MOsOMhJw0nZoUEF011LX8g8d3gvjVbxd89cCio4BCXronjxcTUIJUoqKEUA+n4CqvvRPw==} - dev: true /lodash.memoize@4.1.2: resolution: {integrity: sha512-t7j+NzmgnQzTAYXcsHYLgimltOV1MXHtlOWf6GjL9Kj8GK5FInw5JotxvbOs+IvV1/Dzo04/fCGfLVs7aXb4Ag==} @@ -17034,7 +17046,6 @@ packages: resolution: {integrity: sha512-nolQXZ/4L+bP/UGlkfaIujX9BKxGwmQ9OT4mOt5yvy8iK1h3wqTEJCijzGANTCCl9nWjY41juyAn2K3Q1hLLTg==} dependencies: asap: 2.0.6 - dev: true /promise@8.3.0: resolution: {integrity: sha512-rZPNPKTOYVNEEKFaq1HqTgOwZD+4/YHS5ukLzQCypkj+OkYx7iv0mA91lJlpPPZ8vMau3IIGj5Qlwrx+8iiSmg==} @@ -17247,7 +17258,6 @@ packages: /pure-color@1.3.0: resolution: {integrity: sha512-QFADYnsVoBMw1srW7OVKEYjG+MbIa49s54w1MA1EDY6r2r/sTcKKYqRX1f4GYvnXP7eN/Pe9HFcX+hwzmrXRHA==} - dev: true /q@1.5.1: resolution: {integrity: sha512-kV/CThkXo6xyFEZUugw/+pIOywXcDbFYgSct5cT3gqlbkBE1SJdwy6UQoZvodiWF/ckQLZyDE/Bu1M6gVu5lVw==} @@ -17360,7 +17370,6 @@ packages: lodash.curry: 4.1.1 lodash.flow: 3.5.0 pure-color: 1.3.0 - dev: true /react-beautiful-dnd@13.1.1(react-dom@18.2.0)(react@18.2.0): resolution: {integrity: sha512-0Lvs4tq2VcrEjEgDXHjT98r+63drkKEgqyxdA7qD3mvKwga6a5SscbdLPO2IExotU1jW8L0Ksdl0Cj2AF67nPQ==} @@ -17619,7 +17628,6 @@ packages: /react-lifecycles-compat@3.0.4: resolution: {integrity: sha512-fBASbA6LnOU9dOU2eW7aQ8xmYBSXUIWr+UmF9b1efZBazGNO+rcXT/icdKnYm2pTwcRylVUYwW7H1PHfLekVzA==} - dev: true /react-markdown@8.0.7(@types/react@18.2.45)(react@18.2.0): resolution: {integrity: sha512-bvWbzG4MtOU62XqBx3Xx+zB2raaFFsq4mYiAzfjXJMEz2sixgeAfraA3tvzULF02ZdOMUOKTBFFaZJDDrq+BJQ==} @@ -17814,6 +17822,20 @@ packages: react: 18.2.0 dev: true + /react-textarea-autosize@8.3.4(@types/react@18.2.45)(react@18.2.0): + resolution: {integrity: sha512-CdtmP8Dc19xL8/R6sWvtknD/eCXkQr30dtvC4VmGInhRsfF8X/ihXCq6+9l9qbxmKRiq407/7z5fxE7cVWQNgQ==} + engines: {node: '>=10'} + peerDependencies: + react: ^16.8.0 || ^17.0.0 || ^18.0.0 + dependencies: + '@babel/runtime': 7.25.0 + react: 18.2.0 + use-composed-ref: 1.3.0(react@18.2.0) + use-latest: 1.2.1(@types/react@18.2.45)(react@18.2.0) + transitivePeerDependencies: + - '@types/react' + dev: false + /react-toastify@9.1.3(react-dom@18.2.0)(react@18.2.0): resolution: {integrity: sha512-fPfb8ghtn/XMxw3LkxQBk3IyagNpF/LIKjOBflbexr2AWxAH1MJgvnESwEwBn9liLFXgTKWgBSdZpw9m4OTHTg==} peerDependencies: @@ -20046,7 +20068,6 @@ packages: /ua-parser-js@1.0.38: resolution: {integrity: sha512-Aq5ppTOfvrCMgAPneW1HfWj66Xi7XL+/mIy996R1/CLS/rcyJQm6QZdsKrUeivDFQ+Oc9Wyuwor8Ze8peEoUoQ==} - dev: true /uc.micro@2.1.0: resolution: {integrity: sha512-ARDJmphmdvUk6Glw7y9DQ2bFkKBHwQHLi2lsaH6PPmz/Ka9sFOBsBluozhDltWmnv9u/cF6Rt87znRTPV+yp/A==} @@ -20271,6 +20292,41 @@ packages: querystringify: 2.2.0 requires-port: 1.0.0 + /use-composed-ref@1.3.0(react@18.2.0): + resolution: {integrity: sha512-GLMG0Jc/jiKov/3Ulid1wbv3r54K9HlMW29IWcDFPEqFkSO2nS0MuefWgMJpeHQ9YJeXDL3ZUF+P3jdXlZX/cQ==} + peerDependencies: + react: ^16.8.0 || ^17.0.0 || ^18.0.0 + dependencies: + react: 18.2.0 + dev: false + + /use-isomorphic-layout-effect@1.1.2(@types/react@18.2.45)(react@18.2.0): + resolution: {integrity: sha512-49L8yCO3iGT/ZF9QttjwLF/ZD9Iwto5LnH5LmEdk/6cFmXddqi2ulF0edxTwjj+7mqvpVVGQWvbXZdn32wRSHA==} + peerDependencies: + '@types/react': '*' + react: ^16.8.0 || ^17.0.0 || ^18.0.0 + peerDependenciesMeta: + '@types/react': + optional: true + dependencies: + '@types/react': 18.2.45 + react: 18.2.0 + dev: false + + /use-latest@1.2.1(@types/react@18.2.45)(react@18.2.0): + resolution: {integrity: sha512-xA+AVm/Wlg3e2P/JiItTziwS7FK92LWrDB0p+hgXloIMuVCeJJ8v6f0eeHyPZaJrM+usM1FkFfbNCrJGs8A/zw==} + peerDependencies: + '@types/react': '*' + react: ^16.8.0 || ^17.0.0 || ^18.0.0 + peerDependenciesMeta: + '@types/react': + optional: true + dependencies: + '@types/react': 18.2.45 + react: 18.2.0 + use-isomorphic-layout-effect: 1.1.2(@types/react@18.2.45)(react@18.2.0) + dev: false + /use-memo-one@1.1.3(react@18.2.0): resolution: {integrity: sha512-g66/K7ZQGYrI6dy8GLpVcMsBp4s17xNkYJVSMvTEevGy3nDxHOfE6z8BVE22+5G5x7t3+bhzrlTDB7ObrEE0cQ==} peerDependencies: @@ -21023,6 +21079,7 @@ 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/@planx/components/Checklist/Editor.tsx b/editor.planx.uk/src/@planx/components/Checklist/Editor.tsx index 8cbaefa0d1..603a945170 100644 --- a/editor.planx.uk/src/@planx/components/Checklist/Editor.tsx +++ b/editor.planx.uk/src/@planx/components/Checklist/Editor.tsx @@ -298,6 +298,20 @@ export const ChecklistComponent: React.FC = (props) => { ...parseMoreInformation(props.node?.data), }, onSubmit: ({ options, groupedOptions, ...values }) => { + const sourceOptions = options?.length + ? options + : groupedOptions?.flatMap((group) => group.children); + + const filteredOptions = (sourceOptions || []).filter( + (option) => option.data.text, + ); + + const processedOptions = filteredOptions.map((option) => ({ + ...option, + id: option.id || undefined, + type: TYPES.Answer, + })); + if (props.handleSubmit) { props.handleSubmit( { @@ -316,27 +330,7 @@ export const ChecklistComponent: React.FC = (props) => { }), }, }, - ...[ - options && - options - .filter((o) => o.data.text) - .map((o) => ({ - ...o, - id: o.id || undefined, - type: TYPES.Answer, - })), - ], - ...[ - groupedOptions && - groupedOptions - .flatMap((gr) => gr.children) - .filter((o) => o.data.text) - .map((o) => ({ - ...o, - id: o.id || undefined, - type: TYPES.Answer, - })), - ], + processedOptions, ); } else { alert(JSON.stringify({ type, ...values, options }, null, 2)); diff --git a/editor.planx.uk/src/@planx/components/List/Public/index.test.tsx b/editor.planx.uk/src/@planx/components/List/Public/index.test.tsx index fed2d67c66..0c0c150221 100644 --- a/editor.planx.uk/src/@planx/components/List/Public/index.test.tsx +++ b/editor.planx.uk/src/@planx/components/List/Public/index.test.tsx @@ -552,7 +552,7 @@ describe("Form validation and error handling", () => { test( "an error displays if the maximum number of items is exceeded", - { timeout: 20000 }, + { timeout: 25000 }, async () => { const { user, getAllByTestId, getByTestId, getByText } = setup( , diff --git a/editor.planx.uk/src/@planx/components/List/utils.tsx b/editor.planx.uk/src/@planx/components/List/utils.tsx index 28f8dbb2b4..36ccc47196 100644 --- a/editor.planx.uk/src/@planx/components/List/utils.tsx +++ b/editor.planx.uk/src/@planx/components/List/utils.tsx @@ -44,70 +44,32 @@ export function formatSchemaDisplayValue( return matchingOption?.data.text; } case "map": { - const feature = value[0] as string as any; // won't be necessary to cast once we're only setting "geojsonData" prop in future - const drawType = field.data.mapOptions?.drawType; - - switch (drawType) { - case "Point": - // Our "geojsonData" layer doesn't have a "point" style yet, so make due with center marker for now! - // Once style layers are more comprehensive, share same map as "Polygon" style here - return ( - <> - {/* @ts-ignore */} - - - ); - case "Polygon": - return ( - <> - {/* @ts-ignore */} - - - ); - } + const feature = value[0]; + return ( + <> + {/* @ts-ignore */} + + + ); } } } @@ -203,9 +165,9 @@ export function flatten>( return isObject && depth > 0 ? { - ...acc, - ...flatten(value, { depth: depth - 1, path: newPath, separator }), - } + ...acc, + ...flatten(value, { depth: depth - 1, path: newPath, separator }), + } : { ...acc, [newPath]: value }; }, {} as T); } diff --git a/editor.planx.uk/src/@planx/components/MapAndLabel/Public/Context.tsx b/editor.planx.uk/src/@planx/components/MapAndLabel/Public/Context.tsx index 75b6aabbdf..87ce318cbb 100644 --- a/editor.planx.uk/src/@planx/components/MapAndLabel/Public/Context.tsx +++ b/editor.planx.uk/src/@planx/components/MapAndLabel/Public/Context.tsx @@ -1,14 +1,11 @@ import { useSchema } from "@planx/components/shared/Schema/hook"; -import { - Schema, - SchemaUserData, - SchemaUserResponse, -} from "@planx/components/shared/Schema/model"; +import { Schema, SchemaUserData } from "@planx/components/shared/Schema/model"; import { getPreviouslySubmittedData, makeData, } from "@planx/components/shared/utils"; import { FormikProps, useFormik } from "formik"; +import { get } from "lodash"; import React, { createContext, PropsWithChildren, @@ -21,14 +18,14 @@ import { PresentationalProps } from "."; interface MapAndLabelContextValue { schema: Schema; activeIndex: number; - saveItem: () => Promise; - editItem: (index: number) => void; - cancelEditItem: () => void; + editFeature: (index: number) => void; formik: FormikProps; validateAndSubmitForm: () => void; + isFeatureInvalid: (index: number) => boolean; + addFeature: () => void; + copyFeature: (sourceIndex: number, destinationIndex: number) => void; mapAndLabelProps: PresentationalProps; errors: { - unsavedItem: boolean; min: boolean; max: boolean; }; @@ -44,13 +41,15 @@ export const MapAndLabelProvider: React.FC = ( props, ) => { const { schema, children, handleSubmit } = props; - const { formikConfig, initialValues: _initialValues } = useSchema({ + const { formikConfig, initialValues } = useSchema({ schema, previousValues: getPreviouslySubmittedData(props), }); const formik = useFormik({ ...formikConfig, + // The user interactions are map driven - start with no values added + initialValues: { schemaData: [] }, onSubmit: (values) => { const defaultPassportData = makeData(props, values.schemaData)?.["data"]; @@ -66,35 +65,16 @@ export const MapAndLabelProvider: React.FC = ( props.previouslySubmittedData ? -1 : 0, ); - const [activeItemInitialState, setActiveItemInitialState] = useState< - SchemaUserResponse | undefined - >(undefined); - - const [unsavedItemError, setUnsavedItemError] = useState(false); const [minError, setMinError] = useState(false); const [maxError, setMaxError] = useState(false); const resetErrors = () => { setMinError(false); setMaxError(false); - setUnsavedItemError(false); - }; - - const saveItem = async () => { - resetErrors(); - - const errors = await formik.validateForm(); - const isValid = !errors.schemaData?.length; - if (isValid) { - exitEditMode(); - } }; const validateAndSubmitForm = () => { - // Do not allow submissions with an unsaved item - if (activeIndex !== -1) return setUnsavedItemError(true); - - // Manually validate minimum number of items + // Manually validate minimum number of features if (formik.values.schemaData.length < schema.min) { return setMinError(true); } @@ -102,37 +82,44 @@ export const MapAndLabelProvider: React.FC = ( formik.handleSubmit(); }; - const cancelEditItem = () => { - if (activeItemInitialState) resetItemToPreviousState(); + const editFeature = (index: number) => { + setActiveIndex(index); + }; - setActiveItemInitialState(undefined); + const isFeatureInvalid = (index: number) => + Boolean(get(formik.errors, ["schemaData", index])); - exitEditMode(); - }; + const addFeature = () => { + resetErrors(); - const editItem = (index: number) => { - setActiveItemInitialState(formik.values.schemaData[index]); - setActiveIndex(index); - }; + const currentFeatures = formik.values.schemaData; + const updatedFeatures = [...currentFeatures, initialValues]; + formik.setFieldValue("schemaData", updatedFeatures); - const exitEditMode = () => setActiveIndex(-1); + // TODO: Handle more gracefully - stop user from adding new feature to map? + if (schema.max && updatedFeatures.length > schema.max) { + setMaxError(true); + } + }; - const resetItemToPreviousState = () => - formik.setFieldValue(`schemaData[${activeIndex}]`, activeItemInitialState); + const copyFeature = (sourceIndex: number, destinationIndex: number) => { + const sourceFeature = formik.values.schemaData[sourceIndex]; + formik.setFieldValue(`schemaData[${destinationIndex}]`, sourceFeature); + }; return ( = ({ + destinationIndex, + features, +}) => { + const { schema, copyFeature } = useMapAndLabelContext(); + + // Only enable component if there are multiple features + const isDisabled = features.length < 2; + + // We can only copy from other features + const sourceFeatures = features.filter( + (_, sourceIndex) => sourceIndex !== destinationIndex, + ); + + return ( + + + { + const label = e.target.value as string; + // Convert text label to zero-indexed integer + const sourceIndex = parseInt(label, 10) - 1; + copyFeature(sourceIndex, destinationIndex); + }} + name={"copyFeature"} + style={{ width: "200px" }} + > + + Please add at least two features to the map in order to enable this + feature + + {sourceFeatures.map((option) => ( + + {`${schema.type} ${option.properties?.label}`} + + ))} + + + + ); +}; 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 23b073f7dd..b62bc3d4ea 100644 --- a/editor.planx.uk/src/@planx/components/MapAndLabel/Public/index.tsx +++ b/editor.planx.uk/src/@planx/components/MapAndLabel/Public/index.tsx @@ -1,17 +1,20 @@ +import DeleteIcon from "@mui/icons-material/Delete"; import TabContext from "@mui/lab/TabContext"; import TabPanel from "@mui/lab/TabPanel"; import Box from "@mui/material/Box"; +import Button from "@mui/material/Button"; import Tab from "@mui/material/Tab"; import Tabs from "@mui/material/Tabs"; import Typography from "@mui/material/Typography"; import { SiteAddress } from "@planx/components/FindProperty/model"; import { ErrorSummaryContainer } from "@planx/components/shared/Preview/ErrorSummaryContainer"; import { SchemaFields } from "@planx/components/shared/Schema/SchemaFields"; -import { Feature } from "geojson"; -import { GeoJsonObject } from "geojson"; +import { Feature, GeoJsonObject } from "geojson"; import { useStore } from "pages/FlowEditor/lib/store"; import React, { useEffect, useState } from "react"; +import { FONT_WEIGHT_SEMI_BOLD } from "theme"; import FullWidthWrapper from "ui/public/FullWidthWrapper"; +import ErrorWrapper from "ui/shared/ErrorWrapper"; import Card from "../../shared/Preview/Card"; import CardHeader from "../../shared/Preview/CardHeader"; @@ -19,6 +22,7 @@ import { MapContainer } from "../../shared/Preview/MapContainer"; import { PublicProps } from "../../ui"; import type { MapAndLabel } from "./../model"; import { MapAndLabelProvider, useMapAndLabelContext } from "./Context"; +import { CopyFeature } from "./CopyFeature"; type Props = PublicProps; @@ -38,14 +42,8 @@ function a11yProps(index: number) { const VerticalFeatureTabs: React.FC<{ features: Feature[] }> = ({ features, }) => { - const { schema, activeIndex, formik } = useMapAndLabelContext(); - const [activeTab, setActiveTab] = useState( - features[features.length - 1].properties?.label || "", - ); - - const handleChange = (_event: React.SyntheticEvent, newValue: string) => { - setActiveTab(newValue); - }; + const { schema, activeIndex, formik, editFeature, isFeatureInvalid } = + useMapAndLabelContext(); return ( = ({ maxHeight: "fit-content", }} > - + { + editFeature(parseInt(newValue, 10)); + }} + // TODO! aria-label="Vertical tabs example" sx={{ borderRight: 1, borderColor: "divider" }} > {features.map((feature, i) => ( ({ + borderRight: `12px solid ${theme.palette.error.main}`, + // Appear over tab indicator + zIndex: 2, + }), + })} /> ))} {features.map((feature, i) => ( - - {`${schema.type} ${feature.properties?.label}`} - - - {`${feature.geometry.type}`} - {feature.geometry.type === "Point" - ? ` (${feature.geometry.coordinates.map((coord) => - coord.toFixed(5), - )})` - : ` (area ${ - feature.properties?.["area.squareMetres"] || 0 - } m²)`} - + + + + {`${schema.type} ${feature.properties?.label}`} + + + {`${feature.geometry.type}`} + {feature.geometry.type === "Point" + ? ` (${feature.geometry.coordinates.map((coord) => + coord.toFixed(5), + )})` + : ` (area ${ + feature.properties?.["area.squareMetres"] || 0 + } m²)`} + + + + ({ + display: "flex", + flexDirection: "column", + gap: theme.spacing(2), + })} schema={schema} activeIndex={activeIndex} formik={formik} /> + ))} @@ -105,8 +145,26 @@ const VerticalFeatureTabs: React.FC<{ features: Feature[] }> = ({ ); }; +const PlotFeatureToBegin = () => ( + ({ + backgroundColor: theme.palette.background.paper, + p: theme.spacing(3), + textAlign: "center", + mt: theme.spacing(-1), + color: theme.typography.body2.color, + border: `1px solid ${theme.palette.border.main}`, + })} + > + + Plot a feature on the map to begin + + +); + const Root = () => { - const { validateAndSubmitForm, mapAndLabelProps } = useMapAndLabelContext(); + const { validateAndSubmitForm, mapAndLabelProps, errors } = + useMapAndLabelContext(); const { title, description, @@ -123,11 +181,13 @@ const Root = () => { } = mapAndLabelProps; const [features, setFeatures] = useState(undefined); + const { addFeature, schema } = useMapAndLabelContext(); useEffect(() => { const geojsonChangeHandler = ({ detail: geojson }: any) => { if (geojson["EPSG:3857"]?.features) { setFeatures(geojson["EPSG:3857"].features); + addFeature(); } else { // if the user clicks 'reset' on the map, geojson will be empty object, so set features to undefined setFeatures(undefined); @@ -141,7 +201,14 @@ const Root = () => { return function cleanup() { map?.removeEventListener("geojsonChange", geojsonChangeHandler); }; - }, [setFeatures]); + }, [setFeatures, addFeature]); + + const rootError: string = + (errors.min && + `You must plot at least ${schema.min} ${schema.type}(s) on the map`) || + (errors.max && + `You must plot at most ${schema.max} ${schema.type}(s) on the map`) || + ""; return ( @@ -153,48 +220,40 @@ const Root = () => { howMeasured={howMeasured} /> - - {/* @ts-ignore */} - - + + + {/* @ts-ignore */} + + + {features && features?.length > 0 ? ( ) : ( - - - {`Plot a feature on the map to begin`} - - + )} diff --git a/editor.planx.uk/src/@planx/components/shared/Schema/SchemaFields.tsx b/editor.planx.uk/src/@planx/components/shared/Schema/SchemaFields.tsx index e0b7c0d6d4..74fd0c75c7 100644 --- a/editor.planx.uk/src/@planx/components/shared/Schema/SchemaFields.tsx +++ b/editor.planx.uk/src/@planx/components/shared/Schema/SchemaFields.tsx @@ -1,3 +1,5 @@ +import Box from "@mui/material/Box"; +import { SxProps, Theme } from "@mui/material/styles"; import { FormikProps } from "formik"; import React from "react"; import InputRow from "ui/shared/InputRow"; @@ -15,6 +17,7 @@ interface SchemaFieldsProps { /** Formik instance generated from config provided by useSchema hook */ formik: FormikProps; schema: Schema; + sx?: SxProps; } /** @@ -23,10 +26,14 @@ interface SchemaFieldsProps { export const SchemaFields: React.FC = ({ schema, formik, + sx, activeIndex = 0, -}) => - schema.fields.map((field, i) => ( - - - - )); +}) => ( + + {schema.fields.map((field, i) => ( + + + + ))} + +); diff --git a/editor.planx.uk/src/pages/FlowEditor/components/Sidebar/StyledTab.tsx b/editor.planx.uk/src/pages/FlowEditor/components/Sidebar/StyledTab.tsx index 94bdc60043..7bc7245b4b 100644 --- a/editor.planx.uk/src/pages/FlowEditor/components/Sidebar/StyledTab.tsx +++ b/editor.planx.uk/src/pages/FlowEditor/components/Sidebar/StyledTab.tsx @@ -3,36 +3,23 @@ import Tab, { tabClasses, TabProps } from "@mui/material/Tab"; import React from "react"; import { FONT_WEIGHT_SEMI_BOLD } from "theme"; -interface StyledTabProps extends TabProps { - tabTheme?: "light" | "dark"; -} - -const StyledTab = styled(({ tabTheme, ...props }: StyledTabProps) => ( +const StyledTab = styled((props: TabProps) => ( -))(({ theme, tabTheme }) => ({ +))(({ theme }) => ({ position: "relative", zIndex: 1, textTransform: "none", background: "transparent", - border: `1px solid transparent`, borderBottomColor: theme.palette.border.main, - color: theme.palette.primary.main, - fontWeight: FONT_WEIGHT_SEMI_BOLD, + color: theme.palette.text.primary, + minWidth: "75px", minHeight: "36px", margin: theme.spacing(0, 0.5), - marginBottom: "-1px", - padding: "0.5em", + padding: "0.75em 0.25em", [`&.${tabClasses.selected}`]: { - background: - tabTheme === "dark" - ? theme.palette.background.dark - : theme.palette.background.default, - borderColor: theme.palette.border.main, - borderBottomColor: theme.palette.common.white, - color: - tabTheme === "dark" - ? theme.palette.common.white - : theme.palette.text.primary, + fontWeight: FONT_WEIGHT_SEMI_BOLD, + color: theme.palette.text.primary, + boxShadow: `inset 0 -3px 0 ${theme.palette.info.main}`, }, })); diff --git a/editor.planx.uk/src/pages/FlowEditor/components/Sidebar/index.tsx b/editor.planx.uk/src/pages/FlowEditor/components/Sidebar/index.tsx index f7ac1b7416..b76df2413f 100644 --- a/editor.planx.uk/src/pages/FlowEditor/components/Sidebar/index.tsx +++ b/editor.planx.uk/src/pages/FlowEditor/components/Sidebar/index.tsx @@ -1,3 +1,4 @@ +import ReactJson from "@microlink/react-json-view"; import LanguageIcon from "@mui/icons-material/Language"; import OpenInNewIcon from "@mui/icons-material/OpenInNew"; import OpenInNewOffIcon from "@mui/icons-material/OpenInNewOff"; @@ -69,7 +70,7 @@ const SidebarContainer = styled(Box)(() => ({ })); const Header = styled("header")(({ theme }) => ({ - padding: theme.spacing(1), + padding: theme.spacing(1, 1.5), "& input": { flex: "1", padding: "5px", @@ -108,6 +109,7 @@ const TabList = styled(Box)(({ theme }) => ({ }, "& .MuiTabs-root": { minHeight: "0", + padding: theme.spacing(0, 1.5), }, // Hide default MUI indicator as we're using custom styling "& .MuiTabs-indicator": { @@ -126,21 +128,27 @@ const DebugConsole = () => { ); return ( - - - Download the flow schema - - -
-        {JSON.stringify({ passport, breadcrumbs, cachedBreadcrumbs }, null, 2)}
-      
+
); }; @@ -297,12 +305,7 @@ const Sidebar: React.FC<{ )}
- + - - - + + + {hasFeatureFlag("SEARCH") && ( - + )} - + {activeTab === "PreviewBrowser" && ( diff --git a/editor.planx.uk/src/pages/FlowEditor/components/Team/components/AddNewEditorModal.tsx b/editor.planx.uk/src/pages/FlowEditor/components/Team/components/AddNewEditorModal.tsx index 75961f1ec9..81d9b6340a 100644 --- a/editor.planx.uk/src/pages/FlowEditor/components/Team/components/AddNewEditorModal.tsx +++ b/editor.planx.uk/src/pages/FlowEditor/components/Team/components/AddNewEditorModal.tsx @@ -33,6 +33,7 @@ export const AddNewEditorModal = ({ const clearErrors = () => { setShowUserAlreadyExistsError(false); + setShowErrorToast(false); }; const handleSubmit = async ( @@ -41,7 +42,7 @@ export const AddNewEditorModal = ({ ) => { const { teamId, teamSlug } = useStore.getState(); - const newUserId = await createAndAddUserToTeam( + const createUserResult = await createAndAddUserToTeam( values.email, values.firstName, values.lastName, @@ -51,14 +52,17 @@ export const AddNewEditorModal = ({ if (isUserAlreadyExistsError(err.message)) { setShowUserAlreadyExistsError(true); } + if (err.message === "Unable to create user") { + setShowErrorToast(true); + } console.error(err); }); - if (!newUserId) { + if (!createUserResult) { return; } clearErrors(); - optimisticallyUpdateMembersTable(values, newUserId); + optimisticallyUpdateMembersTable(values, createUserResult.id); setShowModal(false); toast.success("Successfully added a user"); resetForm({ values }); @@ -84,7 +88,7 @@ export const AddNewEditorModal = ({ maxWidth: theme.breakpoints.values.md, borderRadius: 0, borderTop: `20px solid ${theme.palette.primary.main}`, - background: "#FFF", + background: theme.palette.background.paper, margin: theme.spacing(2), }), }} diff --git a/editor.planx.uk/src/pages/FlowEditor/components/Team/components/MembersTable.tsx b/editor.planx.uk/src/pages/FlowEditor/components/Team/components/MembersTable.tsx index 9b3a6eb063..f835a4ca30 100644 --- a/editor.planx.uk/src/pages/FlowEditor/components/Team/components/MembersTable.tsx +++ b/editor.planx.uk/src/pages/FlowEditor/components/Team/components/MembersTable.tsx @@ -5,7 +5,6 @@ import TableCell from "@mui/material/TableCell"; import TableContainer from "@mui/material/TableContainer"; import TableHead from "@mui/material/TableHead"; import TableRow from "@mui/material/TableRow"; -import { hasFeatureFlag } from "lib/featureFlags"; import { AddButton } from "pages/Team"; import React, { useState } from "react"; @@ -88,7 +87,7 @@ export const MembersTable = ({ {member.email} ))} - {showAddMemberButton && hasFeatureFlag("ADD_NEW_EDITOR") && ( + {showAddMemberButton && ( setShowModal(true)}> diff --git a/editor.planx.uk/src/pages/FlowEditor/components/Team/queries/createAndAddUserToTeam.tsx b/editor.planx.uk/src/pages/FlowEditor/components/Team/queries/createAndAddUserToTeam.tsx index 46a4161e55..6bd17e6ab0 100644 --- a/editor.planx.uk/src/pages/FlowEditor/components/Team/queries/createAndAddUserToTeam.tsx +++ b/editor.planx.uk/src/pages/FlowEditor/components/Team/queries/createAndAddUserToTeam.tsx @@ -1,8 +1,12 @@ -import { gql } from "@apollo/client"; +import { FetchResult, gql } from "@apollo/client"; import { GET_USERS_FOR_TEAM_QUERY } from "routes/teamMembers"; import { client } from "../../../../../lib/graphql"; +type CreateAndAddUserResponse = FetchResult<{ + insert_users_one: { id: number; __typename: "users" }; +}>; + export const createAndAddUserToTeam = async ( email: string, firstName: string, @@ -11,7 +15,7 @@ export const createAndAddUserToTeam = async ( teamSlug: string, ) => { // NB: the user is hard-coded with the 'teamEditor' role for now - const response = (await client.mutate({ + const response: CreateAndAddUserResponse = await client.mutate({ mutation: gql` mutation CreateAndAddUserToTeam( $email: String! @@ -40,6 +44,9 @@ export const createAndAddUserToTeam = async ( refetchQueries: [ { query: GET_USERS_FOR_TEAM_QUERY, variables: { teamSlug } }, ], - })) as any; - return response.data.insert_users_one; + }); + if (response.data) { + return response.data.insert_users_one; + } + throw new Error("Unable to create user"); }; diff --git a/editor.planx.uk/src/pages/FlowEditor/components/Team/tests/TeamMembers.addNewEditor.errors.serverSide.test.tsx b/editor.planx.uk/src/pages/FlowEditor/components/Team/tests/TeamMembers.addNewEditor.errors.serverSide.test.tsx new file mode 100644 index 0000000000..22fd4e5db2 --- /dev/null +++ b/editor.planx.uk/src/pages/FlowEditor/components/Team/tests/TeamMembers.addNewEditor.errors.serverSide.test.tsx @@ -0,0 +1,36 @@ +import { screen } from "@testing-library/react"; +import { FullStore, useStore } from "pages/FlowEditor/lib/store"; +import { vi } from "vitest"; + +import { setupTeamMembersScreen } from "./helpers/setupTeamMembersScreen"; +import { userTriesToAddNewEditor } from "./helpers/userTriesToAddNewEditor"; +import { mockTeamMembersData } from "./mocks/mockTeamMembersData"; +import { alreadyExistingUser } from "./mocks/mockUsers"; + +let initialState: FullStore; +vi.mock( + "pages/FlowEditor/components/Team/queries/createAndAddUserToTeam.tsx", + () => ({ + createAndAddUserToTeam: vi.fn().mockRejectedValue({ + message: "Unable to create user", + }), + }), +); + +describe("when a user fills in the 'add a new editor' form correctly but there is a server-side error", () => { + afterAll(() => useStore.setState(initialState)); + beforeEach(async () => { + useStore.setState({ + teamMembers: [...mockTeamMembersData, alreadyExistingUser], + }); + + const { user } = await setupTeamMembersScreen(); + await userTriesToAddNewEditor(user); + }); + + it("shows an appropriate error message", async () => { + expect( + await screen.findByText(/Failed to add new user, please try again/), + ).toBeInTheDocument(); + }); +}); diff --git a/editor.planx.uk/src/pages/FlowEditor/components/Team/tests/TeamMembers.addNewEditor.errors.test.tsx b/editor.planx.uk/src/pages/FlowEditor/components/Team/tests/TeamMembers.addNewEditor.errors.userAlreadyExists.test.tsx similarity index 90% rename from editor.planx.uk/src/pages/FlowEditor/components/Team/tests/TeamMembers.addNewEditor.errors.test.tsx rename to editor.planx.uk/src/pages/FlowEditor/components/Team/tests/TeamMembers.addNewEditor.errors.userAlreadyExists.test.tsx index da52420e26..11f53bc288 100644 --- a/editor.planx.uk/src/pages/FlowEditor/components/Team/tests/TeamMembers.addNewEditor.errors.test.tsx +++ b/editor.planx.uk/src/pages/FlowEditor/components/Team/tests/TeamMembers.addNewEditor.errors.userAlreadyExists.test.tsx @@ -7,10 +7,6 @@ import { userTriesToAddNewEditor } from "./helpers/userTriesToAddNewEditor"; import { mockTeamMembersData } from "./mocks/mockTeamMembersData"; import { alreadyExistingUser } from "./mocks/mockUsers"; -vi.mock("lib/featureFlags.ts", () => ({ - hasFeatureFlag: vi.fn().mockReturnValue(true), -})); - vi.mock( "pages/FlowEditor/components/Team/queries/createAndAddUserToTeam.tsx", () => ({ @@ -20,7 +16,6 @@ vi.mock( }), }), ); - let initialState: FullStore; describe("when a user fills in the 'add a new editor' form correctly but the user already exists", () => { @@ -30,7 +25,7 @@ describe("when a user fills in the 'add a new editor' form correctly but the use teamMembers: [...mockTeamMembersData, alreadyExistingUser], }); - const user = await setupTeamMembersScreen(); + const { user } = await setupTeamMembersScreen(); await userTriesToAddNewEditor(user); }); diff --git a/editor.planx.uk/src/pages/FlowEditor/components/Team/tests/TeamMembers.addNewEditor.test.tsx b/editor.planx.uk/src/pages/FlowEditor/components/Team/tests/TeamMembers.addNewEditor.test.tsx index 85959a34ea..b068698544 100644 --- a/editor.planx.uk/src/pages/FlowEditor/components/Team/tests/TeamMembers.addNewEditor.test.tsx +++ b/editor.planx.uk/src/pages/FlowEditor/components/Team/tests/TeamMembers.addNewEditor.test.tsx @@ -1,15 +1,17 @@ import { screen, waitFor, within } from "@testing-library/react"; import { FullStore, useStore } from "pages/FlowEditor/lib/store"; +import React from "react"; +import { DndProvider } from "react-dnd"; +import { HTML5Backend } from "react-dnd-html5-backend"; import { vi } from "vitest"; +import { axe } from "vitest-axe"; +import { setup } from "../../../../../testUtils"; +import { AddNewEditorModal } from "../components/AddNewEditorModal"; import { setupTeamMembersScreen } from "./helpers/setupTeamMembersScreen"; import { userTriesToAddNewEditor } from "./helpers/userTriesToAddNewEditor"; import { mockTeamMembersData } from "./mocks/mockTeamMembersData"; -vi.mock("lib/featureFlags.ts", () => ({ - hasFeatureFlag: vi.fn().mockReturnValue(true), -})); - vi.mock( "pages/FlowEditor/components/Team/queries/createAndAddUserToTeam.tsx", () => ({ @@ -22,10 +24,10 @@ vi.mock( let initialState: FullStore; -describe("when a user with the ADD_NEW_EDITOR feature flag enabled presses 'add a new editor'", () => { +describe("when a user presses 'add a new editor'", () => { beforeEach(async () => { useStore.setState({ teamMembers: mockTeamMembersData }); - const user = await setupTeamMembersScreen(); + const { user } = await setupTeamMembersScreen(); const teamEditorsTable = screen.getByTestId("team-editors"); const addEditorButton = await within(teamEditorsTable).findByText( @@ -44,7 +46,7 @@ describe("when a user fills in the 'add a new editor' form correctly", () => { afterAll(() => useStore.setState(initialState)); beforeEach(async () => { useStore.setState({ teamMembers: mockTeamMembersData }); - const user = await setupTeamMembersScreen(); + const { user } = await setupTeamMembersScreen(); await userTriesToAddNewEditor(user); }); @@ -70,3 +72,22 @@ describe("when a user fills in the 'add a new editor' form correctly", () => { ).toBeInTheDocument(); }); }); + +describe("when the addNewEditor modal is rendered", () => { + it("should not have any accessibility issues", async () => { + const { container } = setup( + + {}} + showModal={true} + setShowModal={() => {}} + setShowSuccessToast={() => {}} + /> + , + ); + await screen.findByTestId("modal-create-user"); + + const results = await axe(container); + expect(results).toHaveNoViolations(); + }); +}); diff --git a/editor.planx.uk/src/pages/FlowEditor/components/Team/tests/exampleTeamMembersData.tsx b/editor.planx.uk/src/pages/FlowEditor/components/Team/tests/exampleTeamMembersData.tsx deleted file mode 100644 index f1b15fd214..0000000000 --- a/editor.planx.uk/src/pages/FlowEditor/components/Team/tests/exampleTeamMembersData.tsx +++ /dev/null @@ -1,18 +0,0 @@ -import { TeamMember } from "../types"; - -export const exampleTeamMembersData: TeamMember[] = [ - { - firstName: "Donella", - lastName: "Meadows", - email: "donella@example.com", - id: 1, - role: "platformAdmin", - }, - { - firstName: "Bill", - lastName: "Sharpe", - email: "bill@example.com", - id: 2, - role: "teamEditor", - }, -]; diff --git a/editor.planx.uk/src/pages/FlowEditor/components/Team/tests/helpers/setupTeamMembersScreen.tsx b/editor.planx.uk/src/pages/FlowEditor/components/Team/tests/helpers/setupTeamMembersScreen.tsx index b79b31ea84..a5e31dfb44 100644 --- a/editor.planx.uk/src/pages/FlowEditor/components/Team/tests/helpers/setupTeamMembersScreen.tsx +++ b/editor.planx.uk/src/pages/FlowEditor/components/Team/tests/helpers/setupTeamMembersScreen.tsx @@ -8,13 +8,13 @@ import { setup } from "../../../../../../testUtils"; import { TeamMembers } from "../../TeamMembers"; export const setupTeamMembersScreen = async () => { - const { user } = setup( + const setupResult = setup( , ); - await screen.findByText("Team editors"); - return user; + await screen.findByTestId("team-editors"); + return setupResult; }; diff --git a/editor.planx.uk/src/pages/Preview/Node.tsx b/editor.planx.uk/src/pages/Preview/Node.tsx index ad640df365..239511cd57 100644 --- a/editor.planx.uk/src/pages/Preview/Node.tsx +++ b/editor.planx.uk/src/pages/Preview/Node.tsx @@ -44,28 +44,22 @@ interface Props { data?: any; } -const Node: React.FC = (props: Props) => { +const Node: React.FC = (props) => { const [ childNodesOf, resultData, hasPaid, - passport, isFinalCard, resetPreview, - sessionId, cachedBreadcrumbs, - flowName, flowSettings, ] = useStore((state) => [ state.childNodesOf, state.resultData, state.hasPaid(), - state.computePassport(), state.isFinalCard(), state.resetPreview, - state.sessionId, state.cachedBreadcrumbs, - state.flowName, state.flowSettings, ]); diff --git a/editor.planx.uk/src/theme.ts b/editor.planx.uk/src/theme.ts index 66a95480d0..088d927808 100644 --- a/editor.planx.uk/src/theme.ts +++ b/editor.planx.uk/src/theme.ts @@ -470,6 +470,29 @@ const getThemeOptions = ({ }, ], }, + MuiInputBase: { + styleOverrides: { + root: { + "&.Mui-disabled": { + backgroundColor: palette.grey[200], + }, + }, + }, + }, + MuiSelect: { + styleOverrides: { + root: { + "&.Mui-disabled": { + backgroundColor: palette.grey[200], + }, + }, + icon: { + "&.Mui-disabled": { + color: palette.grey[400], + }, + }, + }, + }, MuiSwitch: { styleOverrides: { root: { diff --git a/sharedb.planx.uk/package.json b/sharedb.planx.uk/package.json index f69b3a89b9..2a50982362 100644 --- a/sharedb.planx.uk/package.json +++ b/sharedb.planx.uk/package.json @@ -5,7 +5,7 @@ "dependencies": { "@teamwork/websocket-json-stream": "^2.0.0", "dompurify": "^3.1.2", - "jsdom": "^24.1.0", + "jsdom": "^25.0.0", "jsonwebtoken": "^8.5.1", "pg": "^8.12.0", "sharedb": "^4.1.1", diff --git a/sharedb.planx.uk/pnpm-lock.yaml b/sharedb.planx.uk/pnpm-lock.yaml index efc01ef711..5a058679eb 100644 --- a/sharedb.planx.uk/pnpm-lock.yaml +++ b/sharedb.planx.uk/pnpm-lock.yaml @@ -17,8 +17,8 @@ dependencies: specifier: ^3.1.2 version: 3.1.2 jsdom: - specifier: ^24.1.0 - version: 24.1.0 + specifier: ^25.0.0 + version: 25.0.0 jsonwebtoken: specifier: '>=9.0.0' version: 9.0.1 @@ -200,8 +200,8 @@ packages: - supports-color dev: false - /https-proxy-agent@7.0.4: - resolution: {integrity: sha512-wlwpilI7YdjSkWaQ/7omYBMTliDcmCN8OLihO6I9B86g06lMyAoqgoDpV0XqoaPOKj+0DIdAvnsWfyAAhmimcg==} + /https-proxy-agent@7.0.5: + resolution: {integrity: sha512-1e4Wqeblerz+tMKPIq2EMGiiWW1dIjZOksyHWSUm1rmuvw/how9hBHZ38lAGj5ID4Ik6EdkOw7NmWPy6LAwalw==} engines: {node: '>= 14'} dependencies: agent-base: 7.1.0 @@ -244,8 +244,8 @@ packages: resolution: {integrity: sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==} dev: true - /jsdom@24.1.0: - resolution: {integrity: sha512-6gpM7pRXCwIOKxX47cgOyvyQDN/Eh0f1MeKySBV2xGdKtqJBLj8P25eY3EVCWo2mglDDzozR2r2MW4T+JiNUZA==} + /jsdom@25.0.0: + resolution: {integrity: sha512-OhoFVT59T7aEq75TVw9xxEfkXgacpqAhQaYgP9y/fDqWQCMB/b1H66RfmPm/MaeaAIU9nDwMOVTlPN51+ao6CQ==} engines: {node: '>=18'} peerDependencies: canvas: ^2.11.2 @@ -259,11 +259,11 @@ packages: form-data: 4.0.0 html-encoding-sniffer: 4.0.0 http-proxy-agent: 7.0.2 - https-proxy-agent: 7.0.4 + https-proxy-agent: 7.0.5 is-potential-custom-element-name: 1.0.1 - nwsapi: 2.2.10 + nwsapi: 2.2.12 parse5: 7.1.2 - rrweb-cssom: 0.7.0 + rrweb-cssom: 0.7.1 saxes: 6.0.0 symbol-tree: 3.2.4 tough-cookie: 4.1.4 @@ -272,7 +272,7 @@ packages: whatwg-encoding: 3.1.1 whatwg-mimetype: 4.0.0 whatwg-url: 14.0.0 - ws: 8.17.1 + ws: 8.18.0 xml-name-validator: 5.0.0 transitivePeerDependencies: - bufferutil @@ -365,8 +365,8 @@ packages: which: 2.0.2 dev: true - /nwsapi@2.2.10: - resolution: {integrity: sha512-QK0sRs7MKv0tKe1+5uZIQk/C8XGza4DAnztJG8iD+TpJIORARrCxczA738awHrZoHeTjSSoHqao2teO0dC/gFQ==} + /nwsapi@2.2.12: + resolution: {integrity: sha512-qXDmcVlZV4XRtKFzddidpfVP4oMSGhga+xdMc25mv8kaLUHtgzCDhUxkrN8exkGdTlLNaXj7CV3GtON7zuGZ+w==} dev: false /ot-json0@1.1.0: @@ -497,8 +497,8 @@ packages: resolution: {integrity: sha512-APM0Gt1KoXBz0iIkkdB/kfvGOwC4UuJFeG/c+yV7wSc7q96cG/kJ0HiYCnzivD9SB53cLV1MlHFNfOuPaadYSw==} dev: false - /rrweb-cssom@0.7.0: - resolution: {integrity: sha512-KlSv0pm9kgQSRxXEMgtivPJ4h826YHsuob8pSHcfSZsSXGtvpEAie8S0AnXuObEJ7nhikOb4ahwxDm0H2yW17g==} + /rrweb-cssom@0.7.1: + resolution: {integrity: sha512-TrEMa7JGdVm0UThDJSx7ddw5nVm3UJS9o9CCIZ72B1vSyEZoziDqBYP3XIoi/12lKrJR8rE3jeFHMok2F/Mnsg==} dev: false /safe-buffer@5.2.1: @@ -638,6 +638,19 @@ packages: optional: true dev: false + /ws@8.18.0: + resolution: {integrity: sha512-8VbfWfHLbbwu3+N6OKsOMpBdT4kXPDDB9cJk2bJ6mh9ucxdlnNvH1e+roYkKmN9Nxw2yjz7VzeO9oOz2zJ04Pw==} + engines: {node: '>=10.0.0'} + peerDependencies: + bufferutil: ^4.0.1 + utf-8-validate: '>=5.0.2' + peerDependenciesMeta: + bufferutil: + optional: true + utf-8-validate: + optional: true + dev: false + /xml-name-validator@5.0.0: resolution: {integrity: sha512-EvGK8EJ3DhaHfbRlETOWAS5pO9MZITeauHKJyb8wyajUfQUenkIg2MvLDTZ4T/TgIcm3HU0TFBgWWboAZ30UHg==} engines: {node: '>=18'}