improvements
This commit is contained in:
61
src/App.tsx
61
src/App.tsx
@@ -33,7 +33,7 @@ import {
|
|||||||
} from "@/components/ui/context-menu"
|
} from "@/components/ui/context-menu"
|
||||||
import { Button } from '@/components/ui/button'
|
import { Button } from '@/components/ui/button'
|
||||||
import { ClipboardPaste, Code2, Moon, ScrollText, Sparkles, Sun, Variable } from 'lucide-react'
|
import { ClipboardPaste, Code2, Moon, ScrollText, Sparkles, Sun, Variable } from 'lucide-react'
|
||||||
import { DEFAULT_NODE_STYLE, genId, getDefaultDataForType } from './lib/flowUtils'
|
import { DEFAULT_NODE_STYLE, getDefaultDataForType, getNextNodeId } from './lib/flowUtils'
|
||||||
|
|
||||||
const SNAP_GRID: [number, number] = [15, 15]
|
const SNAP_GRID: [number, number] = [15, 15]
|
||||||
const snapToGrid = (x: number, y: number): { x: number; y: number } => ({
|
const snapToGrid = (x: number, y: number): { x: number; y: number } => ({
|
||||||
@@ -43,23 +43,23 @@ const snapToGrid = (x: number, y: number): { x: number; y: number } => ({
|
|||||||
|
|
||||||
const initialNodes: Node[] = [
|
const initialNodes: Node[] = [
|
||||||
{
|
{
|
||||||
id: 'config-1',
|
id: 'cfg_001',
|
||||||
position: { x: 50, y: 50 },
|
position: { x: 50, y: 50 },
|
||||||
data: { plantuml: '@startuml\nactor User\nparticipant "Render" as R\nUser -> R : config\n@enduml\n' },
|
data: { plantuml: '@startuml\nactor User\nparticipant "Render" as R\nUser -> R : config\n@enduml\n', title: 'config-cfg_001' },
|
||||||
type: 'config',
|
type: 'config',
|
||||||
style: DEFAULT_NODE_STYLE.config,
|
style: DEFAULT_NODE_STYLE.config,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: 'render-1',
|
id: 'rnd_001',
|
||||||
position: { x: 350, y: 80 },
|
position: { x: 350, y: 80 },
|
||||||
data: {},
|
data: {},
|
||||||
type: 'render',
|
type: 'render',
|
||||||
style: DEFAULT_NODE_STYLE.render,
|
style: DEFAULT_NODE_STYLE.render,
|
||||||
},
|
},
|
||||||
].map((n) => ({ ...n, id: n.id ?? genId() }))
|
]
|
||||||
|
|
||||||
const initialEdges: Edge[] = [
|
const initialEdges: Edge[] = [
|
||||||
{ id: 'e-config-render', source: 'config-1', target: 'render-1', type: 'animated' },
|
{ id: 'e-cfg_001-rnd_001', source: 'cfg_001', target: 'rnd_001', type: 'animated' },
|
||||||
]
|
]
|
||||||
|
|
||||||
export default function App() {
|
export default function App() {
|
||||||
@@ -67,6 +67,7 @@ export default function App() {
|
|||||||
const [nodes, setNodes, onNodesChange] = useNodesState(initialNodes)
|
const [nodes, setNodes, onNodesChange] = useNodesState(initialNodes)
|
||||||
const [edges, setEdges, onEdgesChange] = useEdgesState(initialEdges)
|
const [edges, setEdges, onEdgesChange] = useEdgesState(initialEdges)
|
||||||
const [rfInstance, setRfInstance] = React.useState<any | null>(null)
|
const [rfInstance, setRfInstance] = React.useState<any | null>(null)
|
||||||
|
const [renamingNodeId, setRenamingNodeId] = React.useState<string | null>(null)
|
||||||
|
|
||||||
const wrapperRef = React.useRef<HTMLDivElement | null>(null)
|
const wrapperRef = React.useRef<HTMLDivElement | null>(null)
|
||||||
const lastClickRef = React.useRef<{ clientX: number; clientY: number } | null>(null)
|
const lastClickRef = React.useRef<{ clientX: number; clientY: number } | null>(null)
|
||||||
@@ -123,7 +124,6 @@ export default function App() {
|
|||||||
(type: string) => {
|
(type: string) => {
|
||||||
const position = getMenuPosition()
|
const position = getMenuPosition()
|
||||||
if (position == null) return
|
if (position == null) return
|
||||||
const newId = genId()
|
|
||||||
const typeMap: Record<string, Node['type']> = {
|
const typeMap: Record<string, Node['type']> = {
|
||||||
config: 'config',
|
config: 'config',
|
||||||
render: 'render',
|
render: 'render',
|
||||||
@@ -131,15 +131,18 @@ export default function App() {
|
|||||||
function: 'function',
|
function: 'function',
|
||||||
}
|
}
|
||||||
const nodeType = typeMap[type] ?? 'config'
|
const nodeType = typeMap[type] ?? 'config'
|
||||||
const dataMap = getDefaultDataForType(nodeType, newId)
|
setNodes((nds) => {
|
||||||
const newNode: Node = {
|
const newId = getNextNodeId(nodeType, nds.map((n) => n.id))
|
||||||
id: newId,
|
const dataMap = getDefaultDataForType(nodeType, newId)
|
||||||
type: nodeType,
|
const newNode: Node = {
|
||||||
position: { x: position.x, y: position.y },
|
id: newId,
|
||||||
data: dataMap,
|
type: nodeType,
|
||||||
style: DEFAULT_NODE_STYLE[nodeType] ?? DEFAULT_NODE_STYLE.config,
|
position: { x: position.x, y: position.y },
|
||||||
}
|
data: dataMap,
|
||||||
setNodes((nds) => nds.concat(newNode))
|
style: DEFAULT_NODE_STYLE[nodeType] ?? DEFAULT_NODE_STYLE.config,
|
||||||
|
}
|
||||||
|
return nds.concat(newNode)
|
||||||
|
})
|
||||||
lastClickRef.current = null
|
lastClickRef.current = null
|
||||||
setContextTarget(null)
|
setContextTarget(null)
|
||||||
},
|
},
|
||||||
@@ -156,17 +159,19 @@ export default function App() {
|
|||||||
if (!text) return
|
if (!text) return
|
||||||
const raw = JSON.parse(text) as { id?: string; type?: string; data?: any; position?: { x: number; y: number }; style?: any }
|
const raw = JSON.parse(text) as { id?: string; type?: string; data?: any; position?: { x: number; y: number }; style?: any }
|
||||||
if (!raw || typeof raw.type !== 'string' || !VALID_NODE_TYPES.includes(raw.type as any)) return
|
if (!raw || typeof raw.type !== 'string' || !VALID_NODE_TYPES.includes(raw.type as any)) return
|
||||||
const newId = genId()
|
setNodes((nds) => {
|
||||||
const data = raw.data != null && typeof raw.data === 'object' ? { ...raw.data } : {}
|
const newId = getNextNodeId(raw.type as Node['type'], nds.map((n) => n.id))
|
||||||
if (raw.type === 'config' && data.title != null) data.title = `config-${newId}`
|
const data = raw.data != null && typeof raw.data === 'object' ? { ...raw.data } : {}
|
||||||
const newNode: Node = {
|
if (raw.type === 'config' && data.title != null) data.title = `config-${newId}`
|
||||||
id: newId,
|
const newNode: Node = {
|
||||||
type: raw.type as Node['type'],
|
id: newId,
|
||||||
position: { x: position.x, y: position.y },
|
type: raw.type as Node['type'],
|
||||||
data,
|
position: { x: position.x, y: position.y },
|
||||||
style: DEFAULT_NODE_STYLE[raw.type] ?? DEFAULT_NODE_STYLE.config,
|
data,
|
||||||
}
|
style: DEFAULT_NODE_STYLE[raw.type] ?? DEFAULT_NODE_STYLE.config,
|
||||||
setNodes((nds) => nds.concat(newNode))
|
}
|
||||||
|
return nds.concat(newNode)
|
||||||
|
})
|
||||||
lastClickRef.current = null
|
lastClickRef.current = null
|
||||||
setContextTarget(null)
|
setContextTarget(null)
|
||||||
} catch {
|
} catch {
|
||||||
@@ -199,7 +204,7 @@ export default function App() {
|
|||||||
>
|
>
|
||||||
{theme === 'dark' ? <Sun className="h-4 w-4" /> : <Moon className="h-4 w-4" />}
|
{theme === 'dark' ? <Sun className="h-4 w-4" /> : <Moon className="h-4 w-4" />}
|
||||||
</Button>
|
</Button>
|
||||||
<FlowContext.Provider value={{ nodes, setNodes, edges, setEdges }}>
|
<FlowContext.Provider value={{ nodes, setNodes, edges, setEdges, renamingNodeId, setRenamingNodeId }}>
|
||||||
<ContextMenu>
|
<ContextMenu>
|
||||||
<ContextMenuTrigger asChild>
|
<ContextMenuTrigger asChild>
|
||||||
<div style={{ width: '100%', height: '100%' }}>
|
<div style={{ width: '100%', height: '100%' }}>
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import React, { memo, useCallback, useContext, useEffect, useMemo, useRef, useState } from 'react'
|
import React, { memo, useCallback, useContext, useMemo, useRef } from 'react'
|
||||||
import { autocompletion } from '@codemirror/autocomplete'
|
import { autocompletion } from '@codemirror/autocomplete'
|
||||||
import CodeMirror from '@uiw/react-codemirror'
|
import CodeMirror from '@uiw/react-codemirror'
|
||||||
import FlowContext from '../../lib/flowContext'
|
import FlowContext from '../../lib/flowContext'
|
||||||
@@ -21,6 +21,7 @@ import {
|
|||||||
MenubarSubTrigger,
|
MenubarSubTrigger,
|
||||||
} from '../ui/menubar'
|
} from '../ui/menubar'
|
||||||
import { InputHandle, OutputHandle } from './NodeHandles'
|
import { InputHandle, OutputHandle } from './NodeHandles'
|
||||||
|
import { NodeHeaderTitle } from './NodeHeaderTitle'
|
||||||
import { NodeMenubar } from './NodeMenubar'
|
import { NodeMenubar } from './NodeMenubar'
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
@@ -30,12 +31,13 @@ type Props = {
|
|||||||
height?: number
|
height?: number
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const DEFAULT_PLANTUML = '@startuml\n\n@enduml\n'
|
||||||
|
|
||||||
export const ConfigNode = memo(function ConfigNode({ id, data, width, height }: Props) {
|
export const ConfigNode = memo(function ConfigNode({ id, data, width, height }: Props) {
|
||||||
const [value, setValue] = useState<string>(data?.plantuml ?? '@startuml\n\n@enduml\n')
|
const plantumlValue = data?.plantuml ?? DEFAULT_PLANTUML
|
||||||
const { theme } = useTheme()
|
const { theme } = useTheme()
|
||||||
const ctx = useContext(FlowContext)
|
const ctx = useContext(FlowContext)
|
||||||
const setNodes = ctx?.setNodes
|
const setNodes = ctx?.setNodes
|
||||||
const storedPlantuml = ctx?.nodes?.find((n: any) => n.id === id)?.data?.plantuml ?? ''
|
|
||||||
|
|
||||||
const editorRef = useRef<unknown>(null)
|
const editorRef = useRef<unknown>(null)
|
||||||
|
|
||||||
@@ -46,16 +48,8 @@ export const ConfigNode = memo(function ConfigNode({ id, data, width, height }:
|
|||||||
const connectedFunctionNodes = (ctx?.nodes ?? []).filter((n: any) => incomingIds.includes(n.id) && n.type === 'function')
|
const connectedFunctionNodes = (ctx?.nodes ?? []).filter((n: any) => incomingIds.includes(n.id) && n.type === 'function')
|
||||||
const hasDependencies = connectedConfigNodes.length > 0 || connectedVariableNodes.length > 0 || connectedFunctionNodes.length > 0
|
const hasDependencies = connectedConfigNodes.length > 0 || connectedVariableNodes.length > 0 || connectedFunctionNodes.length > 0
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
if (setNodes) {
|
|
||||||
setNodes((nds: any[]) => nds.map((n) => (n.id === id ? { ...n, data: { ...n.data, plantuml: value } } : n)))
|
|
||||||
}
|
|
||||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
||||||
}, [])
|
|
||||||
|
|
||||||
const onChange = useCallback(
|
const onChange = useCallback(
|
||||||
(val: string) => {
|
(val: string) => {
|
||||||
setValue(val)
|
|
||||||
if (setNodes) {
|
if (setNodes) {
|
||||||
setNodes((nds: any[]) => nds.map((n) => (n.id === id ? { ...n, data: { ...n.data, plantuml: val } } : n)))
|
setNodes((nds: any[]) => nds.map((n) => (n.id === id ? { ...n, data: { ...n.data, plantuml: val } } : n)))
|
||||||
}
|
}
|
||||||
@@ -85,12 +79,12 @@ export const ConfigNode = memo(function ConfigNode({ id, data, width, height }:
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
if (mode === 'prepend') {
|
if (mode === 'prepend') {
|
||||||
onChange(insertText + value)
|
onChange(insertText + plantumlValue)
|
||||||
} else {
|
} else {
|
||||||
onChange(value + insertText)
|
onChange(plantumlValue + insertText)
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
[onChange, value]
|
[onChange, plantumlValue]
|
||||||
)
|
)
|
||||||
|
|
||||||
const insertExtendsFromNode = useCallback(
|
const insertExtendsFromNode = useCallback(
|
||||||
@@ -146,6 +140,21 @@ export const ConfigNode = memo(function ConfigNode({ id, data, width, height }:
|
|||||||
)
|
)
|
||||||
const [editorHeight, editorContainerRef] = useResizeHeight(180)
|
const [editorHeight, editorContainerRef] = useResizeHeight(180)
|
||||||
|
|
||||||
|
const nunjucksTagSnippets = useMemo(
|
||||||
|
() => [
|
||||||
|
{ label: 'Variable {{ }}', snippet: '{{ }}' },
|
||||||
|
{ label: 'if / endif', snippet: '{% if %}\n \n{% endif %}' },
|
||||||
|
{ label: 'for / endfor', snippet: '{% for in %}\n \n{% endfor %}' },
|
||||||
|
{ label: 'set', snippet: '{% set = %}' },
|
||||||
|
{ label: 'block / endblock', snippet: '{% block %}\n \n{% endblock %}' },
|
||||||
|
{ label: 'extends', snippet: '{% extends "" %}' },
|
||||||
|
{ label: 'include', snippet: '{% include "" %}' },
|
||||||
|
{ label: 'import', snippet: '{% import "" as %}' },
|
||||||
|
{ label: 'raw / endraw', snippet: '{% raw %}\n \n{% endraw %}' },
|
||||||
|
],
|
||||||
|
[]
|
||||||
|
)
|
||||||
|
|
||||||
const dimensions =
|
const dimensions =
|
||||||
width != null && height != null && width > 0 && height > 0
|
width != null && height != null && width > 0 && height > 0
|
||||||
? { width, height }
|
? { width, height }
|
||||||
@@ -153,7 +162,7 @@ export const ConfigNode = memo(function ConfigNode({ id, data, width, height }:
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<BaseNode className="min-w-80 min-h-[280px]" dimensions={dimensions} resizable nodeId={id} handles={<><InputHandle id="ain" /><OutputHandle id="out" /></>}>
|
<BaseNode className="min-w-80 min-h-[280px]" dimensions={dimensions} resizable nodeId={id} handles={<><InputHandle id="ain" /><OutputHandle id="out" /></>}>
|
||||||
<BaseNodeHeaderRow icon={<ScrollText className="size-4" />} title={`${id}.puml`} />
|
<BaseNodeHeaderRow icon={<ScrollText className="size-4" />} title={<NodeHeaderTitle nodeId={id} displayTitle={`${id}.puml`} />} />
|
||||||
|
|
||||||
<BaseNodeContent>
|
<BaseNodeContent>
|
||||||
<div className="shrink-0 w-full">
|
<div className="shrink-0 w-full">
|
||||||
@@ -223,6 +232,19 @@ export const ConfigNode = memo(function ConfigNode({ id, data, width, height }:
|
|||||||
</>
|
</>
|
||||||
) : undefined
|
) : undefined
|
||||||
}
|
}
|
||||||
|
insertTagsContent={
|
||||||
|
<>
|
||||||
|
{nunjucksTagSnippets.map(({ label, snippet }) => (
|
||||||
|
<MenubarItem
|
||||||
|
key={label}
|
||||||
|
className="text-xs"
|
||||||
|
onClick={() => insertAt(snippet, 'cursor')}
|
||||||
|
>
|
||||||
|
{label}
|
||||||
|
</MenubarItem>
|
||||||
|
))}
|
||||||
|
</>
|
||||||
|
}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -230,7 +252,7 @@ export const ConfigNode = memo(function ConfigNode({ id, data, width, height }:
|
|||||||
<CodeMirror
|
<CodeMirror
|
||||||
// @ts-expect-error ref is { view, state, editor }; package ref type not in our node_modules
|
// @ts-expect-error ref is { view, state, editor }; package ref type not in our node_modules
|
||||||
ref={editorRef}
|
ref={editorRef}
|
||||||
value={value}
|
value={plantumlValue}
|
||||||
height={`${editorHeight}px`}
|
height={`${editorHeight}px`}
|
||||||
theme={theme}
|
theme={theme}
|
||||||
extensions={extensions}
|
extensions={extensions}
|
||||||
@@ -242,7 +264,7 @@ export const ConfigNode = memo(function ConfigNode({ id, data, width, height }:
|
|||||||
</BaseNodeContent>
|
</BaseNodeContent>
|
||||||
|
|
||||||
<BaseNodeFooter>
|
<BaseNodeFooter>
|
||||||
<BaseNodeFooterText>{storedPlantuml ? `${storedPlantuml.length} chars` : 'none'}</BaseNodeFooterText>
|
<BaseNodeFooterText>{plantumlValue ? `${plantumlValue.length} chars` : 'none'}</BaseNodeFooterText>
|
||||||
</BaseNodeFooter>
|
</BaseNodeFooter>
|
||||||
</BaseNode>
|
</BaseNode>
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import React, { memo, useCallback, useContext, useEffect, useMemo, useRef, useState } from 'react'
|
import React, { memo, useCallback, useContext, useMemo } from 'react'
|
||||||
import CodeMirror from '@uiw/react-codemirror'
|
import CodeMirror from '@uiw/react-codemirror'
|
||||||
import { javascript } from '@codemirror/lang-javascript'
|
import { javascript } from '@codemirror/lang-javascript'
|
||||||
import FlowContext from '../../lib/flowContext'
|
import FlowContext from '../../lib/flowContext'
|
||||||
@@ -12,6 +12,7 @@ import {
|
|||||||
BaseNodeHeaderRow,
|
BaseNodeHeaderRow,
|
||||||
} from './BaseNode'
|
} from './BaseNode'
|
||||||
import { InputHandle, OutputHandle } from './NodeHandles'
|
import { InputHandle, OutputHandle } from './NodeHandles'
|
||||||
|
import { NodeHeaderTitle } from './NodeHeaderTitle'
|
||||||
import { NodeMenubar } from './NodeMenubar'
|
import { NodeMenubar } from './NodeMenubar'
|
||||||
import { Code2 } from 'lucide-react'
|
import { Code2 } from 'lucide-react'
|
||||||
|
|
||||||
@@ -22,28 +23,14 @@ type Props = {
|
|||||||
height?: number
|
height?: number
|
||||||
}
|
}
|
||||||
|
|
||||||
const DEFAULT_BODY = `// args[0], args[1], ... are the variable values (in order)
|
|
||||||
return args[0];
|
|
||||||
`
|
|
||||||
|
|
||||||
export const FunctionNode = memo(function FunctionNode({ id, data, width, height }: Props) {
|
export const FunctionNode = memo(function FunctionNode({ id, data, width, height }: Props) {
|
||||||
const [value, setValue] = useState<string>(data?.body ?? DEFAULT_BODY)
|
const bodyValue = data?.body ?? ''
|
||||||
const { theme } = useTheme()
|
const { theme } = useTheme()
|
||||||
const ctx = useContext(FlowContext)
|
const ctx = useContext(FlowContext)
|
||||||
const setNodes = ctx?.setNodes
|
const setNodes = ctx?.setNodes
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
if (setNodes) {
|
|
||||||
setNodes((nds: any[]) =>
|
|
||||||
nds.map((n) => (n.id === id ? { ...n, data: { ...n.data, body: value } } : n))
|
|
||||||
)
|
|
||||||
}
|
|
||||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
||||||
}, [])
|
|
||||||
|
|
||||||
const onChange = useCallback(
|
const onChange = useCallback(
|
||||||
(val: string) => {
|
(val: string) => {
|
||||||
setValue(val)
|
|
||||||
if (setNodes) {
|
if (setNodes) {
|
||||||
setNodes((nds: any[]) =>
|
setNodes((nds: any[]) =>
|
||||||
nds.map((n) => (n.id === id ? { ...n, data: { ...n.data, body: val } } : n))
|
nds.map((n) => (n.id === id ? { ...n, data: { ...n.data, body: val } } : n))
|
||||||
@@ -53,8 +40,6 @@ export const FunctionNode = memo(function FunctionNode({ id, data, width, height
|
|||||||
[id, setNodes]
|
[id, setNodes]
|
||||||
)
|
)
|
||||||
|
|
||||||
const storedBody = ctx?.nodes?.find((n: any) => n.id === id)?.data?.body ?? ''
|
|
||||||
|
|
||||||
const extensions = useMemo(() => [javascript()], [])
|
const extensions = useMemo(() => [javascript()], [])
|
||||||
const [editorHeight, editorContainerRef] = useResizeHeight(120)
|
const [editorHeight, editorContainerRef] = useResizeHeight(120)
|
||||||
const dimensions =
|
const dimensions =
|
||||||
@@ -64,7 +49,7 @@ export const FunctionNode = memo(function FunctionNode({ id, data, width, height
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<BaseNode className="min-w-72 min-h-[260px]" dimensions={dimensions} resizable nodeId={id} handles={<><InputHandle id="in" /><OutputHandle id="out" /></>}>
|
<BaseNode className="min-w-72 min-h-[260px]" dimensions={dimensions} resizable nodeId={id} handles={<><InputHandle id="in" /><OutputHandle id="out" /></>}>
|
||||||
<BaseNodeHeaderRow icon={<Code2 className="size-4" />} title={id} />
|
<BaseNodeHeaderRow icon={<Code2 className="size-4" />} title={<NodeHeaderTitle nodeId={id} displayTitle={id} />} />
|
||||||
|
|
||||||
<BaseNodeContent>
|
<BaseNodeContent>
|
||||||
<div className="shrink-0 w-full mb-1">
|
<div className="shrink-0 w-full mb-1">
|
||||||
@@ -75,7 +60,7 @@ export const FunctionNode = memo(function FunctionNode({ id, data, width, height
|
|||||||
</p>
|
</p>
|
||||||
<div ref={editorContainerRef} className="min-h-0 flex-1 w-full nodrag nopan overflow-hidden rounded border border-input">
|
<div ref={editorContainerRef} className="min-h-0 flex-1 w-full nodrag nopan overflow-hidden rounded border border-input">
|
||||||
<CodeMirror
|
<CodeMirror
|
||||||
value={value}
|
value={bodyValue}
|
||||||
height={`${editorHeight}px`}
|
height={`${editorHeight}px`}
|
||||||
theme={theme}
|
theme={theme}
|
||||||
extensions={extensions}
|
extensions={extensions}
|
||||||
@@ -87,7 +72,7 @@ export const FunctionNode = memo(function FunctionNode({ id, data, width, height
|
|||||||
</BaseNodeContent>
|
</BaseNodeContent>
|
||||||
|
|
||||||
<BaseNodeFooter>
|
<BaseNodeFooter>
|
||||||
<BaseNodeFooterText>Body: {storedBody ? `${storedBody.length} chars` : 'none'}</BaseNodeFooterText>
|
<BaseNodeFooterText>Body: {bodyValue ? `${bodyValue.length} chars` : 'none'}</BaseNodeFooterText>
|
||||||
</BaseNodeFooter>
|
</BaseNodeFooter>
|
||||||
</BaseNode>
|
</BaseNode>
|
||||||
)
|
)
|
||||||
|
|||||||
82
src/components/graph/NodeHeaderTitle.tsx
Normal file
82
src/components/graph/NodeHeaderTitle.tsx
Normal file
@@ -0,0 +1,82 @@
|
|||||||
|
import React, { useCallback, useContext, useEffect, useRef, useState } from 'react'
|
||||||
|
import FlowContext from '../../lib/flowContext'
|
||||||
|
import { replaceNodeIdInGraph } from '../../lib/flowUtils'
|
||||||
|
|
||||||
|
type Props = {
|
||||||
|
nodeId: string
|
||||||
|
displayTitle: string
|
||||||
|
}
|
||||||
|
|
||||||
|
export function NodeHeaderTitle({ nodeId, displayTitle }: Props) {
|
||||||
|
const ctx = useContext(FlowContext)
|
||||||
|
const nodes = ctx?.nodes ?? []
|
||||||
|
const setNodes = ctx?.setNodes
|
||||||
|
const edges = ctx?.edges ?? []
|
||||||
|
const setEdges = ctx?.setEdges
|
||||||
|
const renamingNodeId = ctx?.renamingNodeId ?? null
|
||||||
|
const setRenamingNodeId = ctx?.setRenamingNodeId
|
||||||
|
|
||||||
|
const [inputValue, setInputValue] = useState(nodeId)
|
||||||
|
const inputRef = useRef<HTMLInputElement>(null)
|
||||||
|
|
||||||
|
const isRenaming = renamingNodeId === nodeId
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (isRenaming) {
|
||||||
|
setInputValue(nodeId)
|
||||||
|
inputRef.current?.focus()
|
||||||
|
inputRef.current?.select()
|
||||||
|
}
|
||||||
|
}, [isRenaming, nodeId])
|
||||||
|
|
||||||
|
const applyRename = useCallback(() => {
|
||||||
|
if (!setNodes || !setEdges || !setRenamingNodeId) return
|
||||||
|
const newId = inputValue.trim()
|
||||||
|
if (!newId || newId === nodeId) {
|
||||||
|
setRenamingNodeId(null)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
const existingIds = nodes.map((n: any) => n.id)
|
||||||
|
if (existingIds.includes(newId)) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
const { nodes: nextNodes, edges: nextEdges } = replaceNodeIdInGraph(nodes, edges, nodeId, newId)
|
||||||
|
setNodes(nextNodes)
|
||||||
|
setEdges(nextEdges)
|
||||||
|
setRenamingNodeId(null)
|
||||||
|
}, [nodeId, inputValue, nodes, edges, setNodes, setEdges, setRenamingNodeId])
|
||||||
|
|
||||||
|
const cancelRename = useCallback(() => {
|
||||||
|
setRenamingNodeId?.(null)
|
||||||
|
}, [setRenamingNodeId])
|
||||||
|
|
||||||
|
const handleKeyDown = useCallback(
|
||||||
|
(e: React.KeyboardEvent<HTMLInputElement>) => {
|
||||||
|
if (e.key === 'Enter') {
|
||||||
|
e.preventDefault()
|
||||||
|
applyRename()
|
||||||
|
} else if (e.key === 'Escape') {
|
||||||
|
e.preventDefault()
|
||||||
|
cancelRename()
|
||||||
|
}
|
||||||
|
},
|
||||||
|
[applyRename, cancelRename]
|
||||||
|
)
|
||||||
|
|
||||||
|
if (!isRenaming) {
|
||||||
|
return <>{displayTitle}</>
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<input
|
||||||
|
ref={inputRef}
|
||||||
|
type="text"
|
||||||
|
value={inputValue}
|
||||||
|
onChange={(e) => setInputValue(e.target.value)}
|
||||||
|
onKeyDown={handleKeyDown}
|
||||||
|
onBlur={cancelRename}
|
||||||
|
className="nodrag nopan flex-1 min-w-0 rounded border border-input bg-background px-1.5 py-0 text-sm font-semibold outline-none focus:ring-1 focus:ring-ring"
|
||||||
|
data-slot="base-node-title"
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -1,11 +1,12 @@
|
|||||||
import React, { useCallback, useContext } from 'react'
|
import React, { useCallback, useContext } from 'react'
|
||||||
import FlowContext from '../../lib/flowContext'
|
import FlowContext from '../../lib/flowContext'
|
||||||
import { DEFAULT_NODE_STYLE, genId, getDefaultDataForType } from '../../lib/flowUtils'
|
import { DEFAULT_NODE_STYLE, getNextNodeId, getResetDataForType } from '../../lib/flowUtils'
|
||||||
import {
|
import {
|
||||||
Menubar,
|
Menubar,
|
||||||
MenubarContent,
|
MenubarContent,
|
||||||
MenubarItem,
|
MenubarItem,
|
||||||
MenubarMenu,
|
MenubarMenu,
|
||||||
|
MenubarSeparator,
|
||||||
MenubarSub,
|
MenubarSub,
|
||||||
MenubarSubContent,
|
MenubarSubContent,
|
||||||
MenubarSubTrigger,
|
MenubarSubTrigger,
|
||||||
@@ -19,11 +20,13 @@ type NodeType = 'config' | 'render' | 'variable' | 'function'
|
|||||||
type Props = {
|
type Props = {
|
||||||
nodeId: string
|
nodeId: string
|
||||||
nodeType: NodeType
|
nodeType: NodeType
|
||||||
/** Content for Edit → Inputs (config and function nodes only) */
|
/** Content for Insert → Inputs (config and function nodes only) */
|
||||||
editInputsContent?: React.ReactNode
|
editInputsContent?: React.ReactNode
|
||||||
|
/** Content for Insert → Tags (e.g. Nunjucks tag snippets, config nodes) */
|
||||||
|
insertTagsContent?: React.ReactNode
|
||||||
}
|
}
|
||||||
|
|
||||||
export function NodeMenubar({ nodeId, nodeType, editInputsContent }: Props) {
|
export function NodeMenubar({ nodeId, nodeType, editInputsContent, insertTagsContent }: Props) {
|
||||||
const ctx = useContext(FlowContext)
|
const ctx = useContext(FlowContext)
|
||||||
const nodes = ctx?.nodes ?? []
|
const nodes = ctx?.nodes ?? []
|
||||||
const setNodes = ctx?.setNodes
|
const setNodes = ctx?.setNodes
|
||||||
@@ -36,17 +39,19 @@ export function NodeMenubar({ nodeId, nodeType, editInputsContent }: Props) {
|
|||||||
|
|
||||||
const onDuplicate = useCallback(() => {
|
const onDuplicate = useCallback(() => {
|
||||||
if (!setNodes || !node) return
|
if (!setNodes || !node) return
|
||||||
const newId = genId()
|
|
||||||
const pos = node.position ?? { x: 0, y: 0 }
|
const pos = node.position ?? { x: 0, y: 0 }
|
||||||
const newNode = {
|
setNodes((nds: any[]) => {
|
||||||
id: newId,
|
const newId = getNextNodeId(nodeType, nds.map((n: any) => n.id))
|
||||||
type: node.type,
|
const newNode = {
|
||||||
position: { x: pos.x + DUPLICATE_OFFSET.x, y: pos.y + DUPLICATE_OFFSET.y },
|
id: newId,
|
||||||
data: typeof node.data === 'object' && node.data !== null ? { ...node.data } : node.data,
|
type: node.type,
|
||||||
style: DEFAULT_NODE_STYLE[nodeType] ?? DEFAULT_NODE_STYLE.config,
|
position: { x: pos.x + DUPLICATE_OFFSET.x, y: pos.y + DUPLICATE_OFFSET.y },
|
||||||
}
|
data: typeof node.data === 'object' && node.data !== null ? { ...node.data } : node.data,
|
||||||
if (newNode.data?.title && nodeType === 'config') newNode.data.title = `config-${newId}`
|
style: DEFAULT_NODE_STYLE[nodeType] ?? DEFAULT_NODE_STYLE.config,
|
||||||
setNodes((nds: any[]) => nds.concat(newNode))
|
}
|
||||||
|
if (newNode.data?.title && nodeType === 'config') newNode.data.title = `config-${newId}`
|
||||||
|
return nds.concat(newNode)
|
||||||
|
})
|
||||||
}, [node, nodeType, setNodes])
|
}, [node, nodeType, setNodes])
|
||||||
|
|
||||||
const onCopy = useCallback(() => {
|
const onCopy = useCallback(() => {
|
||||||
@@ -57,9 +62,9 @@ export function NodeMenubar({ nodeId, nodeType, editInputsContent }: Props) {
|
|||||||
|
|
||||||
const onReset = useCallback(() => {
|
const onReset = useCallback(() => {
|
||||||
if (!setNodes) return
|
if (!setNodes) return
|
||||||
const defaultData = getDefaultDataForType(nodeType, nodeId)
|
const resetData = getResetDataForType(nodeType, nodeId)
|
||||||
setNodes((nds: any[]) =>
|
setNodes((nds: any[]) =>
|
||||||
nds.map((n) => (n.id === nodeId ? { ...n, data: defaultData } : n))
|
nds.map((n) => (n.id === nodeId ? { ...n, data: resetData } : n))
|
||||||
)
|
)
|
||||||
}, [nodeId, nodeType, setNodes])
|
}, [nodeId, nodeType, setNodes])
|
||||||
|
|
||||||
@@ -69,11 +74,15 @@ export function NodeMenubar({ nodeId, nodeType, editInputsContent }: Props) {
|
|||||||
setEdges((eds: any[]) => eds.filter((e: any) => e.source !== nodeId && e.target !== nodeId))
|
setEdges((eds: any[]) => eds.filter((e: any) => e.source !== nodeId && e.target !== nodeId))
|
||||||
}, [nodeId, setNodes, setEdges])
|
}, [nodeId, setNodes, setEdges])
|
||||||
|
|
||||||
|
const onRename = useCallback(() => {
|
||||||
|
ctx?.setRenamingNodeId?.(nodeId)
|
||||||
|
}, [nodeId, ctx])
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Menubar className="h-auto min-h-0 flex items-center bg-none p-1 border-none shadow-none">
|
<Menubar className="h-auto min-h-0 flex items-center bg-none p-1 border-none shadow-none">
|
||||||
<MenubarMenu>
|
<MenubarMenu>
|
||||||
<MenubarTrigger className="px-1.5 py-0 text-xs">
|
<MenubarTrigger className="px-1.5 py-0 text-xs">
|
||||||
File
|
Node
|
||||||
</MenubarTrigger>
|
</MenubarTrigger>
|
||||||
<MenubarContent className="min-w-[10rem]">
|
<MenubarContent className="min-w-[10rem]">
|
||||||
<MenubarItem className="text-xs" onClick={onDuplicate}>
|
<MenubarItem className="text-xs" onClick={onDuplicate}>
|
||||||
@@ -82,9 +91,13 @@ export function NodeMenubar({ nodeId, nodeType, editInputsContent }: Props) {
|
|||||||
<MenubarItem className="text-xs" onClick={onCopy}>
|
<MenubarItem className="text-xs" onClick={onCopy}>
|
||||||
Copy
|
Copy
|
||||||
</MenubarItem>
|
</MenubarItem>
|
||||||
|
<MenubarItem className="text-xs" onClick={onRename}>
|
||||||
|
Rename
|
||||||
|
</MenubarItem>
|
||||||
<MenubarItem className="text-xs" onClick={onReset}>
|
<MenubarItem className="text-xs" onClick={onReset}>
|
||||||
Reset
|
Reset
|
||||||
</MenubarItem>
|
</MenubarItem>
|
||||||
|
<MenubarSeparator />
|
||||||
<MenubarItem className="text-xs text-destructive focus:text-destructive" onClick={onDelete}>
|
<MenubarItem className="text-xs text-destructive focus:text-destructive" onClick={onDelete}>
|
||||||
Delete
|
Delete
|
||||||
</MenubarItem>
|
</MenubarItem>
|
||||||
@@ -93,7 +106,7 @@ export function NodeMenubar({ nodeId, nodeType, editInputsContent }: Props) {
|
|||||||
{hasEdit && (
|
{hasEdit && (
|
||||||
<MenubarMenu>
|
<MenubarMenu>
|
||||||
<MenubarTrigger className="px-1.5 py-0 text-xs">
|
<MenubarTrigger className="px-1.5 py-0 text-xs">
|
||||||
Edit
|
Insert
|
||||||
</MenubarTrigger>
|
</MenubarTrigger>
|
||||||
<MenubarContent className="min-w-[10rem]">
|
<MenubarContent className="min-w-[10rem]">
|
||||||
<MenubarSub>
|
<MenubarSub>
|
||||||
@@ -104,6 +117,16 @@ export function NodeMenubar({ nodeId, nodeType, editInputsContent }: Props) {
|
|||||||
{editInputsContent}
|
{editInputsContent}
|
||||||
</MenubarSubContent>
|
</MenubarSubContent>
|
||||||
</MenubarSub>
|
</MenubarSub>
|
||||||
|
{insertTagsContent != null && (
|
||||||
|
<MenubarSub>
|
||||||
|
<MenubarSubTrigger className="text-xs">
|
||||||
|
Tags
|
||||||
|
</MenubarSubTrigger>
|
||||||
|
<MenubarSubContent className="min-w-[12rem]">
|
||||||
|
{insertTagsContent}
|
||||||
|
</MenubarSubContent>
|
||||||
|
</MenubarSub>
|
||||||
|
)}
|
||||||
</MenubarContent>
|
</MenubarContent>
|
||||||
</MenubarMenu>
|
</MenubarMenu>
|
||||||
)}
|
)}
|
||||||
|
|||||||
@@ -10,6 +10,8 @@ import {
|
|||||||
BaseNodeFooterText,
|
BaseNodeFooterText,
|
||||||
BaseNodeHeaderRow,
|
BaseNodeHeaderRow,
|
||||||
} from './BaseNode'
|
} from './BaseNode'
|
||||||
|
import { DEFAULT_NODE_STYLE, getDefaultDataForType, getNextNodeId } from '../../lib/flowUtils'
|
||||||
|
import { NodeHeaderTitle } from './NodeHeaderTitle'
|
||||||
import { NodeMenubar } from './NodeMenubar'
|
import { NodeMenubar } from './NodeMenubar'
|
||||||
import { Sparkles } from 'lucide-react'
|
import { Sparkles } from 'lucide-react'
|
||||||
import { InputHandle, OutputHandle } from './NodeHandles'
|
import { InputHandle, OutputHandle } from './NodeHandles'
|
||||||
@@ -23,8 +25,6 @@ type Props = {
|
|||||||
style?: React.CSSProperties
|
style?: React.CSSProperties
|
||||||
}
|
}
|
||||||
|
|
||||||
const DEFAULT_CONFIG_NODE_STYLE = { width: 320, height: 320 }
|
|
||||||
|
|
||||||
const DARK_SKINPARAMS = `
|
const DARK_SKINPARAMS = `
|
||||||
skinparam backgroundColor #1e1e1e
|
skinparam backgroundColor #1e1e1e
|
||||||
skinparam defaultFontColor #e0e0e0
|
skinparam defaultFontColor #e0e0e0
|
||||||
@@ -357,7 +357,7 @@ export const RenderingNode = memo(function RenderingNode({ id, width, height }:
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<BaseNode className="min-w-96 min-h-[320px]" dimensions={dimensions} resizable nodeId={id} handles={<><InputHandle id="ain" /><OutputHandle id="out" /></>}>
|
<BaseNode className="min-w-96 min-h-[320px]" dimensions={dimensions} resizable nodeId={id} handles={<><InputHandle id="ain" /><OutputHandle id="out" /></>}>
|
||||||
<BaseNodeHeaderRow icon={<Sparkles className="size-4" />} title={id} />
|
<BaseNodeHeaderRow icon={<Sparkles className="size-4" />} title={<NodeHeaderTitle nodeId={id} displayTitle={id} />} />
|
||||||
|
|
||||||
<BaseNodeContent>
|
<BaseNodeContent>
|
||||||
<div className="shrink-0 w-full">
|
<div className="shrink-0 w-full">
|
||||||
@@ -378,15 +378,13 @@ export const RenderingNode = memo(function RenderingNode({ id, width, height }:
|
|||||||
className="inline-flex items-center rounded bg-primary px-3 py-1 text-xs text-primary-foreground"
|
className="inline-flex items-center rounded bg-primary px-3 py-1 text-xs text-primary-foreground"
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
if (!setNodes || !setEdges) return
|
if (!setNodes || !setEdges) return
|
||||||
const genId = () => `node_${Math.random().toString(36).slice(2, 9)}`
|
const nid = getNextNodeId('config', nodes.map((n: any) => n.id))
|
||||||
const nid = genId()
|
|
||||||
const thisNode = nodes.find((n: any) => n.id === id)
|
const thisNode = nodes.find((n: any) => n.id === id)
|
||||||
const pos = thisNode?.position ?? { x: 0, y: 0 }
|
const pos = thisNode?.position ?? { x: 0, y: 0 }
|
||||||
const newPos = { x: pos.x - 220, y: pos.y }
|
const newPos = { x: pos.x - 220, y: pos.y }
|
||||||
const newNode = { id: nid, type: 'config', position: newPos, data: { plantuml: '@startuml\n\n@enduml\n', title: `config-${nid}` }, style: DEFAULT_CONFIG_NODE_STYLE }
|
const newNode = { id: nid, type: 'config', position: newPos, data: getDefaultDataForType('config', nid), style: DEFAULT_NODE_STYLE.config }
|
||||||
setNodes((nds: any[]) => nds.concat(newNode))
|
setNodes((nds: any[]) => nds.concat(newNode))
|
||||||
const edgeId = `e-${nid}-${id}`
|
setEdges((eds: any[]) => eds.concat({ id: `e-${nid}-${id}`, source: nid, target: id }))
|
||||||
setEdges((eds: any[]) => eds.concat({ id: edgeId, source: nid, target: id }))
|
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
Create Config
|
Create Config
|
||||||
|
|||||||
@@ -11,6 +11,7 @@ import { NodeMenubar } from './NodeMenubar'
|
|||||||
import { Input } from '../ui/input'
|
import { Input } from '../ui/input'
|
||||||
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '../ui/select'
|
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '../ui/select'
|
||||||
import { Switch } from '../ui/switch'
|
import { Switch } from '../ui/switch'
|
||||||
|
import { NodeHeaderTitle } from './NodeHeaderTitle'
|
||||||
import { OutputHandle } from './NodeHandles'
|
import { OutputHandle } from './NodeHandles'
|
||||||
import { Variable } from 'lucide-react'
|
import { Variable } from 'lucide-react'
|
||||||
|
|
||||||
@@ -91,7 +92,7 @@ export const VariableNode = memo(function VariableNode({ id, data }: Props) {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<BaseNode className="min-w-56 min-h-[180px]" handles={<OutputHandle id="out" />}>
|
<BaseNode className="min-w-56 min-h-[180px]" handles={<OutputHandle id="out" />}>
|
||||||
<BaseNodeHeaderRow icon={<Variable className="size-4" />} title={id} />
|
<BaseNodeHeaderRow icon={<Variable className="size-4" />} title={<NodeHeaderTitle nodeId={id} displayTitle={id} />} />
|
||||||
|
|
||||||
<BaseNodeContent className="gap-2 p-3">
|
<BaseNodeContent className="gap-2 p-3">
|
||||||
<div className="shrink-0 w-full">
|
<div className="shrink-0 w-full">
|
||||||
|
|||||||
@@ -5,6 +5,8 @@ export type FlowContextValue = {
|
|||||||
setNodes: (updater: any) => void
|
setNodes: (updater: any) => void
|
||||||
edges: any[]
|
edges: any[]
|
||||||
setEdges: (updater: any) => void
|
setEdges: (updater: any) => void
|
||||||
|
renamingNodeId: string | null
|
||||||
|
setRenamingNodeId: (id: string | null) => void
|
||||||
}
|
}
|
||||||
|
|
||||||
const FlowContext = React.createContext<FlowContextValue | null>(null)
|
const FlowContext = React.createContext<FlowContextValue | null>(null)
|
||||||
|
|||||||
@@ -1,5 +1,50 @@
|
|||||||
/** Generate short unique node id */
|
export const PREFIX_BY_TYPE: Record<string, string> = {
|
||||||
export const genId = () => `node_${Math.random().toString(36).slice(2, 9)}`
|
config: 'cfg_',
|
||||||
|
render: 'rnd_',
|
||||||
|
variable: 'var_',
|
||||||
|
function: 'fn_',
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Next node id for type: prefix + 3-digit increasing number (001, 002, …) */
|
||||||
|
export function getNextNodeId(type: string, existingIds: string[]): string {
|
||||||
|
const prefix = PREFIX_BY_TYPE[type] ?? 'node_'
|
||||||
|
const re = new RegExp(`^${prefix.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')}(\\d+)$`)
|
||||||
|
let max = 0
|
||||||
|
for (const id of existingIds) {
|
||||||
|
const m = id.match(re)
|
||||||
|
if (m) max = Math.max(max, parseInt(m[1], 10))
|
||||||
|
}
|
||||||
|
return `${prefix}${String(max + 1).padStart(3, '0')}`
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Recursively replace oldId with newId in string values (for rename propagation) */
|
||||||
|
function replaceInData(value: unknown, oldId: string, newId: string): unknown {
|
||||||
|
if (typeof value === 'string') return value.split(oldId).join(newId)
|
||||||
|
if (value === null || typeof value !== 'object') return value
|
||||||
|
if (Array.isArray(value)) return value.map((v) => replaceInData(v, oldId, newId))
|
||||||
|
const out: Record<string, unknown> = {}
|
||||||
|
for (const [k, v] of Object.entries(value)) out[k] = replaceInData(v, oldId, newId)
|
||||||
|
return out
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Update graph after renaming a node: change node id and all references in data and edges */
|
||||||
|
export function replaceNodeIdInGraph(
|
||||||
|
nodes: Array<{ id: string; data?: any; [k: string]: any }>,
|
||||||
|
edges: Array<{ id: string; source: string; target: string; [k: string]: any }>,
|
||||||
|
oldId: string,
|
||||||
|
newId: string
|
||||||
|
): { nodes: typeof nodes; edges: typeof edges } {
|
||||||
|
const newNodes = nodes.map((n) =>
|
||||||
|
n.id === oldId ? { ...n, id: newId } : { ...n, data: replaceInData(n.data, oldId, newId) as any }
|
||||||
|
)
|
||||||
|
const newEdges = edges.map((e) => ({
|
||||||
|
...e,
|
||||||
|
id: e.id.includes(oldId) ? e.id.split(oldId).join(newId) : e.id,
|
||||||
|
source: e.source === oldId ? newId : e.source,
|
||||||
|
target: e.target === oldId ? newId : e.target,
|
||||||
|
}))
|
||||||
|
return { nodes: newNodes, edges: newEdges }
|
||||||
|
}
|
||||||
|
|
||||||
export const DEFAULT_NODE_STYLE: Record<string, { width: number; height: number }> = {
|
export const DEFAULT_NODE_STYLE: Record<string, { width: number; height: number }> = {
|
||||||
config: { width: 320, height: 320 },
|
config: { width: 320, height: 320 },
|
||||||
@@ -20,3 +65,14 @@ export function getDefaultDataForType(type: string, newId?: string): any {
|
|||||||
if (type === 'config' && newId) base.title = `config-${newId}`
|
if (type === 'config' && newId) base.title = `config-${newId}`
|
||||||
return base
|
return base
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** Data for Reset action: clears code completely for config/function; same as default for others */
|
||||||
|
export function getResetDataForType(type: string, nodeId?: string): any {
|
||||||
|
if (type === 'config') {
|
||||||
|
return { plantuml: '@startuml\n\n@enduml\n', title: nodeId ? `config-${nodeId}` : '' }
|
||||||
|
}
|
||||||
|
if (type === 'function') {
|
||||||
|
return { body: '' }
|
||||||
|
}
|
||||||
|
return getDefaultDataForType(type, nodeId)
|
||||||
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user