mirror of
https://github.com/windmill-labs/windmill.git
synced 2026-08-25 08:00:59 +00:00
Improve selection of nodes
This commit is contained in:
@@ -87,7 +87,7 @@
|
||||
})
|
||||
</script>
|
||||
|
||||
{#if selectionManager && selectionManager.selectedIds.length > 1}
|
||||
{#if selectedId === 'multiple-selection'}
|
||||
<FlowSelectionPanel {selectionManager} {noEditor} />
|
||||
{:else if selectedId?.startsWith('settings')}
|
||||
<FlowSettings {enableAi} {noEditor} />
|
||||
|
||||
@@ -13,10 +13,12 @@
|
||||
|
||||
const noteEditorContext = getNoteEditorContext()
|
||||
|
||||
const selectedNodes = $derived(selectionManager.selectedNodesInGraph.map((node) => node.id))
|
||||
|
||||
function addGroupNote() {
|
||||
if (selectionManager.selectedIds.length > 0 && noteEditorContext?.noteEditor) {
|
||||
if (selectedNodes.length > 0 && noteEditorContext?.noteEditor) {
|
||||
// Create the group note
|
||||
noteEditorContext.noteEditor.createGroupNote(selectionManager.selectedIds)
|
||||
noteEditorContext.noteEditor.createGroupNote(selectedNodes)
|
||||
}
|
||||
}
|
||||
</script>
|
||||
@@ -25,16 +27,16 @@
|
||||
{#snippet action()}
|
||||
<Button
|
||||
onClick={addGroupNote}
|
||||
disabled={!noteEditorContext?.noteEditor || selectionManager.selectedIds.length === 0}
|
||||
disabled={!noteEditorContext?.noteEditor || selectedNodes.length === 0}
|
||||
startIcon={{ icon: StickyNote }}
|
||||
>
|
||||
Create group note
|
||||
</Button>
|
||||
{/snippet}
|
||||
<div class="px-4">
|
||||
<p class="text-xs text-secondary mb-4">{selectionManager.selectedIds.length} nodes selected</p>
|
||||
<p class="text-xs text-secondary mb-4">{selectedNodes.length} nodes selected</p>
|
||||
<div class="space-y-2 mb-4">
|
||||
{#each selectionManager.selectedIds as nodeId}
|
||||
{#each selectedNodes as nodeId}
|
||||
<div class="text-sm px-2 py-1 bg-surface rounded border">
|
||||
{nodeId}
|
||||
</div>
|
||||
|
||||
@@ -16,7 +16,6 @@
|
||||
import { twMerge } from 'tailwind-merge'
|
||||
import type { FlowNodeState } from '$lib/components/graph'
|
||||
import type { AIModuleAction } from '$lib/components/copilot/chat/flow/core'
|
||||
import { getGraphContext } from '$lib/components/graph/graphContext'
|
||||
|
||||
interface Props {
|
||||
moduleId: string
|
||||
@@ -49,6 +48,7 @@
|
||||
flowJob?: Job | undefined
|
||||
isOwner?: boolean
|
||||
maximizeSubflow?: () => void
|
||||
selected: boolean
|
||||
}
|
||||
|
||||
let {
|
||||
@@ -71,11 +71,10 @@
|
||||
onEditInput,
|
||||
flowJob,
|
||||
isOwner = false,
|
||||
maximizeSubflow
|
||||
maximizeSubflow,
|
||||
selected = false
|
||||
}: Props = $props()
|
||||
|
||||
const { selectionManager } = getGraphContext()
|
||||
|
||||
const { flowStore } = getContext<FlowEditorContext | undefined>('FlowEditorContext') || {}
|
||||
|
||||
const dispatch = createEventDispatcher<{
|
||||
@@ -86,9 +85,7 @@
|
||||
}>()
|
||||
|
||||
let itemProps = $derived({
|
||||
selected:
|
||||
selectionManager?.getSelectedId() === mod.id ||
|
||||
(selectionManager && selectionManager.selectedIds.includes(mod.id)),
|
||||
selected,
|
||||
retry: mod.retry?.constant != undefined || mod.retry?.exponential != undefined,
|
||||
earlyStop: mod.stop_after_if != undefined || mod.stop_after_all_iters_if != undefined,
|
||||
skip: Boolean(mod.skip_if),
|
||||
|
||||
@@ -397,7 +397,7 @@
|
||||
},
|
||||
select: (modId) => {
|
||||
if (!notSelectable) {
|
||||
selectionManager.selectId(modId)
|
||||
console.log('dbg select', modId)
|
||||
onSelect?.(modId)
|
||||
}
|
||||
},
|
||||
@@ -809,6 +809,23 @@
|
||||
}
|
||||
|
||||
const modifierKey = isMac() ? 'Meta' : 'Control'
|
||||
|
||||
$inspect(
|
||||
'dbg selectionManager',
|
||||
selectionManager.selectedNodesInGraph,
|
||||
selectionManager.manualSelectedId
|
||||
)
|
||||
|
||||
$effect(() => {
|
||||
if (selectionManager.manualSelectedId) {
|
||||
untrack(() => {
|
||||
nodes = nodes.map((node) => ({
|
||||
...node,
|
||||
selected: node.id === selectionManager.manualSelectedId
|
||||
}))
|
||||
})
|
||||
}
|
||||
})
|
||||
</script>
|
||||
|
||||
{#if insertable}
|
||||
@@ -885,7 +902,7 @@
|
||||
zoomOnDoubleClick={false}
|
||||
elevateNodesOnSelect={false}
|
||||
{proOptions}
|
||||
multiSelectionKey={modifierKey}
|
||||
multiSelectionKey={'Shift'}
|
||||
nodesDraggable={false}
|
||||
--background-color={false}
|
||||
>
|
||||
@@ -896,16 +913,8 @@
|
||||
{/if}
|
||||
|
||||
{#if multiSelectEnabled}
|
||||
<NodeContextMenu
|
||||
selectedNodeIds={selectionManager.selectedIds.filter(
|
||||
(id) =>
|
||||
!id.startsWith('Settings') && !id.startsWith('Trigger') && !id.startsWith('Result')
|
||||
)}
|
||||
>
|
||||
<SelectionBoundingBox
|
||||
selectedNodes={selectionManager.selectedIds}
|
||||
allNodes={nodesWithOffset}
|
||||
/>
|
||||
<NodeContextMenu selectedNodeIds={[]}>
|
||||
<SelectionBoundingBox allNodes={nodesWithOffset} />
|
||||
</NodeContextMenu>
|
||||
{/if}
|
||||
|
||||
|
||||
@@ -9,11 +9,10 @@
|
||||
import { tick } from 'svelte'
|
||||
|
||||
interface Props {
|
||||
selectedNodes: string[]
|
||||
allNodes: Node[]
|
||||
}
|
||||
|
||||
let { selectedNodes, allNodes }: Props = $props()
|
||||
let { allNodes }: Props = $props()
|
||||
|
||||
const { flowToScreenPosition } = useSvelteFlow()
|
||||
|
||||
@@ -22,6 +21,8 @@
|
||||
// Get Graph context for clearFlowSelection function
|
||||
const graphContext = getGraphContext()
|
||||
|
||||
const selectedNodes = $derived(allNodes.filter((node) => node.selected).map((node) => node.id))
|
||||
|
||||
function handleAddGroupNote() {
|
||||
if (selectedNodes.length > 0 && noteEditorContext?.noteEditor && graphContext) {
|
||||
// Create the group note first
|
||||
@@ -85,7 +86,7 @@
|
||||
{#if bounds() && selectedNodes.length > 1}
|
||||
{@const currentBounds = bounds()!}
|
||||
<div
|
||||
class={'absolute cursor-pointer bg-surface-selected/40 rounded-md'}
|
||||
class={'absolute cursor-pointer bg-surface-selected/40 rounded-md pointer-events-none'}
|
||||
style="
|
||||
left: {currentBounds.x}px;
|
||||
top: {currentBounds.y}px;
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
<script lang="ts">
|
||||
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
|
||||
})
|
||||
</script>
|
||||
|
||||
|
||||
@@ -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
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
@@ -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()
|
||||
</script>
|
||||
|
||||
<NodeWrapper offset={data.offset} enableSourceHandle enableTargetHandle>
|
||||
@@ -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)
|
||||
}}
|
||||
|
||||
@@ -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()
|
||||
</script>
|
||||
|
||||
<NodeWrapper offset={data.offset}>
|
||||
@@ -17,7 +18,7 @@
|
||||
<VirtualItem
|
||||
label={data.label}
|
||||
selectable
|
||||
selected={false}
|
||||
{selected}
|
||||
on:select={() => {
|
||||
setTimeout(() => data.eventHandlers.select(data.id))
|
||||
}}
|
||||
|
||||
@@ -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()
|
||||
</script>
|
||||
|
||||
<NodeWrapper offset={data.offset}>
|
||||
@@ -18,7 +19,7 @@
|
||||
label={data.label}
|
||||
preLabel={data.preLabel}
|
||||
selectable
|
||||
selected={false}
|
||||
{selected}
|
||||
on:select={() => {
|
||||
setTimeout(() => data?.eventHandlers?.select(data.id))
|
||||
}}
|
||||
|
||||
@@ -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()
|
||||
</script>
|
||||
|
||||
<NodeWrapper offset={data.offset}>
|
||||
@@ -15,7 +16,7 @@
|
||||
<VirtualItem
|
||||
label={'Each event is processed'}
|
||||
selectable={false}
|
||||
selected={false}
|
||||
{selected}
|
||||
id={data.id}
|
||||
hideId
|
||||
on:select={(e) => {
|
||||
@@ -26,7 +27,7 @@
|
||||
<VirtualItem
|
||||
label={'Collect result of each iteration'}
|
||||
selectable={true}
|
||||
selected={false}
|
||||
{selected}
|
||||
id={data.id}
|
||||
on:select={(e) => {
|
||||
setTimeout(() => data?.eventHandlers?.select(e.detail))
|
||||
|
||||
@@ -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>('PropPickerContext')
|
||||
const pickablePropertiesFiltered = propPickerContext?.pickablePropertiesFiltered
|
||||
@@ -57,7 +58,7 @@
|
||||
<VirtualItem
|
||||
label={data.simplifiedTriggerView ? 'For each new event' : 'Do one iteration'}
|
||||
selectable={false}
|
||||
selected={false}
|
||||
{selected}
|
||||
id={data.id}
|
||||
hideId
|
||||
on:select={(e) => {
|
||||
|
||||
@@ -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 | undefined>('FlowEditorContext') || {}
|
||||
@@ -81,7 +79,7 @@
|
||||
hideId={true}
|
||||
label={inputLabel}
|
||||
selectable
|
||||
selected={selectionManager?.isNodeSelected('Input')}
|
||||
{selected}
|
||||
on:insert={(e) => {
|
||||
setTimeout(() => data?.eventHandlers?.insert(e.detail))
|
||||
}}
|
||||
|
||||
@@ -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, '')
|
||||
}}
|
||||
|
||||
@@ -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()
|
||||
</script>
|
||||
|
||||
<NodeWrapper enableSourceHandle={false}>
|
||||
@@ -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))
|
||||
|
||||
@@ -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))
|
||||
</script>
|
||||
|
||||
<NodeWrapper>
|
||||
|
||||
@@ -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()
|
||||
</script>
|
||||
|
||||
<NodeWrapper offset={data.offset}>
|
||||
@@ -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))
|
||||
}}
|
||||
|
||||
@@ -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<string[]>([])
|
||||
#selectionMode = $state<'normal' | 'rect-select'>('normal')
|
||||
#selectedNodesInGraph = $state<Node[]>([])
|
||||
#manualSelectedId = $state<string | undefined>(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)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user