mirror of
https://github.com/Skyvern-AI/skyvern.git
synced 2026-04-26 10:41:14 +00:00
SKY-8879 copilot-stack/01: hygiene carve-outs (#5489)
This commit is contained in:
parent
ff198cb6f5
commit
5dcc8d8a51
15 changed files with 691 additions and 197 deletions
|
|
@ -42,7 +42,7 @@ Inputs:
|
|||
6. **Max Retries *(optional):*** the number of times a step can be retried if it fails
|
||||
7. **Complete on Download *(optional):*** Allows Skyvern to complete the task after a file has been downloaded
|
||||
8. **File Suffix *(optional):*** an identifier attached to the downloaded file
|
||||
9. **TOTP URL and TOTP Identifier *(optional):*** if you have an internal system where you can store the 2FA TOTP code, this URL calls on this storage space. The identifier allows you to link the code to the task, this is critical if you are running multiple tasks concurrently. [Contact us](https://www.skyvern.com/contact)
|
||||
9. **TOTP URL and TOTP Identifier *(optional):*** if you have an internal system where you can store the 2FA TOTP code, this URL calls on this storage space. The identifier allows you to link the code to the task, this is critical if you are running multiple tasks concurrently. [Contact us](https://www.skyvern.com/contact) if you would like to set up 2FA retreival in your workflows.
|
||||
10. **Parameters *(optional):*** parameters are self-defined placeholders that are specified run-to-run. They can either be workflow parameters, passed in via an API call, or output parameters, extracted from a previous task block. If specified, they are used by Skyvern to assist in the navigation, to fill out forms or further influence what actions to take on a website.
|
||||
|
||||
|
||||
|
|
@ -78,7 +78,7 @@ Iterate over something such as a CSV or the output of a previous block. The bloc
|
|||
Inputs:
|
||||
|
||||
1. **Loop Value *(required):*** This is the value that the loop will iterate over. For instance, if you have for every invoice ID, do X, invoice ID would be the value for this input.
|
||||
* Please [contact us](https://www.skyvern.com/contact)
|
||||
* Please [contact us](https://www.skyvern.com/contact) if you would like to add a loop block. Since we're in beta, the loop value needs to be parameterized from the backend.
|
||||
2. **Another block nested within the loop (required)**
|
||||
|
||||
|
||||
|
|
@ -222,7 +222,7 @@ Inputs:
|
|||
|
||||
1. **Recipients *(required):*** a list of people who will receive the email separated by commas
|
||||
2. **Subject/Body *(optional):*** the header and body of an email
|
||||
3. **File attachments *(optional):*** since we’re still in beta, you will need to [contact us](https://www.skyvern.com/contact)
|
||||
3. **File attachments *(optional):*** since we’re still in beta, you will need to [contact us](https://www.skyvern.com/contact) to upload attachments to the email
|
||||
|
||||
## FileParserBlock
|
||||
|
||||
|
|
@ -240,4 +240,4 @@ Downloads and parses a file to be used within other workflow blocks.
|
|||
Inputs:
|
||||
|
||||
1. **File URL *(required):*** This block allows you to use CSV, TSV, Excel, and PDF files within your workflow.
|
||||
* Since we’re still in beta, you will need to [contact us](https://www.skyvern.com/contact)
|
||||
* Since we’re still in beta, you will need to [contact us](https://www.skyvern.com/contact) to load a value into this block
|
||||
|
|
|
|||
|
|
@ -1,9 +1,9 @@
|
|||
import CodeMirror, { EditorView } from "@uiw/react-codemirror";
|
||||
import CodeMirror, { EditorView, type Extension } from "@uiw/react-codemirror";
|
||||
import { json } from "@codemirror/lang-json";
|
||||
import { python } from "@codemirror/lang-python";
|
||||
import { html } from "@codemirror/lang-html";
|
||||
import { tokyoNightStorm } from "@uiw/codemirror-theme-tokyo-night-storm";
|
||||
import { useEffect, useRef, useState } from "react";
|
||||
import { useEffect, useMemo, useRef, useState } from "react";
|
||||
import { cn } from "@/util/utils";
|
||||
import { useDebouncedCallback } from "use-debounce";
|
||||
|
||||
|
|
@ -31,6 +31,13 @@ type Props = {
|
|||
className?: string;
|
||||
fontSize?: number;
|
||||
fullHeight?: boolean;
|
||||
/**
|
||||
* Additional CodeMirror extensions. Useful for per-use-case concerns
|
||||
* like linting — e.g. the error_code_mapping editor passes a linter
|
||||
* that flags whitespace-bearing keys inline on the offending line.
|
||||
* Pass a stable (e.g. module-level) reference to avoid editor churn.
|
||||
*/
|
||||
extraExtensions?: Extension[];
|
||||
} & Pick<React.HTMLAttributes<HTMLDivElement>, "aria-required">;
|
||||
|
||||
const fullHeightExtension = EditorView.theme({
|
||||
|
|
@ -49,6 +56,7 @@ function CodeEditor({
|
|||
readOnly = false,
|
||||
fontSize = 12,
|
||||
fullHeight = false,
|
||||
extraExtensions,
|
||||
...restProps
|
||||
}: Props) {
|
||||
const viewRef = useRef<EditorView | null>(null);
|
||||
|
|
@ -67,13 +75,28 @@ function CodeEditor({
|
|||
debouncedOnChange(newValue);
|
||||
};
|
||||
|
||||
const extensions = language
|
||||
? [getLanguageExtension(language), lineWrap ? EditorView.lineWrapping : []]
|
||||
: [lineWrap ? EditorView.lineWrapping : []];
|
||||
// Memoize the extension tuple so React hands CodeMirror a stable
|
||||
// reference across renders. Without this, a parent re-render would
|
||||
// rebuild the array (and anything spread in) every cycle and trigger
|
||||
// unnecessary editor state reconfiguration.
|
||||
const extensions = useMemo<Extension[]>(() => {
|
||||
const exts: Extension[] = language
|
||||
? [
|
||||
getLanguageExtension(language),
|
||||
lineWrap ? EditorView.lineWrapping : [],
|
||||
]
|
||||
: [lineWrap ? EditorView.lineWrapping : []];
|
||||
if (extraExtensions) {
|
||||
exts.push(...extraExtensions);
|
||||
}
|
||||
if (fullHeight) {
|
||||
exts.push(fullHeightExtension);
|
||||
}
|
||||
return exts;
|
||||
}, [language, lineWrap, extraExtensions, fullHeight]);
|
||||
|
||||
const style: React.CSSProperties = { fontSize };
|
||||
if (fullHeight) {
|
||||
extensions.push(fullHeightExtension);
|
||||
style.height = "100%";
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -0,0 +1,52 @@
|
|||
import { lintGutter } from "@codemirror/lint";
|
||||
import type { Extension } from "@uiw/react-codemirror";
|
||||
|
||||
import { CodeEditor } from "@/routes/workflows/components/CodeEditor";
|
||||
|
||||
import { errorCodeMappingLinter } from "./errorCodeMappingLinter";
|
||||
import { ErrorCodeMappingValidation } from "./ErrorCodeMappingValidation";
|
||||
|
||||
// Module-level constant so React does not see a fresh array (and a fresh
|
||||
// `lintGutter()` instance) on every render. Passing a new extension tuple
|
||||
// into CodeMirror each cycle would trigger unnecessary editor-state churn.
|
||||
const EXTRA_EXTENSIONS: Extension[] = [errorCodeMappingLinter, lintGutter()];
|
||||
|
||||
/**
|
||||
* Thin wrapper around `CodeEditor` that adds two things for authoring
|
||||
* `error_code_mapping` JSON:
|
||||
*
|
||||
* 1. An inline CodeMirror linter that draws a squiggly underline on the
|
||||
* exact character range of any key with surrounding whitespace, plus a
|
||||
* gutter marker and a hover tooltip explaining the problem.
|
||||
* 2. A persistent summary box below the editor listing every problem the
|
||||
* save-time validator reports (parse errors, wrong shape, whitespace
|
||||
* keys) — same text the save-time toast shows.
|
||||
*
|
||||
* Used by every block type that edits error_code_mapping: task, validation,
|
||||
* action, navigation, login, file_download.
|
||||
*/
|
||||
export function ErrorCodeMappingEditor({
|
||||
label,
|
||||
value,
|
||||
onChange,
|
||||
readOnly = false,
|
||||
}: {
|
||||
label: string;
|
||||
value: string;
|
||||
onChange: (value: string) => void;
|
||||
readOnly?: boolean;
|
||||
}) {
|
||||
return (
|
||||
<div>
|
||||
<CodeEditor
|
||||
language="json"
|
||||
value={value}
|
||||
onChange={readOnly ? undefined : onChange}
|
||||
className="nopan"
|
||||
readOnly={readOnly}
|
||||
extraExtensions={EXTRA_EXTENSIONS}
|
||||
/>
|
||||
<ErrorCodeMappingValidation label={label} value={value} />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
|
@ -0,0 +1,55 @@
|
|||
import { ExclamationTriangleIcon } from "@radix-ui/react-icons";
|
||||
import { useMemo } from "react";
|
||||
|
||||
import { validateErrorCodeMapping } from "./validateErrorCodeMapping";
|
||||
|
||||
/**
|
||||
* Inline validation display for the error_code_mapping CodeEditor. Renders
|
||||
* the same messages that `getWorkflowErrors` produces at save time, directly
|
||||
* below the editor, so the user sees problems next to the field instead of
|
||||
* only in the destructive toast after clicking Save.
|
||||
*
|
||||
* Label-prefix ("block_X: ") is stripped so the message reads naturally
|
||||
* under the field. The component renders nothing when the value is clean.
|
||||
*/
|
||||
export function ErrorCodeMappingValidation({
|
||||
label,
|
||||
value,
|
||||
}: {
|
||||
label: string;
|
||||
value: string;
|
||||
}) {
|
||||
// Memoize so large mappings don't re-validate on every parent render —
|
||||
// only when the value (or label used for error-prefix stripping)
|
||||
// actually changes. `"null"` is the disabled sentinel and skips the
|
||||
// validator entirely.
|
||||
const errors = useMemo(
|
||||
() => (value === "null" ? [] : validateErrorCodeMapping(label, value)),
|
||||
[label, value],
|
||||
);
|
||||
if (errors.length === 0) {
|
||||
return null;
|
||||
}
|
||||
const prefix = `${label}: `;
|
||||
const strip = (err: string) =>
|
||||
err.startsWith(prefix) ? err.slice(prefix.length) : err;
|
||||
return (
|
||||
<div className="mb-2 mt-1 flex items-start gap-1 rounded-md border border-red-500/40 bg-red-500/10 p-2 text-xs text-red-400">
|
||||
<ExclamationTriangleIcon className="mt-0.5 h-3 w-3 shrink-0" />
|
||||
{errors.length === 1 ? (
|
||||
<div className="flex-1">{strip(errors[0]!)}</div>
|
||||
) : (
|
||||
<div className="flex-1">
|
||||
<div className="font-medium">
|
||||
{errors.length} problems with Error Messages
|
||||
</div>
|
||||
<ul className="mt-1 list-disc pl-4">
|
||||
{errors.map((err) => (
|
||||
<li key={err}>{strip(err)}</li>
|
||||
))}
|
||||
</ul>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
|
@ -0,0 +1,102 @@
|
|||
import { describe, expect, test } from "vitest";
|
||||
|
||||
import { scanJsonKeys } from "./errorCodeMappingLinter";
|
||||
|
||||
describe("scanJsonKeys", () => {
|
||||
test("returns empty array for empty source", () => {
|
||||
expect(scanJsonKeys("")).toEqual([]);
|
||||
});
|
||||
|
||||
test("returns empty array for non-JSON garbage", () => {
|
||||
expect(scanJsonKeys("not json at all")).toEqual([]);
|
||||
});
|
||||
|
||||
test("single clean key is found with correct offsets", () => {
|
||||
const source = `{ "FOO": "bar" }`;
|
||||
const keys = scanJsonKeys(source);
|
||||
expect(keys).toHaveLength(1);
|
||||
expect(keys[0]!.raw).toBe("FOO");
|
||||
expect(source.slice(keys[0]!.from, keys[0]!.to)).toBe('"FOO"');
|
||||
});
|
||||
|
||||
test("multiple keys across lines are all found", () => {
|
||||
const source = `{\n "FOO": "a",\n "BAR": "b"\n}`;
|
||||
const keys = scanJsonKeys(source);
|
||||
expect(keys).toHaveLength(2);
|
||||
expect(keys.map((k) => k.raw)).toEqual(["FOO", "BAR"]);
|
||||
});
|
||||
|
||||
test("whitespace-bearing key is captured verbatim (with the space)", () => {
|
||||
const source = `{ " FOO": "bar" }`;
|
||||
const keys = scanJsonKeys(source);
|
||||
expect(keys).toHaveLength(1);
|
||||
expect(keys[0]!.raw).toBe(" FOO");
|
||||
// The linter can now flag it via `raw !== raw.trim()`.
|
||||
expect(keys[0]!.raw !== keys[0]!.raw.trim()).toBe(true);
|
||||
});
|
||||
|
||||
test("string values (not keys) are NOT mis-identified as keys", () => {
|
||||
// The value "BAR" is a string literal but is not followed by a colon,
|
||||
// so the regex lookahead `(?=\s*:)` must skip it.
|
||||
const source = `{ "FOO": "BAR" }`;
|
||||
const keys = scanJsonKeys(source);
|
||||
expect(keys.map((k) => k.raw)).toEqual(["FOO"]);
|
||||
});
|
||||
|
||||
test("escaped quotes inside keys are handled", () => {
|
||||
// Key literal is "\"weird\"": "val"
|
||||
const source = `{ "\\"weird\\"": "val" }`;
|
||||
const keys = scanJsonKeys(source);
|
||||
expect(keys).toHaveLength(1);
|
||||
expect(keys[0]!.raw).toBe('"weird"');
|
||||
});
|
||||
|
||||
test("key offsets point at the opening quote", () => {
|
||||
const source = `{ " FOO": "bar" }`;
|
||||
const keys = scanJsonKeys(source);
|
||||
expect(source[keys[0]!.from]).toBe('"');
|
||||
expect(source[keys[0]!.to - 1]).toBe('"');
|
||||
});
|
||||
|
||||
test("tab-prefixed key is captured with the tab", () => {
|
||||
const source = `{ "\\tFOO": "bar" }`;
|
||||
const keys = scanJsonKeys(source);
|
||||
expect(keys[0]!.raw).toBe("\tFOO");
|
||||
expect(keys[0]!.raw !== keys[0]!.raw.trim()).toBe(true);
|
||||
});
|
||||
|
||||
test("nested object keys are NOT reported (scope matches save-time)", () => {
|
||||
// `validateErrorCodeMapping` only iterates top-level keys, so the
|
||||
// linter must match that scope — otherwise a nested whitespace key
|
||||
// would get an inline squiggle but save cleanly.
|
||||
const source = `{\n "ERR": {\n " BAD": "x",\n "GOOD": "y"\n },\n " TOP_BAD": "z"\n}`;
|
||||
const keys = scanJsonKeys(source);
|
||||
expect(keys.map((k) => k.raw)).toEqual(["ERR", " TOP_BAD"]);
|
||||
});
|
||||
|
||||
test("keys inside array values are NOT reported", () => {
|
||||
const source = `{ "items": [{ "inner": "v" }] }`;
|
||||
const keys = scanJsonKeys(source);
|
||||
expect(keys.map((k) => k.raw)).toEqual(["items"]);
|
||||
});
|
||||
|
||||
test("braces inside string values do not confuse depth tracking", () => {
|
||||
// The `}` inside the value would break a naive depth counter.
|
||||
const source = `{ "FOO": "some { weird } value", "BAR": "b" }`;
|
||||
const keys = scanJsonKeys(source);
|
||||
expect(keys.map((k) => k.raw)).toEqual(["FOO", "BAR"]);
|
||||
});
|
||||
|
||||
test("escaped quotes inside string values do not confuse depth tracking", () => {
|
||||
const source = `{ "FOO": "has \\"quoted\\" text", "BAR": "b" }`;
|
||||
const keys = scanJsonKeys(source);
|
||||
expect(keys.map((k) => k.raw)).toEqual(["FOO", "BAR"]);
|
||||
});
|
||||
|
||||
test("unterminated string stops the scan gracefully", () => {
|
||||
const source = `{ "FOO": "unterminated`;
|
||||
// Should not throw, should return whatever it found before the
|
||||
// unterminated string.
|
||||
expect(() => scanJsonKeys(source)).not.toThrow();
|
||||
});
|
||||
});
|
||||
|
|
@ -0,0 +1,134 @@
|
|||
import { linter, type Diagnostic } from "@codemirror/lint";
|
||||
|
||||
/**
|
||||
* Scan a JSON string for **top-level** object-key literals (`"KEY":` where
|
||||
* KEY sits directly inside the outermost `{…}`) and return the `[from, to)`
|
||||
* character range + parsed content for each one.
|
||||
*
|
||||
* Scope is restricted to the top level on purpose: save-time validation
|
||||
* (`validateErrorCodeMapping`) only iterates `Object.keys(parsed)` at depth
|
||||
* 1, so the inline linter must use the same scope — otherwise a nested key
|
||||
* like `{"ERR": {" BAD": "x"}}` would get an in-editor squiggle but save
|
||||
* cleanly, which is worse than no diagnostic at all. Codex review on
|
||||
* #10116.
|
||||
*
|
||||
* This is a hand-rolled character walk rather than a full JSON parse
|
||||
* because we need character positions even when the JSON is mid-edit and
|
||||
* not yet well-formed. The walker tracks brace depth (ignoring braces
|
||||
* inside string literals) and only emits a key when the brace depth
|
||||
* transition lands at depth 1.
|
||||
*
|
||||
* Return type: `from`/`to` are offsets into the source string; `raw` is the
|
||||
* literal with its surrounding quotes stripped AND escape sequences
|
||||
* unescaped, so `" FOO"` becomes ` FOO` ready for a `trim()` check.
|
||||
*/
|
||||
export function scanJsonKeys(
|
||||
source: string,
|
||||
): Array<{ from: number; to: number; raw: string }> {
|
||||
const keys: Array<{ from: number; to: number; raw: string }> = [];
|
||||
let depth = 0;
|
||||
let i = 0;
|
||||
const len = source.length;
|
||||
|
||||
while (i < len) {
|
||||
const ch = source[i]!;
|
||||
|
||||
if (ch === "{") {
|
||||
depth++;
|
||||
i++;
|
||||
continue;
|
||||
}
|
||||
if (ch === "}") {
|
||||
depth--;
|
||||
i++;
|
||||
continue;
|
||||
}
|
||||
if (ch !== '"') {
|
||||
i++;
|
||||
continue;
|
||||
}
|
||||
|
||||
// At a string-literal opener. Walk to the matching closing quote,
|
||||
// honoring backslash escapes.
|
||||
const stringStart = i;
|
||||
i++;
|
||||
while (i < len) {
|
||||
const c = source[i]!;
|
||||
if (c === "\\") {
|
||||
i += 2;
|
||||
continue;
|
||||
}
|
||||
if (c === '"') {
|
||||
break;
|
||||
}
|
||||
i++;
|
||||
}
|
||||
if (i >= len) {
|
||||
// Unterminated string — stop scanning; nothing sensible left to
|
||||
// report.
|
||||
break;
|
||||
}
|
||||
const stringEnd = i + 1; // include the closing quote
|
||||
i++;
|
||||
|
||||
// This string counts as an object key iff:
|
||||
// - the containing brace depth is exactly 1 (we are inside the
|
||||
// outermost `{`), and
|
||||
// - the next non-whitespace character is a colon.
|
||||
if (depth !== 1) {
|
||||
continue;
|
||||
}
|
||||
let j = i;
|
||||
while (j < len && /\s/.test(source[j]!)) {
|
||||
j++;
|
||||
}
|
||||
if (source[j] !== ":") {
|
||||
continue;
|
||||
}
|
||||
|
||||
const literal = source.slice(stringStart, stringEnd);
|
||||
let raw: string;
|
||||
try {
|
||||
raw = JSON.parse(literal);
|
||||
} catch {
|
||||
// Shouldn't happen — we just walked a well-formed JSON string
|
||||
// literal — but fall back to stripping the surrounding quotes.
|
||||
raw = literal.slice(1, -1);
|
||||
}
|
||||
keys.push({ from: stringStart, to: stringEnd, raw });
|
||||
}
|
||||
|
||||
return keys;
|
||||
}
|
||||
|
||||
/**
|
||||
* CodeMirror linter extension for the `error_code_mapping` editor. Emits a
|
||||
* Diagnostic on every key literal whose content has surrounding whitespace,
|
||||
* rendered as a squiggly underline on the exact character range plus a
|
||||
* gutter marker + hover tooltip. This is the in-editor analogue of the
|
||||
* summary box rendered by ErrorCodeMappingValidation — same problem set,
|
||||
* pinpointed to the offending line.
|
||||
*
|
||||
* Parse errors are NOT reported here — CodeMirror's built-in JSON parser
|
||||
* already highlights syntactic errors. We only add the semantic layer
|
||||
* (whitespace-bearing keys) on top.
|
||||
*/
|
||||
export const errorCodeMappingLinter = linter((view) => {
|
||||
const diagnostics: Diagnostic[] = [];
|
||||
const source = view.state.doc.toString();
|
||||
if (!source || source === "null") {
|
||||
return diagnostics;
|
||||
}
|
||||
for (const key of scanJsonKeys(source)) {
|
||||
if (key.raw !== key.raw.trim()) {
|
||||
diagnostics.push({
|
||||
from: key.from,
|
||||
to: key.to,
|
||||
severity: "error",
|
||||
message: `Key "${key.raw}" has surrounding whitespace — remove it or this error code will never match at runtime.`,
|
||||
source: "error_code_mapping",
|
||||
});
|
||||
}
|
||||
}
|
||||
return diagnostics;
|
||||
});
|
||||
|
|
@ -12,9 +12,8 @@ import { Handle, NodeProps, Position, useEdges, useNodes } from "@xyflow/react";
|
|||
import { useState } from "react";
|
||||
import type { ActionNode } from "./types";
|
||||
import { HelpTooltip } from "@/components/HelpTooltip";
|
||||
import { Checkbox } from "@/components/ui/checkbox";
|
||||
import { errorMappingExampleValue } from "../types";
|
||||
import { CodeEditor } from "@/routes/workflows/components/CodeEditor";
|
||||
import { ErrorCodeMappingEditor } from "@/routes/workflows/editor/ErrorCodeMappingEditor";
|
||||
import { Switch } from "@/components/ui/switch";
|
||||
import { placeholders, helpTooltips } from "../../helpContent";
|
||||
import { AI_IMPROVE_CONFIGS } from "../../constants";
|
||||
|
|
@ -212,7 +211,7 @@ function ActionNode({ id, data, type }: NodeProps<ActionNode>) {
|
|||
/>
|
||||
</div>
|
||||
<div className="space-y-2">
|
||||
<div className="flex gap-4">
|
||||
<div className="flex items-center justify-between">
|
||||
<div className="flex gap-2">
|
||||
<Label className="text-xs font-normal text-slate-300">
|
||||
Error Messages
|
||||
|
|
@ -221,40 +220,35 @@ function ActionNode({ id, data, type }: NodeProps<ActionNode>) {
|
|||
content={helpTooltips["action"]["errorCodeMapping"]}
|
||||
/>
|
||||
</div>
|
||||
<Checkbox
|
||||
checked={data.errorCodeMapping !== "null"}
|
||||
disabled={!editable}
|
||||
onCheckedChange={(checked) => {
|
||||
if (!editable) {
|
||||
return;
|
||||
}
|
||||
update({
|
||||
errorCodeMapping: checked
|
||||
? JSON.stringify(
|
||||
errorMappingExampleValue,
|
||||
null,
|
||||
2,
|
||||
)
|
||||
: "null",
|
||||
});
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
{data.errorCodeMapping !== "null" && (
|
||||
<div>
|
||||
<CodeEditor
|
||||
language="json"
|
||||
value={data.errorCodeMapping}
|
||||
onChange={(value) => {
|
||||
<div className="w-52">
|
||||
<Switch
|
||||
checked={data.errorCodeMapping !== "null"}
|
||||
onCheckedChange={(checked) => {
|
||||
if (!editable) {
|
||||
return;
|
||||
}
|
||||
update({ errorCodeMapping: value });
|
||||
update({
|
||||
errorCodeMapping: checked
|
||||
? JSON.stringify(
|
||||
errorMappingExampleValue,
|
||||
null,
|
||||
2,
|
||||
)
|
||||
: "null",
|
||||
});
|
||||
}}
|
||||
className="nopan"
|
||||
fontSize={8}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
{data.errorCodeMapping !== "null" && (
|
||||
<ErrorCodeMappingEditor
|
||||
label={data.label}
|
||||
value={data.errorCodeMapping}
|
||||
onChange={(value) => {
|
||||
update({ errorCodeMapping: value });
|
||||
}}
|
||||
readOnly={!editable}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
<BlockExecutionOptions
|
||||
|
|
|
|||
|
|
@ -7,13 +7,13 @@ import {
|
|||
AccordionItem,
|
||||
AccordionTrigger,
|
||||
} from "@/components/ui/accordion";
|
||||
import { Checkbox } from "@/components/ui/checkbox";
|
||||
import { Input } from "@/components/ui/input";
|
||||
import { Label } from "@/components/ui/label";
|
||||
import { Switch } from "@/components/ui/switch";
|
||||
import { Separator } from "@/components/ui/separator";
|
||||
import { WorkflowBlockInputTextarea } from "@/components/WorkflowBlockInputTextarea";
|
||||
import { BlockCodeEditor } from "@/routes/workflows/components/BlockCodeEditor";
|
||||
import { CodeEditor } from "@/routes/workflows/components/CodeEditor";
|
||||
import { ErrorCodeMappingEditor } from "@/routes/workflows/editor/ErrorCodeMappingEditor";
|
||||
import { useBlockScriptStore } from "@/store/BlockScriptStore";
|
||||
import { Handle, NodeProps, Position, useEdges, useNodes } from "@xyflow/react";
|
||||
import { useState } from "react";
|
||||
|
|
@ -248,7 +248,7 @@ function FileDownloadNode({ id, data }: NodeProps<FileDownloadNode>) {
|
|||
/>
|
||||
</div>
|
||||
<div className="space-y-2">
|
||||
<div className="flex gap-4">
|
||||
<div className="flex items-center justify-between">
|
||||
<div className="flex gap-2">
|
||||
<Label className="text-xs font-normal text-slate-300">
|
||||
Error Messages
|
||||
|
|
@ -257,34 +257,35 @@ function FileDownloadNode({ id, data }: NodeProps<FileDownloadNode>) {
|
|||
content={helpTooltips["download"]["errorCodeMapping"]}
|
||||
/>
|
||||
</div>
|
||||
<Checkbox
|
||||
checked={data.errorCodeMapping !== "null"}
|
||||
disabled={!editable}
|
||||
onCheckedChange={(checked) => {
|
||||
update({
|
||||
errorCodeMapping: checked
|
||||
? JSON.stringify(
|
||||
errorMappingExampleValue,
|
||||
null,
|
||||
2,
|
||||
)
|
||||
: "null",
|
||||
});
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
{data.errorCodeMapping !== "null" && (
|
||||
<div>
|
||||
<CodeEditor
|
||||
language="json"
|
||||
value={data.errorCodeMapping}
|
||||
onChange={(value) => {
|
||||
update({ errorCodeMapping: value });
|
||||
<div className="w-52">
|
||||
<Switch
|
||||
checked={data.errorCodeMapping !== "null"}
|
||||
onCheckedChange={(checked) => {
|
||||
if (!editable) {
|
||||
return;
|
||||
}
|
||||
update({
|
||||
errorCodeMapping: checked
|
||||
? JSON.stringify(
|
||||
errorMappingExampleValue,
|
||||
null,
|
||||
2,
|
||||
)
|
||||
: "null",
|
||||
});
|
||||
}}
|
||||
className="nopan"
|
||||
fontSize={8}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
{data.errorCodeMapping !== "null" && (
|
||||
<ErrorCodeMappingEditor
|
||||
label={data.label}
|
||||
value={data.errorCodeMapping}
|
||||
onChange={(value) => {
|
||||
update({ errorCodeMapping: value });
|
||||
}}
|
||||
readOnly={!editable}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
<BlockExecutionOptions
|
||||
|
|
|
|||
|
|
@ -7,13 +7,13 @@ import {
|
|||
AccordionItem,
|
||||
AccordionTrigger,
|
||||
} from "@/components/ui/accordion";
|
||||
import { Checkbox } from "@/components/ui/checkbox";
|
||||
import { Input } from "@/components/ui/input";
|
||||
import { Label } from "@/components/ui/label";
|
||||
import { Switch } from "@/components/ui/switch";
|
||||
import { Separator } from "@/components/ui/separator";
|
||||
import { WorkflowBlockInputTextarea } from "@/components/WorkflowBlockInputTextarea";
|
||||
import { BlockCodeEditor } from "@/routes/workflows/components/BlockCodeEditor";
|
||||
import { CodeEditor } from "@/routes/workflows/components/CodeEditor";
|
||||
import { ErrorCodeMappingEditor } from "@/routes/workflows/editor/ErrorCodeMappingEditor";
|
||||
import { useBlockScriptStore } from "@/store/BlockScriptStore";
|
||||
import { Handle, NodeProps, Position, useEdges, useNodes } from "@xyflow/react";
|
||||
import { helpTooltips, placeholders } from "../../helpContent";
|
||||
|
|
@ -258,7 +258,7 @@ function LoginNode({ id, data, type }: NodeProps<LoginNode>) {
|
|||
/>
|
||||
</div>
|
||||
<div className="space-y-2">
|
||||
<div className="flex gap-4">
|
||||
<div className="flex items-center justify-between">
|
||||
<div className="flex gap-2">
|
||||
<Label className="text-xs font-normal text-slate-300">
|
||||
Error Messages
|
||||
|
|
@ -267,34 +267,35 @@ function LoginNode({ id, data, type }: NodeProps<LoginNode>) {
|
|||
content={helpTooltips["login"]["errorCodeMapping"]}
|
||||
/>
|
||||
</div>
|
||||
<Checkbox
|
||||
checked={data.errorCodeMapping !== "null"}
|
||||
disabled={!editable}
|
||||
onCheckedChange={(checked) => {
|
||||
update({
|
||||
errorCodeMapping: checked
|
||||
? JSON.stringify(
|
||||
errorMappingExampleValue,
|
||||
null,
|
||||
2,
|
||||
)
|
||||
: "null",
|
||||
});
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
{data.errorCodeMapping !== "null" && (
|
||||
<div>
|
||||
<CodeEditor
|
||||
language="json"
|
||||
value={data.errorCodeMapping}
|
||||
onChange={(value) => {
|
||||
update({ errorCodeMapping: value });
|
||||
<div className="w-52">
|
||||
<Switch
|
||||
checked={data.errorCodeMapping !== "null"}
|
||||
onCheckedChange={(checked) => {
|
||||
if (!editable) {
|
||||
return;
|
||||
}
|
||||
update({
|
||||
errorCodeMapping: checked
|
||||
? JSON.stringify(
|
||||
errorMappingExampleValue,
|
||||
null,
|
||||
2,
|
||||
)
|
||||
: "null",
|
||||
});
|
||||
}}
|
||||
className="nopan"
|
||||
fontSize={8}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
{data.errorCodeMapping !== "null" && (
|
||||
<ErrorCodeMappingEditor
|
||||
label={data.label}
|
||||
value={data.errorCodeMapping}
|
||||
onChange={(value) => {
|
||||
update({ errorCodeMapping: value });
|
||||
}}
|
||||
readOnly={!editable}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
<BlockExecutionOptions
|
||||
|
|
|
|||
|
|
@ -7,7 +7,6 @@ import {
|
|||
AccordionItem,
|
||||
AccordionTrigger,
|
||||
} from "@/components/ui/accordion";
|
||||
import { Checkbox } from "@/components/ui/checkbox";
|
||||
import { Input } from "@/components/ui/input";
|
||||
import { Label } from "@/components/ui/label";
|
||||
import { Separator } from "@/components/ui/separator";
|
||||
|
|
@ -16,7 +15,7 @@ import { WorkflowBlockInput } from "@/components/WorkflowBlockInput";
|
|||
import { WorkflowBlockInputTextarea } from "@/components/WorkflowBlockInputTextarea";
|
||||
import { useRerender } from "@/hooks/useRerender";
|
||||
import { BlockCodeEditor } from "@/routes/workflows/components/BlockCodeEditor";
|
||||
import { CodeEditor } from "@/routes/workflows/components/CodeEditor";
|
||||
import { ErrorCodeMappingEditor } from "@/routes/workflows/editor/ErrorCodeMappingEditor";
|
||||
import { useBlockScriptStore } from "@/store/BlockScriptStore";
|
||||
import { Handle, NodeProps, Position, useEdges, useNodes } from "@xyflow/react";
|
||||
import { useState } from "react";
|
||||
|
|
@ -399,7 +398,7 @@ function NavigationNode({ id, data, type }: NodeProps<NavigationNode>) {
|
|||
/>
|
||||
</div>
|
||||
<div className="space-y-2">
|
||||
<div className="flex gap-4">
|
||||
<div className="flex items-center justify-between">
|
||||
<div className="flex gap-2">
|
||||
<Label className="text-xs font-normal text-slate-300">
|
||||
Error Messages
|
||||
|
|
@ -408,30 +407,31 @@ function NavigationNode({ id, data, type }: NodeProps<NavigationNode>) {
|
|||
content={helpTooltips["navigation"]["errorCodeMapping"]}
|
||||
/>
|
||||
</div>
|
||||
<Checkbox
|
||||
checked={data.errorCodeMapping !== "null"}
|
||||
disabled={!editable}
|
||||
onCheckedChange={(checked) => {
|
||||
update({
|
||||
errorCodeMapping: checked
|
||||
? JSON.stringify(errorMappingExampleValue, null, 2)
|
||||
: "null",
|
||||
});
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
{data.errorCodeMapping !== "null" && (
|
||||
<div>
|
||||
<CodeEditor
|
||||
language="json"
|
||||
value={data.errorCodeMapping}
|
||||
onChange={(value) => {
|
||||
update({ errorCodeMapping: value });
|
||||
<div className="w-52">
|
||||
<Switch
|
||||
checked={data.errorCodeMapping !== "null"}
|
||||
onCheckedChange={(checked) => {
|
||||
if (!editable) {
|
||||
return;
|
||||
}
|
||||
update({
|
||||
errorCodeMapping: checked
|
||||
? JSON.stringify(errorMappingExampleValue, null, 2)
|
||||
: "null",
|
||||
});
|
||||
}}
|
||||
className="nopan"
|
||||
fontSize={8}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
{data.errorCodeMapping !== "null" && (
|
||||
<ErrorCodeMappingEditor
|
||||
label={data.label}
|
||||
value={data.errorCodeMapping}
|
||||
onChange={(value) => {
|
||||
update({ errorCodeMapping: value });
|
||||
}}
|
||||
readOnly={!editable}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
<BlockExecutionOptions
|
||||
|
|
|
|||
|
|
@ -7,7 +7,6 @@ import {
|
|||
AccordionItem,
|
||||
AccordionTrigger,
|
||||
} from "@/components/ui/accordion";
|
||||
import { Checkbox } from "@/components/ui/checkbox";
|
||||
import { Input } from "@/components/ui/input";
|
||||
import { Label } from "@/components/ui/label";
|
||||
import { Separator } from "@/components/ui/separator";
|
||||
|
|
@ -15,7 +14,7 @@ import { Switch } from "@/components/ui/switch";
|
|||
import { WorkflowBlockInput } from "@/components/WorkflowBlockInput";
|
||||
import { WorkflowBlockInputTextarea } from "@/components/WorkflowBlockInputTextarea";
|
||||
import { BlockCodeEditor } from "@/routes/workflows/components/BlockCodeEditor";
|
||||
import { CodeEditor } from "@/routes/workflows/components/CodeEditor";
|
||||
import { ErrorCodeMappingEditor } from "@/routes/workflows/editor/ErrorCodeMappingEditor";
|
||||
import { useBlockScriptStore } from "@/store/BlockScriptStore";
|
||||
import { Handle, NodeProps, Position, useEdges, useNodes } from "@xyflow/react";
|
||||
import { useState } from "react";
|
||||
|
|
@ -284,7 +283,7 @@ function TaskNode({ id, data, type }: NodeProps<TaskNode>) {
|
|||
/>
|
||||
</div>
|
||||
<div className="space-y-2">
|
||||
<div className="flex gap-4">
|
||||
<div className="flex items-center justify-between">
|
||||
<div className="flex gap-2">
|
||||
<Label className="text-xs font-normal text-slate-300">
|
||||
Error Messages
|
||||
|
|
@ -293,34 +292,35 @@ function TaskNode({ id, data, type }: NodeProps<TaskNode>) {
|
|||
content={helpTooltips["task"]["errorCodeMapping"]}
|
||||
/>
|
||||
</div>
|
||||
<Checkbox
|
||||
checked={data.errorCodeMapping !== "null"}
|
||||
disabled={!editable}
|
||||
onCheckedChange={(checked) => {
|
||||
update({
|
||||
errorCodeMapping: checked
|
||||
? JSON.stringify(
|
||||
errorMappingExampleValue,
|
||||
null,
|
||||
2,
|
||||
)
|
||||
: "null",
|
||||
});
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
{data.errorCodeMapping !== "null" && (
|
||||
<div>
|
||||
<CodeEditor
|
||||
language="json"
|
||||
value={data.errorCodeMapping}
|
||||
onChange={(value) => {
|
||||
update({ errorCodeMapping: value });
|
||||
<div className="w-52">
|
||||
<Switch
|
||||
checked={data.errorCodeMapping !== "null"}
|
||||
onCheckedChange={(checked) => {
|
||||
if (!editable) {
|
||||
return;
|
||||
}
|
||||
update({
|
||||
errorCodeMapping: checked
|
||||
? JSON.stringify(
|
||||
errorMappingExampleValue,
|
||||
null,
|
||||
2,
|
||||
)
|
||||
: "null",
|
||||
});
|
||||
}}
|
||||
className="nopan"
|
||||
fontSize={8}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
{data.errorCodeMapping !== "null" && (
|
||||
<ErrorCodeMappingEditor
|
||||
label={data.label}
|
||||
value={data.errorCodeMapping}
|
||||
onChange={(value) => {
|
||||
update({ errorCodeMapping: value });
|
||||
}}
|
||||
readOnly={!editable}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
<BlockExecutionOptions
|
||||
|
|
|
|||
|
|
@ -7,12 +7,12 @@ import {
|
|||
AccordionItem,
|
||||
AccordionTrigger,
|
||||
} from "@/components/ui/accordion";
|
||||
import { Checkbox } from "@/components/ui/checkbox";
|
||||
import { Label } from "@/components/ui/label";
|
||||
import { Switch } from "@/components/ui/switch";
|
||||
import { Separator } from "@/components/ui/separator";
|
||||
import { WorkflowBlockInputTextarea } from "@/components/WorkflowBlockInputTextarea";
|
||||
import { BlockCodeEditor } from "@/routes/workflows/components/BlockCodeEditor";
|
||||
import { CodeEditor } from "@/routes/workflows/components/CodeEditor";
|
||||
import { ErrorCodeMappingEditor } from "@/routes/workflows/editor/ErrorCodeMappingEditor";
|
||||
import { useBlockScriptStore } from "@/store/BlockScriptStore";
|
||||
import { Handle, NodeProps, Position, useEdges, useNodes } from "@xyflow/react";
|
||||
import { useState } from "react";
|
||||
|
|
@ -158,7 +158,7 @@ function ValidationNode({ id, data, type }: NodeProps<ValidationNode>) {
|
|||
/>
|
||||
</div>
|
||||
<div className="space-y-2">
|
||||
<div className="flex gap-4">
|
||||
<div className="flex items-center justify-between">
|
||||
<div className="flex gap-2">
|
||||
<Label className="text-xs font-normal text-slate-300">
|
||||
Error Messages
|
||||
|
|
@ -169,40 +169,35 @@ function ValidationNode({ id, data, type }: NodeProps<ValidationNode>) {
|
|||
}
|
||||
/>
|
||||
</div>
|
||||
<Checkbox
|
||||
checked={data.errorCodeMapping !== "null"}
|
||||
disabled={!editable}
|
||||
onCheckedChange={(checked) => {
|
||||
if (!editable) {
|
||||
return;
|
||||
}
|
||||
update({
|
||||
errorCodeMapping: checked
|
||||
? JSON.stringify(
|
||||
errorMappingExampleValue,
|
||||
null,
|
||||
2,
|
||||
)
|
||||
: "null",
|
||||
});
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
{data.errorCodeMapping !== "null" && (
|
||||
<div>
|
||||
<CodeEditor
|
||||
language="json"
|
||||
value={data.errorCodeMapping}
|
||||
onChange={(value) => {
|
||||
<div className="w-52">
|
||||
<Switch
|
||||
checked={data.errorCodeMapping !== "null"}
|
||||
onCheckedChange={(checked) => {
|
||||
if (!editable) {
|
||||
return;
|
||||
}
|
||||
update({ errorCodeMapping: value });
|
||||
update({
|
||||
errorCodeMapping: checked
|
||||
? JSON.stringify(
|
||||
errorMappingExampleValue,
|
||||
null,
|
||||
2,
|
||||
)
|
||||
: "null",
|
||||
});
|
||||
}}
|
||||
className="nopan"
|
||||
fontSize={8}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
{data.errorCodeMapping !== "null" && (
|
||||
<ErrorCodeMappingEditor
|
||||
label={data.label}
|
||||
value={data.errorCodeMapping}
|
||||
onChange={(value) => {
|
||||
update({ errorCodeMapping: value });
|
||||
}}
|
||||
readOnly={!editable}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
<BlockExecutionOptions
|
||||
|
|
|
|||
|
|
@ -0,0 +1,80 @@
|
|||
import { describe, test, expect } from "vitest";
|
||||
import { validateErrorCodeMapping } from "./validateErrorCodeMapping";
|
||||
|
||||
describe("validateErrorCodeMapping", () => {
|
||||
test("clean keys return no errors", () => {
|
||||
const json = JSON.stringify({
|
||||
ACCOUNT_GROUP_NOT_FOUND: "if there are no results, terminate",
|
||||
EMPTY_TABLE: "stop",
|
||||
});
|
||||
expect(validateErrorCodeMapping("block_1", json)).toEqual([]);
|
||||
});
|
||||
|
||||
test("invalid JSON returns a parse error", () => {
|
||||
const result = validateErrorCodeMapping("block_1", "{not json");
|
||||
expect(result).toHaveLength(1);
|
||||
expect(result[0]).toContain("Error messages is not valid JSON");
|
||||
});
|
||||
|
||||
test("null value (mapping disabled) returns no errors", () => {
|
||||
expect(validateErrorCodeMapping("block_1", "null")).toEqual([]);
|
||||
});
|
||||
|
||||
test("leading whitespace in key is flagged", () => {
|
||||
const json = JSON.stringify({ " ACCOUNT_GROUP_NOT_FOUND": "terminate" });
|
||||
const result = validateErrorCodeMapping("block_42", json);
|
||||
expect(result).toHaveLength(1);
|
||||
expect(result[0]).toContain("block_42");
|
||||
expect(result[0]).toContain("surrounding whitespace");
|
||||
expect(result[0]).toContain(" ACCOUNT_GROUP_NOT_FOUND");
|
||||
});
|
||||
|
||||
test("trailing whitespace in key is flagged", () => {
|
||||
const json = JSON.stringify({ "ACCOUNT_GROUP_NOT_FOUND ": "terminate" });
|
||||
const result = validateErrorCodeMapping("block_42", json);
|
||||
expect(result).toHaveLength(1);
|
||||
expect(result[0]).toContain("surrounding whitespace");
|
||||
});
|
||||
|
||||
test("tab character in key is flagged", () => {
|
||||
const json = JSON.stringify({ "\tFOO": "bar" });
|
||||
expect(validateErrorCodeMapping("block_1", json)).toHaveLength(1);
|
||||
});
|
||||
|
||||
test("multiple bad keys produce multiple errors", () => {
|
||||
const json = JSON.stringify({
|
||||
" FOO": "a",
|
||||
"BAR ": "b",
|
||||
BAZ: "c",
|
||||
});
|
||||
const result = validateErrorCodeMapping("block_1", json);
|
||||
expect(result).toHaveLength(2);
|
||||
});
|
||||
|
||||
test("empty object returns no errors", () => {
|
||||
expect(validateErrorCodeMapping("block_1", "{}")).toEqual([]);
|
||||
});
|
||||
|
||||
test("array input is flagged as wrong shape", () => {
|
||||
// error_code_mapping must be a JSON object, not an array. Arrays are
|
||||
// syntactically valid JSON so the parse guard does not catch them —
|
||||
// the dedicated shape check does, preventing bad data from reaching
|
||||
// the save-time flow.
|
||||
const result = validateErrorCodeMapping("block_1", "[]");
|
||||
expect(result).toHaveLength(1);
|
||||
expect(result[0]).toContain("must be a JSON object");
|
||||
expect(result[0]).toContain("array");
|
||||
});
|
||||
|
||||
test("JSON primitives are flagged as wrong shape", () => {
|
||||
expect(validateErrorCodeMapping("block_1", "42")[0]).toContain(
|
||||
"must be a JSON object",
|
||||
);
|
||||
expect(validateErrorCodeMapping("block_1", '"foo"')[0]).toContain(
|
||||
"must be a JSON object",
|
||||
);
|
||||
expect(validateErrorCodeMapping("block_1", "true")[0]).toContain(
|
||||
"must be a JSON object",
|
||||
);
|
||||
});
|
||||
});
|
||||
|
|
@ -0,0 +1,42 @@
|
|||
/**
|
||||
* Validate an errorCodeMapping JSON string. The value must:
|
||||
* - parse as JSON,
|
||||
* - parse to a plain object (not an array, not a primitive, not `null`),
|
||||
* - and every key must not have surrounding whitespace — whitespace-bearing
|
||||
* keys look identical to clean keys in most UIs but do not match at
|
||||
* runtime, so they silently mis-fire (SKY-8825).
|
||||
*
|
||||
* `"null"` (literal JSON null) is the sentinel for "error mapping disabled"
|
||||
* and is treated as valid.
|
||||
*/
|
||||
export function validateErrorCodeMapping(
|
||||
label: string,
|
||||
errorCodeMapping: string,
|
||||
): Array<string> {
|
||||
const errors: Array<string> = [];
|
||||
let parsed: unknown;
|
||||
try {
|
||||
parsed = JSON.parse(errorCodeMapping);
|
||||
} catch {
|
||||
errors.push(`${label}: Error messages is not valid JSON.`);
|
||||
return errors;
|
||||
}
|
||||
// `null` is the disabled sentinel — valid.
|
||||
if (parsed === null) {
|
||||
return errors;
|
||||
}
|
||||
if (typeof parsed !== "object" || Array.isArray(parsed)) {
|
||||
errors.push(
|
||||
`${label}: Error messages must be a JSON object (got ${Array.isArray(parsed) ? "array" : typeof parsed}).`,
|
||||
);
|
||||
return errors;
|
||||
}
|
||||
Object.keys(parsed as Record<string, unknown>).forEach((key) => {
|
||||
if (key !== key.trim()) {
|
||||
errors.push(
|
||||
`${label}: Error messages key "${key}" has surrounding whitespace — remove the whitespace or it will never match at runtime.`,
|
||||
);
|
||||
}
|
||||
});
|
||||
return errors;
|
||||
}
|
||||
|
|
@ -114,9 +114,12 @@ import {
|
|||
extractionNodeDefaultData,
|
||||
isExtractionNode,
|
||||
} from "./nodes/ExtractionNode/types";
|
||||
import { loginNodeDefaultData } from "./nodes/LoginNode/types";
|
||||
import { isLoginNode, loginNodeDefaultData } from "./nodes/LoginNode/types";
|
||||
import { isWaitNode, waitNodeDefaultData } from "./nodes/WaitNode/types";
|
||||
import { fileDownloadNodeDefaultData } from "./nodes/FileDownloadNode/types";
|
||||
import {
|
||||
fileDownloadNodeDefaultData,
|
||||
isFileDownloadNode,
|
||||
} from "./nodes/FileDownloadNode/types";
|
||||
import { ProxyLocation, RunEngine } from "@/api/types";
|
||||
import {
|
||||
isPdfParserNode,
|
||||
|
|
@ -133,6 +136,7 @@ import {
|
|||
validateJson,
|
||||
} from "./nodes/HttpRequestNode/httpValidation";
|
||||
import { printPageNodeDefaultData } from "./nodes/PrintPageNode/types";
|
||||
import { validateErrorCodeMapping } from "./validateErrorCodeMapping";
|
||||
import {
|
||||
isWorkflowTriggerNode,
|
||||
workflowTriggerNodeDefaultData,
|
||||
|
|
@ -4091,11 +4095,9 @@ function getWorkflowErrors(nodes: Array<AppNode>): Array<string> {
|
|||
if (node.data.navigationGoal.length === 0) {
|
||||
errors.push(`${node.data.label}: Action Instruction is required.`);
|
||||
}
|
||||
try {
|
||||
JSON.parse(node.data.errorCodeMapping);
|
||||
} catch {
|
||||
errors.push(`${node.data.label}: Error messages is not valid JSON.`);
|
||||
}
|
||||
errors.push(
|
||||
...validateErrorCodeMapping(node.data.label, node.data.errorCodeMapping),
|
||||
);
|
||||
});
|
||||
|
||||
// check loop node parameters
|
||||
|
|
@ -4112,11 +4114,9 @@ function getWorkflowErrors(nodes: Array<AppNode>): Array<string> {
|
|||
// check task node json fields
|
||||
const taskNodes = nodes.filter(isTaskNode);
|
||||
taskNodes.forEach((node) => {
|
||||
try {
|
||||
JSON.parse(node.data.errorCodeMapping);
|
||||
} catch {
|
||||
errors.push(`${node.data.label}: Error messages is not valid JSON.`);
|
||||
}
|
||||
errors.push(
|
||||
...validateErrorCodeMapping(node.data.label, node.data.errorCodeMapping),
|
||||
);
|
||||
// Validate Task data schema JSON when enabled (value different from "null")
|
||||
if (node.data.dataSchema && node.data.dataSchema !== "null") {
|
||||
const result = TSON.parse(node.data.dataSchema);
|
||||
|
|
@ -4131,11 +4131,9 @@ function getWorkflowErrors(nodes: Array<AppNode>): Array<string> {
|
|||
|
||||
const validationNodes = nodes.filter(isValidationNode);
|
||||
validationNodes.forEach((node) => {
|
||||
try {
|
||||
JSON.parse(node.data.errorCodeMapping);
|
||||
} catch {
|
||||
errors.push(`${node.data.label}: Error messages is not valid JSON`);
|
||||
}
|
||||
errors.push(
|
||||
...validateErrorCodeMapping(node.data.label, node.data.errorCodeMapping),
|
||||
);
|
||||
if (
|
||||
node.data.completeCriterion.length === 0 &&
|
||||
node.data.terminateCriterion.length === 0
|
||||
|
|
@ -4165,6 +4163,23 @@ function getWorkflowErrors(nodes: Array<AppNode>): Array<string> {
|
|||
errors.push(`${node.data.label}: Prompt is required.`);
|
||||
}
|
||||
}
|
||||
errors.push(
|
||||
...validateErrorCodeMapping(node.data.label, node.data.errorCodeMapping),
|
||||
);
|
||||
});
|
||||
|
||||
const loginNodes = nodes.filter(isLoginNode);
|
||||
loginNodes.forEach((node) => {
|
||||
errors.push(
|
||||
...validateErrorCodeMapping(node.data.label, node.data.errorCodeMapping),
|
||||
);
|
||||
});
|
||||
|
||||
const fileDownloadNodes = nodes.filter(isFileDownloadNode);
|
||||
fileDownloadNodes.forEach((node) => {
|
||||
errors.push(
|
||||
...validateErrorCodeMapping(node.data.label, node.data.errorCodeMapping),
|
||||
);
|
||||
});
|
||||
|
||||
const conditionalNodes = nodes.filter((node) => node.type === "conditional");
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue