mirror of
https://github.com/windmill-labs/windmill.git
synced 2026-10-22 16:03:43 +00:00
* feat: responsive modal step panel for the flow editor in sessions On narrow layouts the flow editor's step-details pane opens as a modal (double-click a graph node) instead of a split pane, with a dock/float toggle. Scoped to sessions via allowModalPanel; the full-page editor is unchanged. - FlowEditor: modal/docked modes gated by mount width + allowModalPanel, small header (step-id Badge + subtle dock/close), standing double-click hint, and a per-step hint in the name tooltip - selectionManager: onSelectIntent hook so flow-level panels (settings, input, triggers…) open the modal on single click - PropPickerWrapper: collapse the prop picker until connect and animate it in via AnimatedPane (runs-page pattern), no blue connect ring in modal mode - StepInputGen: drop the TAB/Wand autocompletion button + spinner (feature still works via focus + Tab) - InputTransformForm: decouple the Help dropdown from the AI suggestion - FlowModuleHeader: move 'Save to workspace' into an ellipsis dropdown Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * fix: loop editor rendering and nested splitpanes splitters in the sessions modal - Loop iterator/parallelism: keep the picker split pane (forceExpanded) so the editor fills its box and the picker shows; the collapse-until-connect mode stays for the step inputs - Remove the intrusive AI TAB/Wand autocompletion button from IteratorGen (generation still runs headless via focus + Tab) - Size the iterator connect plug and restyle the loop header/labels/toggles - Scope the global `.splitter-hidden` splitter-hiding rule to direct children so it no longer leaks into nested Splitpanes under the sessions preview Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * feat: redesign flow step advanced settings as a single toggle-first column Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * feat: taller step test pane by default and restyle advanced section titles Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * feat: show flow run-settings params disabled when a setting is toggled off Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * feat: single-column for-loop panel reusing the run-settings accordion Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * feat: single-column while-loop panel reusing the run-settings accordion Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * feat: single-column branch panels reusing the run-settings accordion Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * fix: auto-open modal panel when creating an AI agent tool Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * feat: redesign branch panels with card layout and shared predicate editor Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * refactor: remove per-setting status badges from flow map nodes Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * chore: sync package-lock after windmill-utils-internal bump Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * style: polish prop-picker plug button and branch panel layouts * fix: persist skip-if-stopped toggles in early stop settings * fix: open the step panel modal on demand and cap its width * fix: restore graph step setting badges, strip panel header chips instead * feat: docked panel header with detach action and open-details step menu * feat: width-based panel mode on every surface with inline detach action * refactor: single source for flow step settings and their defaults * docs: pin flow editor vocabulary in CONTEXT.md * fix: open the trigger panel on double click or a specific trigger * fix: keep module pickers inside their pane and dismissable * fix: drop the misleading chevron on the MCP tool entry * fix: resolve flow approvals against the job's workspace, not the nav one * refactor: derive the approval workspace from the job, not from callers * fix: restore S3 snippets and gate params while their setting is off * fix: restore branch mock controls and address review findings * chore: drop stray debug log from the flow map item * feat: pinned output section for loop and branch panels * fix: open the panel for deliberate navigation from the flow header * perf: mount branch predicate editors on demand * fix: skip predicate picker previews the previous step's result * fix: flow-level graph nodes open their panel on a single click * fix: open the step panel for AI chat selections, not for undo * chore: drop dead console.log and duplicated modalPanel doc * fix: re-sync expression editors and scope error-handler settings * fix: match the failure module exactly and ignore unselectable nodes * fix: keep concurrency editable, honour module cache_ttl, tighten panel ids * fix: open panel from indirect selections, use presence for value-driven toggles * fix: don't open settings on error-handler delete, flush editors on unmount * fix: guard editor destroy flush, keep retry kind reachable * refactor: name the run settings panel after the domain vocabulary * fix: only write editor flushes to the step they belong to * fix: bind step panels by id so a delete can't retarget editor writes * fix: don't let the trigger picker's escape close the drawer beneath it * docs: condense two comments to the constraint they record * fix: arbitrate escape through the overlay stack instead of deferring to it * fix: key nested step blocks by identity so anchored bindings can't go stale * fix: untrack the overlay-stack push and drop the frozen branch binding * chore: state the escape rationale once, key branch lists, format * fix: let the topmost overlay own escape instead of the graph * fix: keep the dynamic-input help box out of static template fields * fix: restore the graph connect on the for-loop iterator * fix: end connect mode with the modal and keep it to docked panels * fix: never enter graph connect mode from the modal panel * fix: reveal inserted steps, restore editor pane size, unleak the drawer stack * fix: keep the enable-AI popover reachable in session panes * feat: add the connect policy and its single armed slot * refactor: one picker for every expression input * refactor: route every connect through one armed slot * fix: give every connect button the same footprint * fix: keep the connect ring from showing through the button * fix: keep flow card actions right-aligned beside the detach button * fix: give the connect ring an opaque ground to mask against * feat: dock the panel back without reopening it * feat: dock the panel from the graph control bar * style: round the graph control bar and size its glyphs * style: customize the graph controls through their supported api * style: build the graph control bar from lucide icons * fix: use the graph's tooltip component in the zoom controls * style: pad the graph controls and enlarge their glyphs * style: pad the graph controls and put dock at the bar's end * refactor: give settings rows the same popover picker as other expressions * fix: pass the wrapper's pickable properties to nested inputs * refactor: stack step settings and render every expression through the step input form * feat: split loop panels into tabs and rework the approval form * feat: anchor drawers to their host pane and give them a size floor * fix: mark the loop iterator expression as required * refactor: badge ee-only toggles instead of a warning line * fix: flag an empty loop iterator expression as an error * refactor: pick the early-stop flow status from one toggle group * fix: keep parallel loops uncapped unless a limit is opted into * fix: scope the overlay stack to its host and disarm connect on dismissal * fix: anchor the trigger picker to its host pane * feat: move diff into the menu when the top bar is narrow * fix: gate the result logs toggle to the graph popover * feat: raise the modal-panel breakpoint to 1280 * fix: anchor flow editor popovers and fullscreen to their host pane * fix: anchor overlays to their host pane and mute them when hidden * fix: portal hosted modals and menus into the pane they anchor to * fix: keep non-listening dialogs off the overlay stack * fix: drop the topmost gate from confirmation dialogs * fix: silence overlays in a collapsed preview panel * feat: rework the branch panels with tabs, reordering and add/delete * refactor: fold the detached-panel chrome into the card header * fix: give every flow panel a titled card header * fix: stop the step panel oscillating on an auto-height editor * feat: consolidate script panel actions and restore branch predicate AI * fix: restore the logs toggle on the flow result popover * fix: collapse the idle property picker in modal step panels * fix: stop the docked pane scrolling alongside its panel * fix: space the last settings row off the panel bottom * revert: always show the property picker pane in step panels * chore: keep the inline script AI button identical to main * fix: ask for AI input suggestions on click, not on hover * fix: keep graph connects armed and remount the parallelism input * style: reveal the predicate AI button on row hover * style: give branch cards a handle and delete column * refactor: arbitrate flow overlay escape through Disposable * fix: give the popover picker its results and re-narrow the EE badge * docs: correct loopSubset and guard the modal width measurement * fix: insert picked properties at the cursor in expression inputs * fix: give the expanded-subflow panel the shared header chrome * style: rename the suspend setting to Suspend until approval/resume * feat: open a step's modal when clicking the step already selected * feat: add an auto/attached/detached toggle for the step panel * refactor: pick the step panel's placement from one named menu * refactor: keep the panel-mode module's exports to what is consumed * feat: show each configured setting's value on its badge * fix: carry the suspend rename into the step settings registry * docs: name both gestures in the step explore hint * test: pin where the step panel goes for a given width and preference --------- Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
339 lines
12 KiB
Svelte
339 lines
12 KiB
Svelte
<script lang="ts">
|
|
import Toggle from '$lib/components/Toggle.svelte'
|
|
import { stepSettingDefaults } from '../flowStepSettings'
|
|
import InputTransformForm from '$lib/components/InputTransformForm.svelte'
|
|
import PropPickerWrapper from '../propPicker/PropPickerWrapper.svelte'
|
|
import type SimpleEditor from '$lib/components/SimpleEditor.svelte'
|
|
import type { Flow, FlowModule, StopAfterIf } from '$lib/gen'
|
|
import Label from '$lib/components/Label.svelte'
|
|
import TextInput from '$lib/components/text_input/TextInput.svelte'
|
|
import ToggleButtonGroup from '$lib/components/common/toggleButton-v2/ToggleButtonGroup.svelte'
|
|
import ToggleButton from '$lib/components/common/toggleButton-v2/ToggleButton.svelte'
|
|
import type { ExtendedOpenFlow, FlowEditorContext } from '../types'
|
|
import { getContext } from 'svelte'
|
|
import { emptySchema } from '$lib/utils'
|
|
import { NEVER_TESTED_THIS_FAR } from '../models'
|
|
import { getStepPropPicker } from '../previousResults'
|
|
import { dfs } from '../previousResults'
|
|
import { slideDynamic } from '$lib/transitions'
|
|
|
|
const { flowStateStore, flowStore, previewArgs } =
|
|
getContext<FlowEditorContext>('FlowEditorContext')
|
|
|
|
interface Props {
|
|
flowModule: FlowModule
|
|
/** A loop shows both predicates, and puts the per-iteration one (`stop_after_if`)
|
|
* next to its own settings rather than in the run-settings list. */
|
|
blocks?: 'both' | 'stop-after' | 'all-iters'
|
|
}
|
|
|
|
let { flowModule = $bindable(), blocks = 'both' }: Props = $props()
|
|
|
|
let stopAfterEditor: SimpleEditor | undefined = $state(undefined)
|
|
let stopAfterAllItersEditor: SimpleEditor | undefined = $state(undefined)
|
|
|
|
// Both predicates are stored as a bare `{ expr }`, so the form is told their kind
|
|
// through `argType` rather than inferring one from the value.
|
|
let predicateSchema = $state(emptySchema())
|
|
predicateSchema.properties['stop_after_if'] = { type: 'boolean' }
|
|
predicateSchema.properties['stop_after_all_iters_if'] = { type: 'boolean' }
|
|
|
|
let stepPropPicker = $derived(
|
|
getStepPropPicker(
|
|
flowStateStore.val,
|
|
undefined,
|
|
undefined,
|
|
flowModule.id,
|
|
flowStore.val,
|
|
previewArgs.val,
|
|
false
|
|
)
|
|
)
|
|
|
|
function checkIfBreakableParent(
|
|
flowStoreValue: ExtendedOpenFlow
|
|
):
|
|
| { stepId: string; isParallel: boolean; type: 'loop' }
|
|
| { stepId: string; isParallel: true; type: 'branchall' }
|
|
| null {
|
|
const flow: Flow = JSON.parse(JSON.stringify(flowStoreValue))
|
|
const parents = dfs(flowModule.id, flow, true)
|
|
for (const parent of parents.slice(1)) {
|
|
if (parent.value.type === 'forloopflow' || parent.value.type === 'whileloopflow') {
|
|
return { stepId: parent.id, isParallel: parent.value.parallel ?? false, type: 'loop' }
|
|
} else if (parent.value.type === 'branchall' && parent.value.parallel) {
|
|
return { stepId: parent.id, isParallel: true, type: 'branchall' }
|
|
}
|
|
}
|
|
return null
|
|
}
|
|
// `skip_if_stopped` and `error_message` are mutually exclusive in the worker, and
|
|
// setting neither is a third outcome — so the three are one choice, not two flags.
|
|
type StopStatus = 'success' | 'skipped' | 'error'
|
|
function stopStatus(stop: StopAfterIf): StopStatus {
|
|
if (stop.skip_if_stopped) return 'skipped'
|
|
if (stop.error_message != undefined) return 'error'
|
|
return 'success'
|
|
}
|
|
function setStopStatus(stop: StopAfterIf, status: StopStatus) {
|
|
stop.skip_if_stopped = status === 'skipped'
|
|
stop.error_message = status === 'error' ? (stop.error_message ?? '') : undefined
|
|
if (status !== 'error') stop.error_include_result = false
|
|
}
|
|
|
|
let { isLoop, isParallelLoop } = $derived(
|
|
flowModule.value.type === 'forloopflow' || flowModule.value.type === 'whileloopflow'
|
|
? { isLoop: true, isParallelLoop: flowModule.value.parallel ?? false }
|
|
: { isLoop: false, isParallelLoop: false }
|
|
)
|
|
let isBranchAll = $derived(flowModule.value.type === 'branchall')
|
|
let isStopAfterIfEnabled = $derived(Boolean(flowModule.stop_after_if))
|
|
let isStopAfterAllIterationsEnabled = $derived(Boolean(flowModule.stop_after_all_iters_if))
|
|
let result = $derived(flowStateStore.val[flowModule.id]?.previewResult ?? NEVER_TESTED_THIS_FAR)
|
|
let breakableParent = $derived(checkIfBreakableParent(flowStore.val))
|
|
// One `stop_after_if` field, but what stopping early *does* depends on where the step
|
|
// sits — so the name and the explanation are picked together rather than sharing one
|
|
// tooltip that has to enumerate every case.
|
|
let stopAfterCopy = $derived(
|
|
isParallelLoop
|
|
? {
|
|
label: 'Break loop if',
|
|
tooltip:
|
|
'Unavailable on a parallel loop: iterations don\'t run in sequence, so there is nothing to break out of and the worker skips this predicate. Use "Stop flow if" to decide once every iteration has completed.'
|
|
}
|
|
: isLoop
|
|
? {
|
|
label: 'Break loop if',
|
|
tooltip:
|
|
'Evaluated after each iteration. When it returns true the loop stops iterating and the flow carries on with the iterations collected so far.'
|
|
}
|
|
: breakableParent
|
|
? breakableParent.isParallel
|
|
? breakableParent.type === 'loop'
|
|
? {
|
|
label: 'Skip rest of iteration if',
|
|
tooltip:
|
|
'Evaluated after this step. When it returns true the remaining steps of this iteration are skipped; the other iterations are unaffected.'
|
|
}
|
|
: {
|
|
label: 'Skip rest of branch if',
|
|
tooltip:
|
|
'Evaluated after this step. When it returns true the remaining steps of this branch are skipped; the other branches are unaffected.'
|
|
}
|
|
: {
|
|
label: 'Break parent loop if',
|
|
tooltip: `Evaluated after this step. When it returns true the enclosing loop ${breakableParent.stepId} stops iterating and the flow carries on.`
|
|
}
|
|
: {
|
|
label: 'Stop flow if',
|
|
tooltip:
|
|
"Evaluated after this step. When it returns true the flow stops here and returns this step's result."
|
|
}
|
|
)
|
|
|
|
// The all-iterations predicate runs once the loop or branch-all is done, over what
|
|
// every iteration returned, so it can decide on the whole rather than on one result.
|
|
let allItersPrefix = $derived(
|
|
`Evaluated once ${isBranchAll ? 'every branch' : 'every iteration'} has completed, over their collected results.`
|
|
)
|
|
let stopAfterAllItersCopy = $derived(
|
|
breakableParent
|
|
? breakableParent.isParallel
|
|
? breakableParent.type === 'loop'
|
|
? {
|
|
label: 'Skip rest of iteration if',
|
|
tooltip: `${allItersPrefix} When it returns true the remaining steps of the enclosing iteration are skipped.`
|
|
}
|
|
: {
|
|
label: 'Skip rest of branch if',
|
|
tooltip: `${allItersPrefix} When it returns true the remaining steps of the enclosing branch are skipped.`
|
|
}
|
|
: {
|
|
label: `Break parent loop ${breakableParent.stepId} if`,
|
|
tooltip: `${allItersPrefix} When it returns true the enclosing loop stops iterating and the flow carries on.`
|
|
}
|
|
: {
|
|
label: 'Stop flow if',
|
|
tooltip: `${allItersPrefix} When it returns true the flow stops here and returns them.`
|
|
}
|
|
)
|
|
|
|
let earlyStopResult = $derived(
|
|
isLoop
|
|
? Array.isArray(result) && result.length > 0
|
|
? result[result.length - 1]
|
|
: result === NEVER_TESTED_THIS_FAR
|
|
? result
|
|
: undefined
|
|
: result
|
|
)
|
|
</script>
|
|
|
|
{#snippet stopStatusPicker(stop: StopAfterIf)}
|
|
<div class="flex flex-col gap-2 pl-9" transition:slideDynamic>
|
|
<Label
|
|
label="Flow status"
|
|
tooltip="How the flow is reported once this condition stops it. Success returns this step's result, Skipped marks the flow as skipped, and Error fails it."
|
|
>
|
|
<ToggleButtonGroup
|
|
noWFull
|
|
selected={stopStatus(stop)}
|
|
onSelected={(v) => setStopStatus(stop, v)}
|
|
>
|
|
{#snippet children({ item })}
|
|
<ToggleButton value="success" label="Success" {item} small />
|
|
<ToggleButton value="skipped" label="Skipped" {item} small />
|
|
<ToggleButton value="error" label="Error" {item} small />
|
|
{/snippet}
|
|
</ToggleButtonGroup>
|
|
</Label>
|
|
{#if stop.error_message != undefined}
|
|
<div class="flex flex-col gap-2" transition:slideDynamic>
|
|
<TextInput
|
|
size="sm"
|
|
bind:value={() => stop.error_message ?? '', (v) => (stop.error_message = String(v))}
|
|
inputProps={{ placeholder: 'Enter custom error message (optional)' }}
|
|
/>
|
|
<Toggle
|
|
size="xs"
|
|
bind:checked={
|
|
() => stop.error_include_result ?? false, (v) => (stop.error_include_result = v)
|
|
}
|
|
options={{
|
|
right: 'Include result in error',
|
|
rightTooltip:
|
|
"When enabled, this step's output is embedded inside the raised error object (as error.result) instead of being discarded. The flow result stays { error }."
|
|
}}
|
|
/>
|
|
</div>
|
|
{/if}
|
|
</div>
|
|
{/snippet}
|
|
|
|
{#snippet stopAfterToggle()}
|
|
<Toggle
|
|
size="xs"
|
|
textClass="text-xs font-normal text-primary"
|
|
disabled={isParallelLoop}
|
|
checked={isStopAfterIfEnabled}
|
|
on:change={() => {
|
|
if (isStopAfterIfEnabled && flowModule.stop_after_if) {
|
|
flowModule.stop_after_if = undefined
|
|
} else {
|
|
flowModule.stop_after_if = stepSettingDefaults('early-stop')
|
|
}
|
|
}}
|
|
options={{
|
|
title: isParallelLoop ? stopAfterCopy.tooltip : undefined,
|
|
right: stopAfterCopy.label,
|
|
rightTooltip: stopAfterCopy.tooltip,
|
|
rightDocumentationLink: 'https://www.windmill.dev/docs/flows/early_stop'
|
|
}}
|
|
/>
|
|
{/snippet}
|
|
|
|
{#snippet stopAfterAllItersToggle()}
|
|
<Toggle
|
|
size="xs"
|
|
textClass="text-xs font-normal text-primary"
|
|
checked={isStopAfterAllIterationsEnabled}
|
|
on:change={() => {
|
|
if (isStopAfterAllIterationsEnabled && flowModule.stop_after_all_iters_if) {
|
|
flowModule.stop_after_all_iters_if = undefined
|
|
} else {
|
|
flowModule.stop_after_all_iters_if = stepSettingDefaults('early-stop')
|
|
}
|
|
}}
|
|
options={{
|
|
right: stopAfterAllItersCopy.label,
|
|
rightTooltip: stopAfterAllItersCopy.tooltip,
|
|
rightDocumentationLink: 'https://www.windmill.dev/docs/flows/early_stop'
|
|
}}
|
|
/>
|
|
{/snippet}
|
|
|
|
<div class="flex flex-col items-start gap-6">
|
|
{#if blocks !== 'all-iters' && !isBranchAll}
|
|
<div class="w-full flex flex-col gap-2">
|
|
<PropPickerWrapper
|
|
popover={true}
|
|
flow_input={stepPropPicker.pickableProperties.flow_input}
|
|
notSelectable
|
|
result={earlyStopResult}
|
|
extraResults={isLoop ? { all_iters: result } : undefined}
|
|
displayContext={false}
|
|
pickableProperties={stepPropPicker.pickableProperties}
|
|
on:select={({ detail }) => {
|
|
stopAfterEditor?.insertAtCursor(detail)
|
|
stopAfterEditor?.focus()
|
|
}}
|
|
>
|
|
<InputTransformForm
|
|
bind:arg={
|
|
() => flowModule.stop_after_if,
|
|
(v) => {
|
|
flowModule.stop_after_if = v
|
|
}
|
|
}
|
|
argName="stop_after_if"
|
|
argType="javascript"
|
|
collapsed={!isStopAfterIfEnabled || isParallelLoop}
|
|
animateAppear
|
|
header={stopAfterToggle}
|
|
noDynamicToggle
|
|
schema={predicateSchema}
|
|
previousModuleId={undefined}
|
|
pickableProperties={stepPropPicker.pickableProperties}
|
|
extraLib={`declare const result = ${JSON.stringify(earlyStopResult)};\n` +
|
|
stepPropPicker.extraLib +
|
|
(isLoop ? `\ndeclare const all_iters = ${JSON.stringify(result)};` : '')}
|
|
bind:editor={stopAfterEditor}
|
|
/>
|
|
</PropPickerWrapper>
|
|
{#if isStopAfterIfEnabled && !breakableParent && !isLoop && flowModule.stop_after_if}
|
|
{@render stopStatusPicker(flowModule.stop_after_if)}
|
|
{/if}
|
|
</div>
|
|
{/if}
|
|
|
|
{#if blocks !== 'stop-after' && (isLoop || isBranchAll)}
|
|
<div class="w-full flex flex-col gap-2">
|
|
<PropPickerWrapper
|
|
popover={true}
|
|
flow_input={stepPropPicker.pickableProperties.flow_input}
|
|
notSelectable
|
|
{result}
|
|
displayContext={false}
|
|
pickableProperties={stepPropPicker.pickableProperties}
|
|
on:select={({ detail }) => {
|
|
stopAfterAllItersEditor?.insertAtCursor(detail)
|
|
stopAfterAllItersEditor?.focus()
|
|
}}
|
|
>
|
|
<InputTransformForm
|
|
bind:arg={
|
|
() => flowModule.stop_after_all_iters_if,
|
|
(v) => {
|
|
flowModule.stop_after_all_iters_if = v
|
|
}
|
|
}
|
|
argName="stop_after_all_iters_if"
|
|
argType="javascript"
|
|
collapsed={!isStopAfterAllIterationsEnabled}
|
|
animateAppear
|
|
header={stopAfterAllItersToggle}
|
|
noDynamicToggle
|
|
schema={predicateSchema}
|
|
previousModuleId={undefined}
|
|
pickableProperties={stepPropPicker.pickableProperties}
|
|
extraLib={`declare const result = ${JSON.stringify(result)};\n` + stepPropPicker.extraLib}
|
|
bind:editor={stopAfterAllItersEditor}
|
|
/>
|
|
</PropPickerWrapper>
|
|
{#if isStopAfterAllIterationsEnabled && !breakableParent && flowModule.stop_after_all_iters_if}
|
|
{@render stopStatusPicker(flowModule.stop_after_all_iters_if)}
|
|
{/if}
|
|
</div>
|
|
{/if}
|
|
</div>
|