diff --git a/skyvern-frontend/src/components/DataSchemaInputGroup/WorkflowDataSchemaInputGroup.tsx b/skyvern-frontend/src/components/DataSchemaInputGroup/WorkflowDataSchemaInputGroup.tsx index 22eaee1f..a2ac5ba4 100644 --- a/skyvern-frontend/src/components/DataSchemaInputGroup/WorkflowDataSchemaInputGroup.tsx +++ b/skyvern-frontend/src/components/DataSchemaInputGroup/WorkflowDataSchemaInputGroup.tsx @@ -12,11 +12,12 @@ import { useCredentialGetter } from "@/hooks/useCredentialGetter"; import { getClient } from "@/api/AxiosClient"; import { CodeEditor } from "@/routes/workflows/components/CodeEditor"; import { helpTooltips } from "@/routes/workflows/editor/helpContent"; -import { useState } from "react"; +import { useMemo, useState } from "react"; import { AutoResizingTextarea } from "../AutoResizingTextarea/AutoResizingTextarea"; import { Button } from "../ui/button"; import { AxiosError } from "axios"; import { toast } from "../ui/use-toast"; +import { cn } from "@/util/utils"; type Props = { value: string; @@ -35,6 +36,42 @@ function WorkflowDataSchemaInputGroup({ const [generateWithAIActive, setGenerateWithAIActive] = useState(false); const [generateWithAIPrompt, setGenerateWithAIPrompt] = useState(""); + function computeJsonError( + jsonText: string, + ): { message: string; line?: number; column?: number } | null { + try { + JSON.parse(jsonText); + return null; + } catch (e) { + const message = e instanceof Error ? e.message : "Invalid JSON"; + // Try to extract position and compute line/column for friendlier feedback + const match = message.match(/position\s+(\d+)/i); + if (!match) { + return { message }; + } + const pos = Number(match[1]); + if (Number.isNaN(pos)) { + return { message }; + } + let line = 1; + let col = 1; + for (let i = 0; i < Math.min(pos, jsonText.length); i++) { + if (jsonText[i] === "\n") { + line += 1; + col = 1; + } else { + col += 1; + } + } + return { message, line, column: col }; + } + } + + const jsonError = useMemo(() => { + if (value === "null") return null; + return computeJsonError(value); + }, [value]); + const getDataSchemaSuggestionMutation = useMutation({ mutationFn: async () => { const client = await getClient(credentialGetter); @@ -121,13 +158,27 @@ function WorkflowDataSchemaInputGroup({ )} ) : null} - +
+ +
+ {jsonError && ( +
+ {jsonError.line && jsonError.column + ? `Invalid JSON (${jsonError.line}:${jsonError.column}) — ${jsonError.message}` + : `Invalid JSON — ${jsonError.message}`} +
+ )} )} diff --git a/skyvern-frontend/src/routes/workflows/editor/FlowRenderer.tsx b/skyvern-frontend/src/routes/workflows/editor/FlowRenderer.tsx index 5bfb3a12..22700d87 100644 --- a/skyvern-frontend/src/routes/workflows/editor/FlowRenderer.tsx +++ b/skyvern-frontend/src/routes/workflows/editor/FlowRenderer.tsx @@ -79,6 +79,8 @@ import { getWorkflowSettings, layout, } from "./workflowEditorUtils"; +import { getWorkflowErrors } from "./workflowEditorUtils"; +import { toast } from "@/components/ui/use-toast"; import { useAutoPan } from "./useAutoPan"; const nextTick = () => new Promise((resolve) => setTimeout(resolve, 0)); @@ -373,8 +375,25 @@ function FlowRenderer({ setGetSaveDataRef.current(constructSaveData); }, [constructSaveData]); - async function handleSave() { - return await saveWorkflow.mutateAsync(); + async function handleSave(): Promise { + // Validate before saving; block if any workflow errors exist + const errors = getWorkflowErrors(nodes); + if (errors.length > 0) { + toast({ + title: "Can not save workflow because of errors:", + description: ( +
+ {errors.map((error) => ( +

{error}

+ ))} +
+ ), + variant: "destructive", + }); + return false; + } + await saveWorkflow.mutateAsync(); + return true; } function deleteNode(id: string) { @@ -605,8 +624,10 @@ function FlowRenderer({