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:
Guilhem
2026-10-01 13:28:41 +02:00
committed by GitHub
co-authored by Claude Opus 5.5
parent 5bd2c2c254
commit ddde41bc1d
4 changed files with 95 additions and 30 deletions
+12 -2
View File
@@ -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)
+47 -26
View File
@@ -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>
+30 -1
View File
@@ -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}