diff --git a/src/app/(protected)/team-members/_components/add-team-member-dialog.tsx b/src/app/(protected)/team-members/_components/add-team-member-dialog.tsx index 170f3ccf..ef9064df 100644 --- a/src/app/(protected)/team-members/_components/add-team-member-dialog.tsx +++ b/src/app/(protected)/team-members/_components/add-team-member-dialog.tsx @@ -1,4 +1,4 @@ -import { useEffect, useMemo, useState, type Dispatch, type SetStateAction } from "react"; +import { useEffect, useMemo, useRef, useState, type Dispatch, type SetStateAction } from "react"; import { Accordion, AccordionDetails, @@ -278,15 +278,25 @@ export function TeamMemberFormFields({ export function AddTeamMemberDialog({ open, onClose }: { open: boolean; onClose: () => void }) { const [form, setForm] = useState(emptyForm); const [submitted, setSubmitted] = useState(false); - const createTeamMember = useCreateTeamMember(); - const mutationError = createTeamMember.error; + const submissionId = useRef(0); + const { error: mutationError, isPending, mutate, reset } = useCreateTeamMember(); useEffect(() => { + submissionId.current += 1; + reset(); if (open) { setForm(emptyForm()); setSubmitted(false); } - }, [open]); + }, [open, reset]); + + const handleClose = () => { + if (isPending) return; + + submissionId.current += 1; + reset(); + onClose(); + }; const emailIsValid = /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(form.email.trim()); const errors = useMemo( @@ -340,11 +350,18 @@ export function AddTeamMemberDialog({ open, onClose }: { open: boolean; onClose: serviceAreas: form.serviceAreas, permissionOverrides: permissionOverrides(form.role, form.permissions), }; - createTeamMember.mutate(input, { onSuccess: onClose }); + const currentSubmissionId = submissionId.current; + mutate(input, { + onSuccess: () => { + if (submissionId.current !== currentSubmissionId) return; + reset(); + onClose(); + }, + }); }; return ( - + Add Member @@ -365,15 +382,11 @@ export function AddTeamMemberDialog({ open, onClose }: { open: boolean; onClose: /> - - diff --git a/src/test/app/(protected)/team-members/add-team-member-dialog.test.tsx b/src/test/app/(protected)/team-members/add-team-member-dialog.test.tsx index c88dfb56..8bd47668 100644 --- a/src/test/app/(protected)/team-members/add-team-member-dialog.test.tsx +++ b/src/test/app/(protected)/team-members/add-team-member-dialog.test.tsx @@ -11,11 +11,14 @@ vi.mock("@/domain/team-members/use-cases/use-create-team-member", () => ({ describe("AddTeamMemberDialog", () => { const mutate = vi.fn(); + const reset = vi.fn(); beforeEach(() => { mutate.mockReset(); + reset.mockReset(); vi.mocked(useCreateTeamMember).mockReturnValue({ mutate, + reset, isPending: false, error: null, } as unknown as ReturnType); @@ -62,4 +65,40 @@ describe("AddTeamMemberDialog", () => { expect.any(Object), ); }, 15000); + + it("does not close while a member is being created", async () => { + const user = userEvent.setup(); + const onClose = vi.fn(); + vi.mocked(useCreateTeamMember).mockReturnValue({ + mutate, + reset, + isPending: true, + error: null, + } as unknown as ReturnType); + renderWithProviders(, { withAuth: false }); + + await user.keyboard("{Escape}"); + + expect(onClose).not.toHaveBeenCalled(); + }); + + it("clears a previous mutation error when reopened", () => { + const mutation = { + mutate, + reset, + isPending: false, + error: new Error("Email is already in use."), + } as unknown as ReturnType; + vi.mocked(useCreateTeamMember).mockReturnValue(mutation); + const { rerender } = renderWithProviders(, { + withAuth: false, + }); + + expect(screen.getByText("Email is already in use.")).toBeInTheDocument(); + mutation.error = null; + rerender(); + rerender(); + + expect(screen.queryByText("Email is already in use.")).not.toBeInTheDocument(); + }); });