|
@@ -1,5 +1,8 @@
|
|
|
-import { X, Database } from 'lucide-react'
|
|
|
|
|
|
|
+import { useEffect, useState } from 'react'
|
|
|
|
|
+import { X, Database, Maximize2, Minimize2 } from 'lucide-react'
|
|
|
import { useQuery } from '@tanstack/react-query'
|
|
import { useQuery } from '@tanstack/react-query'
|
|
|
|
|
+import Editor from '@monaco-editor/react'
|
|
|
|
|
+import { useTheme } from '../../contexts/ThemeContext'
|
|
|
import { NodeDefinition } from '../../api/workflows'
|
|
import { NodeDefinition } from '../../api/workflows'
|
|
|
import { credentialsApi, CredentialInfo } from '../../api/credentials'
|
|
import { credentialsApi, CredentialInfo } from '../../api/credentials'
|
|
|
import { ConditionBuilder, Condition, AvailableField } from '../ConditionBuilder'
|
|
import { ConditionBuilder, Condition, AvailableField } from '../ConditionBuilder'
|
|
@@ -53,10 +56,30 @@ export function NodeConfigModal({
|
|
|
})
|
|
})
|
|
|
|
|
|
|
|
const credentials: CredentialInfo[] = credentialsData?.credentials || []
|
|
const credentials: CredentialInfo[] = credentialsData?.credentials || []
|
|
|
|
|
+ const { resolvedTheme } = useTheme()
|
|
|
|
|
+
|
|
|
|
|
+ // Editing a node is sometimes a one-line change and sometimes a hundred lines
|
|
|
|
|
+ // of script, so the size is the editor's choice and it is remembered.
|
|
|
|
|
+ const [maximized, setMaximized] = useState(
|
|
|
|
|
+ () => localStorage.getItem('nodeConfigMaximized') === 'true'
|
|
|
|
|
+ )
|
|
|
|
|
+
|
|
|
|
|
+ useEffect(() => {
|
|
|
|
|
+ localStorage.setItem('nodeConfigMaximized', String(maximized))
|
|
|
|
|
+ }, [maximized])
|
|
|
|
|
+
|
|
|
|
|
+ const sizeClasses = maximized
|
|
|
|
|
+ ? 'w-[96vw] h-[92vh] max-w-none'
|
|
|
|
|
+ : showDataPanel
|
|
|
|
|
+ ? 'w-full max-w-6xl max-h-[90vh]'
|
|
|
|
|
+ : 'w-full max-w-3xl max-h-[90vh]'
|
|
|
|
|
|
|
|
return (
|
|
return (
|
|
|
- <div className="fixed inset-0 bg-black/50 flex items-center justify-center z-50">
|
|
|
|
|
- <div className={`bg-white dark:bg-slate-800 rounded-xl max-h-[90vh] overflow-hidden flex ${showDataPanel ? 'w-full max-w-4xl' : 'w-full max-w-lg'}`}>
|
|
|
|
|
|
|
+ <div className="fixed inset-0 bg-black/50 flex items-center justify-center z-50 p-4">
|
|
|
|
|
+ <div
|
|
|
|
|
+ className={`bg-white dark:bg-slate-800 rounded-xl overflow-hidden flex resize ${sizeClasses}`}
|
|
|
|
|
+ style={{ minWidth: '32rem', minHeight: '20rem' }}
|
|
|
|
|
+ >
|
|
|
{/* Main configuration area */}
|
|
{/* Main configuration area */}
|
|
|
<div className="flex-1 flex flex-col overflow-hidden">
|
|
<div className="flex-1 flex flex-col overflow-hidden">
|
|
|
<div className="flex items-center justify-between p-4 border-b border-gray-200 dark:border-slate-700">
|
|
<div className="flex items-center justify-between p-4 border-b border-gray-200 dark:border-slate-700">
|
|
@@ -74,6 +97,13 @@ export function NodeConfigModal({
|
|
|
>
|
|
>
|
|
|
<Database className="w-5 h-5" />
|
|
<Database className="w-5 h-5" />
|
|
|
</button>
|
|
</button>
|
|
|
|
|
+ <button
|
|
|
|
|
+ onClick={() => setMaximized((current) => !current)}
|
|
|
|
|
+ className="p-2 rounded-lg hover:bg-gray-100 dark:hover:bg-slate-700 text-gray-500 dark:text-gray-400"
|
|
|
|
|
+ title={maximized ? 'Restore size' : 'Fill the window'}
|
|
|
|
|
+ >
|
|
|
|
|
+ {maximized ? <Minimize2 className="w-5 h-5" /> : <Maximize2 className="w-5 h-5" />}
|
|
|
|
|
+ </button>
|
|
|
<button
|
|
<button
|
|
|
onClick={onClose}
|
|
onClick={onClose}
|
|
|
className="p-1 hover:bg-gray-100 dark:hover:bg-slate-700 rounded"
|
|
className="p-1 hover:bg-gray-100 dark:hover:bg-slate-700 rounded"
|
|
@@ -423,14 +453,36 @@ export function NodeConfigModal({
|
|
|
className="w-full mt-2 px-3 py-2 border border-gray-200 dark:border-slate-600 rounded-lg font-mono text-xs bg-white dark:bg-slate-800 text-gray-900 dark:text-gray-100 focus:ring-2 focus:ring-primary-500 focus:border-primary-500"
|
|
className="w-full mt-2 px-3 py-2 border border-gray-200 dark:border-slate-600 rounded-lg font-mono text-xs bg-white dark:bg-slate-800 text-gray-900 dark:text-gray-100 focus:ring-2 focus:ring-primary-500 focus:border-primary-500"
|
|
|
/>
|
|
/>
|
|
|
</div>
|
|
</div>
|
|
|
|
|
+ ) : prop.format === 'code' ? (
|
|
|
|
|
+ /* Source code is read as much as it is written, so it gets
|
|
|
|
|
+ a real editor: highlighting, line numbers and a gutter. */
|
|
|
|
|
+ <div className="border border-gray-200 dark:border-slate-600 rounded-lg overflow-hidden">
|
|
|
|
|
+ <Editor
|
|
|
|
|
+ height={maximized ? '60vh' : '360px'}
|
|
|
|
|
+ language="javascript"
|
|
|
|
|
+ theme={resolvedTheme === 'dark' ? 'vs-dark' : 'light'}
|
|
|
|
|
+ value={editingConfig[key] ?? prop.default ?? ''}
|
|
|
|
|
+ onChange={(value) =>
|
|
|
|
|
+ onConfigChange({ ...editingConfig, [key]: value ?? '' })
|
|
|
|
|
+ }
|
|
|
|
|
+ options={{
|
|
|
|
|
+ minimap: { enabled: false },
|
|
|
|
|
+ fontSize: 13,
|
|
|
|
|
+ wordWrap: 'on',
|
|
|
|
|
+ scrollBeyondLastLine: false,
|
|
|
|
|
+ automaticLayout: true,
|
|
|
|
|
+ tabSize: 2,
|
|
|
|
|
+ padding: { top: 8, bottom: 8 },
|
|
|
|
|
+ }}
|
|
|
|
|
+ />
|
|
|
|
|
+ </div>
|
|
|
) : (
|
|
) : (
|
|
|
<ExpressionInput
|
|
<ExpressionInput
|
|
|
value={editingConfig[key] ?? prop.default ?? ''}
|
|
value={editingConfig[key] ?? prop.default ?? ''}
|
|
|
onChange={(value) =>
|
|
onChange={(value) =>
|
|
|
onConfigChange({ ...editingConfig, [key]: value })
|
|
onConfigChange({ ...editingConfig, [key]: value })
|
|
|
}
|
|
}
|
|
|
- multiline={prop.format === 'code' || prop.format === 'textarea'}
|
|
|
|
|
- codeMode={prop.format === 'code'}
|
|
|
|
|
|
|
+ multiline={prop.format === 'textarea'}
|
|
|
placeholder={prop.description}
|
|
placeholder={prop.description}
|
|
|
availableFields={getUpstreamOutputFields(selectedNodeData.id).map((f) => ({
|
|
availableFields={getUpstreamOutputFields(selectedNodeData.id).map((f) => ({
|
|
|
path: f.path,
|
|
path: f.path,
|