mirror of
https://github.com/windmill-labs/windmill.git
synced 2026-10-30 08:16:49 +00:00
fix(frontend): no phantom draft when opening a CLI-pushed script (#10997)
* fix(frontend): no phantom draft when opening a CLI-pushed script Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01QpqVdiTiqVGCvgmpBzL6m3 * fix(frontend): infer the dbt descriptor schema on mount like ScriptEditor Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01QpqVdiTiqVGCvgmpBzL6m3 * fix(frontend): retry the baseline schema inference once like the editors do Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01QpqVdiTiqVGCvgmpBzL6m3 --------- Co-authored-by: Claude Fable 5.1 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Fable 5.1
parent
8f553eab35
commit
1be390aa87
@@ -15,7 +15,7 @@
|
||||
import { copyToClipboard, emptySchema, sendUserToast } from '$lib/utils'
|
||||
import Editor from './Editor.svelte'
|
||||
import { inferArgs, inferAssets, inferAnsibleExecutionMode } from '$lib/infer'
|
||||
import { parsePipelineAnnotations } from '$lib/components/assets/AssetGraph/parsePipelineAnnotations'
|
||||
import { injectPartitionArg } from '$lib/scriptEditorSchema'
|
||||
import { isWorkflowAsCode } from '$lib/components/graph/wacToFlow'
|
||||
import WacDiagram from '$lib/components/graph/WacDiagram.svelte'
|
||||
import { Pane, Splitpanes } from 'svelte-splitpanes'
|
||||
@@ -1074,55 +1074,6 @@
|
||||
}
|
||||
}
|
||||
|
||||
// A `// partitioned` pipeline script is materialized one slice at a time and
|
||||
// receives the slice as a runtime `partition` arg (the cascade injects it in
|
||||
// production). It isn't a code parameter, so schema inference doesn't see it —
|
||||
// surface it in the test form so a partitioned script can be run manually.
|
||||
function injectPartitionArg(
|
||||
s: any,
|
||||
a: Record<string, any> | undefined,
|
||||
l: string | undefined,
|
||||
c: string
|
||||
) {
|
||||
try {
|
||||
if (l !== 'duckdb' || !s?.properties) return
|
||||
const part = parsePipelineAnnotations(c).partition
|
||||
if (!part) return
|
||||
// Date-based partition kinds render a date / datetime picker; a dynamic
|
||||
// key is a free-form string.
|
||||
const format =
|
||||
part.kind === 'hourly'
|
||||
? 'date-time'
|
||||
: part.kind === 'daily' || part.kind === 'weekly' || part.kind === 'monthly'
|
||||
? 'date'
|
||||
: undefined
|
||||
if (!s.properties['partition']) {
|
||||
s.properties['partition'] = {
|
||||
type: 'string',
|
||||
...(format ? { format } : {}),
|
||||
// ISO output so partition keys sort lexicographically (the date
|
||||
// picker defaults to dd-MM-yyyy otherwise).
|
||||
...(format === 'date' ? { dateFormat: 'yyyy-MM-dd' } : {}),
|
||||
description:
|
||||
part.kind === 'dynamic'
|
||||
? 'Partition key value to materialize.'
|
||||
: `Partition (${part.kind}) to materialize.`
|
||||
}
|
||||
if (Array.isArray(s.order) && !s.order.includes('partition')) {
|
||||
s.order = ['partition', ...s.order]
|
||||
}
|
||||
}
|
||||
// Pre-fill the *test* arg with the current slice for date kinds — a
|
||||
// convenience default, kept on the args (not baked into the schema,
|
||||
// where it would persist to the deployed script and go stale).
|
||||
if (format && a && (a['partition'] == null || a['partition'] === '')) {
|
||||
const now = new Date()
|
||||
a['partition'] =
|
||||
format === 'date' ? now.toISOString().slice(0, 10) : now.toISOString().slice(0, 16)
|
||||
}
|
||||
} catch (e) {}
|
||||
}
|
||||
|
||||
async function inferModuleSchema() {
|
||||
if (activeModuleTab === null) return
|
||||
try {
|
||||
|
||||
@@ -8,7 +8,7 @@
|
||||
// single file, so the arguments, the run and the graph are all the project's
|
||||
// whichever file happens to be open.
|
||||
import { untrack } from 'svelte'
|
||||
import { createEventDispatcher, onDestroy } from 'svelte'
|
||||
import { createEventDispatcher, onDestroy, onMount } from 'svelte'
|
||||
import type { Schema, SupportedLanguage } from '$lib/common'
|
||||
import type { Preview, ScriptModule } from '$lib/gen'
|
||||
import { workspaceStore } from '$lib/stores'
|
||||
@@ -97,6 +97,17 @@
|
||||
}
|
||||
})
|
||||
|
||||
// The stored schema can predate the parser (a CLI push, an older version), and
|
||||
// the autosave baseline is taken as the mounted editor holds it
|
||||
// (`schemaAsEditorMounts`), so the descriptor is re-inferred on mount rather
|
||||
// than only on its first edit — otherwise the two only agree once edited.
|
||||
onMount(async () => {
|
||||
await inferSchema(code)
|
||||
// Same single retry as ScriptEditor: the first parse can lose a transient
|
||||
// wasm init race, and the baseline (`schemaAsEditorMounts`) retries too.
|
||||
if (!validDescriptor && code) await inferSchema(code)
|
||||
})
|
||||
|
||||
function flushOpenFile() {
|
||||
if (openFile !== null && modules?.[openFile]) {
|
||||
modules[openFile] = { ...modules[openFile], content: editorCode }
|
||||
|
||||
@@ -0,0 +1,90 @@
|
||||
import type { Schema, SupportedLanguage } from '$lib/common'
|
||||
import type { Script } from '$lib/gen'
|
||||
import { inferArgs } from '$lib/infer'
|
||||
import { emptySchema } from '$lib/utils'
|
||||
import { parsePipelineAnnotations } from '$lib/components/assets/AssetGraph/parsePipelineAnnotations'
|
||||
|
||||
/**
|
||||
* A `// partitioned` pipeline script is materialized one slice at a time and
|
||||
* receives the slice as a runtime `partition` arg (the cascade injects it in
|
||||
* production). It isn't a code parameter, so schema inference doesn't see it —
|
||||
* surface it in the test form so a partitioned script can be run manually.
|
||||
*/
|
||||
export function injectPartitionArg(
|
||||
s: any,
|
||||
a: Record<string, any> | undefined,
|
||||
l: string | undefined,
|
||||
c: string
|
||||
) {
|
||||
try {
|
||||
if (l !== 'duckdb' || !s?.properties) return
|
||||
const part = parsePipelineAnnotations(c).partition
|
||||
if (!part) return
|
||||
// Date-based partition kinds render a date / datetime picker; a dynamic
|
||||
// key is a free-form string.
|
||||
const format =
|
||||
part.kind === 'hourly'
|
||||
? 'date-time'
|
||||
: part.kind === 'daily' || part.kind === 'weekly' || part.kind === 'monthly'
|
||||
? 'date'
|
||||
: undefined
|
||||
if (!s.properties['partition']) {
|
||||
s.properties['partition'] = {
|
||||
type: 'string',
|
||||
...(format ? { format } : {}),
|
||||
// ISO output so partition keys sort lexicographically (the date
|
||||
// picker defaults to dd-MM-yyyy otherwise).
|
||||
...(format === 'date' ? { dateFormat: 'yyyy-MM-dd' } : {}),
|
||||
description:
|
||||
part.kind === 'dynamic'
|
||||
? 'Partition key value to materialize.'
|
||||
: `Partition (${part.kind}) to materialize.`
|
||||
}
|
||||
if (Array.isArray(s.order) && !s.order.includes('partition')) {
|
||||
s.order = ['partition', ...s.order]
|
||||
}
|
||||
}
|
||||
// Pre-fill the *test* arg with the current slice for date kinds — a
|
||||
// convenience default, kept on the args (not baked into the schema,
|
||||
// where it would persist to the deployed script and go stale).
|
||||
if (format && a && (a['partition'] == null || a['partition'] === '')) {
|
||||
const now = new Date()
|
||||
a['partition'] =
|
||||
format === 'date' ? now.toISOString().slice(0, 10) : now.toISOString().slice(0, 16)
|
||||
}
|
||||
} catch (e) {}
|
||||
}
|
||||
|
||||
/**
|
||||
* The schema the script editor holds right after mounting on `content`, without
|
||||
* any user input: `ScriptEditor.inferSchema` re-infers the main signature into
|
||||
* the stored schema and injects the duckdb partition arg. A schema stored by
|
||||
* another producer (a CLI push, an older parser) can lack keys the current
|
||||
* parser emits, so the editor rewrites it on mount; a draft baseline taken from
|
||||
* the raw stored schema would then never compare equal to the untouched editor.
|
||||
* Returns a copy; a parse failure leaves it as far as inference got, which is
|
||||
* also what the editor keeps.
|
||||
*/
|
||||
export async function schemaAsEditorMounts(
|
||||
language: SupportedLanguage | undefined,
|
||||
content: string,
|
||||
schema: unknown,
|
||||
kind: Script['kind'] | undefined
|
||||
): Promise<Schema> {
|
||||
const copy: Schema = schema ? structuredClone(schema as Schema) : emptySchema()
|
||||
const infer = async () => {
|
||||
await inferArgs(language, content, copy, kind === 'preprocessor' ? 'preprocessor' : undefined)
|
||||
injectPartitionArg(copy, undefined, language, content)
|
||||
}
|
||||
try {
|
||||
await infer()
|
||||
} catch {
|
||||
// The editors retry a failed mount inference once (transient wasm init);
|
||||
// the baseline has to land where that retry leaves the editor.
|
||||
if (!content || !language) return copy
|
||||
try {
|
||||
await infer()
|
||||
} catch {}
|
||||
}
|
||||
return copy
|
||||
}
|
||||
@@ -22,6 +22,7 @@
|
||||
import { UserDraftDbSyncer } from '$lib/userDraftDbSyncer.svelte'
|
||||
import { discardDraftAfterDeploy, runResetToDeployed } from '$lib/userDraftToast'
|
||||
import { usePageDraftSync } from '$lib/components/usePageDraftSync.svelte'
|
||||
import { schemaAsEditorMounts } from '$lib/scriptEditorSchema'
|
||||
import UnsavedConfirmationModal from '$lib/components/common/confirmationModal/UnsavedConfirmationModal.svelte'
|
||||
import { importScriptStore } from '$lib/components/scripts/scriptStore.svelte'
|
||||
import { OtherUserDraftLoad } from '$lib/components/otherUserDraftLoad.svelte'
|
||||
@@ -301,6 +302,18 @@
|
||||
getDraft
|
||||
})
|
||||
if (tok !== loadScriptToken) return
|
||||
// The editor re-infers the schema as soon as it mounts, so a baseline
|
||||
// taken from the raw stored schema would flag every unedited open of a
|
||||
// script whose stored schema differs from what this parser emits.
|
||||
const baselineSchema = backendScript.no_deployed
|
||||
? undefined
|
||||
: await schemaAsEditorMounts(
|
||||
backendScript.language,
|
||||
backendScript.content ?? '',
|
||||
backendScript.schema,
|
||||
backendScript.kind
|
||||
)
|
||||
if (tok !== loadScriptToken) return
|
||||
// Backend only computes `other_drafts_users` when `getDraft`. Don't clobber
|
||||
// the known list on a `getDraft:false` reload (e.g. reset-to-deployed, which
|
||||
// discards only OUR draft and must keep other users' drafts visible).
|
||||
@@ -329,11 +342,14 @@
|
||||
savedScript = structuredClone($state.snapshot(effectiveScript))
|
||||
const parentHash = topHash ?? backendScript.hash
|
||||
// Baseline for the autosave `discardIf`: the deployed script with the
|
||||
// same `parent_hash` graft the seed below applies, so the unedited draft
|
||||
// compares equal. `undefined` when there's no deployed row.
|
||||
// same `parent_hash` graft the seed below applies and the schema as the
|
||||
// mounted editor holds it, so the unedited draft compares equal.
|
||||
// `undefined` when there's no deployed row.
|
||||
deployedBaseline = backendScript.no_deployed
|
||||
? undefined
|
||||
: structuredClone($state.snapshot({ ...deployedScript, parent_hash: parentHash }))
|
||||
: structuredClone(
|
||||
$state.snapshot({ ...deployedScript, schema: baselineSchema, parent_hash: parentHash })
|
||||
)
|
||||
// "Load another user's draft" handoff: show their value over the
|
||||
// deployed metadata. If WE already have a draft → overlay mode (never
|
||||
// saved until the user confirms overwriting their own draft).
|
||||
|
||||
Reference in New Issue
Block a user