import { useId, useState } from "react"; import { parseMetadataFilter } from "../lib/metadata-filter"; import { Button } from "./ui/button"; import { Input } from "./ui/input"; import { Textarea } from "./ui/textarea"; const OPERATORS = [ "eq", "ne", "gt", "gte", "lt", "lte", "in", "nin", "all", "exists", ]; /** The JSON text is the single source of truth, including invalid drafts. */ export function MetadataFilter({ value, onChange, }: { value: string; onChange: (value: string) => void; }) { const id = useId(); const [draftValue, setDraftValue] = useState(null); const [draftLeaf, setDraftLeaf] = useState | null>( null ); const { error } = parseMetadataFilter(value); let leaf: Record | null = null; try { const parsed: unknown = JSON.parse(value); if ( parsed && typeof parsed === "object" && !Array.isArray(parsed) && "op" in parsed && OPERATORS.includes(String(parsed.op)) ) leaf = parsed as Record; } catch { /* Invalid JSON stays editable below. */ } if (value.trim()) leaf ??= draftLeaf; const update = (patch: Record) => { const next = { ...leaf, ...patch }; setDraftLeaf(next); if (draftValue !== null) { const property = "values" in next ? "values" : "value"; onChange( `{"op":${JSON.stringify(next.op)},"key":${JSON.stringify(next.key)},"${property}":${draftValue}}` ); } else onChange(JSON.stringify(next)); }; return (
Custom metadata

Filter indexed gno.metadata fields. Text is case-sensitive. Values use JSON: "approved", 0.8, true, or ["alpha"]. Filters are included in this page’s URL.

{leaf ? (
) : !value.trim() ? ( ) : null}