mirror of
https://github.com/windmill-labs/windmill.git
synced 2026-09-07 08:02:40 +00:00
* fix(frontend): session-pane draft seeding + restore actions Seed per-tab last_sync from the server draft's draft_saved_at in the loadFlow/loadScript "no local draft" branches (mirroring loadRawApp) so the seeding save attaches a matching last_sync and the server no longer clobbers an existing server draft with a fresh created_at. Replace the no-op loadFlow/loadRawApp-based diff-drawer restore handlers with proper restoreDeployed/restoreDraft that reset the live UserDraft cell (the inbound sync then updates the preview) and delete the per-user server draft, mirroring ScriptEditorView. Add rawAppValueToDraft to project a deployed raw-app value into the draft shape. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * fix(api): move UserDraftOverlay/UserDraftItemKind out of openflow inline block These two schemas were defined between the python-client's "# -- INLINE START/END --" markers, whose contents build.sh replaces with the openflow legacy wildcard $ref. That deleted both definitions during bundling while ~19 path responses still referenced them, failing the python-client build. Relocated them after the marker block. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * feat(frontend): explain legacy drafts in the draft badge popover The home-page draft badge lists each draft owner; a workspace-level row from before the per-user drafts migration shows as "Legacy workspace draft". Add an info tooltip next to it explaining that a legacy draft isn't tied to any user (email NULL) so everyone with access to the path sees it. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * feat(compare): show friendly draft path on the review & deploy page list_drafts now surfaces the draft JSON's `draft_path` (when set and different from the storage path) alongside summary, mirroring the home-page list endpoints. CompareDrafts displays it instead of the `u/{user}/draft_{uuid}` storage path, while all fetch/deploy/discard calls keep using the storage path (the draft's server-side key). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * fix(compare): delete the storage-path draft when deploying a renamed draft Deploying a draft from the review page replays the editor's create/update at the draft's friendly path, which deletes the draft server-side only at that path. A never-deployed item parked at `u/{user}/draft_{uuid}` therefore left its storage-path draft behind on deploy and kept listing. Delete the storage-path draft for every kind after a successful deploy, mirroring the editors' discardDraftAfterDeploy. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * feat(compare): badge legacy drafts on the review & deploy page list_drafts now reports `legacy_draft` (true when the listed row is a workspace-level NULL-email draft and no per-user row exists at the path). CompareDrafts shows a "Legacy draft" badge with a hover tooltip explaining these predate the per-user drafts migration and aren't tied to a user. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * fix(compare): allow discarding a legacy draft from the review page Legacy drafts (workspace-level, email NULL) aren't owned by the authed user, so the email-scoped draft delete in update_draft never matched them and the discard was a silent no-op. Add a delete-only `legacy` flag that retargets the DELETE (and the conflict re-read) to the NULL-email row, and route the review page's discard of a legacy draft through it. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * chore(backend): prune orphaned sqlx offline cache entries Re-ran the canonical update_sqlx.sh after rebasing windmill-ee-private onto origin/main and re-running substitute_ee_code.sh. Compiling the full workspace with all features recorded every live query and pruned 55 stale cache entries no longer produced by any query (22 are the removed `draft_only`-on-app lookups dropped by the db-backed user drafts work; the rest pre-existing orphans). Orphan entries don't break offline builds — this is cleanup only. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * fix(drafts): stop migrated draft-only items flooding the home list 20260609165313_remove_draft_only inserted the legacy (email IS NULL) draft stubs without an explicit created_at, so every row defaulted to the migration's now() (transaction_timestamp, constant for the whole transaction) and they all bunched at the migration instant — flooding the top of the newest-first home list. Add a corrective migration that resets those rows' created_at to the epoch so they sort to the bottom (their real per-item timestamps are unrecoverable — the source rows were deleted and the draft value carries no timestamp; editing one bumps created_at to now() and floats it back up). The rows are identified exactly via _sqlx_migrations.installed_on, which sqlx writes in the same transaction as the migration so it is byte-identical to the inserted rows' created_at; rows edited since no longer match and are left alone. Leaving remove_draft_only intact (rather than neutralizing it) keeps its essential schema work running everywhere; this migration runs right after and corrects the timestamps. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * docs(migration): note both timestamps are timestamptz in draft created_at repair Pre-empt a misread: draft.created_at became TIMESTAMPTZ in 20260514233244, so `created_at = installed_on` is an exact instant comparison, not a tz-sensitive timestamp/timestamptz cast. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * fix(compare): resolve friendly draft path per kind + strip email from u/ path list_drafts read the friendly path only from value->>'draft_path', which is empty for scripts — the script editor binds the Path widget to script.path, so the typed path round-trips through the draft JSON's own `path` (flows/apps/raw -apps use draft_path). Read the right field per kind, matching the home-page list endpoints, so renamed never-deployed scripts show their friendly name. Also truncate the user segment at `@` when displaying a `u/{user}/…` path: auto-generated draft slots are `u/{user}/draft_{uuid}`, and in the admins workspace (or email-as-username setups) `{user}` is the full email (`u/admin@windmill.dev/…` → `u/admin/…`). Display only — the path/key used for fetch/deploy/discard is unchanged. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * fix(raw-app): make diff-drawer "restore to deployed" reset like the autosave indicator The diff drawer's restoreDeployed ran the same runResetToDeployed as the AutosaveIndicator's "Reset to deployed", but its onResetToDeployed callback also did `redraw++`, remounting RawAppEditor mid-reset (inside the stopSync bracket); the fresh mount's draft write resurrected the draft, so the restore appeared to do nothing. Extract a single `reloadDeployed` callback (drop the draft handle + reload without the draft overlay) and use it for the diff drawer, the conflict modal, and the AutosaveIndicator so all three reset the same way. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * fix(compare): don't show auto-generated draft path as the bold title A never-named draft lives at a synthetic `u/{user}/draft_{uuid}` slot. When it had no summary and no friendly draft path, that uuid showed as the row's bold title. Return '' from displayPath for auto-generated paths so they aren't bolded — the row still shows the storage path in its secondary (grey) line. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * fix(diff-drawer): remove obsolete draft-vs-current tab selector The "Latest saved draft <> Current" comparison is obsolete. Remove the whole diff-type tab selector; normal-mode diffs now always show deployed-vs-current, simple-mode shows its single custom diff. Drop the now-unreachable restore-to-draft button and the `restoreDraft` prop (plus the dead handlers in the session editor views). The content/metadata selector is unchanged. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * fix(compare): make "Reset to deployed" work from the diff drawer Route the raw-app session preview and the low-code app editor diff-drawer restore through the same reset-to-deployed callback the AutosaveIndicator uses. - Raw-app session: add a deployedOnly path to loadRawApp that bypasses the draft (cell + server overlay) and reloads the deployed value; the diff drawer's restore now runs it via runResetToDeployed instead of rebuilding the draft shape in place (which hung and never reset). Also wires the in-session AutosaveIndicator reset. - Low-code app editor: drop the goto in the diff-drawer restoreDeployed that re-ran the page load with the draft overlay on and resurrected the draft; share one reloadDeployed across the diff drawer, AutosaveIndicator and the load-latest-deploy modal. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
241 lines
6.4 KiB
Svelte
241 lines
6.4 KiB
Svelte
<script lang="ts">
|
|
import { Alert, Button, Drawer, DrawerContent } from './common'
|
|
import { Loader2 } from 'lucide-svelte'
|
|
import { scriptLangToEditorLang } from '$lib/scripts'
|
|
import Tabs from './common/tabs/Tabs.svelte'
|
|
import Tab from './common/tabs/Tab.svelte'
|
|
import {
|
|
cleanValueProperties,
|
|
orderedJsonStringify,
|
|
orderedYamlStringify,
|
|
replaceFalseWithUndefined,
|
|
type Value
|
|
} from '$lib/utils'
|
|
import type { Script } from '$lib/gen'
|
|
|
|
type DiffData = {
|
|
lang?: string
|
|
content?: string
|
|
metadata: string
|
|
}
|
|
|
|
let diffType: 'draft' | 'deployed' | 'custom' | undefined = $state(undefined)
|
|
|
|
let contentType = $derived.by(() => {
|
|
if (!data || !diffType) return undefined
|
|
const dataType = diffType === 'custom' ? 'original' : diffType
|
|
return data[dataType]?.content !== data.current.content
|
|
? 'content'
|
|
: data[dataType]?.metadata !== data.current.metadata
|
|
? 'metadata'
|
|
: undefined
|
|
})
|
|
|
|
let diffViewer: Drawer | undefined = $state(undefined)
|
|
|
|
interface Props {
|
|
restoreDeployed?: () => Promise<void>
|
|
isFlow?: boolean
|
|
}
|
|
|
|
let { restoreDeployed = undefined, isFlow = false }: Props = $props()
|
|
|
|
let data:
|
|
| {
|
|
mode: 'normal'
|
|
deployed: DiffData | undefined
|
|
draft: DiffData | undefined
|
|
current: DiffData
|
|
path?: string
|
|
button?: { text: string; onClick: () => void }
|
|
}
|
|
| {
|
|
mode: 'simple'
|
|
title: string
|
|
original: DiffData | undefined
|
|
current: DiffData
|
|
button?: { text: string; onClick: () => void }
|
|
}
|
|
| undefined = $state(undefined)
|
|
|
|
export function openDrawer() {
|
|
data = undefined
|
|
diffType = undefined
|
|
diffViewer?.openDrawer()
|
|
}
|
|
|
|
export function closeDrawer() {
|
|
diffViewer?.closeDrawer()
|
|
}
|
|
|
|
function prepareDiff(data: Value) {
|
|
const metadata = structuredClone(cleanValueProperties(replaceFalseWithUndefined(data)))
|
|
const content = metadata['content']
|
|
if (metadata['content'] !== undefined) {
|
|
metadata['content'] = 'check content diff'
|
|
}
|
|
return {
|
|
lang: data.language ? scriptLangToEditorLang(data.language as Script['language']) : undefined,
|
|
content,
|
|
metadata: orderedYamlStringify(metadata)
|
|
}
|
|
}
|
|
|
|
export function setDiff(
|
|
diff:
|
|
| {
|
|
mode: 'normal'
|
|
deployed: Value
|
|
draft?: Value | undefined
|
|
current: Value
|
|
defaultDiffType?: 'deployed' | 'draft'
|
|
button?: { text: string; onClick: () => void }
|
|
}
|
|
| {
|
|
mode: 'simple'
|
|
original: Value
|
|
current: Value
|
|
title: string
|
|
button?: { text: string; onClick: () => void }
|
|
}
|
|
) {
|
|
if (diff.mode === 'normal') {
|
|
const { deployed, draft, current, button } = diff
|
|
data = {
|
|
mode: 'normal',
|
|
deployed: !deployed.draft_only ? prepareDiff(deployed) : undefined,
|
|
draft: draft ? prepareDiff(draft) : undefined,
|
|
current: prepareDiff(current),
|
|
path: draft?.path || deployed?.path,
|
|
button
|
|
}
|
|
|
|
// The draft-vs-current view (and its tab) is obsolete — always show the
|
|
// deployed-vs-current diff.
|
|
diffType = 'deployed'
|
|
} else {
|
|
const { original, current, title, button } = diff
|
|
data = {
|
|
title,
|
|
mode: 'simple',
|
|
original: prepareDiff(original),
|
|
current: prepareDiff(current),
|
|
button
|
|
}
|
|
diffType = 'custom'
|
|
}
|
|
}
|
|
</script>
|
|
|
|
<Drawer bind:this={diffViewer} size="1200px" on:close>
|
|
<DrawerContent title="Diff" on:close={diffViewer.closeDrawer}>
|
|
<div class="flex flex-col gap-4 h-full">
|
|
{#if data?.mode === 'normal'}
|
|
<Button
|
|
unifiedSize="md"
|
|
variant="default"
|
|
wrapperClasses="self-start"
|
|
onClick={restoreDeployed}
|
|
disabled={!data.draft &&
|
|
orderedJsonStringify(data.deployed) === orderedJsonStringify(data.current)}
|
|
>
|
|
Restore to deployed{data.draft ? ' and discard draft' : ''}
|
|
</Button>
|
|
{/if}
|
|
{#if data}
|
|
{#if contentType}
|
|
{@const content =
|
|
data.mode === 'normal' ? data.deployed?.content : data.original?.content}
|
|
{@const metadata =
|
|
data.mode === 'normal' ? data.deployed?.metadata : data.original?.metadata}
|
|
{@const lang = data.mode === 'normal' ? data.deployed?.lang : data.original?.lang}
|
|
<div class="flex flex-col h-full gap-4">
|
|
{#if data.current.content !== undefined}
|
|
<Tabs bind:selected={contentType}>
|
|
<Tab
|
|
value="content"
|
|
disabled={content === data.current.content}
|
|
label={`Content${content === data.current.content ? ' (no changes)' : ''}`}
|
|
/>
|
|
<Tab
|
|
value="metadata"
|
|
disabled={metadata === data.current.metadata}
|
|
label={`Metadata${metadata === data.current.metadata ? ' (no changes)' : ''}`}
|
|
/>
|
|
</Tabs>
|
|
{/if}
|
|
<div class="flex-1">
|
|
{#key diffType}
|
|
{#if contentType === 'content'}
|
|
{#await import('$lib/components/DiffEditor.svelte')}
|
|
<Loader2 class="animate-spin" />
|
|
{:then Module}
|
|
<Module.default
|
|
open={true}
|
|
automaticLayout
|
|
className="h-full"
|
|
defaultLang={lang}
|
|
defaultModifiedLang={data.current.lang}
|
|
defaultOriginal={content}
|
|
defaultModified={data.current.content}
|
|
readOnly
|
|
/>
|
|
{/await}
|
|
{:else if contentType === 'metadata'}
|
|
{#if isFlow}
|
|
{#await import('$lib/components/FlowDiffViewer.svelte')}
|
|
<Loader2 class="animate-spin" />
|
|
{:then Module}
|
|
<Module.default
|
|
beforeYaml={metadata ?? ''}
|
|
afterYaml={data.current.metadata}
|
|
/>
|
|
{/await}
|
|
{:else}
|
|
{#await import('$lib/components/DiffEditor.svelte')}
|
|
<Loader2 class="animate-spin" />
|
|
{:then Module}
|
|
<Module.default
|
|
open={true}
|
|
automaticLayout
|
|
className="h-full"
|
|
defaultLang="yaml"
|
|
defaultOriginal={metadata}
|
|
defaultModified={data.current.metadata}
|
|
readOnly
|
|
/>
|
|
{/await}
|
|
{/if}
|
|
{/if}
|
|
{/key}
|
|
</div>
|
|
</div>
|
|
{:else}
|
|
<Alert title="No changes detected">
|
|
{#if diffType === 'deployed'}
|
|
There are no differences between deployed and current
|
|
{:else}
|
|
There are no differences
|
|
{/if}
|
|
</Alert>
|
|
{/if}
|
|
{:else}
|
|
<Loader2 class="animate-spin" />
|
|
{/if}
|
|
</div>
|
|
{#snippet actions()}
|
|
{#if data?.button}
|
|
<Button
|
|
variant="subtle"
|
|
onClick={() => {
|
|
if (data?.button) {
|
|
data.button.onClick()
|
|
diffViewer?.closeDrawer()
|
|
}
|
|
}}>{data.button.text}</Button
|
|
>
|
|
{/if}
|
|
{/snippet}
|
|
</DrawerContent>
|
|
</Drawer>
|