refactor: Update forms for inline safety, add MetadataSidePanel, update layout

- NodeFormDecision: option reorder via onUpdate (no premature store writes)
- NodePicker: add allowCreate prop (default true) to hide Create New options
  during inline canvas editing, preventing side-effect node creation
- MetadataSidePanel: 320px right slide-in overlay wrapping TreeMetadataForm,
  closes on backdrop click, close button, and Escape key
- TreeEditorLayout: Flow mode now renders full-width TreeCanvas + MetadataSidePanel
  overlay; Code mode unchanged (Monaco + preview split)

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
chihlasm
2026-02-17 22:45:35 -05:00
parent 9e59b04dcc
commit 79bf051666
4 changed files with 104 additions and 29 deletions

View File

@@ -0,0 +1,66 @@
import { useEffect } from 'react'
import { X } from 'lucide-react'
import { TreeMetadataForm } from './TreeMetadataForm'
interface MetadataSidePanelProps {
isOpen: boolean
onClose: () => void
}
export function MetadataSidePanel({ isOpen, onClose }: MetadataSidePanelProps) {
// Close on Escape key
useEffect(() => {
if (!isOpen) return
const handleKeyDown = (e: KeyboardEvent) => {
if (e.key === 'Escape') {
onClose()
}
}
document.addEventListener('keydown', handleKeyDown)
return () => document.removeEventListener('keydown', handleKeyDown)
}, [isOpen, onClose])
if (!isOpen) return null
return (
<>
{/* Backdrop — click to close */}
<div
className="fixed inset-0 z-40 bg-background/40 backdrop-blur-sm"
onClick={onClose}
aria-hidden="true"
/>
{/* Side panel — slides in from right */}
<div
className="fixed right-0 top-0 z-50 flex h-full w-80 flex-col border-l border-border bg-card shadow-xl"
role="dialog"
aria-label="Flow metadata"
>
{/* Panel header */}
<div className="flex items-center justify-between border-b border-border px-4 py-3">
<h2 className="text-sm font-semibold text-foreground font-heading">
Flow Details
</h2>
<button
type="button"
onClick={onClose}
className="rounded p-1.5 text-muted-foreground hover:bg-accent hover:text-foreground"
aria-label="Close metadata panel"
>
<X className="h-4 w-4" />
</button>
</div>
{/* Scrollable metadata form */}
<div className="flex-1 overflow-y-auto px-4 py-4">
<TreeMetadataForm />
</div>
</div>
</>
)
}
export default MetadataSidePanel

View File

@@ -16,7 +16,7 @@ const indexToLetter = (index: number): string => {
}
export function NodeFormDecision({ node, onUpdate }: NodeFormDecisionProps) {
const { reorderOptions, validationErrors } = useTreeEditorStore()
const { validationErrors } = useTreeEditorStore()
const isRootNode = node.id === 'root'
const questionError = validationErrors.find(
@@ -51,7 +51,12 @@ export function NodeFormDecision({ node, onUpdate }: NodeFormDecisionProps) {
}
const handleReorderOptions = (fromIndex: number, toIndex: number) => {
reorderOptions(node.id, fromIndex, toIndex)
// Mutate local draft via onUpdate (backward-compatible: modal path relays to store,
// canvas path updates local draft without writing to store early)
const newOptions = [...(node.options || [])]
const [moved] = newOptions.splice(fromIndex, 1)
newOptions.splice(toIndex, 0, moved)
onUpdate({ options: newOptions })
}
return (

View File

@@ -35,6 +35,9 @@ interface NodePickerProps {
error?: string
/** Callback when a new node is created (receives the new node ID) */
onNodeCreated?: (nodeId: string) => void
/** Whether to show the "Create New Node" options. Default: true.
* Set to false in inline canvas editing to prevent premature store writes. */
allowCreate?: boolean
}
export function NodePicker({
@@ -46,7 +49,8 @@ export function NodePicker({
className,
label,
error,
onNodeCreated
onNodeCreated,
allowCreate = true
}: NodePickerProps) {
const { getAvailableTargetNodes, addNode, updateNode } = useTreeEditorStore()
const availableNodes = getAvailableTargetNodes(excludeNodeId)
@@ -201,12 +205,14 @@ export function NodePicker({
>
<option value="">{placeholder}</option>
{/* Create new options */}
<optgroup label="Create New Node">
<option value={CREATE_DECISION}>+ New Decision (question)</option>
<option value={CREATE_ACTION}>+ New Action (task)</option>
<option value={CREATE_SOLUTION}>+ New Solution (endpoint)</option>
</optgroup>
{/* Create new options — hidden when allowCreate=false (e.g. canvas inline editing) */}
{allowCreate && (
<optgroup label="Create New Node">
<option value={CREATE_DECISION}>+ New Decision (question)</option>
<option value={CREATE_ACTION}>+ New Action (task)</option>
<option value={CREATE_SOLUTION}>+ New Solution (endpoint)</option>
</optgroup>
)}
{/* Existing nodes grouped by type */}
{groupedNodes.decisions.length > 0 && (

View File

@@ -1,7 +1,7 @@
import { lazy, Suspense } from 'react'
import { TreeMetadataForm } from './TreeMetadataForm'
import { NodeList } from './NodeList'
import { TreePreviewPanel } from '@/components/tree-preview/TreePreviewPanel'
import { TreeCanvas } from './TreeCanvas'
import { MetadataSidePanel } from './MetadataSidePanel'
import { useTreeEditorStore } from '@/store/treeEditorStore'
import { cn } from '@/lib/utils'
@@ -12,9 +12,15 @@ const CodeModeEditor = lazy(() =>
interface TreeEditorLayoutProps {
isMobile?: boolean
isMetadataOpen?: boolean
onCloseMetadata?: () => void
}
export function TreeEditorLayout({ isMobile = false }: TreeEditorLayoutProps) {
export function TreeEditorLayout({
isMobile = false,
isMetadataOpen = false,
onCloseMetadata = () => {},
}: TreeEditorLayoutProps) {
const editorMode = useTreeEditorStore(s => s.editorMode)
return (
@@ -26,7 +32,7 @@ export function TreeEditorLayout({ isMobile = false }: TreeEditorLayoutProps) {
>
{editorMode === 'code' ? (
<>
{/* Code Mode: Monaco editor (60%) + Preview (40%) */}
{/* Code Mode: Monaco editor (60%) + Preview (40%) — unchanged */}
<div className={cn(
'flex flex-col overflow-hidden border-border',
isMobile ? 'h-full w-full border-b' : 'w-3/5 border-r'
@@ -50,24 +56,16 @@ export function TreeEditorLayout({ isMobile = false }: TreeEditorLayoutProps) {
</>
) : (
<>
{/* Flow Mode: Form editor (60%) + Preview (40%) */}
<div className={cn(
'flex flex-col overflow-y-auto border-border',
isMobile ? 'h-full w-full border-b' : 'w-3/5 border-r'
)}>
<div className="space-y-4 p-4">
<TreeMetadataForm />
<NodeList />
</div>
{/* Flow Mode: Full-width visual canvas */}
<div className="flex-1 overflow-hidden">
<TreeCanvas />
</div>
{/* Right Panel - Preview */}
<div className={cn(
'flex-1 overflow-hidden bg-accent/50',
isMobile ? 'hidden' : 'block'
)}>
<TreePreviewPanel />
</div>
{/* Metadata side panel — overlays the canvas from the right */}
<MetadataSidePanel
isOpen={isMetadataOpen}
onClose={onCloseMetadata}
/>
</>
)}
</div>