feat: add node picker dropdown to action node form for cross-references
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -1,6 +1,7 @@
|
|||||||
import { useState } from 'react'
|
import { useState } from 'react'
|
||||||
import { DynamicArrayField } from './DynamicArrayField'
|
import { DynamicArrayField } from './DynamicArrayField'
|
||||||
import { useTreeEditorStore } from '@/store/treeEditorStore'
|
import { useTreeEditorStore, collectAllNodesFlat } from '@/store/treeEditorStore'
|
||||||
|
import { Link2, X } from 'lucide-react'
|
||||||
import { MarkdownContent } from '@/components/ui/MarkdownContent'
|
import { MarkdownContent } from '@/components/ui/MarkdownContent'
|
||||||
import { InfoTip } from '@/components/common/InfoTip'
|
import { InfoTip } from '@/components/common/InfoTip'
|
||||||
import type { TreeStructure } from '@/types'
|
import type { TreeStructure } from '@/types'
|
||||||
@@ -158,16 +159,65 @@ export function NodeFormAction({ node, onUpdate }: NodeFormActionProps) {
|
|||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Next step hint */}
|
{/* Link to existing node */}
|
||||||
{hasNextNode ? (
|
<div>
|
||||||
<p className="text-xs text-muted-foreground">
|
<label className="flex items-center gap-1.5 text-sm font-medium text-foreground">
|
||||||
Next step is linked — click it on the canvas to edit.
|
<Link2 className="h-3.5 w-3.5" />
|
||||||
|
Next Step
|
||||||
|
</label>
|
||||||
|
{hasNextNode ? (
|
||||||
|
<div className="mt-1 flex items-center gap-2 rounded-md border border-primary/30 bg-primary/5 px-3 py-2">
|
||||||
|
<span className="flex-1 truncate text-sm text-foreground">
|
||||||
|
Linked to: {(() => {
|
||||||
|
const treeStructure = useTreeEditorStore.getState().treeStructure
|
||||||
|
const allNodes = collectAllNodesFlat(treeStructure)
|
||||||
|
const target = allNodes.find(n => n.id === node.next_node_id)
|
||||||
|
return target ? target.label : node.next_node_id
|
||||||
|
})()}
|
||||||
|
</span>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
onClick={() => onUpdate({ next_node_id: undefined })}
|
||||||
|
className="rounded p-1 text-muted-foreground hover:bg-accent hover:text-foreground"
|
||||||
|
title="Remove link"
|
||||||
|
>
|
||||||
|
<X className="h-3.5 w-3.5" />
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
<select
|
||||||
|
value=""
|
||||||
|
onChange={(e) => {
|
||||||
|
if (e.target.value) {
|
||||||
|
onUpdate({ next_node_id: e.target.value })
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
className={cn(
|
||||||
|
'mt-1 block w-full rounded-md border border-border px-3 py-2 text-sm',
|
||||||
|
'bg-card text-foreground',
|
||||||
|
'focus:border-primary focus:outline-none focus:ring-1 focus:ring-primary/20'
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
<option value="">Link to existing node...</option>
|
||||||
|
{(() => {
|
||||||
|
const treeStructure = useTreeEditorStore.getState().treeStructure
|
||||||
|
const allNodes = collectAllNodesFlat(treeStructure)
|
||||||
|
return allNodes
|
||||||
|
.filter(n => n.id !== node.id && n.type !== 'answer')
|
||||||
|
.map(n => (
|
||||||
|
<option key={n.id} value={n.id}>
|
||||||
|
{' '.repeat(n.depth)}{n.type === 'decision' ? '?' : n.type === 'action' ? '>' : '*'} {n.label}
|
||||||
|
</option>
|
||||||
|
))
|
||||||
|
})()}
|
||||||
|
</select>
|
||||||
|
)}
|
||||||
|
<p className="mt-1 text-xs text-muted-foreground">
|
||||||
|
{hasNextNode
|
||||||
|
? 'This action will navigate to the linked node.'
|
||||||
|
: 'Select a node to navigate to after this action, or save to create a new placeholder.'}
|
||||||
</p>
|
</p>
|
||||||
) : (
|
</div>
|
||||||
<p className="text-xs text-yellow-400/70">
|
|
||||||
Save to create a placeholder for the next step.
|
|
||||||
</p>
|
|
||||||
)}
|
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -123,6 +123,25 @@ export const findNodeInTree = (
|
|||||||
return null
|
return null
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** Collect all nodes in the tree as a flat list with depth info. */
|
||||||
|
export function collectAllNodesFlat(
|
||||||
|
root: TreeStructure | null
|
||||||
|
): Array<{ id: string; label: string; type: string; depth: number }> {
|
||||||
|
if (!root) return []
|
||||||
|
const result: Array<{ id: string; label: string; type: string; depth: number }> = []
|
||||||
|
|
||||||
|
function walk(node: TreeStructure, depth: number) {
|
||||||
|
const label = node.type === 'decision'
|
||||||
|
? (node.question || 'Untitled Decision')
|
||||||
|
: (node.title || `Untitled ${node.type}`)
|
||||||
|
result.push({ id: node.id, label, type: node.type, depth })
|
||||||
|
node.children?.forEach(child => walk(child, depth + 1))
|
||||||
|
}
|
||||||
|
|
||||||
|
walk(root, 0)
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
|
||||||
// Helper to find parent of a node
|
// Helper to find parent of a node
|
||||||
const findParentNode = (
|
const findParentNode = (
|
||||||
nodeId: string,
|
nodeId: string,
|
||||||
|
|||||||
Reference in New Issue
Block a user