mirror of
https://github.com/windmill-labs/windmill.git
synced 2026-08-22 08:02:19 +00:00
feat(frontend): run steps from graph (#5915)
This commit is contained in:
@@ -48,6 +48,7 @@
|
||||
import type { FlowPropPickerConfig, PropPickerContext } from './prop_picker'
|
||||
import type { PickableProperties } from './flows/previousResults'
|
||||
import { Triggers } from './triggers/triggers.svelte'
|
||||
import { TestSteps } from './flows/testSteps.svelte'
|
||||
|
||||
let flowCopilotContext: FlowCopilotContext = {
|
||||
shouldUpdatePropertyType: writable<{
|
||||
@@ -436,7 +437,7 @@
|
||||
const moving = writable<{ id: string } | undefined>(undefined)
|
||||
const history = initHistory(flowStore.val)
|
||||
|
||||
const testStepStore = writable<Record<string, any>>({})
|
||||
const testSteps = new TestSteps()
|
||||
const selectedIdStore = writable('settings-metadata')
|
||||
|
||||
const triggersCount = writable<TriggersCount | undefined>(undefined)
|
||||
@@ -455,7 +456,7 @@
|
||||
pathStore: writable(''),
|
||||
flowStateStore,
|
||||
flowStore,
|
||||
testStepStore,
|
||||
testSteps,
|
||||
saveDraft: () => {},
|
||||
initialPathStore: writable(''),
|
||||
fakeInitialPath: '',
|
||||
@@ -715,7 +716,7 @@
|
||||
noEditor
|
||||
on:applyArgs={(ev) => {
|
||||
if (ev.detail.kind === 'preprocessor') {
|
||||
$testStepStore['preprocessor'] = ev.detail.args ?? {}
|
||||
testSteps.setStepArgs('preprocessor', ev.detail.args ?? {})
|
||||
$selectedIdStore = 'preprocessor'
|
||||
} else {
|
||||
previewArgsStore.val = ev.detail.args ?? {}
|
||||
|
||||
@@ -72,6 +72,7 @@
|
||||
} from './triggers/utils'
|
||||
import DraftTriggersConfirmationModal from './common/confirmationModal/DraftTriggersConfirmationModal.svelte'
|
||||
import { Triggers } from './triggers/triggers.svelte'
|
||||
import { TestSteps } from './flows/testSteps.svelte'
|
||||
import { aiChatManager } from './copilot/chat/AIChatManager.svelte'
|
||||
|
||||
interface Props {
|
||||
@@ -526,7 +527,7 @@
|
||||
payloadData: undefined
|
||||
})
|
||||
|
||||
const testStepStore = writable<Record<string, any>>({})
|
||||
const testSteps = new TestSteps()
|
||||
|
||||
function select(selectedId: string) {
|
||||
selectedIdStore.set(selectedId)
|
||||
@@ -544,7 +545,7 @@
|
||||
flowStateStore,
|
||||
flowStore,
|
||||
pathStore,
|
||||
testStepStore,
|
||||
testSteps,
|
||||
saveDraft,
|
||||
initialPathStore,
|
||||
fakeInitialPath,
|
||||
@@ -766,6 +767,9 @@
|
||||
|
||||
let flowPreviewButtons: FlowPreviewButtons | undefined = $state()
|
||||
|
||||
let forceTestTab: Record<string, boolean> = $state({})
|
||||
let highlightArg: Record<string, string | undefined> = $state({})
|
||||
|
||||
run(() => {
|
||||
initialPathStore.set(initialPath)
|
||||
})
|
||||
@@ -1020,7 +1024,7 @@
|
||||
{newFlow}
|
||||
on:applyArgs={(ev) => {
|
||||
if (ev.detail.kind === 'preprocessor') {
|
||||
$testStepStore['preprocessor'] = ev.detail.args ?? {}
|
||||
testSteps.setStepArgs('preprocessor', ev.detail.args ?? {})
|
||||
$selectedIdStore = 'preprocessor'
|
||||
}
|
||||
}}
|
||||
@@ -1028,8 +1032,24 @@
|
||||
previewArgsStore.val = JSON.parse(JSON.stringify(e.detail))
|
||||
flowPreviewButtons?.openPreview(true)
|
||||
}}
|
||||
onTestUpTo={() => {
|
||||
flowPreviewButtons?.testUpTo()
|
||||
}}
|
||||
{savedFlow}
|
||||
onDeployTrigger={handleDeployTrigger}
|
||||
onEditInput={(moduleId, key) => {
|
||||
selectedIdStore.set(moduleId)
|
||||
// Use new prop-based system
|
||||
forceTestTab[moduleId] = true
|
||||
highlightArg[moduleId] = key
|
||||
// Reset the force flag after a short delay to allow re-triggering
|
||||
setTimeout(() => {
|
||||
forceTestTab[moduleId] = false
|
||||
highlightArg[moduleId] = undefined
|
||||
}, 500)
|
||||
}}
|
||||
{forceTestTab}
|
||||
{highlightArg}
|
||||
/>
|
||||
{:else}
|
||||
<CenteredPage>Loading...</CenteredPage>
|
||||
|
||||
@@ -379,7 +379,14 @@
|
||||
id="flow-editor-test-flow-drawer"
|
||||
shortCut={{ Icon: CornerDownLeft }}
|
||||
>
|
||||
Test flow
|
||||
{#if previewMode == 'upTo'}
|
||||
Test up to
|
||||
<Badge baseClass="ml-1" color="indigo">
|
||||
{$selectedId}
|
||||
</Badge>
|
||||
{:else}
|
||||
Test flow
|
||||
{/if}
|
||||
</Button>
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
@@ -1,22 +1,13 @@
|
||||
<script lang="ts">
|
||||
import { run } from 'svelte/legacy'
|
||||
|
||||
import type { Schema } from '$lib/common'
|
||||
import { ScriptService, type FlowModule, type Job } from '$lib/gen'
|
||||
import { workspaceStore } from '$lib/stores'
|
||||
import { getScriptByPath } from '$lib/scripts'
|
||||
|
||||
import { type FlowModule, type Job } from '$lib/gen'
|
||||
import { CornerDownLeft, Loader2 } from 'lucide-svelte'
|
||||
import { getContext } from 'svelte'
|
||||
|
||||
import Button from './common/button/Button.svelte'
|
||||
import type { FlowEditorContext } from './flows/types'
|
||||
|
||||
import TestJobLoader from './TestJobLoader.svelte'
|
||||
import ModulePreviewForm from './ModulePreviewForm.svelte'
|
||||
|
||||
import { evalValue } from './flows/utils'
|
||||
import type { PickableProperties } from './flows/previousResults'
|
||||
import ModuleTest from './ModuleTest.svelte'
|
||||
import { getContext } from 'svelte'
|
||||
import type { FlowEditorContext } from './flows/types'
|
||||
|
||||
interface Props {
|
||||
mod: FlowModule
|
||||
@@ -26,6 +17,7 @@
|
||||
testIsLoading?: boolean
|
||||
noEditor?: boolean
|
||||
scriptProgress?: any
|
||||
focusArg?: string
|
||||
}
|
||||
|
||||
let {
|
||||
@@ -35,90 +27,25 @@
|
||||
testJob = $bindable(undefined),
|
||||
testIsLoading = $bindable(false),
|
||||
noEditor = false,
|
||||
scriptProgress = $bindable(undefined)
|
||||
scriptProgress = $bindable(undefined),
|
||||
focusArg = undefined
|
||||
}: Props = $props()
|
||||
|
||||
const { flowStore, flowStateStore, testStepStore, pathStore } =
|
||||
getContext<FlowEditorContext>('FlowEditorContext')
|
||||
|
||||
// Test
|
||||
|
||||
let testJobLoader: TestJobLoader | undefined = $state()
|
||||
|
||||
let jobProgressReset: () => void = () => {}
|
||||
|
||||
let stepArgs: Record<string, any> | undefined = $state(
|
||||
Object.fromEntries(
|
||||
Object.keys(schema.properties ?? {}).map((k) => [
|
||||
k,
|
||||
evalValue(k, mod, $testStepStore, pickableProperties, false)
|
||||
])
|
||||
)
|
||||
)
|
||||
|
||||
run(() => {
|
||||
$testStepStore[mod.id] = stepArgs
|
||||
})
|
||||
const { flowStore } = getContext<FlowEditorContext>('FlowEditorContext')
|
||||
let moduleTest: ModuleTest | undefined = $state()
|
||||
|
||||
export function runTestWithStepArgs() {
|
||||
runTest(stepArgs)
|
||||
}
|
||||
|
||||
export async function runTest(args: any) {
|
||||
// Not defined if JobProgressBar not loaded
|
||||
if (jobProgressReset) jobProgressReset()
|
||||
|
||||
const val = mod.value
|
||||
// let jobId: string | undefined = undefined
|
||||
if (val.type == 'rawscript') {
|
||||
await testJobLoader?.runPreview(
|
||||
val.path ?? ($pathStore ?? '') + '/' + mod.id,
|
||||
val.content,
|
||||
val.language,
|
||||
mod.id === 'preprocessor' ? { _ENTRYPOINT_OVERRIDE: 'preprocessor', ...args } : args,
|
||||
flowStore.val?.tag ?? val.tag
|
||||
)
|
||||
} else if (val.type == 'script') {
|
||||
const script = val.hash
|
||||
? await ScriptService.getScriptByHash({ workspace: $workspaceStore!, hash: val.hash })
|
||||
: await getScriptByPath(val.path)
|
||||
await testJobLoader?.runPreview(
|
||||
val.path,
|
||||
script.content,
|
||||
script.language,
|
||||
mod.id === 'preprocessor' ? { _ENTRYPOINT_OVERRIDE: 'preprocessor', ...args } : args,
|
||||
flowStore.val?.tag ?? (val.tag_override ? val.tag_override : script.tag),
|
||||
script.lock,
|
||||
val.hash ?? script.hash
|
||||
)
|
||||
} else if (val.type == 'flow') {
|
||||
await testJobLoader?.runFlowByPath(val.path, args)
|
||||
} else {
|
||||
throw Error('Not supported module type')
|
||||
}
|
||||
}
|
||||
|
||||
function jobDone() {
|
||||
if (testJob && !testJob.canceled && testJob.type == 'CompletedJob' && `result` in testJob) {
|
||||
if ($flowStateStore[mod.id]) {
|
||||
$flowStateStore[mod.id].previewResult = testJob.result
|
||||
$flowStateStore[mod.id].previewSuccess = testJob.success
|
||||
$flowStateStore[mod.id].previewJobId = testJob.id
|
||||
$flowStateStore[mod.id].previewWorkspaceId = testJob.workspace_id
|
||||
$flowStateStore = $flowStateStore
|
||||
}
|
||||
}
|
||||
testJob = undefined
|
||||
moduleTest?.runTestWithStepArgs()
|
||||
}
|
||||
</script>
|
||||
|
||||
<TestJobLoader
|
||||
toastError={noEditor}
|
||||
on:done={() => jobDone()}
|
||||
<ModuleTest
|
||||
{mod}
|
||||
{noEditor}
|
||||
bind:testJob
|
||||
bind:testIsLoading
|
||||
bind:scriptProgress
|
||||
bind:this={testJobLoader}
|
||||
bind:isLoading={testIsLoading}
|
||||
bind:job={testJob}
|
||||
bind:this={moduleTest}
|
||||
/>
|
||||
|
||||
<div class="p-4">
|
||||
@@ -130,7 +57,7 @@
|
||||
|
||||
<div class="w-full justify-center flex">
|
||||
{#if testIsLoading}
|
||||
<Button size="sm" on:click={testJobLoader?.cancelJob} btnClasses="w-full" color="red">
|
||||
<Button size="sm" on:click={moduleTest?.cancelJob} btnClasses="w-full" color="red">
|
||||
<Loader2 size={16} class="animate-spin mr-1" />
|
||||
Cancel
|
||||
</Button>
|
||||
@@ -139,7 +66,7 @@
|
||||
color="dark"
|
||||
btnClasses="truncate"
|
||||
size="sm"
|
||||
on:click={() => runTest(stepArgs)}
|
||||
on:click={runTestWithStepArgs}
|
||||
shortCut={{
|
||||
Icon: CornerDownLeft
|
||||
}}
|
||||
@@ -149,5 +76,5 @@
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
<ModulePreviewForm {pickableProperties} {mod} {schema} bind:args={stepArgs} />
|
||||
<ModulePreviewForm {pickableProperties} {mod} {schema} {focusArg} />
|
||||
</div>
|
||||
|
||||
@@ -5,77 +5,91 @@
|
||||
import { RefreshCw } from 'lucide-svelte'
|
||||
import ArgInput from './ArgInput.svelte'
|
||||
import { Button } from './common'
|
||||
import { getContext, untrack } from 'svelte'
|
||||
import { getContext, onMount, untrack } from 'svelte'
|
||||
import type { FlowEditorContext } from './flows/types'
|
||||
import { evalValue } from './flows/utils'
|
||||
import type { FlowModule } from '$lib/gen'
|
||||
import type { PickableProperties } from './flows/previousResults'
|
||||
import type SimpleEditor from './SimpleEditor.svelte'
|
||||
import { getResourceTypes } from './resourceTypesStore'
|
||||
import { twMerge } from 'tailwind-merge'
|
||||
|
||||
interface Props {
|
||||
schema: Schema | { properties?: Record<string, any>; required?: string[] }
|
||||
args?: Record<string, any>
|
||||
mod: FlowModule
|
||||
pickableProperties: PickableProperties | undefined
|
||||
isValid?: boolean
|
||||
autofocus?: boolean
|
||||
focusArg?: string
|
||||
}
|
||||
|
||||
let {
|
||||
schema,
|
||||
args = $bindable({}),
|
||||
mod,
|
||||
pickableProperties,
|
||||
isValid = $bindable(true),
|
||||
autofocus = false
|
||||
autofocus = false,
|
||||
focusArg = undefined
|
||||
}: Props = $props()
|
||||
|
||||
const { testStepStore } = getContext<FlowEditorContext>('FlowEditorContext')
|
||||
const { testSteps, flowStateStore, flowStore, previewArgs } =
|
||||
getContext<FlowEditorContext>('FlowEditorContext')
|
||||
|
||||
let inputCheck: { [id: string]: boolean } = $state({})
|
||||
$effect(() => {
|
||||
isValid = allTrue(inputCheck) ?? false
|
||||
})
|
||||
|
||||
$effect(() => {
|
||||
if (args == undefined || typeof args !== 'object') {
|
||||
args = {}
|
||||
}
|
||||
})
|
||||
|
||||
function removeExtraKey() {
|
||||
const nargs = {}
|
||||
Object.keys(args ?? {}).forEach((key) => {
|
||||
if (keys.includes(key)) {
|
||||
nargs[key] = args[key]
|
||||
}
|
||||
})
|
||||
args = nargs
|
||||
}
|
||||
|
||||
let keys: string[] = $state([])
|
||||
$effect(() => {
|
||||
let lkeys = Object.keys(schema?.properties ?? {})
|
||||
if (schema?.properties && JSON.stringify(lkeys) != JSON.stringify(keys)) {
|
||||
keys = lkeys
|
||||
untrack(() => removeExtraKey())
|
||||
untrack(() => testSteps?.removeExtraKey(mod.id, keys))
|
||||
}
|
||||
})
|
||||
|
||||
function plugIt(argName: string) {
|
||||
args[argName] = structuredClone(
|
||||
$state.snapshot(evalValue(argName, mod, testStepStore, pickableProperties, true))
|
||||
testSteps?.setEvaluatedStepArg(
|
||||
mod.id,
|
||||
argName,
|
||||
$state.snapshot(evalValue(argName, mod, pickableProperties, true))
|
||||
)
|
||||
try {
|
||||
editor?.[argName]?.setCode(JSON.stringify(args[argName], null, 4))
|
||||
} catch {
|
||||
//ignore
|
||||
}
|
||||
}
|
||||
|
||||
let editor: Record<string, SimpleEditor | undefined> = $state({})
|
||||
|
||||
// Animation and highlighting for focusArg
|
||||
let animateArg: string | undefined = $state(undefined)
|
||||
$effect(() => {
|
||||
if (focusArg) {
|
||||
// Add a slight delay to ensure the form is rendered
|
||||
setTimeout(() => {
|
||||
const argElement = document.querySelector(`[data-arg="${focusArg}"]`)
|
||||
if (argElement) {
|
||||
// Add highlight animation
|
||||
animateArg = focusArg
|
||||
argElement.scrollIntoView({ behavior: 'smooth', block: 'center' })
|
||||
|
||||
// Focus the input if it exists
|
||||
const input = argElement.querySelector('input, textarea, select') as
|
||||
| HTMLInputElement
|
||||
| HTMLTextAreaElement
|
||||
| HTMLSelectElement
|
||||
| null
|
||||
if (input) {
|
||||
input.focus()
|
||||
}
|
||||
|
||||
// Remove highlight after animation
|
||||
setTimeout(() => {
|
||||
animateArg = undefined
|
||||
}, 2000)
|
||||
}
|
||||
}, 200)
|
||||
}
|
||||
})
|
||||
|
||||
let resourceTypes: string[] | undefined = $state(undefined)
|
||||
|
||||
async function loadResourceTypes() {
|
||||
@@ -83,21 +97,34 @@
|
||||
}
|
||||
|
||||
loadResourceTypes()
|
||||
|
||||
let args = $state(<Record<string, any>>{})
|
||||
|
||||
onMount(() => {
|
||||
testSteps?.updateStepArgs(mod.id, $flowStateStore, flowStore?.val, previewArgs?.val)
|
||||
args = testSteps?.getStepArgs(mod.id) ?? { value: {} }
|
||||
})
|
||||
</script>
|
||||
|
||||
<div class="w-full pt-2">
|
||||
<div class="w-full pt-2" data-popover>
|
||||
{#if keys.length > 0}
|
||||
{#each keys as argName, i (argName)}
|
||||
{#if Object.keys(schema.properties ?? {}).includes(argName)}
|
||||
<div class="flex gap-2">
|
||||
{#if typeof args == 'object' && schema?.properties?.[argName]}
|
||||
<div
|
||||
class={twMerge(
|
||||
'flex gap-2',
|
||||
animateArg === argName && 'animate-pulse ring-2 ring-offset-2 ring-blue-500 rounded'
|
||||
)}
|
||||
data-arg={argName}
|
||||
>
|
||||
{#if typeof args.value == 'object' && schema?.properties?.[argName]}
|
||||
<ArgInput
|
||||
{resourceTypes}
|
||||
minW={false}
|
||||
autofocus={i == 0 && autofocus}
|
||||
autofocus={autofocus && !focusArg && i == 0}
|
||||
label={argName}
|
||||
description={schema.properties[argName].description}
|
||||
bind:value={args[argName]}
|
||||
bind:value={args.value[argName]}
|
||||
type={schema.properties[argName].type}
|
||||
oneOf={schema.properties[argName].oneOf}
|
||||
required={schema?.required?.includes(argName)}
|
||||
@@ -117,15 +144,19 @@
|
||||
placeholder={schema.properties[argName].placeholder}
|
||||
/>
|
||||
{/if}
|
||||
<div class="pt-6 mt-0.5">
|
||||
<Button
|
||||
on:click={() => plugIt(argName)}
|
||||
size="sm"
|
||||
variant="border"
|
||||
color="light"
|
||||
title="Re-evaluate input step"><RefreshCw size={14} /></Button
|
||||
>
|
||||
</div>
|
||||
{#if testSteps?.isArgManuallySet(mod.id, argName)}
|
||||
<div class="pt-6 mt-0.5">
|
||||
<Button
|
||||
on:click={() => {
|
||||
plugIt(argName)
|
||||
}}
|
||||
size="sm"
|
||||
variant="border"
|
||||
color="light"
|
||||
title="Re-evaluate input step"><RefreshCw size={14} /></Button
|
||||
>
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
{/if}
|
||||
{/each}
|
||||
|
||||
@@ -41,7 +41,7 @@
|
||||
onUpdateMock
|
||||
}: Props = $props()
|
||||
|
||||
const { testStepStore } = getContext<FlowEditorContext>('FlowEditorContext')
|
||||
const { testSteps } = getContext<FlowEditorContext>('FlowEditorContext')
|
||||
|
||||
let selectedJob: Job | undefined = $state(undefined)
|
||||
let fetchingLastJob = false
|
||||
@@ -49,7 +49,7 @@
|
||||
let jobProgressReset: () => void = $state(() => {})
|
||||
|
||||
let nlastJob = $derived.by(() => {
|
||||
if (testJob) {
|
||||
if (testJob && testJob.type === 'CompletedJob') {
|
||||
return { ...testJob, preview: true }
|
||||
}
|
||||
if (lastJob) {
|
||||
@@ -90,7 +90,7 @@
|
||||
{disableHistory}
|
||||
>
|
||||
{#snippet copilot_fix()}
|
||||
{#if lang && editor && diffEditor && $testStepStore[mod.id] && selectedJob?.type === 'CompletedJob' && !selectedJob.success && getStringError(selectedJob.result)}
|
||||
{#if lang && editor && diffEditor && testSteps.getStepArgs(mod.id) && selectedJob?.type === 'CompletedJob' && !selectedJob.success && getStringError(selectedJob.result)}
|
||||
<ScriptFix {lang} />
|
||||
{/if}
|
||||
{/snippet}
|
||||
|
||||
@@ -0,0 +1,137 @@
|
||||
<script lang="ts" module>
|
||||
type testModuleState = {
|
||||
loading: boolean
|
||||
instances: number
|
||||
cancel?: () => void
|
||||
}
|
||||
|
||||
let testModulesState = $state<Record<string, testModuleState>>({})
|
||||
</script>
|
||||
|
||||
<script lang="ts">
|
||||
import { ScriptService, type FlowModule, type Job } from '$lib/gen'
|
||||
import { workspaceStore } from '$lib/stores'
|
||||
import { getScriptByPath } from '$lib/scripts'
|
||||
import { getContext, onMount } from 'svelte'
|
||||
import type { FlowEditorContext } from './flows/types'
|
||||
import TestJobLoader from './TestJobLoader.svelte'
|
||||
|
||||
interface Props {
|
||||
mod: FlowModule
|
||||
testJob?: Job | undefined
|
||||
testIsLoading?: boolean
|
||||
noEditor?: boolean
|
||||
scriptProgress?: any
|
||||
}
|
||||
|
||||
let {
|
||||
mod,
|
||||
testJob = $bindable(undefined),
|
||||
testIsLoading = $bindable(false),
|
||||
noEditor = false,
|
||||
scriptProgress = $bindable(undefined)
|
||||
}: Props = $props()
|
||||
|
||||
const { flowStore, flowStateStore, pathStore, testSteps, previewArgs } =
|
||||
getContext<FlowEditorContext>('FlowEditorContext')
|
||||
|
||||
let testJobLoader: TestJobLoader | undefined = $state(undefined)
|
||||
let jobProgressReset: () => void = () => {}
|
||||
|
||||
export function runTestWithStepArgs() {
|
||||
runTest(testSteps.getStepArgs(mod.id)?.value)
|
||||
}
|
||||
|
||||
export function loadArgsAndRunTest() {
|
||||
testSteps?.updateStepArgs(mod.id, $flowStateStore, flowStore?.val, previewArgs?.val)
|
||||
runTest(testSteps.getStepArgs(mod.id)?.value)
|
||||
}
|
||||
|
||||
export async function runTest(args: any) {
|
||||
// Not defined if JobProgressBar not loaded
|
||||
if (jobProgressReset) jobProgressReset()
|
||||
|
||||
testModulesState[mod.id].cancel = testJobLoader?.cancelJob
|
||||
|
||||
const val = mod.value
|
||||
// let jobId: string | undefined = undefined
|
||||
if (val.type == 'rawscript') {
|
||||
await testJobLoader?.runPreview(
|
||||
val.path ?? ($pathStore ?? '') + '/' + mod.id,
|
||||
val.content,
|
||||
val.language,
|
||||
mod.id === 'preprocessor' ? { _ENTRYPOINT_OVERRIDE: 'preprocessor', ...args } : args,
|
||||
flowStore?.val?.tag ?? val.tag
|
||||
)
|
||||
} else if (val.type == 'script') {
|
||||
const script = val.hash
|
||||
? await ScriptService.getScriptByHash({ workspace: $workspaceStore!, hash: val.hash })
|
||||
: await getScriptByPath(val.path)
|
||||
await testJobLoader?.runPreview(
|
||||
val.path,
|
||||
script.content,
|
||||
script.language,
|
||||
mod.id === 'preprocessor' ? { _ENTRYPOINT_OVERRIDE: 'preprocessor', ...args } : args,
|
||||
flowStore?.val?.tag ?? (val.tag_override ? val.tag_override : script.tag),
|
||||
script.lock,
|
||||
val.hash ?? script.hash
|
||||
)
|
||||
} else if (val.type == 'flow') {
|
||||
await testJobLoader?.runFlowByPath(val.path, args)
|
||||
} else {
|
||||
throw Error('Not supported module type')
|
||||
}
|
||||
}
|
||||
|
||||
function jobDone() {
|
||||
if (testJob && !testJob.canceled && testJob.type == 'CompletedJob' && `result` in testJob) {
|
||||
if ($flowStateStore[mod.id]) {
|
||||
$flowStateStore[mod.id].previewResult = testJob.result
|
||||
$flowStateStore[mod.id].previewSuccess = testJob.success
|
||||
$flowStateStore[mod.id].previewJobId = testJob.id
|
||||
$flowStateStore[mod.id].previewWorkspaceId = testJob.workspace_id
|
||||
$flowStateStore = $flowStateStore
|
||||
}
|
||||
}
|
||||
testJob = undefined
|
||||
}
|
||||
|
||||
export function cancelJob() {
|
||||
testModulesState[mod.id]?.cancel?.()
|
||||
}
|
||||
|
||||
$effect(() => {
|
||||
testIsLoading = testModulesState[mod.id]?.loading ?? false
|
||||
})
|
||||
|
||||
onMount(() => {
|
||||
testModulesState[mod.id] = {
|
||||
...(testModulesState[mod.id] ?? { loading: false, instances: 0 }),
|
||||
loading: testIsLoading,
|
||||
instances: testModulesState[mod.id]!.instances + 1
|
||||
}
|
||||
return () => {
|
||||
testModulesState[mod.id]!.instances -= 1
|
||||
if (testModulesState[mod.id]!.instances < 1) {
|
||||
delete testModulesState[mod.id]
|
||||
}
|
||||
}
|
||||
})
|
||||
</script>
|
||||
|
||||
<TestJobLoader
|
||||
toastError={noEditor}
|
||||
on:done={() => jobDone()}
|
||||
bind:scriptProgress
|
||||
bind:this={testJobLoader}
|
||||
bind:isLoading={
|
||||
() => testModulesState[mod.id]?.loading ?? false,
|
||||
(v) =>
|
||||
(testModulesState[mod.id] = {
|
||||
...testModulesState[mod.id],
|
||||
loading: v ?? false,
|
||||
instances: testModulesState[mod.id]?.instances ?? 0
|
||||
})
|
||||
}
|
||||
bind:job={testJob}
|
||||
/>
|
||||
@@ -49,6 +49,7 @@
|
||||
portal?: string
|
||||
}
|
||||
| undefined = undefined
|
||||
export let dropdownBtnClasses: string = ''
|
||||
|
||||
type MenuItem = {
|
||||
label: string
|
||||
@@ -309,7 +310,8 @@
|
||||
'rounded-md m-0 p-0 center-center h-full',
|
||||
variant === 'border' ? 'border-0 border-r border-y ' : 'border-0',
|
||||
'rounded-r-md !rounded-l-none',
|
||||
size === 'xs2' ? '!w-8' : '!w-10'
|
||||
size === 'xs2' ? '!w-8' : '!w-10',
|
||||
dropdownBtnClasses
|
||||
)}
|
||||
>
|
||||
<ChevronDown size={lucideIconSize} />
|
||||
|
||||
@@ -32,6 +32,10 @@
|
||||
})
|
||||
| undefined
|
||||
onDeployTrigger?: (trigger: Trigger) => void
|
||||
onTestUpTo?: ((id: string) => void) | undefined
|
||||
onEditInput?: ((moduleId: string, key: string) => void) | undefined
|
||||
forceTestTab?: Record<string, boolean>
|
||||
highlightArg?: Record<string, string | undefined>
|
||||
}
|
||||
|
||||
let {
|
||||
@@ -44,7 +48,11 @@
|
||||
smallErrorHandler = false,
|
||||
newFlow = false,
|
||||
savedFlow = undefined,
|
||||
onDeployTrigger = () => {}
|
||||
onDeployTrigger = () => {},
|
||||
onTestUpTo = undefined,
|
||||
onEditInput = undefined,
|
||||
forceTestTab,
|
||||
highlightArg
|
||||
}: Props = $props()
|
||||
|
||||
let flowModuleSchemaMap: FlowModuleSchemaMap | undefined = $state()
|
||||
@@ -93,6 +101,8 @@
|
||||
}
|
||||
aiChatManager.generateStep(detail.moduleId, detail.lang, detail.instructions)
|
||||
}}
|
||||
{onTestUpTo}
|
||||
{onEditInput}
|
||||
/>
|
||||
{/if}
|
||||
</div>
|
||||
@@ -113,6 +123,8 @@
|
||||
on:applyArgs
|
||||
on:testWithArgs
|
||||
{onDeployTrigger}
|
||||
{forceTestTab}
|
||||
{highlightArg}
|
||||
/>
|
||||
{/if}
|
||||
</Pane>
|
||||
|
||||
@@ -25,6 +25,8 @@
|
||||
})
|
||||
| undefined
|
||||
onDeployTrigger?: (trigger: Trigger) => void
|
||||
forceTestTab?: Record<string, boolean>
|
||||
highlightArg?: Record<string, string | undefined>
|
||||
}
|
||||
|
||||
let {
|
||||
@@ -33,7 +35,9 @@
|
||||
newFlow = false,
|
||||
disabledFlowInputs = false,
|
||||
savedFlow = undefined,
|
||||
onDeployTrigger = () => {}
|
||||
onDeployTrigger = () => {},
|
||||
forceTestTab,
|
||||
highlightArg
|
||||
}: Props = $props()
|
||||
|
||||
const {
|
||||
@@ -138,6 +142,8 @@
|
||||
previousModule={flowStore.val.value.modules[index - 1]}
|
||||
{enableAi}
|
||||
savedModule={savedFlow?.value.modules[index]}
|
||||
{forceTestTab}
|
||||
{highlightArg}
|
||||
/>
|
||||
{/each}
|
||||
{/key}
|
||||
|
||||
@@ -77,6 +77,8 @@
|
||||
noEditor: boolean
|
||||
enableAi: boolean
|
||||
savedModule?: FlowModule | undefined
|
||||
forceTestTab?: boolean
|
||||
highlightArg?: string
|
||||
}
|
||||
|
||||
let {
|
||||
@@ -89,7 +91,9 @@
|
||||
scriptTemplate = 'script',
|
||||
noEditor,
|
||||
enableAi,
|
||||
savedModule = undefined
|
||||
savedModule = undefined,
|
||||
forceTestTab = false,
|
||||
highlightArg = undefined
|
||||
}: Props = $props()
|
||||
|
||||
let tag: string | undefined = $state(undefined)
|
||||
@@ -274,6 +278,20 @@
|
||||
onDestroy(() => {
|
||||
$currentEditor = undefined
|
||||
})
|
||||
|
||||
// Handle force test tab prop with animation
|
||||
$effect(() => {
|
||||
if (forceTestTab) {
|
||||
selected = 'test'
|
||||
// Add a smooth transition to the test tab
|
||||
setTimeout(() => {
|
||||
const testTab = document.querySelector('[value="test"]')
|
||||
if (testTab) {
|
||||
testTab.scrollIntoView({ behavior: 'smooth', block: 'nearest' })
|
||||
}
|
||||
}, 100)
|
||||
}
|
||||
})
|
||||
</script>
|
||||
|
||||
<svelte:window onkeydown={onKeyDown} />
|
||||
@@ -525,6 +543,7 @@
|
||||
bind:testJob
|
||||
bind:testIsLoading
|
||||
bind:scriptProgress
|
||||
focusArg={highlightArg}
|
||||
/>
|
||||
{:else if selected === 'advanced'}
|
||||
<Tabs bind:selected={advancedSelected}>
|
||||
|
||||
@@ -40,6 +40,8 @@
|
||||
parentModule?: FlowModule | undefined
|
||||
// Pointer to previous module, for easy access to testing results
|
||||
previousModule?: FlowModule | undefined
|
||||
forceTestTab?: Record<string, boolean>
|
||||
highlightArg?: Record<string, string | undefined>
|
||||
}
|
||||
|
||||
let {
|
||||
@@ -48,7 +50,9 @@
|
||||
enableAi = false,
|
||||
savedModule = undefined,
|
||||
parentModule = $bindable(),
|
||||
previousModule = undefined
|
||||
previousModule = undefined,
|
||||
forceTestTab,
|
||||
highlightArg
|
||||
}: Props = $props()
|
||||
|
||||
function initializePrimaryScheduleForTriggerScript(module: FlowModule) {
|
||||
@@ -196,6 +200,8 @@
|
||||
{scriptTemplate}
|
||||
{enableAi}
|
||||
{savedModule}
|
||||
forceTestTab={forceTestTab?.[flowModule.id]}
|
||||
highlightArg={highlightArg?.[flowModule.id]}
|
||||
/>
|
||||
{/if}
|
||||
{:else if flowModule.value.type === 'forloopflow' || flowModule.value.type == 'whileloopflow'}
|
||||
@@ -210,6 +216,8 @@
|
||||
? savedModule.value.modules[index]
|
||||
: undefined}
|
||||
{enableAi}
|
||||
{forceTestTab}
|
||||
{highlightArg}
|
||||
/>
|
||||
{/each}
|
||||
{:else if flowModule.value.type === 'branchone'}
|
||||
@@ -229,6 +237,8 @@
|
||||
? savedModule.value.default[index]
|
||||
: undefined}
|
||||
{enableAi}
|
||||
{forceTestTab}
|
||||
{highlightArg}
|
||||
/>
|
||||
{/each}
|
||||
{/if}
|
||||
@@ -252,6 +262,8 @@
|
||||
? savedModule.value.branches[branchIndex]?.modules[index]
|
||||
: undefined}
|
||||
{enableAi}
|
||||
{forceTestTab}
|
||||
{highlightArg}
|
||||
/>
|
||||
{/each}
|
||||
{/if}
|
||||
@@ -271,6 +283,8 @@
|
||||
savedModule={savedModule?.value.type === 'branchall'
|
||||
? savedModule.value.branches[branchIndex]?.modules[index]
|
||||
: undefined}
|
||||
{forceTestTab}
|
||||
{highlightArg}
|
||||
/>
|
||||
{/each}
|
||||
{/if}
|
||||
|
||||
@@ -70,6 +70,12 @@
|
||||
'triggers'
|
||||
].includes($selectedId) ||
|
||||
$selectedId?.includes('branch')
|
||||
|
||||
export function testUpTo() {
|
||||
if (upToDisabled) return
|
||||
previewMode = 'upTo'
|
||||
previewOpen = true
|
||||
}
|
||||
</script>
|
||||
|
||||
{#if !upToDisabled}
|
||||
@@ -78,10 +84,7 @@
|
||||
disabled={upToDisabled}
|
||||
color="light"
|
||||
variant="border"
|
||||
on:click={() => {
|
||||
previewMode = 'upTo'
|
||||
previewOpen = !previewOpen
|
||||
}}
|
||||
on:click={testUpTo}
|
||||
startIcon={{ icon: Play }}
|
||||
>
|
||||
Test up to
|
||||
|
||||
@@ -14,7 +14,9 @@
|
||||
Square,
|
||||
SkipForward,
|
||||
Pin,
|
||||
X
|
||||
X,
|
||||
Play,
|
||||
Loader2
|
||||
} from 'lucide-svelte'
|
||||
import { createEventDispatcher, getContext, untrack } from 'svelte'
|
||||
import { fade } from 'svelte/transition'
|
||||
@@ -23,6 +25,7 @@
|
||||
import { twMerge } from 'tailwind-merge'
|
||||
import IdEditorInput from '$lib/components/IdEditorInput.svelte'
|
||||
import { dfs } from '../dfs'
|
||||
import { dfs as dfsPreviousResults } from '../previousResults'
|
||||
import { Drawer } from '$lib/components/common'
|
||||
import DrawerContent from '$lib/components/common/drawer/DrawerContent.svelte'
|
||||
import { getDependeeAndDependentComponents } from '../flowExplorer'
|
||||
@@ -32,6 +35,8 @@
|
||||
import OutputPicker from '$lib/components/flows/propPicker/OutputPicker.svelte'
|
||||
import OutputPickerInner from '$lib/components/flows/propPicker/OutputPickerInner.svelte'
|
||||
import type { FlowState } from '$lib/components/flows/flowState'
|
||||
import { Button } from '$lib/components/common'
|
||||
import ModuleTest from '$lib/components/ModuleTest.svelte'
|
||||
|
||||
interface Props {
|
||||
selected?: boolean
|
||||
@@ -63,7 +68,10 @@
|
||||
alwaysShowOutputPicker?: boolean
|
||||
loopStatus?: { type: 'inside' | 'self'; flow: 'forloopflow' | 'whileloopflow' } | undefined
|
||||
icon?: import('svelte').Snippet
|
||||
onTestUpTo?: ((id: string) => void) | undefined
|
||||
inputTransform?: Record<string, any> | undefined
|
||||
onUpdateMock?: (mock: { enabled: boolean; return_value?: unknown }) => void
|
||||
onEditInput?: (moduleId: string, key: string) => void
|
||||
}
|
||||
|
||||
let {
|
||||
@@ -91,7 +99,10 @@
|
||||
alwaysShowOutputPicker = false,
|
||||
loopStatus = undefined,
|
||||
icon,
|
||||
onUpdateMock
|
||||
onTestUpTo,
|
||||
inputTransform,
|
||||
onUpdateMock,
|
||||
onEditInput
|
||||
}: Props = $props()
|
||||
|
||||
let pickableIds: Record<string, any> | undefined = $state(undefined)
|
||||
@@ -116,11 +127,15 @@
|
||||
|
||||
let newId: string = $state(id ?? '')
|
||||
|
||||
let moduleTest: ModuleTest | undefined = $state(undefined)
|
||||
let testIsLoading = $state(false)
|
||||
let hover = $state(false)
|
||||
let connectingData: any | undefined = $state(undefined)
|
||||
let lastJob: any | undefined = $state(undefined)
|
||||
let outputPicker: OutputPicker | undefined = $state(undefined)
|
||||
let historyOpen = $state(false)
|
||||
let testJob: any | undefined = $state(undefined)
|
||||
let outputPickerBarOpen = $state(false)
|
||||
|
||||
let flowStateStore = $derived(flowEditorContext?.flowStateStore)
|
||||
|
||||
@@ -158,10 +173,24 @@
|
||||
flowStateStore && $flowStateStore && untrack(() => updateLastJob($flowStateStore))
|
||||
})
|
||||
|
||||
let nlastJob = $derived.by(() => {
|
||||
if (testJob) {
|
||||
return { ...testJob, preview: true }
|
||||
}
|
||||
if (lastJob) {
|
||||
return { ...lastJob, preview: false }
|
||||
}
|
||||
return undefined
|
||||
})
|
||||
|
||||
let isConnectingCandidate = $derived(
|
||||
!!id && !!$flowPropPickerConfig && !!pickableIds && Object.keys(pickableIds).includes(id)
|
||||
)
|
||||
|
||||
const outputPickerVisible = $derived(
|
||||
editMode && (isConnectingCandidate || alwaysShowOutputPicker) && !!id
|
||||
)
|
||||
|
||||
const icon_render = $derived(icon)
|
||||
</script>
|
||||
|
||||
@@ -223,22 +252,36 @@
|
||||
</Drawer>
|
||||
{/if}
|
||||
|
||||
{#if deletable && id && flowEditorContext?.flowStore && outputPickerVisible}
|
||||
{@const flowStore = flowEditorContext?.flowStore.val}
|
||||
{@const mod = flowStore?.value ? dfsPreviousResults(id, flowStore, false)[0] : undefined}
|
||||
{#if mod && $flowStateStore[id]}
|
||||
<ModuleTest bind:this={moduleTest} {mod} bind:testIsLoading bind:testJob />
|
||||
{/if}
|
||||
{/if}
|
||||
|
||||
<!-- svelte-ignore a11y_click_events_have_key_events -->
|
||||
<!-- svelte-ignore a11y_no_static_element_interactions -->
|
||||
<div
|
||||
class={classNames(
|
||||
'w-full module flex rounded-sm cursor-pointer max-w-full outline-offset-0 outline-slate-500 dark:outline-gray-400',
|
||||
selected ? 'outline outline-2' : 'active:outline active:outline-2',
|
||||
'flex relative'
|
||||
)}
|
||||
style="width: 275px; height: 38px; background-color: {hover && bgHoverColor
|
||||
class={classNames('w-full module flex rounded-sm cursor-pointer max-w-full ', 'flex relative')}
|
||||
style="width: 275px; height: 34px; background-color: {hover && bgHoverColor
|
||||
? bgHoverColor
|
||||
: bgColor};"
|
||||
onmouseenter={() => (hover = true)}
|
||||
onmouseleave={() => (hover = false)}
|
||||
onpointerdown={stopPropagation(preventDefault(() => dispatch('pointerdown')))}
|
||||
>
|
||||
<div class="absolute text-sm right-12 -bottom-3 flex flex-row gap-1 z-10">
|
||||
<div
|
||||
class={classNames(
|
||||
'absolute rounded-sm outline-offset-0 outline-slate-500 dark:outline-gray-400',
|
||||
selected ? 'outline outline-2' : 'active:outline active:outline-2'
|
||||
)}
|
||||
style={`width: 275px; height: ${outputPickerVisible ? '51px' : '34px'};`}
|
||||
></div>
|
||||
<div
|
||||
class="absolute text-sm right-2 flex flex-row gap-1 z-10 transition-all duration-100"
|
||||
style={`bottom: ${outputPickerBarOpen ? '-38px' : '-12px'}`}
|
||||
>
|
||||
{#if retry}
|
||||
<Popover notClickable>
|
||||
<div
|
||||
@@ -358,13 +401,18 @@
|
||||
{/snippet}
|
||||
</FlowModuleSchemaItemViewer>
|
||||
|
||||
{#if editMode && (isConnectingCandidate || alwaysShowOutputPicker)}
|
||||
{#if outputPickerVisible}
|
||||
<OutputPicker
|
||||
bind:this={outputPicker}
|
||||
{selected}
|
||||
{hover}
|
||||
{isConnectingCandidate}
|
||||
{historyOpen}
|
||||
{inputTransform}
|
||||
id={id ?? ''}
|
||||
bind:bottomBarOpen={outputPickerBarOpen}
|
||||
{loopStatus}
|
||||
{onEditInput}
|
||||
>
|
||||
{#snippet children({ allowCopy, isConnecting, selectConnection })}
|
||||
<OutputPickerInner
|
||||
@@ -372,9 +420,9 @@
|
||||
prefix={'results'}
|
||||
connectingData={isConnecting ? connectingData : undefined}
|
||||
{mock}
|
||||
{lastJob}
|
||||
onSelect={selectConnection}
|
||||
lastJob={nlastJob}
|
||||
moduleId={id}
|
||||
onSelect={selectConnection}
|
||||
{onUpdateMock}
|
||||
{path}
|
||||
{loopStatus}
|
||||
@@ -382,6 +430,7 @@
|
||||
bind:derivedHistoryOpen={historyOpen}
|
||||
historyOffset={{ mainAxis: 12, crossAxis: -9 }}
|
||||
clazz="p-1"
|
||||
isLoading={testIsLoading}
|
||||
/>
|
||||
{/snippet}
|
||||
</OutputPicker>
|
||||
@@ -389,10 +438,60 @@
|
||||
</div>
|
||||
|
||||
{#if deletable}
|
||||
<div
|
||||
class="absolute top-1/2 -translate-y-1/2 -translate-x-[100%] -left-[0] flex items-center w-fit px-2 h-9 min-w-14"
|
||||
>
|
||||
{#if (hover || selected) && outputPickerVisible}
|
||||
<div transition:fade={{ duration: 100 }}>
|
||||
{#if !testIsLoading}
|
||||
<Button
|
||||
size="sm"
|
||||
color="dark"
|
||||
title="Run"
|
||||
btnClasses="p-1.5"
|
||||
on:click={() => {
|
||||
outputPicker?.toggleOpen(true)
|
||||
moduleTest?.loadArgsAndRunTest()
|
||||
}}
|
||||
dropdownItems={[
|
||||
{
|
||||
label: 'Test up to here',
|
||||
onClick: () => {
|
||||
if (id) {
|
||||
onTestUpTo?.(id)
|
||||
}
|
||||
}
|
||||
}
|
||||
]}
|
||||
dropdownBtnClasses="!w-4 px-1"
|
||||
>
|
||||
{#if testIsLoading}
|
||||
<Loader2 size={12} class="animate-spin" />
|
||||
{:else}
|
||||
<Play size={12} />
|
||||
{/if}
|
||||
</Button>
|
||||
{:else}
|
||||
<Button
|
||||
size="xs"
|
||||
color="red"
|
||||
variant="contained"
|
||||
btnClasses="!h-[25.5px] !w-[44.5px] !p-1.5 gap-0.5"
|
||||
on:click={async () => {
|
||||
moduleTest?.cancelJob()
|
||||
}}
|
||||
>
|
||||
<Loader2 size={10} class="animate-spin mr-0.5" />
|
||||
<X size={14} />
|
||||
</Button>
|
||||
{/if}
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
<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-red-400 hover:text-white
|
||||
{hover || selected ? '' : '!hidden'}"
|
||||
outline-[1px] outline dark:outline-gray-500 outline-gray-300 bg-surface duration-0 hover:bg-red-400 hover:text-white
|
||||
{hover || selected ? '' : '!hidden'}"
|
||||
title="Delete"
|
||||
onclick={stopPropagation(
|
||||
preventDefault((event) => dispatch('delete', { id, type: modType }))
|
||||
|
||||
@@ -48,6 +48,8 @@
|
||||
newFlow?: boolean
|
||||
smallErrorHandler?: boolean
|
||||
workspace?: string | undefined
|
||||
onTestUpTo?: ((id: string) => void) | undefined
|
||||
onEditInput?: (moduleId: string, key: string) => void
|
||||
}
|
||||
|
||||
let {
|
||||
@@ -58,7 +60,9 @@
|
||||
disableSettings = false,
|
||||
newFlow = false,
|
||||
smallErrorHandler = false,
|
||||
workspace = $workspaceStore
|
||||
workspace = $workspaceStore,
|
||||
onTestUpTo,
|
||||
onEditInput
|
||||
}: Props = $props()
|
||||
|
||||
let flowTutorials: FlowTutorials | undefined = $state(undefined)
|
||||
@@ -343,6 +347,8 @@
|
||||
{flowInputsStore}
|
||||
{workspace}
|
||||
editMode
|
||||
{onTestUpTo}
|
||||
{onEditInput}
|
||||
onDelete={(id) => {
|
||||
dependents = getDependentComponents(id, flowStore.val)
|
||||
const cb = () => {
|
||||
|
||||
@@ -39,10 +39,12 @@
|
||||
editMode?: boolean
|
||||
onSelectedIteration: onSelectedIteration
|
||||
onSelect: (id: string | FlowModule) => void
|
||||
onTestUpTo?: ((id: string) => void) | undefined
|
||||
onUpdateMock?: (detail: {
|
||||
id: string
|
||||
mock: { enabled: boolean; return_value?: unknown }
|
||||
}) => void
|
||||
onEditInput?: (moduleId: string, key: string) => void
|
||||
}
|
||||
|
||||
let {
|
||||
@@ -59,7 +61,9 @@
|
||||
flowJobs,
|
||||
editMode = false,
|
||||
onSelect,
|
||||
onUpdateMock
|
||||
onTestUpTo,
|
||||
onUpdateMock,
|
||||
onEditInput
|
||||
}: Props = $props()
|
||||
|
||||
const { selectedId } = getContext<{
|
||||
@@ -155,6 +159,7 @@
|
||||
: ''}
|
||||
alwaysShowOutputPicker={!mod.id.startsWith('subflow:')}
|
||||
loopStatus={{ type: 'self', flow: mod.value.type }}
|
||||
{onTestUpTo}
|
||||
>
|
||||
{#snippet icon()}
|
||||
<div>
|
||||
@@ -175,6 +180,7 @@
|
||||
label={mod.summary || 'Run one branch'}
|
||||
{bgColor}
|
||||
{bgHoverColor}
|
||||
{onTestUpTo}
|
||||
>
|
||||
{#snippet icon()}
|
||||
<div>
|
||||
@@ -195,6 +201,7 @@
|
||||
label={mod.summary || `Run all branches${mod.value.parallel ? ' (parallel)' : ''}`}
|
||||
{bgColor}
|
||||
{bgHoverColor}
|
||||
{onTestUpTo}
|
||||
>
|
||||
{#snippet icon()}
|
||||
<div>
|
||||
@@ -236,6 +243,9 @@
|
||||
isTrigger={isTriggerStep(mod)}
|
||||
alwaysShowOutputPicker={!mod.id.startsWith('subflow:') && mod.id !== 'preprocessor'}
|
||||
loopStatus={parentLoop ? { type: 'inside', flow: parentLoop.type } : undefined}
|
||||
inputTransform={mod.value.type !== 'identity' ? mod.value.input_transforms : undefined}
|
||||
{onTestUpTo}
|
||||
{onEditInput}
|
||||
>
|
||||
{#snippet icon()}
|
||||
<div>
|
||||
|
||||
@@ -27,6 +27,7 @@
|
||||
editMode?: boolean
|
||||
icon?: import('svelte').Snippet
|
||||
onUpdateMock?: (mock: { enabled: boolean; return_value?: unknown }) => void
|
||||
onEditInput?: (moduleId: string, key: string) => void
|
||||
}
|
||||
|
||||
let {
|
||||
@@ -47,11 +48,25 @@
|
||||
earlyStop = false,
|
||||
editMode = false,
|
||||
icon,
|
||||
onUpdateMock
|
||||
onUpdateMock,
|
||||
onEditInput
|
||||
}: Props = $props()
|
||||
|
||||
const outputPickerVisible = $derived(
|
||||
(alwaysPluggable || (inputJson && Object.keys(inputJson).length > 0)) && editMode
|
||||
)
|
||||
</script>
|
||||
|
||||
<VirtualItemWrapper {label} {bgColor} {bgHoverColor} {selected} {selectable} {id} on:select>
|
||||
<VirtualItemWrapper
|
||||
{label}
|
||||
{bgColor}
|
||||
{bgHoverColor}
|
||||
{selected}
|
||||
{selectable}
|
||||
{id}
|
||||
outputPickerVisible={outputPickerVisible ?? false}
|
||||
on:select
|
||||
>
|
||||
{#snippet children({ hover })}
|
||||
<div class="flex flex-col w-full">
|
||||
<div
|
||||
@@ -80,8 +95,14 @@
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
{#if (alwaysPluggable || (inputJson && Object.keys(inputJson).length > 0)) && editMode}
|
||||
<OutputPicker {selected} {hover} isConnectingCandidate={true} variant="virtual">
|
||||
{#if outputPickerVisible}
|
||||
<OutputPicker
|
||||
{selected}
|
||||
{hover}
|
||||
id={id ?? ''}
|
||||
isConnectingCandidate={true}
|
||||
variant="virtual"
|
||||
>
|
||||
{#snippet children({ allowCopy, isConnecting, selectConnection })}
|
||||
<OutputPickerInner
|
||||
{allowCopy}
|
||||
@@ -95,6 +116,8 @@
|
||||
rightMargin
|
||||
historyOffset={{ mainAxis: 12, crossAxis: -9 }}
|
||||
clazz="p-1"
|
||||
{onEditInput}
|
||||
selectionId={id ?? label ?? ''}
|
||||
/>
|
||||
{/snippet}
|
||||
</OutputPicker>
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
import type { FlowModule } from '$lib/gen'
|
||||
import { classNames } from '$lib/utils'
|
||||
import { createEventDispatcher } from 'svelte'
|
||||
import { twMerge } from 'tailwind-merge'
|
||||
|
||||
interface Props {
|
||||
label: string | undefined
|
||||
@@ -12,6 +13,7 @@
|
||||
bgColor: string
|
||||
bgHoverColor?: string
|
||||
children?: import('svelte').Snippet<[any]>
|
||||
outputPickerVisible?: boolean
|
||||
}
|
||||
|
||||
let {
|
||||
@@ -22,7 +24,8 @@
|
||||
onTop = false,
|
||||
bgColor,
|
||||
bgHoverColor = '',
|
||||
children
|
||||
children,
|
||||
outputPickerVisible = false
|
||||
}: Props = $props()
|
||||
|
||||
const dispatch = createEventDispatcher<{
|
||||
@@ -41,14 +44,8 @@
|
||||
<!-- svelte-ignore a11y_click_events_have_key_events -->
|
||||
<!-- svelte-ignore a11y_no_static_element_interactions -->
|
||||
<div
|
||||
class={classNames(
|
||||
'w-full flex relative rounded-sm',
|
||||
selectable ? 'cursor-pointer active:outline active:outline-2' : '',
|
||||
selected ? 'outline outline-2' : '',
|
||||
onTop ? 'z-[901]' : '',
|
||||
'outline-offset-1 outline-gray-600 dark:outline-gray-400'
|
||||
)}
|
||||
style="width: 275px; max-height: 38px; background-color: {hover && bgHoverColor && selectable
|
||||
class={classNames('w-full flex relative rounded-sm', onTop ? 'z-[901]' : '')}
|
||||
style="width: 275px; max-height: 34px; background-color: {hover && bgHoverColor && selectable
|
||||
? bgHoverColor
|
||||
: bgColor};"
|
||||
onpointerdown={() => {
|
||||
@@ -64,5 +61,15 @@
|
||||
}}
|
||||
title={label ? label + ' ' : ''}
|
||||
id={`flow-editor-virtual-${encodeURIComponent(label || label || '')}`}
|
||||
>{@render children?.({ hover })}</div
|
||||
>
|
||||
<div
|
||||
class={twMerge(
|
||||
'absolute outline-gray-600 dark:outline-gray-400 rounded-sm',
|
||||
selected ? 'outline outline-2' : '',
|
||||
selectable ? 'cursor-pointer active:outline active:outline-2' : ''
|
||||
)}
|
||||
style={`width: 275px; height: ${outputPickerVisible ? '50px' : '34px'};`}
|
||||
>
|
||||
</div>
|
||||
{@render children?.({ hover })}
|
||||
</div>
|
||||
|
||||
@@ -88,7 +88,10 @@ function getFlowInput(
|
||||
}
|
||||
} else {
|
||||
let parentFlowInput = getFlowInput(parentModules, flowState, args, schema)
|
||||
if (parentModule.value.type === 'forloopflow' || parentModule.value.type === 'whileloopflow') {
|
||||
if (
|
||||
parentModule.value.type === 'forloopflow' ||
|
||||
parentModule.value.type === 'whileloopflow'
|
||||
) {
|
||||
let parentFlowInputIter = { ...parentFlowInput }
|
||||
if (parentFlowInputIter.hasOwnProperty('iter')) {
|
||||
parentFlowInputIter['iter_parent'] = parentFlowInputIter['iter']
|
||||
@@ -205,8 +208,8 @@ export function getStepPropPicker(
|
||||
return [
|
||||
id,
|
||||
module?.mock?.enabled
|
||||
? module.mock.return_value ?? {}
|
||||
: flowState[id]?.previewResult ?? {}
|
||||
? (module.mock.return_value ?? {})
|
||||
: (flowState[id]?.previewResult ?? {})
|
||||
]
|
||||
})
|
||||
.reverse()
|
||||
@@ -323,3 +326,46 @@ export function filterNestedObject(obj: any, nestedKeys: string[]) {
|
||||
}
|
||||
return {}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the ID of the previous module within the same container (loop or branch)
|
||||
* based on the same logic used in FlowModuleWrapper.svelte
|
||||
*/
|
||||
export function getPreviousModule(moduleId: string, flow: OpenFlow): FlowModule | undefined {
|
||||
function searchInModules(modules: FlowModule[]): FlowModule | undefined | null {
|
||||
for (let i = 0; i < modules.length; i++) {
|
||||
const module = modules[i]
|
||||
|
||||
if (module.id === moduleId) {
|
||||
// Found the module, return previous module ID if it exists
|
||||
return i > 0 ? modules[i - 1] : undefined
|
||||
}
|
||||
|
||||
// Search in submodules based on module type
|
||||
if (module.value.type === 'forloopflow' || module.value.type === 'whileloopflow') {
|
||||
const result = searchInModules(module.value.modules)
|
||||
if (result !== null) return result
|
||||
} else if (module.value.type === 'branchone') {
|
||||
// Search in default branch
|
||||
const defaultResult = searchInModules(module.value.default)
|
||||
if (defaultResult !== null) return defaultResult
|
||||
|
||||
// Search in each branch
|
||||
for (const branch of module.value.branches) {
|
||||
const branchResult = searchInModules(branch.modules)
|
||||
if (branchResult !== null) return branchResult
|
||||
}
|
||||
} else if (module.value.type === 'branchall') {
|
||||
// Search in each branch
|
||||
for (const branch of module.value.branches) {
|
||||
const branchResult = searchInModules(branch.modules)
|
||||
if (branchResult !== null) return branchResult
|
||||
}
|
||||
}
|
||||
}
|
||||
return null // Continue searching (module not found in this branch)
|
||||
}
|
||||
|
||||
const result = searchInModules(flow.value.modules)
|
||||
return result === null ? undefined : result
|
||||
}
|
||||
|
||||
@@ -0,0 +1,66 @@
|
||||
<script lang="ts">
|
||||
import type { FlowEditorContext } from '../types'
|
||||
import { getContext, onMount } from 'svelte'
|
||||
import ObjectViewer from '$lib/components/propertyPicker/ObjectViewer.svelte'
|
||||
import { twMerge } from 'tailwind-merge'
|
||||
import { DollarSign, Pencil, RefreshCw, SquareFunction } from 'lucide-svelte'
|
||||
|
||||
interface Props {
|
||||
inputTransform: Record<string, any> | undefined
|
||||
id: string
|
||||
onEditInput?: (moduleId: string, key: string) => void
|
||||
}
|
||||
|
||||
let { inputTransform, id, onEditInput }: Props = $props()
|
||||
|
||||
const { flowStore, flowStateStore, testSteps, previewArgs } =
|
||||
getContext<FlowEditorContext | undefined>('FlowEditorContext') || {}
|
||||
|
||||
onMount(() => {
|
||||
testSteps?.updateStepArgs(id, $flowStateStore, flowStore?.val, previewArgs?.val)
|
||||
})
|
||||
|
||||
const input = $derived(testSteps?.getStepArgs(id)?.value)
|
||||
</script>
|
||||
|
||||
<div class="p-4 pr-6 h-full overflow-y-auto">
|
||||
<ObjectViewer json={input} {inputTransform} {metaData} {editKey} />
|
||||
</div>
|
||||
|
||||
{#snippet metaData(key: string)}
|
||||
{#if inputTransform?.[key]}
|
||||
<span
|
||||
class={twMerge(
|
||||
'inline-flex items-center h-4 border px-1 rounded-[0.275rem] rounded-l-none border-l-0 gap-0.5',
|
||||
'text-2xs'
|
||||
)}
|
||||
title={inputTransform[key].type === 'javascript' ? inputTransform[key].expr : 'Static'}
|
||||
>
|
||||
{#if inputTransform[key].type === 'javascript'}
|
||||
<SquareFunction size={14} class="text-blue-500 -my-1 dark:text-blue-400" />
|
||||
{:else if inputTransform[key].type === 'static'}
|
||||
<DollarSign size={12} class="text-tertiary font-mono -my-1" />
|
||||
{/if}
|
||||
{#if testSteps?.isArgManuallySet(id, key)}
|
||||
<button
|
||||
onclick={() => {
|
||||
testSteps?.evalArg(id, key, $flowStateStore, flowStore?.val, previewArgs?.val)
|
||||
}}
|
||||
title="Re-evaluate input"
|
||||
class="-my-1 ml-0.5 hover:text-primary dark:hover:text-primary dark:text-gray-500 text-gray-300"
|
||||
>
|
||||
<RefreshCw size={12} />
|
||||
</button>
|
||||
{/if}
|
||||
</span>
|
||||
{/if}
|
||||
{/snippet}
|
||||
|
||||
{#snippet editKey(key: string)}
|
||||
<button
|
||||
onclick={() => onEditInput?.(id, key)}
|
||||
class="h-4 w-fit items-center text-gray-300 dark:text-gray-500 hover:text-primary dark:hover:text-primary px-1 rounded-[0.275rem] align-baseline"
|
||||
>
|
||||
<Pencil size={12} class="-my-1 inline-flex items-center" />
|
||||
</button>
|
||||
{/snippet}
|
||||
@@ -1,10 +1,12 @@
|
||||
<script lang="ts">
|
||||
import { ChevronDown } from 'lucide-svelte'
|
||||
import Popover from '$lib/components/meltComponents/Popover.svelte'
|
||||
import { twMerge } from 'tailwind-merge'
|
||||
import { getContext } from 'svelte'
|
||||
import type { PropPickerContext } from '$lib/components/prop_picker'
|
||||
import AnimatedButton from '$lib/components/common/button/AnimatedButton.svelte'
|
||||
import InputPickerInner from './InputPickerInner.svelte'
|
||||
import { ChevronDown, Plug } from 'lucide-svelte'
|
||||
import { useSvelteFlow } from '@xyflow/svelte'
|
||||
|
||||
interface Props {
|
||||
selected?: boolean
|
||||
@@ -13,6 +15,11 @@
|
||||
variant?: 'default' | 'virtual'
|
||||
historyOpen?: boolean
|
||||
children?: import('svelte').Snippet<[any]>
|
||||
inputTransform?: Record<string, any> | undefined
|
||||
id: string
|
||||
bottomBarOpen?: boolean
|
||||
loopStatus?: { type: 'inside' | 'self'; flow: 'forloopflow' | 'whileloopflow' } | undefined
|
||||
onEditInput?: (moduleId: string, key: string) => void
|
||||
}
|
||||
|
||||
let {
|
||||
@@ -21,7 +28,12 @@
|
||||
isConnectingCandidate = false,
|
||||
variant = 'default',
|
||||
historyOpen = false,
|
||||
children
|
||||
children,
|
||||
inputTransform,
|
||||
id,
|
||||
bottomBarOpen = $bindable(false),
|
||||
loopStatus,
|
||||
onEditInput
|
||||
}: Props = $props()
|
||||
|
||||
const context = getContext<PropPickerContext>('PropPickerContext')
|
||||
@@ -29,6 +41,11 @@
|
||||
const MIN_WIDTH = 375
|
||||
const MIN_HEIGHT = 375
|
||||
|
||||
let outputOpen = $state(false)
|
||||
let inputOpen = $state(false)
|
||||
|
||||
const zoom = $derived.by(useSvelteFlow().getZoom)
|
||||
|
||||
let showConnecting = $derived(
|
||||
isConnectingCandidate && $flowPropPickerConfig?.insertionMode === 'connect'
|
||||
)
|
||||
@@ -40,98 +57,178 @@
|
||||
}
|
||||
}
|
||||
|
||||
let inputPopover: Popover | undefined = $state(undefined)
|
||||
let popover: Popover | undefined = $state(undefined)
|
||||
|
||||
const virtualItemClasses = {
|
||||
bar: 'dark:hover:bg-[#525d6f] dark:bg-[#414958] bg-[#d7dfea] hover:bg-slate-300',
|
||||
handle:
|
||||
'dark:group-hover:bg-[#525d6f] dark:hover:bg-[#525d6f] dark:bg-[#414958] bg-[#d7dfea] hover:bg-slate-300 group-hover:bg-slate-300'
|
||||
bar: 'dark:hover:bg-[#525d6f] dark:bg-[#414958] bg-[#d7dfea] hover:bg-slate-300'
|
||||
}
|
||||
|
||||
const defaultClasses = {
|
||||
bar: 'bg-surface-disabled hover:bg-surface-hover dark:bg-[#454e5f] dark:hover:bg-[#576278]',
|
||||
handle:
|
||||
'group-hover:bg-surface-hover hover:bg-surface-hover bg-surface-disabled dark:bg-[#454e5f] dark:hover:bg-[#576278] dark:group-hover:bg-[#576278]'
|
||||
bar: 'bg-surface-disabled hover:bg-surface-hover dark:bg-[#454e5f] dark:hover:bg-[#576278]'
|
||||
}
|
||||
|
||||
export function toggleOpen() {
|
||||
if (popover?.isOpened()) {
|
||||
export function toggleOpen(forceOpen: boolean = false) {
|
||||
if (popover?.isOpened() && !forceOpen) {
|
||||
popover?.close()
|
||||
} else {
|
||||
popover?.open()
|
||||
}
|
||||
}
|
||||
|
||||
$effect(() => {
|
||||
bottomBarOpen = inputOpen || outputOpen || selected || hover || showConnecting
|
||||
})
|
||||
|
||||
const showInput = $derived(
|
||||
variant === 'default' && !showConnecting && loopStatus?.type !== 'self'
|
||||
)
|
||||
|
||||
function updatePositioning(historyOpen: boolean, zoom: number) {
|
||||
inputPopover?.updatePositioning({
|
||||
placement: 'bottom',
|
||||
gutter: 0,
|
||||
offset: { mainAxis: 3, crossAxis: 69 * zoom },
|
||||
overflowPadding: historyOpen ? 250 : 8
|
||||
})
|
||||
popover?.updatePositioning({
|
||||
placement: 'bottom',
|
||||
gutter: 0,
|
||||
offset: { mainAxis: 3, crossAxis: showInput ? -69 * zoom : 0 },
|
||||
overflowPadding: historyOpen ? 250 : 8
|
||||
})
|
||||
}
|
||||
|
||||
$effect(() => {
|
||||
updatePositioning(historyOpen, zoom)
|
||||
})
|
||||
</script>
|
||||
|
||||
<Popover
|
||||
floatingConfig={{
|
||||
placement: 'bottom',
|
||||
overflowPadding: historyOpen ? 250 : 8
|
||||
}}
|
||||
usePointerDownOutside
|
||||
closeOnOutsideClick={false}
|
||||
on:click={(e) => {
|
||||
<div
|
||||
class="relative h-1 w-[275px]"
|
||||
onpointerdown={(e) => {
|
||||
e.preventDefault()
|
||||
e.stopPropagation()
|
||||
}}
|
||||
bind:this={popover}
|
||||
allowFullScreen
|
||||
contentClasses="overflow-hidden resize rounded-md"
|
||||
contentStyle={`width: calc(${MIN_WIDTH}px); min-width: calc(${MIN_WIDTH}px); height: calc(${MIN_HEIGHT}px); min-height: calc(${MIN_HEIGHT}px);`}
|
||||
extraProps={{ 'data-prop-picker': true }}
|
||||
closeOnOtherPopoverOpen
|
||||
class="outline-none"
|
||||
>
|
||||
{#snippet trigger({ isOpen })}
|
||||
<div
|
||||
class={twMerge(
|
||||
'bg-slate-200',
|
||||
`w-[275px] h-[4px] flex flex-row items-center justify-center cursor-pointer`,
|
||||
variant === 'virtual' ? virtualItemClasses.bar : defaultClasses.bar,
|
||||
'shadow-[inset_0_1px_5px_0_rgba(0,0,0,0.05)] rounded-b-sm',
|
||||
'group'
|
||||
)}
|
||||
onpointerdown={(e) => {
|
||||
e.preventDefault()
|
||||
e.stopPropagation()
|
||||
}}
|
||||
data-prop-picker
|
||||
title={`${isOpen ? 'Close' : 'Open'} step output`}
|
||||
>
|
||||
<div class="absolute bottom-0 left-1/2 -translate-x-1/2 w-10 h-[14px]">
|
||||
<AnimatedButton
|
||||
animate={showConnecting}
|
||||
wrapperClasses="relative w-full h-full center-center"
|
||||
baseRadius="6px"
|
||||
marginWidth="1px"
|
||||
<!-- Invisible hover area to maintain consistent height -->
|
||||
<div class="absolute w-full h-[20px]"></div>
|
||||
<div
|
||||
class={twMerge(
|
||||
'bg-slate-200 absolute w-full',
|
||||
variant === 'virtual'
|
||||
? `${virtualItemClasses.bar} ${bottomBarOpen ? 'bg-slate-300 dark:bg-[#525d6f]' : ''}`
|
||||
: `${defaultClasses.bar} ${bottomBarOpen ? 'bg-surface-hover dark:bg-[#576278]' : ''}`,
|
||||
'shadow-[inset_0_1px_5px_0_rgba(0,0,0,0.05)] rounded-b-sm',
|
||||
'group transition-all duration-100',
|
||||
'flex flex-row items-center justify-center',
|
||||
'h-1 hover:h-[20px]',
|
||||
bottomBarOpen && 'h-[20px]'
|
||||
)}
|
||||
data-prop-picker
|
||||
>
|
||||
<div class="flex flex-row items-center justify-center w-full h-full">
|
||||
{#if showInput}
|
||||
<Popover
|
||||
floatingConfig={{
|
||||
placement: 'bottom',
|
||||
gutter: 0,
|
||||
offset: { mainAxis: 3, crossAxis: 69 },
|
||||
overflowPadding: historyOpen ? 250 : 8
|
||||
}}
|
||||
usePointerDownOutside
|
||||
closeOnOutsideClick={false}
|
||||
on:click={(e) => {
|
||||
e.preventDefault()
|
||||
e.stopPropagation()
|
||||
}}
|
||||
allowFullScreen
|
||||
contentClasses="overflow-hidden resize"
|
||||
contentStyle={`width: calc(${MIN_WIDTH}px); min-width: calc(${MIN_WIDTH}px); height: calc(${MIN_HEIGHT}px); min-height: calc(${MIN_HEIGHT}px); `}
|
||||
extraProps={{ 'data-prop-picker': true }}
|
||||
closeOnOtherPopoverOpen
|
||||
disableFocusTrap
|
||||
class="flex-1 h-full"
|
||||
bind:isOpen={inputOpen}
|
||||
bind:this={inputPopover}
|
||||
>
|
||||
<div
|
||||
class={twMerge(
|
||||
'w-full h-full rounded-t-md shadow-[inset_0_1px_5px_0_rgba(0,0,0,0.05)]',
|
||||
`hidden group-hover:center-center`,
|
||||
variant === 'virtual' ? virtualItemClasses.handle : defaultClasses.handle,
|
||||
isOpen || selected || hover || showConnecting ? 'center-center' : 'hidden',
|
||||
showConnecting ? 'text-blue-500 bg-surface rounded-b-md' : 'text-secondary'
|
||||
{#snippet trigger({ isOpen })}
|
||||
<button
|
||||
class={twMerge(
|
||||
'h-full center-center transition-opacity duration-150 w-full',
|
||||
bottomBarOpen ? 'opacity-100' : 'opacity-0',
|
||||
'text-2xs font-normal w-full h-full border-t-2 border-transparent',
|
||||
inputOpen ? 'border-primary' : 'hover:border-primary/20'
|
||||
)}
|
||||
>
|
||||
In
|
||||
</button>
|
||||
{/snippet}
|
||||
{#snippet content()}
|
||||
<InputPickerInner {inputTransform} {id} {onEditInput} />
|
||||
{/snippet}
|
||||
</Popover>
|
||||
{/if}
|
||||
<Popover
|
||||
floatingConfig={{
|
||||
placement: 'bottom',
|
||||
gutter: 0,
|
||||
offset: { mainAxis: 3, crossAxis: showInput ? -69 : 0 },
|
||||
overflowPadding: historyOpen ? 250 : 8
|
||||
}}
|
||||
usePointerDownOutside
|
||||
closeOnOutsideClick={false}
|
||||
on:click={(e) => {
|
||||
e.preventDefault()
|
||||
e.stopPropagation()
|
||||
}}
|
||||
bind:this={popover}
|
||||
allowFullScreen
|
||||
contentClasses="overflow-hidden resize"
|
||||
contentStyle={`width: calc(${MIN_WIDTH}px); min-width: calc(${MIN_WIDTH}px); height: calc(${MIN_HEIGHT}px); min-height: calc(${MIN_HEIGHT}px); `}
|
||||
extraProps={{ 'data-prop-picker': true }}
|
||||
closeOnOtherPopoverOpen
|
||||
class="flex-1 h-full"
|
||||
bind:isOpen={outputOpen}
|
||||
>
|
||||
{#snippet trigger({ isOpen })}
|
||||
<AnimatedButton
|
||||
animate={showConnecting}
|
||||
wrapperClasses={twMerge(
|
||||
'h-full center-center transition-opacity duration-150 w-full',
|
||||
bottomBarOpen ? 'opacity-100' : 'opacity-0'
|
||||
)}
|
||||
baseRadius="2px"
|
||||
marginWidth="1px"
|
||||
>
|
||||
<ChevronDown
|
||||
size={12}
|
||||
class="h-fit transition-transform duration-100"
|
||||
style={`transform: rotate(${isOpen ? '180deg' : '0deg'})`}
|
||||
/>
|
||||
</div>
|
||||
</AnimatedButton>
|
||||
</div>
|
||||
<button
|
||||
class={twMerge(
|
||||
'text-2xs font-normal w-full h-full border-t-2 border-transparent',
|
||||
outputOpen ? 'border-primary' : 'hover:border-primary/20',
|
||||
showConnecting ? 'bg-surface-hover rounded-sm border-0' : ''
|
||||
)}
|
||||
>
|
||||
{#if showInput}
|
||||
Out
|
||||
{:else if showConnecting}
|
||||
<Plug size={12} class="w-full text-blue-500" />
|
||||
{:else}
|
||||
<ChevronDown size={12} class="w-full" />
|
||||
{/if}
|
||||
</button>
|
||||
</AnimatedButton>
|
||||
{/snippet}
|
||||
{#snippet content()}
|
||||
{@render children?.({
|
||||
allowCopy: !$flowPropPickerConfig,
|
||||
isConnecting: showConnecting,
|
||||
selectConnection
|
||||
})}
|
||||
{/snippet}
|
||||
</Popover>
|
||||
</div>
|
||||
{/snippet}
|
||||
{#snippet content()}
|
||||
{@render children?.({
|
||||
allowCopy: !$flowPropPickerConfig,
|
||||
isConnecting: showConnecting,
|
||||
selectConnection
|
||||
})}
|
||||
{/snippet}
|
||||
</Popover>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<style>
|
||||
@keyframes moveGradient {
|
||||
|
||||
@@ -16,7 +16,7 @@
|
||||
|
||||
<script lang="ts">
|
||||
import Button from '$lib/components/common/button/Button.svelte'
|
||||
import { Pin, History, Pen, Check, X, Loader2 } from 'lucide-svelte'
|
||||
import { Pin, History, Pen, Check, X, Loader2, Pencil } from 'lucide-svelte'
|
||||
import ObjectViewer from '$lib/components/propertyPicker/ObjectViewer.svelte'
|
||||
import StepHistory from './StepHistory.svelte'
|
||||
import { Popover } from '$lib/components/meltComponents'
|
||||
@@ -62,6 +62,8 @@
|
||||
copilot_fix?: import('svelte').Snippet
|
||||
onSelect?: (key: string) => void
|
||||
onUpdateMock?: (mock: { enabled: boolean; return_value?: unknown }) => void
|
||||
onEditInput?: (moduleId: string, key: string) => void
|
||||
selectionId?: string
|
||||
}
|
||||
|
||||
let {
|
||||
@@ -91,7 +93,9 @@
|
||||
clazz,
|
||||
copilot_fix,
|
||||
onSelect,
|
||||
onUpdateMock
|
||||
onUpdateMock,
|
||||
onEditInput,
|
||||
selectionId
|
||||
}: Props = $props()
|
||||
|
||||
type SelectedJob =
|
||||
@@ -127,6 +131,8 @@
|
||||
selectedJob = job
|
||||
} else if (lastJob && 'result' in lastJob) {
|
||||
selectedJob = lastJob
|
||||
} else {
|
||||
selectedJob = undefined
|
||||
}
|
||||
}
|
||||
|
||||
@@ -136,8 +142,9 @@
|
||||
}
|
||||
selectJob(lastJob)
|
||||
|
||||
if (lastJob.preview) {
|
||||
if (lastJob.preview && mock?.enabled) {
|
||||
preview = 'job'
|
||||
lastJob.preview = false
|
||||
}
|
||||
})
|
||||
|
||||
@@ -589,6 +596,7 @@
|
||||
onSelect?.(e.detail)
|
||||
}}
|
||||
{allowCopy}
|
||||
{editKey}
|
||||
/>
|
||||
{:else if jsonView}
|
||||
{#await import('$lib/components/JsonEditor.svelte')}
|
||||
@@ -694,6 +702,15 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{#snippet editKey(key: string)}
|
||||
<button
|
||||
onclick={() => onEditInput?.(selectionId ?? '', key)}
|
||||
class="h-4 w-fit items-center text-gray-300 dark:text-gray-500 hover:text-primary dark:hover:text-primary px-1 rounded-[0.275rem] align-baseline"
|
||||
>
|
||||
<Pencil size={12} class="-my-1 inline-flex items-center" />
|
||||
</button>
|
||||
{/snippet}
|
||||
|
||||
<style>
|
||||
.dbl-click-editable {
|
||||
cursor: text;
|
||||
|
||||
@@ -0,0 +1,183 @@
|
||||
import type { FlowModule, OpenFlow } from '$lib/gen'
|
||||
import type { FlowState } from './flowState'
|
||||
import {
|
||||
dfs,
|
||||
getPreviousModule,
|
||||
getStepPropPicker,
|
||||
type PickableProperties
|
||||
} from './previousResults'
|
||||
import { evalValue, type ModuleArgs } from './utils'
|
||||
|
||||
export class TestSteps {
|
||||
#stepsEvaluated = $state<Record<string, ModuleArgs>>({})
|
||||
#steps = $state<Record<string, { value: any }>>({})
|
||||
|
||||
constructor() {}
|
||||
|
||||
setStepArgsManually(moduleId: string, args: Record<string, any>) {
|
||||
if (!this.#steps[moduleId]) {
|
||||
this.#steps[moduleId] = { value: {} }
|
||||
}
|
||||
this.#steps[moduleId].value = args
|
||||
}
|
||||
|
||||
getStepArgs(moduleId: string): ModuleArgs | undefined {
|
||||
return this.#steps[moduleId]
|
||||
}
|
||||
|
||||
setStepArgs(moduleId: string, args: Record<string, any>) {
|
||||
if (!this.#steps[moduleId]) {
|
||||
this.#steps[moduleId] = { value: {} }
|
||||
}
|
||||
this.#steps[moduleId].value = args
|
||||
}
|
||||
|
||||
getStepArg(moduleId: string, argName: string): any | undefined {
|
||||
return this.#steps[moduleId]?.[argName]
|
||||
}
|
||||
|
||||
setEvaluatedStepArg(moduleId: string, argName: string, value: any) {
|
||||
if (!this.#steps[moduleId]) {
|
||||
this.#steps[moduleId] = { value: {} }
|
||||
}
|
||||
if (!this.#stepsEvaluated[moduleId]) {
|
||||
this.#stepsEvaluated[moduleId] = { value: {} }
|
||||
}
|
||||
this.#steps[moduleId].value[argName] = $state.snapshot(value)
|
||||
this.#stepsEvaluated[moduleId].value[argName] = $state.snapshot(value)
|
||||
}
|
||||
|
||||
isArgManuallySet(moduleId: string, argName: string): boolean {
|
||||
return (
|
||||
JSON.stringify(this.#steps[moduleId]?.value?.[argName]) !==
|
||||
JSON.stringify(this.#stepsEvaluated[moduleId]?.value?.[argName])
|
||||
)
|
||||
}
|
||||
|
||||
getManuallyEditedArgs(moduleId: string): string[] {
|
||||
const manuallyEditedArgs: string[] = []
|
||||
|
||||
const moduleArgs = this.#steps[moduleId]?.value ?? {}
|
||||
|
||||
Object.keys(moduleArgs).forEach((argName) => {
|
||||
if (this.isArgManuallySet(moduleId, argName)) {
|
||||
manuallyEditedArgs.push(argName)
|
||||
}
|
||||
})
|
||||
return manuallyEditedArgs
|
||||
}
|
||||
|
||||
/*
|
||||
Evaluate the arg value from the flow state and replace the test value.
|
||||
*/
|
||||
evalArg(
|
||||
moduleId: string,
|
||||
argName: string,
|
||||
flowState: FlowState | undefined,
|
||||
flow: OpenFlow | undefined,
|
||||
previewArgs: Record<string, any> | undefined
|
||||
) {
|
||||
if (!flowState || !flow) {
|
||||
return
|
||||
}
|
||||
const modules = dfs(moduleId, flow, true)
|
||||
const previousModule = getPreviousModule(moduleId, flow)
|
||||
if (modules.length < 1) {
|
||||
return
|
||||
}
|
||||
let parentModule: FlowModule | undefined = undefined
|
||||
if (modules.length > 1) {
|
||||
parentModule = modules[modules.length - 1]
|
||||
}
|
||||
const stepPropPicker = getStepPropPicker(
|
||||
flowState,
|
||||
parentModule,
|
||||
previousModule,
|
||||
moduleId,
|
||||
flow,
|
||||
previewArgs,
|
||||
false
|
||||
)
|
||||
const pickableProperties = stepPropPicker.pickableProperties
|
||||
|
||||
const argSnapshot = $state.snapshot(evalValue(argName, modules[0], pickableProperties, false))
|
||||
this.#stepsEvaluated[moduleId].value[argName] = argSnapshot
|
||||
this.#steps[moduleId].value[argName] = structuredClone(argSnapshot)
|
||||
}
|
||||
|
||||
initializeFromSchema(
|
||||
mod: FlowModule,
|
||||
schema: { properties?: Record<string, any> },
|
||||
pickableProperties: PickableProperties | undefined
|
||||
) {
|
||||
const args = Object.fromEntries(
|
||||
Object.keys(schema.properties ?? {}).map((k) => [
|
||||
k,
|
||||
evalValue(k, mod, pickableProperties, false)
|
||||
])
|
||||
)
|
||||
|
||||
const manuallyEditedArgs = this.getManuallyEditedArgs(mod.id)
|
||||
|
||||
if (!this.#steps[mod.id]) {
|
||||
this.#steps[mod.id] = { value: {} }
|
||||
}
|
||||
if (!this.#stepsEvaluated[mod.id]) {
|
||||
this.#stepsEvaluated[mod.id] = { value: {} }
|
||||
}
|
||||
this.#stepsEvaluated[mod.id].value = $state.snapshot(args)
|
||||
|
||||
// Preserve manually edited args
|
||||
const argsSnapshot = $state.snapshot(args)
|
||||
Object.keys(argsSnapshot).forEach((key) => {
|
||||
if (manuallyEditedArgs.includes(key)) {
|
||||
argsSnapshot[key] = this.#steps[mod.id]?.value?.[key]
|
||||
}
|
||||
})
|
||||
this.#steps[mod.id].value = argsSnapshot
|
||||
}
|
||||
|
||||
updateStepArgs(
|
||||
id: string,
|
||||
flowState: FlowState | undefined,
|
||||
flow: OpenFlow | undefined,
|
||||
previewArgs: Record<string, any> | undefined
|
||||
) {
|
||||
if (!flowState || !flow) {
|
||||
return
|
||||
}
|
||||
const modules = dfs(id, flow, true)
|
||||
const previousModule = getPreviousModule(id, flow)
|
||||
if (modules.length < 1) {
|
||||
return
|
||||
}
|
||||
let parentModule: FlowModule | undefined = undefined
|
||||
if (modules.length > 1) {
|
||||
parentModule = modules[modules.length - 1]
|
||||
}
|
||||
const stepPropPicker = getStepPropPicker(
|
||||
flowState,
|
||||
parentModule,
|
||||
previousModule,
|
||||
id,
|
||||
flow,
|
||||
previewArgs,
|
||||
false
|
||||
)
|
||||
const pickableProperties = stepPropPicker.pickableProperties
|
||||
this.initializeFromSchema(modules[0], flowState[id]?.schema ?? {}, pickableProperties)
|
||||
}
|
||||
|
||||
removeExtraKey(moduleId: string, keys: string[]) {
|
||||
if (!this.#stepsEvaluated[moduleId]) {
|
||||
return
|
||||
}
|
||||
const nargs = {}
|
||||
Object.keys(this.#stepsEvaluated[moduleId]?.value ?? {}).forEach((key) => {
|
||||
if (keys.includes(key)) {
|
||||
nargs[key] = this.#stepsEvaluated[moduleId]?.value?.[key]
|
||||
}
|
||||
})
|
||||
this.#stepsEvaluated[moduleId].value = nargs
|
||||
}
|
||||
}
|
||||
@@ -7,6 +7,7 @@ import type { FlowBuilderWhitelabelCustomUi } from '../custom_ui'
|
||||
import type Editor from '../Editor.svelte'
|
||||
import type SimpleEditor from '../SimpleEditor.svelte'
|
||||
import type { StateStore } from '$lib/utils'
|
||||
import type { TestSteps } from './testSteps.svelte'
|
||||
|
||||
export type FlowInput = Record<
|
||||
string,
|
||||
@@ -69,7 +70,7 @@ export type FlowEditorContext = {
|
||||
flowStore: StateStore<ExtendedOpenFlow>
|
||||
flowInputEditorState: Writable<FlowInputEditorState>
|
||||
flowStateStore: Writable<FlowState>
|
||||
testStepStore: Writable<Record<string, any>>
|
||||
testSteps: TestSteps
|
||||
saveDraft: () => void
|
||||
initialPathStore: Writable<string>
|
||||
fakeInitialPath: string
|
||||
|
||||
@@ -29,6 +29,8 @@ return ${eval_string}
|
||||
}`
|
||||
}
|
||||
|
||||
export type ModuleArgs = { value: Record<string, any> }
|
||||
|
||||
function make_context_evaluator(eval_string, context): (context) => any {
|
||||
let template = create_context_function_template(eval_string, context)
|
||||
let functor = Function(template)
|
||||
@@ -38,34 +40,29 @@ function make_context_evaluator(eval_string, context): (context) => any {
|
||||
export function evalValue(
|
||||
k: string,
|
||||
mod: FlowModule,
|
||||
testStepStore: Record<string, any>,
|
||||
pickableProperties: PickableProperties | undefined,
|
||||
showError: boolean
|
||||
) {
|
||||
): any {
|
||||
let inputTransforms = (mod.value['input_transforms'] ?? {}) as Record<string, InputTransform>
|
||||
let v = testStepStore[mod.id]?.[k]
|
||||
let v: any
|
||||
let t = inputTransforms?.[k]
|
||||
if (!v) {
|
||||
if (t.type == 'static') {
|
||||
v = t.value
|
||||
} else {
|
||||
try {
|
||||
let context = {
|
||||
flow_input: pickableProperties?.flow_input,
|
||||
results: pickableProperties?.priorIds
|
||||
}
|
||||
v = make_context_evaluator(t.expr, context)(context)
|
||||
} catch (e) {
|
||||
if (showError) {
|
||||
sendUserToast(`Error evaluating ${k}: ${e.message}`, true)
|
||||
}
|
||||
v = undefined
|
||||
|
||||
if (t.type == 'static') {
|
||||
v = t.value
|
||||
} else {
|
||||
try {
|
||||
let context = {
|
||||
flow_input: pickableProperties?.flow_input,
|
||||
results: pickableProperties?.priorIds
|
||||
}
|
||||
v = make_context_evaluator(t.expr, context)(context)
|
||||
} catch (e) {
|
||||
if (showError) {
|
||||
sendUserToast(`Error evaluating ${k}: ${e.message}`, true)
|
||||
}
|
||||
v = undefined
|
||||
}
|
||||
}
|
||||
if (v == NEVER_TESTED_THIS_FAR) {
|
||||
return undefined
|
||||
}
|
||||
return v
|
||||
}
|
||||
|
||||
|
||||
@@ -107,7 +107,9 @@
|
||||
onChangeId?: (detail: { id: string; newId: string; deps: Record<string, string[]> }) => void
|
||||
onMove?: (id: string) => void
|
||||
onUpdateMock?: (detail: { mock: FlowModule['mock']; id: string }) => void
|
||||
onTestUpTo?: ((id: string) => void) | undefined
|
||||
onSelectedIteration?: onSelectedIteration
|
||||
onEditInput?: (moduleId: string, key: string) => void
|
||||
}
|
||||
|
||||
let {
|
||||
@@ -145,7 +147,9 @@
|
||||
workspace = $workspaceStore ?? 'NO_WORKSPACE',
|
||||
editMode = false,
|
||||
allowSimplifiedPoll = true,
|
||||
expandedSubflows = $bindable({})
|
||||
expandedSubflows = $bindable({}),
|
||||
onTestUpTo = undefined,
|
||||
onEditInput = undefined
|
||||
}: Props = $props()
|
||||
|
||||
setContext<{
|
||||
@@ -301,6 +305,12 @@
|
||||
},
|
||||
updateMock: (detail) => {
|
||||
onUpdateMock?.(detail)
|
||||
},
|
||||
testUpTo: (id: string) => {
|
||||
onTestUpTo?.(id)
|
||||
},
|
||||
editInput: (moduleId: string, key: string) => {
|
||||
onEditInput?.(moduleId, key)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -52,6 +52,8 @@ export type GraphEventHandlers = {
|
||||
expandSubflow: (id: string, path: string) => void
|
||||
minimizeSubflow: (id: string) => void
|
||||
updateMock: (detail: { mock: FlowModule['mock']; id: string }) => void
|
||||
testUpTo: (id: string) => void
|
||||
editInput: (moduleId: string, key: string) => void
|
||||
}
|
||||
|
||||
export type SimplifiableFlow = { simplifiedFlow: boolean }
|
||||
|
||||
@@ -57,7 +57,7 @@
|
||||
)
|
||||
</script>
|
||||
|
||||
<EdgeLabel x={sourceX} y={sourceY + 22} class="base-edge" style="">
|
||||
<EdgeLabel x={sourceX} y={sourceY + 28} class="base-edge" style="">
|
||||
{#if data?.insertable && !$useDataflow && !data?.moving}
|
||||
<div
|
||||
class={twMerge('edgeButtonContainer nodrag nopan top-0')}
|
||||
|
||||
@@ -76,6 +76,7 @@
|
||||
cache={data.cache}
|
||||
earlyStop={data.earlyStop}
|
||||
editMode={data.editMode}
|
||||
onEditInput={data.eventHandlers.editInput}
|
||||
/>
|
||||
{/snippet}
|
||||
</NodeWrapper>
|
||||
|
||||
@@ -89,9 +89,11 @@
|
||||
onSelectedIteration={(e) => {
|
||||
data.eventHandlers.selectedIteration(e)
|
||||
}}
|
||||
onTestUpTo={data.eventHandlers.testUpTo}
|
||||
onUpdateMock={(detail) => {
|
||||
data.eventHandlers.updateMock(detail)
|
||||
}}
|
||||
onEditInput={data.eventHandlers.editInput}
|
||||
/>
|
||||
|
||||
<div class="absolute -bottom-10 left-1/2 transform -translate-x-1/2 z-10">
|
||||
|
||||
@@ -2,7 +2,7 @@ import type { FlowStatusModule } from '$lib/gen'
|
||||
|
||||
export const NODE = {
|
||||
width: 275,
|
||||
height: 38,
|
||||
height: 34,
|
||||
gap: {
|
||||
horizontal: 40,
|
||||
vertical: 50
|
||||
|
||||
@@ -16,6 +16,7 @@
|
||||
import { createEventDispatcher } from 'svelte'
|
||||
import { Button } from '$lib/components/common'
|
||||
import DocLink from '$lib/components/apps/editor/settingsPanel/DocLink.svelte'
|
||||
import type { FloatingConfig } from '@melt-ui/svelte/internal/actions/floating'
|
||||
|
||||
export let closeButton: boolean = false
|
||||
export let displayArrow: boolean = false
|
||||
@@ -33,6 +34,7 @@
|
||||
export let extraProps: Record<string, any> = {}
|
||||
export let disabled: boolean = false
|
||||
export let documentationLink: string | undefined = undefined
|
||||
export let disableFocusTrap: boolean = false
|
||||
|
||||
let fullScreen = false
|
||||
const dispatch = createEventDispatcher()
|
||||
@@ -45,6 +47,7 @@
|
||||
} = createPopover({
|
||||
forceVisible: true,
|
||||
portal,
|
||||
disableFocusTrap,
|
||||
onOpenChange: ({ curr, next }) => {
|
||||
if (curr != next) {
|
||||
dispatch('openChange', next)
|
||||
@@ -94,6 +97,12 @@
|
||||
return isOpen
|
||||
}
|
||||
|
||||
export function updatePositioning(pos: FloatingConfig) {
|
||||
if (positioning) {
|
||||
$positioning = pos
|
||||
}
|
||||
}
|
||||
|
||||
async function getMenuElements(): Promise<HTMLElement[]> {
|
||||
return Array.from(document.querySelectorAll('[data-popover]')) as HTMLElement[]
|
||||
}
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
|
||||
import { copyToClipboard, truncate } from '$lib/utils'
|
||||
|
||||
import { createEventDispatcher, untrack } from 'svelte'
|
||||
import { createEventDispatcher, untrack, type Snippet } from 'svelte'
|
||||
import { computeKey, keepByKeyOrValue } from './utils'
|
||||
import { NEVER_TESTED_THIS_FAR } from '../flows/models'
|
||||
import Portal from '$lib/components/Portal.svelte'
|
||||
@@ -13,6 +13,7 @@
|
||||
import { workspaceStore } from '$lib/stores'
|
||||
import AnimatedButton from '$lib/components/common/button/AnimatedButton.svelte'
|
||||
import Popover from '../Popover.svelte'
|
||||
import { twMerge } from 'tailwind-merge'
|
||||
|
||||
interface Props {
|
||||
json: any
|
||||
@@ -27,6 +28,8 @@
|
||||
prefix?: string
|
||||
expandedEvenOnLevel0?: string | undefined
|
||||
connecting?: boolean
|
||||
metaData?: Snippet<[any]>
|
||||
editKey?: Snippet<[any]>
|
||||
}
|
||||
|
||||
let {
|
||||
@@ -41,7 +44,9 @@
|
||||
collapseLevel = undefined,
|
||||
prefix = '',
|
||||
expandedEvenOnLevel0 = undefined,
|
||||
connecting = false
|
||||
connecting = false,
|
||||
metaData,
|
||||
editKey
|
||||
}: Props = $props()
|
||||
|
||||
let jsonFiltered = $state(json)
|
||||
@@ -248,11 +253,16 @@
|
||||
color="light"
|
||||
variant="border"
|
||||
wrapperClasses="p-0 whitespace-nowrap w-fit"
|
||||
btnClasses="font-mono h-4 py-1 text-2xs font-thin px-1 rounded-[0.275rem]"
|
||||
btnClasses={twMerge(
|
||||
'font-mono h-4 py-1 text-2xs',
|
||||
'font-thin px-1 rounded-[0.275rem]',
|
||||
metaData ? 'rounded-r-none border-r-0.5' : ''
|
||||
)}
|
||||
title={computeFullKey(key, rawKey)}
|
||||
>
|
||||
<span class={pureViewer ? 'cursor-auto' : ''}>{!isArray ? key : index} </span>
|
||||
<span class={pureViewer ? 'cursor-auto' : ''}>{!isArray ? key : index}</span>
|
||||
</Button>
|
||||
{@render metaData?.(key)}
|
||||
</AnimatedButton>
|
||||
<span class="text-2xs -ml-0.5 text-tertiary">:</span>
|
||||
|
||||
@@ -273,6 +283,7 @@
|
||||
{:else}
|
||||
{@render renderScalar(key, jsonFiltered[key])}
|
||||
{/if}
|
||||
{@render editKey?.(key)}
|
||||
</li>
|
||||
{/each}
|
||||
{#if keys.length > keyLimit}
|
||||
|
||||
@@ -24,6 +24,7 @@
|
||||
import type { FlowPropPickerConfig, PropPickerContext } from '$lib/components/prop_picker'
|
||||
import type { PickableProperties } from '$lib/components/flows/previousResults'
|
||||
import { Triggers } from '$lib/components/triggers/triggers.svelte'
|
||||
import { TestSteps } from '$lib/components/flows/testSteps.svelte'
|
||||
|
||||
let token = $page.url.searchParams.get('wm_token') ?? undefined
|
||||
let workspace = $page.url.searchParams.get('workspace') ?? undefined
|
||||
@@ -73,7 +74,7 @@
|
||||
const moving = writable<{ id: string } | undefined>(undefined)
|
||||
const history = initHistory(flowStore.val)
|
||||
|
||||
const testStepStore = writable<Record<string, any>>({})
|
||||
const testSteps = new TestSteps()
|
||||
const selectedIdStore = writable('settings-metadata')
|
||||
const triggersCount = writable<TriggersCount | undefined>(undefined)
|
||||
setContext<TriggerContext>('TriggerContext', {
|
||||
@@ -92,7 +93,7 @@
|
||||
pathStore: writable(''),
|
||||
flowStateStore,
|
||||
flowStore,
|
||||
testStepStore,
|
||||
testSteps,
|
||||
saveDraft: () => {},
|
||||
initialPathStore: writable(''),
|
||||
fakeInitialPath: '',
|
||||
@@ -288,7 +289,7 @@
|
||||
noEditor
|
||||
on:applyArgs={(ev) => {
|
||||
if (ev.detail.kind === 'preprocessor') {
|
||||
$testStepStore['preprocessor'] = ev.detail.args ?? {}
|
||||
testSteps.setStepArgs('preprocessor', ev.detail.args ?? {})
|
||||
$selectedIdStore = 'preprocessor'
|
||||
} else {
|
||||
previewArgsStore.val = ev.detail.args ?? {}
|
||||
|
||||
Reference in New Issue
Block a user