diff --git a/doc/screenshots/app-access-avatar/README.md b/doc/screenshots/app-access-avatar/README.md new file mode 100644 index 00000000000..cce5d0be7a1 --- /dev/null +++ b/doc/screenshots/app-access-avatar/README.md @@ -0,0 +1,19 @@ +# App access user avatars + +Storybook: **Connections / Chat management / 2 · People and access** +(`connections-chat-management--access`). + +These screenshots show the production access component with synthetic fixture +accounts: a linked account with an image, a linked account without an image, +and an account waiting for confirmation. The image fixture is an existing +Cliptoon asset used as a deterministic profile image; it is not a real person's +photo. + +The avatar is centered beside both the Paperclip name and the Slack name, at +the same vertical center as Disconnect. Missing or failed images retain initials. +Truncated Paperclip names retain their full text in the native hover tooltip. + +- [Desktop, 1280 × 900](desktop.png) +- [Mobile, 390 × 844](mobile.png) + +The Access story checks image loading, initials, hover text, and vertical alignment. diff --git a/doc/screenshots/app-access-avatar/desktop.png b/doc/screenshots/app-access-avatar/desktop.png new file mode 100644 index 00000000000..9d56372f29a Binary files /dev/null and b/doc/screenshots/app-access-avatar/desktop.png differ diff --git a/doc/screenshots/app-access-avatar/mobile.png b/doc/screenshots/app-access-avatar/mobile.png new file mode 100644 index 00000000000..4b5e25c33e3 Binary files /dev/null and b/doc/screenshots/app-access-avatar/mobile.png differ diff --git a/packages/shared/src/types/chat-channels.ts b/packages/shared/src/types/chat-channels.ts index 42cdb367382..e83627fbadc 100644 --- a/packages/shared/src/types/chat-channels.ts +++ b/packages/shared/src/types/chat-channels.ts @@ -341,6 +341,7 @@ export interface ChatIdentityLink { externalDetail?: string | null; paperclipUserId?: string | null; paperclipUserLabel?: string | null; + paperclipUserImage?: string | null; status: ChatIdentityLinkStatus; expiresAt?: string | null; confirmedAt?: string | null; diff --git a/server/src/__tests__/chat-channels.integration.test.ts b/server/src/__tests__/chat-channels.integration.test.ts index d8c86374466..809ece2ffa6 100644 --- a/server/src/__tests__/chat-channels.integration.test.ts +++ b/server/src/__tests__/chat-channels.integration.test.ts @@ -6930,7 +6930,10 @@ describeEmbeddedPostgres("chat channel control-plane integration", () => { expect(saved.setup.webhookVerifiedAt).toBeNull(); expect(saved.providerAccountId).toBe("TAUTO"); expect(saved.setup.slackAccount).toEqual({ externalUserId: f.installerId, paperclipUserId: actor.userId, status: "linked", welcomeStatus: "sent", dmChannelId: "DWELCOME" }); - expect(await f.service.listPrincipals(f.endpoint.id)).toEqual([expect.objectContaining({ status: "linked", paperclipUserId: actor.userId })]); + expect(await f.service.listPrincipals(f.endpoint.id)).toEqual([expect.objectContaining({ status: "linked", paperclipUserId: actor.userId, paperclipUserImage: null })]); + await db.update(authUsers).set({ image: "https://example.test/installer.png" }).where(eq(authUsers.id, actor.userId)); + expect(await f.service.listPrincipals(f.endpoint.id)).toEqual([expect.objectContaining({ paperclipUserImage: "https://example.test/installer.png" })]); + await db.update(authUsers).set({ image: null }).where(eq(authUsers.id, actor.userId)); const [, welcome] = f.provider.mock.calls.find(([url]) => String(url).endsWith("chat.postMessage"))!; expect(new URLSearchParams(String(welcome!.body)).get("channel")).toBe(f.installerId); expect(new URLSearchParams(String(welcome!.body)).get("text")).toMatch(/^Hi, I’m .+\. Your Slack account is connected to Paperclip\.$/); diff --git a/server/src/routes/openapi.ts b/server/src/routes/openapi.ts index 6efc1b198cd..25b6c7278f0 100644 --- a/server/src/routes/openapi.ts +++ b/server/src/routes/openapi.ts @@ -910,6 +910,7 @@ const chatPrincipalLinkResponseSchema = z externalDetail: z.string(), paperclipUserId: z.string().nullable(), paperclipUserLabel: z.string().nullable(), + paperclipUserImage: z.string().nullable(), status: chatIdentityLinkStatusSchema, }) .strict(); diff --git a/server/src/services/chat-channels/identity-links.ts b/server/src/services/chat-channels/identity-links.ts index 47596405f3a..b45ee22db66 100644 --- a/server/src/services/chat-channels/identity-links.ts +++ b/server/src/services/chat-channels/identity-links.ts @@ -56,6 +56,7 @@ export function createChatIdentityLinks(db: Db, options: ChatIdentityLinkOptions id: authUsers.id, name: authUsers.name, email: authUsers.email, + image: authUsers.image, }) .from(authUsers) .where(inArray(authUsers.id, userIds)) @@ -80,6 +81,7 @@ export function createChatIdentityLinks(db: Db, options: ChatIdentityLinkOptions : principal.externalId, paperclipUserId: link?.paperclipUserId ?? null, paperclipUserLabel: user?.name ?? user?.email ?? null, + paperclipUserImage: user?.image ?? null, status: link?.status ?? "pending", lastConnectAt: connectByPrincipal.get(principal.id) ?? null, }; diff --git a/ui/src/api/chatEndpoints.ts b/ui/src/api/chatEndpoints.ts index 79e0c43b15f..d50a6756c3b 100644 --- a/ui/src/api/chatEndpoints.ts +++ b/ui/src/api/chatEndpoints.ts @@ -59,6 +59,7 @@ export interface ChatIdentityLink { externalDetail?: string | null; paperclipUserId?: string | null; paperclipUserLabel?: string | null; + paperclipUserImage?: string | null; status: "linked" | "pending" | "revoked"; } diff --git a/ui/src/pages/apps/chat/ChatEndpointDetail.tsx b/ui/src/pages/apps/chat/ChatEndpointDetail.tsx index 3544343a539..2c07fc4d894 100644 --- a/ui/src/pages/apps/chat/ChatEndpointDetail.tsx +++ b/ui/src/pages/apps/chat/ChatEndpointDetail.tsx @@ -1,8 +1,9 @@ +import { deriveInitials } from "@/components/Identity"; +import { Avatar, AvatarFallback, AvatarImage } from "@/components/ui/avatar"; import { agentAvatarUrl } from "@/lib/agent-avatar-url"; import { resolveAgentAppearance } from "@paperclipai/shared"; import { SlackTestMessageButton } from "./SlackTestMessageButton"; import { AgentAvatar } from "@/components/AgentAvatar"; -import { Identity } from "@/components/Identity"; import { SlackSetupAdvanced } from "./SlackAppDetails"; import { Dialog, DialogContent, DialogDescription, DialogHeader, DialogTitle, DialogTrigger } from "@/components/ui/dialog"; import { SlackToolsSettings, SlackSearchAccess } from "./SlackToolSettings"; @@ -782,13 +783,19 @@ function Access({ role="listitem" className="flex flex-wrap items-center gap-3 py-3" > -
- {link.status === "revoked" ? "Disconnected" : link.status === "linked" - ? link.externalLabel - : "Waiting for account confirmation"} -
} +{link.paperclipUserLabel ?? link.externalLabel}
+ {(link.status !== "linked" || link.externalLabel !== link.paperclipUserLabel) &&+ {link.status === "revoked" ? "Disconnected" : link.status === "linked" + ? link.externalLabel + : "Waiting for account confirmation"} +
} +