Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: add new editor - error handling and enhancements #3543

Merged
merged 43 commits into from
Aug 28, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
43 commits
Select commit Hold shift + click to select a range
a4d60bf
fix: refactor Team component folder structure
jamdelion Aug 5, 2024
f7e87fb
Basic "add new editor" button on team editors table
jamdelion Aug 5, 2024
5f38c8a
Merge main
jamdelion Aug 5, 2024
d2f9c59
Hide button behind feature flag
jamdelion Aug 6, 2024
0a566a1
Rename test
jamdelion Aug 6, 2024
edc221b
Test button appears within team editors table only
jamdelion Aug 6, 2024
fe7ce77
Style button correctly
jamdelion Aug 6, 2024
12df25c
Basic open modal functionality
jamdelion Aug 6, 2024
2975369
Move modal to new component and add static input fields
jamdelion Aug 7, 2024
3240488
Extract setup helper from test
jamdelion Aug 7, 2024
50c0a9c
Extract props type
jamdelion Aug 7, 2024
f6d7f25
Use formik for form validation and submission
jamdelion Aug 8, 2024
b419666
Implement changes requested
jamdelion Aug 8, 2024
5d189eb
Merge branch 'jh/add-team-editor-button' into jh/validate-add-team-ed…
jamdelion Aug 8, 2024
c5f31a8
Get basic createUser working
jamdelion Aug 8, 2024
0f368f5
Merge branch 'main' into jh/validate-add-team-editor
jamdelion Aug 12, 2024
4a27c5c
Basic addUserToTeam mutation
jamdelion Aug 12, 2024
3f81d7b
Split out queries into own folder
jamdelion Aug 12, 2024
fc91022
Tidy up - move type defs and schemas
jamdelion Aug 12, 2024
4ec6cb6
Fix import
jamdelion Aug 12, 2024
e616c02
Start writing a test for form submission - close modal
jamdelion Aug 12, 2024
237a0ee
Optimistically update the table when fetching complete, plus refactors
jamdelion Aug 14, 2024
29624c1
Test that new row added to team editors table
jamdelion Aug 14, 2024
522e2d7
Update editor.planx.uk/src/pages/FlowEditor/components/Team/component…
jamdelion Aug 19, 2024
37c8a3f
Use teamId from store instead of fetchCurrentTeam
jamdelion Aug 19, 2024
b6a12d2
Combine createUser and addToTeam mutations into one
jamdelion Aug 19, 2024
2e1fedc
Use store to optimistically update, plus refactor member filtering
jamdelion Aug 20, 2024
8a3459d
Fix tests
jamdelion Aug 20, 2024
126d6d4
Merge branch 'main' into jh/validate-add-team-editor
jamdelion Aug 20, 2024
a5dfb84
Show toast message on success
jamdelion Aug 21, 2024
a78a86d
Handle user already exists er
jamdelion Aug 21, 2024
a4904bb
Refactor tests
jamdelion Aug 21, 2024
0a1aee5
Extract error logic out
jamdelion Aug 21, 2024
034d4d4
Add in correct close-modal test
jamdelion Aug 21, 2024
45ef05c
Improve platformAdmins filter
jamdelion Aug 22, 2024
752fb93
Use returned userId as key in table update
jamdelion Aug 22, 2024
99b38c6
Fix teamMember store setting
jamdelion Aug 22, 2024
55c558b
Refetch query when user created and added to team
jamdelion Aug 23, 2024
1c5651f
Merge branch 'main' into jh/validate-add-team-editor
jamdelion Aug 26, 2024
096f824
Merge branch 'jh/validate-add-team-editor' into jh/add-editor-error-h…
jamdelion Aug 26, 2024
c54b7c7
Merge branch 'main' into jh/add-editor-error-handling
jamdelion Aug 26, 2024
0cfbd19
Delete duplicate files from merge
jamdelion Aug 26, 2024
0cb25f9
Use user.type instead of fireEvent
jamdelion Aug 27, 2024
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion editor.planx.uk/src/components/Feedback/FeedbackForm.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import Button from "@mui/material/Button";
import Link from "@mui/material/Link";
import { styled } from "@mui/material/styles";
import { contentFlowSpacing } from "@planx/components/shared/Preview/Card";
import { Form, Formik, useFormikContext } from "formik";
Expand All @@ -7,8 +8,8 @@ import FeedbackDisclaimer from "ui/public/FeedbackDisclaimer";
import InputLabel from "ui/public/InputLabel";
import ErrorWrapper from "ui/shared/ErrorWrapper";
import Input from "ui/shared/Input";

import { FeedbackFormInput, FormProps, UserFeedback } from ".";
import Link from "@mui/material/Link";

const StyledForm = styled(Form)(({ theme }) => ({
"& > *": contentFlowSpacing(theme),
Expand Down
10 changes: 5 additions & 5 deletions editor.planx.uk/src/components/Feedback/index.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -183,7 +183,7 @@ describe("Feedback component triage journey", () => {

await user.type(
getByTestId("userCommentTextarea"),
"This information is wrong"
"This information is wrong",
);

await user.click(getByText("Send feedback"));
Expand All @@ -210,7 +210,7 @@ describe("Feedback component 'Report an issue with this page journey'", () => {

await waitFor(() => {
expect(
getByText("Report an issue with this service")
getByText("Report an issue with this service"),
).toBeInTheDocument();
expect(getByLabelText("What were you doing?")).toBeInTheDocument();
expect(getByLabelText("What went wrong?")).toBeInTheDocument();
Expand Down Expand Up @@ -325,7 +325,7 @@ describe("Feedback component accessibility", () => {

test("Issue form via triage should have no accessibility violations", async () => {
const { container, getByText, getByLabelText, getByRole, user } = setup(
<Feedback />
<Feedback />,
);

await user.click(getByText("feedback"));
Expand Down Expand Up @@ -389,11 +389,11 @@ describe("Feedback component accessibility", () => {

await user.type(
getByLabelText("What were you doing?"),
"Answering a question"
"Answering a question",
);
await user.type(
getByLabelText("What went wrong?"),
"I couldn't select Continue"
"I couldn't select Continue",
);

await user.click(getByText("Send feedback"));
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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";
Expand All @@ -19,7 +24,15 @@ import { optimisticallyUpdateMembersTable } from "./lib/optimisticallyUpdateMemb
export const AddNewEditorModal = ({
showModal,
setShowModal,
setShowToast,
}: AddNewEditorModalProps) => {
const [showUserAlreadyExistsError, setShowUserAlreadyExistsError] =
useState<boolean>(false);

const clearErrors = () => {
setShowUserAlreadyExistsError(false);
};

const handleSubmit = async (
values: AddNewEditorFormValues,
{ resetForm }: FormikHelpers<AddNewEditorFormValues>,
Expand All @@ -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 });
};

Expand All @@ -53,6 +75,7 @@ export const AddNewEditorModal = ({
return (
<Dialog
aria-labelledby="dialog-heading"
data-testid="dialog-create-user"
PaperProps={{
sx: (theme) => ({
width: "100%",
Expand Down Expand Up @@ -119,26 +142,36 @@ export const AddNewEditorModal = ({
padding: 2,
}}
>
<Box>
<Button
variant="contained"
color="prompt"
type="submit"
data-testid="modal-create-user-button"
>
Create user
</Button>
<Button
variant="contained"
color="secondary"
type="reset"
sx={{ ml: 1.5 }}
onClick={() => setShowModal(false)}
data-testid="modal-cancel-button"
>
Cancel
</Button>
</Box>
<ErrorWrapper
error={
showUserAlreadyExistsError
? AddNewEditorErrors.USER_ALREADY_EXISTS.errorMessage
: undefined
}
>
<Box>
<>
<Button
variant="contained"
color="prompt"
type="submit"
data-testid="modal-create-user-button"
>
Create user
</Button>
<Button
variant="contained"
color="secondary"
type="reset"
sx={{ ml: 1.5 }}
onClick={() => setShowModal(false)}
data-testid="modal-cancel-button"
>
Cancel
</Button>
</>
</Box>
</ErrorWrapper>
</DialogActions>
</form>
</Dialog>
Expand Down
Original file line number Diff line number Diff line change
@@ -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";
Expand All @@ -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<string, string> = {
platformAdmin: "Admin",
Expand Down Expand Up @@ -99,9 +113,28 @@ export const MembersTable = ({
)}
</TableBody>
</Table>
{showAddMemberButton && (
<Snackbar
open={showToast}
autoHideDuration={6000}
onClose={handleCloseToast}
>
<Alert
onClose={handleCloseToast}
severity="success"
sx={{ width: "100%" }}
>
Successfully added a user
</Alert>
</Snackbar>
)}
</TableContainer>
{showModal && (
<AddNewEditorModal showModal={showModal} setShowModal={setShowModal} />
<AddNewEditorModal
setShowToast={setShowToast}
showModal={showModal}
setShowModal={setShowModal}
/>
)}
</>
);
Expand Down
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nice solution!

Original file line number Diff line number Diff line change
@@ -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);
Original file line number Diff line number Diff line change
@@ -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();
});
});
Original file line number Diff line number Diff line change
@@ -1,10 +1,10 @@

import { screen, waitFor, within } from "@testing-library/react";
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),
Expand All @@ -24,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");
Expand All @@ -42,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",
"[email protected]",
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 () => {
Expand All @@ -73,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();
});
});
Original file line number Diff line number Diff line change
@@ -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(
<DndProvider backend={HTML5Backend}>
<TeamMembers />
Expand Down
Original file line number Diff line number Diff line change
@@ -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);
};
Loading
Loading