Merge pull request #245 from multica-ai/forrestchang/fix-dropdown-avatars

fix(ui): show avatar images in assignee dropdowns
This commit is contained in:
Jiayuan Zhang 2026-04-01 04:29:27 +08:00 committed by GitHub
commit 82e45fb0a2
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
4 changed files with 21 additions and 60 deletions

View file

@ -1,7 +1,7 @@
"use client";
import { useState } from "react";
import { X, Trash2, Bot, Lock, UserMinus } from "lucide-react";
import { X, Trash2, Lock, UserMinus } from "lucide-react";
import { toast } from "sonner";
import { Button } from "@/components/ui/button";
import {
@ -22,10 +22,11 @@ import {
import type { Agent, UpdateIssueRequest } from "@/shared/types";
import { ALL_STATUSES, STATUS_CONFIG, PRIORITY_ORDER, PRIORITY_CONFIG } from "@/features/issues/config";
import { useAuthStore } from "@/features/auth";
import { useWorkspaceStore, useActorName } from "@/features/workspace";
import { useWorkspaceStore } from "@/features/workspace";
import { useIssueStore } from "@/features/issues/store";
import { useIssueSelectionStore } from "@/features/issues/stores/selection-store";
import { api } from "@/shared/api";
import { ActorAvatar } from "@/components/common/actor-avatar";
import { StatusIcon } from "./status-icon";
import { PriorityIcon } from "./priority-icon";
@ -229,8 +230,6 @@ function BatchAssigneePicker({
const user = useAuthStore((s) => s.user);
const members = useWorkspaceStore((s) => s.members);
const agents = useWorkspaceStore((s) => s.agents);
const { getActorInitials } = useActorName();
const currentMember = members.find((m) => m.user_id === user?.id);
const memberRole = currentMember?.role;
@ -295,9 +294,7 @@ function BatchAssigneePicker({
}}
className="flex w-full items-center gap-2 rounded-md px-2 py-1.5 text-sm hover:bg-accent transition-colors"
>
<div className="inline-flex size-4.5 shrink-0 items-center justify-center rounded-full bg-muted text-[8px] font-medium text-muted-foreground">
{getActorInitials("member", m.user_id)}
</div>
<ActorAvatar actorType="member" actorId={m.user_id} size={18} />
<span>{m.name}</span>
</button>
))}
@ -323,9 +320,7 @@ function BatchAssigneePicker({
}}
className={`flex w-full items-center gap-2 rounded-md px-2 py-1.5 text-sm transition-colors ${allowed ? "hover:bg-accent" : "opacity-50 cursor-not-allowed"}`}
>
<div className={`inline-flex size-4.5 shrink-0 items-center justify-center rounded-full ${allowed ? "bg-info/10 text-info" : "bg-muted text-muted-foreground"}`}>
<Bot className="size-2.5" />
</div>
<ActorAvatar actorType="agent" actorId={a.id} size={18} />
<span className={allowed ? "" : "text-muted-foreground"}>{a.name}</span>
{a.visibility === "private" && (
<Lock className="ml-auto h-3 w-3 text-muted-foreground" />

View file

@ -5,7 +5,6 @@ import { useDefaultLayout, usePanelRef } from "react-resizable-panels";
import Link from "next/link";
import { useRouter } from "next/navigation";
import {
Bot,
Calendar,
Check,
ChevronLeft,
@ -421,9 +420,7 @@ export function IssueDetail({ issueId, onDelete, defaultSidebarOpen = true, layo
key={m.user_id}
onClick={() => handleUpdateField({ assignee_type: "member", assignee_id: m.user_id })}
>
<div className="inline-flex size-4 shrink-0 items-center justify-center rounded-full bg-muted text-[8px] font-medium text-muted-foreground">
{getActorInitials("member", m.user_id)}
</div>
<ActorAvatar actorType="member" actorId={m.user_id} size={16} />
{m.name}
{issue.assignee_type === "member" && issue.assignee_id === m.user_id && <span className="ml-auto text-xs text-muted-foreground"></span>}
</DropdownMenuItem>
@ -433,9 +430,7 @@ export function IssueDetail({ issueId, onDelete, defaultSidebarOpen = true, layo
key={a.id}
onClick={() => handleUpdateField({ assignee_type: "agent", assignee_id: a.id })}
>
<div className="inline-flex size-4 shrink-0 items-center justify-center rounded-full bg-info/10 text-info">
<Bot className="size-2.5" />
</div>
<ActorAvatar actorType="agent" actorId={a.id} size={16} />
{a.name}
{issue.assignee_type === "agent" && issue.assignee_id === a.id && <span className="ml-auto text-xs text-muted-foreground"></span>}
</DropdownMenuItem>
@ -900,9 +895,7 @@ export function IssueDetail({ issueId, onDelete, defaultSidebarOpen = true, layo
<DropdownMenuLabel>Members</DropdownMenuLabel>
{members.map((m) => (
<DropdownMenuItem key={m.user_id} onClick={() => handleUpdateField({ assignee_type: "member", assignee_id: m.user_id })}>
<div className="inline-flex size-4 shrink-0 items-center justify-center rounded-full bg-muted text-[8px] font-medium text-muted-foreground">
{getActorInitials("member", m.user_id)}
</div>
<ActorAvatar actorType="member" actorId={m.user_id} size={16} />
{m.name}
</DropdownMenuItem>
))}
@ -916,9 +909,7 @@ export function IssueDetail({ issueId, onDelete, defaultSidebarOpen = true, layo
<DropdownMenuLabel>Agents</DropdownMenuLabel>
{agents.map((a) => (
<DropdownMenuItem key={a.id} onClick={() => handleUpdateField({ assignee_type: "agent", assignee_id: a.id })}>
<div className="inline-flex size-4 shrink-0 items-center justify-center rounded-full bg-info/10 text-info">
<Bot className="size-2.5" />
</div>
<ActorAvatar actorType="agent" actorId={a.id} size={16} />
{a.name}
</DropdownMenuItem>
))}

View file

@ -1,10 +1,11 @@
"use client";
import { useState } from "react";
import { Bot, Lock, UserMinus } from "lucide-react";
import { Lock, UserMinus } from "lucide-react";
import type { Agent, IssueAssigneeType, UpdateIssueRequest } from "@/shared/types";
import { useAuthStore } from "@/features/auth";
import { useWorkspaceStore, useActorName } from "@/features/workspace";
import { ActorAvatar } from "@/components/common/actor-avatar";
import {
PropertyPicker,
PickerItem,
@ -35,7 +36,7 @@ export function AssigneePicker({
const user = useAuthStore((s) => s.user);
const members = useWorkspaceStore((s) => s.members);
const agents = useWorkspaceStore((s) => s.agents);
const { getActorName, getActorInitials } = useActorName();
const { getActorName } = useActorName();
const currentMember = members.find((m) => m.user_id === user?.id);
const memberRole = currentMember?.role;
@ -70,19 +71,7 @@ export function AssigneePicker({
trigger={
customTrigger ? customTrigger : assigneeType && assigneeId ? (
<>
<div
className={`inline-flex shrink-0 items-center justify-center rounded-full font-medium text-[8px] size-4.5 ${
assigneeType === "agent"
? "bg-info/10 text-info"
: "bg-muted text-muted-foreground"
}`}
>
{assigneeType === "agent" ? (
<Bot className="size-2.5" />
) : (
getActorInitials(assigneeType, assigneeId)
)}
</div>
<ActorAvatar actorType={assigneeType} actorId={assigneeId} size={18} />
<span className="truncate">{triggerLabel}</span>
</>
) : (
@ -117,9 +106,7 @@ export function AssigneePicker({
setOpen(false);
}}
>
<div className="inline-flex size-4.5 shrink-0 items-center justify-center rounded-full bg-muted text-[8px] font-medium text-muted-foreground">
{getActorInitials("member", m.user_id)}
</div>
<ActorAvatar actorType="member" actorId={m.user_id} size={18} />
<span>{m.name}</span>
</PickerItem>
))}
@ -145,9 +132,7 @@ export function AssigneePicker({
setOpen(false);
}}
>
<div className={`inline-flex size-4.5 shrink-0 items-center justify-center rounded-full ${allowed ? "bg-info/10 text-info" : "bg-muted text-muted-foreground"}`}>
<Bot className="size-2.5" />
</div>
<ActorAvatar actorType="agent" actorId={a.id} size={18} />
<span className={allowed ? "" : "text-muted-foreground"}>{a.name}</span>
{a.visibility === "private" && (
<Lock className="ml-auto h-3 w-3 text-muted-foreground" />

View file

@ -2,7 +2,7 @@
import { useState, useRef } from "react";
import { useRouter } from "next/navigation";
import { Bot, CalendarDays, Check, ChevronRight, Maximize2, Minimize2, UserMinus, X as XIcon } from "lucide-react";
import { CalendarDays, Check, ChevronRight, Maximize2, Minimize2, UserMinus, X as XIcon } from "lucide-react";
import { cn } from "@/lib/utils";
import { toast } from "sonner";
import type { IssueStatus, IssuePriority, IssueAssigneeType } from "@/shared/types";
@ -35,6 +35,7 @@ import { useIssueDraftStore } from "@/features/issues/stores/draft-store";
import { api } from "@/shared/api";
import { useFileUpload } from "@/shared/hooks/use-file-upload";
import { FileUploadButton } from "@/components/common/file-upload-button";
import { ActorAvatar } from "@/components/common/actor-avatar";
// ---------------------------------------------------------------------------
// Pill trigger — shared rounded-full button style for toolbar
@ -69,7 +70,7 @@ export function CreateIssueModal({ onClose, data }: { onClose: () => void; data?
const workspaceName = useWorkspaceStore((s) => s.workspace?.name);
const members = useWorkspaceStore((s) => s.members);
const agents = useWorkspaceStore((s) => s.agents);
const { getActorName, getActorInitials } = useActorName();
const { getActorName } = useActorName();
const draft = useIssueDraftStore((s) => s.draft);
const setDraft = useIssueDraftStore((s) => s.setDraft);
@ -291,14 +292,7 @@ export function CreateIssueModal({ onClose, data }: { onClose: () => void; data?
<PillButton>
{assigneeType && assigneeId ? (
<>
<div
className={cn(
"inline-flex shrink-0 items-center justify-center rounded-full font-medium text-[8px] size-4",
assigneeType === "agent" ? "bg-info/10 text-info" : "bg-muted text-muted-foreground",
)}
>
{assigneeType === "agent" ? <Bot className="size-2.5" /> : getActorInitials(assigneeType, assigneeId)}
</div>
<ActorAvatar actorType={assigneeType} actorId={assigneeId} size={16} />
<span>{assigneeLabel}</span>
</>
) : (
@ -345,9 +339,7 @@ export function CreateIssueModal({ onClose, data }: { onClose: () => void; data?
}}
className="flex w-full items-center gap-2 rounded-md px-2 py-1.5 text-sm hover:bg-accent transition-colors"
>
<div className="inline-flex size-4 shrink-0 items-center justify-center rounded-full bg-muted text-[8px] font-medium text-muted-foreground">
{getActorInitials("member", m.user_id)}
</div>
<ActorAvatar actorType="member" actorId={m.user_id} size={16} />
<span>{m.name}</span>
</button>
))}
@ -368,9 +360,7 @@ export function CreateIssueModal({ onClose, data }: { onClose: () => void; data?
}}
className="flex w-full items-center gap-2 rounded-md px-2 py-1.5 text-sm hover:bg-accent transition-colors"
>
<div className="inline-flex size-4 shrink-0 items-center justify-center rounded-full bg-info/10 text-info">
<Bot className="size-2.5" />
</div>
<ActorAvatar actorType="agent" actorId={a.id} size={16} />
<span>{a.name}</span>
</button>
))}