diff --git a/src/App.tsx b/src/App.tsx index 74d6ab6..9d3e488 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -270,7 +270,7 @@ export default function App() { - Create node + Create Node createNode('config')}> diff --git a/src/components/graph/BaseNode.tsx b/src/components/graph/BaseNode.tsx index 4ce646f..80ef236 100644 --- a/src/components/graph/BaseNode.tsx +++ b/src/components/graph/BaseNode.tsx @@ -173,7 +173,7 @@ export function BaseNodeFooter({ className, ...props }: ComponentProps<"div">) {
(null) + + const edges = ctx?.edges ?? [] + const nodes = ctx?.nodes ?? [] + const incomingEdges = useMemo(() => edges.filter((e: any) => e.target === id), [edges, id]) + const incomingIds = useMemo(() => incomingEdges.map((e: any) => e.source).sort(), [incomingEdges]) + const connectedVariableNodes = useMemo( + () => (nodes as any[]).filter((n: any) => incomingIds.includes(n.id) && n.type === 'variable'), + [nodes, incomingIds] + ) + const hasConnectedVariables = connectedVariableNodes.length > 0 const onChange = useCallback( (val: string) => { @@ -40,6 +52,34 @@ export const FunctionNode = memo(function FunctionNode({ id, data, width, height [id, setNodes] ) + const insertAt = useCallback( + (insertText: string, mode: 'prepend' | 'append' | 'cursor') => { + const ref = editorRef.current as { view: { state: { doc: { length: number }; selection: { main: { from: number } } }; dispatch: (arg: { changes: { from: number; to: number; insert: string } }) => void } } | null + if (ref?.view) { + const view = ref.view + const doc = view.state.doc + const len = doc.length + let from: number + if (mode === 'prepend') from = 0 + else if (mode === 'append') from = len + else from = view.state.selection.main.from + view.dispatch({ changes: { from, to: from, insert: insertText } }) + onChange(view.state.doc.toString()) + return + } + if (mode === 'prepend') onChange(insertText + bodyValue) + else onChange(bodyValue + insertText) + }, + [onChange, bodyValue] + ) + + const insertVariableAtCursor = useCallback( + (variableNode: any) => { + insertAt(variableNode.id, 'cursor') + }, + [insertAt] + ) + const extensions = useMemo(() => [javascript()], []) const [editorHeight, editorContainerRef] = useResizeHeight(120) const dimensions = @@ -53,10 +93,37 @@ export const FunctionNode = memo(function FunctionNode({ id, data, width, height
- + + {connectedVariableNodes.map((n: any) => ( + + + + {n.id} + + + insertVariableAtCursor(n)} + > + Insert at cursor + + + + ))} + + ) : undefined + } + />
{bodyValue ? `${bodyValue.length} chars` : 'none'} -

- In config: {'{{ x | '}{id}{' }}'} or {'{{ x | '}{id}{'(a, b, key=val) }}'}. Use function(num, x, y, kwargs) { ... } — kwargs has keyword args. -

) diff --git a/src/components/graph/RenderingNode.tsx b/src/components/graph/RenderingNode.tsx index b1be1c7..b2b80c8 100644 --- a/src/components/graph/RenderingNode.tsx +++ b/src/components/graph/RenderingNode.tsx @@ -234,16 +234,28 @@ export const RenderingNode = memo(function RenderingNode({ id, width, height }: }, } - // Context: only variables. Function nodes are registered as Nunjucks custom filters (see below). + // Context: variables connected to configs, plus variables connected to functions that feed configs (so they can be injected as constants). const nunjucksContext = Object.create(null) as Record + const setVarInContext = (src: any) => { + const v = src.data?.value + const str = v === undefined || v === null ? '' : String(v) + nunjucksContext[src.id] = + v === undefined || v === null ? '' : typeof v === 'boolean' || typeof v === 'number' ? v : str + } for (const e of edges) { if (!configIdsUsed.has(e.target)) continue const src = nodes.find((n: any) => n.id === e.source) - if (src?.type === 'variable') { - const v = src.data?.value - const str = v === undefined || v === null ? '' : String(v) - nunjucksContext[src.id] = - v === undefined || v === null ? '' : typeof v === 'boolean' || typeof v === 'number' ? v : str + if (src?.type === 'variable') setVarInContext(src) + } + for (const e of edges) { + if (!configIdsUsed.has(e.target)) continue + const src = nodes.find((n: any) => n.id === e.source) + if (src?.type === 'function') { + for (const e2 of edges) { + if (e2.target !== src.id) continue + const vNode = nodes.find((n: any) => n.id === e2.source) + if (vNode?.type === 'variable') setVarInContext(vNode) + } } } @@ -280,12 +292,31 @@ export const RenderingNode = memo(function RenderingNode({ id, width, height }: const isPlainObject = (v: unknown): v is Record => typeof v === 'object' && v !== null && !Array.isArray(v) + // For each function node that feeds a config: which variable node ids are connected to that function? + const functionConnectedVariableIds = Object.create(null) as Record + for (const e of edges) { + if (!configIdsUsed.has(e.target)) continue + const src = nodes.find((n: any) => n.id === e.source) + if (src?.type === 'function') { + const fid = src.id + for (const e2 of edges) { + if (e2.target !== fid) continue + const vNode = nodes.find((n: any) => n.id === e2.source) + if (vNode?.type === 'variable') { + if (!functionConnectedVariableIds[fid]) functionConnectedVariableIds[fid] = [] + functionConnectedVariableIds[fid].push(vNode.id) + } + } + } + } + for (const e of edges) { if (!configIdsUsed.has(e.target)) continue const src = nodes.find((n: any) => n.id === e.source) if (src?.type === 'function') { const body = src.data?.body ?? 'return args[0];' const parsed = parseFunctionSignature(body) + const connectedVarIds = new Set(functionConnectedVariableIds[src.id] ?? []) env.addFilter( src.id, (value: unknown, ...args: unknown[]) => { @@ -301,10 +332,17 @@ export const RenderingNode = memo(function RenderingNode({ id, width, height }: const lastParam = paramNames[paramNames.length - 1] const invocationArgs = paramNames.map((name, i) => { if (name === lastParam && lastParam === 'kwargs') return kwargs + // Connected variables are available as constants: use variable value from context + if (connectedVarIds.has(name) && name in nunjucksContext) + return nunjucksContext[name] return positionals[i] }) - const fn = new Function(...paramNames, innerBody) - invoke = () => fn(...invocationArgs) + // Inject connected variables as extra params so they're in scope in the body (e.g. (value) => value + var_001) + const extraVarIds = [...connectedVarIds].filter((vid) => !paramNames.includes(vid)) + const allParamNames = [...paramNames, ...extraVarIds] + const allArgs = [...invocationArgs, ...extraVarIds.map((vid) => nunjucksContext[vid])] + const fn = new Function(...allParamNames, innerBody) + invoke = () => fn(...allArgs) } else { const fn = new Function('args', body) invoke = () => fn(positionals)