Open the request in a window, and name the endpoint

The preview was a 10rem box in the corner of a docked panel: enough to
see that there is JSON, not enough to read it. It opens as a modal with
the endpoint line and a copy button (with an execCommand fallback,
since navigator.clipboard is undefined on this app's plain-http origin).

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
Paul Trowbridge 2026-09-23 22:11:27 -04:00
parent 065d1c2571
commit 4bce3bbd17

View File

@ -1219,18 +1219,69 @@ function ApplyModeChooser({ op, applyMode, setApplyMode, count }) {
return <Segmented value={applyMode} onChange={setApplyMode} options={options} />
}
function RequestPreview({ payload }) {
// The request as it will be sent. A 40rem-tall box in the corner of a docked
// panel could show about six lines of it, which is enough to see that there is
// JSON and not enough to read it, so it opens as a window of its own.
//
// It is the payload itself -- `submitOp` posts exactly what `buildPayload`
// returns, with no last-minute additions -- so what is shown here is what goes
// over the wire, minus the two things the browser never sees: the session that
// names `pf_user`, and the territory predicate the server ANDs on.
function RequestPreview({ payload, op, versionId }) {
const [open, setOpen] = useState(false)
const [copied, setCopied] = useState(false)
useEffect(() => {
if (!open) return
const onKey = (e) => { if (e.key === 'Escape') setOpen(false) }
window.addEventListener('keydown', onKey)
return () => window.removeEventListener('keydown', onKey)
}, [open])
if (!payload) return null
const text = JSON.stringify(payload, null, 2)
const line = `POST /api/versions/${versionId}/${op}`
// navigator.clipboard is undefined on a plain-http origin, which this app is
// served over, so the fallback is not decoration.
const copy = async () => {
try {
if (navigator.clipboard?.writeText) await navigator.clipboard.writeText(text)
else {
const ta = document.createElement('textarea')
ta.value = text
ta.style.position = 'fixed'; ta.style.opacity = '0'
document.body.appendChild(ta); ta.select()
document.execCommand('copy')
document.body.removeChild(ta)
}
setCopied(true); setTimeout(() => setCopied(false), 1500)
} catch { /* leave the text on screen to select by hand */ }
}
return (
<div>
<TextButton onClick={() => setOpen(o => !o)}>
{open ? '▾' : '▸'} request
</TextButton>
<TextButton onClick={() => setOpen(true)}>▸ request</TextButton>
{open && (
<pre className="mt-1 font-mono text-gray-600 bg-gray-50 border border-gray-100 rounded p-2 overflow-auto max-h-40 leading-relaxed">
{JSON.stringify(payload, null, 2)}
</pre>
<div className="fixed inset-0 z-50 flex items-center justify-center bg-black/40"
onClick={() => setOpen(false)}>
<div className="bg-white rounded-lg shadow-xl w-full max-w-3xl mx-4 flex flex-col max-h-[85vh]"
onClick={e => e.stopPropagation()}>
<div className="flex items-center gap-3 px-5 py-3 border-b border-gray-200 shrink-0">
<span className="font-medium text-gray-700 text-sm">Request</span>
<span className="font-mono text-gray-600 text-xs truncate" title={line}>{line}</span>
<div className="ml-auto flex items-center gap-3">
<TextButton onClick={copy}>{copied ? 'copied' : 'copy'}</TextButton>
<button onClick={() => setOpen(false)}
className="text-gray-600 hover:text-gray-800 text-lg leading-none">×</button>
</div>
</div>
<pre className="overflow-auto p-5 font-mono text-gray-700 text-xs leading-relaxed whitespace-pre">
{text}
</pre>
</div>
</div>
)}
</div>
)
@ -1408,7 +1459,7 @@ export default function OperationPanel({
{opBusy && <span className="text-gray-500 leading-snug">writing rows…</span>}
</div>
<RequestPreview payload={buildPayload(activeOp)} />
<RequestPreview payload={buildPayload(activeOp)} op={activeOp} versionId={versionId} />
</div>
)