Skip to content

Commit 11d4518

Browse files
add extraction block json validation (#3499)
Co-authored-by: Shuchang Zheng <wintonzheng0325@gmail.com>
1 parent 90096bc commit 11d4518

File tree

3 files changed

+100
-12
lines changed

3 files changed

+100
-12
lines changed

skyvern-frontend/src/components/DataSchemaInputGroup/WorkflowDataSchemaInputGroup.tsx

Lines changed: 59 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -12,11 +12,12 @@ import { useCredentialGetter } from "@/hooks/useCredentialGetter";
1212
import { getClient } from "@/api/AxiosClient";
1313
import { CodeEditor } from "@/routes/workflows/components/CodeEditor";
1414
import { helpTooltips } from "@/routes/workflows/editor/helpContent";
15-
import { useState } from "react";
15+
import { useMemo, useState } from "react";
1616
import { AutoResizingTextarea } from "../AutoResizingTextarea/AutoResizingTextarea";
1717
import { Button } from "../ui/button";
1818
import { AxiosError } from "axios";
1919
import { toast } from "../ui/use-toast";
20+
import { cn } from "@/util/utils";
2021

2122
type Props = {
2223
value: string;
@@ -35,6 +36,42 @@ function WorkflowDataSchemaInputGroup({
3536
const [generateWithAIActive, setGenerateWithAIActive] = useState(false);
3637
const [generateWithAIPrompt, setGenerateWithAIPrompt] = useState("");
3738

39+
function computeJsonError(
40+
jsonText: string,
41+
): { message: string; line?: number; column?: number } | null {
42+
try {
43+
JSON.parse(jsonText);
44+
return null;
45+
} catch (e) {
46+
const message = e instanceof Error ? e.message : "Invalid JSON";
47+
// Try to extract position and compute line/column for friendlier feedback
48+
const match = message.match(/position\s+(\d+)/i);
49+
if (!match) {
50+
return { message };
51+
}
52+
const pos = Number(match[1]);
53+
if (Number.isNaN(pos)) {
54+
return { message };
55+
}
56+
let line = 1;
57+
let col = 1;
58+
for (let i = 0; i < Math.min(pos, jsonText.length); i++) {
59+
if (jsonText[i] === "\n") {
60+
line += 1;
61+
col = 1;
62+
} else {
63+
col += 1;
64+
}
65+
}
66+
return { message, line, column: col };
67+
}
68+
}
69+
70+
const jsonError = useMemo(() => {
71+
if (value === "null") return null;
72+
return computeJsonError(value);
73+
}, [value]);
74+
3875
const getDataSchemaSuggestionMutation = useMutation({
3976
mutationFn: async () => {
4077
const client = await getClient(credentialGetter);
@@ -121,13 +158,27 @@ function WorkflowDataSchemaInputGroup({
121158
)}
122159
</div>
123160
) : null}
124-
<CodeEditor
125-
language="json"
126-
value={value}
127-
onChange={onChange}
128-
className="nopan"
129-
fontSize={8}
130-
/>
161+
<div
162+
className={cn(
163+
"rounded-md",
164+
jsonError ? "ring-1 ring-red-500" : undefined,
165+
)}
166+
>
167+
<CodeEditor
168+
language="json"
169+
value={value}
170+
onChange={onChange}
171+
className="nopan"
172+
fontSize={8}
173+
/>
174+
</div>
175+
{jsonError && (
176+
<div className="text-xs text-red-400">
177+
{jsonError.line && jsonError.column
178+
? `Invalid JSON (${jsonError.line}:${jsonError.column}) — ${jsonError.message}`
179+
: `Invalid JSON — ${jsonError.message}`}
180+
</div>
181+
)}
131182
</div>
132183
)}
133184
</div>

skyvern-frontend/src/routes/workflows/editor/FlowRenderer.tsx

Lines changed: 25 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -79,6 +79,8 @@ import {
7979
getWorkflowSettings,
8080
layout,
8181
} from "./workflowEditorUtils";
82+
import { getWorkflowErrors } from "./workflowEditorUtils";
83+
import { toast } from "@/components/ui/use-toast";
8284
import { useAutoPan } from "./useAutoPan";
8385

8486
const nextTick = () => new Promise((resolve) => setTimeout(resolve, 0));
@@ -373,8 +375,25 @@ function FlowRenderer({
373375
setGetSaveDataRef.current(constructSaveData);
374376
}, [constructSaveData]);
375377

376-
async function handleSave() {
377-
return await saveWorkflow.mutateAsync();
378+
async function handleSave(): Promise<boolean> {
379+
// Validate before saving; block if any workflow errors exist
380+
const errors = getWorkflowErrors(nodes);
381+
if (errors.length > 0) {
382+
toast({
383+
title: "Can not save workflow because of errors:",
384+
description: (
385+
<div className="space-y-2">
386+
{errors.map((error) => (
387+
<p key={error}>{error}</p>
388+
))}
389+
</div>
390+
),
391+
variant: "destructive",
392+
});
393+
return false;
394+
}
395+
await saveWorkflow.mutateAsync();
396+
return true;
378397
}
379398

380399
function deleteNode(id: string) {
@@ -605,8 +624,10 @@ function FlowRenderer({
605624
</Button>
606625
<Button
607626
onClick={() => {
608-
handleSave().then(() => {
609-
blocker.proceed?.();
627+
handleSave().then((ok) => {
628+
if (ok) {
629+
blocker.proceed?.();
630+
}
610631
});
611632
}}
612633
disabled={workflowChangesStore.saveIsPending}

skyvern-frontend/src/routes/workflows/editor/workflowEditorUtils.ts

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2186,6 +2186,14 @@ function getWorkflowErrors(nodes: Array<AppNode>): Array<string> {
21862186
} catch {
21872187
errors.push(`${node.data.label}: Error messages is not valid JSON.`);
21882188
}
2189+
// Validate Task data schema JSON when enabled (value different from "null")
2190+
if (node.data.dataSchema && node.data.dataSchema !== "null") {
2191+
try {
2192+
JSON.parse(node.data.dataSchema);
2193+
} catch {
2194+
errors.push(`${node.data.label}: Data schema is not valid JSON.`);
2195+
}
2196+
}
21892197
});
21902198

21912199
const validationNodes = nodes.filter(isValidationNode);
@@ -2217,6 +2225,14 @@ function getWorkflowErrors(nodes: Array<AppNode>): Array<string> {
22172225
if (node.data.dataExtractionGoal.length === 0) {
22182226
errors.push(`${node.data.label}: Data extraction goal is required.`);
22192227
}
2228+
// Validate Extraction data schema JSON when enabled (value different from "null")
2229+
if (node.data.dataSchema && node.data.dataSchema !== "null") {
2230+
try {
2231+
JSON.parse(node.data.dataSchema);
2232+
} catch {
2233+
errors.push(`${node.data.label}: Data schema is not valid JSON.`);
2234+
}
2235+
}
22202236
});
22212237

22222238
const textPromptNodes = nodes.filter(isTextPromptNode);

0 commit comments

Comments
 (0)