diff --git a/src/App.tsx b/src/App.tsx index 44f5233..f5a5739 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -33,7 +33,7 @@ import { } from "@/components/ui/context-menu" import { Button } from '@/components/ui/button' 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 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[] = [ { - id: 'config-1', + id: 'cfg_001', 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', style: DEFAULT_NODE_STYLE.config, }, { - id: 'render-1', + id: 'rnd_001', position: { x: 350, y: 80 }, data: {}, type: 'render', style: DEFAULT_NODE_STYLE.render, }, -].map((n) => ({ ...n, id: n.id ?? genId() })) +] 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() { @@ -67,6 +67,7 @@ export default function App() { const [nodes, setNodes, onNodesChange] = useNodesState(initialNodes) const [edges, setEdges, onEdgesChange] = useEdgesState(initialEdges) const [rfInstance, setRfInstance] = React.useState(null) + const [renamingNodeId, setRenamingNodeId] = React.useState(null) const wrapperRef = React.useRef(null) const lastClickRef = React.useRef<{ clientX: number; clientY: number } | null>(null) @@ -123,7 +124,6 @@ export default function App() { (type: string) => { const position = getMenuPosition() if (position == null) return - const newId = genId() const typeMap: Record = { config: 'config', render: 'render', @@ -131,15 +131,18 @@ export default function App() { function: 'function', } const nodeType = typeMap[type] ?? 'config' - const dataMap = getDefaultDataForType(nodeType, newId) - const newNode: Node = { - id: newId, - type: nodeType, - position: { x: position.x, y: position.y }, - data: dataMap, - style: DEFAULT_NODE_STYLE[nodeType] ?? DEFAULT_NODE_STYLE.config, - } - setNodes((nds) => nds.concat(newNode)) + setNodes((nds) => { + const newId = getNextNodeId(nodeType, nds.map((n) => n.id)) + const dataMap = getDefaultDataForType(nodeType, newId) + const newNode: Node = { + id: newId, + type: nodeType, + position: { x: position.x, y: position.y }, + data: dataMap, + style: DEFAULT_NODE_STYLE[nodeType] ?? DEFAULT_NODE_STYLE.config, + } + return nds.concat(newNode) + }) lastClickRef.current = null setContextTarget(null) }, @@ -156,17 +159,19 @@ export default function App() { if (!text) return 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 - const newId = genId() - const data = raw.data != null && typeof raw.data === 'object' ? { ...raw.data } : {} - if (raw.type === 'config' && data.title != null) data.title = `config-${newId}` - const newNode: Node = { - id: newId, - type: raw.type as Node['type'], - position: { x: position.x, y: position.y }, - data, - style: DEFAULT_NODE_STYLE[raw.type] ?? DEFAULT_NODE_STYLE.config, - } - setNodes((nds) => nds.concat(newNode)) + setNodes((nds) => { + const newId = getNextNodeId(raw.type as Node['type'], nds.map((n) => n.id)) + const data = raw.data != null && typeof raw.data === 'object' ? { ...raw.data } : {} + if (raw.type === 'config' && data.title != null) data.title = `config-${newId}` + const newNode: Node = { + id: newId, + type: raw.type as Node['type'], + position: { x: position.x, y: position.y }, + data, + style: DEFAULT_NODE_STYLE[raw.type] ?? DEFAULT_NODE_STYLE.config, + } + return nds.concat(newNode) + }) lastClickRef.current = null setContextTarget(null) } catch { @@ -199,7 +204,7 @@ export default function App() { > {theme === 'dark' ? : } - +
diff --git a/src/components/graph/ConfigNode.tsx b/src/components/graph/ConfigNode.tsx index c0b0f69..5bcc14c 100644 --- a/src/components/graph/ConfigNode.tsx +++ b/src/components/graph/ConfigNode.tsx @@ -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 CodeMirror from '@uiw/react-codemirror' import FlowContext from '../../lib/flowContext' @@ -21,6 +21,7 @@ import { MenubarSubTrigger, } from '../ui/menubar' import { InputHandle, OutputHandle } from './NodeHandles' +import { NodeHeaderTitle } from './NodeHeaderTitle' import { NodeMenubar } from './NodeMenubar' type Props = { @@ -30,12 +31,13 @@ type Props = { height?: number } +const DEFAULT_PLANTUML = '@startuml\n\n@enduml\n' + export const ConfigNode = memo(function ConfigNode({ id, data, width, height }: Props) { - const [value, setValue] = useState(data?.plantuml ?? '@startuml\n\n@enduml\n') + const plantumlValue = data?.plantuml ?? DEFAULT_PLANTUML const { theme } = useTheme() const ctx = useContext(FlowContext) const setNodes = ctx?.setNodes - const storedPlantuml = ctx?.nodes?.find((n: any) => n.id === id)?.data?.plantuml ?? '' const editorRef = useRef(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 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( (val: string) => { - setValue(val) if (setNodes) { 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 } if (mode === 'prepend') { - onChange(insertText + value) + onChange(insertText + plantumlValue) } else { - onChange(value + insertText) + onChange(plantumlValue + insertText) } }, - [onChange, value] + [onChange, plantumlValue] ) const insertExtendsFromNode = useCallback( @@ -146,6 +140,21 @@ export const ConfigNode = memo(function ConfigNode({ id, data, width, height }: ) 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 = width != null && height != null && width > 0 && height > 0 ? { width, height } @@ -153,7 +162,7 @@ export const ConfigNode = memo(function ConfigNode({ id, data, width, height }: return ( }> - } title={`${id}.puml`} /> + } title={} />
@@ -223,6 +232,19 @@ export const ConfigNode = memo(function ConfigNode({ id, data, width, height }: ) : undefined } + insertTagsContent={ + <> + {nunjucksTagSnippets.map(({ label, snippet }) => ( + insertAt(snippet, 'cursor')} + > + {label} + + ))} + + } />
@@ -230,7 +252,7 @@ export const ConfigNode = memo(function ConfigNode({ id, data, width, height }: - {storedPlantuml ? `${storedPlantuml.length} chars` : 'none'} + {plantumlValue ? `${plantumlValue.length} chars` : 'none'}
) diff --git a/src/components/graph/FunctionNode.tsx b/src/components/graph/FunctionNode.tsx index db0b577..69971ba 100644 --- a/src/components/graph/FunctionNode.tsx +++ b/src/components/graph/FunctionNode.tsx @@ -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 { javascript } from '@codemirror/lang-javascript' import FlowContext from '../../lib/flowContext' @@ -12,6 +12,7 @@ import { BaseNodeHeaderRow, } from './BaseNode' import { InputHandle, OutputHandle } from './NodeHandles' +import { NodeHeaderTitle } from './NodeHeaderTitle' import { NodeMenubar } from './NodeMenubar' import { Code2 } from 'lucide-react' @@ -22,28 +23,14 @@ type Props = { 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) { - const [value, setValue] = useState(data?.body ?? DEFAULT_BODY) + const bodyValue = data?.body ?? '' const { theme } = useTheme() const ctx = useContext(FlowContext) 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( (val: string) => { - setValue(val) if (setNodes) { setNodes((nds: any[]) => 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] ) - const storedBody = ctx?.nodes?.find((n: any) => n.id === id)?.data?.body ?? '' - const extensions = useMemo(() => [javascript()], []) const [editorHeight, editorContainerRef] = useResizeHeight(120) const dimensions = @@ -64,7 +49,7 @@ export const FunctionNode = memo(function FunctionNode({ id, data, width, height return ( }> - } title={id} /> + } title={} />
@@ -75,7 +60,7 @@ export const FunctionNode = memo(function FunctionNode({ id, data, width, height

- Body: {storedBody ? `${storedBody.length} chars` : 'none'} + Body: {bodyValue ? `${bodyValue.length} chars` : 'none'} ) diff --git a/src/components/graph/NodeHeaderTitle.tsx b/src/components/graph/NodeHeaderTitle.tsx new file mode 100644 index 0000000..053bc94 --- /dev/null +++ b/src/components/graph/NodeHeaderTitle.tsx @@ -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(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) => { + if (e.key === 'Enter') { + e.preventDefault() + applyRename() + } else if (e.key === 'Escape') { + e.preventDefault() + cancelRename() + } + }, + [applyRename, cancelRename] + ) + + if (!isRenaming) { + return <>{displayTitle} + } + + return ( + 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" + /> + ) +} diff --git a/src/components/graph/NodeMenubar.tsx b/src/components/graph/NodeMenubar.tsx index 80eb51f..1cf20ca 100644 --- a/src/components/graph/NodeMenubar.tsx +++ b/src/components/graph/NodeMenubar.tsx @@ -1,11 +1,12 @@ import React, { useCallback, useContext } from 'react' import FlowContext from '../../lib/flowContext' -import { DEFAULT_NODE_STYLE, genId, getDefaultDataForType } from '../../lib/flowUtils' +import { DEFAULT_NODE_STYLE, getNextNodeId, getResetDataForType } from '../../lib/flowUtils' import { Menubar, MenubarContent, MenubarItem, MenubarMenu, + MenubarSeparator, MenubarSub, MenubarSubContent, MenubarSubTrigger, @@ -19,11 +20,13 @@ type NodeType = 'config' | 'render' | 'variable' | 'function' type Props = { nodeId: string nodeType: NodeType - /** Content for Edit → Inputs (config and function nodes only) */ + /** Content for Insert → Inputs (config and function nodes only) */ 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 nodes = ctx?.nodes ?? [] const setNodes = ctx?.setNodes @@ -36,17 +39,19 @@ export function NodeMenubar({ nodeId, nodeType, editInputsContent }: Props) { const onDuplicate = useCallback(() => { if (!setNodes || !node) return - const newId = genId() const pos = node.position ?? { x: 0, y: 0 } - const newNode = { - id: newId, - type: node.type, - 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, - style: DEFAULT_NODE_STYLE[nodeType] ?? DEFAULT_NODE_STYLE.config, - } - if (newNode.data?.title && nodeType === 'config') newNode.data.title = `config-${newId}` - setNodes((nds: any[]) => nds.concat(newNode)) + setNodes((nds: any[]) => { + const newId = getNextNodeId(nodeType, nds.map((n: any) => n.id)) + const newNode = { + id: newId, + type: node.type, + 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, + style: DEFAULT_NODE_STYLE[nodeType] ?? DEFAULT_NODE_STYLE.config, + } + if (newNode.data?.title && nodeType === 'config') newNode.data.title = `config-${newId}` + return nds.concat(newNode) + }) }, [node, nodeType, setNodes]) const onCopy = useCallback(() => { @@ -57,9 +62,9 @@ export function NodeMenubar({ nodeId, nodeType, editInputsContent }: Props) { const onReset = useCallback(() => { if (!setNodes) return - const defaultData = getDefaultDataForType(nodeType, nodeId) + const resetData = getResetDataForType(nodeType, nodeId) 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]) @@ -69,11 +74,15 @@ export function NodeMenubar({ nodeId, nodeType, editInputsContent }: Props) { setEdges((eds: any[]) => eds.filter((e: any) => e.source !== nodeId && e.target !== nodeId)) }, [nodeId, setNodes, setEdges]) + const onRename = useCallback(() => { + ctx?.setRenamingNodeId?.(nodeId) + }, [nodeId, ctx]) + return ( - File + Node @@ -82,9 +91,13 @@ export function NodeMenubar({ nodeId, nodeType, editInputsContent }: Props) { Copy + + Rename + Reset + Delete @@ -93,7 +106,7 @@ export function NodeMenubar({ nodeId, nodeType, editInputsContent }: Props) { {hasEdit && ( - Edit + Insert @@ -104,6 +117,16 @@ export function NodeMenubar({ nodeId, nodeType, editInputsContent }: Props) { {editInputsContent} + {insertTagsContent != null && ( + + + Tags + + + {insertTagsContent} + + + )} )} diff --git a/src/components/graph/RenderingNode.tsx b/src/components/graph/RenderingNode.tsx index ba2f358..2c98e8b 100644 --- a/src/components/graph/RenderingNode.tsx +++ b/src/components/graph/RenderingNode.tsx @@ -10,6 +10,8 @@ import { BaseNodeFooterText, BaseNodeHeaderRow, } from './BaseNode' +import { DEFAULT_NODE_STYLE, getDefaultDataForType, getNextNodeId } from '../../lib/flowUtils' +import { NodeHeaderTitle } from './NodeHeaderTitle' import { NodeMenubar } from './NodeMenubar' import { Sparkles } from 'lucide-react' import { InputHandle, OutputHandle } from './NodeHandles' @@ -23,8 +25,6 @@ type Props = { style?: React.CSSProperties } -const DEFAULT_CONFIG_NODE_STYLE = { width: 320, height: 320 } - const DARK_SKINPARAMS = ` skinparam backgroundColor #1e1e1e skinparam defaultFontColor #e0e0e0 @@ -357,7 +357,7 @@ export const RenderingNode = memo(function RenderingNode({ id, width, height }: return ( }> - } title={id} /> + } title={} />
@@ -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" onClick={() => { if (!setNodes || !setEdges) return - const genId = () => `node_${Math.random().toString(36).slice(2, 9)}` - const nid = genId() + const nid = getNextNodeId('config', nodes.map((n: any) => n.id)) const thisNode = nodes.find((n: any) => n.id === id) const pos = thisNode?.position ?? { x: 0, y: 0 } 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)) - const edgeId = `e-${nid}-${id}` - setEdges((eds: any[]) => eds.concat({ id: edgeId, source: nid, target: id })) + setEdges((eds: any[]) => eds.concat({ id: `e-${nid}-${id}`, source: nid, target: id })) }} > Create Config diff --git a/src/components/graph/VariableNode.tsx b/src/components/graph/VariableNode.tsx index 929471f..7de7c08 100644 --- a/src/components/graph/VariableNode.tsx +++ b/src/components/graph/VariableNode.tsx @@ -11,6 +11,7 @@ import { NodeMenubar } from './NodeMenubar' import { Input } from '../ui/input' import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '../ui/select' import { Switch } from '../ui/switch' +import { NodeHeaderTitle } from './NodeHeaderTitle' import { OutputHandle } from './NodeHandles' import { Variable } from 'lucide-react' @@ -91,7 +92,7 @@ export const VariableNode = memo(function VariableNode({ id, data }: Props) { return ( }> - } title={id} /> + } title={} />
diff --git a/src/lib/flowContext.tsx b/src/lib/flowContext.tsx index 4bc0592..abd751f 100644 --- a/src/lib/flowContext.tsx +++ b/src/lib/flowContext.tsx @@ -5,6 +5,8 @@ export type FlowContextValue = { setNodes: (updater: any) => void edges: any[] setEdges: (updater: any) => void + renamingNodeId: string | null + setRenamingNodeId: (id: string | null) => void } const FlowContext = React.createContext(null) diff --git a/src/lib/flowUtils.ts b/src/lib/flowUtils.ts index 6c34dd5..66c5374 100644 --- a/src/lib/flowUtils.ts +++ b/src/lib/flowUtils.ts @@ -1,5 +1,50 @@ -/** Generate short unique node id */ -export const genId = () => `node_${Math.random().toString(36).slice(2, 9)}` +export const PREFIX_BY_TYPE: Record = { + 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 = {} + 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 = { 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}` 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) +}