"use client"; import Link from "next/link"; import { useEffect, useOptimistic, useState, useTransition, type ReactNode } from "react"; import { updateIssueAction } from "@/app/actions/issues"; import { StatusBadge, Textarea } from "@unlocalhosted/metalui"; import { CheckIcon, ChevronRightIcon, PencilIcon } from "@/components/icons"; import { Box, Button } from "@/components/ui"; import { applyPatch, statusLabel, type Issue, type IssuePatch } from "@/lib/issue-meta"; import { STATUS_LED } from "./issue-icons"; import { AssigneePicker, DueDateField, EstimatePicker, LabelsPicker, PriorityPicker, StatusPicker } from "./properties"; import { useTracker } from "./tracker-context"; type PickerKind = "status" | "priority" | "assignee" | "labels"; const SHORTCUT_PICKERS: Record = { s: "status", p: "priority", a: "assignee", l: "labels" }; /** * An issue's title, description and properties, all editable in place. The * rendered description, the activity feed and the sidebar footer are server-rendered slots. */ export function IssueDetail({ issue: serverIssue, description, sidebarFooter, children, }: { issue: Issue; description: ReactNode; sidebarFooter: ReactNode; children: ReactNode; }) { // Only collaborators (canTriage) change issues; everyone else can just comment. const { owner, name, base, members, labels, canTriage, notify } = useTracker(); const [issue, applyOptimistic] = useOptimistic(serverIssue, (i, patch: IssuePatch) => applyPatch(i, patch, { members, labels })); const [pending, startTransition] = useTransition(); const [editingTitle, setEditingTitle] = useState(false); const [editingBody, setEditingBody] = useState(false); const [openPicker, setOpenPicker] = useState(null); const update = (patch: IssuePatch, then?: () => void) => startTransition(async () => { applyOptimistic(patch); const result = await updateIssueAction(owner, name, issue.number, patch); if (result.error) notify(result.error, { tone: "error" }); else then?.(); }); useEffect(() => { const onKey = (e: KeyboardEvent) => { const target = e.target as HTMLElement; if (e.metaKey || e.ctrlKey || e.altKey || target.isContentEditable || /^(INPUT|TEXTAREA|SELECT)$/.test(target.tagName)) return; const kind = SHORTCUT_PICKERS[e.key.toLowerCase()]; if (!kind || !canTriage) return; e.preventDefault(); setOpenPicker(kind); }; window.addEventListener("keydown", onKey); return () => window.removeEventListener("keydown", onKey); }, [canTriage]); const picker = (kind: PickerKind) => ({ open: openPicker === kind ? true : undefined, onOpenChange: (open: boolean) => !open && setOpenPicker(null), }); return (
{statusLabel(issue.status)}
{editingTitle ? (
{ e.preventDefault(); const title = String(new FormData(e.currentTarget).get("title") ?? "").trim(); if (title && title !== issue.title) update({ title }); setEditingTitle(false); }} > e.currentTarget.form?.requestSubmit()} onKeyDown={(e) => e.key === "Escape" && setEditingTitle(false)} className="-mx-2 w-[calc(100%+1rem)] rounded-row px-2 py-0.5 text-[26px] leading-8 font-semibold tracking-[-0.025em] text-ink caret-green-deep recipe-well-field outline-none focus:focus-ring-flush" />
) : (

canTriage && setEditingTitle(true)} className={`text-[26px] leading-8 font-semibold tracking-[-0.025em] text-ink ${canTriage ? "-mx-2 cursor-text rounded-row px-2 py-0.5 transition-[background,box-shadow] hover:recipe-row-list-hover" : ""}`} title={canTriage ? "Click to edit" : undefined} > {issue.title}

)}
{editingBody ? (
{ e.preventDefault(); const body = String(new FormData(e.currentTarget).get("body") ?? ""); update({ body }, () => setEditingBody(false)); }} className="space-y-2" >