From a4321369043feb82f15dc594979fde769c943a12 Mon Sep 17 00:00:00 2001 From: Marius van Niekerk Date: Fri, 19 Jun 2026 15:55:38 -0400 Subject: [PATCH 01/10] fix: show profile images in user pickers Reviewer and assignee editors were falling back to initials even though the surrounding detail view already knows the provider host used for synced profile images. Reusing that host keeps the picker aligned with repo summary avatars while preserving initials for manual or unknown users. Validation: cd frontend && node ../node_modules/vite-plus/bin/vp test run --project unit src/lib/components/detail/UserPicker.test.ts src/lib/components/detail/UserListEditor.test.ts; node node_modules/vite-plus/bin/vp run frontend-package-check; node node_modules/vite-plus/bin/vp fmt --check frontend packages/ui '!frontend/dist/**' '!packages/ui/src/api/generated/**' '!frontend/test-results/**' '!packages/ui/test-results/**' --no-error-on-unmatched-pattern --threads=1 Generated with Codex Co-authored-by: Codex --- .../lib/components/detail/UserPicker.test.ts | 20 +++++++++++++++++++ .../src/components/detail/IssueDetail.svelte | 11 ++++++++++ .../src/components/detail/PullDetail.svelte | 12 +++++++++++ .../components/detail/UserListEditor.svelte | 3 +++ .../src/components/detail/UserPicker.svelte | 10 +++++++++- 5 files changed, 55 insertions(+), 1 deletion(-) diff --git a/frontend/src/lib/components/detail/UserPicker.test.ts b/frontend/src/lib/components/detail/UserPicker.test.ts index e11cc7e45..8248d9540 100644 --- a/frontend/src/lib/components/detail/UserPicker.test.ts +++ b/frontend/src/lib/components/detail/UserPicker.test.ts @@ -55,6 +55,26 @@ describe("UserPicker", () => { expect(screen.getAllByRole("menuitemcheckbox")).toHaveLength(2); }); + it("renders synced profile images when an avatar URL is available", () => { + render(UserPicker, { + props: { + title: "Edit reviewers", + candidates: ["alice", "manual-user"], + selected: [], + avatarUrlForUser: (username) => (username === "alice" ? "https://github.example/alice.png?size=40" : ""), + ontoggle: vi.fn(), + onclose: vi.fn(), + }, + }); + + const aliceRow = screen.getByRole("menuitemcheckbox", { name: /alice/i }); + const aliceAvatar = aliceRow.querySelector("img.user-picker__avatar"); + expect(aliceAvatar?.getAttribute("src")).toBe("https://github.example/alice.png?size=40"); + expect(aliceAvatar?.getAttribute("alt")).toBe(""); + + expect(screen.getByRole("menuitemcheckbox", { name: /manual-user/i }).textContent).toContain("M"); + }); + it("notifies query changes so callers can fetch matching candidates", async () => { const onQuery = vi.fn(); render(UserPicker, { diff --git a/packages/ui/src/components/detail/IssueDetail.svelte b/packages/ui/src/components/detail/IssueDetail.svelte index 8df7866bd..303ab9f9e 100644 --- a/packages/ui/src/components/detail/IssueDetail.svelte +++ b/packages/ui/src/components/detail/IssueDetail.svelte @@ -350,6 +350,16 @@ return data?.users ?? []; } + function userAvatarURL(username: string): string { + const login = encodeURIComponent(username.trim()); + const host = issues.getIssueDetail()?.repo?.platform_host + ?? issues.getIssueDetail()?.platform_host + ?? platformHost + ?? ""; + if (login === "" || host === "") return ""; + return `https://${host}/${login}.png?size=40`; + } + function onDocumentMousedown(e: MouseEvent): void { if (!labelPickerOpen) return; const target = e.target as Node; @@ -904,6 +914,7 @@ disabled={staleIssue || assigneeGate.unavailable} disabledReason={assigneeGate.unavailable ? assigneeGate.reason : undefined} loadCandidates={loadUserCandidates} + avatarUrlForUser={userAvatarURL} onchange={(next) => issues.setIssueAssignees(owner, name, number, next)} > {#snippet icon()} diff --git a/packages/ui/src/components/detail/PullDetail.svelte b/packages/ui/src/components/detail/PullDetail.svelte index d2f624c4d..9ff0b9462 100644 --- a/packages/ui/src/components/detail/PullDetail.svelte +++ b/packages/ui/src/components/detail/PullDetail.svelte @@ -1047,6 +1047,16 @@ return data?.users ?? []; } + function userAvatarURL(username: string): string { + const login = encodeURIComponent(username.trim()); + const host = detailStore.getDetail()?.repo?.platform_host + ?? detailStore.getDetail()?.platform_host + ?? platformHost + ?? ""; + if (login === "" || host === "") return ""; + return `https://${host}/${login}.png?size=40`; + } + function onActionMenuKeydown(e: KeyboardEvent): void { if (actionMenuOpen && e.key === "Escape") { actionMenuOpen = false; @@ -1633,6 +1643,7 @@ disabled={stalePR || assigneeGate.unavailable} disabledReason={assigneeGate.unavailable ? assigneeGate.reason : undefined} loadCandidates={loadUserCandidates} + avatarUrlForUser={userAvatarURL} onchange={(next) => detailStore.setPullAssignees(owner, name, number, next)} > {#snippet icon()} @@ -1647,6 +1658,7 @@ disabledReason={reviewerGate.unavailable ? reviewerGate.reason : undefined} tooltipNote="User review requests only; team requests are not shown" loadCandidates={loadUserCandidates} + avatarUrlForUser={userAvatarURL} onchange={(next) => detailStore.setPullReviewers(owner, name, number, next)} > {#snippet icon()} diff --git a/packages/ui/src/components/detail/UserListEditor.svelte b/packages/ui/src/components/detail/UserListEditor.svelte index 1b014300d..1b4459a09 100644 --- a/packages/ui/src/components/detail/UserListEditor.svelte +++ b/packages/ui/src/components/detail/UserListEditor.svelte @@ -29,6 +29,7 @@ /// with "" when the picker opens and again as the user types, so /// candidates beyond the first page stay reachable by searching. loadCandidates: (query: string) => Promise; + avatarUrlForUser?: ((username: string) => string) | undefined; onchange: (next: string[]) => Promise; icon?: Snippet; } @@ -41,6 +42,7 @@ disabledReason = undefined, tooltipNote = undefined, loadCandidates, + avatarUrlForUser = undefined, onchange, icon = undefined, }: Props = $props(); @@ -267,6 +269,7 @@ {pendingUser} error={mutationError ?? candidatesError} {autofocusFilter} + {avatarUrlForUser} onquery={onPickerQuery} ontoggle={toggleUser} onclear={clearUsers} diff --git a/packages/ui/src/components/detail/UserPicker.svelte b/packages/ui/src/components/detail/UserPicker.svelte index 387cf38af..bb7babba6 100644 --- a/packages/ui/src/components/detail/UserPicker.svelte +++ b/packages/ui/src/components/detail/UserPicker.svelte @@ -12,6 +12,7 @@ pendingUser?: string | null; error?: string | null; autofocusFilter?: boolean; + avatarUrlForUser?: ((username: string) => string) | undefined; /// The query the current candidates were fetched for. When set, /// the exact-username entry row is withheld until the candidate /// list reflects the typed query, so a stale list cannot offer a @@ -33,6 +34,7 @@ pendingUser = null, error = null, autofocusFilter = false, + avatarUrlForUser = undefined, candidatesQuery = undefined, onquery = undefined, ontoggle, @@ -132,6 +134,7 @@