/** * SandboxedPluginPage * * Renders a plugin's admin page using Block Kit. Sends page_load/block_action/form_submit * interactions to the plugin's admin route and renders the returned blocks. */ import { CircleNotch, WarningCircle } from "@phosphor-icons/react"; import { BlockRenderer } from "@emdash-cms/blocks"; import type { Block, BlockInteraction, BlockResponse } from "@emdash-cms/blocks"; import { useCallback, useEffect, useState } from "react"; import { apiFetch, API_BASE } from "../lib/api/client.js"; interface SandboxedPluginPageProps { pluginId: string; page: string; } export function SandboxedPluginPage({ pluginId, page }: SandboxedPluginPageProps) { const [blocks, setBlocks] = useState([]); const [loading, setLoading] = useState(true); const [error, setError] = useState(null); const [toast, setToast] = useState(null); // Send an interaction to the plugin admin route const sendInteraction = useCallback( async (interaction: BlockInteraction) => { try { const response = await apiFetch(`${API_BASE}/plugins/${pluginId}/admin`, { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify(interaction), }); if (!response.ok) { const text = await response.text(); setError(`Plugin responded with ${response.status}: ${text}`); return; } const body = (await response.json()) as { data: BlockResponse }; const data = body.data; setBlocks(data.blocks); setError(null); if (data.toast) { setToast(data.toast); setTimeout(setToast, 4000, null); } } catch (err) { setError(err instanceof Error ? err.message : "Failed to communicate with plugin"); } }, [pluginId], ); // Initial page load useEffect(() => { setLoading(true); setError(null); void sendInteraction({ type: "page_load", page }).finally(() => setLoading(false)); }, [sendInteraction, page]); // Handle block actions const handleAction = useCallback( (interaction: BlockInteraction) => { void sendInteraction(interaction); }, [sendInteraction], ); if (loading) { return (
); } if (error) { return (

Plugin Error

{error}

); } return (
{/* Toast notification */} {toast && (
{toast.message}
)}
); }