Skip to content

Commit

Permalink
feat: delete groups panel
Browse files Browse the repository at this point in the history
  • Loading branch information
huwshimi committed Jul 1, 2024
1 parent fd3b9fa commit b945b94
Show file tree
Hide file tree
Showing 19 changed files with 443 additions and 34 deletions.
129 changes: 129 additions & 0 deletions src/components/DeleteEntityPanel/DeleteEntityPanel.test.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,129 @@
import { screen } from "@testing-library/react";
import userEvent from "@testing-library/user-event";
import { vi } from "vitest";

import { renderComponent } from "test/utils";

import DeleteEntityPanel from "./DeleteEntityPanel";
import { Label } from "./types";

test("should render correctly for 1 entity", async () => {
renderComponent(
<DeleteEntityPanel
entity="test"
count={1}
close={vi.fn()}
onDelete={vi.fn()}
isDeletePending={false}
/>,
);
expect(screen.getByText("Delete 1 test")).toBeInTheDocument();
expect(
screen.getByText("Are you sure you want to delete 1 test?", {
exact: false,
}),
).toBeInTheDocument();
expect(
screen.getByText(
"The deletion of tests is irreversible and might adversely affect your system.",
{ exact: false },
),
).toBeInTheDocument();
const textBoxes = screen.getAllByRole("textbox");
expect(textBoxes).toHaveLength(1);
expect(screen.getByRole("button", { name: Label.DELETE })).toBeDisabled();
expect(
screen.getByRole("button", { name: Label.CANCEL }),
).toBeInTheDocument();
});

test("should render correctly for multiple entities", async () => {
renderComponent(
<DeleteEntityPanel
entity="test"
count={2}
close={vi.fn()}
onDelete={vi.fn()}
isDeletePending={false}
/>,
);
expect(screen.getByText("Delete 2 tests")).toBeInTheDocument();
expect(
screen.getByText("Are you sure you want to delete 2 tests?", {
exact: false,
}),
).toBeInTheDocument();
expect(
screen.getByText(
"The deletion of tests is irreversible and might adversely affect your system.",
{ exact: false },
),
).toBeInTheDocument();
const textBoxes = screen.getAllByRole("textbox");
expect(textBoxes).toHaveLength(1);
expect(screen.getByRole("button", { name: Label.DELETE })).toBeDisabled();
expect(
screen.getByRole("button", { name: Label.CANCEL }),
).toBeInTheDocument();
});

test("should enable the delete button when the confirmation message is correct", async () => {
renderComponent(
<DeleteEntityPanel
entity="test"
count={1}
close={vi.fn()}
onDelete={vi.fn()}
isDeletePending={false}
/>,
);
expect(screen.getByRole("button", { name: Label.DELETE })).toBeDisabled();
await userEvent.type(screen.getByRole("textbox"), "remove 1 test");
expect(screen.getByRole("button", { name: Label.DELETE })).toBeEnabled();
});

test("should disable the delete button when the confirmation message is incorrect", async () => {
renderComponent(
<DeleteEntityPanel
entity="test"
count={2}
close={vi.fn()}
onDelete={vi.fn()}
isDeletePending={false}
/>,
);
expect(screen.getByRole("button", { name: Label.DELETE })).toBeDisabled();
await userEvent.type(screen.getByRole("textbox"), "remove 1 test");
expect(screen.getByRole("button", { name: Label.DELETE })).toBeDisabled();
});

test("should handle delete when the delete button is clicked", async () => {
const handleDelete = vi.fn();
renderComponent(
<DeleteEntityPanel
entity="test"
count={1}
close={vi.fn()}
onDelete={handleDelete}
isDeletePending={false}
/>,
);
await userEvent.type(screen.getByRole("textbox"), "remove 1 test");
await userEvent.click(screen.getByRole("button", { name: Label.DELETE }));
expect(handleDelete).toHaveBeenCalledTimes(1);
});

test("should handle close when the cancel button is clicked", async () => {
const handleClose = vi.fn();
renderComponent(
<DeleteEntityPanel
entity="test"
count={1}
close={handleClose}
onDelete={vi.fn()}
isDeletePending={false}
/>,
);
await userEvent.click(screen.getByRole("button", { name: Label.CANCEL }));
expect(handleClose).toHaveBeenCalledTimes(1);
});
70 changes: 70 additions & 0 deletions src/components/DeleteEntityPanel/DeleteEntityPanel.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
import { Col, Row, FormikField, Icon } from "@canonical/react-components";
import * as Yup from "yup";

import FormPanel from "components/FormPanel";

import type { Props, FormFields } from "./types";
import { Label } from "./types";

const DeleteEntityPanel = ({
entity,
count,
close,
onDelete,
isDeletePending,
}: Props) => {
const entityCount = `${count} ${entity}${count !== 1 ? "s" : ""}`;
const confirmationMessage = `remove ${entityCount}`;

const schema = Yup.object().shape({
confirmationMessage: Yup.string().oneOf(
[confirmationMessage],
Label.CONFIRMATION_MESSAGE_ERROR,
),
});

return (
<FormPanel<FormFields>
title={
<span className="p-heading--4 panel-form-navigation__current-title">
Delete {entityCount}
</span>
}
close={close}
submitLabel={
<>
<Icon name="delete" className="is-light" /> {Label.DELETE}
</>
}
validationSchema={schema}
isSaving={isDeletePending}
onSubmit={onDelete}
initialValues={{ confirmationMessage: "" }}
submitButtonAppearance="negative"
>
<Row>
<Col size={12}>
<p>
Are you sure you want to delete {entityCount}?<br />
The deletion of {entity}s is irreversible and might adversely affect
your system.
</p>
</Col>
<Col size={12}>
<FormikField
label={
<Col size={12}>
Type <b>{confirmationMessage}</b> to confirm.
</Col>
}
name="confirmationMessage"
type="text"
takeFocus
/>
</Col>
</Row>
</FormPanel>
);
};

export default DeleteEntityPanel;
2 changes: 2 additions & 0 deletions src/components/DeleteEntityPanel/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
export { default } from "./DeleteEntityPanel";
export { Label as DeleteEntityPanelLabel } from "./types";
17 changes: 17 additions & 0 deletions src/components/DeleteEntityPanel/types.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
export type FormFields = {
confirmationMessage: string;
};

export type Props = {
entity: string;
count: number;
close: () => void;
onDelete: () => Promise<void>;
isDeletePending: boolean;
};

export enum Label {
CANCEL = "Cancel",
DELETE = "Delete",
CONFIRMATION_MESSAGE_ERROR = "Wrong confirmation message",
}
23 changes: 23 additions & 0 deletions src/components/FormPanel/FormPanel.test.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
import { screen } from "@testing-library/react";
import { vi } from "vitest";

import { renderComponent } from "test/utils";

import FormPanel from "./FormPanel";

test("displays a form", async () => {
renderComponent(
<FormPanel<{ name: string }>
close={vi.fn()}
initialValues={{ name: "" }}
onSubmit={vi.fn()}
submitLabel="Submit!"
title="panel title"
>
Form content
</FormPanel>,
);
expect(screen.getByText("panel title")).toBeInTheDocument();
expect(screen.getByText("Form content")).toBeInTheDocument();
expect(screen.getByRole("button", { name: "Submit!" })).toBeInTheDocument();
});
17 changes: 17 additions & 0 deletions src/components/FormPanel/FormPanel.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
import type { FormikValues } from "formik";

import Panel from "components/Panel";
import PanelForm from "components/PanelForm";
import { SidePanelLabelledById } from "consts";

import { type Props } from "./types";

const FormPanel = <F extends FormikValues>({ title, ...props }: Props<F>) => {
return (
<Panel title={title} titleId={SidePanelLabelledById}>
<PanelForm<F> {...props} />
</Panel>
);
};

export default FormPanel;
2 changes: 2 additions & 0 deletions src/components/FormPanel/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
export { default } from "./FormPanel";
export * from "./types";
8 changes: 8 additions & 0 deletions src/components/FormPanel/types.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
import type { FormikValues } from "formik";

import type { PanelProps } from "components/Panel";
import type { Props as PanelFormProps } from "components/PanelForm";

export type Props<F extends FormikValues> = {
title: PanelProps["title"];
} & PanelFormProps<F>;
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ import {
import { Label as EntitlementsPanelFormLabel } from "components/EntitlementsPanelForm/types";
import { hasNotification, hasToast, renderComponent } from "test/utils";

import { Label as GroupPanelLabel } from "../GroupPanel";
import { GroupPanelLabel } from "../GroupPanel";
import { Label as IdentitiesPanelFormLabel } from "../IdentitiesPanelForm/types";
import { Label as RolesPanelFormLabel } from "../RolesPanelForm/types";

Expand Down
2 changes: 1 addition & 1 deletion src/components/pages/Groups/AddGroupPanel/types.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import type { Props as GroupPanelProps } from "../GroupPanel";
import type { GroupPanelProps } from "../GroupPanel";

export type Props = {
close: GroupPanelProps["close"];
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
import { screen } from "@testing-library/react";
import userEvent from "@testing-library/user-event";
import { setupServer } from "msw/node";
import { vi } from "vitest";

import {
getDeleteGroupsItemMockHandler,
getDeleteGroupsItemMockHandler400,
getDeleteGroupsItemResponseMock400,
} from "api/groups/groups.msw";
import { DeleteEntityPanelLabel } from "components/DeleteEntityPanel";
import { renderComponent, hasToast } from "test/utils";

import DeleteGroupsPanel from "./DeleteGroupsPanel";
import { Label as DeleteGroupsPanelLabel } from "./types";

const mockApiServer = setupServer(getDeleteGroupsItemMockHandler());

beforeAll(() => {
mockApiServer.listen();
});

afterEach(() => {
mockApiServer.resetHandlers();
});

afterAll(() => {
mockApiServer.close();
});

test("should delete groups", async () => {
renderComponent(
<DeleteGroupsPanel groups={["group1", "group2"]} close={vi.fn()} />,
);
const textBoxes = screen.getAllByRole("textbox");
expect(textBoxes).toHaveLength(1);
const confirmationMessageTextBox = textBoxes[0];
await userEvent.type(confirmationMessageTextBox, "remove 2 groups");
await userEvent.click(screen.getByText(DeleteEntityPanelLabel.DELETE));
await hasToast(DeleteGroupsPanelLabel.DEELTE_SUCCESS_MESSAGE, "positive");
});

test("should handle errors when deleting groups", async () => {
mockApiServer.use(
getDeleteGroupsItemMockHandler400(
getDeleteGroupsItemResponseMock400({ message: "Can't remove group" }),
),
);
renderComponent(<DeleteGroupsPanel groups={["group1"]} close={vi.fn()} />);
const textBoxes = screen.getAllByRole("textbox");
expect(textBoxes).toHaveLength(1);
const confirmationMessageTextBox = textBoxes[0];
await userEvent.type(confirmationMessageTextBox, "remove 1 group");
await userEvent.click(screen.getByText(DeleteEntityPanelLabel.DELETE));
await hasToast(DeleteGroupsPanelLabel.DELETE_ERROR_MESSAGE, "negative");
});
Loading

0 comments on commit b945b94

Please sign in to comment.