Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { useEffect, useState } from "react";

Check warning on line 1 in src/components/Editor/IOEditor/InputValueEditor/InputValueEditor.tsx

View workflow job for this annotation

GitHub Actions / Linting

Run autofix to sort these imports!

Check warning on line 1 in src/components/Editor/IOEditor/InputValueEditor/InputValueEditor.tsx

View workflow job for this annotation

GitHub Actions / Linting

Run autofix to sort these imports!

import { updateInputNameOnComponentSpec } from "@/components/Editor/utils/updateInputNameOnComponentSpec";
import { ConfirmationDialog } from "@/components/shared/Dialogs";
Expand All @@ -18,6 +18,8 @@
import { inputNameToNodeId } from "@/utils/nodes/nodeIdUtils";
import { updateSubgraphSpec } from "@/utils/subgraphUtils";

import CodeViewer from "@/components/shared/CodeViewer/CodeViewer";

import { IOZIndexEditor } from "../IOZIndexEditor";
import {
DescriptionField,
Expand Down Expand Up @@ -55,6 +57,7 @@
} = useConfirmationDialog();

const [isValueDialogOpen, setIsValueDialogOpen] = useState(false);
const [isCodeEditorOpen, setIsCodeEditorOpen] = useState(false);
const [triggerSave, setTriggerSave] = useState(false);

const defaultInputValue = input.value ?? input.default ?? "";
Expand Down Expand Up @@ -241,6 +244,21 @@
setTriggerSave(true);
};

const handleOpenCodeEditor = () => {
if (disabled) return;
setIsCodeEditorOpen(true);
};

const handleCodeEditorConfirm = (value: string) => {
setInputValue(value);
setIsCodeEditorOpen(false);
setTriggerSave(true);
};

const handleCodeEditorCancel = () => {
setIsCodeEditorOpen(false);
};

useEffect(() => {
setInputValue(initialInputValue);
setInputName(input.name);
Expand Down Expand Up @@ -295,6 +313,11 @@
hidden: disabled,
onClick: handleExpandValueEditor,
},
{
icon: "Code",
hidden: disabled,
onClick: handleOpenCodeEditor,
},
{
icon: "Copy",
hidden: !disabled && !inputValue,
Expand Down Expand Up @@ -343,6 +366,15 @@
onCancel={handleDialogCancel}
onConfirm={handleDialogConfirm}
/>

{isCodeEditorOpen && (
<CodeViewer
editable
code={inputValue}
onConfirm={handleCodeEditorConfirm}
onCancel={handleCodeEditorCancel}
/>
)}
</BlockStack>
);
};
32 changes: 32 additions & 0 deletions src/components/shared/CodeViewer/CodeEditor.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
import MonacoEditor from "@monaco-editor/react";
import { memo } from "react";

interface CodeEditorProps {
value: string;
language: string;
onChange: (value: string) => void;
}

const CodeEditor = memo(function CodeEditor({
value,
language,
onChange,
}: CodeEditorProps) {
return (
<MonacoEditor
language={language}
theme="vs-dark"
value={value}
onChange={(v) => onChange(v ?? "")}
options={{
minimap: { enabled: false },
scrollBeyondLastLine: false,
lineNumbers: "on",
wordWrap: "on",
automaticLayout: true,
}}
/>
);
});

export default CodeEditor;
133 changes: 108 additions & 25 deletions src/components/shared/CodeViewer/CodeViewer.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,17 +2,38 @@ import { useEffect, useState } from "react";

import { Button } from "@/components/ui/button";
import { Icon } from "@/components/ui/icon";
import {
Select,
SelectContent,
SelectItem,
SelectTrigger,
SelectValue,
} from "@/components/ui/select";

import { FullscreenElement } from "../FullscreenElement";
import CodeEditor from "./CodeEditor";
import CodeSyntaxHighlighter from "./CodeSyntaxHighlighter";

const LANGUAGE_OPTIONS = [
{ value: "plaintext", label: "Plain Text" },
{ value: "yaml", label: "YAML" },
{ value: "python", label: "Python" },
{ value: "javascript", label: "JavaScript" },
{ value: "json", label: "JSON" },
{ value: "sql", label: "SQL" },
];

interface CodeViewerProps {
code: string;
language?: string;
filename?: string;
fullscreen?: boolean;
scrollToBottom?: boolean;
editable?: boolean;
initialLanguage?: string;
onClose?: () => void;
onConfirm?: (value: string) => void;
onCancel?: () => void;
}

const DEFAULT_CODE_VIEWER_HEIGHT = 128;
Expand All @@ -23,9 +44,17 @@ const CodeViewer = ({
filename = "",
fullscreen = false,
scrollToBottom = false,
editable = false,
initialLanguage = "plaintext",
onClose,
onConfirm,
onCancel,
}: CodeViewerProps) => {
const [isFullscreen, setIsFullscreen] = useState(fullscreen);
const [isFullscreen, setIsFullscreen] = useState(
editable ? true : fullscreen,
);
const [editValue, setEditValue] = useState(code);
const [selectedLanguage, setSelectedLanguage] = useState(initialLanguage);

const handleToggleFullscreen = () => {
if (isFullscreen && onClose) {
Expand All @@ -38,7 +67,11 @@ const CodeViewer = ({
useEffect(() => {
const handleEscapeKey = (e: KeyboardEvent) => {
if (isFullscreen && e.key === "Escape") {
setIsFullscreen(false);
if (editable && onCancel) {
onCancel();
} else {
setIsFullscreen(false);
}
e.preventDefault();
e.stopPropagation();
}
Expand All @@ -49,29 +82,71 @@ const CodeViewer = ({
return () => {
document.removeEventListener("keydown", handleEscapeKey);
};
}, [isFullscreen]);
}, [isFullscreen, editable, onCancel]);

return (
<FullscreenElement fullscreen={isFullscreen}>
<div className="flex flex-col transition-shadow duration-150 bg-slate-900 h-full rounded-md">
<div className="flex items-center justify-between gap-2 bg-slate-800 sticky top-0 z-10 rounded-t-md px-3 py-2.5">
<div className="flex items-baseline gap-2">
<span className="font-semibold text-base text-secondary">
{filename}
</span>
<span className="text-sm text-secondary">(Read Only)</span>
</div>
<Button
type="button"
variant="ghost"
size="icon"
onClick={handleToggleFullscreen}
className="text-gray-200 hover:text-black"
title={isFullscreen ? "Exit fullscreen" : "View fullscreen"}
aria-label={isFullscreen ? "Exit fullscreen" : "View fullscreen"}
>
{isFullscreen ? <Icon name="X" /> : <Icon name="Maximize2" />}
</Button>
{editable ? (
<>
<Select
value={selectedLanguage}
onValueChange={setSelectedLanguage}
>
<SelectTrigger className="w-36 h-7 text-gray-200 border-slate-600 bg-slate-700 hover:bg-slate-600">
<SelectValue />
</SelectTrigger>
<SelectContent className="z-2147483647">
{LANGUAGE_OPTIONS.map((opt) => (
<SelectItem key={opt.value} value={opt.value}>
{opt.label}
</SelectItem>
))}
</SelectContent>
</Select>
<div className="flex items-center gap-2">
<Button
type="button"
variant="ghost"
size="sm"
onClick={onCancel}
className="text-gray-200 hover:text-black"
>
Cancel
</Button>
<Button
type="button"
size="sm"
onClick={() => onConfirm?.(editValue)}
>
Confirm
</Button>
</div>
</>
) : (
<>
<div className="flex items-baseline gap-2">
<span className="font-semibold text-base text-secondary">
{filename}
</span>
<span className="text-sm text-secondary">(Read Only)</span>
</div>
<Button
type="button"
variant="ghost"
size="icon"
onClick={handleToggleFullscreen}
className="text-gray-200 hover:text-black"
title={isFullscreen ? "Exit fullscreen" : "View fullscreen"}
aria-label={
isFullscreen ? "Exit fullscreen" : "View fullscreen"
}
>
{isFullscreen ? <Icon name="X" /> : <Icon name="Maximize2" />}
</Button>
</>
)}
</div>
<div className="flex-1 relative">
<div
Expand All @@ -81,11 +156,19 @@ const CodeViewer = ({
minHeight: DEFAULT_CODE_VIEWER_HEIGHT,
}}
>
<CodeSyntaxHighlighter
code={code}
language={language}
scrollToBottom={scrollToBottom}
/>
{editable ? (
<CodeEditor
value={editValue}
language={selectedLanguage}
onChange={setEditValue}
/>
) : (
<CodeSyntaxHighlighter
code={code}
language={language}
scrollToBottom={scrollToBottom}
/>
)}
</div>
</div>
</div>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import {
useState,
} from "react";

import CodeViewer from "@/components/shared/CodeViewer/CodeViewer";
import { MultilineTextInputDialog } from "@/components/shared/Dialogs/MultilineTextInputDialog";
import { Button } from "@/components/ui/button";
import { Icon } from "@/components/ui/icon";
Expand Down Expand Up @@ -76,6 +77,28 @@ export const AnnotationsInput = ({
setIsDialogOpen(false);
}, []);

const [isCodeEditorOpen, setIsCodeEditorOpen] = useState(false);

const handleOpenCodeEditor = useCallback(() => {
setIsCodeEditorOpen(true);
}, []);

const handleCodeEditorConfirm = useCallback(
(newValue: string) => {
setInputValue(newValue);
setIsCodeEditorOpen(false);
if (onBlur && newValue !== lastSavedValue) {
onBlur(newValue);
setLastSavedValue(newValue);
}
},
[onBlur, lastSavedValue],
);

const handleCodeEditorCancel = useCallback(() => {
setIsCodeEditorOpen(false);
}, []);

const validateChange = useCallback((e: ChangeEvent<HTMLInputElement>) => {
const newValue = e.target.value;

Expand Down Expand Up @@ -358,6 +381,15 @@ export const AnnotationsInput = ({
>
<Icon name="Maximize2" />
</Button>
<Button
className="absolute right-8 top-1/2 -translate-y-1/2 hover:bg-transparent hover:text-blue-500 hidden group-hover:flex h-8 w-8 p-0"
onClick={handleOpenCodeEditor}
variant="ghost"
type="button"
title="Code Editor"
>
<Icon name="Code" />
</Button>
</div>
{isInvalid && (
<InlineStack gap="1" className="my-1">
Expand Down Expand Up @@ -411,6 +443,15 @@ export const AnnotationsInput = ({
required={config?.required}
/>
)}

{isCodeEditorOpen && (
<CodeViewer
editable
code={inputValue}
onConfirm={handleCodeEditorConfirm}
onCancel={handleCodeEditorCancel}
/>
)}
</>
);
};
Expand Down
Loading