feat(team-members): warn when the invite to a corrected email could not be sent

This commit is contained in:
Alexandre Brandizzi 2026-09-25 12:54:31 -03:00
parent ca42d6e59f
commit 383d5c3c6c
5 changed files with 97 additions and 1 deletions

View file

@ -88,9 +88,12 @@ export function mapTeamMemberListItems(rawItems: unknown[]): TeamMemberListItem[
export function mapTeamMemberDetail(raw: unknown): TeamMemberDetail {
const record = (typeof raw === "object" && raw !== null ? raw : {}) as Record<string, unknown>;
const listItem = mapTeamMemberListItem(record);
// Present only on an update that re-sent a pending member's invite.
const inviteEmailSent = record.inviteEmailSent ?? record.InviteEmailSent;
return {
...listItem,
serviceAreas: readStringArray(record, "serviceAreas", "ServiceAreas"),
permissions: readPermissions(record),
...(typeof inviteEmailSent === "boolean" && { inviteEmailSent }),
};
}

View file

@ -47,6 +47,8 @@ export interface TeamPermissionValue {
export interface TeamMemberDetail extends TeamMemberListItem {
serviceAreas: string[];
permissions: TeamPermissionValue[];
/** Set only after an update that changed a pending member's email and re-sent their invite. */
inviteEmailSent?: boolean;
}
export interface UpdateTeamMemberInput {

View file

@ -23,6 +23,15 @@ export function useTeamMemberDetail(
});
}
export const UPDATED_INVITE_EMAIL_FAILED_MESSAGE =
"Team member updated, but the invite to their new email couldn't be sent. Use Resend invite.";
function updatedMessage(member: TeamMemberDetail): string {
return member.inviteEmailSent === true
? `Team member updated. A new invite was sent to ${member.email}.`
: "Team member updated.";
}
interface UpdateVariables {
id: string | number;
input: UpdateTeamMemberInput;
@ -36,7 +45,11 @@ export function useUpdateTeamMember(): UseMutationResult<TeamMemberDetail, Error
onSuccess: (member) => {
void queryClient.invalidateQueries({ queryKey: queryKeys.teamMembers.all });
void queryClient.setQueryData(queryKeys.teamMembers.detail(member.id), member);
toast.success("Team member updated.");
if (member.inviteEmailSent === false) {
toast.warning(UPDATED_INVITE_EMAIL_FAILED_MESSAGE);
return;
}
toast.success(updatedMessage(member));
},
onError: (error: Error) => {
toast.error(error.message || "Failed to update team member");

View file

@ -54,4 +54,10 @@ describe("mapTeamMemberDetail", () => {
],
});
});
it("keeps the invite email result only when the update reported one", () => {
expect(mapTeamMemberDetail({ id: "u1", inviteEmailSent: false }).inviteEmailSent).toBe(false);
expect(mapTeamMemberDetail({ Id: "u1", InviteEmailSent: true }).inviteEmailSent).toBe(true);
expect(mapTeamMemberDetail({ id: "u1" })).not.toHaveProperty("inviteEmailSent");
});
});

View file

@ -0,0 +1,72 @@
import type { ReactNode } from "react";
import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
import { act, renderHook, waitFor } from "@testing-library/react";
import { toast } from "react-toastify";
import { beforeEach, describe, expect, it, vi } from "vitest";
import { teamMembersApi } from "@/domain/team-members/api/team-members-api";
import type {
TeamMemberDetail,
UpdateTeamMemberInput,
} from "@/domain/team-members/types/team-member";
import {
UPDATED_INVITE_EMAIL_FAILED_MESSAGE,
useUpdateTeamMember,
} from "@/domain/team-members/use-cases/use-team-member-detail";
vi.mock("react-toastify", () => ({
toast: { success: vi.fn(), warning: vi.fn(), error: vi.fn() },
}));
vi.mock("@/domain/team-members/api/team-members-api", () => ({
teamMembersApi: { update: vi.fn() },
}));
const UPDATED = {
id: "user-7",
name: "Taylor Reed",
email: "taylor.reed@example.com",
pendingRegistration: true,
serviceAreas: ["East"],
permissions: [],
} as unknown as TeamMemberDetail;
function wrapper({ children }: { children: ReactNode }) {
const client = new QueryClient({ defaultOptions: { mutations: { retry: false } } });
return <QueryClientProvider client={client}>{children}</QueryClientProvider>;
}
async function update(updated: TeamMemberDetail) {
vi.mocked(teamMembersApi.update).mockResolvedValue(updated);
const { result } = renderHook(() => useUpdateTeamMember(), { wrapper });
act(() => result.current.mutate({ id: "user-7", input: {} as UpdateTeamMemberInput }));
await waitFor(() => expect(result.current.isSuccess).toBe(true));
}
describe("useUpdateTeamMember", () => {
beforeEach(() => {
vi.clearAllMocks();
});
it("confirms an ordinary update", async () => {
await update(UPDATED);
expect(toast.success).toHaveBeenCalledWith("Team member updated.");
expect(toast.warning).not.toHaveBeenCalled();
});
it("tells the admin a new invite went to the corrected email", async () => {
await update({ ...UPDATED, inviteEmailSent: true });
expect(toast.success).toHaveBeenCalledWith(
"Team member updated. A new invite was sent to taylor.reed@example.com.",
);
});
it("warns the admin to re-send when the new invite could not be emailed", async () => {
await update({ ...UPDATED, inviteEmailSent: false });
expect(toast.warning).toHaveBeenCalledWith(UPDATED_INVITE_EMAIL_FAILED_MESSAGE);
expect(toast.success).not.toHaveBeenCalled();
});
});