windmill-components 1.382.1 → 1.382.7

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (44) hide show
  1. package/package/base.d.ts +1 -1
  2. package/package/base.js +1 -1
  3. package/package/components/EditorBar.svelte +48 -38
  4. package/package/components/EditorBar.svelte.d.ts +2 -0
  5. package/package/components/FlowBuilder.svelte +15 -12
  6. package/package/components/FlowBuilder.svelte.d.ts +2 -2
  7. package/package/components/FlowStatusViewer.svelte +7 -1
  8. package/package/components/FlowStatusViewer.svelte.d.ts +3 -0
  9. package/package/components/FlowStatusViewerInner.svelte +12 -5
  10. package/package/components/FlowViewer.svelte +6 -46
  11. package/package/components/FlowViewer.svelte.d.ts +1 -0
  12. package/package/components/FlowViewerInner.svelte +109 -0
  13. package/package/components/FlowViewerInner.svelte.d.ts +22 -0
  14. package/package/components/ScriptBuilder.svelte +48 -40
  15. package/package/components/ScriptBuilder.svelte.d.ts +3 -0
  16. package/package/components/ScriptEditor.svelte +3 -1
  17. package/package/components/ScriptEditor.svelte.d.ts +2 -0
  18. package/package/components/apps/components/display/AppText.svelte +8 -3
  19. package/package/components/apps/components/inputs/AppSelect.svelte +45 -29
  20. package/package/components/apps/editor/component/components.d.ts +6 -0
  21. package/package/components/apps/editor/component/components.js +6 -0
  22. package/package/components/custom_ui.d.ts +35 -1
  23. package/package/components/details/DetailPageDetailPanel.svelte +4 -38
  24. package/package/components/details/WebhooksPanel.svelte +3 -3
  25. package/package/components/flows/content/FlowModuleComponent.svelte +8 -2
  26. package/package/components/flows/content/FlowModuleEarlyStop.svelte +193 -66
  27. package/package/components/flows/content/FlowModuleHeader.svelte +1 -1
  28. package/package/components/flows/content/FlowSettings.svelte +26 -15
  29. package/package/components/flows/flowExplorer.js +3 -0
  30. package/package/components/flows/map/MapItem.svelte +1 -1
  31. package/package/components/flows/propPicker/PropPickerWrapper.svelte +2 -0
  32. package/package/components/flows/propPicker/PropPickerWrapper.svelte.d.ts +1 -0
  33. package/package/components/flows/types.d.ts +2 -0
  34. package/package/components/graph/model.d.ts +3 -0
  35. package/package/components/propertyPicker/PropPickerResult.svelte +6 -1
  36. package/package/components/propertyPicker/PropPickerResult.svelte.d.ts +1 -0
  37. package/package/components/sidebar/changelogs.js +5 -0
  38. package/package/gen/core/OpenAPI.js +1 -1
  39. package/package/gen/schemas.gen.d.ts +12 -0
  40. package/package/gen/schemas.gen.js +12 -0
  41. package/package/gen/services.gen.d.ts +4 -3
  42. package/package/gen/services.gen.js +7 -5
  43. package/package/gen/types.gen.d.ts +16 -8
  44. package/package.json +1 -1
@@ -1,6 +1,7 @@
1
1
  import { SvelteComponent } from "svelte";
2
2
  import { type NewScript, type NewScriptWithDraft } from '../gen';
3
3
  import type DiffDrawer from './DiffDrawer.svelte';
4
+ import type { ScriptBuilderWhitelabelCustomUi } from './custom_ui';
4
5
  declare const __propDef: {
5
6
  props: {
6
7
  script: NewScript;
@@ -10,11 +11,13 @@ declare const __propDef: {
10
11
  initialArgs?: Record<string, any> | undefined;
11
12
  lockedLanguage?: boolean | undefined;
12
13
  showMeta?: boolean | undefined;
14
+ neverShowMeta?: boolean | undefined;
13
15
  diffDrawer?: DiffDrawer | undefined;
14
16
  savedScript?: NewScriptWithDraft | undefined;
15
17
  searchParams?: URLSearchParams | undefined;
16
18
  disableHistoryChange?: boolean | undefined;
17
19
  replaceStateFn?: ((url: string) => void) | undefined;
20
+ customUi?: ScriptBuilderWhitelabelCustomUi | undefined;
18
21
  setCode?: ((code: string) => void) | undefined;
19
22
  };
20
23
  events: {
@@ -38,6 +38,7 @@ export let edit = true;
38
38
  export let noHistory = false;
39
39
  export let saveToWorkspace = false;
40
40
  export let watchChanges = false;
41
+ export let customUi = {};
41
42
  let websocketAlive = {
42
43
  pyright: false,
43
44
  deno: false,
@@ -204,6 +205,7 @@ function collabUrl() {
204
205
  setCollaborationMode()
205
206
  }
206
207
  }}
208
+ customUi={customUi?.editorBar}
207
209
  collabLive={wsProvider?.shouldConnect}
208
210
  {collabMode}
209
211
  {validCode}
@@ -221,7 +223,7 @@ function collabUrl() {
221
223
  {noHistory}
222
224
  {saveToWorkspace}
223
225
  />
224
- {#if !noSyncFromGithub}
226
+ {#if !noSyncFromGithub && customUi?.editorBar?.useVsCode != false}
225
227
  <div class="py-1">
226
228
  <Button
227
229
  target="_blank"
@@ -3,6 +3,7 @@ import type { Schema, SupportedLanguage } from '../common';
3
3
  import { type Preview } from '../gen';
4
4
  import Editor from './Editor.svelte';
5
5
  import DiffEditor from './DiffEditor.svelte';
6
+ import type { ScriptEditorWhitelabelCustomUi } from './custom_ui';
6
7
  declare const __propDef: {
7
8
  props: {
8
9
  schema?: Schema | any;
@@ -22,6 +23,7 @@ declare const __propDef: {
22
23
  noHistory?: boolean | undefined;
23
24
  saveToWorkspace?: boolean | undefined;
24
25
  watchChanges?: boolean | undefined;
26
+ customUi?: ScriptEditorWhitelabelCustomUi | undefined;
25
27
  inferSchema?: ((code: string, nlang?: SupportedLanguage) => Promise<void>) | undefined;
26
28
  setCollaborationMode?: (() => Promise<void>) | undefined;
27
29
  disableCollaboration?: (() => void) | undefined;
@@ -21,6 +21,11 @@ export let customCss = undefined;
21
21
  export let render;
22
22
  export let editorMode = false;
23
23
  let resolvedConfig = initConfig(components['textcomponent'].initialData.configuration, configuration);
24
+ $: editorMode && onEditorMode();
25
+ function onEditorMode() {
26
+ autosize();
27
+ setTimeout(() => autosize(), 50);
28
+ }
24
29
  const { app, worldStore, mode, componentControl } = getContext('AppViewerContext');
25
30
  let css = initCss($app.css?.textcomponent, customCss);
26
31
  let result = undefined;
@@ -105,6 +110,7 @@ function autosize() {
105
110
  el.style.cssText = 'height:auto; padding:0';
106
111
  el.style.cssText = 'height:' + el.scrollHeight + 'px';
107
112
  }
113
+ // console.log(el, el?.scrollHeight)
108
114
  }, 0);
109
115
  }
110
116
  </script>
@@ -137,7 +143,6 @@ function autosize() {
137
143
  if (!editorMode) {
138
144
  editorMode = true
139
145
  document.getElementById(`text-${id}`)?.focus()
140
- autosize()
141
146
  }
142
147
  }}
143
148
  on:keydown|stopPropagation
@@ -146,7 +151,7 @@ function autosize() {
146
151
  <AlignWrapper {verticalAlignment}>
147
152
  <textarea
148
153
  class={twMerge(
149
- 'whitespace-pre-wrap !outline-none !border-0 !bg-transparent !resize-none !overflow-hidden !ring-0 !p-0',
154
+ 'whitespace-pre-wrap !outline-none !border-0 !bg-transparent !resize-none !ring-0 !p-0',
150
155
  css?.text?.class,
151
156
  'wm-text',
152
157
  classes,
@@ -188,7 +193,7 @@ function autosize() {
188
193
  class="flex flex-wrap gap-2 pb-0.5 w-full {$mode === 'dnd' &&
189
194
  (componentInput?.type == 'template' || componentInput?.type == 'templatev2')
190
195
  ? 'cursor-text'
191
- : ''}"
196
+ : 'overflow-auto'}"
192
197
  >
193
198
  <svelte:element
194
199
  this={component}
@@ -101,6 +101,11 @@ function onChange(e) {
101
101
  onSelect.forEach((id) => $runnableComponents?.[id]?.cb?.forEach((f) => f()));
102
102
  }
103
103
  }
104
+ function onNativeChange(e) {
105
+ const target = e.target;
106
+ const value = target.value;
107
+ setValue(value);
108
+ }
104
109
  function setValue(nvalue) {
105
110
  let result = undefined;
106
111
  try {
@@ -188,35 +193,46 @@ let filterText = '';
188
193
  }}
189
194
  >
190
195
  {#if Array.isArray(listItems) && listItems.every((x) => x && typeof x == 'object' && typeof x['label'] == 'string' && `value` in x)}
191
- <Select
192
- inAppEditor={true}
193
- --border-radius="0.250rem"
194
- --clear-icon-color="#6b7280"
195
- --border={$darkMode ? '1px solid #6b7280' : '1px solid #d1d5db'}
196
- bind:filterText
197
- on:filter={handleFilter}
198
- on:clear={onClear}
199
- on:change={onChange}
200
- items={listItems}
201
- listAutoWidth={resolvedConfig.fullWidth}
202
- inputStyles={SELECT_INPUT_DEFAULT_STYLE.inputStyles}
203
- containerStyles={($darkMode
204
- ? SELECT_INPUT_DEFAULT_STYLE.containerStylesDark
205
- : SELECT_INPUT_DEFAULT_STYLE.containerStyles) + css?.input?.style}
206
- {value}
207
- class={css?.input?.class}
208
- placeholder={resolvedConfig.placeholder}
209
- disabled={resolvedConfig.disabled}
210
- on:focus={() => {
211
- if (!$connectingInput.opened) {
212
- $selectedComponent = [id]
213
- }
214
- }}
215
- >
216
- <svelte:fragment slot="item" let:item
217
- >{#if resolvedConfig.create}{item.created ? 'Add new: ' : ''}{/if}{item.label}
218
- </svelte:fragment>
219
- </Select>
196
+ {#if resolvedConfig.nativeHtmlSelect}
197
+ <select class={css?.input?.class} style={css?.input?.style} on:change={onNativeChange}>
198
+ {#if resolvedConfig.placeholder}
199
+ <option value="" disabled selected>{resolvedConfig.placeholder}</option>
200
+ {/if}
201
+ {#each listItems as item (item.value)}
202
+ <option value={item.value} selected={item.value === value}>{item.label}</option>
203
+ {/each}
204
+ </select>
205
+ {:else}
206
+ <Select
207
+ inAppEditor={true}
208
+ --border-radius="0.250rem"
209
+ --clear-icon-color="#6b7280"
210
+ --border={$darkMode ? '1px solid #6b7280' : '1px solid #d1d5db'}
211
+ bind:filterText
212
+ on:filter={handleFilter}
213
+ on:clear={onClear}
214
+ on:change={onChange}
215
+ items={listItems}
216
+ listAutoWidth={resolvedConfig.fullWidth}
217
+ inputStyles={SELECT_INPUT_DEFAULT_STYLE.inputStyles}
218
+ containerStyles={($darkMode
219
+ ? SELECT_INPUT_DEFAULT_STYLE.containerStylesDark
220
+ : SELECT_INPUT_DEFAULT_STYLE.containerStyles) + css?.input?.style}
221
+ {value}
222
+ class={css?.input?.class}
223
+ placeholder={resolvedConfig.placeholder}
224
+ disabled={resolvedConfig.disabled}
225
+ on:focus={() => {
226
+ if (!$connectingInput.opened) {
227
+ $selectedComponent = [id]
228
+ }
229
+ }}
230
+ >
231
+ <svelte:fragment slot="item" let:item
232
+ >{#if resolvedConfig.create}{item.created ? 'Add new: ' : ''}{/if}{item.label}
233
+ </svelte:fragment>
234
+ </Select>
235
+ {/if}
220
236
  {:else}
221
237
  <Popover notClickable placement="bottom" popupClass="!bg-surface border w-96">
222
238
  <div
@@ -2388,6 +2388,12 @@ export declare const components: {
2388
2388
  readonly fieldType: "boolean";
2389
2389
  readonly tooltip: "Preselect first item in the options if no default value is set";
2390
2390
  };
2391
+ readonly nativeHtmlSelect: {
2392
+ readonly type: "static";
2393
+ readonly fieldType: "boolean";
2394
+ readonly value: false;
2395
+ readonly tooltip: "Use a native html select instead of the Windmill select component";
2396
+ };
2391
2397
  readonly fullWidth: {
2392
2398
  readonly type: "static";
2393
2399
  readonly fieldType: "boolean";
@@ -1625,6 +1625,12 @@ This is a paragraph.
1625
1625
  fieldType: 'boolean',
1626
1626
  tooltip: 'Preselect first item in the options if no default value is set'
1627
1627
  },
1628
+ nativeHtmlSelect: {
1629
+ type: 'static',
1630
+ fieldType: 'boolean',
1631
+ value: false,
1632
+ tooltip: 'Use a native html select instead of the Windmill select component'
1633
+ },
1628
1634
  fullWidth: {
1629
1635
  type: 'static',
1630
1636
  fieldType: 'boolean',
@@ -1,5 +1,5 @@
1
1
  import type { SupportedLanguage } from '../common';
2
- export type WhitelabelCustomUi = {
2
+ export type FlowBuilderWhitelabelCustomUi = {
3
3
  topBar?: {
4
4
  path?: boolean;
5
5
  export?: boolean;
@@ -7,8 +7,18 @@ export type WhitelabelCustomUi = {
7
7
  aiBuilder?: boolean;
8
8
  tutorials?: boolean;
9
9
  diff?: boolean;
10
+ extraDeployOptions?: boolean;
10
11
  };
11
12
  settingsPanel?: boolean;
13
+ settingsTabs?: {
14
+ schedule?: boolean;
15
+ sharedDiretory?: boolean;
16
+ earlyStop?: boolean;
17
+ earlyReturn?: boolean;
18
+ workerGroup?: boolean;
19
+ concurrency?: boolean;
20
+ cache?: boolean;
21
+ };
12
22
  triggers?: boolean;
13
23
  flowNode?: boolean;
14
24
  hub?: boolean;
@@ -22,4 +32,28 @@ export type WhitelabelCustomUi = {
22
32
  stepAdvancedSettings?: boolean;
23
33
  languages?: (SupportedLanguage | 'docker' | 'bunnative')[];
24
34
  scriptFork?: boolean;
35
+ editorBar?: EditorBarUi;
36
+ };
37
+ export type EditorBarUi = {
38
+ contextVar?: boolean;
39
+ variable?: boolean;
40
+ type?: boolean;
41
+ assistants?: boolean;
42
+ multiplayer?: boolean;
43
+ autoformatting?: boolean;
44
+ aiGen?: boolean;
45
+ aiFix?: boolean;
46
+ library?: boolean;
47
+ useVsCode?: boolean;
48
+ };
49
+ export type ScriptEditorWhitelabelCustomUi = {
50
+ editorBar?: EditorBarUi;
51
+ };
52
+ export type ScriptBuilderWhitelabelCustomUi = {
53
+ topBar?: {
54
+ path?: boolean;
55
+ settings?: boolean;
56
+ extraDeployOptions?: boolean;
57
+ };
58
+ editorBar?: EditorBarUi;
25
59
  };
@@ -1,18 +1,13 @@
1
- <script>import { Tabs, Tab, TabContent, Button } from '../common';
2
- import { copyToClipboard } from '../../utils';
3
- import { CalendarCheck2, Clipboard, MailIcon, Terminal, Webhook } from 'lucide-svelte';
4
- import { Highlight } from 'svelte-highlight';
5
- import { yaml } from 'svelte-highlight/languages';
6
- import json from 'svelte-highlight/languages/json';
1
+ <script>import { Tabs, Tab, TabContent } from '../common';
2
+ import { CalendarCheck2, MailIcon, Terminal, Webhook } from 'lucide-svelte';
7
3
  import { Pane, Splitpanes } from 'svelte-splitpanes';
8
- import YAML from 'yaml';
9
4
  import HighlightTheme from '../HighlightTheme.svelte';
5
+ import FlowViewerInner from '../FlowViewerInner.svelte';
10
6
  export let triggerSelected = 'webhooks';
11
7
  export let flow_json = undefined;
12
8
  export let hasStepDetails = false;
13
9
  export let isOperator = false;
14
10
  export let selected;
15
- let rawType = 'yaml';
16
11
  $: if (hasStepDetails) {
17
12
  selected = 'flow_step';
18
13
  }
@@ -90,36 +85,7 @@ $: !hasStepDetails && selected === 'flow_step' && (selected = 'saved_inputs');
90
85
  </Splitpanes>
91
86
  </TabContent>
92
87
  <TabContent value="raw" class="flex flex-col flex-1 h-full overflow-auto">
93
- <Tabs bind:selected={rawType} wrapperClass="mt-4">
94
- <Tab value="yaml">YAML</Tab>
95
- <Tab value="json">JSON</Tab>
96
- <svelte:fragment slot="content">
97
- <div class="relative pt-2">
98
- <Button
99
- on:click={() =>
100
- copyToClipboard(
101
- rawType === 'yaml'
102
- ? YAML.stringify(flow_json)
103
- : JSON.stringify(flow_json, null, 4)
104
- )}
105
- color="light"
106
- variant="border"
107
- size="xs"
108
- startIcon={{ icon: Clipboard }}
109
- btnClasses="absolute top-2 right-2 w-min"
110
- >
111
- Copy content
112
- </Button>
113
- <Highlight
114
- class="overflow-auto px-1"
115
- language={rawType === 'yaml' ? yaml : json}
116
- code={rawType === 'yaml'
117
- ? YAML.stringify(flow_json)
118
- : JSON.stringify(flow_json, null, 4)}
119
- />
120
- </div>
121
- </svelte:fragment>
122
- </Tabs>
88
+ <FlowViewerInner flow={flow_json} />
123
89
  </TabContent>
124
90
  <TabContent value="flow_step" class="flex flex-col flex-1 h-full">
125
91
  <slot name="flow_step" />
@@ -39,9 +39,9 @@ function computeScriptWebhooks(hash, path) {
39
39
  };
40
40
  }
41
41
  function computeFlowWebhooks(path) {
42
- let base = `${$page.url.origin}/api/w/${$workspaceStore}/jobs`;
43
- let urlAsync = `${base}/run/f/${path}`;
44
- let urlSync = `${base}/run_wait_result/f/${path}`;
42
+ let webhooksBase = `${$page.url.origin}${base}/api/w/${$workspaceStore}/jobs`;
43
+ let urlAsync = `${webhooksBase}/run/f/${path}`;
44
+ let urlSync = `${webhooksBase}/run_wait_result/f/${path}`;
45
45
  return {
46
46
  async: {
47
47
  path: urlAsync
@@ -41,7 +41,7 @@ import { loadSchemaFromModule } from '../flowInfers';
41
41
  import { computeFlowStepWarning, initFlowStepWarnings } from '../utils';
42
42
  import { debounce } from '../../../utils';
43
43
  import { dfs } from '../dfs';
44
- const { selectedId, previewArgs, flowStateStore, flowStore, pathStore, saveDraft, flowInputsStore } = getContext('FlowEditorContext');
44
+ const { selectedId, previewArgs, flowStateStore, flowStore, pathStore, saveDraft, flowInputsStore, customUi } = getContext('FlowEditorContext');
45
45
  export let flowModule;
46
46
  export let failureModule = false;
47
47
  export let parentModule = undefined;
@@ -216,6 +216,7 @@ function setFlowInput(argName) {
216
216
  {#if flowModule.value.type === 'rawscript' && !noEditor}
217
217
  <div class="border-b-2 shadow-sm px-1">
218
218
  <EditorBar
219
+ customUi={customUi?.editorBar}
219
220
  {validCode}
220
221
  {editor}
221
222
  {diffEditor}
@@ -353,7 +354,12 @@ function setFlowInput(argName) {
353
354
  {#if !$selectedId.includes('failure')}
354
355
  <Tab value="runtime">Runtime</Tab>
355
356
  <Tab value="cache" active={Boolean(flowModule.cache_ttl)}>Cache</Tab>
356
- <Tab value="early-stop" active={Boolean(flowModule.stop_after_if)}>
357
+ <Tab
358
+ value="early-stop"
359
+ active={Boolean(
360
+ flowModule.stop_after_if || flowModule.stop_after_all_iters_if
361
+ )}
362
+ >
357
363
  Early Stop
358
364
  </Tab>
359
365
  <Tab value="suspend" active={Boolean(flowModule.suspend)}>Suspend</Tab>
@@ -6,84 +6,211 @@ import { getContext } from 'svelte';
6
6
  import { NEVER_TESTED_THIS_FAR } from '../models';
7
7
  import Section from '../../Section.svelte';
8
8
  import { getStepPropPicker } from '../previousResults';
9
+ import { dfs } from '../previousResults';
9
10
  const { flowStateStore, flowStore, previewArgs } = getContext('FlowEditorContext');
10
11
  export let flowModule;
11
12
  let editor = undefined;
12
13
  $: stepPropPicker = getStepPropPicker($flowStateStore, undefined, undefined, flowModule.id, $flowStore, $previewArgs, false);
14
+ function checkIfParentLoop(flowStore) {
15
+ const flow = JSON.parse(JSON.stringify(flowStore));
16
+ const parents = dfs(flowModule.id, flow, true);
17
+ for (const parent of parents.slice(1)) {
18
+ if (parent.value.type === 'forloopflow' || parent.value.type === 'whileloopflow') {
19
+ return parent.id;
20
+ }
21
+ }
22
+ return null;
23
+ }
24
+ $: isLoop = flowModule.value.type === 'forloopflow' || flowModule.value.type === 'whileloopflow';
25
+ $: isBranchAll = flowModule.value.type === 'branchall';
13
26
  $: isStopAfterIfEnabled = Boolean(flowModule.stop_after_if);
27
+ $: isStopAfterAllIterationsEnabled = Boolean(flowModule.stop_after_all_iters_if);
14
28
  $: result = $flowStateStore[flowModule.id]?.previewResult ?? NEVER_TESTED_THIS_FAR;
29
+ $: parentLoopId = checkIfParentLoop($flowStore);
15
30
  </script>
16
31
 
17
32
  <div class="flex flex-col items-start space-y-2 {$$props.class}">
18
- <Section label="Early stop/Break" class="w-full">
19
- <svelte:fragment slot="header">
20
- <Tooltip documentationLink="https://www.windmill.dev/docs/flows/early_stop">
21
- If defined, at the end of the step, the predicate expression will be evaluated to decide if
22
- the flow should stop early.
23
- </Tooltip>
24
- </svelte:fragment>
33
+ {#if !isBranchAll}
34
+ <Section
35
+ label={(isLoop
36
+ ? 'Break loop'
37
+ : parentLoopId
38
+ ? 'Break parent loop module ' + parentLoopId
39
+ : 'Stop flow early') + (isLoop ? ' (evaluated after each iteration)' : '')}
40
+ class="w-full"
41
+ >
42
+ <svelte:fragment slot="header">
43
+ <Tooltip documentationLink="https://www.windmill.dev/docs/flows/early_stop">
44
+ If defined, at the end of the step, the predicate expression will be evaluated to decide
45
+ if the flow should stop early or break if inside a for/while loop.
46
+ </Tooltip>
47
+ </svelte:fragment>
25
48
 
26
- <Toggle
27
- checked={isStopAfterIfEnabled}
28
- on:change={() => {
29
- if (isStopAfterIfEnabled && flowModule.stop_after_if) {
30
- flowModule.stop_after_if = undefined
31
- } else {
32
- flowModule.stop_after_if = {
33
- expr: 'result == undefined',
34
- skip_if_stopped: false
49
+ <Toggle
50
+ checked={isStopAfterIfEnabled}
51
+ on:change={() => {
52
+ if (isStopAfterIfEnabled && flowModule.stop_after_if) {
53
+ flowModule.stop_after_if = undefined
54
+ } else {
55
+ flowModule.stop_after_if = {
56
+ expr: 'result == undefined',
57
+ skip_if_stopped: false
58
+ }
35
59
  }
36
- }
37
- }}
38
- options={{
39
- right: 'Early stop or Break if condition met'
40
- }}
41
- />
60
+ }}
61
+ options={{
62
+ right: isLoop
63
+ ? 'Break loop'
64
+ : parentLoopId
65
+ ? 'Break parent loop module'
66
+ : 'Stop flow' + ' if condition met'
67
+ }}
68
+ />
69
+
70
+ <div
71
+ class="w-full border p-2 flex flex-col {flowModule.stop_after_if
72
+ ? ''
73
+ : 'bg-surface-secondary'}"
74
+ >
75
+ {#if flowModule.stop_after_if}
76
+ {@const earlyStopResult = isLoop
77
+ ? Array.isArray(result) && result.length > 0
78
+ ? result[result.length - 1]
79
+ : result === NEVER_TESTED_THIS_FAR
80
+ ? result
81
+ : undefined
82
+ : result}
83
+ {#if !parentLoopId && !isLoop}
84
+ <Toggle
85
+ size="xs"
86
+ bind:checked={flowModule.stop_after_if.skip_if_stopped}
87
+ options={{
88
+ right: 'Label flow as "skipped" if stopped'
89
+ }}
90
+ />
91
+ {/if}
92
+ <span class="mt-2 text-xs font-bold">Stop condition expression</span>
93
+ <div class="border w-full">
94
+ <PropPickerWrapper
95
+ notSelectable
96
+ flow_input={stepPropPicker.pickableProperties.flow_input}
97
+ pickableProperties={undefined}
98
+ result={earlyStopResult}
99
+ extraResults={isLoop ? { all_iters: result } : undefined}
100
+ on:select={({ detail }) => {
101
+ editor?.insertAtCursor(detail)
102
+ editor?.focus()
103
+ }}
104
+ >
105
+ <SimpleEditor
106
+ bind:this={editor}
107
+ lang="javascript"
108
+ bind:code={flowModule.stop_after_if.expr}
109
+ class="few-lines-editor"
110
+ extraLib={`declare const result = ${JSON.stringify(earlyStopResult)};` +
111
+ (isLoop ? `\ndeclare const all_iters = ${JSON.stringify(result)};` : '')}
112
+ />
113
+ </PropPickerWrapper>
114
+ </div>
115
+ {:else}
116
+ {#if !parentLoopId && !isLoop}
117
+ <Toggle
118
+ disabled
119
+ size="xs"
120
+ options={{
121
+ right: 'Label flow as "skipped" if stopped'
122
+ }}
123
+ />
124
+ {/if}
125
+ <span class="mt-2 text-xs font-bold">Stop condition expression</span>
126
+ <textarea disabled rows="3" class="min-h-[80px]" />
127
+ {/if}
128
+ </div>
129
+ </Section>
130
+ {/if}
42
131
 
43
- <div
44
- class="w-full border p-2 flex flex-col {flowModule.stop_after_if
45
- ? ''
46
- : 'bg-surface-secondary'}"
132
+ {#if isLoop || isBranchAll}
133
+ <Section
134
+ label={(parentLoopId ? 'Break parent loop module ' + parentLoopId : 'Stop flow early') +
135
+ (isBranchAll
136
+ ? ' (evaluated after all branches have been run)'
137
+ : ' (evaluated after all iterations)')}
138
+ class="w-full"
47
139
  >
48
- {#if flowModule.stop_after_if}
49
- <Toggle
50
- size="xs"
51
- bind:checked={flowModule.stop_after_if.skip_if_stopped}
52
- options={{
53
- right: 'Label flow as "skipped" if stopped'
54
- }}
55
- />
56
- <span class="mt-2 text-xs font-bold">Stop condition expression</span>
57
- <div class="border w-full">
58
- <PropPickerWrapper
59
- notSelectable
60
- flow_input={stepPropPicker.pickableProperties.flow_input}
61
- pickableProperties={undefined}
62
- {result}
63
- on:select={({ detail }) => {
64
- editor?.insertAtCursor(detail)
65
- editor?.focus()
66
- }}
67
- >
68
- <SimpleEditor
69
- bind:this={editor}
70
- lang="javascript"
71
- bind:code={flowModule.stop_after_if.expr}
72
- class="few-lines-editor"
73
- extraLib={`declare const result = ${JSON.stringify(result)};`}
140
+ <svelte:fragment slot="header">
141
+ <Tooltip documentationLink="https://www.windmill.dev/docs/flows/early_stop">
142
+ If defined, at the end of the step, the predicate expression will be evaluated to decide
143
+ if the flow should stop early or break if inside a for/while loop.
144
+ </Tooltip>
145
+ </svelte:fragment>
146
+
147
+ <Toggle
148
+ checked={isStopAfterAllIterationsEnabled}
149
+ on:change={() => {
150
+ if (isStopAfterAllIterationsEnabled && flowModule.stop_after_all_iters_if) {
151
+ flowModule.stop_after_all_iters_if = undefined
152
+ } else {
153
+ flowModule.stop_after_all_iters_if = {
154
+ expr: 'result == undefined',
155
+ skip_if_stopped: false
156
+ }
157
+ }
158
+ }}
159
+ options={{
160
+ right: (parentLoopId ? 'Break parent loop module' : 'Stop flow') + ' if condition met'
161
+ }}
162
+ />
163
+
164
+ <div
165
+ class="w-full border p-2 flex flex-col {flowModule.stop_after_all_iters_if
166
+ ? ''
167
+ : 'bg-surface-secondary'}"
168
+ >
169
+ {#if flowModule.stop_after_all_iters_if}
170
+ {#if !parentLoopId}
171
+ <Toggle
172
+ size="xs"
173
+ bind:checked={flowModule.stop_after_all_iters_if.skip_if_stopped}
174
+ options={{
175
+ right: 'Label flow as "skipped" if stopped'
176
+ }}
177
+ />
178
+ {/if}
179
+ <span class="mt-2 text-xs font-bold">Stop condition expression</span>
180
+ <div class="border w-full">
181
+ <PropPickerWrapper
182
+ notSelectable
183
+ flow_input={stepPropPicker.pickableProperties.flow_input}
184
+ pickableProperties={undefined}
185
+ {result}
186
+ on:select={({ detail }) => {
187
+ editor?.insertAtCursor(detail)
188
+ editor?.focus()
189
+ }}
190
+ >
191
+ <SimpleEditor
192
+ bind:this={editor}
193
+ lang="javascript"
194
+ bind:code={flowModule.stop_after_all_iters_if.expr}
195
+ class="few-lines-editor"
196
+ extraLib={`declare const result = ${JSON.stringify(result)};`}
197
+ />
198
+ </PropPickerWrapper>
199
+ </div>
200
+ {:else}
201
+ {#if !parentLoopId}
202
+ <Toggle
203
+ disabled
204
+ size="xs"
205
+ options={{
206
+ right: 'Label flow as "skipped" if stopped'
207
+ }}
74
208
  />
75
- </PropPickerWrapper>
76
- </div>
77
- {:else}
78
- <Toggle
79
- disabled
80
- size="xs"
81
- options={{
82
- right: 'Label flow as "skipped" if stopped'
83
- }}
84
- /> <span class="mt-2 text-xs font-bold">Stop condition expression</span>
85
- <textarea disabled rows="3" class="min-h-[80px]" />
86
- {/if}
87
- </div>
88
- </Section>
209
+ {/if}
210
+ <span class="mt-2 text-xs font-bold">Stop condition expression</span>
211
+ <textarea disabled rows="3" class="min-h-[80px]" />
212
+ {/if}
213
+ </div>
214
+ </Section>
215
+ {/if}
89
216
  </div>