'use client'; /** * Inline per-row review actions — the fast-triage cluster for a status table (bd * startsim-768w.18.15). Renders the (up to) three decisions from the schema as * compact icon buttons ✕ reject · ✓ good · ✎ edit; a click writes status AND * team_verdict together (the coherent pair, same as ReviewDrawer) IN PLACE — * optimistic, no drawer. The current verdict shows as the active button. * * The ✎ "edit / needs work" decision opens a small popover with a note box so the * reviewer says HOW it should change — saved to the note attr alongside the * status+verdict move. Meant to live in a table's trailing "Actions" column; it * stops click propagation so acting never triggers the row's drawer-open. * * Generic + client-injected: pass a CollectionClient + the record; defaults match * the OGMC content schema but everything routes through resolveReviewConfig. */ import { useMemo, useState } from 'react'; import { Check, X, PencilLine } from 'lucide-react'; import { Button } from '../ui/button'; import { Textarea } from '../ui/textarea'; import { Popover, PopoverContent, PopoverTrigger } from '../ui/popover'; import { notify } from '../toast/use-notify'; import { cn } from '../../lib/utils'; import type { CollectionClient, EntityRecord, EntityTypeDef } from './types'; import { toCamelKey } from './data'; import { applyDecisionToData, fieldStr, resolveReviewConfig, reviewDecisions, type ReviewConfig, type ReviewDecision, type VerdictTone, } from './review'; const ICON: Record = { good: Check, bad: X, neutral: PencilLine }; const ACTIVE: Record = { good: 'bg-emerald-600 text-white border-emerald-600', bad: 'bg-rose-600 text-white border-rose-600', neutral: 'bg-amber-500 text-white border-amber-500', }; const IDLE: Record = { good: 'border-border bg-card text-emerald-600 hover:bg-emerald-50', bad: 'border-border bg-card text-rose-600 hover:bg-rose-50', neutral: 'border-border bg-card text-amber-600 hover:bg-amber-50', }; const BTN = 'flex h-8 w-8 items-center justify-center rounded-md border transition disabled:opacity-50'; // Display order: ✕ reject · ✓ good · ✎ edit — a scan-left-to-right pass/fail/fix. const ORDER = ['reject', 'approve', 'needs_work']; export interface InlineReviewActionsProps { client: CollectionClient; type: EntityTypeDef; record: EntityRecord; /** Field-map + transition overrides; defaults match the OGMC content schema. */ config?: ReviewConfig; /** Fired after a successful save so the parent can refetch/invalidate. */ onSaved?: () => void; } export function InlineReviewActions({ client, type, record, config, onSaved }: InlineReviewActionsProps) { const cfg = useMemo(() => resolveReviewConfig(type, config), [type, config]); const decisions = useMemo( () => [...reviewDecisions(cfg)].sort((a, b) => ORDER.indexOf(a.key) - ORDER.indexOf(b.key)), [cfg], ); // Which decision the record currently reflects — by verdict when the type has // one (topics), else by status (news: acceptable→approve, rejected→reject). The // matching button shows active. Optimistically updated on click. const initialKey = useMemo(() => { const v = fieldStr(record.data, cfg.verdictAttr); const s = fieldStr(record.data, cfg.statusName); return ( decisions.find((d) => (d.verdict ? d.verdict === v : d.status ? d.status === s : false))?.key ?? null ); }, [record.data, cfg, decisions]); const [appliedKey, setAppliedKey] = useState(initialKey); const [busy, setBusy] = useState(null); const [noteOpen, setNoteOpen] = useState(false); const [note, setNote] = useState(''); /** Apply a decision (status + verdict), optionally attaching a note. Optimistic. */ async function apply(d: ReviewDecision, noteText?: string) { const prev = appliedKey; setAppliedKey(d.key); // optimistic setBusy(d.key); try { const data = applyDecisionToData(record.data, d, cfg); if (noteText !== undefined) { const camel = toCamelKey(cfg.noteAttr); const t = noteText.trim(); if (t) { data[camel] = t; if (camel !== cfg.noteAttr) delete data[cfg.noteAttr]; } else { delete data[camel]; delete data[cfg.noteAttr]; } } await client.updateEntity(record.id, { data }); onSaved?.(); return true; } catch (err) { setAppliedKey(prev); // roll back notify.error(err instanceof Error ? err.message : 'Could not save.'); return false; } finally { setBusy(null); } } function iconButton(d: ReviewDecision, extraOnClick?: () => void) { const Icon = ICON[d.tone]; const active = d.key === appliedKey; return ( ); } if (decisions.length === 0) return null; return (
e.stopPropagation()}> {decisions.map((d) => d.promptNote ? ( // ✎ edit: open a note box for HOW it should change, then save note + move. {iconButton(d, () => setNote(fieldStr(record.data, cfg.noteAttr)))} e.stopPropagation()}>

What should change?