mirror of
https://github.com/sourcebot-dev/sourcebot.git
synced 2025-12-12 04:15:30 +00:00
149 lines
4.8 KiB
TypeScript
149 lines
4.8 KiB
TypeScript
|
|
'use client';
|
||
|
|
|
||
|
|
import { ScrollArea } from "@/components/ui/scroll-area";
|
||
|
|
import { useKeymapExtension } from "@/hooks/useKeymapExtension";
|
||
|
|
import { useThemeNormalized } from "@/hooks/useThemeNormalized";
|
||
|
|
import { json, jsonLanguage, jsonParseLinter } from "@codemirror/lang-json";
|
||
|
|
import { linter } from "@codemirror/lint";
|
||
|
|
import { EditorView, hoverTooltip } from "@codemirror/view";
|
||
|
|
import CodeMirror, { ReactCodeMirrorRef } from "@uiw/react-codemirror";
|
||
|
|
import {
|
||
|
|
handleRefresh,
|
||
|
|
jsonCompletion,
|
||
|
|
jsonSchemaHover,
|
||
|
|
jsonSchemaLinter,
|
||
|
|
stateExtensions
|
||
|
|
} from "codemirror-json-schema";
|
||
|
|
import { useMemo, useRef } from "react";
|
||
|
|
import { Button } from "@/components/ui/button";
|
||
|
|
import { Separator } from "@/components/ui/separator";
|
||
|
|
import { Schema } from "ajv";
|
||
|
|
|
||
|
|
export type QuickActionFn<T> = (previous: T) => T;
|
||
|
|
export type QuickAction<T> = {
|
||
|
|
name: string;
|
||
|
|
fn: QuickActionFn<T>;
|
||
|
|
};
|
||
|
|
|
||
|
|
interface ConfigEditorProps<T> {
|
||
|
|
value: string;
|
||
|
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||
|
|
onChange: (...event: any[]) => void;
|
||
|
|
actions: QuickAction<T>[],
|
||
|
|
schema: Schema;
|
||
|
|
}
|
||
|
|
|
||
|
|
const customAutocompleteStyle = EditorView.baseTheme({
|
||
|
|
".cm-tooltip.cm-completionInfo": {
|
||
|
|
padding: "8px",
|
||
|
|
fontSize: "12px",
|
||
|
|
fontFamily: "monospace",
|
||
|
|
},
|
||
|
|
".cm-tooltip-hover.cm-tooltip": {
|
||
|
|
padding: "8px",
|
||
|
|
fontSize: "12px",
|
||
|
|
fontFamily: "monospace",
|
||
|
|
}
|
||
|
|
});
|
||
|
|
|
||
|
|
|
||
|
|
export function ConfigEditor<T>({
|
||
|
|
value,
|
||
|
|
onChange,
|
||
|
|
actions,
|
||
|
|
schema,
|
||
|
|
}: ConfigEditorProps<T>) {
|
||
|
|
const editorRef = useRef<ReactCodeMirrorRef>(null);
|
||
|
|
const keymapExtension = useKeymapExtension(editorRef.current?.view);
|
||
|
|
const { theme } = useThemeNormalized();
|
||
|
|
|
||
|
|
const isQuickActionsDisabled = useMemo(() => {
|
||
|
|
try {
|
||
|
|
JSON.parse(value);
|
||
|
|
return false;
|
||
|
|
} catch {
|
||
|
|
return true;
|
||
|
|
}
|
||
|
|
}, [value]);
|
||
|
|
|
||
|
|
const onQuickAction = (action: QuickActionFn<T>) => {
|
||
|
|
let previousConfig: T;
|
||
|
|
try {
|
||
|
|
previousConfig = JSON.parse(value) as T;
|
||
|
|
} catch {
|
||
|
|
return;
|
||
|
|
}
|
||
|
|
|
||
|
|
const nextConfig = action(previousConfig);
|
||
|
|
const next = JSON.stringify(nextConfig, null, 2);
|
||
|
|
|
||
|
|
const cursorPos = next.lastIndexOf(`""`) + 1;
|
||
|
|
|
||
|
|
editorRef.current?.view?.focus();
|
||
|
|
editorRef.current?.view?.dispatch({
|
||
|
|
changes: {
|
||
|
|
from: 0,
|
||
|
|
to: value.length,
|
||
|
|
insert: next,
|
||
|
|
}
|
||
|
|
});
|
||
|
|
editorRef.current?.view?.dispatch({
|
||
|
|
selection: { anchor: cursorPos, head: cursorPos }
|
||
|
|
});
|
||
|
|
}
|
||
|
|
|
||
|
|
return (
|
||
|
|
<>
|
||
|
|
<div className="flex flex-row items-center flex-wrap w-full">
|
||
|
|
{actions.map(({ name, fn }, index) => (
|
||
|
|
<div
|
||
|
|
key={index}
|
||
|
|
className="flex flex-row items-center"
|
||
|
|
>
|
||
|
|
<Button
|
||
|
|
variant="ghost"
|
||
|
|
className="disabled:opacity-100 disabled:pointer-events-auto disabled:cursor-not-allowed"
|
||
|
|
disabled={isQuickActionsDisabled}
|
||
|
|
onClick={(e) => {
|
||
|
|
e.preventDefault();
|
||
|
|
onQuickAction(fn);
|
||
|
|
}}
|
||
|
|
>
|
||
|
|
{name}
|
||
|
|
</Button>
|
||
|
|
{index !== actions.length - 1 && (
|
||
|
|
<Separator
|
||
|
|
orientation="vertical" className="h-4 mx-1"
|
||
|
|
/>
|
||
|
|
)}
|
||
|
|
</div>
|
||
|
|
))}
|
||
|
|
</div>
|
||
|
|
<ScrollArea className="rounded-md border p-1 overflow-auto flex-1 h-64">
|
||
|
|
<CodeMirror
|
||
|
|
ref={editorRef}
|
||
|
|
value={value}
|
||
|
|
onChange={onChange}
|
||
|
|
extensions={[
|
||
|
|
keymapExtension,
|
||
|
|
json(),
|
||
|
|
linter(jsonParseLinter(), {
|
||
|
|
delay: 300,
|
||
|
|
}),
|
||
|
|
linter(jsonSchemaLinter(), {
|
||
|
|
needsRefresh: handleRefresh,
|
||
|
|
}),
|
||
|
|
jsonLanguage.data.of({
|
||
|
|
autocomplete: jsonCompletion(),
|
||
|
|
}),
|
||
|
|
hoverTooltip(jsonSchemaHover()),
|
||
|
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||
|
|
stateExtensions(schema as any),
|
||
|
|
customAutocompleteStyle,
|
||
|
|
]}
|
||
|
|
theme={theme === "dark" ? "dark" : "light"}
|
||
|
|
/>
|
||
|
|
</ScrollArea>
|
||
|
|
</>
|
||
|
|
)
|
||
|
|
}
|