import { useState, useEffect, useRef, useCallback } from "react"; import { useQuery, useMutation, useQueryClient } from "@tanstack/react-query"; import { useDialog } from "../context/DialogContext"; import { useCompany } from "../context/CompanyContext"; import { issuesApi } from "../api/issues"; import { projectsApi } from "../api/projects"; import { agentsApi } from "../api/agents"; import { queryKeys } from "../lib/queryKeys"; import { Dialog, DialogContent, } from "@/components/ui/dialog"; import { Button } from "@/components/ui/button"; import { Popover, PopoverContent, PopoverTrigger, } from "@/components/ui/popover"; import { Maximize2, Minimize2, MoreHorizontal, CircleDot, Minus, ArrowUp, ArrowDown, AlertTriangle, User, Hexagon, Tag, Calendar, } from "lucide-react"; import { cn } from "../lib/utils"; import type { Project, Agent } from "@paperclip/shared"; const DRAFT_KEY = "paperclip:issue-draft"; const DEBOUNCE_MS = 800; interface IssueDraft { title: string; description: string; status: string; priority: string; assigneeId: string; projectId: string; } function loadDraft(): IssueDraft | null { try { const raw = localStorage.getItem(DRAFT_KEY); if (!raw) return null; return JSON.parse(raw) as IssueDraft; } catch { return null; } } function saveDraft(draft: IssueDraft) { localStorage.setItem(DRAFT_KEY, JSON.stringify(draft)); } function clearDraft() { localStorage.removeItem(DRAFT_KEY); } const statuses = [ { value: "backlog", label: "Backlog", color: "text-muted-foreground" }, { value: "todo", label: "Todo", color: "text-blue-400" }, { value: "in_progress", label: "In Progress", color: "text-yellow-400" }, { value: "in_review", label: "In Review", color: "text-violet-400" }, { value: "done", label: "Done", color: "text-green-400" }, ]; const priorities = [ { value: "critical", label: "Critical", icon: AlertTriangle, color: "text-red-400" }, { value: "high", label: "High", icon: ArrowUp, color: "text-orange-400" }, { value: "medium", label: "Medium", icon: Minus, color: "text-yellow-400" }, { value: "low", label: "Low", icon: ArrowDown, color: "text-blue-400" }, ]; export function NewIssueDialog() { const { newIssueOpen, newIssueDefaults, closeNewIssue } = useDialog(); const { selectedCompanyId, selectedCompany } = useCompany(); const queryClient = useQueryClient(); const [title, setTitle] = useState(""); const [description, setDescription] = useState(""); const [status, setStatus] = useState("todo"); const [priority, setPriority] = useState(""); const [assigneeId, setAssigneeId] = useState(""); const [projectId, setProjectId] = useState(""); const [expanded, setExpanded] = useState(false); const draftTimer = useRef | null>(null); // Popover states const [statusOpen, setStatusOpen] = useState(false); const [priorityOpen, setPriorityOpen] = useState(false); const [assigneeOpen, setAssigneeOpen] = useState(false); const [projectOpen, setProjectOpen] = useState(false); const [moreOpen, setMoreOpen] = useState(false); const { data: agents } = useQuery({ queryKey: queryKeys.agents.list(selectedCompanyId!), queryFn: () => agentsApi.list(selectedCompanyId!), enabled: !!selectedCompanyId && newIssueOpen, }); const { data: projects } = useQuery({ queryKey: queryKeys.projects.list(selectedCompanyId!), queryFn: () => projectsApi.list(selectedCompanyId!), enabled: !!selectedCompanyId && newIssueOpen, }); const createIssue = useMutation({ mutationFn: (data: Record) => issuesApi.create(selectedCompanyId!, data), onSuccess: () => { queryClient.invalidateQueries({ queryKey: queryKeys.issues.list(selectedCompanyId!) }); clearDraft(); reset(); closeNewIssue(); }, }); // Debounced draft saving const scheduleSave = useCallback( (draft: IssueDraft) => { if (draftTimer.current) clearTimeout(draftTimer.current); draftTimer.current = setTimeout(() => { if (draft.title.trim()) saveDraft(draft); }, DEBOUNCE_MS); }, [], ); // Save draft on meaningful changes useEffect(() => { if (!newIssueOpen) return; scheduleSave({ title, description, status, priority, assigneeId, projectId }); }, [title, description, status, priority, assigneeId, projectId, newIssueOpen, scheduleSave]); // Restore draft or apply defaults when dialog opens useEffect(() => { if (!newIssueOpen) return; const draft = loadDraft(); if (draft && draft.title.trim()) { setTitle(draft.title); setDescription(draft.description); setStatus(draft.status || "todo"); setPriority(draft.priority); setAssigneeId(newIssueDefaults.assigneeAgentId ?? draft.assigneeId); setProjectId(newIssueDefaults.projectId ?? draft.projectId); } else { setStatus(newIssueDefaults.status ?? "todo"); setPriority(newIssueDefaults.priority ?? ""); setProjectId(newIssueDefaults.projectId ?? ""); setAssigneeId(newIssueDefaults.assigneeAgentId ?? ""); } }, [newIssueOpen, newIssueDefaults]); // Cleanup timer on unmount useEffect(() => { return () => { if (draftTimer.current) clearTimeout(draftTimer.current); }; }, []); function reset() { setTitle(""); setDescription(""); setStatus("todo"); setPriority(""); setAssigneeId(""); setProjectId(""); setExpanded(false); } function discardDraft() { clearDraft(); reset(); closeNewIssue(); } function handleSubmit() { if (!selectedCompanyId || !title.trim()) return; createIssue.mutate({ title: title.trim(), description: description.trim() || undefined, status, priority: priority || "medium", ...(assigneeId ? { assigneeAgentId: assigneeId } : {}), ...(projectId ? { projectId } : {}), }); } function handleKeyDown(e: React.KeyboardEvent) { if (e.key === "Enter" && (e.metaKey || e.ctrlKey)) { e.preventDefault(); handleSubmit(); } } const hasDraft = title.trim().length > 0 || description.trim().length > 0; const currentStatus = statuses.find((s) => s.value === status) ?? statuses[1]!; const currentPriority = priorities.find((p) => p.value === priority); const currentAssignee = (agents ?? []).find((a) => a.id === assigneeId); const currentProject = (projects ?? []).find((p) => p.id === projectId); return ( { if (!open) closeNewIssue(); }} > {/* Header bar */}
{selectedCompany && ( {selectedCompany.name.slice(0, 3).toUpperCase()} )} New issue
{/* Title */}
setTitle(e.target.value)} autoFocus />
{/* Description */}