- import { useOnSelectionChange, useStore, type Node } from '@xyflow/svelte'
+ import { useOnSelectionChange, useStore } from '@xyflow/svelte'
import type { SelectionManager } from './selectionUtils.svelte'
interface Props {
selectionManager: SelectionManager
@@ -12,14 +12,7 @@
// Handle selection changes from SvelteFlow
useOnSelectionChange(({ nodes: selectedNodes, edges: _selectedEdges }) => {
- // Notes are already non-selectable, so no filtering needed
- const selectedNodeIds = selectedNodes.map((node: Node) => node.id)
-
- // Only select nodes if multiple nodes are selected
- // To avoid conflicting with the node-level click events
- if (selectedNodeIds.length > 1) {
- selectionManager.selectNodes(selectedNodeIds, false)
- }
+ selectionManager.selectedNodesInGraph = selectedNodes
})
diff --git a/frontend/src/lib/components/graph/renderers/nodes/AssetNode.svelte b/frontend/src/lib/components/graph/renderers/nodes/AssetNode.svelte
index 9a8eda2a93..d782540dc2 100644
--- a/frontend/src/lib/components/graph/renderers/nodes/AssetNode.svelte
+++ b/frontend/src/lib/components/graph/renderers/nodes/AssetNode.svelte
@@ -133,14 +133,16 @@
source: n.id ?? '',
target: n.parentId ?? '',
type: 'empty',
- data: { class: '!opacity-35 dark:!opacity-20' }
+ data: { class: '!opacity-35 dark:!opacity-20' },
+ selectable: false
}))
const outputAssetEdges: Edge[] = outputAssetNodes?.map((n) => ({
id: `${n.id}-edge`,
source: n.parentId ?? '',
target: n.id ?? '',
type: 'empty',
- data: { class: '!opacity-35 dark:!opacity-20' }
+ data: { class: '!opacity-35 dark:!opacity-20' },
+ selectable: false
}))
allAssetEdges.push(...(outputAssetEdges ?? []), ...(inputAssetEdges ?? []))
@@ -157,14 +159,16 @@
position: {
x: MAX_ASSET_ROW_WIDTH - ASSETS_OVERFLOWED_NODE_WIDTH - 14,
y: READ_ASSET_Y_OFFSET
- }
+ },
+ selectable: false
} satisfies Node & AssetsOverflowedN)
allAssetEdges.push({
id: `${node.id}-assets-overflowed-in-edge`,
source: `${node.id}-assets-overflowed-in`,
target: node.id,
type: 'empty',
- data: { class: '!opacity-35 dark:!opacity-20' }
+ data: { class: '!opacity-35 dark:!opacity-20' },
+ selectable: false
})
if (overflowedOutputAssets.length)
allAssetNodes.push({
@@ -176,14 +180,16 @@
position: {
x: MAX_ASSET_ROW_WIDTH - ASSETS_OVERFLOWED_NODE_WIDTH - 14,
y: WRITE_ASSET_Y_OFFSET
- }
+ },
+ selectable: false
} satisfies Node & AssetsOverflowedN)
allAssetEdges.push({
id: `${node.id}-assets-overflowed-out-edge`,
source: node.id,
target: `${node.id}-assets-overflowed-out`,
type: 'empty',
- data: { class: '!opacity-35 dark:!opacity-25' }
+ data: { class: '!opacity-35 dark:!opacity-25' },
+ selectable: false
})
}
diff --git a/frontend/src/lib/components/graph/renderers/nodes/BranchAllEndNode.svelte b/frontend/src/lib/components/graph/renderers/nodes/BranchAllEndNode.svelte
index 230d04d4b3..913bceb194 100644
--- a/frontend/src/lib/components/graph/renderers/nodes/BranchAllEndNode.svelte
+++ b/frontend/src/lib/components/graph/renderers/nodes/BranchAllEndNode.svelte
@@ -4,9 +4,10 @@
import type { BranchAllEndN } from '../../graphBuilder.svelte'
interface Props {
data: BranchAllEndN['data']
+ selected: boolean
}
- let { data }: Props = $props()
+ let { data, selected }: Props = $props()
@@ -15,7 +16,7 @@
label={'Collect result from all branches'}
id={data.id}
selectable={true}
- selected={false}
+ {selected}
on:select={(e) => {
data?.eventHandlers?.select(e.detail)
}}
diff --git a/frontend/src/lib/components/graph/renderers/nodes/BranchAllStart.svelte b/frontend/src/lib/components/graph/renderers/nodes/BranchAllStart.svelte
index b59805a871..9af02feea6 100644
--- a/frontend/src/lib/components/graph/renderers/nodes/BranchAllStart.svelte
+++ b/frontend/src/lib/components/graph/renderers/nodes/BranchAllStart.svelte
@@ -7,9 +7,10 @@
import type { BranchAllStartN } from '../../graphBuilder.svelte'
interface Props {
data: BranchAllStartN['data']
+ selected: boolean
}
- let { data }: Props = $props()
+ let { data, selected }: Props = $props()
@@ -17,7 +18,7 @@
{
setTimeout(() => data.eventHandlers.select(data.id))
}}
diff --git a/frontend/src/lib/components/graph/renderers/nodes/BranchOneStart.svelte b/frontend/src/lib/components/graph/renderers/nodes/BranchOneStart.svelte
index c5c259b4a8..b16f355bed 100644
--- a/frontend/src/lib/components/graph/renderers/nodes/BranchOneStart.svelte
+++ b/frontend/src/lib/components/graph/renderers/nodes/BranchOneStart.svelte
@@ -7,9 +7,10 @@
import type { BranchOneStartN } from '../../graphBuilder.svelte'
interface Props {
data: BranchOneStartN['data']
+ selected: boolean
}
- let { data }: Props = $props()
+ let { data, selected }: Props = $props()
@@ -18,7 +19,7 @@
label={data.label}
preLabel={data.preLabel}
selectable
- selected={false}
+ {selected}
on:select={() => {
setTimeout(() => data?.eventHandlers?.select(data.id))
}}
diff --git a/frontend/src/lib/components/graph/renderers/nodes/ForLoopEndNode.svelte b/frontend/src/lib/components/graph/renderers/nodes/ForLoopEndNode.svelte
index 379d75a6ba..bc10450b32 100644
--- a/frontend/src/lib/components/graph/renderers/nodes/ForLoopEndNode.svelte
+++ b/frontend/src/lib/components/graph/renderers/nodes/ForLoopEndNode.svelte
@@ -4,9 +4,10 @@
import type { ForLoopEndN } from '../../graphBuilder.svelte'
interface Props {
data: ForLoopEndN['data']
+ selected: boolean
}
- let { data }: Props = $props()
+ let { data, selected }: Props = $props()
@@ -15,7 +16,7 @@
{
@@ -26,7 +27,7 @@
{
setTimeout(() => data?.eventHandlers?.select(e.detail))
diff --git a/frontend/src/lib/components/graph/renderers/nodes/ForLoopStartNode.svelte b/frontend/src/lib/components/graph/renderers/nodes/ForLoopStartNode.svelte
index 70b900957e..c428faa6a2 100644
--- a/frontend/src/lib/components/graph/renderers/nodes/ForLoopStartNode.svelte
+++ b/frontend/src/lib/components/graph/renderers/nodes/ForLoopStartNode.svelte
@@ -7,9 +7,10 @@
import type { ForLoopStartN } from '../../graphBuilder.svelte'
interface Props {
data: ForLoopStartN['data']
+ selected: boolean
}
- let { data }: Props = $props()
+ let { data, selected }: Props = $props()
const propPickerContext = getContext('PropPickerContext')
const pickablePropertiesFiltered = propPickerContext?.pickablePropertiesFiltered
@@ -57,7 +58,7 @@
{
diff --git a/frontend/src/lib/components/graph/renderers/nodes/InputNode.svelte b/frontend/src/lib/components/graph/renderers/nodes/InputNode.svelte
index dc76c013b6..ef5023b2bb 100644
--- a/frontend/src/lib/components/graph/renderers/nodes/InputNode.svelte
+++ b/frontend/src/lib/components/graph/renderers/nodes/InputNode.svelte
@@ -11,15 +11,13 @@
import type { FlowEditorContext } from '$lib/components/flows/types'
import { MessageSquare, DiffIcon } from 'lucide-svelte'
import { Button } from '$lib/components/common'
- import { getGraphContext } from '../../graphContext'
interface Props {
data: InputN['data']
+ selected: boolean
}
- let { data }: Props = $props()
-
- const { selectionManager } = getGraphContext()
+ let { data, selected }: Props = $props()
const { previewArgs, flowStore } =
getContext('FlowEditorContext') || {}
@@ -81,7 +79,7 @@
hideId={true}
label={inputLabel}
selectable
- selected={selectionManager?.isNodeSelected('Input')}
+ {selected}
on:insert={(e) => {
setTimeout(() => data?.eventHandlers?.insert(e.detail))
}}
diff --git a/frontend/src/lib/components/graph/renderers/nodes/ModuleNode.svelte b/frontend/src/lib/components/graph/renderers/nodes/ModuleNode.svelte
index 99086cb9b2..76f0525877 100644
--- a/frontend/src/lib/components/graph/renderers/nodes/ModuleNode.svelte
+++ b/frontend/src/lib/components/graph/renderers/nodes/ModuleNode.svelte
@@ -10,9 +10,10 @@
interface Props {
data: ModuleN['data']
+ selected: boolean
}
- let { data }: Props = $props()
+ let { data, selected }: Props = $props()
// Get NoteEditor context for group note creation
const noteEditorContext = getNoteEditorContext()
@@ -71,6 +72,7 @@
duration_ms={state?.duration_ms}
retries={state?.retries}
{flowJobs}
+ {selected}
on:delete={(e) => {
data.eventHandlers.delete(e.detail, '')
}}
diff --git a/frontend/src/lib/components/graph/renderers/nodes/ResultNode.svelte b/frontend/src/lib/components/graph/renderers/nodes/ResultNode.svelte
index b12c3369dc..e35bb0bafd 100644
--- a/frontend/src/lib/components/graph/renderers/nodes/ResultNode.svelte
+++ b/frontend/src/lib/components/graph/renderers/nodes/ResultNode.svelte
@@ -2,15 +2,13 @@
import VirtualItem from '$lib/components/flows/map/VirtualItem.svelte'
import NodeWrapper from './NodeWrapper.svelte'
import type { ResultN } from '../../graphBuilder.svelte'
- import { getGraphContext } from '../../graphContext'
interface Props {
data: ResultN['data']
+ selected: boolean
}
- let { data }: Props = $props()
-
- const { selectionManager } = getGraphContext()
+ let { data, selected }: Props = $props()
@@ -19,7 +17,7 @@
id={'Result'}
label={'Result'}
selectable={true}
- selected={selectionManager?.getSelectedId() === 'Result'}
+ {selected}
hideId={true}
on:select={(e) => {
setTimeout(() => data?.eventHandlers?.select(e.detail))
diff --git a/frontend/src/lib/components/graph/renderers/nodes/TriggersNode.svelte b/frontend/src/lib/components/graph/renderers/nodes/TriggersNode.svelte
index 58f95d4b33..70d91fba14 100644
--- a/frontend/src/lib/components/graph/renderers/nodes/TriggersNode.svelte
+++ b/frontend/src/lib/components/graph/renderers/nodes/TriggersNode.svelte
@@ -26,9 +26,10 @@
disableAi: boolean
simplifiableFlow: SimplifiableFlow
}
+ selected: boolean
}
- let { data }: Props = $props()
+ let { data, selected }: Props = $props()
const { selectionManager } = getGraphContext()
@@ -48,9 +49,7 @@
}
}
- let colorClasses = $derived(
- getNodeColorClasses('_VirtualItem', selectionManager?.isNodeSelected('Trigger'))
- )
+ let colorClasses = $derived(getNodeColorClasses('_VirtualItem', selected))
diff --git a/frontend/src/lib/components/graph/renderers/nodes/branchOneEndNode.svelte b/frontend/src/lib/components/graph/renderers/nodes/branchOneEndNode.svelte
index 88a4985b8c..e9eaeb1316 100644
--- a/frontend/src/lib/components/graph/renderers/nodes/branchOneEndNode.svelte
+++ b/frontend/src/lib/components/graph/renderers/nodes/branchOneEndNode.svelte
@@ -4,9 +4,10 @@
import type { BranchOneEndN } from '../../graphBuilder.svelte'
interface Props {
data: BranchOneEndN['data']
+ selected: boolean
}
- let { data }: Props = $props()
+ let { data, selected }: Props = $props()
@@ -15,7 +16,7 @@
label={'Collect result from chosen branch'}
id={data.id}
selectable={true}
- selected={false}
+ {selected}
on:select={(e) => {
setTimeout(() => data?.eventHandlers?.select(e.detail))
}}
diff --git a/frontend/src/lib/components/graph/selectionUtils.svelte.ts b/frontend/src/lib/components/graph/selectionUtils.svelte.ts
index e1e1ba512a..9a281d6bec 100644
--- a/frontend/src/lib/components/graph/selectionUtils.svelte.ts
+++ b/frontend/src/lib/components/graph/selectionUtils.svelte.ts
@@ -1,20 +1,69 @@
import type { Node } from '@xyflow/svelte'
+const MULTIPLE_SELECTION_ID = 'multiple-selection'
+const SETTINGS_ID = 'settings'
+
+/**
+ *
+ * @param node - The node to get the module id from
+ * @returns The module id from the node
+ */
+function getModuleIdFromNode(node: Node): string | undefined {
+ if (node.type === 'branchOneEnd') {
+ return /^(.*)-end$/.exec(node.id)?.[1]
+ } else if (node.type === 'branchAllEnd') {
+ return /^(.*)-end$/.exec(node.id)?.[1]
+ } else if (node.type === 'forLoopEnd') {
+ return /^(.*)-end$/.exec(node.id)?.[1]
+ } else if (node.type === 'whileLoopEnd') {
+ return /^(.*)-end$/.exec(node.id)?.[1]
+ } else if (node.type === 'subflowBound') {
+ return /^(.*)-end$/.exec(node.id)?.[1]
+ }
+ return undefined
+}
+
export class SelectionManager {
- #selectedIds = $state([])
#selectionMode = $state<'normal' | 'rect-select'>('normal')
+ #selectedNodesInGraph = $state([])
+ #manualSelectedId = $state(undefined)
constructor() {}
selectId(id: string) {
- if (this.#selectedIds.length === 1 && this.#selectedIds[0] === id) {
- return
- }
- this.#selectedIds = [id]
+ // If not in the graph, set the selected id outside the graph
+ this.#manualSelectedId = id
}
getSelectedId(): string {
- return this.#selectedIds[0] || 'settings'
+ if (this.#manualSelectedId !== undefined) {
+ return this.#manualSelectedId
+ }
+ if (this.#selectedNodesInGraph.length === 1) {
+ const selectedNode = this.#selectedNodesInGraph[0]
+ const moduleId = getModuleIdFromNode(selectedNode)
+ if (moduleId) {
+ return moduleId
+ }
+ return selectedNode.id
+ } else if (this.#selectedNodesInGraph.length > 1) {
+ return MULTIPLE_SELECTION_ID
+ } else {
+ return SETTINGS_ID
+ }
+ }
+
+ get selectedNodesInGraph() {
+ return this.#selectedNodesInGraph
+ }
+
+ get manualSelectedId() {
+ return this.#manualSelectedId
+ }
+
+ set selectedNodesInGraph(nodes: Node[]) {
+ this.#manualSelectedId = undefined
+ this.#selectedNodesInGraph = nodes
}
get mode() {
@@ -25,56 +74,9 @@ export class SelectionManager {
this.#selectionMode = mode
}
- get selectedIds() {
- if (this.#selectedIds.length === 0) {
- return ['settings']
- }
- return [...this.#selectedIds]
- }
-
- // Select nodes with optional hierarchical selection
- selectNodes(nodeIds: string[], addToExisting = false) {
- // Guard against empty nodeIds or uninitialized state
- if (!nodeIds || nodeIds.length === 0) {
- if (!addToExisting) {
- this.clearSelection()
- }
- return
- }
-
- const newSelection = addToExisting ? [...this.#selectedIds, ...nodeIds] : nodeIds
-
- // If the new selection is the same as the current selection, do nothing
- if (JSON.stringify(newSelection) === JSON.stringify($state.snapshot(this.#selectedIds))) {
- return
- }
-
- this.#selectedIds = newSelection
- }
-
// Clear all selections
clearSelection() {
- this.#selectedIds = ['settings']
- }
-
- // Check if a node is selected
- isNodeSelected(nodeId: string): boolean {
- return this.#selectedIds.includes(nodeId)
- }
-
- // Get selected node count
- get selectedCount(): number {
- return this.#selectedIds.length
- }
-
- // Check if multiple nodes are selected
- get hasMultipleSelection(): boolean {
- return this.selectedCount > 1
- }
-
- // Get all selected node IDs
- get selectedNodeIds(): string[] {
- return [...this.#selectedIds]
+ this.#manualSelectedId = undefined
}
// Handle keyboard shortcuts
@@ -82,13 +84,6 @@ export class SelectionManager {
if (event.key === 'Escape') {
// Escape key clears selection regardless of mode
this.clearSelection()
- } else if ((event.ctrlKey || event.metaKey) && event.key === 'a') {
- event.preventDefault()
- // Select all visible nodes (exclude note nodes)
- if (nodes) {
- const allNodeIds = nodes.filter((node) => node.type !== 'note').map((node) => node.id)
- this.selectNodes(allNodeIds)
- }
}
}
}