"use client"; import { useState, useRef } from "react"; import { useRouter } from "next/navigation"; import { CalendarDays, Check, ChevronRight, Maximize2, Minimize2, UserMinus, X as XIcon } from "lucide-react"; import { cn } from "@/lib/utils"; import { toast } from "sonner"; import type { IssueStatus, IssuePriority, IssueAssigneeType } from "@/shared/types"; import { Dialog, DialogContent, DialogTitle, } from "@/components/ui/dialog"; import { DropdownMenu, DropdownMenuContent, DropdownMenuItem, DropdownMenuTrigger, } from "@/components/ui/dropdown-menu"; import { Popover, PopoverTrigger, PopoverContent, } from "@/components/ui/popover"; import { Calendar } from "@/components/ui/calendar"; import { Tooltip, TooltipTrigger, TooltipContent } from "@/components/ui/tooltip"; import { Button } from "@/components/ui/button"; import { ContentEditor, type ContentEditorRef } from "@/features/editor"; import { TitleEditor } from "@/features/editor"; import { StatusIcon, PriorityIcon } from "@/features/issues/components"; import { ALL_STATUSES, STATUS_CONFIG, PRIORITY_ORDER, PRIORITY_CONFIG } from "@/features/issues/config"; import { useWorkspaceStore, useActorName } from "@/features/workspace"; import { useQuery } from "@tanstack/react-query"; import { useWorkspaceId } from "@core/hooks"; import { memberListOptions, agentListOptions } from "@core/workspace/queries"; import { useIssueDraftStore } from "@/features/issues/stores/draft-store"; import { useCreateIssue } from "@core/issues/mutations"; import { useFileUpload } from "@/shared/hooks/use-file-upload"; import { FileUploadButton } from "@/components/common/file-upload-button"; import { ActorAvatar } from "@/components/common/actor-avatar"; // --------------------------------------------------------------------------- // 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 = useRouter(); const workspaceName = useWorkspaceStore((s) => s.workspace?.name); const wsId = useWorkspaceId(); const { data: members = [] } = useQuery(memberListOptions(wsId)); const { data: agents = [] } = useQuery(agentListOptions(wsId)); const { getActorName } = useActorName(); 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 [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 [isExpanded, setIsExpanded] = useState(false); // Assignee popover const [assigneeOpen, setAssigneeOpen] = useState(false); const [assigneeFilter, setAssigneeFilter] = useState(""); // 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(); const handleUpload = async (file: File) => { const result = await uploadWithToast(file); if (result) { setAttachmentIds((prev) => [...prev, result.id]); } return result; }; const assigneeQuery = assigneeFilter.toLowerCase(); const filteredMembers = members.filter((m) => m.name.toLowerCase().includes(assigneeQuery)); const filteredAgents = agents.filter((a) => !a.archived_at && a.name.toLowerCase().includes(assigneeQuery)); const assigneeLabel = assigneeType && assigneeId ? getActorName(assigneeType, assigneeId) : "Assignee"; 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 }); }; 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 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, }); clearDraft(); onClose(); toast.custom((t) => (
Issue created
{issue.identifier} – {issue.title}
), { duration: 5000 }); } catch { toast.error("Failed to create issue"); } finally { setSubmitting(false); } }; return ( { if (!v) 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} />
{/* Property toolbar */}
{/* Status */} {STATUS_CONFIG[status].label} } /> {ALL_STATUSES.map((s) => ( updateStatus(s)}> {STATUS_CONFIG[s].label} ))} {/* Priority */} {PRIORITY_CONFIG[priority].label} } /> {PRIORITY_ORDER.map((p) => ( updatePriority(p)}> {PRIORITY_CONFIG[p].label} ))} {/* Assignee — Popover for search support */} { setAssigneeOpen(v); if (!v) setAssigneeFilter(""); }}> {assigneeType && assigneeId ? ( <> {assigneeLabel} ) : ( Assignee )} } />
setAssigneeFilter(e.target.value)} placeholder="Assign to..." className="w-full bg-transparent text-sm placeholder:text-muted-foreground outline-none" />
{/* Unassigned */} {/* Members */} {filteredMembers.length > 0 && ( <>
Members
{filteredMembers.map((m) => ( ))} )} {/* Agents */} {filteredAgents.length > 0 && ( <>
Agents
{filteredAgents.map((a) => ( ))} )} {filteredMembers.length === 0 && filteredAgents.length === 0 && assigneeFilter && (
No results
)}
{/* Due date */} {dueDateObj ? ( {dueDateObj.toLocaleDateString("en-US", { month: "short", day: "numeric" })} ) : ( Due date )} } /> { updateDueDate(d ? d.toISOString() : null); setDueDateOpen(false); }} /> {dueDateObj && (
)}
{/* Footer */}
descEditorRef.current?.uploadFile(file)} />
); }