add indicators
This commit is contained in:
@@ -10,7 +10,6 @@ import {
|
||||
BaseNode,
|
||||
BaseNodeContent,
|
||||
BaseNodeFooter,
|
||||
BaseNodeFooterText,
|
||||
BaseNodeHeaderRow,
|
||||
} from './BaseNode'
|
||||
import { ScrollText } from 'lucide-react'
|
||||
@@ -21,6 +20,7 @@ import {
|
||||
MenubarSubTrigger,
|
||||
} from '../ui/menubar'
|
||||
import { InputHandle, OutputHandle } from './NodeHandles'
|
||||
import { NodeFooterEdgeIndicators } from './NodeFooterEdgeIndicators'
|
||||
import { NodeHeaderTitle } from './NodeHeaderTitle'
|
||||
import { NodeMenubar } from './NodeMenubar'
|
||||
|
||||
@@ -264,7 +264,9 @@ export const ConfigNode = memo(function ConfigNode({ id, data, width, height }:
|
||||
</BaseNodeContent>
|
||||
|
||||
<BaseNodeFooter>
|
||||
<BaseNodeFooterText>{plantumlValue ? `${plantumlValue.length} chars` : 'none'}</BaseNodeFooterText>
|
||||
<NodeFooterEdgeIndicators nodeId={id} nodeType="config">
|
||||
{plantumlValue ? `${plantumlValue.length} chars` : 'none'}
|
||||
</NodeFooterEdgeIndicators>
|
||||
</BaseNodeFooter>
|
||||
</BaseNode>
|
||||
)
|
||||
|
||||
@@ -8,10 +8,10 @@ import {
|
||||
BaseNode,
|
||||
BaseNodeContent,
|
||||
BaseNodeFooter,
|
||||
BaseNodeFooterText,
|
||||
BaseNodeHeaderRow,
|
||||
} from './BaseNode'
|
||||
import { InputHandle, OutputHandle } from './NodeHandles'
|
||||
import { NodeFooterEdgeIndicators } from './NodeFooterEdgeIndicators'
|
||||
import { NodeHeaderTitle } from './NodeHeaderTitle'
|
||||
import { NodeMenubar } from './NodeMenubar'
|
||||
import { Code2 } from 'lucide-react'
|
||||
@@ -69,7 +69,9 @@ export const FunctionNode = memo(function FunctionNode({ id, data, width, height
|
||||
</BaseNodeContent>
|
||||
|
||||
<BaseNodeFooter>
|
||||
<BaseNodeFooterText>{bodyValue ? `${bodyValue.length} chars` : 'none'}</BaseNodeFooterText>
|
||||
<NodeFooterEdgeIndicators nodeId={id} nodeType="function">
|
||||
{bodyValue ? `${bodyValue.length} chars` : 'none'}
|
||||
</NodeFooterEdgeIndicators>
|
||||
</BaseNodeFooter>
|
||||
</BaseNode>
|
||||
)
|
||||
|
||||
63
src/components/graph/NodeFooterEdgeIndicators.tsx
Normal file
63
src/components/graph/NodeFooterEdgeIndicators.tsx
Normal file
@@ -0,0 +1,63 @@
|
||||
import React, { useContext, useMemo } from 'react'
|
||||
import FlowContext from '../../lib/flowContext'
|
||||
import { ArrowDownLeft, ArrowUpRight } from 'lucide-react'
|
||||
|
||||
const NODE_HAS_INPUT: Record<string, boolean> = {
|
||||
config: true,
|
||||
function: true,
|
||||
render: true,
|
||||
variable: false,
|
||||
}
|
||||
const NODE_HAS_OUTPUT: Record<string, boolean> = {
|
||||
config: true,
|
||||
function: true,
|
||||
render: true,
|
||||
variable: true,
|
||||
}
|
||||
|
||||
type Props = {
|
||||
nodeId: string
|
||||
nodeType: 'config' | 'function' | 'render' | 'variable'
|
||||
children?: React.ReactNode
|
||||
}
|
||||
|
||||
export function NodeFooterEdgeIndicators({ nodeId, nodeType, children }: Props) {
|
||||
const ctx = useContext(FlowContext)
|
||||
const edges = ctx?.edges ?? []
|
||||
|
||||
const { inputs, outputs } = useMemo(() => {
|
||||
let inputs = 0
|
||||
let outputs = 0
|
||||
for (const e of edges) {
|
||||
if (e.target === nodeId) inputs += 1
|
||||
if (e.source === nodeId) outputs += 1
|
||||
}
|
||||
return { inputs, outputs }
|
||||
}, [edges, nodeId])
|
||||
|
||||
const showInput = NODE_HAS_INPUT[nodeType] ?? false
|
||||
const showOutput = NODE_HAS_OUTPUT[nodeType] ?? false
|
||||
|
||||
return (
|
||||
<div className="flex items-center gap-2 w-full text-xs text-muted-foreground">
|
||||
{showInput && (
|
||||
<span className="flex items-center gap-1 shrink-0" title="Input connections">
|
||||
<ArrowDownLeft className="size-3.5" />
|
||||
<span>{inputs}</span>
|
||||
</span>
|
||||
)}
|
||||
{showOutput && (
|
||||
<span className="flex items-center gap-1 shrink-0" title="Output connections">
|
||||
<ArrowUpRight className="size-3.5" />
|
||||
<span>{outputs}</span>
|
||||
</span>
|
||||
)}
|
||||
{children != null && (
|
||||
<>
|
||||
{(showInput || showOutput) && <span className="shrink-0">|</span>}
|
||||
<span className="min-w-0 truncate">{children}</span>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -24,9 +24,11 @@ type Props = {
|
||||
editInputsContent?: React.ReactNode
|
||||
/** Content for Insert → Tags (e.g. Nunjucks tag snippets, config nodes) */
|
||||
insertTagsContent?: React.ReactNode
|
||||
/** Extra content in Node menu (e.g. Export submenu for render nodes), before the separator */
|
||||
nodeMenuExtraContent?: React.ReactNode
|
||||
}
|
||||
|
||||
export function NodeMenubar({ nodeId, nodeType, editInputsContent, insertTagsContent }: Props) {
|
||||
export function NodeMenubar({ nodeId, nodeType, editInputsContent, insertTagsContent, nodeMenuExtraContent }: Props) {
|
||||
const ctx = useContext(FlowContext)
|
||||
const nodes = ctx?.nodes ?? []
|
||||
const setNodes = ctx?.setNodes
|
||||
@@ -79,7 +81,7 @@ export function NodeMenubar({ nodeId, nodeType, editInputsContent, insertTagsCon
|
||||
}, [nodeId, ctx])
|
||||
|
||||
return (
|
||||
<Menubar className="h-auto min-h-0 flex items-center bg-none p-1 border-t-0 border-l-0 border-r-0 border-b shadow-none rounded-none text-muted-foreground">
|
||||
<Menubar className="h-auto min-h-0 flex items-center bg-none p-1 border-t-0 border-l-0 border-r-0 border-b border-b-secondary shadow-none rounded-none text-muted-foreground">
|
||||
<MenubarMenu>
|
||||
<MenubarTrigger className="px-1.5 py-0 text-xs">
|
||||
Node
|
||||
@@ -97,6 +99,7 @@ export function NodeMenubar({ nodeId, nodeType, editInputsContent, insertTagsCon
|
||||
<MenubarItem className="text-xs" onClick={onReset}>
|
||||
Reset
|
||||
</MenubarItem>
|
||||
{nodeMenuExtraContent}
|
||||
<MenubarSeparator />
|
||||
<MenubarItem className="text-xs text-destructive focus:text-destructive" onClick={onDelete}>
|
||||
Delete
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { memo, useContext, useEffect, useMemo, useRef, useState } from 'react'
|
||||
import { memo, useCallback, useContext, useEffect, useMemo, useRef, useState } from 'react'
|
||||
import nunjucks from 'nunjucks'
|
||||
import FlowContext from '../../lib/flowContext'
|
||||
import { useTheme } from '../../lib/themeContext'
|
||||
@@ -7,12 +7,13 @@ import {
|
||||
BaseNode,
|
||||
BaseNodeContent,
|
||||
BaseNodeFooter,
|
||||
BaseNodeFooterText,
|
||||
BaseNodeHeaderRow,
|
||||
} from './BaseNode'
|
||||
import { DEFAULT_NODE_STYLE, getDefaultDataForType, getNextNodeId } from '../../lib/flowUtils'
|
||||
import { NodeFooterEdgeIndicators } from './NodeFooterEdgeIndicators'
|
||||
import { NodeHeaderTitle } from './NodeHeaderTitle'
|
||||
import { NodeMenubar } from './NodeMenubar'
|
||||
import { MenubarItem, MenubarSub, MenubarSubContent, MenubarSubTrigger } from '../ui/menubar'
|
||||
import { Sparkles } from 'lucide-react'
|
||||
import { InputHandle, OutputHandle } from './NodeHandles'
|
||||
|
||||
@@ -355,13 +356,63 @@ export const RenderingNode = memo(function RenderingNode({ id, width, height }:
|
||||
? { width, height }
|
||||
: undefined
|
||||
|
||||
const downloadSvg = useCallback(() => {
|
||||
if (!svgContent) return
|
||||
const blob = new Blob([svgContent], { type: 'image/svg+xml' })
|
||||
const url = URL.createObjectURL(blob)
|
||||
const a = document.createElement('a')
|
||||
a.href = url
|
||||
a.download = `${id}.svg`
|
||||
a.click()
|
||||
URL.revokeObjectURL(url)
|
||||
}, [id, svgContent])
|
||||
|
||||
const downloadPng = useCallback(() => {
|
||||
if (!svgContent) return
|
||||
const dataUrl = 'data:image/svg+xml;charset=utf-8,' + encodeURIComponent(svgContent)
|
||||
const img = new Image()
|
||||
img.onload = () => {
|
||||
const canvas = document.createElement('canvas')
|
||||
canvas.width = img.naturalWidth
|
||||
canvas.height = img.naturalHeight
|
||||
const ctx = canvas.getContext('2d')
|
||||
if (!ctx) return
|
||||
ctx.drawImage(img, 0, 0)
|
||||
const pngUrl = canvas.toDataURL('image/png')
|
||||
const a = document.createElement('a')
|
||||
a.href = pngUrl
|
||||
a.download = `${id}.png`
|
||||
a.click()
|
||||
}
|
||||
img.onerror = () => {}
|
||||
img.src = dataUrl
|
||||
}, [id, svgContent])
|
||||
|
||||
return (
|
||||
<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={<NodeHeaderTitle nodeId={id} displayTitle={id} />} />
|
||||
|
||||
<BaseNodeContent>
|
||||
<div className="shrink-0 w-full">
|
||||
<NodeMenubar nodeId={id} nodeType="render" />
|
||||
<NodeMenubar
|
||||
nodeId={id}
|
||||
nodeType="render"
|
||||
nodeMenuExtraContent={
|
||||
<MenubarSub>
|
||||
<MenubarSubTrigger className="text-xs" disabled={!svgContent}>
|
||||
Export
|
||||
</MenubarSubTrigger>
|
||||
<MenubarSubContent className="min-w-[10rem]">
|
||||
<MenubarItem className="text-xs" onClick={downloadPng} disabled={!svgContent}>
|
||||
PNG
|
||||
</MenubarItem>
|
||||
<MenubarItem className="text-xs" onClick={downloadSvg} disabled={!svgContent}>
|
||||
SVG
|
||||
</MenubarItem>
|
||||
</MenubarSubContent>
|
||||
</MenubarSub>
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
<div className="min-h-0 flex-1 flex flex-col">
|
||||
{incomingIds.length === 0 ? (
|
||||
@@ -411,9 +462,9 @@ export const RenderingNode = memo(function RenderingNode({ id, width, height }:
|
||||
</BaseNodeContent>
|
||||
|
||||
<BaseNodeFooter>
|
||||
<BaseNodeFooterText>
|
||||
<NodeFooterEdgeIndicators nodeId={id} nodeType="render">
|
||||
{svgContent ? 'PlantUML diagram' : error ? 'Error' : '—'}
|
||||
</BaseNodeFooterText>
|
||||
</NodeFooterEdgeIndicators>
|
||||
</BaseNodeFooter>
|
||||
</BaseNode>
|
||||
)
|
||||
|
||||
@@ -4,13 +4,13 @@ import {
|
||||
BaseNode,
|
||||
BaseNodeContent,
|
||||
BaseNodeFooter,
|
||||
BaseNodeFooterText,
|
||||
BaseNodeHeaderRow,
|
||||
} from './BaseNode'
|
||||
import { NodeMenubar } from './NodeMenubar'
|
||||
import { Input } from '../ui/input'
|
||||
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '../ui/select'
|
||||
import { Switch } from '../ui/switch'
|
||||
import { NodeFooterEdgeIndicators } from './NodeFooterEdgeIndicators'
|
||||
import { NodeHeaderTitle } from './NodeHeaderTitle'
|
||||
import { OutputHandle } from './NodeHandles'
|
||||
import { Variable } from 'lucide-react'
|
||||
@@ -134,8 +134,7 @@ export const VariableNode = memo(function VariableNode({ id, data }: Props) {
|
||||
</BaseNodeContent>
|
||||
|
||||
<BaseNodeFooter>
|
||||
<BaseNodeFooterText>
|
||||
</BaseNodeFooterText>
|
||||
<NodeFooterEdgeIndicators nodeId={id} nodeType="variable" />
|
||||
</BaseNodeFooter>
|
||||
</BaseNode>
|
||||
)
|
||||
|
||||
Reference in New Issue
Block a user