diff --git a/packages/views/issues/components/batch-action-toolbar.tsx b/packages/views/issues/components/batch-action-toolbar.tsx index 137b8d52f..681ed885d 100644 --- a/packages/views/issues/components/batch-action-toolbar.tsx +++ b/packages/views/issues/components/batch-action-toolbar.tsx @@ -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() { {/* Status */} - - - } - > - - Status - - - {ALL_STATUSES.map((s) => { - const cfg = STATUS_CONFIG[s]; - return ( - { - 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`} - > - - {cfg.label} - - ); - })} - - + } + trigger="Status" + align="center" + /> {/* Priority */} - - - } - > - - Priority - - - {PRIORITY_ORDER.map((p) => { - const cfg = PRIORITY_CONFIG[p]; - return ( - { - 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" - > - - - {cfg.label} - - - ); - })} - - + } + trigger="Priority" + align="center" + /> {/* Assignee */} {/* Status */} - - - - {STATUS_CONFIG[issue.status].label} - - - {ALL_STATUSES.map((s) => ( - handleUpdateField({ status: s })}> - - {STATUS_CONFIG[s].label} - {s === issue.status && } - - ))} - - + {/* Priority */} - - - - {PRIORITY_CONFIG[issue.priority].label} - - - {PRIORITY_ORDER.map((p) => ( - handleUpdateField({ priority: p })}> - - - {PRIORITY_CONFIG[p].label} - - {p === issue.priority && } - - ))} - - + {/* Assignee */} diff --git a/packages/views/issues/components/pickers/due-date-picker.tsx b/packages/views/issues/components/pickers/due-date-picker.tsx index 52003f9ca..baceb5a0d 100644 --- a/packages/views/issues/components/pickers/due-date-picker.tsx +++ b/packages/views/issues/components/pickers/due-date-picker.tsx @@ -15,10 +15,14 @@ export function DueDatePicker({ dueDate, onUpdate, trigger: customTrigger, + triggerRender, + align = "start", }: { dueDate: string | null; onUpdate: (updates: Partial) => 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 ( - + {customTrigger ?? ( <> @@ -40,7 +47,7 @@ export function DueDatePicker({ > )} - + ) => 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 ?? ( <> diff --git a/packages/views/issues/components/pickers/status-picker.tsx b/packages/views/issues/components/pickers/status-picker.tsx index 95b5ade14..febec36ef 100644 --- a/packages/views/issues/components/pickers/status-picker.tsx +++ b/packages/views/issues/components/pickers/status-picker.tsx @@ -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) => 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={ - <> - - {cfg.label} - > + customTrigger ?? ( + <> + + {cfg.label} + > + ) } > {ALL_STATUSES.map((s) => { diff --git a/packages/views/modals/create-issue.tsx b/packages/views/modals/create-issue.tsx index 5786d6dcd..e52be91ab 100644 --- a/packages/views/modals/create-issue.tsx +++ b/packages/views/modals/create-issue.tsx @@ -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 | 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([]); 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 */} {/* Status */} - - - - {STATUS_CONFIG[status].label} - - } - /> - - {ALL_STATUSES.map((s) => ( - updateStatus(s)}> - - {STATUS_CONFIG[s].label} - - ))} - - + { if (u.status) updateStatus(u.status); }} + triggerRender={} + align="start" + /> {/* Priority */} - - - - {PRIORITY_CONFIG[priority].label} - - } - /> - - {PRIORITY_ORDER.map((p) => ( - updatePriority(p)}> - - - {PRIORITY_CONFIG[p].label} - - - ))} - - + { if (u.priority) updatePriority(u.priority); }} + triggerRender={} + align="start" + /> {/* Assignee */} void; data? /> {/* Due date */} - - - - {dueDateObj ? ( - {dueDateObj.toLocaleDateString("en-US", { month: "short", day: "numeric" })} - ) : ( - Due date - )} - - } - /> - - { - updateDueDate(d ? d.toISOString() : null); - setDueDateOpen(false); - }} - /> - {dueDateObj && ( - - { - updateDueDate(null); - setDueDateOpen(false); - }} - className="text-muted-foreground hover:text-foreground" - > - Clear date - - - )} - - + updateDueDate(u.due_date ?? null)} + triggerRender={} + align="start" + /> {/* Project */} - - - - {projectId ? ( - {projects.find((p) => p.id === projectId)?.title ?? "Project"} - ) : ( - Project - )} - - } - /> - - {projects.length === 0 ? ( - No projects yet - ) : ( - <> - {projects.map((p) => ( - setProjectId(p.id)}> - {p.icon || "📁"} - {p.title} - {p.id === projectId && } - - ))} - {projectId && ( - <> - - setProjectId(undefined)}> - - No project - - > - )} - > - )} - - + setProjectId(u.project_id ?? undefined)} + triggerRender={} + align="start" + /> {/* Footer */} diff --git a/packages/views/projects/components/project-picker.tsx b/packages/views/projects/components/project-picker.tsx index 6c5f96fc4..1dff9c176 100644 --- a/packages/views/projects/components/project-picker.tsx +++ b/packages/views/projects/components/project-picker.tsx @@ -16,9 +16,13 @@ import { export function ProjectPicker({ projectId, onUpdate, + triggerRender, + align = "start", }: { projectId: string | null; onUpdate: (updates: Partial) => 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 ( - + {current ? current.title : "No project"} - + {projects.map((p) => ( onUpdate({ project_id: p.id })}> {p.icon || "📁"}