mirror of
https://github.com/Hestia-Homes/assessment-model.git
synced 2026-06-08 11:37:25 +00:00
Drop the unused invitee name field from the invite flow
The Full name input on the user-access card was never persisted (no column on portfolio_invitations), never used in the invitation email (only the inviter's name appears there), and never displayed in the pending-invitations table — purely dead weight on the form. Removing the input, the request-body field, and the response-payload reference. Extracts the POST body schema to a named inviteRequestSchema export so the contract change is locked in by a unit test rather than left implicit. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
3b63c5ea1a
commit
618a92a06d
3 changed files with 60 additions and 29 deletions
|
|
@ -0,0 +1,46 @@
|
|||
import { describe, expect, it } from "vitest";
|
||||
|
||||
import { inviteRequestSchema } from "./route";
|
||||
|
||||
describe("inviteRequestSchema", () => {
|
||||
it("accepts an invite request with just email and role (no name)", () => {
|
||||
const result = inviteRequestSchema.parse({
|
||||
email: "alice@example.com",
|
||||
role: "read",
|
||||
});
|
||||
|
||||
expect(result).toEqual({
|
||||
email: "alice@example.com",
|
||||
role: "read",
|
||||
});
|
||||
});
|
||||
|
||||
it("silently drops an unknown name field (in-flight clients still parse)", () => {
|
||||
const result = inviteRequestSchema.parse({
|
||||
email: "alice@example.com",
|
||||
role: "write",
|
||||
name: "Alice",
|
||||
});
|
||||
|
||||
expect(result).toEqual({
|
||||
email: "alice@example.com",
|
||||
role: "write",
|
||||
});
|
||||
expect(result).not.toHaveProperty("name");
|
||||
});
|
||||
|
||||
it("rejects an invite request with a malformed email", () => {
|
||||
expect(() =>
|
||||
inviteRequestSchema.parse({ email: "not-an-email", role: "read" }),
|
||||
).toThrow();
|
||||
});
|
||||
|
||||
it("rejects an invite request with a role outside the enum", () => {
|
||||
expect(() =>
|
||||
inviteRequestSchema.parse({
|
||||
email: "alice@example.com",
|
||||
role: "superuser",
|
||||
}),
|
||||
).toThrow();
|
||||
});
|
||||
});
|
||||
|
|
@ -192,6 +192,13 @@ export async function PUT(
|
|||
}
|
||||
}
|
||||
|
||||
// Body schema for the invite endpoint. Exported so the contract can be
|
||||
// unit-tested directly — see route.test.ts.
|
||||
export const inviteRequestSchema = z.object({
|
||||
email: z.string().email(),
|
||||
role: z.enum(ROLE_OPTIONS),
|
||||
});
|
||||
|
||||
// POST: invite a user by email.
|
||||
//
|
||||
// Unified flow: in nearly every case we write a pending portfolio_invitations
|
||||
|
|
@ -204,15 +211,9 @@ export async function POST(
|
|||
) {
|
||||
const { portfolioId } = await props.params;
|
||||
|
||||
const bodySchema = z.object({
|
||||
email: z.string().email(),
|
||||
role: z.enum(ROLE_OPTIONS),
|
||||
name: z.string(),
|
||||
});
|
||||
|
||||
let body: z.infer<typeof bodySchema>;
|
||||
let body: z.infer<typeof inviteRequestSchema>;
|
||||
try {
|
||||
body = bodySchema.parse(await req.json());
|
||||
body = inviteRequestSchema.parse(await req.json());
|
||||
} catch {
|
||||
return NextResponse.json({ error: "Invalid body" }, { status: 400 });
|
||||
}
|
||||
|
|
@ -283,7 +284,7 @@ export async function POST(
|
|||
portfolioUserId: existingMembership.id.toString(),
|
||||
userId: existingUser.id.toString(),
|
||||
role: body.role,
|
||||
name: existingUser.firstName ?? body.name ?? null,
|
||||
name: existingUser.firstName ?? null,
|
||||
email,
|
||||
kind: "member" as const,
|
||||
},
|
||||
|
|
@ -337,7 +338,7 @@ export async function POST(
|
|||
userId: null,
|
||||
invitationId: invitation.id.toString(),
|
||||
role: invitation.role,
|
||||
name: body.name ?? null,
|
||||
name: null,
|
||||
email,
|
||||
kind: "invitation" as const,
|
||||
},
|
||||
|
|
|
|||
|
|
@ -77,12 +77,11 @@ async function invitePortfolioUser(
|
|||
portfolioId: string,
|
||||
email: string,
|
||||
role: Role,
|
||||
name: string,
|
||||
): Promise<void> {
|
||||
const res = await fetch(`/api/portfolio/${portfolioId}/collaborators`, {
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify({ email, role, name }),
|
||||
body: JSON.stringify({ email, role }),
|
||||
});
|
||||
if (!res.ok) {
|
||||
const msg = await res.text().catch(() => "");
|
||||
|
|
@ -123,7 +122,6 @@ async function revokePortfolioInvitation(
|
|||
export function UsersPermissionsCard({ portfolioId }: { portfolioId: string }) {
|
||||
const [inviteEmail, setInviteEmail] = useState("");
|
||||
const [inviteRole, setInviteRole] = useState<Role>("read");
|
||||
const [inviteName, setInviteName] = useState("");
|
||||
const [pendingRemoval, setPendingRemoval] =
|
||||
useState<{ portfolioUserId: string; email: string } | null>(null);
|
||||
const [pendingRevoke, setPendingRevoke] =
|
||||
|
|
@ -210,16 +208,13 @@ export function UsersPermissionsCard({ portfolioId }: { portfolioId: string }) {
|
|||
mutationFn: ({
|
||||
email,
|
||||
role,
|
||||
name,
|
||||
}: {
|
||||
email: string;
|
||||
role: Role;
|
||||
name: string;
|
||||
}) => invitePortfolioUser(portfolioId, email, role, name),
|
||||
}) => invitePortfolioUser(portfolioId, email, role),
|
||||
onSuccess: (_data, vars) => {
|
||||
invalidateBoth();
|
||||
setInviteEmail("");
|
||||
setInviteName("");
|
||||
toast({
|
||||
title: "Invitation sent",
|
||||
description: `We've emailed ${vars.email} an invitation to this portfolio.`,
|
||||
|
|
@ -324,7 +319,6 @@ export function UsersPermissionsCard({ portfolioId }: { portfolioId: string }) {
|
|||
inviteUserMutation.mutate({
|
||||
email: inviteEmail,
|
||||
role: inviteRole,
|
||||
name: inviteName,
|
||||
});
|
||||
}
|
||||
|
||||
|
|
@ -380,12 +374,6 @@ export function UsersPermissionsCard({ portfolioId }: { portfolioId: string }) {
|
|||
</p>
|
||||
</TableHead>
|
||||
<TableCell className="flex gap-2 items-center">
|
||||
<Input
|
||||
type="text"
|
||||
placeholder="Full name"
|
||||
value={inviteName}
|
||||
onChange={(e) => setInviteName(e.target.value)}
|
||||
/>
|
||||
<Input
|
||||
type="email"
|
||||
placeholder="email@example.com"
|
||||
|
|
@ -404,11 +392,7 @@ export function UsersPermissionsCard({ portfolioId }: { portfolioId: string }) {
|
|||
<Button
|
||||
className="w-28"
|
||||
onClick={handleInvite}
|
||||
disabled={
|
||||
!inviteEmail ||
|
||||
!inviteName ||
|
||||
inviteUserMutation.isPending
|
||||
}
|
||||
disabled={!inviteEmail || inviteUserMutation.isPending}
|
||||
>
|
||||
{inviteUserMutation.isPending ? "Inviting..." : "Invite"}
|
||||
</Button>
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue