mirror of
https://github.com/windmill-labs/windmill.git
synced 2026-09-09 16:05:42 +00:00
Introduce node multiselect
This commit is contained in:
@@ -31,6 +31,7 @@
|
||||
import type { FlowState } from './flows/flowState'
|
||||
import { initHistory } from '$lib/history.svelte'
|
||||
import type { FlowEditorContext, FlowInput, FlowInputEditorState } from './flows/types'
|
||||
import { SelectionManager } from './graph/selectionUtils.svelte'
|
||||
import { dfs } from './flows/dfs'
|
||||
import { loadSchemaFromModule } from './flows/flowInfers'
|
||||
import { CornerDownLeft, Play } from 'lucide-svelte'
|
||||
@@ -475,7 +476,7 @@
|
||||
let ids = dfs(flowStore.val.value.modules ?? [], (m) => m.id)
|
||||
flowStateStore.val = Object.fromEntries(ids.map((k) => [k, {}]))
|
||||
} catch (e) {}
|
||||
inferModuleArgs($selectedIdStore)
|
||||
inferModuleArgs(selectionManager.getSelectedId()!)
|
||||
}
|
||||
} catch (e) {
|
||||
console.error('issue setting new flowstore', e)
|
||||
@@ -489,7 +490,8 @@
|
||||
const moving = writable<{ id: string } | undefined>(undefined)
|
||||
const history = initHistory(flowStore.val)
|
||||
const stepsInputArgs = new StepsInputArgs()
|
||||
const selectedIdStore = writable('settings-metadata')
|
||||
const selectionManager = new SelectionManager()
|
||||
selectionManager.selectId('settings-metadata')
|
||||
const triggersCount = writable<TriggersCount | undefined>(undefined)
|
||||
const modulesTestStates = new ModulesTestStates((moduleId) => {
|
||||
// console.log('FOO')
|
||||
@@ -508,7 +510,7 @@
|
||||
let pathStore = writable('')
|
||||
let initialPathStore = writable('')
|
||||
setContext<FlowEditorContext>('FlowEditorContext', {
|
||||
selectedId: selectedIdStore,
|
||||
selectionManager,
|
||||
previewArgs: previewArgsStore,
|
||||
scriptEditorDrawer,
|
||||
moving,
|
||||
@@ -618,7 +620,7 @@
|
||||
flowStore.val && untrack(() => updateFlow(flowStore.val))
|
||||
})
|
||||
$effect(() => {
|
||||
$selectedIdStore && untrack(() => inferModuleArgs($selectedIdStore))
|
||||
selectionManager.getSelectedId() && untrack(() => inferModuleArgs(selectionManager.getSelectedId()!))
|
||||
})
|
||||
|
||||
let localModuleStates: Record<string, GraphModuleState> = $state({})
|
||||
@@ -640,7 +642,7 @@
|
||||
job.success &&
|
||||
flowPreviewButtons?.getPreviewMode() === 'whole'
|
||||
) {
|
||||
if (flowModuleSchemaMap?.isNodeVisible('result') && $selectedIdStore !== 'Result') {
|
||||
if (flowModuleSchemaMap?.isNodeVisible('result') && selectionManager.getSelectedId() !== 'Result') {
|
||||
outputPickerOpenFns['Result']?.()
|
||||
}
|
||||
} else {
|
||||
@@ -846,7 +848,7 @@
|
||||
on:applyArgs={(ev) => {
|
||||
if (ev.detail.kind === 'preprocessor') {
|
||||
stepsInputArgs.setStepArgs('preprocessor', ev.detail.args ?? {})
|
||||
$selectedIdStore = 'preprocessor'
|
||||
selectionManager.selectId('preprocessor')
|
||||
} else {
|
||||
previewArgsStore.val = ev.detail.args ?? {}
|
||||
flowPreviewButtons?.openPreview()
|
||||
|
||||
@@ -43,6 +43,7 @@
|
||||
import FlowImportExportMenu from './flows/header/FlowImportExportMenu.svelte'
|
||||
import FlowPreviewButtons from './flows/header/FlowPreviewButtons.svelte'
|
||||
import type { FlowEditorContext, FlowInput, FlowInputEditorState } from './flows/types'
|
||||
import { SelectionManager } from './graph/selectionUtils.svelte'
|
||||
import { cleanInputs } from './flows/utils'
|
||||
import {
|
||||
Calendar,
|
||||
@@ -338,11 +339,11 @@
|
||||
|
||||
let savedAtNewPath = false
|
||||
if (newFlow) {
|
||||
onSaveInitial?.({ path: $pathStore, id: getSelectedId() })
|
||||
onSaveInitial?.({ path: $pathStore, id: getSelectedId() ?? 'settings' })
|
||||
} else if (savedFlow?.draft_only && $pathStore !== initialPath) {
|
||||
savedAtNewPath = true
|
||||
initialPath = $pathStore
|
||||
onSaveDraftOnlyAtNewPath?.({ path: $pathStore, selectedId: getSelectedId() })
|
||||
onSaveDraftOnlyAtNewPath?.({ path: $pathStore, selectedId: getSelectedId() ?? 'settings' })
|
||||
// this is so we can use the flow builder outside of sveltekit
|
||||
}
|
||||
onSaveDraft?.({ path: $pathStore, savedAtNewPath, newFlow })
|
||||
@@ -561,7 +562,7 @@
|
||||
encodeState({
|
||||
flow: flowStore.val,
|
||||
path: $pathStore,
|
||||
selectedId: $selectedIdStore,
|
||||
selectedId: selectionManager.getSelectedId(),
|
||||
draft_triggers: triggersState.getDraftTriggersSnapshot(),
|
||||
selected_trigger: triggersState.getSelectedTriggerSnapshot(),
|
||||
loadedFromHistory: {
|
||||
@@ -576,10 +577,16 @@
|
||||
}, 500)
|
||||
}
|
||||
|
||||
const selectedIdStore = writable<string>(selectedId ?? 'settings-metadata')
|
||||
const selectionManager = new SelectionManager()
|
||||
// Initialize with selected id if provided
|
||||
if (selectedId) {
|
||||
selectionManager.selectId(selectedId)
|
||||
} else {
|
||||
selectionManager.selectId('settings-metadata')
|
||||
}
|
||||
|
||||
export function getSelectedId() {
|
||||
return $selectedIdStore
|
||||
return selectionManager.getSelectedId()
|
||||
}
|
||||
|
||||
const previewArgsStore = $state({ val: initialArgs })
|
||||
@@ -598,7 +605,7 @@
|
||||
const stepsInputArgs = new StepsInputArgs()
|
||||
|
||||
function select(selectedId: string) {
|
||||
selectedIdStore.set(selectedId)
|
||||
selectionManager.selectId(selectedId)
|
||||
}
|
||||
|
||||
let insertButtonOpen = writable<boolean>(false)
|
||||
@@ -607,7 +614,7 @@
|
||||
let flowEditor: FlowEditor | undefined = $state(undefined)
|
||||
|
||||
setContext<FlowEditorContext>('FlowEditorContext', {
|
||||
selectedId: selectedIdStore,
|
||||
selectionManager,
|
||||
currentEditor: writable(undefined),
|
||||
previewArgs: previewArgsStore,
|
||||
scriptEditorDrawer,
|
||||
@@ -695,7 +702,7 @@
|
||||
case 'z':
|
||||
if (event.ctrlKey || event.metaKey) {
|
||||
flowStore.val = undo(history, flowStore.val)
|
||||
$selectedIdStore = 'Input'
|
||||
selectionManager.selectId('Input')
|
||||
event.preventDefault()
|
||||
}
|
||||
break
|
||||
@@ -708,9 +715,9 @@
|
||||
case 'ArrowDown': {
|
||||
if (!$insertButtonOpen && !flowPreviewButtons?.getPreviewOpen()) {
|
||||
let ids = generateIds()
|
||||
let idx = ids.indexOf($selectedIdStore)
|
||||
let idx = ids.indexOf(selectionManager.getSelectedId()!)
|
||||
if (idx > -1 && idx < ids.length - 1) {
|
||||
$selectedIdStore = ids[idx + 1]
|
||||
selectionManager.selectId(ids[idx + 1])
|
||||
event.preventDefault()
|
||||
}
|
||||
}
|
||||
@@ -719,9 +726,9 @@
|
||||
case 'ArrowUp': {
|
||||
if (!$insertButtonOpen && !flowPreviewButtons?.getPreviewOpen()) {
|
||||
let ids = generateIds()
|
||||
let idx = ids.indexOf($selectedIdStore)
|
||||
let idx = ids.indexOf(selectionManager.getSelectedId()!)
|
||||
if (idx > 0 && idx < ids.length) {
|
||||
$selectedIdStore = ids[idx - 1]
|
||||
selectionManager.selectId(ids[idx - 1])
|
||||
event.preventDefault()
|
||||
}
|
||||
}
|
||||
@@ -868,7 +875,7 @@
|
||||
setContext('customUi', customUi)
|
||||
})
|
||||
$effect.pre(() => {
|
||||
if (flowStore.val || $selectedIdStore) {
|
||||
if (flowStore.val || selectionManager.getSelectedId()) {
|
||||
readFieldsRecursively(flowStore.val)
|
||||
untrack(() => saveSessionDraft())
|
||||
}
|
||||
@@ -932,7 +939,7 @@
|
||||
job.success &&
|
||||
flowPreviewButtons?.getPreviewMode() === 'whole'
|
||||
) {
|
||||
if (flowEditor?.isNodeVisible('result') && $selectedIdStore !== 'Result') {
|
||||
if (flowEditor?.isNodeVisible('result') && selectionManager.getSelectedId() !== 'Result') {
|
||||
outputPickerOpenFns['Result']?.()
|
||||
}
|
||||
} else {
|
||||
@@ -1026,7 +1033,7 @@
|
||||
}
|
||||
}
|
||||
|
||||
$selectedIdStore = 'Input'
|
||||
selectionManager.selectId('Input')
|
||||
}}
|
||||
on:redo={() => {
|
||||
flowStore.val = redo(history)
|
||||
@@ -1190,7 +1197,7 @@
|
||||
on:applyArgs={(ev) => {
|
||||
if (ev.detail.kind === 'preprocessor') {
|
||||
stepsInputArgs.setStepArgs('preprocessor', ev.detail.args ?? {})
|
||||
$selectedIdStore = 'preprocessor'
|
||||
selectionManager.selectId('preprocessor')
|
||||
}
|
||||
}}
|
||||
on:testWithArgs={(e) => {
|
||||
@@ -1203,7 +1210,7 @@
|
||||
{savedFlow}
|
||||
onDeployTrigger={handleDeployTrigger}
|
||||
onEditInput={(moduleId, key) => {
|
||||
selectedIdStore.set(moduleId)
|
||||
selectionManager.selectId(moduleId)
|
||||
// Use new prop-based system
|
||||
forceTestTab[moduleId] = true
|
||||
highlightArg[moduleId] = key
|
||||
|
||||
@@ -46,7 +46,7 @@
|
||||
failureModule={flow?.value?.failure_module}
|
||||
preprocessorModule={flow?.value?.preprocessor_module}
|
||||
onSelect={(nodeId) => {
|
||||
if (nodeId === 'triggers') {
|
||||
if (nodeId === 'Trigger') {
|
||||
dispatch('triggerDetail')
|
||||
return
|
||||
} else if (nodeId === 'failure') {
|
||||
|
||||
@@ -100,7 +100,7 @@
|
||||
}
|
||||
|
||||
const {
|
||||
selectedId,
|
||||
selectionManager,
|
||||
previewArgs,
|
||||
flowStateStore,
|
||||
flowStore,
|
||||
@@ -126,7 +126,7 @@
|
||||
} else {
|
||||
const flow = previewFlow ?? stateSnapshot(flowStore).val
|
||||
const idOrders = dfs(flow.value.modules, (x) => x.id)
|
||||
let upToIndex = idOrders.indexOf(upToId ?? $selectedId)
|
||||
let upToIndex = idOrders.indexOf(upToId ?? selectionManager.getSelectedId() ?? '')
|
||||
|
||||
if (upToIndex != -1) {
|
||||
flow.value.modules = sliceModules(flow.value.modules, upToIndex, idOrders)
|
||||
@@ -430,7 +430,7 @@
|
||||
{#if previewMode == 'upTo'}
|
||||
Test up to
|
||||
<Badge baseClass="ml-1" color="indigo">
|
||||
{$selectedId}
|
||||
{selectionManager.getSelectedId()}
|
||||
</Badge>
|
||||
{:else}
|
||||
Test flow
|
||||
|
||||
@@ -39,7 +39,6 @@
|
||||
import type { FlowGraphAssetContext } from './flows/types'
|
||||
import { createState } from '$lib/svelte5Utils.svelte'
|
||||
import JobLoader from './JobLoader.svelte'
|
||||
import { writable } from 'svelte/store'
|
||||
import {
|
||||
AI_TOOL_CALL_PREFIX,
|
||||
AI_TOOL_MESSAGE_PREFIX,
|
||||
@@ -48,6 +47,7 @@
|
||||
} from './graph/renderers/nodes/AIToolNode.svelte'
|
||||
import JobAssetsViewer from './assets/JobAssetsViewer.svelte'
|
||||
import McpToolCallDetails from './McpToolCallDetails.svelte'
|
||||
import { SelectionManager } from './graph/selectionUtils.svelte'
|
||||
|
||||
let {
|
||||
flowState: flowStateStore,
|
||||
@@ -232,7 +232,7 @@
|
||||
|
||||
let expandedSubflows: Record<string, FlowModule[]> = $state({})
|
||||
|
||||
let selectedId = writable<string | undefined>(selectedNode)
|
||||
let selectionManager = new SelectionManager()
|
||||
|
||||
function onFlowModuleId() {
|
||||
let modId = flowJobIds?.moduleId
|
||||
@@ -1730,7 +1730,7 @@
|
||||
{/each}
|
||||
</div>
|
||||
<FlowGraphV2
|
||||
{selectedId}
|
||||
{selectionManager}
|
||||
triggerNode={true}
|
||||
download={!hideDownloadInGraph}
|
||||
minHeight={wrapperHeight}
|
||||
|
||||
@@ -35,7 +35,7 @@
|
||||
)
|
||||
|
||||
let abortController = new AbortController()
|
||||
const { flowStore, selectedId } = getContext<FlowEditorContext>('FlowEditorContext')
|
||||
const { flowStore, selectionManager } = getContext<FlowEditorContext>('FlowEditorContext')
|
||||
|
||||
async function generateIteratorExpr() {
|
||||
if (generatedContent.length > 0 || loading) {
|
||||
@@ -45,7 +45,7 @@
|
||||
loading = true
|
||||
const flow: Flow = JSON.parse(JSON.stringify(flowStore.val))
|
||||
const idOrders = dfs(flow.value.modules, (x) => x.id)
|
||||
const upToIndex = idOrders.indexOf($selectedId)
|
||||
const upToIndex = idOrders.indexOf(selectionManager.getSelectedId()!)
|
||||
if (upToIndex === -1) {
|
||||
throw new Error('Could not find the selected id in the flow')
|
||||
}
|
||||
@@ -60,7 +60,7 @@
|
||||
flow_input: pickableProperties?.flow_input
|
||||
}
|
||||
const user = `I'm building a workflow which is a DAG of script steps.
|
||||
The current step is ${$selectedId} and represents a for-loop. You can find the details of all the steps below:
|
||||
The current step is ${selectionManager.getSelectedId()!} and represents a for-loop. You can find the details of all the steps below:
|
||||
${flowDetails}
|
||||
Determine the iterator expression to pass either from the previous results or the flow inputs. Here's a summary of the available data:
|
||||
<available>
|
||||
|
||||
@@ -29,7 +29,7 @@
|
||||
})
|
||||
|
||||
let abortController = $state(new AbortController())
|
||||
const { flowStore, selectedId } = getContext<FlowEditorContext>('FlowEditorContext')
|
||||
const { flowStore, selectionManager } = getContext<FlowEditorContext>('FlowEditorContext')
|
||||
|
||||
const dispatch = createEventDispatcher()
|
||||
|
||||
@@ -38,7 +38,7 @@
|
||||
loading = true
|
||||
const flow: Flow = JSON.parse(JSON.stringify(flowStore.val))
|
||||
const idOrders = dfs(flow.value.modules, (x) => x.id)
|
||||
const upToIndex = idOrders.indexOf($selectedId)
|
||||
const upToIndex = idOrders.indexOf(selectionManager.getSelectedId()!)
|
||||
if (upToIndex === -1) {
|
||||
throw new Error('Could not find the selected id in the flow')
|
||||
}
|
||||
@@ -53,7 +53,7 @@
|
||||
flow_input: pickableProperties?.flow_input
|
||||
}
|
||||
const user = `I'm building a workflow which is a DAG of script steps.
|
||||
The current step is ${$selectedId} and is a branching step (if-else).
|
||||
The current step is ${selectionManager.getSelectedId()!} and is a branching step (if-else).
|
||||
The user wants to generate a predicate for the branching condition.
|
||||
Here's the user's request: ${instructions}
|
||||
You can find the details of all the steps below:
|
||||
|
||||
@@ -54,7 +54,7 @@
|
||||
let abortController = new AbortController()
|
||||
let newFlowInput = $state('')
|
||||
|
||||
const { flowStore, selectedId } = getContext<FlowEditorContext>('FlowEditorContext')
|
||||
const { flowStore, selectionManager } = getContext<FlowEditorContext>('FlowEditorContext')
|
||||
const { stepInputsLoading, generatedExprs } =
|
||||
getContext<FlowCopilotContext | undefined>('FlowCopilotContext') || {}
|
||||
|
||||
@@ -86,7 +86,7 @@
|
||||
loading = true
|
||||
const flow: Flow = JSON.parse(JSON.stringify(flowStore.val))
|
||||
const idOrders = dfs(flow.value.modules, (x) => x.id)
|
||||
const upToIndex = idOrders.indexOf($selectedId)
|
||||
const upToIndex = idOrders.indexOf(selectionManager.getSelectedId()!)
|
||||
if (upToIndex === -1) {
|
||||
throw new Error('Could not find the selected id in the flow')
|
||||
}
|
||||
@@ -102,7 +102,7 @@
|
||||
}
|
||||
const isInsideLoop = availableData.flow_input && 'iter' in availableData.flow_input
|
||||
const user = `I'm building a workflow which is a DAG of script steps.
|
||||
The current step is ${$selectedId}, you can find the details for the step and previous ones below:
|
||||
The current step is ${selectionManager.getSelectedId()!}, you can find the details for the step and previous ones below:
|
||||
${flowDetails}
|
||||
Determine for the input "${argName}", what to pass either from the previous results or the flow inputs.
|
||||
All possibles inputs either start with results. or flow_input. and are followed by the key of the input.
|
||||
|
||||
@@ -30,7 +30,7 @@
|
||||
|
||||
let { pickableProperties = undefined, argNames = [], schema = undefined }: Props = $props()
|
||||
|
||||
const { flowStore, selectedId } = getContext<FlowEditorContext>('FlowEditorContext')
|
||||
const { flowStore, selectionManager } = getContext<FlowEditorContext>('FlowEditorContext')
|
||||
|
||||
const { exprsToSet, stepInputsLoading, generatedExprs } =
|
||||
getContext<FlowCopilotContext | undefined>('FlowCopilotContext') || {}
|
||||
@@ -49,7 +49,7 @@
|
||||
stepInputsLoading?.set(true)
|
||||
const flow: Flow = JSON.parse(JSON.stringify(flowStore.val))
|
||||
const idOrders = dfs(flow.value.modules, (x) => x.id)
|
||||
const upToIndex = idOrders.indexOf($selectedId)
|
||||
const upToIndex = idOrders.indexOf(selectionManager.getSelectedId()!)
|
||||
if (upToIndex === -1) {
|
||||
throw new Error('Could not find the selected id in the flow')
|
||||
}
|
||||
@@ -65,7 +65,7 @@
|
||||
}
|
||||
const isInsideLoop = availableData.flow_input && 'iter' in availableData.flow_input
|
||||
const user = `I'm building a workflow which is a DAG of script steps.
|
||||
The current step is ${$selectedId}, you can find the details for the step and previous ones below:
|
||||
The current step is ${selectionManager.getSelectedId()!}, you can find the details for the step and previous ones below:
|
||||
${flowDetails}
|
||||
|
||||
Determine for all the inputs "${argNames.join(
|
||||
|
||||
@@ -872,7 +872,7 @@ class AIChatManager {
|
||||
}
|
||||
|
||||
listenForSelectedIdChanges = (
|
||||
selectedId: string,
|
||||
selectedId: string | undefined,
|
||||
flowStore: ExtendedOpenFlow,
|
||||
flowStateStore: FlowState,
|
||||
currentEditor: CurrentEditor
|
||||
|
||||
@@ -25,7 +25,7 @@
|
||||
flowModuleSchemaMap: FlowModuleSchemaMap | undefined
|
||||
} = $props()
|
||||
|
||||
const { flowStore, flowStateStore, selectedId, currentEditor } =
|
||||
const { flowStore, flowStateStore, selectionManager, currentEditor } =
|
||||
getContext<FlowEditorContext>('FlowEditorContext')
|
||||
|
||||
const { exprsToSet } = getContext<FlowCopilotContext | undefined>('FlowCopilotContext') ?? {}
|
||||
@@ -84,7 +84,7 @@
|
||||
const flow = $state.snapshot(flowStore).val
|
||||
return {
|
||||
flow,
|
||||
selectedId: $selectedId
|
||||
selectedId: selectionManager.getSelectedId()!
|
||||
}
|
||||
},
|
||||
// flow apply/reject
|
||||
@@ -382,7 +382,7 @@
|
||||
value: match[2].trim()
|
||||
}))
|
||||
|
||||
if (id === $selectedId) {
|
||||
if (id === selectionManager.getSelectedId()!) {
|
||||
exprsToSet?.set({})
|
||||
const argsToUpdate = {}
|
||||
for (const { input, value } of parsedInputs) {
|
||||
@@ -421,7 +421,7 @@
|
||||
setModuleStatus('Input', 'modified')
|
||||
},
|
||||
selectStep: (id) => {
|
||||
$selectedId = id
|
||||
selectionManager.selectId(id)
|
||||
},
|
||||
getStepCode: (id) => {
|
||||
const module = getModule(id)
|
||||
@@ -611,7 +611,7 @@
|
||||
|
||||
$effect(() => {
|
||||
const cleanup = aiChatManager.listenForSelectedIdChanges(
|
||||
$selectedId,
|
||||
selectionManager.getSelectedId(),
|
||||
flowStore.val,
|
||||
flowStateStore.val,
|
||||
$currentEditor
|
||||
@@ -626,21 +626,21 @@
|
||||
|
||||
// Automatically show revert review when selecting a rawscript module with pending changes
|
||||
$effect(() => {
|
||||
const selectedId = selectionManager.getSelectedId()
|
||||
if (
|
||||
$currentEditor?.type === 'script' &&
|
||||
$selectedId &&
|
||||
affectedModules[$selectedId] &&
|
||||
selectedId &&
|
||||
affectedModules[selectedId] &&
|
||||
$currentEditor.editor.getAiChatEditorHandler()
|
||||
) {
|
||||
const moduleLastSnapshot = getModule($selectedId, lastSnapshot)
|
||||
const moduleLastSnapshot = getModule(selectedId, lastSnapshot)
|
||||
const content =
|
||||
moduleLastSnapshot?.value.type === 'rawscript' ? moduleLastSnapshot.value.content : ''
|
||||
if (content.length > 0) {
|
||||
untrack(() =>
|
||||
$currentEditor.editor.reviewAppliedCode(content, {
|
||||
onFinishedReview: () => {
|
||||
const id = $selectedId
|
||||
flowHelpers.acceptModuleAction(id)
|
||||
flowHelpers.acceptModuleAction(selectedId)
|
||||
$currentEditor.hideDiffMode()
|
||||
}
|
||||
})
|
||||
|
||||
@@ -55,7 +55,7 @@
|
||||
}: Props = $props()
|
||||
|
||||
const {
|
||||
selectedId,
|
||||
selectionManager,
|
||||
flowStore,
|
||||
flowStateStore,
|
||||
flowInputsStore,
|
||||
@@ -84,14 +84,26 @@
|
||||
})
|
||||
</script>
|
||||
|
||||
{#if $selectedId?.startsWith('settings')}
|
||||
{#if selectionManager && selectionManager.selectedIds.length > 1}
|
||||
<div class="p-4">
|
||||
<h3 class="text-lg font-semibold mb-2">Multiple Selection</h3>
|
||||
<p class="text-sm text-secondary mb-4">{selectionManager.selectedIds.length} nodes selected</p>
|
||||
<div class="space-y-2">
|
||||
{#each selectionManager.selectedIds as nodeId}
|
||||
<div class="text-sm px-2 py-1 bg-surface rounded border">
|
||||
{nodeId}
|
||||
</div>
|
||||
{/each}
|
||||
</div>
|
||||
</div>
|
||||
{:else if selectionManager.getSelectedId()?.startsWith('settings')}
|
||||
<FlowSettings {enableAi} {noEditor} />
|
||||
{:else if $selectedId === 'Input'}
|
||||
{:else if selectionManager.getSelectedId() === 'Input'}
|
||||
<FlowInput
|
||||
{noEditor}
|
||||
disabled={disabledFlowInputs}
|
||||
on:openTriggers={(ev) => {
|
||||
$selectedId = 'triggers'
|
||||
selectionManager.selectId('Trigger')
|
||||
handleSelectTriggerFromKind(triggersState, triggersCount, savedFlow?.path, ev.detail.kind)
|
||||
showCaptureHint.set(true)
|
||||
}}
|
||||
@@ -99,22 +111,22 @@
|
||||
{onTestFlow}
|
||||
{previewOpen}
|
||||
/>
|
||||
{:else if $selectedId === 'Result'}
|
||||
{:else if selectionManager.getSelectedId() === 'Result'}
|
||||
<FlowResult {noEditor} {job} {isOwner} {suspendStatus} {onOpenDetails} />
|
||||
{:else if $selectedId === 'constants'}
|
||||
{:else if selectionManager.getSelectedId() === 'constants'}
|
||||
<FlowConstants {noEditor} />
|
||||
{:else if $selectedId === 'failure'}
|
||||
{:else if selectionManager.getSelectedId() === 'failure'}
|
||||
<FlowFailureModule {noEditor} savedModule={savedFlow?.value.failure_module} />
|
||||
{:else if $selectedId === 'preprocessor'}
|
||||
{:else if selectionManager.getSelectedId() === 'preprocessor'}
|
||||
<FlowPreprocessorModule {noEditor} savedModule={savedFlow?.value.preprocessor_module} />
|
||||
{:else if $selectedId === 'triggers'}
|
||||
{:else if selectionManager.getSelectedId() === 'Trigger'}
|
||||
<TriggersEditor
|
||||
on:applyArgs
|
||||
on:addPreprocessor={async () => {
|
||||
await insertNewPreprocessorModule(flowStore, flowStateStore, {
|
||||
language: 'bun'
|
||||
})
|
||||
$selectedId = 'preprocessor'
|
||||
selectionManager.selectId('preprocessor')
|
||||
}}
|
||||
on:updateSchema={(e) => {
|
||||
const { payloadData, redirect } = e.detail
|
||||
@@ -122,7 +134,7 @@
|
||||
previewArgs.val = JSON.parse(JSON.stringify(payloadData))
|
||||
}
|
||||
if (redirect) {
|
||||
$selectedId = 'Input'
|
||||
selectionManager.selectId('Input')
|
||||
$flowInputEditorState.selectedTab = 'captures'
|
||||
$flowInputEditorState.payloadData = payloadData
|
||||
}
|
||||
@@ -141,7 +153,7 @@
|
||||
schema={flowStore.val.schema}
|
||||
{onDeployTrigger}
|
||||
/>
|
||||
{:else if $selectedId.startsWith('subflow:')}
|
||||
{:else if selectionManager.getSelectedId()?.startsWith('subflow:')}
|
||||
<div class="p-4"
|
||||
>Selected step is witin an expanded subflow and is not directly editable in the flow editor</div
|
||||
>
|
||||
@@ -150,7 +162,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 $selectedId}
|
||||
{#key selectionManager.getSelectedId()}
|
||||
{#each flowStore.val.value.modules as flowModule, index (flowModule.id ?? index)}
|
||||
<FlowModuleWrapper
|
||||
{noEditor}
|
||||
|
||||
@@ -59,7 +59,7 @@
|
||||
import { DynamicInput } from '$lib/utils'
|
||||
|
||||
const {
|
||||
selectedId,
|
||||
selectionManager,
|
||||
currentEditor,
|
||||
previewArgs,
|
||||
flowStateStore,
|
||||
@@ -215,7 +215,7 @@
|
||||
let stepHistoryLoader = getStepHistoryLoaderContext()
|
||||
|
||||
function onSelectedIdChange() {
|
||||
if (!flowStateStore?.val?.[$selectedId]?.schema && flowModule) {
|
||||
if (!flowStateStore?.val?.[selectionManager.getSelectedId()!]?.schema && flowModule) {
|
||||
reload(flowModule)
|
||||
}
|
||||
}
|
||||
@@ -252,7 +252,7 @@
|
||||
)
|
||||
|
||||
$effect.pre(() => {
|
||||
$selectedId && untrack(() => onSelectedIdChange())
|
||||
selectionManager.getSelectedId() && untrack(() => onSelectedIdChange())
|
||||
})
|
||||
let parentLoop = $derived(
|
||||
flowStore.val && flowModule ? checkIfParentLoop(flowStore.val, flowModule.id) : undefined
|
||||
@@ -404,7 +404,7 @@
|
||||
on:createScriptFromInlineScript={async () => {
|
||||
const [module, state] = await createScriptFromInlineScript(
|
||||
flowModule,
|
||||
$selectedId,
|
||||
selectionManager.getSelectedId()!,
|
||||
flowStateStore.val[flowModule.id].schema,
|
||||
$pathStore
|
||||
)
|
||||
@@ -468,7 +468,7 @@
|
||||
automaticLayout={true}
|
||||
cmdEnterAction={async () => {
|
||||
selected = 'test'
|
||||
if ($selectedId == flowModule.id) {
|
||||
if (selectionManager.getSelectedId() == flowModule.id) {
|
||||
if (flowModule.value.type === 'rawscript' && editor) {
|
||||
flowModule.value.content = editor.getCode()
|
||||
}
|
||||
@@ -578,7 +578,8 @@
|
||||
class="px-2 xl:px-4"
|
||||
bind:this={inputTransformSchemaForm}
|
||||
pickableProperties={stepPropPicker.pickableProperties}
|
||||
schema={flowStateStore.val[$selectedId]?.schema ?? {}}
|
||||
schema={flowStateStore.val[selectionManager.getSelectedId()!]?.schema ??
|
||||
{}}
|
||||
previousModuleId={previousModule?.id}
|
||||
bind:args={
|
||||
() => {
|
||||
@@ -609,7 +610,7 @@
|
||||
bind:this={modulePreview}
|
||||
mod={flowModule}
|
||||
{noEditor}
|
||||
schema={flowStateStore.val[$selectedId]?.schema ?? {}}
|
||||
schema={flowStateStore.val[selectionManager.getSelectedId()!]?.schema ?? {}}
|
||||
bind:testJob
|
||||
bind:testIsLoading
|
||||
bind:scriptProgress
|
||||
@@ -623,7 +624,7 @@
|
||||
active={flowModule.retry !== undefined}
|
||||
label="Retries"
|
||||
/>
|
||||
{#if !$selectedId.includes('failure')}
|
||||
{#if !selectionManager.getSelectedId()?.includes('failure')}
|
||||
<Tab value="runtime" label="Runtime" />
|
||||
<Tab value="cache" active={Boolean(flowModule.cache_ttl)} label="Cache" />
|
||||
<Tab
|
||||
@@ -838,7 +839,7 @@
|
||||
<Button
|
||||
btnClasses="mt-4"
|
||||
on:click={() => {
|
||||
$selectedId = 'settings-same-worker'
|
||||
selectionManager.selectId('settings-same-worker')
|
||||
}}
|
||||
>
|
||||
Set shared directory in the flow settings
|
||||
|
||||
@@ -20,7 +20,7 @@
|
||||
|
||||
let { flowModule = $bindable(), previousModuleId }: Props = $props()
|
||||
|
||||
const { selectedId, flowStore, flowStateStore, previewArgs } =
|
||||
const { selectionManager, flowStore, flowStateStore, previewArgs } =
|
||||
getContext<FlowEditorContext>('FlowEditorContext')
|
||||
let schema = $state(emptySchema())
|
||||
schema.properties['sleep'] = {
|
||||
@@ -41,7 +41,7 @@
|
||||
)
|
||||
)
|
||||
|
||||
const result = flowStateStore.val[$selectedId]?.previewResult ?? {}
|
||||
const result = flowStateStore.val[selectionManager.getSelectedId()!]?.previewResult ?? {}
|
||||
|
||||
let isSleepEnabled = $derived(Boolean(flowModule.sleep))
|
||||
</script>
|
||||
|
||||
@@ -18,8 +18,8 @@
|
||||
import EditableSchemaDrawer from '$lib/components/schema/EditableSchemaDrawer.svelte'
|
||||
import AddProperty from '$lib/components/schema/AddProperty.svelte'
|
||||
|
||||
const { selectedId, flowStateStore } = getContext<FlowEditorContext>('FlowEditorContext')
|
||||
const result = flowStateStore.val[$selectedId]?.previewResult ?? {}
|
||||
const { selectionManager, flowStateStore } = getContext<FlowEditorContext>('FlowEditorContext')
|
||||
const result = flowStateStore.val[selectionManager.getSelectedId()!]?.previewResult ?? {}
|
||||
let editor: SimpleEditor | undefined = $state(undefined)
|
||||
|
||||
interface Props {
|
||||
|
||||
@@ -17,7 +17,7 @@
|
||||
noLabel?: boolean
|
||||
} = $props()
|
||||
|
||||
const { flowStore, selectedId } = getContext<FlowEditorContext>('FlowEditorContext')
|
||||
const { flowStore, selectionManager } = getContext<FlowEditorContext>('FlowEditorContext')
|
||||
|
||||
const dispatch = createEventDispatcher()
|
||||
loadWorkerGroups()
|
||||
@@ -44,7 +44,7 @@
|
||||
<button
|
||||
title="Worker Group is defined at the flow level"
|
||||
class="w-full text-left items-center font-normal p-1 py-2 border text-xs rounded"
|
||||
onclick={() => ($selectedId = 'settings-worker-group')}
|
||||
onclick={() => selectionManager.selectId('settings-worker-group')}
|
||||
>
|
||||
Flow's WG: {flowStore.val.tag}
|
||||
</button>
|
||||
|
||||
@@ -23,7 +23,7 @@
|
||||
import { formatCron } from '$lib/utils'
|
||||
import AgentToolWrapper from './AgentToolWrapper.svelte'
|
||||
|
||||
const { selectedId, flowStateStore } = getContext<FlowEditorContext>('FlowEditorContext')
|
||||
const { selectionManager, flowStateStore } = getContext<FlowEditorContext>('FlowEditorContext')
|
||||
|
||||
const { triggersState, triggersCount } = getContext<TriggerContext>('TriggerContext')
|
||||
|
||||
@@ -113,7 +113,7 @@
|
||||
}
|
||||
</script>
|
||||
|
||||
{#if flowModule.id === $selectedId}
|
||||
{#if flowModule.id === selectionManager.getSelectedId()!}
|
||||
{#if flowModule.value.type === 'forloopflow'}
|
||||
<FlowLoop {noEditor} bind:mod={flowModule} {parentModule} {previousModule} {enableAi} />
|
||||
{:else if flowModule.value.type === 'whileloopflow'}
|
||||
@@ -123,13 +123,13 @@
|
||||
{:else if flowModule.value.type === 'branchall'}
|
||||
<FlowBranchesAllWrapper {noEditor} {previousModule} {parentModule} bind:flowModule />
|
||||
{:else if flowModule.value.type === 'identity'}
|
||||
{#if $selectedId == 'failure'}
|
||||
{#if selectionManager.getSelectedId() == '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 $selectedId == 'preprocessor'}
|
||||
{:else if selectionManager.getSelectedId() == 'preprocessor'}
|
||||
<div class="p-4">
|
||||
<Alert
|
||||
type="info"
|
||||
@@ -157,8 +157,8 @@
|
||||
summary={flowModule.summary}
|
||||
shouldDisableTriggerScripts={parentModule !== undefined ||
|
||||
previousModule !== undefined ||
|
||||
$selectedId == 'failure' ||
|
||||
$selectedId == 'preprocessor'}
|
||||
selectionManager.getSelectedId() == 'failure' ||
|
||||
selectionManager.getSelectedId() == 'preprocessor'}
|
||||
on:pick={async ({ detail }) => {
|
||||
const { path, summary, kind, hash } = detail
|
||||
createModuleFromScript(path, summary, kind, hash)
|
||||
@@ -187,8 +187,8 @@
|
||||
flowModule = module
|
||||
flowStateStore.val[module.id] = state
|
||||
}}
|
||||
failureModule={$selectedId === 'failure'}
|
||||
preprocessorModule={$selectedId === 'preprocessor'}
|
||||
failureModule={selectionManager.getSelectedId() === 'failure'}
|
||||
preprocessorModule={selectionManager.getSelectedId() === 'preprocessor'}
|
||||
/>
|
||||
{/if}
|
||||
{:else if flowModule.value.type === 'rawscript' || flowModule.value.type === 'script' || flowModule.value.type === 'flow' || flowModule.value.type === 'aiagent'}
|
||||
@@ -197,8 +197,8 @@
|
||||
bind:flowModule
|
||||
{parentModule}
|
||||
{previousModule}
|
||||
failureModule={$selectedId === 'failure'}
|
||||
preprocessorModule={$selectedId === 'preprocessor'}
|
||||
failureModule={selectionManager.getSelectedId() === 'failure'}
|
||||
preprocessorModule={selectionManager.getSelectedId() === 'preprocessor'}
|
||||
{scriptKind}
|
||||
{scriptTemplate}
|
||||
{enableAi}
|
||||
@@ -225,7 +225,7 @@
|
||||
/>
|
||||
{/each}
|
||||
{:else if flowModule.value.type === 'branchone'}
|
||||
{#if $selectedId === `${flowModule?.id}-branch-default`}
|
||||
{#if selectionManager.getSelectedId() === `${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 +247,7 @@
|
||||
{/each}
|
||||
{/if}
|
||||
{#each flowModule.value.branches as branch, branchIndex (branchIndex)}
|
||||
{#if $selectedId === `${flowModule?.id}-branch-${branchIndex}`}
|
||||
{#if selectionManager.getSelectedId() === `${flowModule?.id}-branch-${branchIndex}`}
|
||||
<FlowBranchOneWrapper
|
||||
{noEditor}
|
||||
bind:branch={flowModule.value.branches[branchIndex]}
|
||||
@@ -274,7 +274,7 @@
|
||||
{/each}
|
||||
{:else if flowModule.value.type === 'branchall'}
|
||||
{#each flowModule.value.branches as branch, branchIndex (branchIndex)}
|
||||
{#if $selectedId === `${flowModule?.id}-branch-${branchIndex}`}
|
||||
{#if selectionManager.getSelectedId() === `${flowModule?.id}-branch-${branchIndex}`}
|
||||
<FlowBranchAllWrapper {noEditor} bind:branch={flowModule.value.branches[branchIndex]} />
|
||||
{:else}
|
||||
{#each branch.modules as _, index}
|
||||
@@ -295,7 +295,7 @@
|
||||
{/each}
|
||||
{:else if flowModule.value.type === 'aiagent'}
|
||||
{#each flowModule.value.tools as tool, toolIndex (toolIndex)}
|
||||
{#if $selectedId === tool.id}
|
||||
{#if selectionManager.getSelectedId() === tool.id}
|
||||
<AgentToolWrapper
|
||||
{noEditor}
|
||||
bind:tool={flowModule.value.tools[toolIndex]}
|
||||
|
||||
@@ -25,7 +25,7 @@
|
||||
localModuleStates = $bindable({})
|
||||
}: Props = $props()
|
||||
|
||||
const { selectedId } = getContext<FlowEditorContext>('FlowEditorContext')
|
||||
const { selectionManager } = getContext<FlowEditorContext>('FlowEditorContext')
|
||||
|
||||
let flowPreviewContent: FlowPreviewContent | undefined = $state(undefined)
|
||||
let preventEscape = $state(false)
|
||||
@@ -70,7 +70,7 @@
|
||||
$state('timeline')
|
||||
|
||||
let upToDisabled = $derived.by(() => {
|
||||
const upToSelected = upToId ?? $selectedId
|
||||
const upToSelected = upToId ?? selectionManager.getSelectedId()
|
||||
return (
|
||||
upToSelected == undefined ||
|
||||
[
|
||||
@@ -92,7 +92,7 @@
|
||||
'constants',
|
||||
'Result',
|
||||
'Input',
|
||||
'triggers'
|
||||
'Trigger'
|
||||
].includes(upToSelected) ||
|
||||
upToSelected?.includes('branch') ||
|
||||
aiChatManager.flowAiChatHelpers?.getModuleAction(upToSelected) === 'removed'
|
||||
@@ -144,8 +144,8 @@
|
||||
dropdownItems={!upToDisabled
|
||||
? [
|
||||
{
|
||||
label: 'Test up to ' + $selectedId,
|
||||
onClick: () => testUpTo($selectedId, true)
|
||||
label: 'Test up to ' + selectionManager.getSelectedId(),
|
||||
onClick: () => testUpTo(selectionManager.getSelectedId(), true)
|
||||
}
|
||||
]
|
||||
: undefined}
|
||||
|
||||
@@ -29,7 +29,7 @@
|
||||
generateStep: { moduleId: string; instructions: string; lang: ScriptLang }
|
||||
}>()
|
||||
|
||||
const { selectedId, flowStateStore, flowStore } =
|
||||
const { selectionManager, flowStateStore, flowStore } =
|
||||
getContext<FlowEditorContext>('FlowEditorContext')
|
||||
|
||||
async function insertFailureModule(
|
||||
@@ -50,7 +50,7 @@
|
||||
})
|
||||
}
|
||||
|
||||
$selectedId = 'failure'
|
||||
selectionManager.selectId('failure')
|
||||
refreshStateStore(flowStore)
|
||||
}
|
||||
|
||||
@@ -70,10 +70,10 @@
|
||||
aiModuleActionToTextColor(action)
|
||||
)}
|
||||
id="flow-editor-error-handler"
|
||||
selected={$selectedId?.includes('failure')}
|
||||
selected={selectionManager.getSelectedId()?.includes('failure')}
|
||||
onClick={() => {
|
||||
if (flowStore.val?.value?.failure_module) {
|
||||
$selectedId = 'failure'
|
||||
selectionManager.selectId('failure')
|
||||
}
|
||||
}}
|
||||
>
|
||||
@@ -95,7 +95,7 @@
|
||||
class="ml-1"
|
||||
onclick={() => {
|
||||
flowStore.val.value.failure_module = undefined
|
||||
$selectedId = 'settings-metadata'
|
||||
selectionManager.selectId('settings-metadata')
|
||||
}}
|
||||
>
|
||||
<X size={12} />
|
||||
|
||||
@@ -105,7 +105,7 @@
|
||||
|
||||
let flowTutorials: FlowTutorials | undefined = $state(undefined)
|
||||
|
||||
const { customUi, selectedId, moving, history, flowStateStore, flowStore, pathStore } =
|
||||
const { customUi, selectionManager, moving, history, flowStateStore, flowStore, pathStore } =
|
||||
getContext<FlowEditorContext>('FlowEditorContext')
|
||||
const { triggersCount, triggersState } = getContext<TriggerContext>('TriggerContext')
|
||||
|
||||
@@ -238,9 +238,9 @@
|
||||
let allIds = dfs(flowStore.val.value.modules, (mod) => mod.id)
|
||||
if (allIds.length > 1) {
|
||||
const idx = allIds.indexOf(id)
|
||||
$selectedId = idx == 0 ? allIds[0] : allIds[idx - 1]
|
||||
selectionManager.selectId(idx == 0 ? allIds[0] : allIds[idx - 1])
|
||||
} else {
|
||||
$selectedId = 'settings-metadata'
|
||||
selectionManager.selectId('settings-metadata')
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -431,7 +431,7 @@
|
||||
flowStore.val.value.notes = newNotes
|
||||
}}
|
||||
preprocessorModule={flowStore.val.value?.preprocessor_module}
|
||||
{selectedId}
|
||||
{selectionManager}
|
||||
{workspace}
|
||||
editMode
|
||||
{onTestUpTo}
|
||||
@@ -450,7 +450,7 @@
|
||||
const cb = () => {
|
||||
push(history, flowStore.val)
|
||||
if (id === 'preprocessor') {
|
||||
$selectedId = 'Input'
|
||||
selectionManager.selectId('Input')
|
||||
flowStore.val.value.preprocessor_module = undefined
|
||||
} else {
|
||||
selectNextId(id)
|
||||
@@ -509,7 +509,7 @@
|
||||
|
||||
let [removedModule] = originalModules.splice(indexToRemove, 1)
|
||||
targetModules.splice(detail.index, 0, removedModule)
|
||||
$selectedId = removedModule.id
|
||||
selectionManager.selectId(removedModule.id)
|
||||
$moving = undefined
|
||||
} else {
|
||||
if (detail.isPreprocessor) {
|
||||
@@ -519,7 +519,7 @@
|
||||
detail.inlineScript,
|
||||
detail.script
|
||||
)
|
||||
$selectedId = 'preprocessor'
|
||||
selectionManager.selectId('preprocessor')
|
||||
|
||||
if (detail.inlineScript?.instructions) {
|
||||
dispatch('generateStep', {
|
||||
@@ -546,7 +546,7 @@
|
||||
toolKind
|
||||
)
|
||||
const id = targetModules[index].id
|
||||
$selectedId = id
|
||||
selectionManager.selectId(id)
|
||||
|
||||
if (detail.inlineScript?.instructions) {
|
||||
dispatch('generateStep', {
|
||||
@@ -631,13 +631,13 @@
|
||||
flowStateStore.val[newId] = flowStateStore.val[id]
|
||||
delete flowStateStore.val[id]
|
||||
refreshStateStore(flowStore)
|
||||
$selectedId = newId
|
||||
selectionManager.selectId(newId)
|
||||
}}
|
||||
onDeleteBranch={async ({ id, index }) => {
|
||||
if (id) {
|
||||
await removeBranch(id, index)
|
||||
refreshStateStore(flowStore)
|
||||
$selectedId = id
|
||||
selectionManager.selectId(id)
|
||||
}
|
||||
}}
|
||||
onMove={(id) => {
|
||||
@@ -658,6 +658,7 @@
|
||||
{onOpenPreview}
|
||||
{onHideJobStatus}
|
||||
exitNoteMode={() => (noteMode = false)}
|
||||
multiSelectEnabled
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -32,7 +32,7 @@
|
||||
disableAi
|
||||
}: Props = $props()
|
||||
|
||||
const { selectedId, flowStore } = getContext<FlowEditorContext>('FlowEditorContext')
|
||||
const { selectionManager, flowStore } = getContext<FlowEditorContext>('FlowEditorContext')
|
||||
</script>
|
||||
|
||||
<div class="flex flex-row gap-2 p-1 rounded-md bg-surface">
|
||||
@@ -41,10 +41,10 @@
|
||||
unifiedSize="sm"
|
||||
wrapperClasses="min-w-36"
|
||||
startIcon={{ icon: Settings }}
|
||||
selected={$selectedId?.startsWith('settings')}
|
||||
selected={selectionManager.getSelectedId()?.startsWith('settings')}
|
||||
variant="default"
|
||||
title="Settings"
|
||||
onClick={() => ($selectedId = 'settings')}
|
||||
onClick={() => selectionManager.selectId('settings')}
|
||||
>
|
||||
Settings
|
||||
{#if flowStore.val.value.same_worker}
|
||||
@@ -64,10 +64,10 @@
|
||||
wrapperClasses="h-full"
|
||||
unifiedSize="sm"
|
||||
startIcon={{ icon: DollarSign }}
|
||||
selected={$selectedId === 'constants'}
|
||||
selected={selectionManager.getSelectedId() === 'constants'}
|
||||
variant="default"
|
||||
iconOnly
|
||||
onClick={() => ($selectedId = 'constants')}
|
||||
onClick={() => selectionManager.selectId('constants')}
|
||||
/>
|
||||
{#snippet text()}
|
||||
Static inputs
|
||||
|
||||
@@ -2,7 +2,6 @@
|
||||
import { Button } from '$lib/components/common'
|
||||
import type { FlowModule, Job } from '$lib/gen'
|
||||
import { createEventDispatcher, getContext } from 'svelte'
|
||||
import type { Writable } from 'svelte/store'
|
||||
import FlowModuleSchemaItem from './FlowModuleSchemaItem.svelte'
|
||||
import FlowModuleIcon from '../FlowModuleIcon.svelte'
|
||||
import { prettyLanguage } from '$lib/common'
|
||||
@@ -17,6 +16,7 @@
|
||||
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
|
||||
@@ -74,9 +74,7 @@
|
||||
maximizeSubflow
|
||||
}: Props = $props()
|
||||
|
||||
const { selectedId } = getContext<{
|
||||
selectedId: Writable<string | undefined>
|
||||
}>('FlowGraphContext')
|
||||
const { selectionManager } = getGraphContext()
|
||||
|
||||
const { flowStore } = getContext<FlowEditorContext | undefined>('FlowEditorContext') || {}
|
||||
|
||||
@@ -88,7 +86,9 @@
|
||||
}>()
|
||||
|
||||
let itemProps = $derived({
|
||||
selected: $selectedId === mod.id,
|
||||
selected:
|
||||
selectionManager?.getSelectedId() === mod.id ||
|
||||
(selectionManager && selectionManager.selectedIds.includes(mod.id)),
|
||||
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),
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import type { Job, OpenFlow } from '$lib/gen'
|
||||
import type { FlowNote, Job, OpenFlow } from '$lib/gen'
|
||||
import type { History } from '$lib/history.svelte'
|
||||
import type { Writable } from 'svelte/store'
|
||||
import type ScriptEditorDrawer from './content/ScriptEditorDrawer.svelte'
|
||||
@@ -15,16 +15,7 @@ import type ResourceEditorDrawer from '../ResourceEditorDrawer.svelte'
|
||||
import type { ModulesTestStates } from '../modulesTest.svelte'
|
||||
import type { ButtonProp } from '$lib/components/DiffEditor.svelte'
|
||||
|
||||
import type { NoteColor } from '../graph/noteColors'
|
||||
|
||||
// Type for flow notes stored in the UI field
|
||||
export type Note = {
|
||||
id: string
|
||||
text: string
|
||||
position: { x: number; y: number }
|
||||
size: { width: number; height: number }
|
||||
color: NoteColor
|
||||
}
|
||||
import type { SelectionManager } from '../graph/selectionUtils.svelte'
|
||||
|
||||
export type FlowInput = Record<
|
||||
string,
|
||||
@@ -47,7 +38,7 @@ export type ExtendedOpenFlow = OpenFlow & {
|
||||
visible_to_runner_only?: boolean
|
||||
on_behalf_of_email?: string
|
||||
ui?: {
|
||||
notes?: Note[]
|
||||
notes?: FlowNote[]
|
||||
}
|
||||
}
|
||||
|
||||
@@ -83,7 +74,7 @@ export type CurrentEditor =
|
||||
| undefined
|
||||
|
||||
export type FlowEditorContext = {
|
||||
selectedId: Writable<string>
|
||||
selectionManager: SelectionManager
|
||||
currentEditor: Writable<CurrentEditor>
|
||||
moving: Writable<{ id: string } | undefined>
|
||||
previewArgs: StateStore<Record<string, any>>
|
||||
|
||||
@@ -1,9 +1,8 @@
|
||||
<script lang="ts">
|
||||
import { FlowService, type FlowModule, type Job } from '../../gen'
|
||||
import { FlowService, type FlowModule, type FlowNote, type Job } from '../../gen'
|
||||
import { NODE, type GraphModuleState } from '.'
|
||||
import type { Note } from '../flows/types'
|
||||
import { DEFAULT_NOTE_COLOR, type NoteColor } from './noteColors'
|
||||
import { getContext, onDestroy, setContext, tick, untrack, type Snippet } from 'svelte'
|
||||
import { getContext, onDestroy, tick, untrack, type Snippet } from 'svelte'
|
||||
|
||||
import { get, writable, type Writable } from 'svelte/store'
|
||||
import '@xyflow/svelte/dist/base.css'
|
||||
@@ -37,7 +36,7 @@
|
||||
import BaseEdge from './renderers/edges/BaseEdge.svelte'
|
||||
import EmptyEdge from './renderers/edges/EmptyEdge.svelte'
|
||||
import { sugiyama, dagStratify, coordCenter, decrossTwoLayer, decrossOpt } from 'd3-dag'
|
||||
import { Expand } from 'lucide-svelte'
|
||||
import { Expand, MousePointer } from 'lucide-svelte'
|
||||
import Toggle from '../Toggle.svelte'
|
||||
import DataflowEdge from './renderers/edges/DataflowEdge.svelte'
|
||||
import { encodeState, readFieldsRecursively } from '$lib/utils'
|
||||
@@ -61,11 +60,15 @@
|
||||
import NewAiToolNode from './renderers/nodes/NewAIToolNode.svelte'
|
||||
import NoteNode from './renderers/nodes/NoteNode.svelte'
|
||||
import NoteTool from './NoteTool.svelte'
|
||||
import SelectionBoundingBox from './SelectionBoundingBox.svelte'
|
||||
import SelectionTool from './SelectionTool.svelte'
|
||||
import { SelectionManager } from './selectionUtils.svelte'
|
||||
import { ChangeTracker } from '$lib/svelte5Utils.svelte'
|
||||
import type { ModulesTestStates } from '../modulesTest.svelte'
|
||||
import { deepEqual } from 'fast-equals'
|
||||
import type { AssetWithAltAccessType } from '../assets/lib'
|
||||
import type { AIModuleAction } from '../copilot/chat/flow/core'
|
||||
import { setGraphContext } from './graphContext'
|
||||
|
||||
let useDataflow: Writable<boolean | undefined> = writable<boolean | undefined>(false)
|
||||
let showAssets: Writable<boolean | undefined> = writable<boolean | undefined>(true)
|
||||
@@ -89,7 +92,7 @@
|
||||
testModuleStates?: ModulesTestStates
|
||||
moduleActions?: Record<string, AIModuleAction>
|
||||
inputSchemaModified?: boolean
|
||||
selectedId?: Writable<string | undefined>
|
||||
selectionManager?: SelectionManager
|
||||
path?: string | undefined
|
||||
newFlow?: boolean
|
||||
insertable?: boolean
|
||||
@@ -113,9 +116,10 @@
|
||||
showJobStatus?: boolean
|
||||
suspendStatus?: Record<string, { job: Job; nb: number }>
|
||||
noteMode?: boolean
|
||||
notes?: Note[]
|
||||
notes?: FlowNote[]
|
||||
chatInputEnabled?: boolean
|
||||
onNotesChange?: (notes: Note[]) => void
|
||||
multiSelectEnabled?: boolean
|
||||
onNotesChange?: (notes: FlowNote[]) => void
|
||||
onDelete?: (id: string) => void
|
||||
onInsert?: (detail: {
|
||||
sourceId?: string
|
||||
@@ -173,7 +177,7 @@
|
||||
testModuleStates = undefined,
|
||||
moduleActions = undefined,
|
||||
inputSchemaModified = undefined,
|
||||
selectedId = writable<string | undefined>(undefined),
|
||||
selectionManager = undefined,
|
||||
path = undefined,
|
||||
newFlow = false,
|
||||
insertable = false,
|
||||
@@ -210,14 +214,18 @@
|
||||
chatInputEnabled = false,
|
||||
sharedViewport = undefined,
|
||||
onViewportChange = undefined,
|
||||
leftHeader = undefined
|
||||
leftHeader = undefined,
|
||||
multiSelectEnabled = false
|
||||
}: Props = $props()
|
||||
|
||||
setContext<{
|
||||
selectedId: Writable<string | undefined>
|
||||
useDataflow: Writable<boolean | undefined>
|
||||
showAssets: Writable<boolean | undefined>
|
||||
}>('FlowGraphContext', { selectedId, useDataflow, showAssets })
|
||||
// Selection manager - create one if not provided
|
||||
let actualSelectionManager = selectionManager || new SelectionManager()
|
||||
|
||||
setGraphContext({
|
||||
selectionManager: actualSelectionManager,
|
||||
useDataflow,
|
||||
showAssets
|
||||
})
|
||||
|
||||
if (triggerContext && allowSimplifiedPoll) {
|
||||
if (isSimplifiable(modules)) {
|
||||
@@ -327,7 +335,7 @@
|
||||
|
||||
let eventHandler = {
|
||||
deleteBranch: (detail, label) => {
|
||||
$selectedId = label
|
||||
actualSelectionManager.selectId(label)
|
||||
onDeleteBranch?.(detail)
|
||||
},
|
||||
insert: (detail) => {
|
||||
@@ -335,9 +343,9 @@
|
||||
},
|
||||
select: (modId) => {
|
||||
if (!notSelectable) {
|
||||
if ($selectedId != modId) {
|
||||
$selectedId = modId
|
||||
}
|
||||
// TODO: Handle Ctrl/Cmd and Shift modifiers when node-level click events are available
|
||||
// For now, normal click behavior
|
||||
actualSelectionManager.selectId(modId)
|
||||
onSelect?.(modId)
|
||||
}
|
||||
},
|
||||
@@ -413,10 +421,19 @@
|
||||
return false
|
||||
}
|
||||
|
||||
// Keyboard event handling
|
||||
function handleKeyDown(event: KeyboardEvent) {
|
||||
actualSelectionManager.handleKeyDown(event, nodes)
|
||||
}
|
||||
|
||||
function handleKeyUp(event: KeyboardEvent) {
|
||||
// Keep for potential future use
|
||||
}
|
||||
|
||||
function onNoteAdded(newNoteFromTool: any) {
|
||||
// Add the note to our separate notes array if a note was created
|
||||
if (newNoteFromTool && onNotesChange) {
|
||||
const newNote: Note = {
|
||||
const newNote: FlowNote = {
|
||||
id: `note-${nextNoteId}`,
|
||||
text: '',
|
||||
position: newNoteFromTool.position,
|
||||
@@ -589,7 +606,7 @@
|
||||
testModuleStates: untrack(() => testModuleStates),
|
||||
moduleActions: untrack(() => moduleActions),
|
||||
inputSchemaModified: untrack(() => inputSchemaModified),
|
||||
selectedId: untrack(() => $selectedId),
|
||||
selectedId: untrack(() => actualSelectionManager.getSelectedId()),
|
||||
path,
|
||||
newFlow,
|
||||
cache,
|
||||
@@ -611,7 +628,7 @@
|
||||
eventHandler,
|
||||
success,
|
||||
$useDataflow,
|
||||
untrack(() => $selectedId),
|
||||
untrack(() => actualSelectionManager.getSelectedId()),
|
||||
moving,
|
||||
simplifiableFlow,
|
||||
triggerNode ? path : undefined,
|
||||
@@ -624,14 +641,44 @@
|
||||
untrack(() => updateStores())
|
||||
})
|
||||
|
||||
// Add global keyboard event listener for selection controls
|
||||
$effect(() => {
|
||||
function globalKeyDownHandler(event: KeyboardEvent) {
|
||||
// Only handle if the graph container has focus or no input is focused
|
||||
const activeElement = document.activeElement
|
||||
const isInputFocused =
|
||||
activeElement &&
|
||||
(activeElement.tagName === 'INPUT' ||
|
||||
activeElement.tagName === 'TEXTAREA' ||
|
||||
(activeElement as HTMLElement).contentEditable === 'true')
|
||||
|
||||
if (!isInputFocused) {
|
||||
handleKeyDown(event)
|
||||
}
|
||||
}
|
||||
|
||||
function globalKeyUpHandler(event: KeyboardEvent) {
|
||||
handleKeyUp(event)
|
||||
}
|
||||
|
||||
document.addEventListener('keydown', globalKeyDownHandler)
|
||||
document.addEventListener('keyup', globalKeyUpHandler)
|
||||
|
||||
return () => {
|
||||
document.removeEventListener('keydown', globalKeyDownHandler)
|
||||
document.removeEventListener('keyup', globalKeyUpHandler)
|
||||
}
|
||||
})
|
||||
|
||||
let showDataflow = $derived(
|
||||
$selectedId != undefined &&
|
||||
!$selectedId.startsWith('constants') &&
|
||||
!$selectedId.startsWith('settings') &&
|
||||
$selectedId !== 'failure' &&
|
||||
$selectedId !== 'preprocessor' &&
|
||||
$selectedId !== 'Result' &&
|
||||
$selectedId !== 'triggers'
|
||||
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'
|
||||
)
|
||||
let debouncedWidth: number | undefined = $state(undefined)
|
||||
let timeout: number | undefined = $state(undefined)
|
||||
@@ -667,6 +714,8 @@
|
||||
export function zoomOut() {
|
||||
viewportSynchronizer?.zoomOut()
|
||||
}
|
||||
|
||||
$inspect('dbg modules', modules, nodes)
|
||||
</script>
|
||||
|
||||
{#if insertable}
|
||||
@@ -674,7 +723,7 @@
|
||||
{/if}
|
||||
<div
|
||||
style={`height: ${height}px; max-height: ${maxHeight}px;`}
|
||||
class="overflow-clip"
|
||||
class="overflow-clip relative"
|
||||
bind:clientWidth={debouncedWidth}
|
||||
>
|
||||
{#if graph?.error}
|
||||
@@ -703,6 +752,9 @@
|
||||
<SvelteFlow
|
||||
onpaneclick={() => {
|
||||
document.dispatchEvent(new Event('focus'))
|
||||
if (actualSelectionManager.mode === 'normal') {
|
||||
actualSelectionManager.clearSelection()
|
||||
}
|
||||
}}
|
||||
onnodedragstop={(event) => {
|
||||
const node = event.targetNode
|
||||
@@ -736,12 +788,46 @@
|
||||
<NoteTool {onNoteAdded} />
|
||||
{/if}
|
||||
|
||||
<SelectionBoundingBox
|
||||
selectedNodes={nodes.filter((node) =>
|
||||
actualSelectionManager.selectedIds.includes(node.id)
|
||||
)}
|
||||
/>
|
||||
|
||||
<SelectionTool
|
||||
selectionMode={actualSelectionManager.mode}
|
||||
onNodesSelected={(nodeIds, addToExisting) =>
|
||||
actualSelectionManager.selectNodes(nodeIds, addToExisting, modules, nodes)}
|
||||
{nodes}
|
||||
/>
|
||||
|
||||
{#if leftHeader}
|
||||
<div class="absolute top-2 left-2 z-10">
|
||||
{@render leftHeader()}
|
||||
</div>
|
||||
{:else}
|
||||
<Controls position="top-right" orientation="horizontal" showLock={false}>
|
||||
{#if multiSelectEnabled}
|
||||
<div class="flex items-center gap-2">
|
||||
<ControlButton
|
||||
onclick={() => {
|
||||
actualSelectionManager.mode =
|
||||
actualSelectionManager.mode === 'normal' ? 'rect-select' : 'normal'
|
||||
}}
|
||||
title="Toggle rectangle selection"
|
||||
class={actualSelectionManager.mode === 'rect-select'
|
||||
? 'text-accent !bg-surface-selected'
|
||||
: ''}
|
||||
>
|
||||
<MousePointer size="14" />
|
||||
</ControlButton>
|
||||
{#if actualSelectionManager.selectedIds.length > 0}
|
||||
<span class="text-xs text-secondary"
|
||||
>{actualSelectionManager.selectedIds.length} selected</span
|
||||
>
|
||||
{/if}
|
||||
</div>
|
||||
{/if}
|
||||
{#if download}
|
||||
<ControlButton
|
||||
onclick={() => {
|
||||
|
||||
@@ -0,0 +1,53 @@
|
||||
<script lang="ts">
|
||||
import type { Node } from '@xyflow/svelte'
|
||||
import { NODE } from './util'
|
||||
|
||||
interface Props {
|
||||
selectedNodes: Node[]
|
||||
}
|
||||
|
||||
let { selectedNodes }: Props = $props()
|
||||
|
||||
let bounds = $derived(() => {
|
||||
if (selectedNodes.length === 0) {
|
||||
return null
|
||||
}
|
||||
|
||||
let minX = Infinity
|
||||
let maxX = -Infinity
|
||||
let minY = Infinity
|
||||
let maxY = -Infinity
|
||||
|
||||
selectedNodes.forEach(node => {
|
||||
minX = Math.min(minX, node.position.x)
|
||||
maxX = Math.max(maxX, node.position.x + NODE.width)
|
||||
minY = Math.min(minY, node.position.y)
|
||||
maxY = Math.max(maxY, node.position.y + NODE.height)
|
||||
})
|
||||
|
||||
return {
|
||||
x: minX - 10, // Add padding
|
||||
y: minY - 10,
|
||||
width: maxX - minX + 20,
|
||||
height: maxY - minY + 20
|
||||
}
|
||||
})
|
||||
</script>
|
||||
|
||||
{#if bounds() && selectedNodes.length > 1}
|
||||
{@const currentBounds = bounds()!}
|
||||
<div
|
||||
class="absolute pointer-events-none border-2 border-dashed border-accent bg-accent/5 rounded"
|
||||
style="
|
||||
left: {currentBounds.x}px;
|
||||
top: {currentBounds.y}px;
|
||||
width: {currentBounds.width}px;
|
||||
height: {currentBounds.height}px;
|
||||
z-index: -1;
|
||||
"
|
||||
>
|
||||
<div class="absolute -top-6 left-0 text-xs text-accent font-medium bg-surface px-2 py-1 rounded shadow">
|
||||
{selectedNodes.length} nodes selected
|
||||
</div>
|
||||
</div>
|
||||
{/if}
|
||||
@@ -0,0 +1,180 @@
|
||||
<script lang="ts">
|
||||
import { useSvelteFlow, type XYPosition } from '@xyflow/svelte'
|
||||
import { NODE } from './util'
|
||||
|
||||
interface Props {
|
||||
selectionMode: 'normal' | 'rect-select'
|
||||
onNodesSelected: (nodeIds: string[], addToExisting: boolean) => void
|
||||
nodes: any[]
|
||||
}
|
||||
|
||||
let { selectionMode, onNodesSelected, nodes }: Props = $props()
|
||||
|
||||
const { screenToFlowPosition } = useSvelteFlow()
|
||||
|
||||
let isDrawing = $state(false)
|
||||
let startPosition: XYPosition | null = $state(null)
|
||||
let endPosition: XYPosition | null = $state(null)
|
||||
let rect: DOMRect | null = $state(null)
|
||||
|
||||
function onPointerDown(event: PointerEvent) {
|
||||
if (selectionMode !== 'rect-select') return
|
||||
|
||||
// Allow middle-click (button 1) to pass through for graph panning
|
||||
if (event.button === 1) {
|
||||
return
|
||||
}
|
||||
|
||||
// Only handle left-click (button 0) for rectangle selection
|
||||
if (event.button !== 0) return
|
||||
|
||||
// Capture pointer to continue tracking outside the element
|
||||
const target = event.currentTarget as Element
|
||||
target?.setPointerCapture?.(event.pointerId)
|
||||
|
||||
// Use page coordinates as reference
|
||||
rect = target.getBoundingClientRect()
|
||||
startPosition = {
|
||||
x: event.pageX - rect.left,
|
||||
y: event.pageY - rect.top
|
||||
}
|
||||
endPosition = startPosition
|
||||
isDrawing = true
|
||||
event.preventDefault()
|
||||
}
|
||||
|
||||
function onPointerMove(event: PointerEvent) {
|
||||
if (!isDrawing || !rect) return
|
||||
|
||||
// Use page coordinates as reference
|
||||
endPosition = {
|
||||
x: event.pageX - rect.left,
|
||||
y: event.pageY - rect.top
|
||||
}
|
||||
}
|
||||
|
||||
function onPointerUp(event: PointerEvent) {
|
||||
if (!isDrawing || !startPosition || !endPosition || !rect) return
|
||||
|
||||
// Only proceed if we have a meaningful selection area
|
||||
const deltaX = Math.abs(endPosition.x - startPosition.x)
|
||||
const deltaY = Math.abs(endPosition.y - startPosition.y)
|
||||
|
||||
if (deltaX > 5 || deltaY > 5) {
|
||||
// Convert the start and end positions to absolute positions
|
||||
const absoluteStartPosition = {
|
||||
x: startPosition.x + rect.left,
|
||||
y: startPosition.y + rect.top
|
||||
}
|
||||
const absoluteEndPosition = {
|
||||
x: endPosition.x + rect.left,
|
||||
y: endPosition.y + rect.top
|
||||
}
|
||||
|
||||
// Convert to flow coordinates
|
||||
const flowStart = screenToFlowPosition({
|
||||
x: Math.min(absoluteStartPosition.x, absoluteEndPosition.x),
|
||||
y: Math.min(absoluteStartPosition.y, absoluteEndPosition.y)
|
||||
})
|
||||
|
||||
const flowEnd = screenToFlowPosition({
|
||||
x: Math.max(absoluteStartPosition.x, absoluteEndPosition.x),
|
||||
y: Math.max(absoluteStartPosition.y, absoluteEndPosition.y)
|
||||
})
|
||||
|
||||
// Find nodes within the selection rectangle
|
||||
const selectedNodeIds = getNodesInFlowRectangle({
|
||||
x1: flowStart.x,
|
||||
y1: flowStart.y,
|
||||
x2: flowEnd.x,
|
||||
y2: flowEnd.y
|
||||
})
|
||||
|
||||
if (selectedNodeIds.length > 0) {
|
||||
onNodesSelected(selectedNodeIds, event.shiftKey)
|
||||
}
|
||||
}
|
||||
|
||||
// Reset state
|
||||
isDrawing = false
|
||||
startPosition = null
|
||||
endPosition = null
|
||||
rect = null
|
||||
}
|
||||
|
||||
function getNodesInFlowRectangle(rect: {
|
||||
x1: number
|
||||
y1: number
|
||||
x2: number
|
||||
y2: number
|
||||
}): string[] {
|
||||
const minX = Math.min(rect.x1, rect.x2)
|
||||
const maxX = Math.max(rect.x1, rect.x2)
|
||||
const minY = Math.min(rect.y1, rect.y2)
|
||||
const maxY = Math.max(rect.y1, rect.y2)
|
||||
|
||||
return nodes
|
||||
.filter((node) => {
|
||||
const nodeMinX = node.position.x
|
||||
const nodeMaxX = node.position.x + NODE.width
|
||||
const nodeMinY = node.position.y
|
||||
const nodeMaxY = node.position.y + NODE.height
|
||||
|
||||
// Check if node intersects with selection rectangle
|
||||
return !(nodeMaxX < minX || nodeMinX > maxX || nodeMaxY < minY || nodeMinY > maxY)
|
||||
})
|
||||
.map((node) => node.id)
|
||||
}
|
||||
|
||||
const previewNote = $derived.by(() => {
|
||||
if (!startPosition || !endPosition) return null
|
||||
return {
|
||||
position: {
|
||||
x: Math.min(startPosition.x, endPosition.x),
|
||||
y: Math.min(startPosition.y, endPosition.y)
|
||||
},
|
||||
size: {
|
||||
width: Math.abs(endPosition.x - startPosition.x),
|
||||
height: Math.abs(endPosition.y - startPosition.y)
|
||||
}
|
||||
}
|
||||
})
|
||||
</script>
|
||||
|
||||
{#if selectionMode === 'rect-select'}
|
||||
<div
|
||||
class="selection-overlay"
|
||||
onpointerdown={onPointerDown}
|
||||
onpointermove={onPointerMove}
|
||||
onpointerup={onPointerUp}
|
||||
role="button"
|
||||
tabindex="0"
|
||||
aria-label="Click and drag to select nodes"
|
||||
>
|
||||
<!-- Preview selection rectangle while drawing -->
|
||||
{#if previewNote && isDrawing}
|
||||
<div
|
||||
class="absolute border-2 border-dashed border-accent bg-accent/10 rounded pointer-events-none"
|
||||
style="
|
||||
width: {previewNote.size.width}px;
|
||||
height: {previewNote.size.height}px;
|
||||
transform: translate({previewNote.position.x}px, {previewNote.position.y}px);
|
||||
"
|
||||
></div>
|
||||
{/if}
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
<style>
|
||||
.selection-overlay {
|
||||
pointer-events: auto;
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
z-index: 50;
|
||||
height: 100%;
|
||||
width: 100%;
|
||||
cursor: crosshair;
|
||||
touch-action: none;
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,15 @@
|
||||
import { getContext, setContext } from 'svelte'
|
||||
import type { SelectionManager } from './selectionUtils.svelte'
|
||||
import type { Writable } from 'svelte/store'
|
||||
|
||||
type GraphContext = {
|
||||
selectionManager: SelectionManager
|
||||
useDataflow: Writable<boolean | undefined>
|
||||
showAssets: Writable<boolean | undefined>
|
||||
}
|
||||
|
||||
const graphContextKey = 'FlowGraphContext'
|
||||
|
||||
//TODO: use https://svelte.dev/docs/svelte/context#Type-safe-context after migrating svelte 5 to latest version
|
||||
export const getGraphContext = () => getContext<GraphContext>(graphContextKey)
|
||||
export const setGraphContext = (context: GraphContext) => setContext(graphContextKey, context)
|
||||
@@ -2,8 +2,6 @@
|
||||
import InsertModulePopover from '$lib/components/flows/map/InsertModulePopover.svelte'
|
||||
import { getBezierPath, BaseEdge, type EdgeProps, EdgeLabel } from '@xyflow/svelte'
|
||||
import { ClipboardCopy, Hourglass } from 'lucide-svelte'
|
||||
import { getContext } from 'svelte'
|
||||
import type { Writable } from 'svelte/store'
|
||||
import type { GraphEventHandlers } from '../../graphBuilder.svelte'
|
||||
import { getStraightLinePath } from '../utils'
|
||||
import { twMerge } from 'tailwind-merge'
|
||||
@@ -13,11 +11,9 @@
|
||||
import type { Job } from '$lib/gen'
|
||||
import type { GraphModuleState } from '../../model'
|
||||
import InsertModuleButton from '$lib/components/flows/map/InsertModuleButton.svelte'
|
||||
import { getGraphContext } from '../../graphContext'
|
||||
|
||||
const { useDataflow, showAssets } = getContext<{
|
||||
useDataflow: Writable<boolean | undefined>
|
||||
showAssets?: Writable<boolean>
|
||||
}>('FlowGraphContext')
|
||||
const { useDataflow, showAssets } = getGraphContext()
|
||||
|
||||
let {
|
||||
// id,
|
||||
|
||||
@@ -1,8 +1,7 @@
|
||||
<script lang="ts">
|
||||
import { getBezierPath, BaseEdge, type Position } from '@xyflow/svelte'
|
||||
import { getContext } from 'svelte'
|
||||
import type { Writable } from 'svelte/store'
|
||||
import { twMerge } from 'tailwind-merge'
|
||||
import { getGraphContext } from '../../graphContext'
|
||||
|
||||
interface Props {
|
||||
sourceX: number
|
||||
@@ -26,9 +25,7 @@
|
||||
data = {}
|
||||
}: Props = $props()
|
||||
|
||||
const { useDataflow } = getContext<{
|
||||
useDataflow: Writable<boolean | undefined>
|
||||
}>('FlowGraphContext')
|
||||
const { useDataflow } = getGraphContext()
|
||||
|
||||
let [edgePath] = $derived(
|
||||
getBezierPath({
|
||||
|
||||
@@ -253,13 +253,12 @@
|
||||
} from '../../graphBuilder.svelte'
|
||||
import { MessageCircle, Play, Plug, Wrench, X } from 'lucide-svelte'
|
||||
import { twMerge } from 'tailwind-merge'
|
||||
import { getContext } from 'svelte'
|
||||
import type { Edge, Node } from '@xyflow/svelte'
|
||||
|
||||
import type { Writable } from 'svelte/store'
|
||||
import type { GraphModuleState } from '../../model'
|
||||
import { getNodeColorClasses } from '../../util'
|
||||
import { deepEqual } from 'fast-equals'
|
||||
import { getGraphContext } from '../../graphContext'
|
||||
|
||||
let hover = $state(false)
|
||||
|
||||
@@ -269,15 +268,13 @@
|
||||
|
||||
let { data }: Props = $props()
|
||||
|
||||
const { selectedId } = getContext<{
|
||||
selectedId: Writable<string | undefined>
|
||||
}>('FlowGraphContext')
|
||||
const { selectionManager } = getGraphContext()
|
||||
|
||||
const flowModuleState = $derived(data.flowModuleStates?.[data.moduleId])
|
||||
let colorClasses = $derived(
|
||||
getNodeColorClasses(
|
||||
!validateToolName(data.tool) ? 'Failure' : flowModuleState?.type,
|
||||
$selectedId === data.moduleId
|
||||
selectionManager?.getSelectedId() === data.moduleId
|
||||
)
|
||||
)
|
||||
</script>
|
||||
@@ -322,7 +319,7 @@
|
||||
<button
|
||||
class={twMerge(
|
||||
'absolute -top-[8px] -right-[8px] rounded-full h-[16px] w-[16px] center-center text-secondary outline-[1px] outline dark:outline-gray-500 outline-gray-300 bg-surface duration-0 hover:bg-red-400 hover:text-white !hidden',
|
||||
$selectedId === data.moduleId || hover ? '!flex' : ''
|
||||
selectionManager?.getSelectedId() === data.moduleId || hover ? '!flex' : ''
|
||||
)}
|
||||
title="Delete"
|
||||
onclick={() => data.eventHandlers.delete({ id: data.moduleId }, '')}
|
||||
|
||||
@@ -2,12 +2,14 @@
|
||||
import VirtualItem from '$lib/components/flows/map/VirtualItem.svelte'
|
||||
import NodeWrapper from './NodeWrapper.svelte'
|
||||
import type { BranchAllEndN } from '../../graphBuilder.svelte'
|
||||
|
||||
import { getGraphContext } from '../../graphContext'
|
||||
interface Props {
|
||||
data: BranchAllEndN['data']
|
||||
}
|
||||
|
||||
let { data }: Props = $props()
|
||||
|
||||
const { selectionManager } = getGraphContext()
|
||||
</script>
|
||||
|
||||
<NodeWrapper offset={data.offset} enableSourceHandle enableTargetHandle>
|
||||
@@ -16,7 +18,7 @@
|
||||
label={'Collect result from all branches'}
|
||||
id={data.id}
|
||||
selectable={true}
|
||||
selected={false}
|
||||
selected={selectionManager?.isNodeSelected(data.id)}
|
||||
on:select={(e) => {
|
||||
data?.eventHandlers?.select(e.detail)
|
||||
}}
|
||||
|
||||
@@ -5,12 +5,14 @@
|
||||
import NodeWrapper from './NodeWrapper.svelte'
|
||||
import { X } from 'lucide-svelte'
|
||||
import type { BranchAllStartN } from '../../graphBuilder.svelte'
|
||||
|
||||
import { getGraphContext } from '../../graphContext'
|
||||
interface Props {
|
||||
data: BranchAllStartN['data']
|
||||
}
|
||||
|
||||
let { data }: Props = $props()
|
||||
|
||||
const { selectionManager } = getGraphContext()
|
||||
</script>
|
||||
|
||||
<NodeWrapper offset={data.offset}>
|
||||
@@ -18,7 +20,7 @@
|
||||
<VirtualItem
|
||||
label={data.label}
|
||||
selectable
|
||||
selected={false}
|
||||
selected={selectionManager?.isNodeSelected(data.id)}
|
||||
on:select={() => {
|
||||
setTimeout(() => data.eventHandlers.select(data.id))
|
||||
}}
|
||||
|
||||
@@ -5,12 +5,14 @@
|
||||
import NodeWrapper from './NodeWrapper.svelte'
|
||||
import { X } from 'lucide-svelte'
|
||||
import type { BranchOneStartN } from '../../graphBuilder.svelte'
|
||||
|
||||
import { getGraphContext } from '../../graphContext'
|
||||
interface Props {
|
||||
data: BranchOneStartN['data']
|
||||
}
|
||||
|
||||
let { data }: Props = $props()
|
||||
|
||||
const { selectionManager } = getGraphContext()
|
||||
</script>
|
||||
|
||||
<NodeWrapper offset={data.offset}>
|
||||
@@ -19,7 +21,7 @@
|
||||
label={data.label}
|
||||
preLabel={data.preLabel}
|
||||
selectable
|
||||
selected={data.selected}
|
||||
selected={selectionManager?.isNodeSelected(data.id)}
|
||||
on:select={() => {
|
||||
setTimeout(() => data?.eventHandlers?.select(data.id))
|
||||
}}
|
||||
|
||||
@@ -2,12 +2,14 @@
|
||||
import VirtualItem from '$lib/components/flows/map/VirtualItem.svelte'
|
||||
import NodeWrapper from './NodeWrapper.svelte'
|
||||
import type { ForLoopEndN } from '../../graphBuilder.svelte'
|
||||
|
||||
import { getGraphContext } from '../../graphContext'
|
||||
interface Props {
|
||||
data: ForLoopEndN['data']
|
||||
}
|
||||
|
||||
let { data }: Props = $props()
|
||||
|
||||
const { selectionManager } = getGraphContext()
|
||||
</script>
|
||||
|
||||
<NodeWrapper offset={data.offset}>
|
||||
@@ -16,7 +18,7 @@
|
||||
<VirtualItem
|
||||
label={'Each event is processed'}
|
||||
selectable={false}
|
||||
selected={false}
|
||||
selected={selectionManager?.isNodeSelected(data.id)}
|
||||
id={data.id}
|
||||
hideId
|
||||
on:select={(e) => {
|
||||
@@ -27,7 +29,7 @@
|
||||
<VirtualItem
|
||||
label={'Collect result of each iteration'}
|
||||
selectable={true}
|
||||
selected={false}
|
||||
selected={selectionManager?.isNodeSelected(data.id)}
|
||||
id={data.id}
|
||||
on:select={(e) => {
|
||||
setTimeout(() => data?.eventHandlers?.select(e.detail))
|
||||
|
||||
@@ -5,7 +5,7 @@
|
||||
import { getContext } from 'svelte'
|
||||
import type { PropPickerContext } from '$lib/components/prop_picker'
|
||||
import type { ForLoopStartN } from '../../graphBuilder.svelte'
|
||||
|
||||
import { getGraphContext } from '../../graphContext'
|
||||
interface Props {
|
||||
data: ForLoopStartN['data']
|
||||
}
|
||||
@@ -51,6 +51,8 @@
|
||||
return 'none'
|
||||
}
|
||||
let filteredInput = $derived(filterIterFromInput($pickablePropertiesFiltered?.flow_input))
|
||||
|
||||
const { selectionManager } = getGraphContext()
|
||||
</script>
|
||||
|
||||
<NodeWrapper offset={data.offset}>
|
||||
@@ -58,7 +60,7 @@
|
||||
<VirtualItem
|
||||
label={data.simplifiedTriggerView ? 'For each new event' : 'Do one iteration'}
|
||||
selectable={false}
|
||||
selected={false}
|
||||
selected={selectionManager?.isNodeSelected(data.id)}
|
||||
id={data.id}
|
||||
hideId
|
||||
on:select={(e) => {
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
import NodeWrapper from './NodeWrapper.svelte'
|
||||
import type { InputN } from '../../graphBuilder.svelte'
|
||||
import { getContext } from 'svelte'
|
||||
import type { Writable } from 'svelte/store'
|
||||
|
||||
import InsertModulePopover from '$lib/components/flows/map/InsertModulePopover.svelte'
|
||||
import InsertModuleButton from '$lib/components/flows/map/InsertModuleButton.svelte'
|
||||
import { schemaToObject } from '$lib/schema'
|
||||
@@ -11,6 +11,7 @@
|
||||
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']
|
||||
@@ -18,9 +19,7 @@
|
||||
|
||||
let { data }: Props = $props()
|
||||
|
||||
const { selectedId } = getContext<{
|
||||
selectedId: Writable<string | undefined>
|
||||
}>('FlowGraphContext')
|
||||
const { selectionManager } = getGraphContext()
|
||||
|
||||
const { previewArgs, flowStore } =
|
||||
getContext<FlowEditorContext | undefined>('FlowEditorContext') || {}
|
||||
@@ -82,7 +81,7 @@
|
||||
hideId={true}
|
||||
label={inputLabel}
|
||||
selectable
|
||||
selected={$selectedId === 'Input'}
|
||||
selected={selectionManager?.isNodeSelected('Input')}
|
||||
on:insert={(e) => {
|
||||
setTimeout(() => data?.eventHandlers?.insert(e.detail))
|
||||
}}
|
||||
|
||||
@@ -2,12 +2,14 @@
|
||||
import VirtualItem from '$lib/components/flows/map/VirtualItem.svelte'
|
||||
import NodeWrapper from './NodeWrapper.svelte'
|
||||
import type { NoBranchN } from '../../graphBuilder.svelte'
|
||||
|
||||
import { getGraphContext } from '../../graphContext'
|
||||
interface Props {
|
||||
data: NoBranchN['data']
|
||||
}
|
||||
|
||||
let { data }: Props = $props()
|
||||
|
||||
const { selectionManager } = getGraphContext()
|
||||
</script>
|
||||
|
||||
<NodeWrapper offset={data.offset} enableSourceHandle enableTargetHandle>
|
||||
@@ -17,7 +19,7 @@
|
||||
id={data.id}
|
||||
hideId={true}
|
||||
selectable={true}
|
||||
selected={false}
|
||||
selected={selectionManager?.isNodeSelected(data.id)}
|
||||
on:select={(e) => {
|
||||
setTimeout(() => data?.eventHandlers?.select(e.detail))
|
||||
}}
|
||||
|
||||
@@ -1,9 +1,8 @@
|
||||
<script lang="ts">
|
||||
import VirtualItem from '$lib/components/flows/map/VirtualItem.svelte'
|
||||
import NodeWrapper from './NodeWrapper.svelte'
|
||||
import type { Writable } from 'svelte/store'
|
||||
import { getContext } from 'svelte'
|
||||
import type { ResultN } from '../../graphBuilder.svelte'
|
||||
import { getGraphContext } from '../../graphContext'
|
||||
|
||||
interface Props {
|
||||
data: ResultN['data']
|
||||
@@ -11,9 +10,7 @@
|
||||
|
||||
let { data }: Props = $props()
|
||||
|
||||
const { selectedId } = getContext<{
|
||||
selectedId: Writable<string | undefined>
|
||||
}>('FlowGraphContext')
|
||||
const { selectionManager } = getGraphContext()
|
||||
</script>
|
||||
|
||||
<NodeWrapper enableSourceHandle={false}>
|
||||
@@ -22,7 +19,7 @@
|
||||
id={'Result'}
|
||||
label={'Result'}
|
||||
selectable={true}
|
||||
selected={$selectedId === 'Result'}
|
||||
selected={selectionManager?.getSelectedId() === 'Result'}
|
||||
hideId={true}
|
||||
on:select={(e) => {
|
||||
setTimeout(() => data?.eventHandlers?.select(e.detail))
|
||||
|
||||
@@ -1,9 +1,10 @@
|
||||
<script lang="ts">
|
||||
import { preventDefault, stopPropagation } from 'svelte/legacy'
|
||||
|
||||
import NodeWrapper from './NodeWrapper.svelte'
|
||||
import TriggersWrapper from '../triggers/TriggersWrapper.svelte'
|
||||
import type { FlowModule, TriggersCount } from '$lib/gen'
|
||||
import { getContext } from 'svelte'
|
||||
import type { Writable } from 'svelte/store'
|
||||
import { Maximize2, Minimize2, Calendar } from 'lucide-svelte'
|
||||
import { getNodeColorClasses } from '../../util'
|
||||
import { setScheduledPollSchedule, type TriggerContext } from '$lib/components/triggers'
|
||||
@@ -11,24 +12,27 @@
|
||||
import { type Trigger, type TriggerType } from '$lib/components/triggers/utils'
|
||||
import { tick } from 'svelte'
|
||||
import type { GraphEventHandlers, SimplifiableFlow } from '../../graphBuilder.svelte'
|
||||
import { getGraphContext } from '../../graphContext'
|
||||
|
||||
export let data: {
|
||||
path: string
|
||||
isEditor: boolean
|
||||
newFlow: boolean
|
||||
extra_perms: Record<string, any>
|
||||
eventHandlers: GraphEventHandlers
|
||||
modules: FlowModule[]
|
||||
index: number
|
||||
disableAi: boolean
|
||||
simplifiableFlow: SimplifiableFlow
|
||||
interface Props {
|
||||
data: {
|
||||
path: string
|
||||
isEditor: boolean
|
||||
newFlow: boolean
|
||||
extra_perms: Record<string, any>
|
||||
eventHandlers: GraphEventHandlers
|
||||
modules: FlowModule[]
|
||||
index: number
|
||||
disableAi: boolean
|
||||
simplifiableFlow: SimplifiableFlow
|
||||
}
|
||||
}
|
||||
|
||||
const { selectedId } = getContext<{
|
||||
selectedId: Writable<string | undefined>
|
||||
}>('FlowGraphContext')
|
||||
let { data }: Props = $props()
|
||||
|
||||
const { triggersCount, triggersState } = getContext<TriggerContext>('TriggerContext')
|
||||
const { selectionManager } = getGraphContext()
|
||||
|
||||
const { triggersCount, triggersState } = $state(getContext<TriggerContext>('TriggerContext'))
|
||||
|
||||
function getScheduleCfg(primary: Trigger | undefined, triggersCount: TriggersCount | undefined) {
|
||||
return primary?.draftConfig
|
||||
@@ -44,7 +48,9 @@
|
||||
}
|
||||
}
|
||||
|
||||
$: colorClasses = getNodeColorClasses('_VirtualItem', $selectedId == 'triggers')
|
||||
let colorClasses = $derived(
|
||||
getNodeColorClasses('_VirtualItem', selectionManager?.isNodeSelected('Trigger'))
|
||||
)
|
||||
</script>
|
||||
|
||||
<NodeWrapper>
|
||||
@@ -76,26 +82,26 @@
|
||||
const primarySchedule = triggersState.triggers.findIndex((t) => t.isPrimary && !t.isDraft)
|
||||
triggersState.selectedTriggerIndex = primarySchedule
|
||||
}}
|
||||
on:select={() => data?.eventHandlers?.select('triggers')}
|
||||
on:select={() => data?.eventHandlers?.select('Trigger')}
|
||||
onSelect={async (triggerIndex: number) => {
|
||||
data?.eventHandlers?.select('triggers')
|
||||
data?.eventHandlers?.select('Trigger')
|
||||
await tick()
|
||||
triggersState.selectedTriggerIndex = triggerIndex
|
||||
}}
|
||||
onAddDraftTrigger={async (type: TriggerType) => {
|
||||
const newTrigger = triggersState.addDraftTrigger(triggersCount, type)
|
||||
data?.eventHandlers?.select('triggers')
|
||||
data?.eventHandlers?.select('Trigger')
|
||||
await tick()
|
||||
triggersState.selectedTriggerIndex = newTrigger
|
||||
}}
|
||||
selected={$selectedId == 'triggers'}
|
||||
selected={selectionManager?.getSelectedId() === 'Trigger'}
|
||||
newItem={data.newFlow}
|
||||
/>
|
||||
{:else}
|
||||
<VirtualItemWrapper
|
||||
label="Check for new events"
|
||||
selectable={true}
|
||||
id={'triggers'}
|
||||
id={'Trigger'}
|
||||
on:select={(e) => {
|
||||
data?.eventHandlers?.select(e.detail)
|
||||
}}
|
||||
@@ -116,7 +122,7 @@
|
||||
{:else}
|
||||
<button
|
||||
class="px-2 py-1 hover:bg-surface-inverse w-full hover:text-primary-inverse"
|
||||
on:click={() => {
|
||||
onclick={() => {
|
||||
setScheduledPollSchedule(triggersState, triggersCount)
|
||||
}}
|
||||
>
|
||||
@@ -129,8 +135,11 @@
|
||||
<button
|
||||
class="absolute -top-[10px] -right-[10px] rounded-full h-[20px] w-[20px] trash center-center text-secondary
|
||||
outline-[1px] outline dark:outline-gray-500 outline-gray-300 bg-surface duration-0 hover:bg-nord-950 hover:text-white"
|
||||
on:click|preventDefault|stopPropagation={() =>
|
||||
data?.eventHandlers?.simplifyFlow(!data.simplifiableFlow?.simplifiedFlow)}
|
||||
onclick={stopPropagation(
|
||||
preventDefault(() =>
|
||||
data?.eventHandlers?.simplifyFlow(!data.simplifiableFlow?.simplifiedFlow)
|
||||
)
|
||||
)}
|
||||
title={data.simplifiableFlow?.simplifiedFlow
|
||||
? 'Expand to full flow view'
|
||||
: 'Simplify flow view for scheduled poll'}
|
||||
|
||||
@@ -2,12 +2,14 @@
|
||||
import VirtualItem from '$lib/components/flows/map/VirtualItem.svelte'
|
||||
import NodeWrapper from './NodeWrapper.svelte'
|
||||
import type { BranchOneEndN } from '../../graphBuilder.svelte'
|
||||
|
||||
import { getGraphContext } from '../../graphContext'
|
||||
interface Props {
|
||||
data: BranchOneEndN['data']
|
||||
}
|
||||
|
||||
let { data }: Props = $props()
|
||||
|
||||
const { selectionManager } = getGraphContext()
|
||||
</script>
|
||||
|
||||
<NodeWrapper offset={data.offset}>
|
||||
@@ -16,7 +18,7 @@
|
||||
label={'Collect result from chosen branch'}
|
||||
id={data.id}
|
||||
selectable={true}
|
||||
selected={false}
|
||||
selected={selectionManager?.isNodeSelected(data.id)}
|
||||
on:select={(e) => {
|
||||
setTimeout(() => data?.eventHandlers?.select(e.detail))
|
||||
}}
|
||||
|
||||
@@ -0,0 +1,194 @@
|
||||
import type { FlowModule } from '$lib/gen'
|
||||
import type { Node } from '@xyflow/svelte'
|
||||
|
||||
export interface SelectionState {
|
||||
selectedId: string | undefined
|
||||
selectedIds: string[]
|
||||
selectionMode: 'normal' | 'rect-select'
|
||||
}
|
||||
|
||||
export class SelectionManager {
|
||||
public selectedIds = $state<string[]>([])
|
||||
#selectionMode = $state<'normal' | 'rect-select'>('normal')
|
||||
|
||||
constructor() {}
|
||||
|
||||
selectId(id: string) {
|
||||
this.selectedIds = [id]
|
||||
}
|
||||
|
||||
getSelectedId(): string | undefined {
|
||||
return this.selectedIds[0] || undefined
|
||||
}
|
||||
|
||||
get mode() {
|
||||
return this.#selectionMode
|
||||
}
|
||||
|
||||
set mode(mode: 'normal' | 'rect-select') {
|
||||
this.#selectionMode = mode
|
||||
if (mode === 'normal') {
|
||||
// When exiting rect mode, preserve the first item if there are selections
|
||||
const firstSelected = this.selectedIds[0]
|
||||
this.selectId(firstSelected ?? 'settings')
|
||||
}
|
||||
}
|
||||
|
||||
// Get hierarchical children of a node
|
||||
getNodeChildrenIds(nodeId: string, modules: FlowModule[] | undefined, nodes: Node[]): string[] {
|
||||
const module = modules?.find((m) => m.id === nodeId)
|
||||
if (!module) return []
|
||||
|
||||
const childrenIds: string[] = []
|
||||
|
||||
// For hierarchical modules, find all children between start and end using proper graph traversal
|
||||
if (
|
||||
module.value.type === 'forloopflow' ||
|
||||
module.value.type === 'whileloopflow' ||
|
||||
module.value.type === 'branchall' ||
|
||||
module.value.type === 'branchone'
|
||||
) {
|
||||
const endNodeId = `${nodeId}-end`
|
||||
const endNode = nodes.find((n) => n.id === endNodeId)
|
||||
|
||||
if (endNode) {
|
||||
// Traverse from end node back to start using parentIds
|
||||
const visited = new Set<string>()
|
||||
const toVisit = [endNodeId]
|
||||
|
||||
while (toVisit.length > 0) {
|
||||
const currentId = toVisit.shift()!
|
||||
if (visited.has(currentId) || currentId === nodeId) continue
|
||||
|
||||
visited.add(currentId)
|
||||
const currentNode = nodes.find((n) => n.id === currentId)
|
||||
|
||||
if (currentNode && (currentNode as any).parentIds) {
|
||||
const parentIds = (currentNode as any).parentIds as string[]
|
||||
for (const parentId of parentIds) {
|
||||
if (parentId !== nodeId && !visited.has(parentId)) {
|
||||
childrenIds.push(parentId)
|
||||
toVisit.push(parentId)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return childrenIds
|
||||
}
|
||||
|
||||
// Select nodes with optional hierarchical selection
|
||||
selectNodes(nodeIds: string[], addToExisting = false, modules?: FlowModule[], nodes?: Node[]) {
|
||||
// Guard against empty nodeIds or uninitialized state
|
||||
if (!nodeIds || nodeIds.length === 0) {
|
||||
if (!addToExisting) {
|
||||
this.clearSelection()
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
const newSelection = addToExisting ? [...this.selectedIds] : []
|
||||
|
||||
nodeIds.forEach((nodeId) => {
|
||||
// Only add valid node IDs that exist in the current nodes
|
||||
if (!nodes || nodes.some((node) => node.id === nodeId)) {
|
||||
if (!newSelection.includes(nodeId)) {
|
||||
newSelection.push(nodeId)
|
||||
}
|
||||
// Auto-select children for hierarchical modules
|
||||
if (modules && nodes) {
|
||||
const children = this.getNodeChildrenIds(nodeId, modules, nodes)
|
||||
children.forEach((childId) => {
|
||||
if (nodes.some((node) => node.id === childId) && !newSelection.includes(childId)) {
|
||||
newSelection.push(childId)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
this.selectedIds = newSelection
|
||||
}
|
||||
|
||||
// Toggle node selection
|
||||
toggleNodeSelection(nodeId: string, modules?: FlowModule[], nodes?: Node[]) {
|
||||
const newSelection = [...this.selectedIds]
|
||||
if (newSelection.includes(nodeId)) {
|
||||
// Remove node
|
||||
const index = newSelection.indexOf(nodeId)
|
||||
newSelection.splice(index, 1)
|
||||
// Also remove children
|
||||
if (modules && nodes) {
|
||||
const children = this.getNodeChildrenIds(nodeId, modules, nodes)
|
||||
children.forEach((childId) => {
|
||||
const childIndex = newSelection.indexOf(childId)
|
||||
if (childIndex > -1) {
|
||||
newSelection.splice(childIndex, 1)
|
||||
}
|
||||
})
|
||||
}
|
||||
} else {
|
||||
// Add node
|
||||
newSelection.push(nodeId)
|
||||
// Auto-select children for hierarchical modules
|
||||
if (modules && nodes) {
|
||||
const children = this.getNodeChildrenIds(nodeId, modules, nodes)
|
||||
children.forEach((childId) => {
|
||||
if (!newSelection.includes(childId)) {
|
||||
newSelection.push(childId)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
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]
|
||||
}
|
||||
|
||||
// Get primary selected node ID (for backwards compatibility)
|
||||
get primarySelectedId(): string | undefined {
|
||||
return this.getSelectedId()
|
||||
}
|
||||
|
||||
// Handle keyboard shortcuts
|
||||
handleKeyDown(event: KeyboardEvent, nodes?: Node[]) {
|
||||
if (event.key === 'Escape') {
|
||||
if (this.#selectionMode === 'rect-select') {
|
||||
// Exit rect mode (this will preserve first item via mode setter)
|
||||
this.mode = 'normal'
|
||||
}
|
||||
} 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)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -8,7 +8,7 @@
|
||||
import { nextId } from '../flows/flowModuleNextId'
|
||||
|
||||
const dispatch = createEventDispatcher()
|
||||
const { flowStore, selectedId, flowStateStore } =
|
||||
const { flowStore, selectionManager, flowStateStore } =
|
||||
getContext<FlowEditorContext>('FlowEditorContext')
|
||||
|
||||
let tutorial: Tutorial | undefined = undefined
|
||||
@@ -158,7 +158,7 @@
|
||||
title: 'Step of the loop',
|
||||
description: 'We added an action to the loop. Let’s configure it',
|
||||
onNextClick: () => {
|
||||
$selectedId = tempId
|
||||
selectionManager.selectId(tempId)
|
||||
|
||||
dispatch('reload')
|
||||
setTimeout(() => {
|
||||
|
||||
@@ -9,6 +9,7 @@
|
||||
FlowInput,
|
||||
FlowInputEditorState
|
||||
} from '$lib/components/flows/types'
|
||||
import { SelectionManager } from '$lib/components/graph/selectionUtils.svelte'
|
||||
import { writable } from 'svelte/store'
|
||||
import { OpenAPI, type OpenFlow, type TriggersCount } from '$lib/gen'
|
||||
import { initHistory } from '$lib/history.svelte'
|
||||
@@ -76,7 +77,8 @@
|
||||
const history = initHistory(flowStore.val)
|
||||
|
||||
const stepsInputArgs = new StepsInputArgs()
|
||||
const selectedIdStore = writable('settings-metadata')
|
||||
const selectionManager = new SelectionManager()
|
||||
selectionManager.selectId('settings-metadata')
|
||||
const triggersCount = writable<TriggersCount | undefined>(undefined)
|
||||
setContext<TriggerContext>('TriggerContext', {
|
||||
triggersCount: triggersCount,
|
||||
@@ -86,7 +88,7 @@
|
||||
})
|
||||
|
||||
setContext<FlowEditorContext>('FlowEditorContext', {
|
||||
selectedId: selectedIdStore,
|
||||
selectionManager,
|
||||
previewArgs: previewArgsStore,
|
||||
scriptEditorDrawer,
|
||||
moving,
|
||||
@@ -293,7 +295,7 @@
|
||||
on:applyArgs={(ev) => {
|
||||
if (ev.detail.kind === 'preprocessor') {
|
||||
stepsInputArgs.setStepArgs('preprocessor', ev.detail.args ?? {})
|
||||
$selectedIdStore = 'preprocessor'
|
||||
selectionManager.selectId('preprocessor')
|
||||
} else {
|
||||
previewArgsStore.val = ev.detail.args ?? {}
|
||||
flowPreviewButtons?.openPreview()
|
||||
|
||||
Reference in New Issue
Block a user