mirror of
https://github.com/windmill-labs/windmill.git
synced 2026-08-18 16:02:10 +00:00
pasting YAML over existing code fails to apply (#10453)
* fix: sync SimpleEditor code on the leading edge of a change burst * style: condense change-debounce comment to the 4-line rule * fix: scope leading code sync to the YAML drawers that gate on it * fix: apply leading code sync to the remaining code-gated editors * fix: reject OpenFlow YAML without value.modules instead of crashing the editor
This commit is contained in:
@@ -59,6 +59,11 @@
|
||||
// import { createConfiguredEditor } from 'vscode/monaco'
|
||||
// import type { IStandaloneCodeEditor } from 'vscode/vscode/vs/editor/standalone/browser/standaloneCodeEditor'
|
||||
|
||||
/** Trailing debounce window (ms) on Monaco's onDidChangeModelContent. */
|
||||
const CHANGE_TIMEOUT = 200
|
||||
|
||||
let changeTimeoutId: number | undefined = undefined
|
||||
|
||||
let divEl: HTMLDivElement | null = null
|
||||
let editor = $state<meditor.IStandaloneCodeEditor | null>(null)
|
||||
let model: meditor.ITextModel
|
||||
@@ -98,7 +103,8 @@
|
||||
readOnly = false,
|
||||
minHeight = 1000,
|
||||
renderLineHighlight = 'none',
|
||||
suggestion
|
||||
suggestion,
|
||||
leadingChangeSync = false
|
||||
}: {
|
||||
lang: string
|
||||
code?: string
|
||||
@@ -130,6 +136,11 @@
|
||||
minHeight?: number
|
||||
renderLineHighlight?: 'all' | 'line' | 'gutter' | 'none'
|
||||
suggestion?: string
|
||||
/** Materialize `code` on the first change of a burst instead of only after
|
||||
* the trailing debounce. Set it when a control's enabled state derives from
|
||||
* `code`; leave it off where each extra sync costs work downstream (an app
|
||||
* code input feeding an autoRefresh runnable re-runs a job per sync). */
|
||||
leadingChangeSync?: boolean
|
||||
} = $props()
|
||||
|
||||
let yPadding = MONACO_Y_PADDING
|
||||
@@ -156,11 +167,21 @@
|
||||
code = ncode
|
||||
}
|
||||
editor?.setValue(ncode)
|
||||
// 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()
|
||||
if (formatCode) {
|
||||
format()
|
||||
}
|
||||
}
|
||||
|
||||
function cancelPendingChanges(): void {
|
||||
if (changeTimeoutId !== undefined) {
|
||||
clearTimeout(changeTimeoutId)
|
||||
changeTimeoutId = undefined
|
||||
}
|
||||
}
|
||||
|
||||
export function formatCode(): void {
|
||||
format()
|
||||
}
|
||||
@@ -408,12 +429,21 @@
|
||||
pasteListenerCleanup = () => pasteTarget?.removeEventListener('keydown', onPasteKeydown, true)
|
||||
}
|
||||
|
||||
let timeoutModel: number | undefined = undefined
|
||||
editor.onDidChangeModelContent((event) => {
|
||||
timeoutModel && clearTimeout(timeoutModel)
|
||||
timeoutModel = setTimeout(() => {
|
||||
editor.onDidChangeModelContent(() => {
|
||||
// A paste is a single change, so under a trailing-only sync `code` stays
|
||||
// stale for CHANGE_TIMEOUT after it: a consumer gating a control on `code`
|
||||
// (FlowYamlEditor disables "Apply changes" until it differs from a snapshot)
|
||||
// then swallows a click made in that window. Schedule before firing so a
|
||||
// re-entrant change from a consumer does not count as leading too.
|
||||
const leading = leadingChangeSync && changeTimeoutId === undefined
|
||||
cancelPendingChanges()
|
||||
changeTimeoutId = setTimeout(() => {
|
||||
changeTimeoutId = undefined
|
||||
updateCode()
|
||||
}, 200)
|
||||
}, CHANGE_TIMEOUT)
|
||||
if (leading) {
|
||||
updateCode()
|
||||
}
|
||||
})
|
||||
editor.onDidChangeCursorPosition((event) => {
|
||||
if (key) editorPositionMap[key] = event.position
|
||||
@@ -621,6 +651,7 @@
|
||||
onDestroy(() => {
|
||||
try {
|
||||
valueAfterDispose = getCode()
|
||||
cancelPendingChanges()
|
||||
pasteListenerCleanup?.()
|
||||
vimDisposable?.dispose()
|
||||
model && model.dispose()
|
||||
|
||||
@@ -31,6 +31,23 @@
|
||||
editor?.setCode(code)
|
||||
}
|
||||
|
||||
/** `value` is assigned unconditionally below and `FlowEditor` dereferences
|
||||
* `flowStore.val.value.modules`, so an OpenFlow document missing it takes the
|
||||
* whole editor down mid-render — after the toast has already claimed success.
|
||||
* Reject it up front instead. */
|
||||
function validateShape(parsed: unknown) {
|
||||
if (typeof parsed !== 'object' || parsed === null || Array.isArray(parsed)) {
|
||||
throw new Error('the document must be a mapping (key: value)')
|
||||
}
|
||||
const value = (parsed as Record<string, unknown>).value
|
||||
if (typeof value !== 'object' || value === null || Array.isArray(value)) {
|
||||
throw new Error("missing 'value' - paste a whole OpenFlow document, not just its value")
|
||||
}
|
||||
if (!Array.isArray((value as Record<string, unknown>).modules)) {
|
||||
throw new Error("'value.modules' must be a list")
|
||||
}
|
||||
}
|
||||
|
||||
function validateGroups(groups: { start_id: string; end_id: string }[] | undefined) {
|
||||
if (!groups) return
|
||||
const seen = new Set<string>()
|
||||
@@ -46,6 +63,7 @@
|
||||
function apply() {
|
||||
try {
|
||||
const parsed = YAML.parse(code)
|
||||
validateShape(parsed)
|
||||
validateGroups(parsed.value?.groups)
|
||||
if (parsed.summary && typeof parsed.summary === 'string') {
|
||||
flowStore.val.summary = parsed.summary
|
||||
@@ -104,6 +122,7 @@
|
||||
minHeight={editorHeight}
|
||||
bind:code
|
||||
lang="yaml"
|
||||
leadingChangeSync
|
||||
/>
|
||||
</div>
|
||||
{/await}
|
||||
|
||||
@@ -25,14 +25,7 @@
|
||||
onApply: (update: RawAppYamlUpdate) => void
|
||||
}
|
||||
|
||||
let {
|
||||
drawer = $bindable(),
|
||||
summary,
|
||||
files,
|
||||
runnables,
|
||||
data,
|
||||
onApply
|
||||
}: Props = $props()
|
||||
let { drawer = $bindable(), summary, files, runnables, data, onApply }: Props = $props()
|
||||
|
||||
let code = $state('')
|
||||
let initialCode = $state('')
|
||||
@@ -106,6 +99,7 @@
|
||||
minHeight={editorHeight}
|
||||
bind:code
|
||||
lang="yaml"
|
||||
leadingChangeSync
|
||||
/>
|
||||
</div>
|
||||
{/await}
|
||||
|
||||
@@ -289,7 +289,7 @@
|
||||
{/if}
|
||||
{#if selected === 'OpenAPI' || (selected === 'OpenAPI_File' && !emptyStringTrimmed(openApiFile)) || (selected === 'OpenAPI_URL' && !emptyStringTrimmed(openApiUrl))}
|
||||
{#key forceRerender}
|
||||
<SimpleEditor class="h-96" {lang} bind:code />
|
||||
<SimpleEditor class="h-96" {lang} bind:code leadingChangeSync />
|
||||
{/key}
|
||||
{/if}
|
||||
<Button
|
||||
|
||||
@@ -624,6 +624,7 @@
|
||||
lang="yaml"
|
||||
class="h-full"
|
||||
fixedOverflowWidgets={false}
|
||||
leadingChangeSync
|
||||
/>
|
||||
{#snippet actions()}
|
||||
<Button
|
||||
@@ -676,6 +677,7 @@
|
||||
lang="yaml"
|
||||
class="h-full"
|
||||
fixedOverflowWidgets={false}
|
||||
leadingChangeSync
|
||||
/>
|
||||
{/if}
|
||||
{#snippet actions()}
|
||||
|
||||
Reference in New Issue
Block a user