Skip to content

Commit fab5652

Browse files
authored
Allow empty payloads in test runs (#1962)
* Allow empty payloads in test runs * Expose a basic linter configs in the code editor component * Touch up the linting error elements in the code editor component
1 parent cedd932 commit fab5652

File tree

5 files changed

+89
-37
lines changed

5 files changed

+89
-37
lines changed

Diff for: apps/webapp/app/components/code/JSONEditor.tsx

+35-2
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
1-
import { json as jsonLang } from "@codemirror/lang-json";
2-
import type { ViewUpdate } from "@codemirror/view";
1+
import { json as jsonLang, jsonParseLinter } from "@codemirror/lang-json";
2+
import type { EditorView, ViewUpdate } from "@codemirror/view";
33
import { CheckIcon, ClipboardIcon, TrashIcon } from "@heroicons/react/20/solid";
44
import type { ReactCodeMirrorProps, UseCodeMirror } from "@uiw/react-codemirror";
55
import { useCodeMirror } from "@uiw/react-codemirror";
@@ -8,6 +8,7 @@ import { cn } from "~/utils/cn";
88
import { Button } from "../primitives/Buttons";
99
import { getEditorSetup } from "./codeMirrorSetup";
1010
import { darkTheme } from "./codeMirrorTheme";
11+
import { linter, lintGutter, type Diagnostic } from "@codemirror/lint";
1112

1213
export interface JSONEditorProps extends Omit<ReactCodeMirrorProps, "onBlur"> {
1314
defaultValue?: string;
@@ -18,18 +19,35 @@ export interface JSONEditorProps extends Omit<ReactCodeMirrorProps, "onBlur"> {
1819
onBlur?: (code: string) => void;
1920
showCopyButton?: boolean;
2021
showClearButton?: boolean;
22+
linterEnabled?: boolean;
23+
allowEmpty?: boolean;
2124
}
2225

2326
const languages = {
2427
json: jsonLang,
2528
};
2629

30+
function emptyAwareJsonLinter() {
31+
return (view: EditorView): Diagnostic[] => {
32+
const content = view.state.doc.toString().trim();
33+
34+
// return no errors if content is empty
35+
if (!content) {
36+
return [];
37+
}
38+
39+
return jsonParseLinter()(view);
40+
};
41+
}
42+
2743
type JSONEditorDefaultProps = Partial<JSONEditorProps>;
2844

2945
const defaultProps: JSONEditorDefaultProps = {
3046
language: "json",
3147
readOnly: true,
3248
basicSetup: false,
49+
linterEnabled: true,
50+
allowEmpty: true,
3351
};
3452

3553
export function JSONEditor(opts: JSONEditorProps) {
@@ -44,6 +62,8 @@ export function JSONEditor(opts: JSONEditorProps) {
4462
autoFocus,
4563
showCopyButton = true,
4664
showClearButton = true,
65+
linterEnabled,
66+
allowEmpty,
4767
} = {
4868
...defaultProps,
4969
...opts,
@@ -56,6 +76,19 @@ export function JSONEditor(opts: JSONEditorProps) {
5676

5777
extensions.push(languageExtension());
5878

79+
if (linterEnabled) {
80+
extensions.push(lintGutter());
81+
82+
switch (language) {
83+
case "json": {
84+
extensions.push(allowEmpty ? linter(emptyAwareJsonLinter()) : linter(jsonParseLinter()));
85+
break;
86+
}
87+
default:
88+
language satisfies never;
89+
}
90+
}
91+
5992
const editor = useRef<HTMLDivElement>(null);
6093
const settings: Omit<UseCodeMirror, "onBlur"> = {
6194
...opts,

Diff for: apps/webapp/app/components/code/codeMirrorSetup.ts

+1-4
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,7 @@
11
import { closeBrackets } from "@codemirror/autocomplete";
22
import { indentWithTab } from "@codemirror/commands";
3-
import { jsonParseLinter } from "@codemirror/lang-json";
43
import { bracketMatching } from "@codemirror/language";
5-
import { lintGutter, lintKeymap, linter } from "@codemirror/lint";
4+
import { lintKeymap } from "@codemirror/lint";
65
import { highlightSelectionMatches } from "@codemirror/search";
76
import { Prec, type Extension } from "@codemirror/state";
87
import {
@@ -21,8 +20,6 @@ export function getEditorSetup(showLineNumbers = true, showHighlights = true): A
2120
dropCursor(),
2221
bracketMatching(),
2322
closeBrackets(),
24-
lintGutter(),
25-
linter(jsonParseLinter()),
2623
Prec.highest(
2724
keymap.of([
2825
{

Diff for: apps/webapp/app/components/code/codeMirrorTheme.ts

+27
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,32 @@ export function darkTheme(): Extension {
3939
fontSize: "14px",
4040
},
4141

42+
".cm-tooltip.cm-tooltip-lint": {
43+
backgroundColor: tooltipBackground,
44+
},
45+
46+
".cm-diagnostic": {
47+
padding: "4px 8px",
48+
color: ivory,
49+
fontFamily: "Geist Mono Variable",
50+
fontSize: "12px",
51+
},
52+
53+
".cm-diagnostic-error": {
54+
borderLeft: "2px solid #e11d48",
55+
},
56+
57+
".cm-lint-marker-error": {
58+
content: "none",
59+
backgroundColor: "#e11d48",
60+
height: "100%",
61+
width: "2px",
62+
},
63+
64+
".cm-lintPoint:after": {
65+
borderBottom: "4px solid #e11d48",
66+
},
67+
4268
".cm-cursor, .cm-dropCursor": { borderLeftColor: cursor },
4369
"&.cm-focused .cm-selectionBackground, .cm-selectionBackground, .cm-content ::selection": {
4470
backgroundColor: selection,
@@ -82,6 +108,7 @@ export function darkTheme(): Extension {
82108

83109
".cm-tooltip": {
84110
border: "none",
111+
marginTop: "6px",
85112
backgroundColor: tooltipBackground,
86113
},
87114
".cm-tooltip .cm-tooltip-arrow:before": {

Diff for: apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.env.$envParam.test.tasks.$taskParam/route.tsx

-1
Original file line numberDiff line numberDiff line change
@@ -291,7 +291,6 @@ function StandardTaskForm({ task, runs }: { task: TestTask["task"]; runs: Standa
291291
}}
292292
height="100%"
293293
autoFocus={!tab || tab === "payload"}
294-
placeholder="{ }"
295294
className={cn("h-full overflow-auto", tab === "metadata" && "hidden")}
296295
/>
297296
<JSONEditor

Diff for: apps/webapp/app/v3/testTask.ts

+26-30
Original file line numberDiff line numberDiff line change
@@ -4,46 +4,42 @@ export const TestTaskData = z
44
.discriminatedUnion("triggerSource", [
55
z.object({
66
triggerSource: z.literal("STANDARD"),
7-
payload: z.string().transform((payload, ctx) => {
8-
try {
9-
const data = JSON.parse(payload);
10-
return data as any;
11-
} catch (e) {
12-
console.log("parsing error", e);
7+
payload: z
8+
.string()
9+
.optional()
10+
.transform((val, ctx) => {
11+
if (!val) {
12+
return {};
13+
}
1314

14-
if (e instanceof Error) {
15-
ctx.addIssue({
16-
code: z.ZodIssueCode.custom,
17-
message: e.message,
18-
});
19-
} else {
15+
try {
16+
return JSON.parse(val);
17+
} catch {
2018
ctx.addIssue({
2119
code: z.ZodIssueCode.custom,
22-
message: "This is invalid JSON",
20+
message: "Payload must be a valid JSON string",
2321
});
22+
return z.NEVER;
23+
}
24+
}),
25+
metadata: z
26+
.string()
27+
.optional()
28+
.transform((val, ctx) => {
29+
if (!val) {
30+
return {};
2431
}
25-
}
26-
}),
27-
metadata: z.string().transform((metadata, ctx) => {
28-
try {
29-
const data = JSON.parse(metadata);
30-
return data as any;
31-
} catch (e) {
32-
console.log("parsing error", e);
3332

34-
if (e instanceof Error) {
35-
ctx.addIssue({
36-
code: z.ZodIssueCode.custom,
37-
message: e.message,
38-
});
39-
} else {
33+
try {
34+
return JSON.parse(val);
35+
} catch {
4036
ctx.addIssue({
4137
code: z.ZodIssueCode.custom,
42-
message: "This is invalid JSON",
38+
message: "Metadata must be a valid JSON string",
4339
});
40+
return z.NEVER;
4441
}
45-
}
46-
}),
42+
}),
4743
}),
4844
z.object({
4945
triggerSource: z.literal("SCHEDULED"),

0 commit comments

Comments
 (0)