mirror of
https://github.com/windmill-labs/windmill.git
synced 2026-09-09 00:04:10 +00:00
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:
co-authored by
Claude Opus 5
parent
2439a610be
commit
a9112b72a5
@@ -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">
|
||||
|
||||
@@ -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')
|
||||
})
|
||||
})
|
||||
|
||||
@@ -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') {
|
||||
|
||||
Reference in New Issue
Block a user