import { Input, Select, Switch } from "@cloudflare/kumo"; import type { Element } from "@emdash-cms/blocks"; import { useLingui } from "@lingui/react/macro"; import * as React from "react"; import { BlockKitMediaPickerField } from "./BlockKitMediaPickerField"; interface BlockKitFieldWidgetProps { label: string; elements: Element[]; value: unknown; onChange: (value: unknown) => void; } /** * Renders Block Kit elements as a field widget for sandboxed plugins. * Decomposes a JSON value into per-element values keyed by action_id, * and recomposes on change. */ export function BlockKitFieldWidget({ label, elements, value, onChange, }: BlockKitFieldWidgetProps) { const obj = (value && typeof value === "object" ? value : {}) as Record; // Use a ref to avoid stale closure -- rapid changes to different elements // would otherwise lose updates because each callback spreads from a stale obj. const objRef = React.useRef(obj); objRef.current = obj; const handleElementChange = React.useCallback( (actionId: string, elementValue: unknown) => { onChange({ ...objRef.current, [actionId]: elementValue }); }, [onChange], ); // Filter out elements without action_id -- they can't be mapped to values const validElements = elements.filter((el) => el.action_id); return (
{label}
{validElements.map((el) => ( ))}
); } function BlockKitFieldElement({ element, value, onChange, }: { element: Element; value: unknown; onChange: (actionId: string, value: unknown) => void; }) { const { t } = useLingui(); switch (element.type) { case "text_input": return ( onChange(element.action_id, e.target.value)} /> ); case "number_input": return ( { const n = Number(e.target.value); onChange(element.action_id, e.target.value && Number.isFinite(n) ? n : undefined); }} /> ); case "toggle": return ( onChange(element.action_id, checked)} /> ); case "select": { const options = Array.isArray(element.options) ? element.options : []; return (