refactor(web): unify all avatar rendering with ActorAvatar
Replace all inline avatar implementations (initials divs, Bot icons, inline img tags) with the shared ActorAvatar component for consistency. - Extend AssigneePicker with controlled open/onOpenChange, triggerRender, and align props to support batch toolbar and other contexts - Replace BatchAssigneePicker (~130 lines) with shared AssigneePicker - Replace issue-detail sidebar inline DropdownMenu with AssigneePicker - Add canAssignAgent filtering to issue-detail more menu - Replace inline avatars in: filter panel, members-tab, agents page, mention-hover-card, subscribers AvatarGroup - Add data-slot="avatar" to ActorAvatar for AvatarGroup compatibility - Add triggerRender prop to PropertyPicker for custom trigger elements Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
39c5cf2cbe
commit
4d74091f8d
9 changed files with 65 additions and 249 deletions
|
|
@ -76,6 +76,7 @@ import { useWorkspaceStore } from "@/features/workspace";
|
|||
import { useRuntimeStore } from "@/features/runtimes";
|
||||
import { useIssueStore } from "@/features/issues";
|
||||
import { useFileUpload } from "@/shared/hooks/use-file-upload";
|
||||
import { ActorAvatar } from "@/components/common/actor-avatar";
|
||||
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
|
|
@ -99,14 +100,6 @@ const taskStatusConfig: Record<string, { label: string; icon: typeof CheckCircle
|
|||
cancelled: { label: "Cancelled", icon: XCircle, color: "text-muted-foreground" },
|
||||
};
|
||||
|
||||
function getInitials(name: string): string {
|
||||
return name
|
||||
.split(/[\s-]+/)
|
||||
.map((w) => w[0])
|
||||
.join("")
|
||||
.toUpperCase()
|
||||
.slice(0, 2);
|
||||
}
|
||||
|
||||
function generateId(): string {
|
||||
return `${Date.now()}-${Math.random().toString(36).slice(2, 9)}`;
|
||||
|
|
@ -343,13 +336,7 @@ function AgentListItem({
|
|||
isSelected ? "bg-accent" : "hover:bg-accent/50"
|
||||
}`}
|
||||
>
|
||||
<div className="flex h-8 w-8 shrink-0 items-center justify-center rounded-lg bg-muted text-xs font-semibold overflow-hidden">
|
||||
{agent.avatar_url ? (
|
||||
<img src={agent.avatar_url} alt={agent.name} className="h-full w-full object-cover" />
|
||||
) : (
|
||||
getInitials(agent.name)
|
||||
)}
|
||||
</div>
|
||||
<ActorAvatar actorType="agent" actorId={agent.id} size={32} className="rounded-lg" />
|
||||
|
||||
<div className="min-w-0 flex-1">
|
||||
<div className="flex items-center gap-2">
|
||||
|
|
@ -1231,17 +1218,7 @@ function SettingsTab({
|
|||
onClick={() => fileInputRef.current?.click()}
|
||||
disabled={uploading}
|
||||
>
|
||||
{agent.avatar_url ? (
|
||||
<img
|
||||
src={agent.avatar_url}
|
||||
alt={agent.name}
|
||||
className="h-full w-full object-cover"
|
||||
/>
|
||||
) : (
|
||||
<span className="flex h-full w-full items-center justify-center text-lg font-semibold text-muted-foreground">
|
||||
{getInitials(agent.name)}
|
||||
</span>
|
||||
)}
|
||||
<ActorAvatar actorType="agent" actorId={agent.id} size={64} className="rounded-none" />
|
||||
<div className="absolute inset-0 flex items-center justify-center bg-black/40 opacity-0 transition-opacity group-hover:opacity-100">
|
||||
{uploading ? (
|
||||
<Loader2 className="h-5 w-5 animate-spin text-white" />
|
||||
|
|
@ -1385,13 +1362,7 @@ function AgentDetail({
|
|||
<div className="flex h-full flex-col">
|
||||
{/* Header */}
|
||||
<div className="flex h-12 shrink-0 items-center gap-3 border-b px-4">
|
||||
<div className="flex h-7 w-7 shrink-0 items-center justify-center rounded-md bg-muted text-xs font-bold overflow-hidden">
|
||||
{agent.avatar_url ? (
|
||||
<img src={agent.avatar_url} alt={agent.name} className="h-full w-full object-cover" />
|
||||
) : (
|
||||
getInitials(agent.name)
|
||||
)}
|
||||
</div>
|
||||
<ActorAvatar actorType="agent" actorId={agent.id} size={28} className="rounded-md" />
|
||||
<div className="min-w-0 flex-1">
|
||||
<div className="flex items-center gap-2">
|
||||
<h2 className="text-sm font-semibold truncate">{agent.name}</h2>
|
||||
|
|
|
|||
|
|
@ -2,6 +2,7 @@
|
|||
|
||||
import { useState } from "react";
|
||||
import { Crown, Shield, User, Plus, MoreHorizontal, UserMinus, Users } from "lucide-react";
|
||||
import { ActorAvatar } from "@/components/common/actor-avatar";
|
||||
import type { MemberWithUser, MemberRole } from "@/shared/types";
|
||||
import { Input } from "@/components/ui/input";
|
||||
import { Button } from "@/components/ui/button";
|
||||
|
|
@ -70,14 +71,7 @@ function MemberRow({
|
|||
|
||||
return (
|
||||
<div className="flex items-center gap-3 px-4 py-3">
|
||||
<div className="flex h-8 w-8 shrink-0 items-center justify-center rounded-full bg-muted text-xs font-semibold">
|
||||
{member.name
|
||||
.split(" ")
|
||||
.map((w) => w[0])
|
||||
.join("")
|
||||
.toUpperCase()
|
||||
.slice(0, 2)}
|
||||
</div>
|
||||
<ActorAvatar actorType="member" actorId={member.user_id} size={32} />
|
||||
<div className="min-w-0 flex-1">
|
||||
<div className="text-sm font-medium truncate">{member.name}</div>
|
||||
<div className="text-xs text-muted-foreground truncate">{member.email}</div>
|
||||
|
|
|
|||
|
|
@ -45,6 +45,7 @@ function ActorAvatar({
|
|||
|
||||
return (
|
||||
<div
|
||||
data-slot="avatar"
|
||||
className={cn(
|
||||
"inline-flex shrink-0 items-center justify-center rounded-full font-medium overflow-hidden",
|
||||
"bg-muted text-muted-foreground",
|
||||
|
|
|
|||
|
|
@ -1,7 +1,6 @@
|
|||
"use client";
|
||||
|
||||
import type { ReactNode } from "react";
|
||||
import { Bot } from "lucide-react";
|
||||
import { HoverCard, HoverCardTrigger, HoverCardContent } from "@/components/ui/hover-card";
|
||||
import { ActorAvatar } from "@/components/common/actor-avatar";
|
||||
import { useWorkspaceStore } from "@/features/workspace";
|
||||
|
|
@ -49,9 +48,7 @@ function MentionHoverCard({ type, id, children }: MentionHoverCardProps) {
|
|||
</HoverCardTrigger>
|
||||
<HoverCardContent align="start" className="w-auto min-w-48 max-w-72">
|
||||
<div className="flex items-center gap-2.5">
|
||||
<div className="flex h-8 w-8 shrink-0 items-center justify-center rounded-full bg-muted">
|
||||
<Bot className="h-4 w-4 text-muted-foreground" />
|
||||
</div>
|
||||
<ActorAvatar actorType="agent" actorId={id} size={32} />
|
||||
<div className="min-w-0">
|
||||
<p className="text-sm font-medium truncate">{agent.name}</p>
|
||||
{agent.description && (
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
"use client";
|
||||
|
||||
import { useState } from "react";
|
||||
import { X, Trash2, Lock, UserMinus } from "lucide-react";
|
||||
import { X, Trash2 } from "lucide-react";
|
||||
import { toast } from "sonner";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import {
|
||||
|
|
@ -19,16 +19,14 @@ import {
|
|||
PopoverTrigger,
|
||||
PopoverContent,
|
||||
} from "@/components/ui/popover";
|
||||
import type { Agent, UpdateIssueRequest } from "@/shared/types";
|
||||
import type { UpdateIssueRequest } from "@/shared/types";
|
||||
import { ALL_STATUSES, STATUS_CONFIG, PRIORITY_ORDER, PRIORITY_CONFIG } from "@/features/issues/config";
|
||||
import { useAuthStore } from "@/features/auth";
|
||||
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";
|
||||
import { AssigneePicker } from "./pickers";
|
||||
|
||||
export function BatchActionToolbar() {
|
||||
const selectedIds = useIssueSelectionStore((s) => s.selectedIds);
|
||||
|
|
@ -45,7 +43,7 @@ export function BatchActionToolbar() {
|
|||
|
||||
const ids = Array.from(selectedIds);
|
||||
|
||||
const handleBatchUpdate = async (updates: UpdateIssueRequest) => {
|
||||
const handleBatchUpdate = async (updates: Partial<UpdateIssueRequest>) => {
|
||||
setLoading(true);
|
||||
try {
|
||||
await api.batchUpdateIssues(ids, updates);
|
||||
|
|
@ -162,11 +160,15 @@ export function BatchActionToolbar() {
|
|||
</Popover>
|
||||
|
||||
{/* Assignee */}
|
||||
<BatchAssigneePicker
|
||||
<AssigneePicker
|
||||
assigneeType={null}
|
||||
assigneeId={null}
|
||||
onUpdate={handleBatchUpdate}
|
||||
open={assigneeOpen}
|
||||
onOpenChange={setAssigneeOpen}
|
||||
onUpdate={handleBatchUpdate}
|
||||
loading={loading}
|
||||
triggerRender={<Button variant="ghost" size="sm" disabled={loading} />}
|
||||
trigger="Assignee"
|
||||
align="center"
|
||||
/>
|
||||
|
||||
{/* Delete */}
|
||||
|
|
@ -208,130 +210,3 @@ export function BatchActionToolbar() {
|
|||
);
|
||||
}
|
||||
|
||||
function canAssignAgent(agent: Agent, userId: string | undefined, memberRole: string | undefined): boolean {
|
||||
if (agent.visibility !== "private") return true;
|
||||
if (agent.owner_id === userId) return true;
|
||||
if (memberRole === "owner" || memberRole === "admin") return true;
|
||||
return false;
|
||||
}
|
||||
|
||||
function BatchAssigneePicker({
|
||||
open,
|
||||
onOpenChange,
|
||||
onUpdate,
|
||||
loading,
|
||||
}: {
|
||||
open: boolean;
|
||||
onOpenChange: (v: boolean) => void;
|
||||
onUpdate: (updates: UpdateIssueRequest) => void;
|
||||
loading: boolean;
|
||||
}) {
|
||||
const [filter, setFilter] = useState("");
|
||||
const user = useAuthStore((s) => s.user);
|
||||
const members = useWorkspaceStore((s) => s.members);
|
||||
const agents = useWorkspaceStore((s) => s.agents);
|
||||
const currentMember = members.find((m) => m.user_id === user?.id);
|
||||
const memberRole = currentMember?.role;
|
||||
|
||||
const query = filter.toLowerCase();
|
||||
const filteredMembers = members.filter((m) =>
|
||||
m.name.toLowerCase().includes(query),
|
||||
);
|
||||
const filteredAgents = agents.filter((a) =>
|
||||
a.name.toLowerCase().includes(query),
|
||||
);
|
||||
|
||||
return (
|
||||
<Popover
|
||||
open={open}
|
||||
onOpenChange={(v) => {
|
||||
onOpenChange(v);
|
||||
if (!v) setFilter("");
|
||||
}}
|
||||
>
|
||||
<PopoverTrigger
|
||||
render={
|
||||
<Button variant="ghost" size="sm" disabled={loading} />
|
||||
}
|
||||
>
|
||||
Assignee
|
||||
</PopoverTrigger>
|
||||
<PopoverContent align="center" className="w-52 p-0">
|
||||
<div className="px-2 py-1.5 border-b">
|
||||
<input
|
||||
type="text"
|
||||
value={filter}
|
||||
onChange={(e) => setFilter(e.target.value)}
|
||||
placeholder="Assign to..."
|
||||
className="w-full bg-transparent text-sm placeholder:text-muted-foreground outline-none"
|
||||
/>
|
||||
</div>
|
||||
<div className="p-1 max-h-60 overflow-y-auto">
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => {
|
||||
onUpdate({ assignee_type: null, assignee_id: null });
|
||||
onOpenChange(false);
|
||||
}}
|
||||
className="flex w-full items-center gap-2 rounded-md px-2 py-1.5 text-sm hover:bg-accent transition-colors"
|
||||
>
|
||||
<UserMinus className="h-3.5 w-3.5 text-muted-foreground" />
|
||||
<span className="text-muted-foreground">Unassigned</span>
|
||||
</button>
|
||||
|
||||
{filteredMembers.length > 0 && (
|
||||
<div>
|
||||
<div className="px-2 pt-2 pb-1 text-xs font-medium text-muted-foreground uppercase tracking-wider">
|
||||
Members
|
||||
</div>
|
||||
{filteredMembers.map((m) => (
|
||||
<button
|
||||
key={m.user_id}
|
||||
type="button"
|
||||
onClick={() => {
|
||||
onUpdate({ assignee_type: "member", assignee_id: m.user_id });
|
||||
onOpenChange(false);
|
||||
}}
|
||||
className="flex w-full items-center gap-2 rounded-md px-2 py-1.5 text-sm hover:bg-accent transition-colors"
|
||||
>
|
||||
<ActorAvatar actorType="member" actorId={m.user_id} size={18} />
|
||||
<span>{m.name}</span>
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{filteredAgents.length > 0 && (
|
||||
<div>
|
||||
<div className="px-2 pt-2 pb-1 text-xs font-medium text-muted-foreground uppercase tracking-wider">
|
||||
Agents
|
||||
</div>
|
||||
{filteredAgents.map((a) => {
|
||||
const allowed = canAssignAgent(a, user?.id, memberRole);
|
||||
return (
|
||||
<button
|
||||
key={a.id}
|
||||
type="button"
|
||||
disabled={!allowed}
|
||||
onClick={() => {
|
||||
if (!allowed) return;
|
||||
onUpdate({ assignee_type: "agent", assignee_id: a.id });
|
||||
onOpenChange(false);
|
||||
}}
|
||||
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"}`}
|
||||
>
|
||||
<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" />
|
||||
)}
|
||||
</button>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</PopoverContent>
|
||||
</Popover>
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -53,11 +53,11 @@ import {
|
|||
import { Popover, PopoverTrigger, PopoverContent } from "@/components/ui/popover";
|
||||
import { Checkbox } from "@/components/ui/checkbox";
|
||||
import { Command, CommandInput, CommandList, CommandEmpty, CommandGroup, CommandItem } from "@/components/ui/command";
|
||||
import { Avatar, AvatarFallback, AvatarGroup, AvatarGroupCount } from "@/components/ui/avatar";
|
||||
import { AvatarGroup, AvatarGroupCount } from "@/components/ui/avatar";
|
||||
import { ActorAvatar } from "@/components/common/actor-avatar";
|
||||
import type { UpdateIssueRequest, IssueStatus, IssuePriority, TimelineEntry } from "@/shared/types";
|
||||
import { ALL_STATUSES, STATUS_CONFIG, PRIORITY_ORDER, PRIORITY_CONFIG } from "@/features/issues/config";
|
||||
import { StatusIcon, PriorityIcon, DueDatePicker } from "@/features/issues/components";
|
||||
import { StatusIcon, PriorityIcon, DueDatePicker, AssigneePicker } from "@/features/issues/components";
|
||||
import { CommentCard } from "./comment-card";
|
||||
import { CommentInput } from "./comment-input";
|
||||
import { AgentLiveCard, TaskRunHistory } from "./agent-live-card";
|
||||
|
|
@ -173,13 +173,15 @@ export function IssueDetail({ issueId, onDelete, defaultSidebarOpen = true, layo
|
|||
const workspace = useWorkspaceStore((s) => s.workspace);
|
||||
const members = useWorkspaceStore((s) => s.members);
|
||||
const agents = useWorkspaceStore((s) => s.agents);
|
||||
const currentMember = members.find((m) => m.user_id === user?.id);
|
||||
const memberRole = currentMember?.role;
|
||||
|
||||
// Issue navigation
|
||||
const allIssues = useIssueStore((s) => s.issues);
|
||||
const currentIndex = allIssues.findIndex((i) => i.id === id);
|
||||
const prevIssue = currentIndex > 0 ? allIssues[currentIndex - 1] : null;
|
||||
const nextIssue = currentIndex < allIssues.length - 1 ? allIssues[currentIndex + 1] : null;
|
||||
const { getActorName, getActorInitials } = useActorName();
|
||||
const { getActorName } = useActorName();
|
||||
const { uploadWithToast } = useFileUpload();
|
||||
const { defaultLayout, onLayoutChanged } = useDefaultLayout({
|
||||
id: layoutId,
|
||||
|
|
@ -425,7 +427,12 @@ export function IssueDetail({ issueId, onDelete, defaultSidebarOpen = true, layo
|
|||
{issue.assignee_type === "member" && issue.assignee_id === m.user_id && <span className="ml-auto text-xs text-muted-foreground">✓</span>}
|
||||
</DropdownMenuItem>
|
||||
))}
|
||||
{agents.map((a) => (
|
||||
{agents.filter((a) => {
|
||||
if (a.visibility !== "private") return true;
|
||||
if (a.owner_id === user?.id) return true;
|
||||
if (memberRole === "owner" || memberRole === "admin") return true;
|
||||
return false;
|
||||
}).map((a) => (
|
||||
<DropdownMenuItem
|
||||
key={a.id}
|
||||
onClick={() => handleUpdateField({ assignee_type: "agent", assignee_id: a.id })}
|
||||
|
|
@ -597,9 +604,12 @@ export function IssueDetail({ issueId, onDelete, defaultSidebarOpen = true, layo
|
|||
{subscribers.length > 0 ? (
|
||||
<AvatarGroup>
|
||||
{subscribers.slice(0, 4).map((sub) => (
|
||||
<Avatar key={`${sub.user_type}-${sub.user_id}`} size="sm">
|
||||
<AvatarFallback>{getActorInitials(sub.user_type, sub.user_id)}</AvatarFallback>
|
||||
</Avatar>
|
||||
<ActorAvatar
|
||||
key={`${sub.user_type}-${sub.user_id}`}
|
||||
actorType={sub.user_type}
|
||||
actorId={sub.user_id}
|
||||
size={24}
|
||||
/>
|
||||
))}
|
||||
{subscribers.length > 4 && (
|
||||
<AvatarGroupCount>+{subscribers.length - 4}</AvatarGroupCount>
|
||||
|
|
@ -868,56 +878,12 @@ export function IssueDetail({ issueId, onDelete, defaultSidebarOpen = true, layo
|
|||
|
||||
{/* Assignee */}
|
||||
<PropRow label="Assignee">
|
||||
<DropdownMenu>
|
||||
<DropdownMenuTrigger className="flex items-center gap-1.5 cursor-pointer rounded px-1 -mx-1 hover:bg-accent/30 transition-colors overflow-hidden">
|
||||
{issue.assignee_type && issue.assignee_id ? (
|
||||
<>
|
||||
<ActorAvatar
|
||||
actorType={issue.assignee_type}
|
||||
actorId={issue.assignee_id}
|
||||
size={18}
|
||||
/>
|
||||
<span className="truncate">{getActorName(issue.assignee_type, issue.assignee_id)}</span>
|
||||
</>
|
||||
) : (
|
||||
<span className="text-muted-foreground">Unassigned</span>
|
||||
)}
|
||||
</DropdownMenuTrigger>
|
||||
<DropdownMenuContent align="start" className="w-52">
|
||||
<DropdownMenuItem onClick={() => handleUpdateField({ assignee_type: null, assignee_id: null })}>
|
||||
<UserMinus className="h-3.5 w-3.5 text-muted-foreground" />
|
||||
Unassigned
|
||||
</DropdownMenuItem>
|
||||
{members.length > 0 && (
|
||||
<>
|
||||
<DropdownMenuSeparator />
|
||||
<DropdownMenuGroup>
|
||||
<DropdownMenuLabel>Members</DropdownMenuLabel>
|
||||
{members.map((m) => (
|
||||
<DropdownMenuItem key={m.user_id} onClick={() => handleUpdateField({ assignee_type: "member", assignee_id: m.user_id })}>
|
||||
<ActorAvatar actorType="member" actorId={m.user_id} size={16} />
|
||||
{m.name}
|
||||
</DropdownMenuItem>
|
||||
))}
|
||||
</DropdownMenuGroup>
|
||||
</>
|
||||
)}
|
||||
{agents.length > 0 && (
|
||||
<>
|
||||
<DropdownMenuSeparator />
|
||||
<DropdownMenuGroup>
|
||||
<DropdownMenuLabel>Agents</DropdownMenuLabel>
|
||||
{agents.map((a) => (
|
||||
<DropdownMenuItem key={a.id} onClick={() => handleUpdateField({ assignee_type: "agent", assignee_id: a.id })}>
|
||||
<ActorAvatar actorType="agent" actorId={a.id} size={16} />
|
||||
{a.name}
|
||||
</DropdownMenuItem>
|
||||
))}
|
||||
</DropdownMenuGroup>
|
||||
</>
|
||||
)}
|
||||
</DropdownMenuContent>
|
||||
</DropdownMenu>
|
||||
<AssigneePicker
|
||||
assigneeType={issue.assignee_type}
|
||||
assigneeId={issue.assignee_id}
|
||||
onUpdate={handleUpdateField}
|
||||
align="start"
|
||||
/>
|
||||
</PropRow>
|
||||
|
||||
{/* Due date */}
|
||||
|
|
|
|||
|
|
@ -4,7 +4,6 @@ import { useMemo, useState } from "react";
|
|||
import {
|
||||
ArrowDown,
|
||||
ArrowUp,
|
||||
Bot,
|
||||
Check,
|
||||
ChevronDown,
|
||||
CircleDot,
|
||||
|
|
@ -47,7 +46,8 @@ import {
|
|||
PRIORITY_CONFIG,
|
||||
} from "@/features/issues/config";
|
||||
import { StatusIcon, PriorityIcon } from "@/features/issues/components";
|
||||
import { useWorkspaceStore, useActorName } from "@/features/workspace";
|
||||
import { useWorkspaceStore } from "@/features/workspace";
|
||||
import { ActorAvatar } from "@/components/common/actor-avatar";
|
||||
import {
|
||||
useIssueViewStore,
|
||||
SORT_OPTIONS,
|
||||
|
|
@ -147,7 +147,6 @@ function ActorSubContent({
|
|||
const [search, setSearch] = useState("");
|
||||
const members = useWorkspaceStore((s) => s.members);
|
||||
const agents = useWorkspaceStore((s) => s.agents);
|
||||
const { getActorInitials } = useActorName();
|
||||
|
||||
const query = search.toLowerCase();
|
||||
const filteredMembers = members.filter((m) =>
|
||||
|
|
@ -208,9 +207,7 @@ function ActorSubContent({
|
|||
className={FILTER_ITEM_CLASS}
|
||||
>
|
||||
<HoverCheck checked={checked} />
|
||||
<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 className="truncate">{m.name}</span>
|
||||
{count > 0 && (
|
||||
<span className="ml-auto text-xs text-muted-foreground">
|
||||
|
|
@ -239,9 +236,7 @@ function ActorSubContent({
|
|||
className={FILTER_ITEM_CLASS}
|
||||
>
|
||||
<HoverCheck checked={checked} />
|
||||
<div className="inline-flex size-4.5 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={18} />
|
||||
<span className="truncate">{a.name}</span>
|
||||
{count > 0 && (
|
||||
<span className="ml-auto text-xs text-muted-foreground">
|
||||
|
|
|
|||
|
|
@ -25,13 +25,23 @@ export function AssigneePicker({
|
|||
assigneeId,
|
||||
onUpdate,
|
||||
trigger: customTrigger,
|
||||
triggerRender,
|
||||
open: controlledOpen,
|
||||
onOpenChange: controlledOnOpenChange,
|
||||
align,
|
||||
}: {
|
||||
assigneeType: IssueAssigneeType | null;
|
||||
assigneeId: string | null;
|
||||
onUpdate: (updates: Partial<UpdateIssueRequest>) => void;
|
||||
trigger?: React.ReactNode;
|
||||
triggerRender?: React.ReactElement;
|
||||
open?: boolean;
|
||||
onOpenChange?: (v: boolean) => void;
|
||||
align?: "start" | "center" | "end";
|
||||
}) {
|
||||
const [open, setOpen] = useState(false);
|
||||
const [internalOpen, setInternalOpen] = useState(false);
|
||||
const open = controlledOpen ?? internalOpen;
|
||||
const setOpen = controlledOnOpenChange ?? setInternalOpen;
|
||||
const [filter, setFilter] = useState("");
|
||||
const user = useAuthStore((s) => s.user);
|
||||
const members = useWorkspaceStore((s) => s.members);
|
||||
|
|
@ -65,6 +75,8 @@ export function AssigneePicker({
|
|||
if (!v) setFilter("");
|
||||
}}
|
||||
width="w-52"
|
||||
align={align}
|
||||
triggerRender={triggerRender}
|
||||
searchable
|
||||
searchPlaceholder="Assign to..."
|
||||
onSearchChange={setFilter}
|
||||
|
|
|
|||
|
|
@ -16,6 +16,7 @@ export function PropertyPicker({
|
|||
open,
|
||||
onOpenChange,
|
||||
trigger,
|
||||
triggerRender,
|
||||
width = "w-48",
|
||||
align = "end",
|
||||
searchable = false,
|
||||
|
|
@ -26,6 +27,7 @@ export function PropertyPicker({
|
|||
open: boolean;
|
||||
onOpenChange: (v: boolean) => void;
|
||||
trigger: React.ReactNode;
|
||||
triggerRender?: React.ReactElement;
|
||||
width?: string;
|
||||
align?: "start" | "center" | "end";
|
||||
searchable?: boolean;
|
||||
|
|
@ -48,7 +50,10 @@ export function PropertyPicker({
|
|||
|
||||
return (
|
||||
<Popover open={open} onOpenChange={handleOpenChange}>
|
||||
<PopoverTrigger className="flex items-center gap-1.5 cursor-pointer rounded px-1 -mx-1 hover:bg-accent/30 transition-colors overflow-hidden">
|
||||
<PopoverTrigger
|
||||
className={triggerRender ? undefined : "flex items-center gap-1.5 cursor-pointer rounded px-1 -mx-1 hover:bg-accent/30 transition-colors overflow-hidden"}
|
||||
render={triggerRender}
|
||||
>
|
||||
{trigger}
|
||||
</PopoverTrigger>
|
||||
<PopoverContent align={align} className={`${width} gap-0 p-0`}>
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue