improved menus
This commit is contained in:
@@ -26,10 +26,13 @@ import {
|
||||
import { Code2, ScrollText, Variable } from 'lucide-react'
|
||||
import {
|
||||
MenubarItem,
|
||||
MenubarSeparator,
|
||||
MenubarShortcut,
|
||||
MenubarSub,
|
||||
MenubarSubContent,
|
||||
MenubarSubTrigger,
|
||||
} from '../ui/menubar'
|
||||
import { Kbd } from '../ui/kbd'
|
||||
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '../ui/select'
|
||||
import { InputHandle, OutputHandle } from './NodeHandles'
|
||||
import { NodeFooterEdgeIndicators } from './NodeFooterEdgeIndicators'
|
||||
@@ -191,31 +194,55 @@ export const ConfigNode = memo(function ConfigNode({ id, data, width, height }:
|
||||
const [editorHeight, editorContainerRef] = useResizeHeight(180)
|
||||
|
||||
const insertBlocksContent = useMemo(() => {
|
||||
return configType.insertBlocks.map((block, idx) =>
|
||||
isGroup(block) ? (
|
||||
<MenubarSub key={`${block.label}-${idx}`}>
|
||||
<MenubarSubTrigger className="text-xs">{block.label}</MenubarSubTrigger>
|
||||
<MenubarSubContent className="min-w-[12rem]">
|
||||
{block.items.map(({ label, snippet }) => (
|
||||
<MenubarItem
|
||||
key={label}
|
||||
className="text-xs"
|
||||
onClick={() => insertAt(snippet, 'cursor')}
|
||||
>
|
||||
{label}
|
||||
</MenubarItem>
|
||||
))}
|
||||
</MenubarSubContent>
|
||||
</MenubarSub>
|
||||
) : (
|
||||
const templatingGroup = configType.insertBlocks.find(
|
||||
(b): b is import('../../lib/configTypes').InsertBlockGroup => isGroup(b) && b.label === 'Templating'
|
||||
)
|
||||
const typeBlocks = configType.insertBlocks.filter((b) => !(isGroup(b) && b.label === 'Templating'))
|
||||
const insertShortcut = <MenubarShortcut className="opacity-0 group-hover:opacity-100 transition-opacity"><Kbd>Insert</Kbd></MenubarShortcut>
|
||||
const typeItems = typeBlocks.flatMap((block) =>
|
||||
isGroup(block)
|
||||
? block.items.map(({ label, snippet }) => (
|
||||
<MenubarItem
|
||||
key={label}
|
||||
className="text-xs flex items-center group"
|
||||
onClick={() => insertAt(snippet, 'cursor')}
|
||||
>
|
||||
{label}
|
||||
{insertShortcut}
|
||||
</MenubarItem>
|
||||
))
|
||||
: [
|
||||
<MenubarItem
|
||||
key={block.label}
|
||||
className="text-xs flex items-center group"
|
||||
onClick={() => insertAt(block.snippet, 'cursor')}
|
||||
>
|
||||
{block.label}
|
||||
{insertShortcut}
|
||||
</MenubarItem>,
|
||||
]
|
||||
)
|
||||
const templatingItems =
|
||||
templatingGroup?.items.map(({ label, snippet }) => (
|
||||
<MenubarItem
|
||||
key={block.label}
|
||||
className="text-xs"
|
||||
onClick={() => insertAt(block.snippet, 'cursor')}
|
||||
key={label}
|
||||
className="text-xs flex items-center group"
|
||||
onClick={() => insertAt(snippet, 'cursor')}
|
||||
>
|
||||
{block.label}
|
||||
{label}
|
||||
<MenubarShortcut className="opacity-0 group-hover:opacity-100 transition-opacity"><Kbd>Insert</Kbd></MenubarShortcut>
|
||||
</MenubarItem>
|
||||
)
|
||||
)) ?? []
|
||||
return (
|
||||
<>
|
||||
{typeItems}
|
||||
{templatingItems.length > 0 && (
|
||||
<>
|
||||
<MenubarSeparator />
|
||||
{templatingItems}
|
||||
</>
|
||||
)}
|
||||
</>
|
||||
)
|
||||
}, [configType.insertBlocks, insertAt])
|
||||
|
||||
@@ -250,7 +277,7 @@ export const ConfigNode = memo(function ConfigNode({ id, data, width, height }:
|
||||
<NodeMenubar
|
||||
nodeId={id}
|
||||
nodeType="config"
|
||||
editInputsContent={
|
||||
inputsMenuContent={
|
||||
hasDependencies ? (
|
||||
<>
|
||||
{connectedConfigNodes.map((n: any) => (
|
||||
@@ -261,63 +288,59 @@ export const ConfigNode = memo(function ConfigNode({ id, data, width, height }:
|
||||
</MenubarSubTrigger>
|
||||
<MenubarSubContent>
|
||||
<MenubarItem
|
||||
className="text-xs"
|
||||
className="text-xs flex items-center group"
|
||||
onClick={() => insertExtendsFromNode(n, 'cursor')}
|
||||
>
|
||||
Extend
|
||||
<MenubarShortcut className="opacity-0 group-hover:opacity-100 transition-opacity"><Kbd>Insert</Kbd></MenubarShortcut>
|
||||
</MenubarItem>
|
||||
<MenubarItem
|
||||
className="text-xs"
|
||||
className="text-xs flex items-center group"
|
||||
onClick={() => insertIncludeFromNode(n, 'cursor')}
|
||||
>
|
||||
Include
|
||||
<MenubarShortcut className="opacity-0 group-hover:opacity-100 transition-opacity"><Kbd>Insert</Kbd></MenubarShortcut>
|
||||
</MenubarItem>
|
||||
<MenubarItem
|
||||
className="text-xs"
|
||||
className="text-xs flex items-center group"
|
||||
onClick={() => insertImportFromNode(n, 'cursor')}
|
||||
>
|
||||
Import
|
||||
<MenubarShortcut className="opacity-0 group-hover:opacity-100 transition-opacity"><Kbd>Insert</Kbd></MenubarShortcut>
|
||||
</MenubarItem>
|
||||
</MenubarSubContent>
|
||||
</MenubarSub>
|
||||
))}
|
||||
{connectedVariableNodes.map((n: any) => (
|
||||
<MenubarSub key={`${n.id}`}>
|
||||
<MenubarSubTrigger className="text-xs flex items-center gap-2">
|
||||
<Variable className="size-3.5 shrink-0" />
|
||||
{n.id}
|
||||
</MenubarSubTrigger>
|
||||
<MenubarSubContent>
|
||||
<MenubarItem
|
||||
className="text-xs"
|
||||
onClick={() => insertVariableReference(n, 'cursor')}
|
||||
>
|
||||
Insert
|
||||
</MenubarItem>
|
||||
</MenubarSubContent>
|
||||
</MenubarSub>
|
||||
<MenubarItem
|
||||
key={n.id}
|
||||
className="text-xs flex items-center gap-2 group"
|
||||
onClick={() => insertVariableReference(n, 'cursor')}
|
||||
>
|
||||
<Variable className="size-3.5 shrink-0" />
|
||||
{n.id}
|
||||
<MenubarShortcut className="opacity-0 group-hover:opacity-100 transition-opacity"><Kbd>Insert</Kbd></MenubarShortcut>
|
||||
</MenubarItem>
|
||||
))}
|
||||
{connectedFunctionNodes.map((n: any) => (
|
||||
<MenubarSub key={`${n.id}`}>
|
||||
<MenubarSubTrigger className="text-xs flex items-center gap-2">
|
||||
<Code2 className="size-3.5 shrink-0" />
|
||||
{n.id}
|
||||
</MenubarSubTrigger>
|
||||
<MenubarSubContent>
|
||||
<MenubarItem
|
||||
className="text-xs"
|
||||
onClick={() => insertFunctionCall(n, 'cursor')}
|
||||
>
|
||||
Insert
|
||||
</MenubarItem>
|
||||
</MenubarSubContent>
|
||||
</MenubarSub>
|
||||
<MenubarItem
|
||||
key={n.id}
|
||||
className="text-xs flex items-center gap-2 group"
|
||||
onClick={() => insertFunctionCall(n, 'cursor')}
|
||||
>
|
||||
<Code2 className="size-3.5 shrink-0" />
|
||||
{n.id}
|
||||
<MenubarShortcut className="opacity-0 group-hover:opacity-100 transition-opacity"><Kbd>Insert</Kbd></MenubarShortcut>
|
||||
</MenubarItem>
|
||||
))}
|
||||
</>
|
||||
) : undefined
|
||||
) : (
|
||||
<span className="text-xs text-muted-foreground px-2 py-1">Connect nodes to insert references</span>
|
||||
)
|
||||
}
|
||||
insertMenuLabel="Blocks"
|
||||
insertContentDirect
|
||||
insertTagsContent={insertBlocksContent}
|
||||
insertTagsLabel={configType.label}
|
||||
/>
|
||||
</div>
|
||||
|
||||
@@ -338,7 +361,7 @@ export const ConfigNode = memo(function ConfigNode({ id, data, width, height }:
|
||||
|
||||
<BaseNodeFooter>
|
||||
<NodeFooterEdgeIndicators nodeId={id} nodeType="config">
|
||||
{content ? `${content.length} chars` : 'none'}
|
||||
{configType.label} · {content ? `${content.length} chars` : 'none'}
|
||||
</NodeFooterEdgeIndicators>
|
||||
</BaseNodeFooter>
|
||||
</BaseNode>
|
||||
|
||||
@@ -15,7 +15,8 @@ import { InputHandle, OutputHandle } from './NodeHandles'
|
||||
import { NodeFooterEdgeIndicators } from './NodeFooterEdgeIndicators'
|
||||
import { NodeHeaderTitle } from './NodeHeaderTitle'
|
||||
import { NodeMenubar } from './NodeMenubar'
|
||||
import { MenubarItem, MenubarSub, MenubarSubContent, MenubarSubTrigger } from '../ui/menubar'
|
||||
import { MenubarItem, MenubarShortcut } from '../ui/menubar'
|
||||
import { Kbd } from '../ui/kbd'
|
||||
import { Code2, Variable } from 'lucide-react'
|
||||
|
||||
type Props = {
|
||||
@@ -110,43 +111,35 @@ export const FunctionNode = memo(function FunctionNode({ id, data, width, height
|
||||
<NodeMenubar
|
||||
nodeId={id}
|
||||
nodeType="function"
|
||||
editInputsContent={
|
||||
inputsMenuContent={
|
||||
hasConnectedInputs ? (
|
||||
<>
|
||||
{connectedVariableNodes.map((n: any) => (
|
||||
<MenubarSub key={n.id}>
|
||||
<MenubarSubTrigger className="text-xs flex items-center gap-2">
|
||||
<Variable className="size-3.5 shrink-0" />
|
||||
{n.id}
|
||||
</MenubarSubTrigger>
|
||||
<MenubarSubContent>
|
||||
<MenubarItem
|
||||
className="text-xs"
|
||||
onClick={() => insertVariableAtCursor(n)}
|
||||
>
|
||||
Insert at cursor
|
||||
</MenubarItem>
|
||||
</MenubarSubContent>
|
||||
</MenubarSub>
|
||||
<MenubarItem
|
||||
key={n.id}
|
||||
className="text-xs flex items-center gap-2 group"
|
||||
onClick={() => insertVariableAtCursor(n)}
|
||||
>
|
||||
<Variable className="size-3.5 shrink-0" />
|
||||
{n.id}
|
||||
<MenubarShortcut className="opacity-0 group-hover:opacity-100 transition-opacity"><Kbd>Insert</Kbd></MenubarShortcut>
|
||||
</MenubarItem>
|
||||
))}
|
||||
{connectedFunctionNodes.map((n: any) => (
|
||||
<MenubarSub key={n.id}>
|
||||
<MenubarSubTrigger className="text-xs flex items-center gap-2">
|
||||
<Code2 className="size-3.5 shrink-0" />
|
||||
{n.id}
|
||||
</MenubarSubTrigger>
|
||||
<MenubarSubContent>
|
||||
<MenubarItem
|
||||
className="text-xs"
|
||||
onClick={() => insertFunctionAtCursor(n)}
|
||||
>
|
||||
Insert at cursor
|
||||
</MenubarItem>
|
||||
</MenubarSubContent>
|
||||
</MenubarSub>
|
||||
<MenubarItem
|
||||
key={n.id}
|
||||
className="text-xs flex items-center gap-2 group"
|
||||
onClick={() => insertFunctionAtCursor(n)}
|
||||
>
|
||||
<Code2 className="size-3.5 shrink-0" />
|
||||
{n.id}
|
||||
<MenubarShortcut className="opacity-0 group-hover:opacity-100 transition-opacity"><Kbd>Insert</Kbd></MenubarShortcut>
|
||||
</MenubarItem>
|
||||
))}
|
||||
</>
|
||||
) : undefined
|
||||
) : (
|
||||
<span className="text-xs text-muted-foreground px-2 py-1">Connect nodes to insert at cursor</span>
|
||||
)
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
@@ -167,7 +160,7 @@ export const FunctionNode = memo(function FunctionNode({ id, data, width, height
|
||||
|
||||
<BaseNodeFooter>
|
||||
<NodeFooterEdgeIndicators nodeId={id} nodeType="function">
|
||||
{bodyValue ? `Javascript | ${bodyValue.length} chars` : 'none'}
|
||||
{bodyValue ? `JavaScript · ${bodyValue.length} chars` : 'JavaScript · none'}
|
||||
</NodeFooterEdgeIndicators>
|
||||
</BaseNodeFooter>
|
||||
</BaseNode>
|
||||
|
||||
@@ -20,17 +20,23 @@ type NodeType = 'config' | 'render' | 'variable' | 'function'
|
||||
type Props = {
|
||||
nodeId: string
|
||||
nodeType: NodeType
|
||||
/** Content for Insert → Inputs (config and function nodes only) */
|
||||
/** Content for Insert → Inputs (function nodes); when inputsMenuContent is set, Inputs is a separate menu and this is not used in Insert */
|
||||
editInputsContent?: React.ReactNode
|
||||
/** When set, Inputs is rendered as its own top-level menu (config nodes); Insert then only shows markup-specific options */
|
||||
inputsMenuContent?: React.ReactNode
|
||||
/** Content for Insert → [Blocks/Tags] (e.g. type-specific snippets, config nodes) */
|
||||
insertTagsContent?: React.ReactNode
|
||||
/** Label for Insert submenu that shows insertTagsContent (default "Tags") */
|
||||
/** Label for the Insert/Blocks menu trigger (default "Insert") */
|
||||
insertMenuLabel?: string
|
||||
/** When true, render insertTagsContent directly in the menu (no submenu level). Use for flat Blocks list. */
|
||||
insertContentDirect?: boolean
|
||||
/** Label for Insert submenu that shows insertTagsContent when not insertContentDirect (default "Tags") */
|
||||
insertTagsLabel?: string
|
||||
/** 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, insertTagsLabel = 'Tags', nodeMenuExtraContent }: Props) {
|
||||
export function NodeMenubar({ nodeId, nodeType, editInputsContent, inputsMenuContent, insertTagsContent, insertMenuLabel = 'Insert', insertContentDirect, insertTagsLabel = 'Tags', nodeMenuExtraContent }: Props) {
|
||||
const ctx = useContext(FlowContext)
|
||||
const nodes = ctx?.nodes ?? []
|
||||
const setNodes = ctx?.setNodes
|
||||
@@ -108,30 +114,45 @@ export function NodeMenubar({ nodeId, nodeType, editInputsContent, insertTagsCon
|
||||
</MenubarItem>
|
||||
</MenubarContent>
|
||||
</MenubarMenu>
|
||||
{hasEdit && (
|
||||
{inputsMenuContent != null && (
|
||||
<MenubarMenu>
|
||||
<MenubarTrigger className="px-1.5 py-0 text-xs" disabled={!hasConnectedNodes}>
|
||||
Inputs
|
||||
</MenubarTrigger>
|
||||
<MenubarContent className="min-w-[12rem]">
|
||||
{inputsMenuContent}
|
||||
</MenubarContent>
|
||||
</MenubarMenu>
|
||||
)}
|
||||
{hasEdit && (insertTagsContent != null || (editInputsContent != null && inputsMenuContent == null)) && (
|
||||
<MenubarMenu>
|
||||
<MenubarTrigger className="px-1.5 py-0 text-xs">
|
||||
Insert
|
||||
{insertMenuLabel}
|
||||
</MenubarTrigger>
|
||||
<MenubarContent className="min-w-[10rem]">
|
||||
<MenubarSub>
|
||||
<MenubarSubTrigger className="text-xs" disabled={!hasConnectedNodes}>
|
||||
Inputs
|
||||
</MenubarSubTrigger>
|
||||
<MenubarSubContent className="min-w-[12rem]">
|
||||
{editInputsContent}
|
||||
</MenubarSubContent>
|
||||
</MenubarSub>
|
||||
{insertTagsContent != null && (
|
||||
{inputsMenuContent == null && editInputsContent != null && (
|
||||
<MenubarSub>
|
||||
<MenubarSubTrigger className="text-xs">
|
||||
{insertTagsLabel}
|
||||
<MenubarSubTrigger className="text-xs" disabled={!hasConnectedNodes}>
|
||||
Inputs
|
||||
</MenubarSubTrigger>
|
||||
<MenubarSubContent className="min-w-[12rem]">
|
||||
{insertTagsContent}
|
||||
{editInputsContent}
|
||||
</MenubarSubContent>
|
||||
</MenubarSub>
|
||||
)}
|
||||
{insertTagsContent != null &&
|
||||
(insertContentDirect ? (
|
||||
insertTagsContent
|
||||
) : (
|
||||
<MenubarSub>
|
||||
<MenubarSubTrigger className="text-xs">
|
||||
{insertTagsLabel}
|
||||
</MenubarSubTrigger>
|
||||
<MenubarSubContent className="min-w-[12rem]">
|
||||
{insertTagsContent}
|
||||
</MenubarSubContent>
|
||||
</MenubarSub>
|
||||
))}
|
||||
</MenubarContent>
|
||||
</MenubarMenu>
|
||||
)}
|
||||
|
||||
@@ -599,7 +599,11 @@ export const RenderingNode = memo(function RenderingNode({ id, width, height }:
|
||||
|
||||
<BaseNodeFooter>
|
||||
<NodeFooterEdgeIndicators nodeId={id} nodeType="render">
|
||||
{renderedContent ? (isSvgOutput ? 'Diagram' : 'Markdown') : error ? 'Error' : '—'}
|
||||
{renderedContent
|
||||
? `${isSvgOutput ? 'Diagram' : 'Markdown'} · ${renderedContent.length} chars`
|
||||
: error
|
||||
? 'Error'
|
||||
: '—'}
|
||||
</NodeFooterEdgeIndicators>
|
||||
</BaseNodeFooter>
|
||||
</BaseNode>
|
||||
|
||||
@@ -135,7 +135,9 @@ export const VariableNode = memo(function VariableNode({ id, data }: Props) {
|
||||
</BaseNodeContent>
|
||||
|
||||
<BaseNodeFooter>
|
||||
<NodeFooterEdgeIndicators nodeId={id} nodeType="variable" />
|
||||
<NodeFooterEdgeIndicators nodeId={id} nodeType="variable">
|
||||
{`${valueType.charAt(0).toUpperCase() + valueType.slice(1)} · ${displayValue.length ? `${displayValue.length} chars` : 'none'}`}
|
||||
</NodeFooterEdgeIndicators>
|
||||
</BaseNodeFooter>
|
||||
</BaseNode>
|
||||
)
|
||||
|
||||
@@ -38,7 +38,7 @@ const PLANTUML_INSERT_BLOCKS: InsertBlockOrGroup[] = [
|
||||
{ label: 'Arrow', snippet: ' -> ' },
|
||||
{ label: 'Note', snippet: 'note right of ' },
|
||||
{
|
||||
label: 'Nunjucks',
|
||||
label: 'Templating',
|
||||
items: [
|
||||
{ label: 'Variable {{ }}', snippet: '{{ }}' },
|
||||
{ label: 'if / endif', snippet: '{% if %}\n \n{% endif %}' },
|
||||
@@ -64,7 +64,7 @@ const MARKDOWN_INSERT_BLOCKS: InsertBlockOrGroup[] = [
|
||||
{ label: 'List item', snippet: '- ' },
|
||||
{ label: 'Blockquote', snippet: '> ' },
|
||||
{
|
||||
label: 'Nunjucks',
|
||||
label: 'Templating',
|
||||
items: [
|
||||
{ label: 'Variable {{ }}', snippet: '{{ }}' },
|
||||
{ label: 'if / endif', snippet: '{% if %}\n \n{% endif %}' },
|
||||
|
||||
Reference in New Issue
Block a user