From 0b6b0109a6698034f8d0871ce84b60623f0cdfb7 Mon Sep 17 00:00:00 2001 From: Jo Humphrey <31373245+jamdelion@users.noreply.github.com> Date: Wed, 28 Aug 2024 09:54:00 +0100 Subject: [PATCH] feat: add new editor - error handling and enhancements (#3543) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Dafydd Llŷr Pearson --- .../Team/components/AddNewEditorModal.tsx | 79 +++++++++++++------ .../Team/components/MembersTable.tsx | 35 +++++++- .../Team/errors/addNewEditorErrors.tsx | 8 ++ .../TeamMembers.addNewEditor.errors.test.tsx | 44 +++++++++++ .../tests/TeamMembers.addNewEditor.test.tsx | 37 ++++----- .../tests/helpers/setupTeamMembersScreen.tsx | 4 - .../Team/tests/helpers/userEntersInput.tsx | 8 +- .../tests/helpers/userTriesToAddNewEditor.tsx | 27 +++++++ .../Team/tests/mocks/mockTeamMembersData.tsx | 18 +++++ .../components/Team/tests/mocks/mockUsers.tsx | 9 +++ .../pages/FlowEditor/components/Team/types.ts | 1 + 11 files changed, 218 insertions(+), 52 deletions(-) create mode 100644 editor.planx.uk/src/pages/FlowEditor/components/Team/errors/addNewEditorErrors.tsx create mode 100644 editor.planx.uk/src/pages/FlowEditor/components/Team/tests/TeamMembers.addNewEditor.errors.test.tsx create mode 100644 editor.planx.uk/src/pages/FlowEditor/components/Team/tests/helpers/userTriesToAddNewEditor.tsx create mode 100644 editor.planx.uk/src/pages/FlowEditor/components/Team/tests/mocks/mockTeamMembersData.tsx create mode 100644 editor.planx.uk/src/pages/FlowEditor/components/Team/tests/mocks/mockUsers.tsx 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 a2b24ff48a..0f942e9de0 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 @@ -6,11 +6,16 @@ import DialogContent from "@mui/material/DialogContent"; import Typography from "@mui/material/Typography"; import { FormikHelpers, useFormik } from "formik"; import { useStore } from "pages/FlowEditor/lib/store"; -import React from "react"; +import React, { useState } from "react"; import InputGroup from "ui/editor/InputGroup"; import InputLabel from "ui/editor/InputLabel"; +import ErrorWrapper from "ui/shared/ErrorWrapper"; import Input from "ui/shared/Input"; +import { + AddNewEditorErrors, + isUserAlreadyExistsError, +} from "../errors/addNewEditorErrors"; import { addNewEditorFormSchema } from "../formSchema"; import { createAndAddUserToTeam } from "../queries/createAndAddUserToTeam"; import { AddNewEditorFormValues, AddNewEditorModalProps } from "../types"; @@ -19,7 +24,15 @@ import { optimisticallyUpdateMembersTable } from "./lib/optimisticallyUpdateMemb export const AddNewEditorModal = ({ showModal, setShowModal, + setShowToast, }: AddNewEditorModalProps) => { + const [showUserAlreadyExistsError, setShowUserAlreadyExistsError] = + useState(false); + + const clearErrors = () => { + setShowUserAlreadyExistsError(false); + }; + const handleSubmit = async ( values: AddNewEditorFormValues, { resetForm }: FormikHelpers, @@ -32,11 +45,20 @@ export const AddNewEditorModal = ({ values.lastName, teamId, teamSlug, - ); + ).catch((err) => { + if (isUserAlreadyExistsError(err.message)) { + setShowUserAlreadyExistsError(true); + } + console.error(err); + }); + if (!newUserId) { + return; + } + clearErrors(); optimisticallyUpdateMembersTable(values, newUserId); - setShowModal(false); + setShowToast(true); resetForm({ values }); }; @@ -53,6 +75,7 @@ export const AddNewEditorModal = ({ return ( ({ width: "100%", @@ -119,26 +142,36 @@ export const AddNewEditorModal = ({ padding: 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..39755d6a60 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 @@ -1,4 +1,6 @@ +import Alert from "@mui/material/Alert"; import Chip from "@mui/material/Chip"; +import Snackbar from "@mui/material/Snackbar"; import Table from "@mui/material/Table"; import TableBody from "@mui/material/TableBody"; import TableCell from "@mui/material/TableCell"; @@ -18,6 +20,18 @@ export const MembersTable = ({ showAddMemberButton, }: MembersTableProps) => { const [showModal, setShowModal] = useState(false); + const [showToast, setShowToast] = useState(false); + + const handleCloseToast = ( + _event?: React.SyntheticEvent | Event, + reason?: string, + ) => { + if (reason === "clickaway") { + return; + } + + setShowToast(false); + }; const roleLabels: Record = { platformAdmin: "Admin", @@ -99,9 +113,28 @@ export const MembersTable = ({ )} + {showAddMemberButton && ( + + + Successfully added a user + + + )} {showModal && ( - + )} ); diff --git a/editor.planx.uk/src/pages/FlowEditor/components/Team/errors/addNewEditorErrors.tsx b/editor.planx.uk/src/pages/FlowEditor/components/Team/errors/addNewEditorErrors.tsx new file mode 100644 index 0000000000..c700f17bae --- /dev/null +++ b/editor.planx.uk/src/pages/FlowEditor/components/Team/errors/addNewEditorErrors.tsx @@ -0,0 +1,8 @@ +export const AddNewEditorErrors = { + USER_ALREADY_EXISTS: { + regex: /violates unique constraint "users_email_key"/i, + errorMessage: "User already exists", + }, +}; +export const isUserAlreadyExistsError = (error: string) => + AddNewEditorErrors.USER_ALREADY_EXISTS.regex.test(error); 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.test.tsx new file mode 100644 index 0000000000..da52420e26 --- /dev/null +++ b/editor.planx.uk/src/pages/FlowEditor/components/Team/tests/TeamMembers.addNewEditor.errors.test.tsx @@ -0,0 +1,44 @@ +import { screen, within } 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"; + +vi.mock("lib/featureFlags.ts", () => ({ + hasFeatureFlag: vi.fn().mockReturnValue(true), +})); + +vi.mock( + "pages/FlowEditor/components/Team/queries/createAndAddUserToTeam.tsx", + () => ({ + createAndAddUserToTeam: vi.fn().mockRejectedValue({ + message: + 'Uniqueness violation. duplicate key value violates unique constraint "users_email_key"', + }), + }), +); + +let initialState: FullStore; + +describe("when a user fills in the 'add a new editor' form correctly but the user already exists", () => { + 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 () => { + const addNewEditorModal = await screen.findByTestId("dialog-create-user"); + + expect( + await within(addNewEditorModal).findByText(/User already exists/), + ).toBeInTheDocument(); + }); +}); 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 ec240778e5..85959a34ea 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 @@ -3,7 +3,8 @@ import { FullStore, useStore } from "pages/FlowEditor/lib/store"; import { vi } from "vitest"; import { setupTeamMembersScreen } from "./helpers/setupTeamMembersScreen"; -import { userEntersInput } from "./helpers/userEntersInput"; +import { userTriesToAddNewEditor } from "./helpers/userTriesToAddNewEditor"; +import { mockTeamMembersData } from "./mocks/mockTeamMembersData"; vi.mock("lib/featureFlags.ts", () => ({ hasFeatureFlag: vi.fn().mockReturnValue(true), @@ -23,6 +24,7 @@ let initialState: FullStore; describe("when a user with the ADD_NEW_EDITOR feature flag enabled presses 'add a new editor'", () => { beforeEach(async () => { + useStore.setState({ teamMembers: mockTeamMembersData }); const user = await setupTeamMembersScreen(); const teamEditorsTable = screen.getByTestId("team-editors"); @@ -41,26 +43,9 @@ describe("when a user with the ADD_NEW_EDITOR feature flag enabled presses 'add 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 teamEditorsTable = screen.getByTestId("team-editors"); - const addEditorButton = await within(teamEditorsTable).findByText( - "Add a new editor", - ); - user.click(addEditorButton); - const addNewEditorModal = await screen.findByTestId("modal-create-user"); - await userEntersInput("First name", "Mickey", addNewEditorModal); - await userEntersInput("Last name", "Mouse", addNewEditorModal); - await userEntersInput( - "Email address", - "mickeymouse@email.com", - addNewEditorModal, - ); - - const createUserButton = await screen.findByTestId( - "modal-create-user-button", - ); - - user.click(createUserButton); + await userTriesToAddNewEditor(user); }); it("adds the new user row to the Team Editors table", async () => { @@ -72,4 +57,16 @@ describe("when a user fills in the 'add a new editor' form correctly", () => { ).toBeInTheDocument(); }); }); + + it("closes the modal", async () => { + await waitFor(() => { + expect(screen.queryByTestId("modal-create-user")).not.toBeInTheDocument(); + }); + }); + + it("shows a success message", async () => { + expect( + await screen.findByText(/Successfully added a user/), + ).toBeInTheDocument(); + }); }); 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 a6eea16445..f5fdb35b33 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 @@ -1,16 +1,12 @@ import { screen } from "@testing-library/react"; -import { useStore } from "pages/FlowEditor/lib/store"; import React from "react"; import { DndProvider } from "react-dnd"; import { HTML5Backend } from "react-dnd-html5-backend"; import { setup } from "../../../../../../testUtils"; import { TeamMembers } from "../../TeamMembers"; -import { exampleTeamMembersData } from "../exampleTeamMembersData"; export const setupTeamMembersScreen = async () => { - useStore.setState({ teamMembers: exampleTeamMembersData }); - const { user } = setup( diff --git a/editor.planx.uk/src/pages/FlowEditor/components/Team/tests/helpers/userEntersInput.tsx b/editor.planx.uk/src/pages/FlowEditor/components/Team/tests/helpers/userEntersInput.tsx index 546955d2c6..f414d307ad 100644 --- a/editor.planx.uk/src/pages/FlowEditor/components/Team/tests/helpers/userEntersInput.tsx +++ b/editor.planx.uk/src/pages/FlowEditor/components/Team/tests/helpers/userEntersInput.tsx @@ -1,13 +1,13 @@ -import { fireEvent, within } from "@testing-library/react"; +import { within } from "@testing-library/react"; +import { UserEvent } from "@testing-library/user-event/dist/types/setup/setup"; export const userEntersInput = async ( labelText: string, inputString: string, container: HTMLElement, + user: UserEvent, ) => { const inputField = await within(container).findByLabelText(labelText); - fireEvent.change(inputField, { - target: { value: inputString }, - }); + await user.type(inputField, inputString); }; diff --git a/editor.planx.uk/src/pages/FlowEditor/components/Team/tests/helpers/userTriesToAddNewEditor.tsx b/editor.planx.uk/src/pages/FlowEditor/components/Team/tests/helpers/userTriesToAddNewEditor.tsx new file mode 100644 index 0000000000..c78eb956c0 --- /dev/null +++ b/editor.planx.uk/src/pages/FlowEditor/components/Team/tests/helpers/userTriesToAddNewEditor.tsx @@ -0,0 +1,27 @@ +import { screen, within } from "@testing-library/react"; +import { UserEvent } from "@testing-library/user-event/dist/types/setup/setup"; + +import { userEntersInput } from "./userEntersInput"; + +export const userTriesToAddNewEditor = async (user: UserEvent) => { + const teamEditorsTable = screen.getByTestId("team-editors"); + const addEditorButton = await within(teamEditorsTable).findByText( + "Add a new editor", + ); + user.click(addEditorButton); + const addNewEditorModal = await screen.findByTestId("modal-create-user"); + await userEntersInput("First name", "Mickey", addNewEditorModal, user); + await userEntersInput("Last name", "Mouse", addNewEditorModal, user); + await userEntersInput( + "Email address", + "mickeymouse@email.com", + addNewEditorModal, + user, + ); + + const createUserButton = await screen.findByTestId( + "modal-create-user-button", + ); + + user.click(createUserButton); +}; diff --git a/editor.planx.uk/src/pages/FlowEditor/components/Team/tests/mocks/mockTeamMembersData.tsx b/editor.planx.uk/src/pages/FlowEditor/components/Team/tests/mocks/mockTeamMembersData.tsx new file mode 100644 index 0000000000..092873fa1d --- /dev/null +++ b/editor.planx.uk/src/pages/FlowEditor/components/Team/tests/mocks/mockTeamMembersData.tsx @@ -0,0 +1,18 @@ +import { TeamMember } from "../../types"; + +export const mockTeamMembersData: 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/mocks/mockUsers.tsx b/editor.planx.uk/src/pages/FlowEditor/components/Team/tests/mocks/mockUsers.tsx new file mode 100644 index 0000000000..c8d2de0121 --- /dev/null +++ b/editor.planx.uk/src/pages/FlowEditor/components/Team/tests/mocks/mockUsers.tsx @@ -0,0 +1,9 @@ +import { TeamMember } from "../../types"; + +export const alreadyExistingUser: TeamMember = { + firstName: "Mickey", + lastName: "Mouse", + email: "mickeymouse@email.com", + id: 3, + role: "teamEditor", +}; diff --git a/editor.planx.uk/src/pages/FlowEditor/components/Team/types.ts b/editor.planx.uk/src/pages/FlowEditor/components/Team/types.ts index bf823c5352..ef03981734 100644 --- a/editor.planx.uk/src/pages/FlowEditor/components/Team/types.ts +++ b/editor.planx.uk/src/pages/FlowEditor/components/Team/types.ts @@ -12,6 +12,7 @@ export interface MembersTableProps { export interface AddNewEditorModalProps { showModal: boolean; setShowModal: React.Dispatch>; + setShowToast: React.Dispatch>; } export interface AddNewEditorFormValues {