refactor(web): unify assignee dropdowns with ActorAvatar and shared AssigneePicker
- Replace inline initials/Bot divs with ActorAvatar across all assignee UIs - Replace issue-detail sidebar DropdownMenu with shared AssigneePicker - Delete BatchAssigneePicker (~130 lines), reuse AssigneePicker in controlled mode - Add controlled mode (open/onOpenChange), align, and triggerRender props to AssigneePicker/PropertyPicker - Add canAssignAgent visibility check to issue-detail more menu - Clean up unused imports (Bot, useAuthStore, useWorkspaceStore, etc.) Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
afdefa9b65
commit
f891a5bbd7
7 changed files with 54 additions and 240 deletions
|
|
@ -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,
|
||||
|
|
@ -12,7 +13,7 @@ import {
|
|||
PickerEmpty,
|
||||
} from "./property-picker";
|
||||
|
||||
function canAssignAgent(agent: Agent, userId: string | undefined, memberRole: string | undefined): boolean {
|
||||
export 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;
|
||||
|
|
@ -24,18 +25,28 @@ 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);
|
||||
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;
|
||||
|
|
@ -64,25 +75,15 @@ export function AssigneePicker({
|
|||
if (!v) setFilter("");
|
||||
}}
|
||||
width="w-52"
|
||||
align={align}
|
||||
searchable
|
||||
searchPlaceholder="Assign to..."
|
||||
onSearchChange={setFilter}
|
||||
triggerRender={triggerRender}
|
||||
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 +118,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 +144,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" />
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
export { PropertyPicker, PickerItem, PickerSection, PickerEmpty } from "./property-picker";
|
||||
export { StatusPicker } from "./status-picker";
|
||||
export { PriorityPicker } from "./priority-picker";
|
||||
export { AssigneePicker } from "./assignee-picker";
|
||||
export { AssigneePicker, canAssignAgent } from "./assignee-picker";
|
||||
export { DueDatePicker } from "./due-date-picker";
|
||||
|
|
|
|||
|
|
@ -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