mirror of
https://github.com/multica-ai/multica.git
synced 2026-07-27 04:56:20 +02:00
refactor(views): consolidate all picker components across the codebase
Enhance shared pickers (StatusPicker, PriorityPicker, DueDatePicker, ProjectPicker) with triggerRender, controlled open/onOpenChange, and align props — matching the AssigneePicker API. Replace inline implementations in: - create-issue.tsx: Status, Priority, DueDate, Project (4 pickers) - issue-detail.tsx sidebar: Status, Priority (2 pickers) - batch-action-toolbar.tsx: Status, Priority (2 pickers) StatusPicker now has its first consumer (was defined but unused). Removes ~200 lines of duplicated picker code.
This commit is contained in:
@@ -14,18 +14,10 @@ import {
|
||||
AlertDialogHeader,
|
||||
AlertDialogTitle,
|
||||
} from "@multica/ui/components/ui/alert-dialog";
|
||||
import {
|
||||
Popover,
|
||||
PopoverTrigger,
|
||||
PopoverContent,
|
||||
} from "@multica/ui/components/ui/popover";
|
||||
import type { UpdateIssueRequest } from "@multica/core/types";
|
||||
import { ALL_STATUSES, STATUS_CONFIG, PRIORITY_ORDER, PRIORITY_CONFIG } from "@multica/core/issues/config";
|
||||
import { useIssueSelectionStore } from "@multica/core/issues/stores/selection-store";
|
||||
import { useBatchUpdateIssues, useBatchDeleteIssues } from "@multica/core/issues/mutations";
|
||||
import { StatusIcon } from "./status-icon";
|
||||
import { PriorityIcon } from "./priority-icon";
|
||||
import { AssigneePicker } from "./pickers";
|
||||
import { StatusPicker, PriorityPicker, AssigneePicker } from "./pickers";
|
||||
|
||||
export function BatchActionToolbar() {
|
||||
const selectedIds = useIssueSelectionStore((s) => s.selectedIds);
|
||||
@@ -80,68 +72,26 @@ export function BatchActionToolbar() {
|
||||
</div>
|
||||
|
||||
{/* Status */}
|
||||
<Popover open={statusOpen} onOpenChange={setStatusOpen}>
|
||||
<PopoverTrigger
|
||||
render={
|
||||
<Button variant="ghost" size="sm" disabled={loading} />
|
||||
}
|
||||
>
|
||||
<StatusIcon status="todo" className="h-3.5 w-3.5 mr-1" />
|
||||
Status
|
||||
</PopoverTrigger>
|
||||
<PopoverContent align="center" className="w-44 p-1">
|
||||
{ALL_STATUSES.map((s) => {
|
||||
const cfg = STATUS_CONFIG[s];
|
||||
return (
|
||||
<button
|
||||
key={s}
|
||||
type="button"
|
||||
onClick={() => {
|
||||
handleBatchUpdate({ status: s });
|
||||
setStatusOpen(false);
|
||||
}}
|
||||
className={`flex w-full items-center gap-2 rounded-md px-2 py-1.5 text-sm ${cfg.hoverBg} transition-colors`}
|
||||
>
|
||||
<StatusIcon status={s} className="h-3.5 w-3.5" />
|
||||
<span>{cfg.label}</span>
|
||||
</button>
|
||||
);
|
||||
})}
|
||||
</PopoverContent>
|
||||
</Popover>
|
||||
<StatusPicker
|
||||
status="todo"
|
||||
onUpdate={handleBatchUpdate}
|
||||
open={statusOpen}
|
||||
onOpenChange={setStatusOpen}
|
||||
triggerRender={<Button variant="ghost" size="sm" disabled={loading} />}
|
||||
trigger="Status"
|
||||
align="center"
|
||||
/>
|
||||
|
||||
{/* Priority */}
|
||||
<Popover open={priorityOpen} onOpenChange={setPriorityOpen}>
|
||||
<PopoverTrigger
|
||||
render={
|
||||
<Button variant="ghost" size="sm" disabled={loading} />
|
||||
}
|
||||
>
|
||||
<PriorityIcon priority="high" className="mr-1" />
|
||||
Priority
|
||||
</PopoverTrigger>
|
||||
<PopoverContent align="center" className="w-44 p-1">
|
||||
{PRIORITY_ORDER.map((p) => {
|
||||
const cfg = PRIORITY_CONFIG[p];
|
||||
return (
|
||||
<button
|
||||
key={p}
|
||||
type="button"
|
||||
onClick={() => {
|
||||
handleBatchUpdate({ priority: p });
|
||||
setPriorityOpen(false);
|
||||
}}
|
||||
className="flex w-full items-center gap-2 rounded-md px-2 py-1.5 text-sm hover:bg-accent transition-colors"
|
||||
>
|
||||
<span className={`inline-flex items-center gap-1 rounded px-1.5 py-0.5 text-xs font-medium ${cfg.badgeBg} ${cfg.badgeText}`}>
|
||||
<PriorityIcon priority={p} className="h-3 w-3" inheritColor />
|
||||
{cfg.label}
|
||||
</span>
|
||||
</button>
|
||||
);
|
||||
})}
|
||||
</PopoverContent>
|
||||
</Popover>
|
||||
<PriorityPicker
|
||||
priority="none"
|
||||
onUpdate={handleBatchUpdate}
|
||||
open={priorityOpen}
|
||||
onOpenChange={setPriorityOpen}
|
||||
triggerRender={<Button variant="ghost" size="sm" disabled={loading} />}
|
||||
trigger="Priority"
|
||||
align="center"
|
||||
/>
|
||||
|
||||
{/* Assignee */}
|
||||
<AssigneePicker
|
||||
|
||||
@@ -60,7 +60,7 @@ import { AvatarGroup, AvatarGroupCount } from "@multica/ui/components/ui/avatar"
|
||||
import { ActorAvatar } from "../../common/actor-avatar";
|
||||
import type { Issue, UpdateIssueRequest, IssueStatus, IssuePriority, TimelineEntry } from "@multica/core/types";
|
||||
import { ALL_STATUSES, STATUS_CONFIG, PRIORITY_ORDER, PRIORITY_CONFIG } from "@multica/core/issues/config";
|
||||
import { StatusIcon, PriorityIcon, DueDatePicker, AssigneePicker, canAssignAgent } from ".";
|
||||
import { StatusIcon, PriorityIcon, StatusPicker, PriorityPicker, DueDatePicker, AssigneePicker, canAssignAgent } from ".";
|
||||
import { ProjectPicker } from "../../projects/components/project-picker";
|
||||
import { CommentCard } from "./comment-card";
|
||||
import { CommentInput } from "./comment-input";
|
||||
@@ -1113,42 +1113,20 @@ export function IssueDetail({ issueId, onDelete, defaultSidebarOpen = true, layo
|
||||
{propertiesOpen && <div className="space-y-0.5 pl-2">
|
||||
{/* Status */}
|
||||
<PropRow label="Status">
|
||||
<DropdownMenu>
|
||||
<DropdownMenuTrigger className="flex items-center gap-1.5 cursor-pointer rounded px-1 -mx-1 hover:bg-accent/30 transition-colors overflow-hidden">
|
||||
<StatusIcon status={issue.status} className="h-3.5 w-3.5 shrink-0" />
|
||||
<span className="truncate">{STATUS_CONFIG[issue.status].label}</span>
|
||||
</DropdownMenuTrigger>
|
||||
<DropdownMenuContent align="start" className="w-44">
|
||||
{ALL_STATUSES.map((s) => (
|
||||
<DropdownMenuItem key={s} onClick={() => handleUpdateField({ status: s })}>
|
||||
<StatusIcon status={s} className="h-3.5 w-3.5" />
|
||||
{STATUS_CONFIG[s].label}
|
||||
{s === issue.status && <Check className="ml-auto h-3.5 w-3.5" />}
|
||||
</DropdownMenuItem>
|
||||
))}
|
||||
</DropdownMenuContent>
|
||||
</DropdownMenu>
|
||||
<StatusPicker
|
||||
status={issue.status}
|
||||
onUpdate={handleUpdateField}
|
||||
align="start"
|
||||
/>
|
||||
</PropRow>
|
||||
|
||||
{/* Priority */}
|
||||
<PropRow label="Priority">
|
||||
<DropdownMenu>
|
||||
<DropdownMenuTrigger className="flex items-center gap-1.5 cursor-pointer rounded px-1 -mx-1 hover:bg-accent/30 transition-colors overflow-hidden">
|
||||
<PriorityIcon priority={issue.priority} className="shrink-0" />
|
||||
<span className="truncate">{PRIORITY_CONFIG[issue.priority].label}</span>
|
||||
</DropdownMenuTrigger>
|
||||
<DropdownMenuContent align="start" className="w-44">
|
||||
{PRIORITY_ORDER.map((p) => (
|
||||
<DropdownMenuItem key={p} onClick={() => handleUpdateField({ priority: p })}>
|
||||
<span className={`inline-flex items-center gap-1 rounded px-1.5 py-0.5 text-xs font-medium ${PRIORITY_CONFIG[p].badgeBg} ${PRIORITY_CONFIG[p].badgeText}`}>
|
||||
<PriorityIcon priority={p} className="h-3 w-3" inheritColor />
|
||||
{PRIORITY_CONFIG[p].label}
|
||||
</span>
|
||||
{p === issue.priority && <Check className="ml-auto h-3.5 w-3.5" />}
|
||||
</DropdownMenuItem>
|
||||
))}
|
||||
</DropdownMenuContent>
|
||||
</DropdownMenu>
|
||||
<PriorityPicker
|
||||
priority={issue.priority}
|
||||
onUpdate={handleUpdateField}
|
||||
align="start"
|
||||
/>
|
||||
</PropRow>
|
||||
|
||||
{/* Assignee */}
|
||||
|
||||
@@ -15,10 +15,14 @@ export function DueDatePicker({
|
||||
dueDate,
|
||||
onUpdate,
|
||||
trigger: customTrigger,
|
||||
triggerRender,
|
||||
align = "start",
|
||||
}: {
|
||||
dueDate: string | null;
|
||||
onUpdate: (updates: Partial<UpdateIssueRequest>) => void;
|
||||
trigger?: React.ReactNode;
|
||||
triggerRender?: React.ReactElement;
|
||||
align?: "start" | "center" | "end";
|
||||
}) {
|
||||
const [open, setOpen] = useState(false);
|
||||
const date = dueDate ? new Date(dueDate) : undefined;
|
||||
@@ -26,7 +30,10 @@ export function DueDatePicker({
|
||||
|
||||
return (
|
||||
<Popover open={open} onOpenChange={setOpen}>
|
||||
<PopoverTrigger className="flex items-center gap-1.5 cursor-pointer rounded px-1 -mx-1 hover:bg-accent/30 transition-colors">
|
||||
<PopoverTrigger
|
||||
className={triggerRender ? undefined : "flex items-center gap-1.5 cursor-pointer rounded px-1 -mx-1 hover:bg-accent/30 transition-colors"}
|
||||
render={triggerRender}
|
||||
>
|
||||
{customTrigger ?? (
|
||||
<>
|
||||
<CalendarDays className="h-3.5 w-3.5 text-muted-foreground" />
|
||||
@@ -40,7 +47,7 @@ export function DueDatePicker({
|
||||
</>
|
||||
)}
|
||||
</PopoverTrigger>
|
||||
<PopoverContent className="w-auto p-0" align="start">
|
||||
<PopoverContent className="w-auto p-0" align={align}>
|
||||
<Calendar
|
||||
mode="single"
|
||||
selected={date}
|
||||
|
||||
@@ -10,12 +10,22 @@ export function PriorityPicker({
|
||||
priority,
|
||||
onUpdate,
|
||||
trigger: customTrigger,
|
||||
triggerRender,
|
||||
open: controlledOpen,
|
||||
onOpenChange: controlledOnOpenChange,
|
||||
align,
|
||||
}: {
|
||||
priority: IssuePriority;
|
||||
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 cfg = PRIORITY_CONFIG[priority];
|
||||
|
||||
return (
|
||||
@@ -23,6 +33,8 @@ export function PriorityPicker({
|
||||
open={open}
|
||||
onOpenChange={setOpen}
|
||||
width="w-44"
|
||||
align={align}
|
||||
triggerRender={triggerRender}
|
||||
trigger={
|
||||
customTrigger ?? (
|
||||
<>
|
||||
|
||||
@@ -9,11 +9,23 @@ import { PropertyPicker, PickerItem } from "./property-picker";
|
||||
export function StatusPicker({
|
||||
status,
|
||||
onUpdate,
|
||||
trigger: customTrigger,
|
||||
triggerRender,
|
||||
open: controlledOpen,
|
||||
onOpenChange: controlledOnOpenChange,
|
||||
align,
|
||||
}: {
|
||||
status: IssueStatus;
|
||||
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 cfg = STATUS_CONFIG[status];
|
||||
|
||||
return (
|
||||
@@ -21,11 +33,15 @@ export function StatusPicker({
|
||||
open={open}
|
||||
onOpenChange={setOpen}
|
||||
width="w-44"
|
||||
align={align}
|
||||
triggerRender={triggerRender}
|
||||
trigger={
|
||||
<>
|
||||
<StatusIcon status={status} className="h-3.5 w-3.5 shrink-0" />
|
||||
<span className="truncate">{cfg.label}</span>
|
||||
</>
|
||||
customTrigger ?? (
|
||||
<>
|
||||
<StatusIcon status={status} className="h-3.5 w-3.5 shrink-0" />
|
||||
<span className="truncate">{cfg.label}</span>
|
||||
</>
|
||||
)
|
||||
}
|
||||
>
|
||||
{ALL_STATUSES.map((s) => {
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
|
||||
import { useState, useRef } from "react";
|
||||
import { useNavigation } from "../navigation";
|
||||
import { CalendarDays, Check, ChevronRight, FolderKanban, Maximize2, Minimize2, X as XIcon } from "lucide-react";
|
||||
import { Check, ChevronRight, Maximize2, Minimize2, X as XIcon } from "lucide-react";
|
||||
import { cn } from "@multica/ui/lib/utils";
|
||||
import { toast } from "sonner";
|
||||
import type { IssueStatus, IssuePriority, IssueAssigneeType } from "@multica/core/types";
|
||||
@@ -11,29 +11,13 @@ import {
|
||||
DialogContent,
|
||||
DialogTitle,
|
||||
} from "@multica/ui/components/ui/dialog";
|
||||
import {
|
||||
DropdownMenu,
|
||||
DropdownMenuContent,
|
||||
DropdownMenuItem,
|
||||
DropdownMenuSeparator,
|
||||
DropdownMenuTrigger,
|
||||
} from "@multica/ui/components/ui/dropdown-menu";
|
||||
import {
|
||||
Popover,
|
||||
PopoverTrigger,
|
||||
PopoverContent,
|
||||
} from "@multica/ui/components/ui/popover";
|
||||
import { Calendar } from "@multica/ui/components/ui/calendar";
|
||||
import { Tooltip, TooltipTrigger, TooltipContent } from "@multica/ui/components/ui/tooltip";
|
||||
import { Button } from "@multica/ui/components/ui/button";
|
||||
import { ContentEditor, type ContentEditorRef } from "../editor";
|
||||
import { TitleEditor } from "../editor";
|
||||
import { StatusIcon, PriorityIcon, AssigneePicker } from "../issues/components";
|
||||
import { ALL_STATUSES, STATUS_CONFIG, PRIORITY_ORDER, PRIORITY_CONFIG } from "@multica/core/issues/config";
|
||||
import { StatusIcon, StatusPicker, PriorityPicker, AssigneePicker, DueDatePicker } from "../issues/components";
|
||||
import { ProjectPicker } from "../projects/components/project-picker";
|
||||
import { useWorkspaceStore } from "@multica/core/workspace";
|
||||
import { useQuery } from "@tanstack/react-query";
|
||||
import { useWorkspaceId } from "@multica/core/hooks";
|
||||
import { projectListOptions } from "@multica/core/projects/queries";
|
||||
import { useIssueDraftStore } from "@multica/core/issues/stores/draft-store";
|
||||
import { useCreateIssue } from "@multica/core/issues/mutations";
|
||||
import { useFileUpload } from "@multica/core/hooks/use-file-upload";
|
||||
@@ -71,8 +55,6 @@ function PillButton({
|
||||
export function CreateIssueModal({ onClose, data }: { onClose: () => void; data?: Record<string, unknown> | null }) {
|
||||
const router = useNavigation();
|
||||
const workspaceName = useWorkspaceStore((s) => s.workspace?.name);
|
||||
const wsId = useWorkspaceId();
|
||||
const { data: projects = [] } = useQuery(projectListOptions(wsId));
|
||||
|
||||
const draft = useIssueDraftStore((s) => s.draft);
|
||||
const setDraft = useIssueDraftStore((s) => s.setDraft);
|
||||
@@ -91,9 +73,6 @@ export function CreateIssueModal({ onClose, data }: { onClose: () => void; data?
|
||||
);
|
||||
const [isExpanded, setIsExpanded] = useState(false);
|
||||
|
||||
// Due date popover
|
||||
const [dueDateOpen, setDueDateOpen] = useState(false);
|
||||
|
||||
// File upload — collect attachment IDs so we can link them after issue creation.
|
||||
const [attachmentIds, setAttachmentIds] = useState<string[]>([]);
|
||||
const { uploadWithToast } = useFileUpload(api);
|
||||
@@ -105,8 +84,6 @@ export function CreateIssueModal({ onClose, data }: { onClose: () => void; data?
|
||||
return result;
|
||||
};
|
||||
|
||||
const dueDateObj = dueDate ? new Date(dueDate) : undefined;
|
||||
|
||||
// Sync field changes to draft store
|
||||
const updateTitle = (v: string) => { setTitle(v); setDraft({ title: v }); };
|
||||
const updateStatus = (v: IssueStatus) => { setStatus(v); setDraft({ status: v }); };
|
||||
@@ -252,46 +229,20 @@ export function CreateIssueModal({ onClose, data }: { onClose: () => void; data?
|
||||
{/* Property toolbar */}
|
||||
<div className="flex items-center gap-1.5 px-4 py-2 shrink-0 flex-wrap">
|
||||
{/* Status */}
|
||||
<DropdownMenu>
|
||||
<DropdownMenuTrigger
|
||||
render={
|
||||
<PillButton>
|
||||
<StatusIcon status={status} className="size-3.5" />
|
||||
<span>{STATUS_CONFIG[status].label}</span>
|
||||
</PillButton>
|
||||
}
|
||||
/>
|
||||
<DropdownMenuContent align="start" className="w-44">
|
||||
{ALL_STATUSES.map((s) => (
|
||||
<DropdownMenuItem key={s} onClick={() => updateStatus(s)}>
|
||||
<StatusIcon status={s} className="size-3.5" />
|
||||
<span>{STATUS_CONFIG[s].label}</span>
|
||||
</DropdownMenuItem>
|
||||
))}
|
||||
</DropdownMenuContent>
|
||||
</DropdownMenu>
|
||||
<StatusPicker
|
||||
status={status}
|
||||
onUpdate={(u) => { if (u.status) updateStatus(u.status); }}
|
||||
triggerRender={<PillButton />}
|
||||
align="start"
|
||||
/>
|
||||
|
||||
{/* Priority */}
|
||||
<DropdownMenu>
|
||||
<DropdownMenuTrigger
|
||||
render={
|
||||
<PillButton>
|
||||
<PriorityIcon priority={priority} />
|
||||
<span>{PRIORITY_CONFIG[priority].label}</span>
|
||||
</PillButton>
|
||||
}
|
||||
/>
|
||||
<DropdownMenuContent align="start" className="w-44">
|
||||
{PRIORITY_ORDER.map((p) => (
|
||||
<DropdownMenuItem key={p} onClick={() => updatePriority(p)}>
|
||||
<span className={`inline-flex items-center gap-1 rounded px-1.5 py-0.5 text-xs font-medium ${PRIORITY_CONFIG[p].badgeBg} ${PRIORITY_CONFIG[p].badgeText}`}>
|
||||
<PriorityIcon priority={p} className="h-3 w-3" inheritColor />
|
||||
{PRIORITY_CONFIG[p].label}
|
||||
</span>
|
||||
</DropdownMenuItem>
|
||||
))}
|
||||
</DropdownMenuContent>
|
||||
</DropdownMenu>
|
||||
<PriorityPicker
|
||||
priority={priority}
|
||||
onUpdate={(u) => { if (u.priority) updatePriority(u.priority); }}
|
||||
triggerRender={<PillButton />}
|
||||
align="start"
|
||||
/>
|
||||
|
||||
{/* Assignee */}
|
||||
<AssigneePicker
|
||||
@@ -306,85 +257,20 @@ export function CreateIssueModal({ onClose, data }: { onClose: () => void; data?
|
||||
/>
|
||||
|
||||
{/* Due date */}
|
||||
<Popover open={dueDateOpen} onOpenChange={setDueDateOpen}>
|
||||
<PopoverTrigger
|
||||
render={
|
||||
<PillButton>
|
||||
<CalendarDays className="size-3.5 text-muted-foreground" />
|
||||
{dueDateObj ? (
|
||||
<span>{dueDateObj.toLocaleDateString("en-US", { month: "short", day: "numeric" })}</span>
|
||||
) : (
|
||||
<span className="text-muted-foreground">Due date</span>
|
||||
)}
|
||||
</PillButton>
|
||||
}
|
||||
/>
|
||||
<PopoverContent className="w-auto p-0" align="start">
|
||||
<Calendar
|
||||
mode="single"
|
||||
selected={dueDateObj}
|
||||
onSelect={(d: Date | undefined) => {
|
||||
updateDueDate(d ? d.toISOString() : null);
|
||||
setDueDateOpen(false);
|
||||
}}
|
||||
/>
|
||||
{dueDateObj && (
|
||||
<div className="border-t px-3 py-2">
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="xs"
|
||||
onClick={() => {
|
||||
updateDueDate(null);
|
||||
setDueDateOpen(false);
|
||||
}}
|
||||
className="text-muted-foreground hover:text-foreground"
|
||||
>
|
||||
Clear date
|
||||
</Button>
|
||||
</div>
|
||||
)}
|
||||
</PopoverContent>
|
||||
</Popover>
|
||||
<DueDatePicker
|
||||
dueDate={dueDate}
|
||||
onUpdate={(u) => updateDueDate(u.due_date ?? null)}
|
||||
triggerRender={<PillButton />}
|
||||
align="start"
|
||||
/>
|
||||
|
||||
{/* Project */}
|
||||
<DropdownMenu>
|
||||
<DropdownMenuTrigger
|
||||
render={
|
||||
<PillButton>
|
||||
<FolderKanban className="size-3.5 text-muted-foreground" />
|
||||
{projectId ? (
|
||||
<span>{projects.find((p) => p.id === projectId)?.title ?? "Project"}</span>
|
||||
) : (
|
||||
<span className="text-muted-foreground">Project</span>
|
||||
)}
|
||||
</PillButton>
|
||||
}
|
||||
/>
|
||||
<DropdownMenuContent align="start" className="w-52">
|
||||
{projects.length === 0 ? (
|
||||
<div className="px-2 py-1.5 text-xs text-muted-foreground">No projects yet</div>
|
||||
) : (
|
||||
<>
|
||||
{projects.map((p) => (
|
||||
<DropdownMenuItem key={p.id} onClick={() => setProjectId(p.id)}>
|
||||
<span className="mr-1">{p.icon || "📁"}</span>
|
||||
<span className="truncate">{p.title}</span>
|
||||
{p.id === projectId && <Check className="ml-auto h-3.5 w-3.5 shrink-0" />}
|
||||
</DropdownMenuItem>
|
||||
))}
|
||||
{projectId && (
|
||||
<>
|
||||
<DropdownMenuSeparator />
|
||||
<DropdownMenuItem onClick={() => setProjectId(undefined)}>
|
||||
<XIcon className="h-3.5 w-3.5 text-muted-foreground" />
|
||||
No project
|
||||
</DropdownMenuItem>
|
||||
</>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
</DropdownMenuContent>
|
||||
</DropdownMenu>
|
||||
<ProjectPicker
|
||||
projectId={projectId ?? null}
|
||||
onUpdate={(u) => setProjectId(u.project_id ?? undefined)}
|
||||
triggerRender={<PillButton />}
|
||||
align="start"
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* Footer */}
|
||||
|
||||
@@ -16,9 +16,13 @@ import {
|
||||
export function ProjectPicker({
|
||||
projectId,
|
||||
onUpdate,
|
||||
triggerRender,
|
||||
align = "start",
|
||||
}: {
|
||||
projectId: string | null;
|
||||
onUpdate: (updates: Partial<UpdateIssueRequest>) => void;
|
||||
triggerRender?: React.ReactElement;
|
||||
align?: "start" | "center" | "end";
|
||||
}) {
|
||||
const wsId = useWorkspaceId();
|
||||
const { data: projects = [] } = useQuery(projectListOptions(wsId));
|
||||
@@ -26,11 +30,14 @@ export function ProjectPicker({
|
||||
|
||||
return (
|
||||
<DropdownMenu>
|
||||
<DropdownMenuTrigger className="flex items-center gap-1.5 cursor-pointer rounded px-1 -mx-1 hover:bg-accent/30 transition-colors overflow-hidden">
|
||||
<DropdownMenuTrigger
|
||||
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}
|
||||
>
|
||||
<FolderKanban className="h-3.5 w-3.5 shrink-0 text-muted-foreground" />
|
||||
<span className="truncate">{current ? current.title : "No project"}</span>
|
||||
</DropdownMenuTrigger>
|
||||
<DropdownMenuContent align="start" className="w-52">
|
||||
<DropdownMenuContent align={align} className="w-52">
|
||||
{projects.map((p) => (
|
||||
<DropdownMenuItem key={p.id} onClick={() => onUpdate({ project_id: p.id })}>
|
||||
<span className="mr-1">{p.icon || "📁"}</span>
|
||||
|
||||
Reference in New Issue
Block a user