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,7 +1,7 @@
|
|||
"use client";
|
||||
|
||||
import { useState } from "react";
|
||||
import { X, Trash2, Bot, Lock, UserMinus } from "lucide-react";
|
||||
import { X, Trash2 } from "lucide-react";
|
||||
import { toast } from "sonner";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import {
|
||||
|
|
@ -19,15 +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, useActorName } from "@/features/workspace";
|
||||
import { useIssueStore } from "@/features/issues/store";
|
||||
import { useIssueSelectionStore } from "@/features/issues/stores/selection-store";
|
||||
import { api } from "@/shared/api";
|
||||
import { StatusIcon } from "./status-icon";
|
||||
import { PriorityIcon } from "./priority-icon";
|
||||
import { AssigneePicker } from "./pickers";
|
||||
|
||||
export function BatchActionToolbar() {
|
||||
const selectedIds = useIssueSelectionStore((s) => s.selectedIds);
|
||||
|
|
@ -44,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);
|
||||
|
|
@ -161,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 */}
|
||||
|
|
@ -207,136 +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 { getActorInitials } = useActorName();
|
||||
|
||||
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"
|
||||
>
|
||||
<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>
|
||||
<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"}`}
|
||||
>
|
||||
<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>
|
||||
<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>
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
export { StatusIcon } from "./status-icon";
|
||||
export { PriorityIcon } from "./priority-icon";
|
||||
export { StatusPicker, PriorityPicker, AssigneePicker, DueDatePicker } from "./pickers";
|
||||
export { StatusPicker, PriorityPicker, AssigneePicker, canAssignAgent, DueDatePicker } from "./pickers";
|
||||
export { IssueDetail } from "./issue-detail";
|
||||
export { IssuesPage } from "./issues-page";
|
||||
export { CommentCard } from "./comment-card";
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
@ -58,7 +57,7 @@ import { Avatar, AvatarFallback, AvatarGroup, AvatarGroupCount } from "@/compone
|
|||
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, canAssignAgent } from "@/features/issues/components";
|
||||
import { CommentCard } from "./comment-card";
|
||||
import { CommentInput } from "./comment-input";
|
||||
import { AgentLiveCard, TaskRunHistory } from "./agent-live-card";
|
||||
|
|
@ -174,6 +173,7 @@ 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 currentMemberRole = members.find((m) => m.user_id === user?.id)?.role;
|
||||
|
||||
// Issue navigation
|
||||
const allIssues = useIssueStore((s) => s.issues);
|
||||
|
|
@ -421,21 +421,17 @@ 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>
|
||||
))}
|
||||
{agents.map((a) => (
|
||||
{agents.filter((a) => canAssignAgent(a, user?.id, currentMemberRole)).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}
|
||||
{issue.assignee_type === "agent" && issue.assignee_id === a.id && <span className="ml-auto text-xs text-muted-foreground">✓</span>}
|
||||
</DropdownMenuItem>
|
||||
|
|
@ -873,60 +869,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 })}>
|
||||
<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>
|
||||
{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 })}>
|
||||
<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>
|
||||
{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,8 +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) =>
|
||||
m.name.toLowerCase().includes(query),
|
||||
|
|
@ -208,9 +206,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 +235,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">
|
||||
|
|
|
|||
|
|
@ -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