From c981115f229d77ea6f3f88a1621ce95a2aec0a21 Mon Sep 17 00:00:00 2001 From: Dotta Date: Mon, 13 Apr 2026 08:21:05 -0500 Subject: [PATCH] fix(ui): show requester identity in join approvals --- ui/src/pages/Inbox.test.tsx | 66 ++++++++++++++++++++++++++++++++++++- ui/src/pages/Inbox.tsx | 38 ++++++++++++++++++--- 2 files changed, 99 insertions(+), 5 deletions(-) diff --git a/ui/src/pages/Inbox.test.tsx b/ui/src/pages/Inbox.test.tsx index 4a3c61994e6..b270496514e 100644 --- a/ui/src/pages/Inbox.test.tsx +++ b/ui/src/pages/Inbox.test.tsx @@ -5,7 +5,13 @@ import type { ComponentProps } from "react"; import { createRoot } from "react-dom/client"; import type { Issue } from "@paperclipai/shared"; import { afterEach, beforeEach, describe, expect, it, vi } from "vitest"; -import { FailedRunInboxRow, InboxIssueMetaLeading, InboxIssueTrailingColumns } from "./Inbox"; +import type { CompanyJoinRequest } from "../api/access"; +import { + FailedRunInboxRow, + InboxIssueMetaLeading, + InboxIssueTrailingColumns, + formatJoinRequestInboxLabel, +} from "./Inbox"; vi.mock("@/lib/router", () => ({ Link: ({ children, className, ...props }: ComponentProps<"a">) => ( @@ -62,6 +68,44 @@ function createIssue(overrides: Partial = {}): Issue { }; } +function createJoinRequest( + overrides: Partial = {}, +): CompanyJoinRequest { + return { + id: "join-1", + inviteId: "invite-1", + companyId: "company-1", + requestType: "human", + status: "pending_approval", + requestIp: "127.0.0.1", + requestingUserId: "user-1", + requestEmailSnapshot: "joiner@example.com", + agentName: null, + adapterType: null, + capabilities: null, + agentDefaultsPayload: null, + claimSecretExpiresAt: null, + claimSecretConsumedAt: null, + createdAgentId: null, + approvedByUserId: null, + approvedAt: null, + rejectedByUserId: null, + rejectedAt: null, + createdAt: new Date("2026-03-11T00:00:00.000Z"), + updatedAt: new Date("2026-03-11T00:00:00.000Z"), + requesterUser: { + id: "user-1", + name: "Jordan Example", + email: "joiner@example.com", + image: null, + }, + approvedByUser: null, + rejectedByUser: null, + invite: null, + ...overrides, + }; +} + describe("FailedRunInboxRow", () => { let container: HTMLDivElement; @@ -245,3 +289,23 @@ describe("InboxIssueTrailingColumns", () => { }); }); }); + +describe("formatJoinRequestInboxLabel", () => { + it("shows the human requester's name and email when available", () => { + expect(formatJoinRequestInboxLabel(createJoinRequest())).toBe( + "Jordan Example (joiner@example.com)", + ); + }); + + it("falls back to the email snapshot when the requester profile is missing", () => { + expect( + formatJoinRequestInboxLabel( + createJoinRequest({ + requesterUser: null, + requestEmailSnapshot: "snapshot@example.com", + requestingUserId: null, + }), + ), + ).toBe("snapshot@example.com"); + }); +}); diff --git a/ui/src/pages/Inbox.tsx b/ui/src/pages/Inbox.tsx index 8a7db66ee5a..9aad965812c 100644 --- a/ui/src/pages/Inbox.tsx +++ b/ui/src/pages/Inbox.tsx @@ -197,6 +197,39 @@ function readIssueIdFromRun(run: HeartbeatRun): string | null { return null; } +function nonEmptyLabel(value: string | null | undefined): string | null { + const trimmed = value?.trim(); + return trimmed ? trimmed : null; +} + +export function formatJoinRequestInboxLabel( + joinRequest: Pick< + JoinRequest, + "requestType" | "agentName" | "requestEmailSnapshot" | "requestingUserId" + > & { + requesterUser?: { + name: string | null; + email: string | null; + } | null; + }, +) { + if (joinRequest.requestType !== "human") { + return `Agent join request${joinRequest.agentName ? `: ${joinRequest.agentName}` : ""}`; + } + + const requesterName = nonEmptyLabel(joinRequest.requesterUser?.name); + const requesterEmail = + nonEmptyLabel(joinRequest.requesterUser?.email) ?? + nonEmptyLabel(joinRequest.requestEmailSnapshot); + const requesterId = nonEmptyLabel(joinRequest.requestingUserId); + + if (requesterName && requesterEmail) return `${requesterName} (${requesterEmail})`; + if (requesterEmail) return requesterEmail; + if (requesterName) return requesterName; + if (requesterId) return requesterId; + return "Human join request"; +} + type NonIssueUnreadState = "visible" | "fading" | "hidden" | null; @@ -523,10 +556,7 @@ function JoinRequestInboxRow({ selected?: boolean; className?: string; }) { - const label = - joinRequest.requestType === "human" - ? "Human join request" - : `Agent join request${joinRequest.agentName ? `: ${joinRequest.agentName}` : ""}`; + const label = formatJoinRequestInboxLabel(joinRequest); const showUnreadSlot = unreadState !== null; const showUnreadDot = unreadState === "visible" || unreadState === "fading";