mirror of
https://github.com/windmill-labs/windmill.git
synced 2026-10-03 08:02:19 +00:00
fix: block runs while a JSON input does not parse (#11463)
* fix: block runs while a JSON input does not parse Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> * fix: clear json editor error on unmount and flush editors before run Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> * fix: restore validity when a nullable arg input is cleared Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> * fix: let field editors parse before the run check and reset the message on view switch Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> * fix: clear the run refusal message when form validity changes Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 5.5
parent
5bd2c2c254
commit
ddde41bc1d
@@ -253,6 +253,9 @@
|
||||
|
||||
let ignoreValueUndefined = $state(false)
|
||||
let error: string = $state('')
|
||||
// Parse error of the JSON editor below, which already displays it. Text that does not parse
|
||||
// never reaches `value`, so without this the field would stay valid on its last parsed value.
|
||||
let jsonError: string = $state('')
|
||||
let isListJson = $state(false)
|
||||
let hasIsListJsonChanged = $state(false)
|
||||
|
||||
@@ -417,9 +420,12 @@
|
||||
'^(([0-9a-fA-F]{1,4}:){7,7}[0-9a-fA-F]{1,4}|([0-9a-fA-F]{1,4}:){1,7}:|([0-9a-fA-F]{1,4}:){1,6}:[0-9a-fA-F]{1,4}|([0-9a-fA-F]{1,4}:){1,5}(:[0-9a-fA-F]{1,4}){1,2}|([0-9a-fA-F]{1,4}:){1,4}(:[0-9a-fA-F]{1,4}){1,3}|([0-9a-fA-F]{1,4}:){1,3}(:[0-9a-fA-F]{1,4}){1,4}|([0-9a-fA-F]{1,4}:){1,2}(:[0-9a-fA-F]{1,4}){1,5}|[0-9a-fA-F]{1,4}:((:[0-9a-fA-F]{1,4}){1,6})|:((:[0-9a-fA-F]{1,4}){1,7}|:)|fe80:(:[0-9a-fA-F]{0,4}){0,4}%[0-9a-zA-Z]{1,}|::(ffff(:0{1,4}){0,1}:){0,1}((25[0-5]|(2[0-4]|1{0,1}[0-9]){0,1}[0-9])\\.){3,3}(25[0-5]|(2[0-4]|1{0,1}[0-9]){0,1}[0-9])|([0-9a-fA-F]{1,4}:){1,4}:((25[0-5]|(2[0-4]|1{0,1}[0-9]){0,1}[0-9])\\.){3,3}(25[0-5]|(2[0-4]|1{0,1}[0-9]){0,1}[0-9]))$'
|
||||
|
||||
function validateInput(pattern: string | undefined, v: any, required: boolean): void {
|
||||
if (nullable && emptyString(v)) {
|
||||
if (jsonError) {
|
||||
error = ''
|
||||
valid && (valid = true)
|
||||
valid && (valid = false)
|
||||
} else if (nullable && emptyString(v)) {
|
||||
error = ''
|
||||
!valid && (valid = true)
|
||||
} else if (
|
||||
typeof v === 'string' &&
|
||||
(v.startsWith('$var:') || v.startsWith('$res:') || v.startsWith('$jsonvar:'))
|
||||
@@ -563,6 +569,7 @@
|
||||
|
||||
$effect(() => {
|
||||
extra?.['nonEmpty']
|
||||
jsonError
|
||||
let args = [pattern, value, required] as const
|
||||
untrack(() => validateInput(...args))
|
||||
})
|
||||
@@ -1262,6 +1269,7 @@
|
||||
dispatch('blur')
|
||||
}}
|
||||
code={rawValue}
|
||||
bind:error={jsonError}
|
||||
on:changeValue={(e) => {
|
||||
setNewValueFromCode(e.detail)
|
||||
}}
|
||||
@@ -1284,6 +1292,7 @@
|
||||
dispatch('blur')
|
||||
}}
|
||||
code={rawValue}
|
||||
bind:error={jsonError}
|
||||
on:change={(e) => {
|
||||
value = e.detail
|
||||
}}
|
||||
@@ -1373,6 +1382,7 @@
|
||||
dispatch('blur')
|
||||
}}
|
||||
code={rawValue}
|
||||
bind:error={jsonError}
|
||||
on:changeValue={(e) => {
|
||||
setNewValueFromCode(e.detail)
|
||||
}}
|
||||
|
||||
@@ -40,7 +40,12 @@
|
||||
// Identity for this editor's entry in the unparseable registry, so a caller about to
|
||||
// persist what is on screen can refuse rather than save the last value that parsed.
|
||||
const unparseableKey = {}
|
||||
onDestroy(() => setEditorUnparseable(unparseableKey, false))
|
||||
// Also clears a bound `error`: a parent gating validity on it would otherwise stay invalid
|
||||
// after the editor that reported the error is gone.
|
||||
onDestroy(() => {
|
||||
error = ''
|
||||
setEditorUnparseable(unparseableKey, false)
|
||||
})
|
||||
|
||||
const dispatch = createEventDispatcher()
|
||||
const dispatchIfMounted = createDispatcherIfMounted(dispatch)
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
<script lang="ts">
|
||||
import SimpleEditor from '$lib/components/SimpleEditor.svelte'
|
||||
import { createEventDispatcher, untrack } from 'svelte'
|
||||
import { createEventDispatcher, onDestroy, untrack } from 'svelte'
|
||||
import { setEditorUnparseable } from './pendingEditorFlush'
|
||||
|
||||
const dispatch = createEventDispatcher()
|
||||
|
||||
@@ -31,6 +32,17 @@
|
||||
let userEdited = false
|
||||
let simpleEditor: SimpleEditor | undefined = $state(undefined)
|
||||
let focusTrap: HTMLElement | undefined = $state()
|
||||
let parseError = $state('')
|
||||
|
||||
// Text that does not parse dispatches `undefined`, which callers read as "keep the last args":
|
||||
// registering it lets whoever runs or persists those args refuse instead of using stale ones.
|
||||
const unparseableKey = {}
|
||||
onDestroy(() => setEditorUnparseable(unparseableKey, false))
|
||||
|
||||
function setParseError(message: string) {
|
||||
parseError = message
|
||||
setEditorUnparseable(unparseableKey, message !== '')
|
||||
}
|
||||
|
||||
$effect(() => {
|
||||
const next = initialCode
|
||||
@@ -48,17 +60,21 @@
|
||||
userEdited = false
|
||||
pendingJson = code
|
||||
simpleEditor?.setCode(code)
|
||||
setParseError('')
|
||||
}
|
||||
|
||||
function updatePayloadFromJson(jsonInput: string) {
|
||||
if (jsonInput === undefined || jsonInput === null || jsonInput.trim() === '') {
|
||||
setParseError('')
|
||||
dispatch('select', undefined)
|
||||
return
|
||||
}
|
||||
try {
|
||||
const parsed = JSON.parse(jsonInput)
|
||||
setParseError('')
|
||||
dispatch('select', parsed)
|
||||
} catch (error) {
|
||||
setParseError(error instanceof Error ? error.message : String(error))
|
||||
dispatch('select', undefined)
|
||||
}
|
||||
}
|
||||
@@ -88,31 +104,36 @@
|
||||
<!-- Add a hidden button that can receive focus -->
|
||||
<button bind:this={focusTrap} class="sr-only" tabindex="-1" aria-hidden="true">Focus trap</button>
|
||||
|
||||
<div class="h-full rounded-md border">
|
||||
<SimpleEditor
|
||||
bind:this={simpleEditor}
|
||||
on:input={() => (userEdited = true)}
|
||||
on:focus={() => {
|
||||
if (updateOnBlur) {
|
||||
dispatch('focus')
|
||||
updatePayloadFromJson(pendingJson)
|
||||
}
|
||||
}}
|
||||
on:blur={async () => {
|
||||
if (updateOnBlur) {
|
||||
dispatch('blur')
|
||||
}
|
||||
}}
|
||||
on:change={(e) => {
|
||||
if (e.detail?.code !== undefined) {
|
||||
updatePayloadFromJson(e.detail.code)
|
||||
}
|
||||
}}
|
||||
bind:code={pendingJson}
|
||||
lang="json"
|
||||
class="h-full json-inputs-editor"
|
||||
{placeholder}
|
||||
/>
|
||||
<div class="h-full flex flex-col">
|
||||
<div class="flex-1 min-h-0 rounded-md border">
|
||||
<SimpleEditor
|
||||
bind:this={simpleEditor}
|
||||
on:input={() => (userEdited = true)}
|
||||
on:focus={() => {
|
||||
if (updateOnBlur) {
|
||||
dispatch('focus')
|
||||
updatePayloadFromJson(pendingJson)
|
||||
}
|
||||
}}
|
||||
on:blur={async () => {
|
||||
if (updateOnBlur) {
|
||||
dispatch('blur')
|
||||
}
|
||||
}}
|
||||
on:change={(e) => {
|
||||
if (e.detail?.code !== undefined) {
|
||||
updatePayloadFromJson(e.detail.code)
|
||||
}
|
||||
}}
|
||||
bind:code={pendingJson}
|
||||
lang="json"
|
||||
class="h-full json-inputs-editor"
|
||||
{placeholder}
|
||||
/>
|
||||
</div>
|
||||
{#if parseError}
|
||||
<span class="text-red-600 dark:text-red-400 text-xs mt-1">Invalid JSON: {parseError}</span>
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
<style>
|
||||
|
||||
@@ -22,10 +22,11 @@
|
||||
import { argsToJsonPayload } from '$lib/schema'
|
||||
import { triggerableByAI } from '$lib/actions/triggerableByAI.svelte'
|
||||
import InputSelectedBadge from './schema/InputSelectedBadge.svelte'
|
||||
import { untrack } from 'svelte'
|
||||
import { tick, untrack } from 'svelte'
|
||||
import { processSecretArgs } from './secretArgUtils'
|
||||
import { enforceDisabledDefaults, resetKeysToast } from './job_args'
|
||||
import PowerShellCommonParams from './PowerShellCommonParams.svelte'
|
||||
import { anyEditorUnparseable, flushAllPendingEditorChanges } from './pendingEditorFlush'
|
||||
|
||||
let reloadArgs = $state(0)
|
||||
let jsonEditor: JsonInputs | undefined = $state(undefined)
|
||||
@@ -33,6 +34,13 @@
|
||||
let showInputSelectedBadge = $state(false)
|
||||
let savedPreviousArgs: Record<string, any> | undefined = $state(undefined)
|
||||
let psCommonParams: Record<string, any> = $state({})
|
||||
// Reset on a view switch, where the editor that refused the run is gone, and on a form
|
||||
// validity change, where the field's own error and the disabled button take over.
|
||||
let blockedByUnparseable = $derived.by(() => {
|
||||
void jsonView
|
||||
void isValid
|
||||
return false
|
||||
})
|
||||
|
||||
function extractPsCommonParams(allArgs: Record<string, any>): {
|
||||
scriptArgs: Record<string, any>
|
||||
@@ -60,6 +68,15 @@
|
||||
}
|
||||
|
||||
export async function run(overrideScheduledForStr?: string | undefined | null) {
|
||||
// An editor whose text does not parse never wrote it to `args`, so running now would send
|
||||
// the last value that did parse. Flush first: a keystroke still inside the editor debounce
|
||||
// has not been parsed yet, and per-field editors parse it in an effect, hence the tick.
|
||||
flushAllPendingEditorChanges()
|
||||
await tick()
|
||||
blockedByUnparseable = anyEditorUnparseable()
|
||||
if (blockedByUnparseable) {
|
||||
return
|
||||
}
|
||||
let processedArgs: Record<string, any>
|
||||
const { args: withDefaults, resetKeys } = enforceDisabledDefaults(args ?? {}, runnable?.schema)
|
||||
if (resetKeys.length > 0) {
|
||||
@@ -303,6 +320,7 @@
|
||||
<JsonInputs
|
||||
bind:this={jsonEditor}
|
||||
on:select={(e) => {
|
||||
blockedByUnparseable = false
|
||||
if (e.detail) {
|
||||
args = enforceDisabledDefaults(e.detail, runnable?.schema).args
|
||||
}
|
||||
@@ -374,6 +392,7 @@
|
||||
</Popover>
|
||||
</div>
|
||||
</div>
|
||||
{@render unparseableError()}
|
||||
{#if overrideTag}
|
||||
<div class="flex-row-reverse flex w-full text-primary text-sm">
|
||||
tag override: {overrideTag}
|
||||
@@ -405,6 +424,7 @@
|
||||
</Button>
|
||||
<div>{@render actions()}</div>
|
||||
</div>
|
||||
{@render unparseableError()}
|
||||
{:else}
|
||||
<Button
|
||||
btnClasses="!px-6 !py-1 w-full"
|
||||
@@ -415,5 +435,14 @@
|
||||
>
|
||||
{buttonText}
|
||||
</Button>
|
||||
{@render unparseableError()}
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
{#snippet unparseableError()}
|
||||
{#if blockedByUnparseable}
|
||||
<div class="flex-row-reverse flex w-full text-red-600 dark:text-red-400 text-xs mt-1">
|
||||
Some input is not valid JSON. Fix it before running.
|
||||
</div>
|
||||
{/if}
|
||||
{/snippet}
|
||||
|
||||
Reference in New Issue
Block a user