diff --git a/design/studio/app/studies/data/QueryForm.tsx b/design/studio/app/studies/data/QueryForm.tsx new file mode 100644 index 00000000..f8ade332 --- /dev/null +++ b/design/studio/app/studies/data/QueryForm.tsx @@ -0,0 +1,95 @@ +"use client"; + +import { useRouter, useSearchParams } from "next/navigation"; +import { useEffect, useState, useTransition, type FormEvent } from "react"; + +export function QueryForm({ + paramName, + basePath = "/studies/data", + defaultValue, + placeholder, + multiline, + submitLabel = "run ↻", + alsoClear = ["force"], +}: { + /** Which URL search param this form writes. */ + paramName: string; + basePath?: string; + defaultValue: string; + placeholder: string; + multiline?: boolean; + submitLabel?: string; + /** Other params to clear when this form submits (default: clear `force`). */ + alsoClear?: string[]; +}) { + const router = useRouter(); + const searchParams = useSearchParams(); + const [value, setValue] = useState(defaultValue); + const [isPending, startTransition] = useTransition(); + + // Sync local state when the URL-driven defaultValue changes from the outside + // (e.g. clicking a shortcut). Does not interfere with in-flight typing — + // submits set URL = local state, so the effect is a no-op then. + useEffect(() => { + setValue(defaultValue); + }, [defaultValue]); + + function buildHref(next: string): string { + const params = new URLSearchParams(searchParams?.toString() ?? ""); + for (const k of alsoClear) params.delete(k); + if (next.trim()) params.set(paramName, next); + else params.delete(paramName); + const qs = params.toString(); + return qs ? `${basePath}?${qs}` : basePath; + } + + function submit(e: FormEvent) { + e.preventDefault(); + startTransition(() => { + router.push(buildHref(value), { scroll: false }); + }); + } + + const inputCls = + "flex-1 rounded border border-studio-edge bg-studio-canvas px-2 py-1 font-mono text-[11px] text-studio-ink placeholder:text-studio-ink-faint focus:outline-none focus:ring-1 focus:ring-studio-ink-faint"; + + return ( +
+ {multiline ? ( +