feat: multi-select steps in the simplified view

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
This commit is contained in:
Diego Imbert
2026-09-27 03:00:13 +02:00
co-authored by Claude Opus 5.5
parent 931892d7c1
commit 13d9ec652e
3 changed files with 54 additions and 17 deletions
@@ -1335,6 +1335,9 @@
{minHeight}
{maxHeight}
{selectedId}
{selectionManager}
selectionOverlay={multiSelectEnabled ? selectionBox : undefined}
selectionKey={editMode ? modifierKey : undefined}
structure={'structureTree' in graph ? graph.structureTree : undefined}
{editMode}
{insertable}
@@ -1422,19 +1425,7 @@
{/if}
{#if multiSelectEnabled}
<SelectionBoundingBox
selectedNodes={selectionManager.selectedIds.filter((id) =>
nodesWithOffset.some((n) => n.id === id)
)}
allNodes={nodesWithOffset as (Node & { type: string })[]}
onDeleteSelected={() => onDeleteMultiple?.(resolvedModuleIds)}
onDuplicateSelected={() => onDuplicateMultiple?.(resolvedModuleIds)}
onMoveSelected={() => onMoveMultiple?.(resolvedModuleIds)}
onCancelMove={() => onMoveMultiple?.(movingIds ?? [])}
{canMoveSelected}
isMoving={movingIds != null && movingIds.length > 0}
{resolvedModuleIds}
/>
{@render selectionBox(nodesWithOffset)}
{/if}
<GroupOverlay
@@ -1548,6 +1539,20 @@
{/if}
</div>
{#snippet selectionBox(allNodes: Node[])}
<SelectionBoundingBox
selectedNodes={selectionManager.selectedIds.filter((id) => allNodes.some((n) => n.id === id))}
allNodes={allNodes as (Node & { type: string })[]}
onDeleteSelected={() => onDeleteMultiple?.(resolvedModuleIds)}
onDuplicateSelected={() => onDuplicateMultiple?.(resolvedModuleIds)}
onMoveSelected={() => onMoveMultiple?.(resolvedModuleIds)}
onCancelMove={() => onMoveMultiple?.(movingIds ?? [])}
{canMoveSelected}
isMoving={movingIds != null && movingIds.length > 0}
{resolvedModuleIds}
/>
{/snippet}
{#snippet simplifiedViewToggle()}
<Toggle bind:checked={simplifiedView} size="xs" options={{ right: 'Simplified view' }} />
{/snippet}
@@ -1,5 +1,11 @@
<script lang="ts">
import { SvelteFlow, SvelteFlowProvider, Controls } from '@xyflow/svelte'
import {
SvelteFlow,
SvelteFlowProvider,
Controls,
SelectionMode,
type Node
} from '@xyflow/svelte'
import '@xyflow/svelte/dist/base.css'
import type { FlowModule } from '$lib/gen'
import type { Snippet } from 'svelte'
@@ -7,6 +13,8 @@
import { setSimplifiedEditContext } from './simplifiedContext'
import type { FlowStructureNode } from '../flowStructure'
import type { GraphEventHandlers } from '../graphBuilder.svelte'
import type { SelectionManager } from '../selectionUtils.svelte'
import SelectionTool from '../SelectionTool.svelte'
import SimplifiedNode from './SimplifiedNode.svelte'
import SimplifiedBox from './SimplifiedBox.svelte'
import SimplifiedEdge from './SimplifiedEdge.svelte'
@@ -32,6 +40,12 @@
insertable?: boolean
eventHandlers?: GraphEventHandlers
disableAi?: boolean
/** The graph's selection manager; with `selectionOverlay`, several steps can be selected */
selectionManager?: SelectionManager
/** Drawn over a multi-step selection (its bulk actions), given the laid-out nodes */
selectionOverlay?: Snippet<[Node[]]>
/** Held while dragging on the canvas to select a rectangle of steps */
selectionKey?: string
}
let {
@@ -47,7 +61,10 @@
editMode = false,
insertable = false,
eventHandlers,
disableAi = false
disableAi = false,
selectionManager,
selectionOverlay,
selectionKey
}: Props = $props()
setSimplifiedEditContext({
@@ -76,8 +93,11 @@
let nodes = $derived(
layout.nodes.map((n) => ({
...n,
// The selection box measures nodes by their `measured` size
measured: { width: n.width, height: n.height },
selected:
n.id === selectedId ||
!!selectionManager?.selectedIds.includes(n.id) ||
(n.type === 'simplifiedBox' && !!selectedId?.startsWith(`${n.id}-branch-`))
}))
)
@@ -131,17 +151,27 @@
maxZoom={1.6}
nodesDraggable={false}
nodesConnectable={false}
elementsSelectable={false}
elementsSelectable={!!selectionOverlay}
multiSelectionKey="Shift"
selectionKey={selectionOverlay ? (selectionKey ?? null) : null}
selectionMode={SelectionMode.Partial}
zoomOnDoubleClick={false}
deleteKey={null}
onnodeclick={({ node }) => {
onnodeclick={({ node, event }) => {
// Adding to a multi-selection is xyflow's, synced by SelectionTool
if (event.shiftKey) return
// Boxes stand for their loop or branch step; placeholders are not selectable
if (node.type === 'simplifiedBox' || node.selectable) onSelect?.(node.id)
}}
onpaneclick={() => selectionManager?.clearSelection()}
{proOptions}
--background-color={false}
>
<div class="absolute inset-0 !bg-surface-secondary h-full"></div>
{#if selectionOverlay && selectionManager}
<SelectionTool {selectionManager} />
{@render selectionOverlay(nodes)}
{/if}
<Controls position="top-right" orientation="horizontal" showLock={false} />
{#if topLeftControls}
<Controls
@@ -839,6 +839,8 @@ export function layoutSimplifiedFlow(
position: { x: left, y: b.y },
data,
draggable: false,
// A rectangle drawn over the steps inside would otherwise also select the loop or branch
selectable: false,
zIndex: -1,
// Only the group's label is interactive; SimplifiedBox re-enables pointer events on it.
style: 'pointer-events: none;',