import { Button } from "@cloudflare/kumo"; import { useLingui } from "@lingui/react/macro"; import { Image as ImageIcon, ImageBroken, X } from "@phosphor-icons/react"; import * as React from "react"; import type { MediaItem } from "../lib/api"; import { isSafeUrl } from "../lib/url"; import { MediaPickerModal } from "./MediaPickerModal"; export interface BlockKitMediaPickerFieldProps { actionId: string; label: string; placeholder?: string; mimeTypeFilter?: string; value: unknown; onChange: (actionId: string, value: unknown) => void; } /** * Shared media_picker BlockKit element renderer used by `BlockKitFieldWidget` * (sandboxed plugin field widgets) and the `BlockKitField` switch inside * `PortableTextEditor` (plugin block forms). * * The stored value is the asset URL string, so values are interchangeable * with `text_input`. Existing arbitrary URLs are tolerated but only previewed * when they pass scheme/path safety checks. */ export function BlockKitMediaPickerField({ actionId, label, placeholder, mimeTypeFilter, value, onChange, }: BlockKitMediaPickerFieldProps) { const { t } = useLingui(); const [pickerOpen, setPickerOpen] = React.useState(false); const [imageBroken, setImageBroken] = React.useState(false); const url = typeof value === "string" && value.length > 0 ? value : ""; React.useEffect(() => { setImageBroken(false); }, [url]); const filter = mimeTypeFilter ?? "image/"; const canPreview = isSafePreviewUrl(url); const handleSelect = (item: MediaItem) => { // `MediaPickerModal` returns URL-inserted items with `id: ""` and no // `provider`/`storageKey`, so we cannot infer "local" from absence of // `provider` alone — that would rewrite the external URL to a broken // `/_emdash/api/media/file/` path. Detect local explicitly. const isLocalMedia = item.provider === "local" || !!item.storageKey; const localKey = item.storageKey || item.id; const nextUrl = isLocalMedia && localKey ? `/_emdash/api/media/file/${localKey}` : item.url; if (!nextUrl) return; onChange(actionId, nextUrl); }; return (