diff --git a/surfsense_web/app/dashboard/[workspace_id]/playground/components/playground-runner.tsx b/surfsense_web/app/dashboard/[workspace_id]/playground/components/playground-runner.tsx index 6fbf7b98b..6bee3e4e5 100644 --- a/surfsense_web/app/dashboard/[workspace_id]/playground/components/playground-runner.tsx +++ b/surfsense_web/app/dashboard/[workspace_id]/playground/components/playground-runner.tsx @@ -209,11 +209,21 @@ export function PlaygroundRunner({ workspaceId, platform, verb }: PlaygroundRunn } if (run.status === "error") { - setFieldErrors(fieldErrorsFromError(run.error)); + const nextFieldErrors = fieldErrorsFromError(run.error); + setFieldErrors(nextFieldErrors); const key = `${run.runId ?? "run"}:error`; if (notifiedRunRef.current === key) return; notifiedRunRef.current = key; - toast.error(getRunErrorMessage(run.error)); + // Field-level failures are shown inline (the form reveals + focuses the + // first one), so only toast global failures that have no field to anchor to. + // Keep run errors until dismissed — they're actionable and shouldn't + // vanish before the user reads them. + if (Object.keys(nextFieldErrors).length === 0) { + toast.error(getRunErrorMessage(run.error), { + duration: Number.POSITIVE_INFINITY, + closeButton: true, + }); + } } }, [run.status, run.runId, run.error]); diff --git a/surfsense_web/app/dashboard/[workspace_id]/playground/components/schema-form.tsx b/surfsense_web/app/dashboard/[workspace_id]/playground/components/schema-form.tsx index c86dd9839..b8eb72c66 100644 --- a/surfsense_web/app/dashboard/[workspace_id]/playground/components/schema-form.tsx +++ b/surfsense_web/app/dashboard/[workspace_id]/playground/components/schema-form.tsx @@ -1,7 +1,7 @@ "use client"; import { ChevronDown } from "lucide-react"; -import { useMemo, useState } from "react"; +import { useEffect, useMemo, useState } from "react"; import { Badge } from "@/components/ui/badge"; import { Input } from "@/components/ui/input"; import { Label } from "@/components/ui/label"; @@ -205,6 +205,25 @@ export function SchemaForm({ return { primary: primaryFields, advanced: advancedFields }; }, [fields]); + // First invalid field in display order; drives reveal + focus below. + const firstErrorName = useMemo( + () => (fieldErrors ? fields.find((f) => fieldErrors[f.name])?.name : undefined), + [fields, fieldErrors] + ); + + // Reveal the section holding an invalid field, then move focus to it, so an + // error is never left hidden inside the collapsed "Advanced" group. + useEffect(() => { + if (!firstErrorName) return; + if (advanced.some((f) => f.name === firstErrorName)) setShowAdvanced(true); + const raf = requestAnimationFrame(() => { + const el = document.getElementById(`field-${firstErrorName}`); + el?.focus(); + el?.scrollIntoView({ block: "center", behavior: "smooth" }); + }); + return () => cancelAnimationFrame(raf); + }, [firstErrorName, advanced]); + return (