"use client"; import { useState, useRef } from "react"; import { useNavigation } from "../navigation"; 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"; import { Dialog, DialogContent, DialogTitle, } from "@multica/ui/components/ui/dialog"; import { Tooltip, TooltipTrigger, TooltipContent } from "@multica/ui/components/ui/tooltip"; import { Button } from "@multica/ui/components/ui/button"; import { ContentEditor, type ContentEditorRef, TitleEditor, useFileDropZone, FileDropOverlay } from "../editor"; import { StatusIcon, StatusPicker, PriorityPicker, AssigneePicker, DueDatePicker } from "../issues/components"; import { BacklogAgentHintContent } from "../issues/components/backlog-agent-hint-dialog"; import { ProjectPicker } from "../projects/components/project-picker"; import { useCurrentWorkspace, useWorkspacePaths } from "@multica/core/paths"; import { useIssueDraftStore } from "@multica/core/issues/stores/draft-store"; import { useCreateIssue, useUpdateIssue } from "@multica/core/issues/mutations"; import { useFileUpload } from "@multica/core/hooks/use-file-upload"; import { api } from "@multica/core/api"; import { FileUploadButton } from "@multica/ui/components/common/file-upload-button"; // --------------------------------------------------------------------------- // Pill trigger — shared rounded-full button style for toolbar // --------------------------------------------------------------------------- function PillButton({ children, className, ...props }: React.ButtonHTMLAttributes) { return ( ); } // --------------------------------------------------------------------------- // CreateIssueModal // --------------------------------------------------------------------------- export function CreateIssueModal({ onClose, data }: { onClose: () => void; data?: Record | null }) { const router = useNavigation(); const p = useWorkspacePaths(); const workspaceName = useCurrentWorkspace()?.name; const draft = useIssueDraftStore((s) => s.draft); const setDraft = useIssueDraftStore((s) => s.setDraft); const clearDraft = useIssueDraftStore((s) => s.clearDraft); const [title, setTitle] = useState(draft.title); const descEditorRef = useRef(null); const { isDragOver: descDragOver, dropZoneProps: descDropZoneProps } = useFileDropZone({ onDrop: (files) => files.forEach((f) => descEditorRef.current?.uploadFile(f)), }); const [status, setStatus] = useState((data?.status as IssueStatus) || draft.status); const [priority, setPriority] = useState(draft.priority); const [submitting, setSubmitting] = useState(false); const [assigneeType, setAssigneeType] = useState(draft.assigneeType); const [assigneeId, setAssigneeId] = useState(draft.assigneeId); const [dueDate, setDueDate] = useState(draft.dueDate); const [projectId, setProjectId] = useState( (data?.project_id as string) || undefined, ); const [isExpanded, setIsExpanded] = useState(false); const [backlogHintIssueId, setBacklogHintIssueId] = useState(null); // File upload — collect attachment IDs so we can link them after issue creation. const [attachmentIds, setAttachmentIds] = useState([]); const { uploadWithToast } = useFileUpload(api); const handleUpload = async (file: File) => { const result = await uploadWithToast(file); if (result) { setAttachmentIds((prev) => [...prev, result.id]); } return result; }; // Sync field changes to draft store const updateTitle = (v: string) => { setTitle(v); setDraft({ title: v }); }; const updateStatus = (v: IssueStatus) => { setStatus(v); setDraft({ status: v }); }; const updatePriority = (v: IssuePriority) => { setPriority(v); setDraft({ priority: v }); }; const updateAssignee = (type?: IssueAssigneeType, id?: string) => { setAssigneeType(type); setAssigneeId(id); setDraft({ assigneeType: type, assigneeId: id }); }; const updateDueDate = (v: string | null) => { setDueDate(v); setDraft({ dueDate: v }); }; const createIssueMutation = useCreateIssue(); const updateIssueMutation = useUpdateIssue(); const handleSubmit = async () => { if (!title.trim() || submitting) return; setSubmitting(true); try { const issue = await createIssueMutation.mutateAsync({ title: title.trim(), description: descEditorRef.current?.getMarkdown()?.trim() || undefined, status, priority, assignee_type: assigneeType, assignee_id: assigneeId, due_date: dueDate || undefined, attachment_ids: attachmentIds.length > 0 ? attachmentIds : undefined, parent_issue_id: (data?.parent_issue_id as string) || undefined, project_id: projectId, }); clearDraft(); const shouldShowBacklogHint = status === "backlog" && assigneeType === "agent" && assigneeId && localStorage.getItem("multica:backlog-agent-hint-dismissed") !== "true"; if (shouldShowBacklogHint) { setBacklogHintIssueId(issue.id); } else { onClose(); } if (!shouldShowBacklogHint) { toast.custom((t) => (
Issue created
{issue.identifier} – {issue.title}
), { duration: 5000 }); } } catch { toast.error("Failed to create issue"); } finally { setSubmitting(false); } }; return ( { if (!v) { setBacklogHintIssueId(null); onClose(); } }} > {backlogHintIssueId ? ( { setBacklogHintIssueId(null); onClose(); }} onDismissPermanently={() => { localStorage.setItem("multica:backlog-agent-hint-dismissed", "true"); }} onMoveToTodo={() => { updateIssueMutation.mutate( { id: backlogHintIssueId, status: "todo" }, { onError: () => toast.error("Failed to update status") }, ); setBacklogHintIssueId(null); onClose(); }} /> ) : ( <> New Issue {/* Header */}
{workspaceName} {typeof data?.parent_issue_identifier === "string" && ( <> {data.parent_issue_identifier} )} {data?.parent_issue_id ? "New sub-issue" : "New issue"}
setIsExpanded(!isExpanded)} className="rounded-sm p-1.5 opacity-70 hover:opacity-100 hover:bg-accent/60 transition-all cursor-pointer" > {isExpanded ? : } } /> {isExpanded ? "Collapse" : "Expand"} } /> Close
{/* Title */}
updateTitle(v)} onSubmit={handleSubmit} />
{/* Description — takes remaining space */}
setDraft({ description: md })} onUploadFile={handleUpload} debounceMs={500} /> {descDragOver && }
{/* Property toolbar */}
{/* Status */} { if (u.status) updateStatus(u.status); }} triggerRender={} align="start" /> {/* Priority */} { if (u.priority) updatePriority(u.priority); }} triggerRender={} align="start" /> {/* Assignee */} updateAssignee( u.assignee_type ?? undefined, u.assignee_id ?? undefined, )} triggerRender={} align="start" /> {/* Due date */} updateDueDate(u.due_date ?? null)} triggerRender={} align="start" /> {/* Project */} setProjectId(u.project_id ?? undefined)} triggerRender={} align="start" />
{/* Footer */}
descEditorRef.current?.uploadFile(file)} />
)}
); }