"use client"; import { useRouter } from "next/navigation"; import { useEffect, useState, useTransition } from "react"; import { addIssueNote, getIssueNotes, updateIssueTriage, } from "@/actions/issues"; import { IssuesLoadError } from "@/components/issues/issues-load-error"; import { Button } from "@/components/shadcn/button/button"; import { DateWithTime } from "@/components/shadcn/entities"; import { Textarea } from "@/components/shadcn/textarea/textarea"; import { useToast } from "@/components/shadcn/toast/use-toast"; import { TriageStatusPicker } from "@/components/shared/triage/triage-status-picker"; import { FINDING_TRIAGE_MANUAL_STATUS_VALUES, FINDING_TRIAGE_NOTE_MAX_LENGTH, isManualStatus, } from "@/types/findings-triage"; import { IssueTriageNote } from "@/types/issues"; import type { IssueTriageProps } from "./types"; /** * The decision on the path, and why. * * It belongs to the account and the pattern, not to this scan's row, so it * still applies after the next scan. The verdict beside it is the engine's: * accepting a risk does not make a path unexploitable. */ export const IssueTriage = ({ id, status }: IssueTriageProps) => { const router = useRouter(); const { toast } = useToast(); const [pending, startTransition] = useTransition(); const [nextStatus, setNextStatus] = useState(status); const [shownStatus, setShownStatus] = useState(status); const [note, setNote] = useState(""); const [notes, setNotes] = useState([]); const [reload, setReload] = useState(0); // Follow the status the server now reports (a refresh after a save, or // someone else's decision), so a note-only save never writes back a stale one. if (status !== shownStatus) { setShownStatus(status); setNextStatus(status); } useEffect(() => { let cancelled = false; getIssueNotes(id).then((loaded) => { if (!cancelled) setNotes(loaded); }); return () => { cancelled = true; }; }, [id, reload]); const statusChanged = nextStatus !== status; const trimmedNote = note.trim(); const save = () => startTransition(async () => { const result = statusChanged && isManualStatus(nextStatus) ? await updateIssueTriage(id, { status: nextStatus, note: trimmedNote || undefined, }) : await addIssueNote(id, trimmedNote); if ("error" in result) { toast({ variant: "destructive", title: "Triage not saved", description: result.error, }); return; } setNote(""); setReload((value) => value + 1); router.refresh(); }); return (

Triage