mirror of
https://github.com/windmill-labs/windmill.git
synced 2026-09-06 00:02:13 +00:00
fix: pre-fill the test panel JSON args editor and align its placeholder (#10871)
* fix: pre-fill the test panel JSON args editor and align its placeholder Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> * fix: reseed the JSON args editor when the preprocessor tab is selected Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> * fix: seed schema defaults and own-property args in the JSON payload Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> * fix: follow the schema in an untouched JSON payload, ignore same-tab clicks Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> * fix: latch JSON editor ownership from Monaco, drop the late remounts The pristine check read the bound `code` value, which trails the buffer by SimpleEditor's 200ms debounce — a reseed arriving in that window overwrote text already typed. Latch ownership from Monaco's own change event instead, via a new undebounced `input` event guarded so `setCode`'s `setValue` does not read as an edit. Both `.then(() => argsRender++)` bumps are gone: the arg views now remount at the tab transition only, and follow the schema in through `initialCode` when inference resolves, so a remount can no longer land on an in-progress payload. `FlowPreviewContent.selectInput` overwrote the editor on select but not on deselect, leaving the abandoned input's payload over reverted args. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 5
parent
90b40fffc3
commit
fb82f36e6d
@@ -1,6 +1,7 @@
|
||||
<script lang="ts">
|
||||
import SchemaForm from '$lib/components/SchemaForm.svelte'
|
||||
import JsonInputs from '$lib/components/JsonInputs.svelte'
|
||||
import { argsToJsonPayload } from '$lib/schema'
|
||||
import JobLoader from '$lib/components/JobLoader.svelte'
|
||||
import { Button } from '$lib/components/common'
|
||||
import { WindmillIcon } from '$lib/components/icons'
|
||||
@@ -161,7 +162,6 @@
|
||||
let args: Record<string, any> = $state({})
|
||||
let isValid: boolean = $state(true)
|
||||
let jsonView: boolean = $state(false)
|
||||
let jsonEditor: JsonInputs | undefined = $state(undefined)
|
||||
let schemaHeight = $state(0)
|
||||
|
||||
// Test
|
||||
@@ -1171,20 +1171,17 @@
|
||||
rightTooltip: 'Fill args from JSON'
|
||||
}}
|
||||
lightMode
|
||||
on:change={() => {
|
||||
jsonEditor?.setCode(JSON.stringify(args ?? {}, null, '\t'))
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
{#if jsonView}
|
||||
<div class="py-2" style="height: {Math.max(schemaHeight, 300)}px">
|
||||
<JsonInputs
|
||||
bind:this={jsonEditor}
|
||||
on:select={(e) => {
|
||||
if (e.detail) {
|
||||
args = e.detail
|
||||
}
|
||||
}}
|
||||
initialCode={argsToJsonPayload(schema, args)}
|
||||
updateOnBlur={false}
|
||||
placeholder={`Write args as JSON.<br/><br/>Example:<br/><br/>{<br/> "foo": "12"<br/>}`}
|
||||
/>
|
||||
|
||||
@@ -34,6 +34,8 @@
|
||||
import InputSelectedBadge from './schema/InputSelectedBadge.svelte'
|
||||
import Toggle from './Toggle.svelte'
|
||||
import JsonInputs from './JsonInputs.svelte'
|
||||
import { argsToJsonPayload } from '$lib/schema'
|
||||
import type { Schema } from '$lib/common'
|
||||
import FlowHistoryJobPicker from './FlowHistoryJobPicker.svelte'
|
||||
import type { DurationStatus, GraphModuleState } from './graph'
|
||||
import { getStepHistoryLoaderContext } from './stepHistoryLoader.svelte'
|
||||
@@ -264,8 +266,12 @@
|
||||
previewArgs.val = input
|
||||
inputSelected = type
|
||||
preventEscape = true
|
||||
jsonEditor?.setCode(JSON.stringify(previewArgs.val ?? {}, null, '\t'))
|
||||
}
|
||||
// Deselecting restores the args the same way selecting replaced them, so both branches
|
||||
// owe the editor an overwrite — it holds a payload for the input being left behind.
|
||||
jsonEditor?.setCode(
|
||||
argsToJsonPayload(flowStore.val.schema as Schema | undefined, previewArgs.val)
|
||||
)
|
||||
}
|
||||
|
||||
export function refresh() {
|
||||
@@ -510,8 +516,7 @@
|
||||
rightTooltip: 'Fill args from JSON'
|
||||
}}
|
||||
lightMode
|
||||
on:change={(e) => {
|
||||
jsonEditor?.setCode(JSON.stringify(previewArgs.val ?? {}, null, '\t'))
|
||||
on:change={() => {
|
||||
refresh()
|
||||
}}
|
||||
/>
|
||||
@@ -526,6 +531,10 @@
|
||||
previewArgs.val = e.detail
|
||||
}
|
||||
}}
|
||||
initialCode={argsToJsonPayload(
|
||||
flowStore.val.schema as Schema | undefined,
|
||||
previewArgs.val
|
||||
)}
|
||||
updateOnBlur={false}
|
||||
placeholder={`Write args as JSON.<br/><br/>Example:<br/><br/>{<br/> "foo": "12"<br/>}`}
|
||||
/>
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
<script lang="ts">
|
||||
import SimpleEditor from '$lib/components/SimpleEditor.svelte'
|
||||
import { createEventDispatcher } from 'svelte'
|
||||
import { createEventDispatcher, untrack } from 'svelte'
|
||||
|
||||
const dispatch = createEventDispatcher()
|
||||
|
||||
@@ -8,18 +8,48 @@
|
||||
updateOnBlur?: boolean
|
||||
placeholder?: string
|
||||
selected?: boolean
|
||||
/** Content the editor opens with, and keeps following while the buffer is untouched — so a
|
||||
* payload nobody has typed into tracks the schema instead of going stale. The first edit
|
||||
* hands the buffer to the user and later changes stop overwriting it. */
|
||||
initialCode?: string
|
||||
}
|
||||
|
||||
let {
|
||||
updateOnBlur = true,
|
||||
placeholder = 'Write a JSON payload. The input schema will be inferred.<br/><br/>Example:<br/><br/>{<br/> "foo": "12"<br/>}',
|
||||
selected = false
|
||||
selected = false,
|
||||
initialCode = ''
|
||||
}: Props = $props()
|
||||
|
||||
let pendingJson = $state('')
|
||||
let pendingJson = $state(untrack(() => initialCode))
|
||||
// The last content this component wrote, kept only to skip a reseed that would replace the
|
||||
// buffer with what it already holds — `setValue` resets the cursor and the undo stack.
|
||||
let seededCode = untrack(() => initialCode)
|
||||
// Latched from Monaco's own change event, never from `pendingJson`: that trails the buffer by
|
||||
// SimpleEditor's debounce, a window in which typed text still looks like the seeded payload
|
||||
// and a reseed lands on top of it.
|
||||
let userEdited = false
|
||||
let simpleEditor: SimpleEditor | undefined = $state(undefined)
|
||||
let focusTrap: HTMLElement | undefined = $state()
|
||||
|
||||
$effect(() => {
|
||||
const next = initialCode
|
||||
untrack(() => {
|
||||
if (next !== seededCode && !userEdited) {
|
||||
seed(next)
|
||||
}
|
||||
})
|
||||
})
|
||||
|
||||
// `SimpleEditor.setCode` cancels the change burst its own `setValue` opens, so reseeding
|
||||
// never dispatches `select` — the payload reaches `args` only when the user edits it.
|
||||
function seed(code: string) {
|
||||
seededCode = code
|
||||
userEdited = false
|
||||
pendingJson = code
|
||||
simpleEditor?.setCode(code)
|
||||
}
|
||||
|
||||
function updatePayloadFromJson(jsonInput: string) {
|
||||
if (jsonInput === undefined || jsonInput === null || jsonInput.trim() === '') {
|
||||
dispatch('select', undefined)
|
||||
@@ -33,8 +63,10 @@
|
||||
}
|
||||
}
|
||||
|
||||
/** Authoritative overwrite: replaces the buffer whether or not it has been typed into, and
|
||||
* re-establishes it as the content to keep following. */
|
||||
export function setCode(code: string) {
|
||||
simpleEditor?.setCode(code)
|
||||
seed(code)
|
||||
}
|
||||
|
||||
export function resetSelected(dispatchEvent?: boolean) {
|
||||
@@ -59,6 +91,7 @@
|
||||
<div class="h-full rounded-md border">
|
||||
<SimpleEditor
|
||||
bind:this={simpleEditor}
|
||||
on:input={() => (userEdited = true)}
|
||||
on:focus={() => {
|
||||
if (updateOnBlur) {
|
||||
dispatch('focus')
|
||||
|
||||
@@ -19,6 +19,7 @@
|
||||
import { page } from '$app/state'
|
||||
import { replaceState } from '$app/navigation'
|
||||
import JsonInputs from '$lib/components/JsonInputs.svelte'
|
||||
import { argsToJsonPayload } from '$lib/schema'
|
||||
import { triggerableByAI } from '$lib/actions/triggerableByAI.svelte'
|
||||
import InputSelectedBadge from './schema/InputSelectedBadge.svelte'
|
||||
import { untrack } from 'svelte'
|
||||
@@ -53,6 +54,8 @@
|
||||
args = scriptArgs
|
||||
psCommonParams = commonParams
|
||||
reloadArgs++
|
||||
// `reloadArgs` only keys the form; the JSON editor reads its payload once, at mount.
|
||||
syncJsonEditor()
|
||||
}
|
||||
|
||||
export async function run(overrideScheduledForStr?: string | undefined | null) {
|
||||
@@ -199,8 +202,10 @@
|
||||
return result
|
||||
}
|
||||
|
||||
export function setCode(code: string) {
|
||||
jsonEditor?.setCode(code)
|
||||
/** Rewrite the open JSON editor from the current args. Only for args replaced from outside
|
||||
* the editor: entering the JSON view already starts from whatever `args` holds. */
|
||||
export function syncJsonEditor() {
|
||||
jsonEditor?.setCode(argsToJsonPayload(runnable?.schema, args))
|
||||
}
|
||||
$effect(() => {
|
||||
overrideTag
|
||||
@@ -320,6 +325,7 @@
|
||||
args = enforceDisabledDefaults(e.detail)
|
||||
}
|
||||
}}
|
||||
initialCode={argsToJsonPayload(runnable.schema, args)}
|
||||
updateOnBlur={false}
|
||||
placeholder={`Write args as JSON.<br/><br/>Example:<br/><br/>{<br/> "foo": "12"<br/>}`}
|
||||
/>
|
||||
|
||||
@@ -122,6 +122,7 @@
|
||||
import { updateDelegateToGitRepoConfig, insertAdditionalInventories } from '$lib/ansibleUtils'
|
||||
import { copilotInfo } from '$lib/aiStore'
|
||||
import JsonInputs from '$lib/components/JsonInputs.svelte'
|
||||
import { argsToJsonPayload } from '$lib/schema'
|
||||
import Toggle from './Toggle.svelte'
|
||||
import { deepEqual } from 'fast-equals'
|
||||
import { usePreparedAssetSqlQueries } from '$lib/infer.svelte'
|
||||
@@ -310,6 +311,10 @@
|
||||
let moduleTestState: Record<string, { args: Record<string, any>; schema: Schema }> = $state({})
|
||||
let testPanelArgs: Record<string, any> = $state({})
|
||||
let testPanelSchema: Schema = $state(emptySchema())
|
||||
// Bumped whenever the args under test are replaced from outside the arg panel. Both arg
|
||||
// views key off it: without a bump the JSON editor keeps showing, and on the next
|
||||
// keystroke commits, the payload it was seeded with for the previous args.
|
||||
let argsRender = $state(0)
|
||||
// editorCode is what the editor shows; code always holds the main script content
|
||||
let editorCode: string = $state(code)
|
||||
// Sync editorCode when code changes externally (template reset, copilot,
|
||||
@@ -329,7 +334,13 @@
|
||||
})
|
||||
|
||||
function switchToModule(modulePath: string) {
|
||||
if (activeModuleTab !== null && modules && activeModuleTab !== modulePath) {
|
||||
// Re-clicking the tab you are already on is a no-op. Re-running the body would reset this
|
||||
// module's test state whenever its inference is still pending or has failed (the catch
|
||||
// leaves `moduleTestState` unwritten), losing both the filled-in args and the arg views.
|
||||
if (activeModuleTab === modulePath) {
|
||||
return
|
||||
}
|
||||
if (activeModuleTab !== null && modules) {
|
||||
// Switching from another module: save its content and test state
|
||||
modules[activeModuleTab] = { ...modules[activeModuleTab], content: editorCode }
|
||||
moduleTestState[activeModuleTab] = { args: testPanelArgs, schema: testPanelSchema }
|
||||
@@ -345,13 +356,20 @@
|
||||
} else {
|
||||
testPanelArgs = {}
|
||||
testPanelSchema = emptySchema()
|
||||
// Inference lands after the bump below, so the editor opens on `{}` and the arg
|
||||
// views follow the schema in once it arrives. Remounting them again on arrival
|
||||
// instead would discard anything typed while it was in flight.
|
||||
inferModuleSchema()
|
||||
}
|
||||
argsRender++
|
||||
}
|
||||
}
|
||||
|
||||
function switchToMain() {
|
||||
if (activeModuleTab !== null && modules) {
|
||||
if (activeModuleTab === null) {
|
||||
return
|
||||
}
|
||||
if (modules) {
|
||||
// Save current module content and test state
|
||||
modules[activeModuleTab] = { ...modules[activeModuleTab], content: editorCode }
|
||||
moduleTestState[activeModuleTab] = { args: testPanelArgs, schema: testPanelSchema }
|
||||
@@ -360,6 +378,7 @@
|
||||
editorCode = code
|
||||
lastSyncedCode = code
|
||||
editor?.setCode(editorCode)
|
||||
argsRender++
|
||||
}
|
||||
|
||||
// Whether the open file is tested as a runnable of its own. A `__mod` helper
|
||||
@@ -854,6 +873,7 @@
|
||||
|
||||
export function setArgs(nargs: Record<string, any>) {
|
||||
args = nargs
|
||||
argsRender++
|
||||
}
|
||||
|
||||
export async function runTest(opts?: { cascade?: boolean; skipDdlGuard?: boolean }) {
|
||||
@@ -1654,15 +1674,30 @@
|
||||
$effect(() => {
|
||||
!hasPreprocessor && (selectedTab = 'main')
|
||||
})
|
||||
// `main` and `preprocessor` describe the same args under different schemas; every other tab
|
||||
// (`diagram`) runs against main's schema, so it collapses into `main` here.
|
||||
let lastSchemaTab = untrack(() => (selectedTab === 'preprocessor' ? 'preprocessor' : 'main'))
|
||||
$effect(() => {
|
||||
// Only depend on selectedTab (preprocessor ↔ main toggle).
|
||||
// Code changes are handled by the editor on:change handler and
|
||||
// explicit inferSchema calls (initContent, onMount), so we read
|
||||
// `code` inside untrack to avoid a redundant double-inference race.
|
||||
selectedTab && untrack(() => code && inferSchema(code))
|
||||
selectedTab &&
|
||||
untrack(() => {
|
||||
const schemaTab = selectedTab === 'preprocessor' ? 'preprocessor' : 'main'
|
||||
const switched = schemaTab !== lastSchemaTab
|
||||
lastSchemaTab = schemaTab
|
||||
if (!code) return
|
||||
// Bump on the switch itself, not on the inference it starts: the other tab's schema
|
||||
// only lands once that resolves, and remounting the arg views then would discard
|
||||
// anything typed while it was in flight. An untouched editor follows the schema in.
|
||||
if (switched) {
|
||||
argsRender++
|
||||
}
|
||||
inferSchema(code)
|
||||
})
|
||||
})
|
||||
|
||||
let argsRender = $state(0)
|
||||
export async function updateArgs(newArgs: Record<string, any>) {
|
||||
if (Object.keys(newArgs).length > 0) {
|
||||
args = { ...newArgs }
|
||||
@@ -2299,19 +2334,24 @@
|
||||
style="height: {!schemaHeight || schemaHeight < 600 ? 600 : schemaHeight}px"
|
||||
data-schema-picker
|
||||
>
|
||||
<JsonInputs
|
||||
on:select={(e) => {
|
||||
if (e.detail) {
|
||||
if (onModuleArgs) {
|
||||
testPanelArgs = e.detail
|
||||
} else {
|
||||
args = e.detail
|
||||
{#key argsRender}
|
||||
<JsonInputs
|
||||
on:select={(e) => {
|
||||
if (e.detail) {
|
||||
if (onModuleArgs) {
|
||||
testPanelArgs = e.detail
|
||||
} else {
|
||||
args = e.detail
|
||||
}
|
||||
}
|
||||
}
|
||||
}}
|
||||
updateOnBlur={false}
|
||||
placeholder={`Write args as JSON.<br/><br/>Example:<br/><br/>{<br/> "foo": "12"<br/>}`}
|
||||
/>
|
||||
}}
|
||||
initialCode={onModuleArgs
|
||||
? argsToJsonPayload(testPanelSchema, testPanelArgs)
|
||||
: argsToJsonPayload(schema, args)}
|
||||
updateOnBlur={false}
|
||||
placeholder={`Write args as JSON.<br/><br/>Example:<br/><br/>{<br/> "foo": "12"<br/>}`}
|
||||
/>
|
||||
{/key}
|
||||
</div>
|
||||
{:else}
|
||||
<div class="px-4">
|
||||
|
||||
@@ -64,6 +64,9 @@
|
||||
const CHANGE_TIMEOUT = 200
|
||||
|
||||
let changeTimeoutId: number | undefined = undefined
|
||||
// Monaco fires onDidChangeModelContent synchronously from within `setValue`, so without
|
||||
// this an authoritative overwrite reads as a user edit on the `input` event.
|
||||
let applyingCode = false
|
||||
|
||||
let divEl: HTMLDivElement | null = null
|
||||
let editor = $state<meditor.IStandaloneCodeEditor | null>(null)
|
||||
@@ -74,6 +77,10 @@
|
||||
let width = $state(0)
|
||||
let initialized = $state(false)
|
||||
let placeholderVisible = $state(false)
|
||||
// Monaco's content origin. The placeholder is a plain overlay on the editor container, so
|
||||
// without these it sits over the line-number gutter and off the line-1 baseline.
|
||||
let contentLeft = $state(0)
|
||||
let contentLineHeight = $state(0)
|
||||
let mounted = $state(false)
|
||||
|
||||
let valueAfterDispose: string | undefined = undefined
|
||||
@@ -179,7 +186,12 @@
|
||||
if (ncode != code) {
|
||||
code = ncode
|
||||
}
|
||||
editor?.setValue(ncode)
|
||||
applyingCode = true
|
||||
try {
|
||||
editor?.setValue(ncode)
|
||||
} finally {
|
||||
applyingCode = false
|
||||
}
|
||||
// setValue emits a change event of its own; drop the burst it opens so an edit
|
||||
// made right after an authoritative overwrite still counts as a leading change.
|
||||
cancelPendingChanges()
|
||||
@@ -454,6 +466,12 @@
|
||||
changeTimeoutId = undefined
|
||||
updateCode()
|
||||
}, CHANGE_TIMEOUT)
|
||||
// `change` trails the buffer by CHANGE_TIMEOUT, too late for a consumer that has to
|
||||
// know the moment the buffer stopped being the one it wrote. `input` says only that,
|
||||
// carrying no value: read `getCode()` for what is on screen.
|
||||
if (!applyingCode) {
|
||||
dispatch('input')
|
||||
}
|
||||
if (leading) {
|
||||
updateCode()
|
||||
}
|
||||
@@ -533,6 +551,13 @@
|
||||
}
|
||||
|
||||
if (placeholder) {
|
||||
const syncPlaceholderOrigin = () => {
|
||||
if (!editor) return
|
||||
contentLeft = editor.getLayoutInfo().contentLeft
|
||||
contentLineHeight = editor.getOption(meditor.EditorOption.lineHeight)
|
||||
}
|
||||
syncPlaceholderOrigin()
|
||||
editor.onDidLayoutChange(syncPlaceholderOrigin)
|
||||
editor.onDidChangeModelContent(() => {
|
||||
if (!editor) return
|
||||
const value = editor.getValue()
|
||||
@@ -755,9 +780,10 @@
|
||||
{#if placeholder}
|
||||
<div
|
||||
id="placeholder"
|
||||
class="absolute text-gray-500 text-sm pointer-events-none font-mono z-10 {placeholderVisible
|
||||
class="absolute text-tertiary pointer-events-none font-mono z-10 {placeholderVisible
|
||||
? ''
|
||||
: 'hidden'}"
|
||||
style="left: {contentLeft}px; top: {yPadding}px; font-size: {fontSize}px; line-height: {contentLineHeight}px;"
|
||||
>
|
||||
{@html placeholder}
|
||||
</div>
|
||||
|
||||
@@ -0,0 +1,62 @@
|
||||
import { describe, it, expect } from 'vitest'
|
||||
import { argsToJsonPayload } from './schema'
|
||||
import type { Schema } from './common'
|
||||
|
||||
const schemaOf = (...names: string[]): Schema =>
|
||||
({
|
||||
$schema: undefined,
|
||||
type: 'object',
|
||||
properties: Object.fromEntries(names.map((n) => [n, { type: 'string' }])),
|
||||
required: []
|
||||
}) as Schema
|
||||
|
||||
describe('argsToJsonPayload', () => {
|
||||
it('spells out every schema property in schema order, unset ones as null', () => {
|
||||
// `0`, `false` and `''` are values, not gaps: only a missing arg becomes `null`.
|
||||
expect(argsToJsonPayload(schemaOf('a', 'b', 'c'), { c: 0, a: false })).toBe(
|
||||
JSON.stringify({ a: false, b: null, c: 0 }, null, '\t')
|
||||
)
|
||||
})
|
||||
|
||||
it('keeps args the schema does not declare, after the declared ones', () => {
|
||||
expect(argsToJsonPayload(schemaOf('a'), { z: 9, a: 1 })).toBe(
|
||||
JSON.stringify({ a: 1, z: 9 }, null, '\t')
|
||||
)
|
||||
})
|
||||
|
||||
it('falls back to the schema default for an absent arg, but not over an explicit null', () => {
|
||||
// `args` only carries defaults once a `SchemaForm` has mounted for that schema, and the
|
||||
// JSON view alone never mounts one — seeding `null` there would commit `null` over the
|
||||
// argument's default on the first keystroke.
|
||||
const schema = schemaOf('a', 'b')
|
||||
schema.properties.a.default = 'hi'
|
||||
schema.properties.b.default = 42
|
||||
expect(argsToJsonPayload(schema, {})).toBe(JSON.stringify({ a: 'hi', b: 42 }, null, '\t'))
|
||||
expect(argsToJsonPayload(schema, { a: null })).toBe(
|
||||
JSON.stringify({ a: null, b: 42 }, null, '\t')
|
||||
)
|
||||
})
|
||||
|
||||
it('keeps declared args named after Object.prototype members', () => {
|
||||
// A plain `nargs[key]` read returns the inherited function for an unset `constructor`,
|
||||
// and `JSON.stringify` drops function-valued properties — the argument would vanish.
|
||||
expect(argsToJsonPayload(schemaOf('constructor', 'toString', 'ok'), {})).toBe(
|
||||
JSON.stringify({ constructor: null, toString: null, ok: null }, null, '\t')
|
||||
)
|
||||
})
|
||||
|
||||
it('keeps undeclared args named after Object.prototype members', () => {
|
||||
// On a plain `{}` accumulator, `'constructor' in payload` is true before anything is
|
||||
// assigned to it.
|
||||
expect(argsToJsonPayload(undefined, { constructor: 'x', toString: 'y', ok: 1 })).toBe(
|
||||
JSON.stringify({ constructor: 'x', toString: 'y', ok: 1 }, null, '\t')
|
||||
)
|
||||
})
|
||||
|
||||
it('handles a missing schema or missing args', () => {
|
||||
expect(argsToJsonPayload(undefined, undefined)).toBe('{}')
|
||||
expect(argsToJsonPayload(schemaOf('a'), undefined)).toBe(
|
||||
JSON.stringify({ a: null }, null, '\t')
|
||||
)
|
||||
})
|
||||
})
|
||||
@@ -52,3 +52,33 @@ export function schemaToObject(schema: Schema, args: Record<string, any>): Objec
|
||||
})
|
||||
return object
|
||||
}
|
||||
|
||||
/** Args as the JSON payload the JSON editor starts from. Every schema property is spelled out,
|
||||
* so an argument with no value yet still shows its name; args the schema does not declare are
|
||||
* kept, since what the editor holds replaces the args wholesale on the next keystroke. */
|
||||
export function argsToJsonPayload(
|
||||
schema: Schema | undefined,
|
||||
args: Record<string, any> | undefined
|
||||
): string {
|
||||
const nargs = args ?? {}
|
||||
// Null prototype: an arg named after an `Object.prototype` member (`constructor`,
|
||||
// `toString`) has to be an own key here, or the `in` check below reads it as already
|
||||
// present and its value never reaches the payload.
|
||||
const payload: Record<string, any> = Object.create(null)
|
||||
const props = schema?.properties ?? {}
|
||||
// Schema order first, so the payload reads like the form it replaces.
|
||||
for (const key of Object.keys(props)) {
|
||||
// Own-property read: an arg named after an `Object.prototype` member (`constructor`,
|
||||
// `toString`) would otherwise come back as the inherited function, which `JSON.stringify`
|
||||
// drops. An arg that is merely absent falls back to the schema default — `args` only
|
||||
// carries defaults once a `SchemaForm` has mounted, which the JSON view alone never does.
|
||||
payload[key] =
|
||||
(Object.prototype.hasOwnProperty.call(nargs, key) ? nargs[key] : props[key]?.default) ?? null
|
||||
}
|
||||
for (const [key, value] of Object.entries(nargs)) {
|
||||
if (!(key in payload)) {
|
||||
payload[key] = value
|
||||
}
|
||||
}
|
||||
return JSON.stringify(payload, null, '\t')
|
||||
}
|
||||
|
||||
@@ -732,9 +732,6 @@
|
||||
rightTooltip: 'Fill args from JSON'
|
||||
}}
|
||||
lightMode
|
||||
on:change={(e) => {
|
||||
runForm?.setCode(JSON.stringify(args ?? {}, null, '\t'))
|
||||
}}
|
||||
/>
|
||||
{/if}
|
||||
</div>
|
||||
@@ -817,7 +814,7 @@
|
||||
const nargs = JSON.parse(JSON.stringify(e.detail))
|
||||
args = nargs
|
||||
if (jsonView) {
|
||||
runForm?.setCode(JSON.stringify(args ?? {}, null, '\t'))
|
||||
runForm?.syncJsonEditor()
|
||||
}
|
||||
}}
|
||||
/>
|
||||
|
||||
@@ -388,7 +388,7 @@
|
||||
if (!held || !current || block?.label !== 'retry' || block['dbt_retry_job']) return
|
||||
args = { ...current, command: { ...block, dbt_retry_job: held } }
|
||||
if (jsonView) {
|
||||
runForm?.setCode(JSON.stringify(args, null, '\t'))
|
||||
runForm?.syncJsonEditor()
|
||||
}
|
||||
})
|
||||
.catch(() => {})
|
||||
@@ -430,7 +430,7 @@
|
||||
}
|
||||
}
|
||||
if (jsonView) {
|
||||
runForm?.setCode(JSON.stringify(args, null, '\t'))
|
||||
runForm?.syncJsonEditor()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -934,9 +934,6 @@
|
||||
rightTooltip: 'Fill args from JSON'
|
||||
}}
|
||||
lightMode
|
||||
on:change={(e) => {
|
||||
runForm?.setCode(JSON.stringify(args ?? {}, null, '\t'))
|
||||
}}
|
||||
/>
|
||||
{/if}
|
||||
</div>
|
||||
@@ -1051,7 +1048,7 @@
|
||||
const nargs = JSON.parse(JSON.stringify(e.detail))
|
||||
args = nargs
|
||||
if (jsonView) {
|
||||
runForm?.setCode(JSON.stringify(args ?? {}, null, '\t'))
|
||||
runForm?.syncJsonEditor()
|
||||
}
|
||||
}}
|
||||
/>
|
||||
|
||||
Reference in New Issue
Block a user