fix: make workspace preprocessor scripts selectable in flow preprocessor steps (#10786)

* fix: pick workspace preprocessor scripts in the flow preprocessor step

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01QqKXVsBXynMFtMZ26uvLw7

* fix: explain the empty preprocessor list and keep the editor bar hub populated

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01QqKXVsBXynMFtMZ26uvLw7

* fix: derive the editor bar's script kind from the preprocessor slot

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01QqKXVsBXynMFtMZ26uvLw7

* fix: keep the preprocessor entrypoint when resetting a step's content

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01QqKXVsBXynMFtMZ26uvLw7

* docs: state the preprocessor reset invariant instead of the old control flow

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01QqKXVsBXynMFtMZ26uvLw7

---------

Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
Ruben Fiszel
2026-08-21 01:05:26 +02:00
committed by GitHub
co-authored by Claude Opus 5
parent 2439a610be
commit a9112b72a5
8 changed files with 115 additions and 51 deletions
+8 -2
View File
@@ -94,7 +94,7 @@
// editor). `undefined` = not applicable; `false` makes the badge red
// even if the main function parses.
validAssets?: boolean | undefined
kind?: 'script' | 'trigger' | 'approval'
kind?: 'script' | 'trigger' | 'approval' | 'preprocessor'
template?:
| 'pgsql'
| 'mysql'
@@ -580,7 +580,13 @@
<Drawer bind:this={scriptPicker} size="900px">
<DrawerContent title="Code" on:close={scriptPicker.closeDrawer}>
{#if pick_existing == 'hub'}
<PickHubScript bind:filter {kind} on:pick={onScriptPick}>
<!-- The hub publishes no preprocessor script, so that kind falls back to the action list
rather than showing an empty hub. -->
<PickHubScript
bind:filter
kind={kind == 'preprocessor' ? 'script' : kind}
on:pick={onScriptPick}
>
<ToggleHubWorkspace bind:selected={pick_existing} />
</PickHubScript>
{:else}
@@ -44,6 +44,11 @@
? 'approval'
: 'script'
)
// The preprocessor slot shows no kind toggle, so `kind` stays 'script' there. Everything that
// tags a script (inline template, pre-made list) must key off this, never off `kind`.
let scriptKind: 'script' | 'failure' | 'approval' | 'trigger' | 'preprocessor' = $derived(
preprocessorModule ? 'preprocessor' : kind
)
let pick_existing: 'workspace' | 'hub' = $state('hub')
let filter = $state('')
@@ -56,7 +61,7 @@
)
function displayLang(lang: SupportedLanguage | 'docker', kind: string) {
if (preprocessorModule) {
if (kind === 'preprocessor') {
return canHavePreprocessor(lang as SupportedLanguage)
}
@@ -218,21 +223,23 @@
<h3 class="pb-2 pt-4 flex gap-x-8 flex-wrap">
<div>
Inline new <span class="text-blue-500 dark:text-blue-400"
>{kind == 'script' ? 'action' : kind}</span
>{scriptKind == 'script' ? 'action' : scriptKind}</span
>
script
<Tooltip
documentationLink={kind === 'script'
documentationLink={scriptKind === 'script'
? 'https://www.windmill.dev/docs/flows/editor_components#flow-actions'
: kind === 'trigger'
: scriptKind === 'trigger'
? 'https://www.windmill.dev/docs/flows/flow_trigger'
: kind === 'approval'
: scriptKind === 'approval'
? 'https://www.windmill.dev/docs/flows/flow_approval'
: 'https://www.windmill.dev/docs/getting_started/flows_quickstart#flow-editor'}
: scriptKind === 'preprocessor'
? 'https://www.windmill.dev/docs/core_concepts/preprocessors'
: 'https://www.windmill.dev/docs/getting_started/flows_quickstart#flow-editor'}
>
Embed <span>{kind == 'script' ? 'action' : kind}</span> script directly inside a flow instead
of saving the script into your workspace for reuse. You can always save an inline script to
your workspace later.
Embed <span>{scriptKind == 'script' ? 'action' : scriptKind}</span> script directly inside
a flow instead of saving the script into your workspace for reuse. You can always save an inline
script to your workspace later.
</Tooltip>
</div>
<DefaultScripts />
@@ -250,7 +257,7 @@
{/if}
<div class="flex flex-row flex-wrap gap-2" id="flow-editor-action-script">
{#each langs.filter((lang) => customUi?.languages == undefined || customUi?.languages?.includes(lang?.[1])) as [label, lang] (lang)}
{#if displayLang(lang, kind)}
{#if displayLang(lang, scriptKind)}
<FlowScriptPicker
id={`flow-editor-action-script-${lang}`}
disabled={noEditor && (summary == undefined || summary == '')}
@@ -259,7 +266,7 @@
on:click={() => {
dispatch('new', {
language: lang == 'docker' ? 'bash' : lang,
kind,
kind: scriptKind,
subkind: lang == 'docker' ? 'docker' : preprocessorModule ? 'preprocessor' : 'flow',
summary
})
@@ -289,10 +296,13 @@
<h3 class="mb-2 mt-6"
>Use pre-made <span class="text-blue-500 dark:text-blue-400"
>{kind == 'script' ? 'action' : kind}</span
>{scriptKind == 'script' ? 'action' : scriptKind}</span
> script</h3
>
{#if pick_existing == 'hub'}
{#if preprocessorModule}
<!-- The hub publishes no preprocessor script, so this slot only ever picks from the workspace. -->
<WorkspaceScriptPicker displayLock bind:filter kind="preprocessor" on:pick />
{:else if pick_existing == 'hub'}
<PickHubScript bind:filter {kind} on:pick>
<ToggleHubWorkspace bind:selected={pick_existing} />
</PickHubScript>
@@ -109,7 +109,7 @@
preprocessorModule?: boolean
parentModule?: FlowModule | undefined
previousModule: FlowModule | undefined
scriptKind?: 'script' | 'trigger' | 'approval'
scriptKind?: 'script' | 'trigger' | 'approval' | 'preprocessor'
scriptTemplate?: 'pgsql' | 'mysql' | 'script' | 'docker' | 'powershell'
noEditor: boolean
enableAi: boolean
@@ -171,6 +171,11 @@
shellcheck: false
})
// `scriptKind` only records how a step was created this session, so it is back to 'script' on
// any remount. Being a preprocessor is a property of the slot, and the editor bar's reset code
// and script library depend on it, so derive it rather than reading the stale state.
let editorScriptKind = $derived(preprocessorModule ? 'preprocessor' : scriptKind)
let selected = $state(untrack(() => preprocessorModule) ? 'test' : 'inputs')
let canShowChatTab = $derived(
!preprocessorModule &&
@@ -864,7 +869,7 @@
{websocketAlive}
iconOnly={width < EDITOR_BAR_WIDTH_THRESHOLD}
compactHelpers={width < EDITOR_BAR_HELPERS_INLINE_THRESHOLD}
kind={scriptKind}
kind={editorScriptKind}
template={scriptTemplate}
args={Object.entries(flowModule.value.input_transforms).reduce((acc, [key, obj]) => {
acc[key] = obj.type === 'static' ? obj.value : undefined
@@ -32,7 +32,7 @@
const { triggersState, triggersCount } = getContext<TriggerContext>('TriggerContext')
let scriptKind: 'script' | 'trigger' | 'approval' = $state('script')
let scriptKind: 'script' | 'trigger' | 'approval' | 'preprocessor' = $state('script')
let scriptTemplate: 'pgsql' | 'mysql' | 'script' | 'docker' | 'powershell' = $state('script')
// These pointers are used to easily access previewArgs of parent module, and previous module
@@ -26,7 +26,7 @@
const flowEditorContext = getContext<FlowEditorContext>('FlowEditorContext')
let opWs = $derived(flowEditorContext?.opWorkspace?.() ?? $workspaceStore)
interface Props {
kind?: 'script' | 'trigger' | 'approval' | 'failure'
kind?: 'script' | 'trigger' | 'approval' | 'failure' | 'preprocessor'
isTemplate?: boolean | undefined
displayLock?: boolean
filter?: string
@@ -122,38 +122,48 @@
/>
</div>
{/if}
{#if filter.length > 0 && filteredItems.length == 0}
<NoItemFound />
{/if}
<ul class="divide-y border rounded-md">
{#each filteredItems as { path, hash, summary, description, marked }}
<li class="flex flex-row w-full">
<button
class="p-4 gap-1 flex flex-row grow hover:bg-surface-hover bg-surface transition-all text-primary"
onclick={() => {
dispatch('pick', { path, hash: lockHash ? hash : undefined, summary })
}}
>
<div class="flex flex-col">
<div class="text-sm font-semibold flex flex-col">
<span class="mr-2 text-left">
{#if marked}
{@html marked}
{:else}
{!summary || summary.length == 0 ? path : summary}
{/if}</span
>
<span class="font-normal text-xs text-left italic overflow-hidden"
>{path ?? ''}</span
>
{#if filteredItems.length == 0}
{#if filter.length > 0}
<NoItemFound hasFilters />
{:else}
<div class="text-2xs text-primary font-light text-center py-2 px-3">No scripts found.</div>
{/if}
{#if kind == 'preprocessor'}
<div class="text-2xs text-hint text-center pb-2 px-3">
Only workspace scripts whose kind is set to Preprocessor are listed here.
</div>
{/if}
{:else}
<ul class="divide-y border rounded-md">
{#each filteredItems as { path, hash, summary, description, marked }}
<li class="flex flex-row w-full">
<button
class="p-4 gap-1 flex flex-row grow hover:bg-surface-hover bg-surface transition-all text-primary"
onclick={() => {
dispatch('pick', { path, hash: lockHash ? hash : undefined, summary })
}}
>
<div class="flex flex-col">
<div class="text-sm font-semibold flex flex-col">
<span class="mr-2 text-left">
{#if marked}
{@html marked}
{:else}
{!summary || summary.length == 0 ? path : summary}
{/if}</span
>
<span class="font-normal text-xs text-left italic overflow-hidden"
>{path ?? ''}</span
>
</div>
<div class="text-xs font-light italic text-left">{description ?? ''}</div>
</div>
<div class="text-xs font-light italic text-left">{description ?? ''}</div>
</div>
{#if lockHash}<Badge large baseClass="ml-4">{truncateHash(hash ?? '')}</Badge>{/if}
</button>
</li>
{/each}
</ul>
{#if lockHash}<Badge large baseClass="ml-4">{truncateHash(hash ?? '')}</Badge>{/if}
</button>
</li>
{/each}
</ul>
{/if}
{:else}
<div class="mt-6"></div>
@@ -165,6 +165,11 @@
{#if filteredItems.length == 0}
<div class="text-2xs text-primary font-light text-center py-2 px-3 items-center">
{kind == 'flow' ? 'No flows found.' : 'No scripts found.'}
{#if kind == 'preprocessor'}
<div class="text-hint">
Only workspace scripts whose kind is set to Preprocessor are listed here.
</div>
{/if}
</div>
{/if}
<ul class="gap-1 flex flex-col">
+24 -1
View File
@@ -1,5 +1,5 @@
import { describe, it, expect } from 'vitest'
import { bashRunsInCustomImage } from './script_helpers'
import { bashRunsInCustomImage, getResetCode } from './script_helpers'
// bashRunsInCustomImage decides whether the +Variable/+Resource pickers insert a
// curl/wget snippet (custom image, no wmill CLI) or the wmill CLI snippet. It must
@@ -41,3 +41,26 @@ describe('bashRunsInCustomImage', () => {
expect(bashRunsInCustomImage('# shellcheck shell=bash\necho hi')).toBe(false)
})
})
// Preprocessors must bypass getResetCode's per-language `main` templates: a preprocessor step
// resets through the same button as an action script, and a `main` body cannot run under the
// preprocessor entrypoint.
describe('getResetCode for preprocessors', () => {
// The concrete `language` values a preprocessor step can carry. PREPROCESSOR_SUPPORTED_LANGUAGES
// also holds the 'typescript'/'python' aliases, which no script is ever stored with.
const langs = ['deno', 'bun', 'python3', 'php'] as const
it('keeps the preprocessor entrypoint in every language that can have one', () => {
for (const lang of langs) {
const code = getResetCode(lang, 'preprocessor', undefined)
expect(code, lang).toContain('preprocessor')
expect(code, lang).not.toContain('function main')
expect(code, lang).not.toContain('def main')
}
})
it('still resets action scripts to a main stub', () => {
expect(getResetCode('python3', 'script', undefined)).toContain('def main')
expect(getResetCode('bun', 'script', undefined)).toContain('function main')
})
})
+5
View File
@@ -1703,6 +1703,11 @@ export function getResetCode(
| 'ci_test_python'
| undefined
) {
// Every *_INIT_CODE_CLEAR below is a `main` stub, which cannot run under the preprocessor
// entrypoint. Preprocessors must go through initialCode to keep theirs.
if (kind === 'preprocessor') {
return initialCode(language, kind, subkind)
}
if (language === 'deno') {
return DENO_INIT_CODE_CLEAR
} else if (language === 'python3') {