diff --git a/src/components/Editor/IOEditor/InputValueEditor/InputValueEditor.tsx b/src/components/Editor/IOEditor/InputValueEditor/InputValueEditor.tsx index 4f1736632..fa4626c18 100644 --- a/src/components/Editor/IOEditor/InputValueEditor/InputValueEditor.tsx +++ b/src/components/Editor/IOEditor/InputValueEditor/InputValueEditor.tsx @@ -18,6 +18,8 @@ import { checkInputConnectionToRequiredFields } from "@/utils/inputConnectionUti import { inputNameToNodeId } from "@/utils/nodes/nodeIdUtils"; import { updateSubgraphSpec } from "@/utils/subgraphUtils"; +import CodeViewer from "@/components/shared/CodeViewer/CodeViewer"; + import { IOZIndexEditor } from "../IOZIndexEditor"; import { DescriptionField, @@ -55,6 +57,7 @@ export const InputValueEditor = ({ } = useConfirmationDialog(); const [isValueDialogOpen, setIsValueDialogOpen] = useState(false); + const [isCodeEditorOpen, setIsCodeEditorOpen] = useState(false); const [triggerSave, setTriggerSave] = useState(false); const defaultInputValue = input.value ?? input.default ?? ""; @@ -241,6 +244,21 @@ export const InputValueEditor = ({ 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); @@ -295,6 +313,11 @@ export const InputValueEditor = ({ hidden: disabled, onClick: handleExpandValueEditor, }, + { + icon: "Code", + hidden: disabled, + onClick: handleOpenCodeEditor, + }, { icon: "Copy", hidden: !disabled && !inputValue, @@ -343,6 +366,15 @@ export const InputValueEditor = ({ onCancel={handleDialogCancel} onConfirm={handleDialogConfirm} /> + + {isCodeEditorOpen && ( + + )} ); }; diff --git a/src/components/shared/CodeViewer/CodeEditor.tsx b/src/components/shared/CodeViewer/CodeEditor.tsx new file mode 100644 index 000000000..23f0da956 --- /dev/null +++ b/src/components/shared/CodeViewer/CodeEditor.tsx @@ -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 ( + onChange(v ?? "")} + options={{ + minimap: { enabled: false }, + scrollBeyondLastLine: false, + lineNumbers: "on", + wordWrap: "on", + automaticLayout: true, + }} + /> + ); +}); + +export default CodeEditor; diff --git a/src/components/shared/CodeViewer/CodeViewer.tsx b/src/components/shared/CodeViewer/CodeViewer.tsx index 63e4f5858..05f207a5e 100644 --- a/src/components/shared/CodeViewer/CodeViewer.tsx +++ b/src/components/shared/CodeViewer/CodeViewer.tsx @@ -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; @@ -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) { @@ -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(); } @@ -49,29 +82,71 @@ const CodeViewer = ({ return () => { document.removeEventListener("keydown", handleEscapeKey); }; - }, [isFullscreen]); + }, [isFullscreen, editable, onCancel]); return ( - - - {filename} - - (Read Only) - - - {isFullscreen ? : } - + {editable ? ( + <> + + + + + + {LANGUAGE_OPTIONS.map((opt) => ( + + {opt.label} + + ))} + + + + + Cancel + + onConfirm?.(editValue)} + > + Confirm + + + > + ) : ( + <> + + + {filename} + + (Read Only) + + + {isFullscreen ? : } + + > + )} - + {editable ? ( + + ) : ( + + )} diff --git a/src/components/shared/ReactFlow/FlowCanvas/TaskNode/AnnotationsEditor/AnnotationsInput.tsx b/src/components/shared/ReactFlow/FlowCanvas/TaskNode/AnnotationsEditor/AnnotationsInput.tsx index 59f7f1393..80e6314d9 100644 --- a/src/components/shared/ReactFlow/FlowCanvas/TaskNode/AnnotationsEditor/AnnotationsInput.tsx +++ b/src/components/shared/ReactFlow/FlowCanvas/TaskNode/AnnotationsEditor/AnnotationsInput.tsx @@ -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"; @@ -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) => { const newValue = e.target.value; @@ -358,6 +381,15 @@ export const AnnotationsInput = ({ > + + + {isInvalid && ( @@ -411,6 +443,15 @@ export const AnnotationsInput = ({ required={config?.required} /> )} + + {isCodeEditorOpen && ( + + )} > ); }; diff --git a/src/components/shared/ReactFlow/FlowCanvas/TaskNode/ArgumentsEditor/ArgumentInputField.tsx b/src/components/shared/ReactFlow/FlowCanvas/TaskNode/ArgumentsEditor/ArgumentInputField.tsx index e358e18d0..f669c6ff8 100644 --- a/src/components/shared/ReactFlow/FlowCanvas/TaskNode/ArgumentsEditor/ArgumentInputField.tsx +++ b/src/components/shared/ReactFlow/FlowCanvas/TaskNode/ArgumentsEditor/ArgumentInputField.tsx @@ -35,6 +35,8 @@ import { isGraphImplementation, } from "@/utils/componentSpec"; +import CodeViewer from "@/components/shared/CodeViewer/CodeViewer"; + import { ArgumentInputDialog } from "./ArgumentInputDialog"; import { DynamicDataArgumentInput } from "./DynamicDataArgumentInput"; import { DynamicDataDropdown } from "./DynamicDataDropdown"; @@ -64,6 +66,7 @@ interface PlainArgumentInputProps { onInputChange: (e: ChangeEvent) => void; onBlur: () => void; onExpand: () => void; + onOpenCodeEditor: () => void; onCopy: () => void; onReset: () => void; onRemove: () => void; @@ -87,6 +90,7 @@ const PlainArgumentInput = ({ onInputChange, onBlur, onExpand, + onOpenCodeEditor, onCopy, onReset, onRemove, @@ -154,6 +158,17 @@ const PlainArgumentInput = ({ > + + + {!disabledCopy && ( { + if (disabled) return; + setIsCodeEditorOpen(true); + }, [disabled]); + + const handleCodeEditorConfirm = useCallback( + (value: string) => { + setInputValue(value); + setIsCodeEditorOpen(false); + handleSubmit(value); + }, + [handleSubmit], + ); + + const handleCodeEditorCancel = useCallback(() => { + setIsCodeEditorOpen(false); + }, []); + const handleOpenSecretDialog = useCallback(() => { if (disabled) return; setIsSelectSecretDialogOpen(true); @@ -514,6 +548,7 @@ export const ArgumentInputField = ({ onInputChange={handleInputChange} onBlur={handleBlur} onExpand={handleExpand} + onOpenCodeEditor={handleOpenCodeEditor} onCopy={handleCopy} onReset={handleReset} onRemove={handleRemove} @@ -538,6 +573,15 @@ export const ArgumentInputField = ({ onOpenChange={setIsSelectSecretDialogOpen} onSelect={handleSecretSelect} /> + + {isCodeEditorOpen && ( + + )} > ); };