* Remove $$props.field usage * Rename slots to ensure no hyphen * _props * _trigger * OnSelectedIteration type correct capitalization * rename _content * Remove afterUpdate * Migrate everything to svelte 5 * array bind * Fix popover * type never * nit fixes * Fixed many trivial errors * onClick * Fix errors * use let: * nit typing * fix: wrap state_referenced_locally vars with untrack() Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com> * Add untrack import * Fix all syntax errors due to untrack migration * Fix undefined errors * Fix more undefined errors * untrack(() => initialOpen) * svelte-ignore * Fix state_descriptors_fixed error in Chart.svelte Use $state.snapshot() to pass plain copies of data/options to Chart.js instead of $state proxies. Chart.js's listenArrayEvents tries to define property descriptors on data arrays, which Svelte 5 proxies reject. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * nit typing * Merge issue * Fix "path is not set" error in resource picker / editor * Fix InputTransformForm error when rerunning some flows * fix npm run check --------- Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
659 lines
19 KiB
Svelte
659 lines
19 KiB
Svelte
<script lang="ts">
|
|
import { preventDefault, stopPropagation } from 'svelte/legacy'
|
|
|
|
import Popover from '$lib/components/Popover.svelte'
|
|
import DropdownV2 from '$lib/components/DropdownV2.svelte'
|
|
import { classNames, type Item, type StateStore } from '$lib/utils'
|
|
import {
|
|
Bed,
|
|
Database,
|
|
Gauge,
|
|
EllipsisVertical,
|
|
PhoneIncoming,
|
|
Repeat,
|
|
Square,
|
|
SkipForward,
|
|
Pin,
|
|
X,
|
|
Play,
|
|
Loader2,
|
|
TriangleAlert,
|
|
Timer,
|
|
Maximize2
|
|
} from 'lucide-svelte'
|
|
import { createEventDispatcher, getContext, untrack } from 'svelte'
|
|
import { fade } from 'svelte/transition'
|
|
import type { FlowEditorContext } from '../types'
|
|
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'
|
|
import { replaceId } from '../flowStore.svelte'
|
|
import FlowModuleSchemaItemViewer from './FlowModuleSchemaItemViewer.svelte'
|
|
import type { PropPickerContext } from '$lib/components/prop_picker'
|
|
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'
|
|
import { getStepHistoryLoaderContext } from '$lib/components/stepHistoryLoader.svelte'
|
|
import type { Job } from '$lib/gen'
|
|
import {
|
|
getNodeColorClasses,
|
|
aiActionToNodeState,
|
|
type FlowNodeState
|
|
} from '$lib/components/graph'
|
|
import type { ModuleActionInfo } from '$lib/components/flows/flowDiff'
|
|
import DiffActionBar from './DiffActionBar.svelte'
|
|
import { getGraphContext } from '$lib/components/graph/graphContext'
|
|
import MoveHandleButton from '$lib/components/graph/MoveHandleButton.svelte'
|
|
|
|
interface Props {
|
|
selected?: boolean
|
|
deletable?: boolean
|
|
moduleAction: ModuleActionInfo | undefined
|
|
retry?: boolean
|
|
cache?: boolean
|
|
earlyStop?: boolean
|
|
skip?: boolean
|
|
suspend?: boolean
|
|
sleep?: boolean
|
|
mock?:
|
|
| {
|
|
enabled?: boolean
|
|
return_value?: unknown
|
|
}
|
|
| undefined
|
|
bold?: boolean
|
|
id?: string | undefined
|
|
label: string
|
|
path?: string
|
|
nodeState?: FlowNodeState
|
|
concurrency?: boolean
|
|
// TODO: Implement for this one. See how concurrency is implemented.
|
|
debouncing?: boolean
|
|
retries?: number | undefined
|
|
warningMessage?: string | undefined
|
|
isTrigger?: boolean
|
|
editMode?: boolean
|
|
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
|
|
flowJob?: Job | undefined
|
|
isOwner?: boolean
|
|
enableTestRun?: boolean
|
|
maximizeSubflow?: () => void
|
|
menuItems?: Item[]
|
|
}
|
|
|
|
let {
|
|
selected = false,
|
|
deletable = false,
|
|
moduleAction = undefined,
|
|
retry = false,
|
|
cache = false,
|
|
earlyStop = false,
|
|
skip = false,
|
|
suspend = false,
|
|
sleep = false,
|
|
mock = { enabled: false },
|
|
bold = false,
|
|
id = undefined,
|
|
label,
|
|
path = '',
|
|
nodeState,
|
|
concurrency = false,
|
|
debouncing = false,
|
|
retries = undefined,
|
|
warningMessage = undefined,
|
|
isTrigger = false,
|
|
editMode = false,
|
|
alwaysShowOutputPicker = false,
|
|
loopStatus = undefined,
|
|
icon,
|
|
onTestUpTo,
|
|
inputTransform,
|
|
onUpdateMock,
|
|
onEditInput,
|
|
flowJob,
|
|
enableTestRun = false,
|
|
maximizeSubflow = undefined,
|
|
menuItems = undefined
|
|
}: Props = $props()
|
|
|
|
// AI action colors take priority over execution state
|
|
let effectiveState = $derived(aiActionToNodeState(moduleAction?.action) ?? nodeState)
|
|
let colorClasses = $derived(getNodeColorClasses(effectiveState, selected))
|
|
|
|
const flowEditorContext = getContext<FlowEditorContext | undefined>('FlowEditorContext')
|
|
const flowInputsStore = flowEditorContext?.flowInputsStore
|
|
const flowStore = flowEditorContext?.flowStore
|
|
|
|
const flowGraphContext = getGraphContext()
|
|
const diffManager = flowGraphContext?.diffManager
|
|
const moveManager = flowGraphContext?.moveManager
|
|
|
|
// Hide per-node action buttons when multiple nodes are selected (multi-select mode)
|
|
let isMultiSelected = $derived((flowGraphContext?.selectionManager?.selectedIds?.length ?? 0) > 1)
|
|
|
|
let pickableIds: Record<string, any> | undefined = $state(undefined)
|
|
|
|
const dispatch = createEventDispatcher()
|
|
|
|
const propPickerContext = getContext<PropPickerContext>('PropPickerContext')
|
|
const flowPropPickerConfig = propPickerContext?.flowPropPickerConfig
|
|
const pickablePropertiesFiltered = propPickerContext?.pickablePropertiesFiltered
|
|
|
|
$effect(() => {
|
|
pickableIds = $pickablePropertiesFiltered?.priorIds
|
|
})
|
|
|
|
let editId = $state(false)
|
|
|
|
let newId: string = $state(untrack(() => id) ?? '')
|
|
|
|
let moduleTest: ModuleTest | undefined = $state(undefined)
|
|
let testIsLoading = $state(false)
|
|
let hover = $state(false)
|
|
let connectingData: any | undefined = $state(undefined)
|
|
let outputPicker: OutputPicker | undefined = $state(undefined)
|
|
let testJob: any | undefined = $state(undefined)
|
|
let outputPickerBarOpen = $state(false)
|
|
let dropdownOpen = $state(false)
|
|
|
|
let flowStateStore = $derived(flowEditorContext?.flowStateStore)
|
|
|
|
let stepHistoryLoader = getStepHistoryLoaderContext()
|
|
|
|
function updateConnectingData(
|
|
id: string | undefined,
|
|
pickableIds: Record<string, any> | undefined,
|
|
flowPropPickerConfig: any | undefined,
|
|
flowStateStore: StateStore<FlowState> | undefined
|
|
) {
|
|
if (!id) return
|
|
connectingData =
|
|
flowPropPickerConfig && pickableIds && Object.keys(pickableIds).includes(id)
|
|
? pickableIds[id]
|
|
: (flowStateStore?.val?.[id]?.previewResult ?? {})
|
|
}
|
|
$effect(() => {
|
|
updateConnectingData(id, pickableIds, $flowPropPickerConfig, flowStateStore)
|
|
})
|
|
|
|
let isConnectingCandidate = $derived(
|
|
!!id && !!$flowPropPickerConfig && !!pickableIds && Object.keys(pickableIds).includes(id)
|
|
)
|
|
|
|
let isDragging = $derived(!!moveManager?.dragging)
|
|
|
|
const outputPickerVisible = $derived(
|
|
editMode && (isConnectingCandidate || alwaysShowOutputPicker) && !!id && !isDragging
|
|
)
|
|
|
|
const icon_render = $derived(icon)
|
|
|
|
let testRunDropdownOpen = $state(false)
|
|
|
|
let outputPickerInner: OutputPickerInner | undefined = $state(undefined)
|
|
let historyOpen = $derived.by(() => outputPickerInner?.getHistoryOpen?.() ?? false)
|
|
</script>
|
|
|
|
{#if deletable && id && editId}
|
|
{@const flowStore = flowEditorContext?.flowStore ?? undefined}
|
|
{@const getDeps = getDependeeAndDependentComponents(
|
|
id,
|
|
flowStore?.val?.value.modules ?? [],
|
|
flowStore?.val?.value.failure_module
|
|
)}
|
|
<Drawer bind:open={editId}>
|
|
<DrawerContent title="Edit Step Id {id}" on:close={() => (editId = false)}>
|
|
<div>
|
|
<IdEditorInput
|
|
buttonText="Edit Id "
|
|
btnClasses="!ml-1"
|
|
label=""
|
|
initialId={id}
|
|
acceptUnderScores
|
|
reservedIds={dfs(flowStore?.val?.value.modules ?? [], (x) => x.id)}
|
|
bind:value={newId}
|
|
onSave={({ oldId, newId }) => {
|
|
dispatch('changeId', { id: oldId, newId, deps: getDeps?.dependents ?? {} })
|
|
editId = false
|
|
}}
|
|
onClose={() => {
|
|
editId = false
|
|
}}
|
|
/>
|
|
<div class="mt-8">
|
|
<h3>Step Inputs Replacements</h3>
|
|
<div class="text-2xs text-primary pt-0.5">
|
|
Replace all occurrences of `results.<span class="font-bold">{id}</span>` with{' '}
|
|
results.<span class="font-bold">{newId}</span> in the step inputs of all steps that depend
|
|
on it.
|
|
</div>
|
|
<div class="pt-8 flex flex-col gap-y-4">
|
|
{#if Object.keys(getDeps?.dependents ?? {})?.length > 0}
|
|
{#each Object.entries(getDeps?.dependents ?? {}) as dependents}
|
|
<div>
|
|
<h4>{dependents[0]}</h4>
|
|
<div>
|
|
{#each dependents?.[1] as d}
|
|
<div>
|
|
<span class="font-mono text-sm">{d}</span> →
|
|
<span class="font-mono text-sm">{replaceId(d, id, newId)}</span>
|
|
</div>
|
|
{/each}
|
|
</div>
|
|
</div>
|
|
{/each}
|
|
{:else}
|
|
<div class="text-2xs text-primary"> No dependents </div>
|
|
{/if}
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</DrawerContent>
|
|
</Drawer>
|
|
{/if}
|
|
|
|
{#if deletable && id && flowStore && outputPickerVisible}
|
|
{@const flowStoreVal = flowStore.val}
|
|
{@const mod = flowStoreVal?.value ? dfsPreviousResults(id, flowStoreVal, false)[0] : undefined}
|
|
{#if mod && flowStateStore?.val?.[id]}
|
|
<ModuleTest
|
|
bind:this={moduleTest}
|
|
{mod}
|
|
bind:testIsLoading
|
|
bind:testJob
|
|
onJobDone={() => {
|
|
outputPickerInner?.setJobPreview?.()
|
|
}}
|
|
/>
|
|
{/if}
|
|
{/if}
|
|
|
|
<div class="relative">
|
|
<!-- svelte-ignore a11y_click_events_have_key_events -->
|
|
<!-- svelte-ignore a11y_no_static_element_interactions -->
|
|
<div
|
|
class={classNames(
|
|
'w-full module flex rounded-md cursor-pointer max-w-full drop-shadow-base',
|
|
colorClasses.bg
|
|
)}
|
|
style="width: 275px; height: 34px;"
|
|
onmouseenter={() => (hover = true)}
|
|
onmouseleave={() => (hover = false)}
|
|
onpointerdown={stopPropagation(preventDefault((e) => dispatch('pointerdown', e)))}
|
|
>
|
|
{#if id}
|
|
<DiffActionBar moduleId={id} {moduleAction} {diffManager} {flowStore} />
|
|
{/if}
|
|
<div
|
|
class={classNames('absolute z-0 rounded-md outline-offset-0', colorClasses.outline)}
|
|
style={`width: 275px; height: 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
|
|
transition:fade|local={{ duration: 200 }}
|
|
class="center-center rounded border bg-surface border-gray-400 text-secondary px-1 py-0.5"
|
|
>
|
|
{#if retries}<span class="text-red-400 mr-2">{retries}</span>{/if}
|
|
<Repeat size={12} />
|
|
</div>
|
|
{#snippet text()}
|
|
Retries
|
|
{/snippet}
|
|
</Popover>
|
|
{/if}
|
|
|
|
{#if concurrency}
|
|
<Popover notClickable>
|
|
<div
|
|
transition:fade|local={{ duration: 200 }}
|
|
class="center-center rounded border bg-surface border-gray-400 text-secondary px-1 py-0.5"
|
|
>
|
|
<Gauge size={12} />
|
|
</div>
|
|
{#snippet text()}
|
|
Concurrency Limits
|
|
{/snippet}
|
|
</Popover>
|
|
{/if}
|
|
{#if debouncing}
|
|
<Popover notClickable>
|
|
<div
|
|
transition:fade|local={{ duration: 200 }}
|
|
class="center-center rounded border bg-surface border-gray-400 text-secondary px-1 py-0.5"
|
|
>
|
|
<Timer size={12} />
|
|
</div>
|
|
{#snippet text()}
|
|
Debouncing
|
|
{/snippet}
|
|
</Popover>
|
|
{/if}
|
|
{#if cache}
|
|
<Popover notClickable>
|
|
<div
|
|
transition:fade|local={{ duration: 200 }}
|
|
class="center-center rounded border bg-surface border-gray-400 text-secondary px-1 py-0.5"
|
|
>
|
|
<Database size={12} />
|
|
</div>
|
|
{#snippet text()}
|
|
Cached
|
|
{/snippet}
|
|
</Popover>
|
|
{/if}
|
|
{#if earlyStop}
|
|
<Popover notClickable>
|
|
<div
|
|
transition:fade|local={{ duration: 200 }}
|
|
class="center-center bg-surface rounded border border-gray-400 text-secondary px-1 py-0.5"
|
|
>
|
|
<Square size={12} />
|
|
</div>
|
|
{#snippet text()}
|
|
{isTrigger ? 'Stop early if there are no new events' : 'Early stop/break'}
|
|
{/snippet}
|
|
</Popover>
|
|
{/if}
|
|
{#if skip}
|
|
<Popover notClickable>
|
|
<div
|
|
transition:fade|local={{ duration: 200 }}
|
|
class="center-center bg-surface rounded border border-gray-400 text-secondary px-1 py-0.5"
|
|
>
|
|
<SkipForward size={12} />
|
|
</div>
|
|
{#snippet text()}
|
|
Skip
|
|
{/snippet}
|
|
</Popover>
|
|
{/if}
|
|
{#if suspend}
|
|
<Popover notClickable>
|
|
<div
|
|
transition:fade|local={{ duration: 200 }}
|
|
class="center-center bg-surface rounded border border-gray-400 text-secondary px-1 py-0.5"
|
|
>
|
|
<PhoneIncoming size={12} />
|
|
</div>
|
|
{#snippet text()}
|
|
Suspend
|
|
{/snippet}
|
|
</Popover>
|
|
{/if}
|
|
{#if sleep}
|
|
<Popover notClickable>
|
|
<div
|
|
transition:fade|local={{ duration: 200 }}
|
|
class="center-center bg-surface rounded border border-gray-400 text-secondary px-1 py-0.5"
|
|
>
|
|
<Bed size={12} />
|
|
</div>
|
|
{#snippet text()}
|
|
Sleep
|
|
{/snippet}
|
|
</Popover>
|
|
{/if}
|
|
{#if mock?.enabled}
|
|
<Popover notClickable>
|
|
<button
|
|
transition:fade|local={{ duration: 200 }}
|
|
class="center-center bg-surface rounded border border-gray-400 text-secondary px-1 py-0.5"
|
|
onclick={() => {
|
|
outputPicker?.toggleOpen()
|
|
}}
|
|
data-popover
|
|
>
|
|
<Pin size={12} />
|
|
</button>
|
|
{#snippet text()}
|
|
Pinned
|
|
{/snippet}
|
|
</Popover>
|
|
{/if}
|
|
</div>
|
|
|
|
<div class="flex flex-col w-full">
|
|
<FlowModuleSchemaItemViewer
|
|
{label}
|
|
{path}
|
|
{id}
|
|
{deletable}
|
|
{bold}
|
|
bind:editId
|
|
disableEditId={isMultiSelected}
|
|
{hover}
|
|
{colorClasses}
|
|
>
|
|
{#snippet icon()}
|
|
{@render icon_render?.()}
|
|
{/snippet}
|
|
</FlowModuleSchemaItemViewer>
|
|
|
|
{#if outputPickerVisible && !isMultiSelected}
|
|
<OutputPicker
|
|
bind:this={outputPicker}
|
|
{selected}
|
|
{hover}
|
|
{isConnectingCandidate}
|
|
{historyOpen}
|
|
{inputTransform}
|
|
id={id ?? ''}
|
|
bind:bottomBarOpen={outputPickerBarOpen}
|
|
{loopStatus}
|
|
{onEditInput}
|
|
>
|
|
{#snippet children({ allowCopy, isConnecting, selectConnection })}
|
|
<OutputPickerInner
|
|
{allowCopy}
|
|
prefix={'results'}
|
|
connectingData={isConnecting ? connectingData : undefined}
|
|
{mock}
|
|
{testJob}
|
|
moduleId={id}
|
|
onSelect={selectConnection}
|
|
{onUpdateMock}
|
|
{path}
|
|
{loopStatus}
|
|
rightMargin
|
|
historyOffset={{ mainAxis: 12, crossAxis: -9 }}
|
|
clazz="p-1"
|
|
isLoading={testIsLoading ||
|
|
(id ? stepHistoryLoader?.stepStates[id]?.loadingJobs : false)}
|
|
initial={id ? stepHistoryLoader?.stepStates[id]?.initial : undefined}
|
|
bind:this={outputPickerInner}
|
|
/>
|
|
{/snippet}
|
|
</OutputPicker>
|
|
{/if}
|
|
</div>
|
|
|
|
{#if deletable && !isDragging}
|
|
{#if maximizeSubflow !== undefined}
|
|
{@render buttonMaximizeSubflow?.()}
|
|
{/if}
|
|
|
|
{#if (id && Object.values($flowInputsStore?.[id]?.flowStepWarnings || {}).length > 0) || Boolean(warningMessage)}
|
|
<Popover
|
|
style="will-change: transform;"
|
|
class={twMerge(
|
|
'absolute -translate-y-[100%] top-1 -left-1',
|
|
'flex items-center justify-center rounded-b-none rounded-md p-1 shadow-md duration-0 ',
|
|
id &&
|
|
Object.values($flowInputsStore?.[id]?.flowStepWarnings || {})?.some(
|
|
(x) => x.type === 'error'
|
|
)
|
|
? 'border-red-600 text-red-600 bg-red-100 hover:bg-red-300'
|
|
: ' text-yellow-600 bg-yellow-100 hover:bg-yellow-300'
|
|
)}
|
|
>
|
|
{#snippet text()}
|
|
<ul class="list-disc px-2">
|
|
{#if id}
|
|
{#each Object.values($flowInputsStore?.[id]?.flowStepWarnings || {}) as m}
|
|
<li>
|
|
{m.message}
|
|
</li>
|
|
{/each}
|
|
{/if}
|
|
</ul>
|
|
{/snippet}
|
|
|
|
<TriangleAlert size={12} strokeWidth={2} />
|
|
</Popover>
|
|
{/if}
|
|
|
|
{#if !isMultiSelected && id !== 'preprocessor' && moveManager && id}
|
|
<div
|
|
class="absolute -translate-y-[100%] top-2 right-5 h-7 p-1 min-w-7"
|
|
style="will-change: transform;"
|
|
>
|
|
<MoveHandleButton
|
|
{moveManager}
|
|
moduleId={id}
|
|
singleNode
|
|
visible={hover || selected || dropdownOpen}
|
|
onClickMove={() => dispatch('move')}
|
|
class="trash group-hover:block"
|
|
/>
|
|
</div>
|
|
{/if}
|
|
|
|
{#if !isMultiSelected && menuItems && menuItems.length > 0}
|
|
<div
|
|
class="absolute -translate-y-[100%] top-2 -right-2 h-7 p-1 min-w-7"
|
|
style="will-change: transform;"
|
|
>
|
|
<DropdownV2
|
|
items={menuItems}
|
|
placement="bottom-end"
|
|
bind:open={dropdownOpen}
|
|
fixedHeight={false}
|
|
usePointerDownOutside
|
|
>
|
|
{#snippet buttonReplacement()}
|
|
<button
|
|
class={twMerge(
|
|
'trash center-center p-1 text-secondary shadow-sm bg-surface duration-0 hover:bg-surface-tertiary',
|
|
hover || selected || dropdownOpen ? 'block' : '!hidden',
|
|
'shadow-md rounded-md',
|
|
'group-hover:block'
|
|
)}
|
|
onpointerdown={stopPropagation(preventDefault(() => {}))}
|
|
title="Actions"
|
|
>
|
|
<EllipsisVertical size={12} />
|
|
</button>
|
|
{/snippet}
|
|
</DropdownV2>
|
|
</div>
|
|
{/if}
|
|
{:else if maximizeSubflow !== undefined}
|
|
{@render buttonMaximizeSubflow?.()}
|
|
{/if}
|
|
</div>
|
|
|
|
{#if editMode && enableTestRun && flowJob?.type !== 'QueuedJob' && !isDragging}
|
|
<!-- svelte-ignore a11y_no_static_element_interactions -->
|
|
<div
|
|
class="absolute top-1/2 -translate-y-1/2 -translate-x-[100%] -left-[0] flex items-center w-fit px-1 h-9 min-w-9"
|
|
onmouseenter={() => (hover = true)}
|
|
onmouseleave={() => (hover = false)}
|
|
>
|
|
{#if !isMultiSelected && (hover || selected || testRunDropdownOpen) && outputPickerVisible}
|
|
<div transition:fade={{ duration: 100 }}>
|
|
{#if !testIsLoading}
|
|
<Button
|
|
size="xs"
|
|
title="Run"
|
|
variant="default"
|
|
btnClasses="px-1 py-1.5 bg-surface"
|
|
on:click={() => {
|
|
outputPicker?.toggleOpen(true)
|
|
moduleTest?.loadArgsAndRunTest()
|
|
}}
|
|
dropdownItems={[
|
|
{
|
|
label: 'Test up to here',
|
|
onClick: () => {
|
|
if (id) {
|
|
onTestUpTo?.(id)
|
|
}
|
|
}
|
|
}
|
|
]}
|
|
dropdownBtnClasses="!w-3 px-0.5"
|
|
bind:dropdownOpen={testRunDropdownOpen}
|
|
>
|
|
{#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-[36px] !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>
|
|
{/if}
|
|
</div>
|
|
|
|
{#snippet buttonMaximizeSubflow()}
|
|
<div class="absolute -translate-y-[100%] top-2 right-12 h-7 p-1">
|
|
<button
|
|
title="Expand subflow"
|
|
class={twMerge(
|
|
'center-center text-secondary shadow-sm bg-surface duration-0 hover:bg-surface-tertiary p-1',
|
|
'shadow-md rounded-md',
|
|
!isMultiSelected && (hover || selected) ? 'opacity-100' : 'opacity-50'
|
|
)}
|
|
onclick={(e) => {
|
|
e.stopPropagation()
|
|
e.preventDefault()
|
|
maximizeSubflow?.()
|
|
}}
|
|
onpointerdown={(e) => {
|
|
e.stopPropagation()
|
|
e.preventDefault()
|
|
}}
|
|
>
|
|
<Maximize2 size={12} />
|
|
</button>
|
|
</div>
|
|
{/snippet}
|
|
|
|
<style>
|
|
.module:hover .trash {
|
|
display: flex !important;
|
|
}
|
|
</style>
|