Files
triggerdotdev--trigger.dev/apps/webapp/app/components/code/JSONEditor.tsx
T
Saadi Myftija 3cfde48bfd feat: expose all run options in the test run page (#2227)
* Implement a new primitive UI component for picking durations

* Implement a new component to input run tags

* Expose all run options in the test run page

* Add subtle animations when adding/removing run tags in the test page

* Add a new resource endpoint for fetching queues

* Fetch usable queues for the selected task

* Fix width display issue in the select component

* Enable locking a run to a version from the test page

* Disable entering max attemps <0

* Validate tags

* Add recent runs popover

* Only show latest version for development environments

* Update run options when selecting a recent run

* Rearrange the test page layout

* Add subtle animation to the duration picker segments on focus

* Improve queue selection dropdown styling

* Fix disabled state issue for the SelectTrigger component

* Disable version selection field for dev envs

* Add usage hints next to the run option fields

* Add machine preset to the run options list

* Allow arbitrary queue inputs for v1 engine runs

* Show truncated run ID instead of run numbers for recent runs

Run numbers will soon get deprecated due to contention issues

* Fix duplicate queue issue

* Extract common elements across the standard and scheduled test task forms

* Apply values from recent runs to scheduled tasks too

* Add additional run options for scheduled tasks

* Use a slightly smaller font size for run option labels

* Disallow commas in the run tag input field

* Switch to a custom icon for recent runs button

* Flatten the load function test task result object

* Avoid redefining machine presets, use zod schema instead

* Fix ClockRotateLeftIcon jsx issues

* Remove recent runs button tooltip as it causes nesting errors

* Adjust the page layout to make it clear which task is currently selected

* Inline the tab group with the copy/clear buttons
2025-07-09 10:40:10 +01:00

202 lines
5.2 KiB
TypeScript

import { json as jsonLang, jsonParseLinter } from "@codemirror/lang-json";
import type { EditorView, ViewUpdate } from "@codemirror/view";
import { CheckIcon, ClipboardIcon, TrashIcon } from "@heroicons/react/20/solid";
import type { ReactCodeMirrorProps, UseCodeMirror } from "@uiw/react-codemirror";
import { useCodeMirror } from "@uiw/react-codemirror";
import { useCallback, useEffect, useRef, useState } from "react";
import { cn } from "~/utils/cn";
import { Button } from "../primitives/Buttons";
import { getEditorSetup } from "./codeMirrorSetup";
import { darkTheme } from "./codeMirrorTheme";
import { linter, lintGutter, type Diagnostic } from "@codemirror/lint";
export interface JSONEditorProps extends Omit<ReactCodeMirrorProps, "onBlur"> {
defaultValue?: string;
language?: "json";
readOnly?: boolean;
onChange?: (value: string) => void;
onUpdate?: (update: ViewUpdate) => void;
onBlur?: (code: string) => void;
showCopyButton?: boolean;
showClearButton?: boolean;
linterEnabled?: boolean;
allowEmpty?: boolean;
additionalActions?: React.ReactNode;
}
const languages = {
json: jsonLang,
};
function emptyAwareJsonLinter() {
return (view: EditorView): Diagnostic[] => {
const content = view.state.doc.toString().trim();
// return no errors if content is empty
if (!content) {
return [];
}
return jsonParseLinter()(view);
};
}
type JSONEditorDefaultProps = Partial<JSONEditorProps>;
const defaultProps: JSONEditorDefaultProps = {
language: "json",
readOnly: true,
basicSetup: false,
linterEnabled: true,
allowEmpty: true,
};
export function JSONEditor(opts: JSONEditorProps) {
const {
defaultValue = "",
language,
readOnly,
onChange,
onUpdate,
onBlur,
basicSetup,
autoFocus,
showCopyButton = true,
showClearButton = true,
linterEnabled,
allowEmpty,
additionalActions,
} = {
...defaultProps,
...opts,
};
const extensions = getEditorSetup();
if (!language) throw new Error("language is required");
const languageExtension = languages[language];
extensions.push(languageExtension());
if (linterEnabled) {
extensions.push(lintGutter());
switch (language) {
case "json": {
extensions.push(allowEmpty ? linter(emptyAwareJsonLinter()) : linter(jsonParseLinter()));
break;
}
default:
language satisfies never;
}
}
const editor = useRef<HTMLDivElement>(null);
const settings: Omit<UseCodeMirror, "onBlur"> = {
...opts,
container: editor.current,
extensions,
editable: !readOnly,
contentEditable: !readOnly,
value: defaultValue,
autoFocus,
theme: darkTheme(),
indentWithTab: false,
basicSetup,
onChange,
onUpdate,
};
const { setContainer, view } = useCodeMirror(settings);
const [copied, setCopied] = useState(false);
useEffect(() => {
if (editor.current) {
setContainer(editor.current);
}
}, [setContainer]);
//if the defaultValue changes update the editor
useEffect(() => {
if (view !== undefined) {
if (view.state.doc.toString() === defaultValue) return;
view.dispatch({
changes: { from: 0, to: view.state.doc.length, insert: defaultValue },
});
}
}, [defaultValue, view]);
const clear = useCallback(() => {
if (view === undefined) return;
view.dispatch({
changes: { from: 0, to: view.state.doc.length, insert: undefined },
});
onChange?.("");
}, [view]);
const copy = useCallback(() => {
if (view === undefined) return;
navigator.clipboard.writeText(view.state.doc.toString());
setCopied(true);
setTimeout(() => {
setCopied(false);
}, 1500);
}, [view]);
const showButtons = showClearButton || showCopyButton;
return (
<div
className={cn(
"grid",
showButtons ? "grid-rows-[2.5rem_1fr]" : "grid-rows-[1fr]",
opts.className
)}
>
{showButtons && (
<div className="mx-3 flex items-center justify-end gap-2 border-b border-grid-dimmed">
{additionalActions && additionalActions}
{showClearButton && (
<Button
type="button"
variant="minimal/small"
TrailingIcon={TrashIcon}
onClick={(event) => {
event.preventDefault();
event.stopPropagation();
clear();
}}
>
Clear
</Button>
)}
{showCopyButton && (
<Button
type="button"
variant="minimal/small"
TrailingIcon={copied ? CheckIcon : ClipboardIcon}
trailingIconClassName={
copied ? "text-green-500 group-hover:text-green-500" : undefined
}
onClick={(event) => {
event.preventDefault();
event.stopPropagation();
copy();
}}
>
Copy
</Button>
)}
</div>
)}
<div
className="w-full overflow-auto"
ref={editor}
onBlur={() => {
if (!onBlur) return;
onBlur(editor.current?.textContent ?? "");
}}
/>
</div>
);
}