Files
resolutionflow/docs/plans/IMPLEMENTATION-PLAN-TREE-EDITOR-CANVAS.md
2026-02-19 01:53:54 -05:00

575 lines
24 KiB
Markdown

# Implementation Plan: Tree Editor Canvas Redesign
> **Date:** February 17, 2026
> **Scope:** Replace NodeList + NodeEditorModal + TreePreviewPanel in Flow mode with a visual canvas + inline card editing
> **Estimated Components:** 3 new files, 4 modified files
> **Phases:** 4 (sequential)
> **Branch:** `feature/tree-editor-canvas`
---
## Overview
Replace the current text-outline + modal + passive preview layout with a single-pane visual canvas where nodes are cards, editing is inline, and branches are visually connected. The tree IS the editor — no separate preview panel needed.
### Current State
The tree editor uses a text-outline metaphor:
- Nodes listed as indented rows with ASCII tree lines in `NodeList.tsx`
- Editing requires opening `NodeEditorModal.tsx` for each node (click row → modal opens → edit → Done → modal closes)
- Passive `TreePreviewPanel.tsx` takes 40% of space but offers no editing
- Adding nodes is a two-step picker-then-modal flow
- Options/branches are opaque — can't see where each branch leads without clicking into the node
### Target State (5 Outcomes)
1. Flow mode uses a full-width `TreeCanvas` editor; preview panel is removed from Flow mode
2. Node editing is inline in cards with local draft + ✓ save + ✕ cancel (no modal)
3. Branches are visually rendered with parent-child connector lines and horizontal splits
4. Metadata moves to a right slide-in panel, collapsed by default, opened from toolbar in Flow mode only
5. Code mode remains functionally unchanged (Monaco + preview split)
### Layout After Redesign
```
┌─────────────────────────────────────────────────────────┐
│ TOOLBAR: [Flow Name] [Undo] [Redo] [Metadata] [Save] [Publish] │
├─────────────────────────────────────────────────────────┤
│ │
│ [START] │
│ │ │
│ ┌─────────────────────────────┐ │
│ │ ? What type of issue? │ ← Decision card │
│ │ ↳ [A] Network Issues │ │
│ │ ↳ [B] App Errors │ │
│ └─────────────────────────────┘ │
│ │ │ │
│ [Network card] [App Errors card] │
│ │ │
│ [Solution card] │
│ │
│ [+ Add node here] ← contextual add buttons │
│ │
└─────────────────────────────────────────────────────────┘
```
---
## Reusable Code Inventory
Before building anything, confirm these existing pieces are available and understand their APIs:
| Asset | Location | How It's Used |
|-------|----------|---------------|
| `useTreeEditorStore()` | `store/treeEditorStore.ts` | All CRUD: `addNode`, `updateNode`, `deleteNode`, `duplicateNode`, `reorderNodes`, `selectNode`, `findNode`, `validationErrors` |
| `NodeFormDecision` | `components/tree-editor/NodeFormDecision.tsx` | Decision node fields (question, help_text, options) — will be rendered inline in card |
| `NodeFormAction` | `components/tree-editor/NodeFormAction.tsx` | Action node fields — will be rendered inline in card |
| `NodeFormResolution` | `components/tree-editor/NodeFormResolution.tsx` | Solution node fields — will be rendered inline in card |
| `DynamicArrayField` | `components/tree-editor/DynamicArrayField.tsx` | Reuse for options array in inline card |
| `NodePicker` | `components/tree-editor/NodePicker.tsx` | Reuse for option target selection (needs `allowCreate` prop added) |
| `TreeMetadataForm` | `components/tree-editor/TreeMetadataForm.tsx` | Wraps into MetadataSidePanel as-is |
| `cn()` | `@/lib/utils` | Tailwind class merging utility |
| Design tokens | `tailwind.config.js` | `bg-card`, `border-border`, `text-foreground`, `font-heading`, `font-label` |
| Brand colors | `tailwind.config.js` | Blue (decision), Yellow (action), Green (solution) |
| `buildSharedLinksMap()` | `components/tree-preview/TreePreviewPanel.tsx` | Shared node detection logic — extract and reuse for jump/reference indicators |
---
## Link Model & Rendering Rules
**IMPORTANT:** These rules govern how the canvas renders node connections. Read before implementing Phase 2.
### Tree-First Rendering
Render only structural children (nodes in `node.children[]`) with visual connector lines. Do NOT draw cross-canvas connector lines for shared/cross-linked nodes.
For links to non-child/shared targets (where `option.next_node_id` points to a node that is NOT a direct child), show a compact "jump/reference" indicator badge in the card content instead of a connector line.
### Decision Child Lane Ordering
When a decision node has children, order the horizontal child lanes:
1. **First:** Children whose `id` matches an `option.next_node_id` — ordered by option order
2. **Then:** Append any remaining unlinked children
### Action Next-Child Rule
- If a child's `id` matches the action node's `next_node_id`, treat it as the primary "next" lane
- If no child matches `next_node_id`, keep child visible but show the link as an "unbound reference" indicator
### Deletion Safety
**Before calling `deleteNode(nodeId)`**, the canvas must clean up all inbound references:
- Scan all decision nodes: clear any `options[].next_node_id` that equals the deleted node's ID
- Scan all action nodes: clear any `next_node_id` that equals the deleted node's ID
This prevents stale link references. The current `treeEditorStore.deleteNode()` does NOT do this cleanup — the canvas orchestration layer handles it.
---
## Phase 1: TreeCanvasNode Component (Core Inline Editor Card)
**File:** `frontend/src/components/tree-editor/TreeCanvasNode.tsx` (NEW)
### Props Interface
```typescript
interface TreeCanvasNodeProps {
node: TreeStructure
depth: number
fromOption?: string // Which parent option label leads here
isExpanded: boolean
isNew: boolean // Show "Unsaved" badge, cancel triggers delete
onToggleExpand: () => void
onSave: (nodeId: string, updates: Partial<TreeStructure>) => void
onCancelNew: (nodeId: string) => void // Delete unsaved node
onDelete: (nodeId: string) => void
onDuplicate: (nodeId: string) => void
onDragStart: (e: React.DragEvent, nodeId: string) => void
onDragOver: (e: React.DragEvent) => void
onDrop: (e: React.DragEvent) => void
}
```
### Card States
**Compact (default):**
- Node type badge/icon (Decision ?, Action ⚡, Solution ✓)
- Title text (question for decisions, title for action/solution)
- Option labels or option count for decisions
- Validation error badge (red dot if errors on this node)
- "Unsaved" badge (yellow, if `isNew`)
- Click anywhere on card → calls `onToggleExpand`
**Expanded (editing):**
- Local draft state: `const [draft, setDraft] = useState<Partial<TreeStructure>>(() => cloneNodeWithoutChildren(node))`
- Renders the appropriate existing form subcomponent inline:
- Decision: `<NodeFormDecision node={draft} onUpdate={setDraftField} />`
- Action: `<NodeFormAction node={draft} onUpdate={setDraftField} />`
- Solution: `<NodeFormResolution node={draft} onUpdate={setDraftField} />`
- Header actions row:
- ✓ Save button → calls `onSave(node.id, draft)` (strip children from draft before passing)
- ✕ Cancel button → if `isNew`, calls `onCancelNew(node.id)`. Otherwise resets draft to node values and collapses
- Duplicate button (hide if root)
- Delete button (hide if root)
- Drag handle in header (hide if root)
### Card Styling
Aesthetic direction: "Precision engineering tool" — clean, minimal chrome, confident typography.
```
All cards: bg-card border-border rounded-xl shadow-sm
Decision: border-l-4 border-blue-500
Action: border-l-4 border-yellow-500
Solution: border-l-4 border-green-500
Expanded ring: ring-1 ring-primary
Titles: font-heading (Plus Jakarta Sans)
Type badges: font-label (Outfit)
```
### Implementation Steps
1. Create `TreeCanvasNode.tsx` with compact view only (type badge, title, option count)
2. Add expanded view with local draft state and inline form rendering
3. Wire save/cancel/delete/duplicate actions
4. Add drag handle events
5. Add validation badge and unsaved badge
6. Style with brand tokens
### Verification
- Render a single card in isolation with mock data
- Confirm compact → expanded toggle works
- Confirm save commits draft (log output), cancel resets
- Confirm cancel on `isNew=true` calls `onCancelNew`
- Run `npm run build` — no TypeScript errors
---
## Phase 2: TreeCanvas Component (Layout & Orchestration)
**File:** `frontend/src/components/tree-editor/TreeCanvas.tsx` (NEW)
### Canvas State Model
```typescript
// Local UI state (NOT in Zustand store — canvas-only concerns)
const [expandedNodeIds, setExpandedNodeIds] = useState<Set<string>>(new Set())
const [newNodeIds, setNewNodeIds] = useState<Set<string>>(new Set())
const [pendingAddTarget, setPendingAddTarget] = useState<string | null>(null)
const [pendingLinkByNodeId, setPendingLinkByNodeId] = useState<Map<string, {
parentId: string
optionId?: string // For decision option linking
}>>(new Map())
const [dragState, setDragState] = useState<{
nodeId: string
parentId: string
index: number
} | null>(null)
```
**Single expanded card policy:** Only one card expanded at a time. When expanding a card, collapse the previously expanded one.
### Rendering
Recursive rendering of `treeStructure` from the store:
- Root at top, rendered as a "START" card
- Vertical flow downward
- When a decision node has multiple options with children, render children in horizontal lanes side-by-side
- Single-pane scrollable area (`overflow-auto`)
- Background: `bg-background` with subtle CSS radial dot grid pattern
### Connector Lines
Use CSS borders (not SVG) for connecting lines:
- **Parent-to-children trunk line:** `border-l border-border` extending down from parent
- **Horizontal fork line:** `border-t border-border` connecting sibling lane tops
- **Vertical stubs:** `border-l border-border` dropping into each child card
### Add-Node Flow
| Parent Type | Add Button Behavior |
|-------------|-------------------|
| Decision | Show `+ Add child` per option row (next to each option label) |
| Action | Show single `+ Add child` below the card |
| Solution | No add button (terminal node) |
- `+ Add` buttons use dashed border, appear on hover of parent card bottom edge
- Clicking `+ Add` sets `pendingAddTarget` → shows inline type picker (decision/action/solution buttons) at that position
- Selecting a type:
1. Calls `addNode(parentId, type)` → gets new node ID
2. Adds node ID to `newNodeIds`
3. Adds entry to `pendingLinkByNodeId` (parent ID + option ID if from a decision option)
4. Auto-expands the new node card
5. Clears `pendingAddTarget`
### Save Behavior for New Child Nodes
When user clicks ✓ on a new node:
1. Call `updateNode(nodeId, draft)` to save content to store
2. If `pendingLinkByNodeId.has(nodeId)`:
- Get the pending link info (`parentId`, `optionId`)
- If `optionId` exists: update parent's `options[].next_node_id` to point to this node
- If no `optionId` (action parent): update parent's `next_node_id` to point to this node
3. Remove from `newNodeIds`
4. Remove from `pendingLinkByNodeId`
### Cancel Behavior for New Nodes
When user clicks ✕ on a new (unsaved) node:
1. Call `deleteNode(nodeId)`
2. Remove from `newNodeIds`
3. Remove from `pendingLinkByNodeId`
### Delete Behavior (Any Node)
When user clicks delete on any node:
1. **Clean inbound references first** (see Link Model section above):
- Walk the full tree and clear any `options[].next_node_id` or `next_node_id` matching the node being deleted
2. Then call `deleteNode(nodeId)`
3. Remove from `expandedNodeIds` if present
### Sibling Reorder
- Drag handle in card header (Phase 1 wired the events)
- Drop zones rendered between sibling cards (visual indicator line)
- On drop: call `reorderNodes(parentId, fromIndex, toIndex)`
### Selection Integration
- Click on a card calls `selectNode(nodeId)` in the store
- Watch `selectedNodeId` from store (including changes from `ValidationSummary` clicks):
- Auto-expand the selected node's card
- Scroll the card into view with `scrollIntoView({ behavior: 'smooth', block: 'nearest' })`
### Implementation Steps
1. Create `TreeCanvas.tsx` with recursive tree rendering (compact cards only, no editing)
2. Add CSS connector lines between parent and children
3. Add horizontal branching for decision nodes with multiple children
4. Add canvas state model (expandedNodeIds, newNodeIds, etc.)
5. Wire card expand/collapse with single-expanded-card policy
6. Add inline type picker and add-node flow with pending link tracking
7. Wire save/cancel with pending link resolution
8. Wire delete with inbound reference cleanup
9. Add drag-and-drop sibling reorder
10. Add selection sync (auto-expand + scroll into view)
11. Add grid background pattern
12. Style add buttons (dashed border, hover reveal)
### Verification
- Create a new tree → see canvas with root START card
- Click root → expands inline (no modal)
- Add 2 options, save → see branch lanes
- Add child from each option → pending link resolves on save
- Cancel a new node → node deleted, link cleaned up
- Delete a linked node → parent's reference cleared
- Drag reorder siblings
- Run `npm run build` — no TypeScript errors
---
## Phase 3: Form Refactoring + Layout Update
### 3A: Form Refactoring for Inline Reuse Safety
**Files to modify:**
- `frontend/src/components/tree-editor/NodeFormDecision.tsx`
- `frontend/src/components/tree-editor/NodePicker.tsx`
#### NodeFormDecision.tsx Changes
**Problem:** Currently, option reordering calls `reorderOptions()` directly on the store. In inline canvas editing, this would write to the store before the user clicks ✓ save (breaking the local draft model).
**Fix:** Change option reordering to mutate the local `node.options` array through the `onUpdate` callback instead of calling the store directly.
```typescript
// BEFORE (writes to store immediately):
const handleReorderOptions = (fromIndex: number, toIndex: number) => {
reorderOptions(node.id, fromIndex, toIndex)
}
// AFTER (mutates local draft via onUpdate):
const handleReorderOptions = (fromIndex: number, toIndex: number) => {
const newOptions = [...(node.options || [])]
const [moved] = newOptions.splice(fromIndex, 1)
newOptions.splice(toIndex, 0, moved)
onUpdate({ options: newOptions })
}
```
**Keep modal compatibility:** This change is backward-compatible. In the legacy modal path, `onUpdate` already propagates to the store. In the canvas path, `onUpdate` updates the local draft.
#### NodePicker.tsx Changes
**Problem:** `NodePicker` currently has create-new-node options (`__create_decision__`, etc.) that call `addNode()` on the store. In canvas inline editing, this would create nodes as a side effect of browsing the picker during draft editing.
**Fix:** Add an `allowCreate` prop:
```typescript
interface NodePickerProps {
// ... existing props
allowCreate?: boolean // default: true
}
```
- When `allowCreate={false}`, hide the "Create New" option group
- Pass `allowCreate={false}` from `TreeCanvasNode` expanded editing
- Pass `allowCreate={true}` (default) in legacy modal path
### 3B: Layout Update
**File:** `frontend/src/components/tree-editor/TreeEditorLayout.tsx`
#### Changes
```typescript
interface TreeEditorLayoutProps {
isMobile?: boolean
isMetadataOpen: boolean // NEW
onCloseMetadata: () => void // NEW
}
```
**Flow mode (replace the 60/40 split):**
```
BEFORE:
<div className="w-3/5">
<TreeMetadataForm />
<NodeList />
</div>
<div className="flex-1">
<TreePreviewPanel />
</div>
AFTER:
<TreeCanvas /> (full width)
<MetadataSidePanel isOpen={isMetadataOpen} onClose={onCloseMetadata} /> (overlay)
```
**Code mode:** Unchanged. Keep existing 60/40 Monaco + preview behavior.
### 3C: MetadataSidePanel Component
**File:** `frontend/src/components/tree-editor/MetadataSidePanel.tsx` (NEW)
Right-side slide-in panel (320px wide) that wraps `<TreeMetadataForm />`.
```typescript
interface MetadataSidePanelProps {
isOpen: boolean
onClose: () => void
}
```
Behavior:
- Slides in from right edge, overlays the canvas (does NOT resize it)
- Close triggers: panel close button, backdrop click, Escape key
- Uses existing overlay/backdrop pattern from other modals in the app
- Only rendered in Flow mode
### Implementation Steps
1. Refactor `NodeFormDecision.tsx` — option reorder through `onUpdate`
2. Add `allowCreate` prop to `NodePicker.tsx`
3. Create `MetadataSidePanel.tsx`
4. Update `TreeEditorLayout.tsx` — swap Flow mode layout, add metadata panel props
5. Verify legacy modal path still works (if still referenced anywhere)
### Verification
- Open tree editor in Flow mode → see full-width canvas (no 60/40 split)
- Open tree editor in Code mode → see unchanged Monaco + preview layout
- Click Metadata button → panel slides in from right, canvas doesn't resize
- Close metadata panel via close button, backdrop click, and Escape key
- Edit options in inline card → reorder does NOT write to store until ✓ save
- NodePicker in inline card → no "Create New" options shown
- Run `npm run build` — no TypeScript errors
---
## Phase 4: Toolbar Wiring + Exports
**File:** `frontend/src/pages/TreeEditorPage.tsx` (modify)
### Changes
1. Add local state: `const [isMetadataOpen, setIsMetadataOpen] = useState(false)`
2. Add "Metadata" toolbar button — visible in Flow mode only
3. Auto-close metadata panel when switching to Code mode:
```typescript
// In mode switch handler:
if (newMode === 'code') setIsMetadataOpen(false)
```
4. Pass metadata props into `TreeEditorLayout`:
```typescript
<TreeEditorLayout
isMetadataOpen={isMetadataOpen}
onCloseMetadata={() => setIsMetadataOpen(false)}
/>
```
5. Keep all existing toolbar actions unchanged: undo/redo, save/publish, validate, analytics
**File:** `frontend/src/components/tree-editor/index.ts` (modify)
Add exports:
```typescript
export { TreeCanvas } from './TreeCanvas'
export { TreeCanvasNode } from './TreeCanvasNode'
export { MetadataSidePanel } from './MetadataSidePanel'
```
Keep all legacy exports (`NodeList`, `NodeEditorModal`, `TreePreviewPanel`) — they remain in the codebase but are no longer imported in the active Flow path.
### Implementation Steps
1. Add metadata panel state and toolbar button to `TreeEditorPage.tsx`
2. Add auto-close on Code mode switch
3. Pass props through to `TreeEditorLayout`
4. Update `index.ts` exports
5. Verify no dead imports remain
### Verification
- Flow mode toolbar shows "Metadata" button
- Code mode toolbar does NOT show "Metadata" button
- Click Metadata → panel opens. Switch to Code → panel auto-closes
- Undo/redo/save/publish/validate all still work
- Run `npm run build` — no TypeScript errors
---
## Critical Files Summary
| File | Action | Phase | Notes |
|------|--------|-------|-------|
| `TreeCanvasNode.tsx` | Create | 1 | Inline-editing card with local draft + commit model |
| `TreeCanvas.tsx` | Create | 2 | Main canvas orchestration with full state model |
| `MetadataSidePanel.tsx` | Create | 3 | 320px right slide-in overlay wrapping TreeMetadataForm |
| `NodeFormDecision.tsx` | Refactor | 3 | Option reorder through onUpdate (remove store write) |
| `NodePicker.tsx` | Refactor | 3 | Add `allowCreate` prop (default true) |
| `TreeEditorLayout.tsx` | Modify | 3 | Replace 60/40 split with full-width canvas + overlay |
| `TreeEditorPage.tsx` | Modify | 4 | Add metadata panel toggle, auto-close on Code switch |
| `index.ts` | Update | 4 | Export new components, keep legacy exports |
| `treeEditorStore.ts` | No changes | — | Store logic is solid as-is |
| `NodeList.tsx` | Keep (legacy) | — | Removed from active Flow path |
| `NodeEditorModal.tsx` | Keep (legacy) | — | Removed from active Flow path |
| `TreePreviewPanel.tsx` | Keep (legacy) | — | Removed from active Flow path |
---
## Assumptions & Defaults
- **Link rendering:** Tree-first (no full cross-canvas graph lines for shared targets)
- **Inline edit commit:** On checkmark save (local draft, cancel discards)
- **Metadata drawer:** Flow mode only
- **Expanded card policy:** One expanded card at a time
- **Legacy components:** Remain in repo, removed from active Flow path only
- **Connector lines:** CSS borders (not SVG — simpler, matches existing patterns)
---
## Testing
### Automated Tests
**Files:** `TreeCanvas.test.tsx`, `TreeCanvasNode.test.tsx`
| Test Case | What It Verifies |
|-----------|-----------------|
| Select node expands and scrolls card | Selection sync works |
| Inline save commits store updates | Draft → store pipeline works |
| Cancel on new node triggers deletion | Unsaved node cleanup works |
| Add child from decision option sets `next_node_id` on save | Pending link resolution works |
| Delete node clears inbound references | Reference cleanup works |
| Sibling drag reorder calls `reorderNodes` | Drag-and-drop wiring works |
| `allowCreate={false}` hides create options in NodePicker | Form safety works |
| Option reorder in inline card does NOT write to store | Draft isolation works |
Run: `npm run build && npm run test` (or targeted vitest files)
### Manual Acceptance Checklist
- [ ] Create a new tree — canvas shows root START card
- [ ] Click root card — expands inline with decision fields (no modal appears)
- [ ] Fill in question, add 2 options, click ✓ — saves inline, see branch lanes
- [ ] `+ Add child` buttons appear below each option
- [ ] Add a child node inline — verify parent link is set correctly
- [ ] Cancel a new unsaved node — confirm it is deleted from the tree
- [ ] Delete a node that is referenced by another — confirm references are cleaned
- [ ] Drag to reorder sibling nodes
- [ ] Open Metadata panel — edit metadata — close panel (button, backdrop, Escape)
- [ ] Validate and Publish — confirm tree saves correctly
- [ ] Switch Flow → Code mode — metadata panel auto-closes, Code mode works normally
- [ ] Switch Code → Flow mode — canvas renders correctly
- [ ] Run `npm run build` — no TypeScript errors
- [ ] Run `npm run test` — all tests pass
---
## Git Strategy
- Branch: `feature/tree-editor-canvas`
- One commit per phase (4 commits total)
- Commit messages:
1. `feat: Add TreeCanvasNode inline editor card component`
2. `feat: Add TreeCanvas layout with visual branching and orchestration`
3. `refactor: Update forms for inline safety, add MetadataSidePanel, update layout`
4. `feat: Wire toolbar metadata toggle and update exports`
- PR when all 4 phases pass `npm run build && npm run test`
- Include `Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>`
---
## Notes for Implementation
1. **Read existing code first:** Before creating any new file, read the files listed in the Reusable Code Inventory to understand current patterns and prop interfaces
2. **Follow existing patterns:** Match the component structure, Tailwind usage, and TypeScript conventions already in the tree-editor directory
3. **Dark mode:** All new components must support light/dark themes via existing Tailwind classes
4. **Keyboard navigation:** Support Escape to close metadata panel, Tab through form fields in expanded cards
5. **Loading states:** Canvas should handle the case where `treeStructure` is null (show empty state)
6. **No store changes:** The `treeEditorStore.ts` should NOT be modified. All new state is local to the canvas components
7. **Test each phase independently:** Each phase should leave the app in a buildable, testable state before moving to the next