{hook.matcher}
@@ -95,9 +98,10 @@ export function HookListRow({ hook, onClick, onToggle, onEdit, onDelete, onTest diff --git a/ui/web/src/pages/hooks/components/script-editor.test.ts b/ui/web/src/pages/hooks/components/script-editor.test.ts new file mode 100644 index 00000000..a8ca3b2d --- /dev/null +++ b/ui/web/src/pages/hooks/components/script-editor.test.ts @@ -0,0 +1,33 @@ +import { describe, expect, it } from "vitest"; +import { parseLineCol } from "./script-editor"; + +// parseLineCol extracts {line, col} from goja compile-error strings. Format +// comes from goja parser/error.go: "%s: Line %d:%d %s". Keep this test +// permissive about the prefix but strict about the "Line N:M" shape because +// that's what the dispatcher surfaces verbatim to the UI. +describe("parseLineCol", () => { + it("parses goja format (anonymous): Line N:M message", () => { + const got = parseLineCol("(anonymous): Line 5:12 Unexpected token"); + expect(got).toEqual({ line: 5, col: 12 }); + }); + + it("parses multi-digit line + col", () => { + const got = parseLineCol("Line 123:456 Something went wrong"); + expect(got).toEqual({ line: 123, col: 456 }); + }); + + it("returns null for non-matching shape", () => { + expect(parseLineCol("some unstructured error")).toBeNull(); + expect(parseLineCol("line 5 col 10 — wrong separator")).toBeNull(); + }); + + it("returns null for empty / undefined input", () => { + expect(parseLineCol(undefined)).toBeNull(); + expect(parseLineCol("")).toBeNull(); + }); + + it("picks the first Line occurrence (multi-error case)", () => { + const got = parseLineCol("Line 2:3 first err; Line 9:9 later"); + expect(got).toEqual({ line: 2, col: 3 }); + }); +}); diff --git a/ui/web/src/pages/hooks/components/script-editor.tsx b/ui/web/src/pages/hooks/components/script-editor.tsx new file mode 100644 index 00000000..e985705a --- /dev/null +++ b/ui/web/src/pages/hooks/components/script-editor.tsx @@ -0,0 +1,61 @@ +import { useMemo } from "react"; +import Editor from "react-simple-code-editor"; +import Prism from "prismjs"; +import "prismjs/components/prism-javascript"; +import { useTranslation } from "react-i18next"; + +interface ScriptEditorProps { + value: string; + onChange: (v: string) => void; + // Server-returned compile error. goja prints "(anonymous): Line N:M message" + // (see goja parser/error.go — "%s: Line %d:%d %s"). We extract the location + // with the regex below; when the shape doesn't match we surface the raw + // string unchanged. + error?: string; + readOnly?: boolean; +} + +// parseLineCol extracts {line, col} from a goja compile-error string. Format +// is "... Line N:M ...". Any other shape returns null so the caller can fall +// back to rendering the raw message. +export function parseLineCol(err?: string): { line: number; col: number } | null { + if (!err) return null; + const m = err.match(/Line (\d+):(\d+)/); + if (!m || !m[1] || !m[2]) return null; + return { line: parseInt(m[1], 10), col: parseInt(m[2], 10) }; +} + +export function ScriptEditor({ value, onChange, error, readOnly }: ScriptEditorProps) { + const { t } = useTranslation("hooks"); + const loc = useMemo(() => parseLineCol(error), [error]); + + return ( ++ {loc + ? t("form.compileError", { line: loc.line, col: loc.col }) + : error} +
+ ) : ( +{t("form.scriptSchemaHelp")}
+ )} +