clean selection manager

This commit is contained in:
Guilhem
2025-11-11 16:19:32 +01:00
parent 2e8a739b14
commit 8e4985eddb
8 changed files with 81 additions and 74 deletions
+5 -3
View File
@@ -476,7 +476,7 @@
let ids = dfs(flowStore.val.value.modules ?? [], (m) => m.id)
flowStateStore.val = Object.fromEntries(ids.map((k) => [k, {}]))
} catch (e) {}
inferModuleArgs(selectionManager.getSelectedId()!)
inferModuleArgs(selectedId!)
}
} catch (e) {
console.error('issue setting new flowstore', e)
@@ -620,7 +620,7 @@
flowStore.val && untrack(() => updateFlow(flowStore.val))
})
$effect(() => {
selectionManager.getSelectedId() && untrack(() => inferModuleArgs(selectionManager.getSelectedId()!))
selectedId && untrack(() => inferModuleArgs(selectedId!))
})
let localModuleStates: Record<string, GraphModuleState> = $state({})
@@ -642,7 +642,7 @@
job.success &&
flowPreviewButtons?.getPreviewMode() === 'whole'
) {
if (flowModuleSchemaMap?.isNodeVisible('result') && selectionManager.getSelectedId() !== 'Result') {
if (flowModuleSchemaMap?.isNodeVisible('result') && selectedId !== 'Result') {
outputPickerOpenFns['Result']?.()
}
} else {
@@ -667,6 +667,8 @@
}
const flowHasChanged = $derived(flowPreviewContent?.flowHasChanged())
const selectedId = $derived(selectionManager.getSelectedId())
</script>
<svelte:window onkeydown={onKeyDown} />
@@ -564,7 +564,7 @@
encodeState({
flow: flowStore.val,
path: $pathStore,
selectedId: selectionManager.getSelectedId(),
selectedId: selectedIdStore,
draft_triggers: triggersState.getDraftTriggersSnapshot(),
selected_trigger: triggersState.getSelectedTriggerSnapshot(),
loadedFromHistory: {
@@ -580,6 +580,7 @@
}
const selectionManager = new SelectionManager()
const selectedIdStore = $derived(selectionManager.getSelectedId())
// Initialize with selected id if provided
if (selectedId) {
selectionManager.selectId(selectedId)
@@ -588,7 +589,7 @@
}
export function getSelectedId() {
return selectionManager.getSelectedId()
return selectedIdStore
}
const previewArgsStore = $state({ val: initialArgs })
@@ -721,7 +722,7 @@
case 'ArrowDown': {
if (!$insertButtonOpen && !flowPreviewButtons?.getPreviewOpen()) {
let ids = generateIds()
let idx = ids.indexOf(selectionManager.getSelectedId()!)
let idx = ids.indexOf(selectedIdStore!)
if (idx > -1 && idx < ids.length - 1) {
selectionManager.selectId(ids[idx + 1])
event.preventDefault()
@@ -732,7 +733,7 @@
case 'ArrowUp': {
if (!$insertButtonOpen && !flowPreviewButtons?.getPreviewOpen()) {
let ids = generateIds()
let idx = ids.indexOf(selectionManager.getSelectedId()!)
let idx = ids.indexOf(selectedIdStore!)
if (idx > 0 && idx < ids.length) {
selectionManager.selectId(ids[idx - 1])
event.preventDefault()
@@ -881,7 +882,7 @@
setContext('customUi', customUi)
})
$effect.pre(() => {
if (flowStore.val || selectionManager.getSelectedId()) {
if (flowStore.val || selectedIdStore) {
readFieldsRecursively(flowStore.val)
untrack(() => saveSessionDraft())
}
@@ -945,7 +946,7 @@
job.success &&
flowPreviewButtons?.getPreviewMode() === 'whole'
) {
if (flowEditor?.isNodeVisible('result') && selectionManager.getSelectedId() !== 'Result') {
if (flowEditor?.isNodeVisible('result') && selectedIdStore !== 'Result') {
outputPickerOpenFns['Result']?.()
}
} else {
@@ -27,6 +27,7 @@
const { flowStore, flowStateStore, selectionManager, currentEditor } =
getContext<FlowEditorContext>('FlowEditorContext')
const selectedId = $derived(selectionManager.getSelectedId())
const { exprsToSet } = getContext<FlowCopilotContext | undefined>('FlowCopilotContext') ?? {}
@@ -84,7 +85,7 @@
const flow = $state.snapshot(flowStore).val
return {
flow,
selectedId: selectionManager.getSelectedId()!
selectedId: selectedId!
}
},
// flow apply/reject
@@ -382,7 +383,7 @@
value: match[2].trim()
}))
if (id === selectionManager.getSelectedId()!) {
if (id === selectedId!) {
exprsToSet?.set({})
const argsToUpdate = {}
for (const { input, value } of parsedInputs) {
@@ -611,7 +612,7 @@
$effect(() => {
const cleanup = aiChatManager.listenForSelectedIdChanges(
selectionManager.getSelectedId(),
selectedId,
flowStore.val,
flowStateStore.val,
$currentEditor
@@ -626,7 +627,6 @@
// Automatically show revert review when selecting a rawscript module with pending changes
$effect(() => {
const selectedId = selectionManager.getSelectedId()
if (
$currentEditor?.type === 'script' &&
selectedId &&
@@ -66,6 +66,8 @@
flowInputEditorState
} = getContext<FlowEditorContext>('FlowEditorContext')
const selectedId = $derived(selectionManager.getSelectedId())
const { showCaptureHint, triggersState, triggersCount } =
getContext<TriggerContext>('TriggerContext')
function checkDup(modules: FlowModule[]): string | undefined {
@@ -96,9 +98,9 @@
{/each}
</div>
</div>
{:else if selectionManager.getSelectedId()?.startsWith('settings')}
{:else if selectedId?.startsWith('settings')}
<FlowSettings {enableAi} {noEditor} />
{:else if selectionManager.getSelectedId() === 'Input'}
{:else if selectedId === 'Input'}
<FlowInput
{noEditor}
disabled={disabledFlowInputs}
@@ -111,15 +113,15 @@
{onTestFlow}
{previewOpen}
/>
{:else if selectionManager.getSelectedId() === 'Result'}
{:else if selectedId === 'Result'}
<FlowResult {noEditor} {job} {isOwner} {suspendStatus} {onOpenDetails} />
{:else if selectionManager.getSelectedId() === 'constants'}
{:else if selectedId === 'constants'}
<FlowConstants {noEditor} />
{:else if selectionManager.getSelectedId() === 'failure'}
{:else if selectedId === 'failure'}
<FlowFailureModule {noEditor} savedModule={savedFlow?.value.failure_module} />
{:else if selectionManager.getSelectedId() === 'preprocessor'}
{:else if selectedId === 'preprocessor'}
<FlowPreprocessorModule {noEditor} savedModule={savedFlow?.value.preprocessor_module} />
{:else if selectionManager.getSelectedId() === 'Trigger'}
{:else if selectedId === 'Trigger'}
<TriggersEditor
on:applyArgs
on:addPreprocessor={async () => {
@@ -153,7 +155,7 @@
schema={flowStore.val.schema}
{onDeployTrigger}
/>
{:else if selectionManager.getSelectedId()?.startsWith('subflow:')}
{:else if selectedId?.startsWith('subflow:')}
<div class="p-4"
>Selected step is witin an expanded subflow and is not directly editable in the flow editor</div
>
@@ -162,7 +164,7 @@
{#if dup}
<div class="text-red-600 text-xl p-2">There are duplicate modules in the flow at id: {dup}</div>
{:else}
{#key selectionManager.getSelectedId()}
{#key selectedId}
{#each flowStore.val.value.modules as flowModule, index (flowModule.id ?? index)}
<FlowModuleWrapper
{noEditor}
@@ -70,6 +70,8 @@
executionCount
} = getContext<FlowEditorContext>('FlowEditorContext')
const selectedId = $derived(selectionManager.getSelectedId())
interface Props {
flowModule: FlowModule
failureModule?: boolean
@@ -215,7 +217,7 @@
let stepHistoryLoader = getStepHistoryLoaderContext()
function onSelectedIdChange() {
if (!flowStateStore?.val?.[selectionManager.getSelectedId()!]?.schema && flowModule) {
if (!flowStateStore?.val?.[selectedId!]?.schema && flowModule) {
reload(flowModule)
}
}
@@ -252,7 +254,7 @@
)
$effect.pre(() => {
selectionManager.getSelectedId() && untrack(() => onSelectedIdChange())
selectedId && untrack(() => onSelectedIdChange())
})
let parentLoop = $derived(
flowStore.val && flowModule ? checkIfParentLoop(flowStore.val, flowModule.id) : undefined
@@ -404,7 +406,7 @@
on:createScriptFromInlineScript={async () => {
const [module, state] = await createScriptFromInlineScript(
flowModule,
selectionManager.getSelectedId()!,
selectedId!,
flowStateStore.val[flowModule.id].schema,
$pathStore
)
@@ -468,7 +470,7 @@
automaticLayout={true}
cmdEnterAction={async () => {
selected = 'test'
if (selectionManager.getSelectedId() == flowModule.id) {
if (selectedId == flowModule.id) {
if (flowModule.value.type === 'rawscript' && editor) {
flowModule.value.content = editor.getCode()
}
@@ -578,8 +580,7 @@
class="px-2 xl:px-4"
bind:this={inputTransformSchemaForm}
pickableProperties={stepPropPicker.pickableProperties}
schema={flowStateStore.val[selectionManager.getSelectedId()!]?.schema ??
{}}
schema={flowStateStore.val[selectedId!]?.schema ?? {}}
previousModuleId={previousModule?.id}
bind:args={
() => {
@@ -610,7 +611,7 @@
bind:this={modulePreview}
mod={flowModule}
{noEditor}
schema={flowStateStore.val[selectionManager.getSelectedId()!]?.schema ?? {}}
schema={flowStateStore.val[selectedId!]?.schema ?? {}}
bind:testJob
bind:testIsLoading
bind:scriptProgress
@@ -624,7 +625,7 @@
active={flowModule.retry !== undefined}
label="Retries"
/>
{#if !selectionManager.getSelectedId()?.includes('failure')}
{#if !selectedId?.includes('failure')}
<Tab value="runtime" label="Runtime" />
<Tab value="cache" active={Boolean(flowModule.cache_ttl)} label="Cache" />
<Tab
@@ -24,6 +24,7 @@
import AgentToolWrapper from './AgentToolWrapper.svelte'
const { selectionManager, flowStateStore } = getContext<FlowEditorContext>('FlowEditorContext')
const selectedId = $derived(selectionManager.getSelectedId())
const { triggersState, triggersCount } = getContext<TriggerContext>('TriggerContext')
@@ -113,7 +114,7 @@
}
</script>
{#if flowModule.id === selectionManager.getSelectedId()!}
{#if flowModule.id === selectedId!}
{#if flowModule.value.type === 'forloopflow'}
<FlowLoop {noEditor} bind:mod={flowModule} {parentModule} {previousModule} {enableAi} />
{:else if flowModule.value.type === 'whileloopflow'}
@@ -123,13 +124,13 @@
{:else if flowModule.value.type === 'branchall'}
<FlowBranchesAllWrapper {noEditor} {previousModule} {parentModule} bind:flowModule />
{:else if flowModule.value.type === 'identity'}
{#if selectionManager.getSelectedId() == 'failure'}
{#if selectedId == 'failure'}
<div class="p-4">
<Alert type="info" title="Error handlers are triggered upon non recovered errors">
If defined, the error handler will take the error as input.
</Alert>
</div>
{:else if selectionManager.getSelectedId() == 'preprocessor'}
{:else if selectedId == 'preprocessor'}
<div class="p-4">
<Alert
type="info"
@@ -157,8 +158,8 @@
summary={flowModule.summary}
shouldDisableTriggerScripts={parentModule !== undefined ||
previousModule !== undefined ||
selectionManager.getSelectedId() == 'failure' ||
selectionManager.getSelectedId() == 'preprocessor'}
selectedId == 'failure' ||
selectedId == 'preprocessor'}
on:pick={async ({ detail }) => {
const { path, summary, kind, hash } = detail
createModuleFromScript(path, summary, kind, hash)
@@ -187,8 +188,8 @@
flowModule = module
flowStateStore.val[module.id] = state
}}
failureModule={selectionManager.getSelectedId() === 'failure'}
preprocessorModule={selectionManager.getSelectedId() === 'preprocessor'}
failureModule={selectedId === 'failure'}
preprocessorModule={selectedId === 'preprocessor'}
/>
{/if}
{:else if flowModule.value.type === 'rawscript' || flowModule.value.type === 'script' || flowModule.value.type === 'flow' || flowModule.value.type === 'aiagent'}
@@ -197,8 +198,8 @@
bind:flowModule
{parentModule}
{previousModule}
failureModule={selectionManager.getSelectedId() === 'failure'}
preprocessorModule={selectionManager.getSelectedId() === 'preprocessor'}
failureModule={selectedId === 'failure'}
preprocessorModule={selectedId === 'preprocessor'}
{scriptKind}
{scriptTemplate}
{enableAi}
@@ -225,7 +226,7 @@
/>
{/each}
{:else if flowModule.value.type === 'branchone'}
{#if selectionManager.getSelectedId() === `${flowModule?.id}-branch-default`}
{#if selectedId === `${flowModule?.id}-branch-default`}
<div class="p-2">
<h3 class="mb-4">Default branch</h3>
Nothing to configure, this is the default branch if none of the predicates are met.
@@ -247,7 +248,7 @@
{/each}
{/if}
{#each flowModule.value.branches as branch, branchIndex (branchIndex)}
{#if selectionManager.getSelectedId() === `${flowModule?.id}-branch-${branchIndex}`}
{#if selectedId === `${flowModule?.id}-branch-${branchIndex}`}
<FlowBranchOneWrapper
{noEditor}
bind:branch={flowModule.value.branches[branchIndex]}
@@ -274,7 +275,7 @@
{/each}
{:else if flowModule.value.type === 'branchall'}
{#each flowModule.value.branches as branch, branchIndex (branchIndex)}
{#if selectionManager.getSelectedId() === `${flowModule?.id}-branch-${branchIndex}`}
{#if selectedId === `${flowModule?.id}-branch-${branchIndex}`}
<FlowBranchAllWrapper {noEditor} bind:branch={flowModule.value.branches[branchIndex]} />
{:else}
{#each branch.modules as _, index}
@@ -295,7 +296,7 @@
{/each}
{:else if flowModule.value.type === 'aiagent'}
{#each flowModule.value.tools as tool, toolIndex (toolIndex)}
{#if selectionManager.getSelectedId() === tool.id}
{#if selectedId === tool.id}
<AgentToolWrapper
{noEditor}
bind:tool={flowModule.value.tools[toolIndex]}
@@ -33,6 +33,7 @@
}: Props = $props()
const { selectionManager, flowStore } = getContext<FlowEditorContext>('FlowEditorContext')
const selectedId = $derived(selectionManager.getSelectedId())
</script>
<div class="flex flex-row gap-2 p-1 rounded-md bg-surface">
@@ -41,7 +42,7 @@
unifiedSize="sm"
wrapperClasses="min-w-36"
startIcon={{ icon: Settings }}
selected={selectionManager.getSelectedId()?.startsWith('settings')}
selected={selectedId?.startsWith('settings')}
variant="default"
title="Settings"
onClick={() => selectionManager.selectId('settings')}
@@ -64,7 +65,7 @@
wrapperClasses="h-full"
unifiedSize="sm"
startIcon={{ icon: DollarSign }}
selected={selectionManager.getSelectedId() === 'constants'}
selected={selectedId === 'constants'}
variant="default"
iconOnly
onClick={() => selectionManager.selectId('constants')}
@@ -179,7 +179,7 @@
testModuleStates = undefined,
moduleActions = undefined,
inputSchemaModified = undefined,
selectionManager = undefined,
selectionManager: selectionManagerProp = undefined,
path = undefined,
newFlow = false,
insertable = false,
@@ -234,10 +234,11 @@
let noteTextHeights = $state<Record<string, number>>({})
// Selection manager - create one if not provided
let actualSelectionManager = selectionManager || new SelectionManager()
let selectionManager = selectionManagerProp || new SelectionManager()
const selectedId = $derived(selectionManager.getSelectedId())
setGraphContext({
selectionManager: actualSelectionManager,
selectionManager: selectionManager,
useDataflow,
showAssets
})
@@ -365,7 +366,7 @@
let eventHandler = {
deleteBranch: (detail, label) => {
actualSelectionManager.selectId(label)
selectionManager.selectId(label)
onDeleteBranch?.(detail)
},
insert: (detail) => {
@@ -375,7 +376,7 @@
if (!notSelectable) {
// TODO: Handle Ctrl/Cmd and Shift modifiers when node-level click events are available
// For now, normal click behavior
actualSelectionManager.selectId(modId)
selectionManager.selectId(modId)
onSelect?.(modId)
}
},
@@ -452,7 +453,7 @@
// Keyboard event handling
function handleKeyDown(event: KeyboardEvent) {
actualSelectionManager.handleKeyDown(event, nodes)
selectionManager.handleKeyDown(event, nodes)
}
function handleKeyUp(_event: KeyboardEvent) {
@@ -577,7 +578,7 @@
testModuleStates: untrack(() => testModuleStates),
moduleActions: untrack(() => moduleActions),
inputSchemaModified: untrack(() => inputSchemaModified),
selectedId: untrack(() => actualSelectionManager.getSelectedId()),
selectedId: untrack(() => selectedId),
path,
newFlow,
cache,
@@ -599,7 +600,7 @@
eventHandler,
success,
$useDataflow,
untrack(() => actualSelectionManager.getSelectedId()),
untrack(() => selectedId),
moving,
simplifiableFlow,
triggerNode ? path : undefined,
@@ -642,14 +643,14 @@
})
let showDataflow = $derived(
actualSelectionManager.getSelectedId() !== undefined &&
actualSelectionManager.getSelectedId() !== null &&
!actualSelectionManager.getSelectedId()?.startsWith('constants') &&
!actualSelectionManager.getSelectedId()?.startsWith('settings') &&
actualSelectionManager.getSelectedId() !== 'failure' &&
actualSelectionManager.getSelectedId() !== 'preprocessor' &&
actualSelectionManager.getSelectedId() !== 'Result' &&
actualSelectionManager.getSelectedId() !== 'Trigger'
selectedId !== undefined &&
selectedId !== null &&
!selectedId?.startsWith('constants') &&
!selectedId?.startsWith('settings') &&
selectedId !== 'failure' &&
selectedId !== 'preprocessor' &&
selectedId !== 'Result' &&
selectedId !== 'Trigger'
)
let debouncedWidth: number | undefined = $state(undefined)
let timeout: number | undefined = $state(undefined)
@@ -721,8 +722,8 @@
<SvelteFlow
onpaneclick={() => {
document.dispatchEvent(new Event('focus'))
if (actualSelectionManager.mode === 'normal') {
actualSelectionManager.clearSelection()
if (selectionManager.mode === 'normal') {
selectionManager.clearSelection()
}
}}
onnodedragstop={(event) => {
@@ -759,22 +760,20 @@
{#if multiSelectEnabled}
<NodeContextMenu
selectedNodeIds={actualSelectionManager.selectedIds.filter(
selectedNodeIds={selectionManager.selectedIds.filter(
(id) =>
!id.startsWith('Settings') && !id.startsWith('Trigger') && !id.startsWith('Result')
)}
>
<SelectionBoundingBox
selectedNodes={nodes.filter((node) =>
actualSelectionManager.selectedIds.includes(node.id)
)}
selectedNodes={nodes.filter((node) => selectionManager.selectedIds.includes(node.id))}
/>
</NodeContextMenu>
<SelectionTool
selectionMode={actualSelectionManager.mode}
selectionMode={selectionManager.mode}
onNodesSelected={(nodeIds, addToExisting) =>
actualSelectionManager.selectNodes(nodeIds, addToExisting, modules, nodes)}
selectionManager.selectNodes(nodeIds, addToExisting, modules, nodes)}
{nodes}
/>
{/if}
@@ -789,19 +788,19 @@
<div class="flex items-center gap-2">
<ControlButton
onclick={() => {
actualSelectionManager.mode =
actualSelectionManager.mode === 'normal' ? 'rect-select' : 'normal'
selectionManager.mode =
selectionManager.mode === 'normal' ? 'rect-select' : 'normal'
}}
title="Toggle rectangle selection"
class={actualSelectionManager.mode === 'rect-select'
class={selectionManager.mode === 'rect-select'
? 'text-accent !bg-surface-selected'
: ''}
>
<MousePointer size="14" />
</ControlButton>
{#if actualSelectionManager.selectedIds.length > 0}
{#if selectionManager.selectedIds.length > 0}
<span class="text-xs text-secondary"
>{actualSelectionManager.selectedIds.length} selected</span
>{selectionManager.selectedIds.length} selected</span
>
{/if}
</div>