mirror of
https://github.com/windmill-labs/windmill.git
synced 2026-10-29 08:12:56 +00:00
* feat(frontend): surface local drafts in drawer editors with an unsaved-changes banner Drawer-based editors (the 11 trigger types, plus resource and variable) restore unsaved edits from browser localStorage on open using the same mechanism as flows/scripts, but only showed a transient "Reset to deployed" toast with no way to review the diff. Add a persistent "You have unsaved changes" banner below the drawer header with Show diff / Discard actions, shown whenever the form diverges from the deployed baseline. Replaces the toast for these editors; flows/scripts/apps (full-page) keep their existing toast. - new shared LocalDraftBanner.svelte (Alert-styled bar + DiffDrawer) - DrawerContent: optional `banner` snippet rendered below the header - useTriggerDraftSync: reactive `hasDraft`, `deployed`/`current` getters and `resetToDeployed`; drop the restore toast (banner supersedes it) - wire the banner into all 11 trigger editors + variable; resource lifts its dirty state up to ResourceEditorDrawer via a callback + accessors - fix ScheduleEditorInner.openNew not resetting initialConfig (reused editor instance kept a stale baseline, wrongly flagging a new schedule dirty) Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * fix(frontend): address PR review on local-draft drawer banners - LocalDraftBanner: snapshot diff sides at click time so the diff drawer doesn't keep updating as the user types behind it. - VariableEditor / ResourceEditor: scope the banner and its Discard action to the selected workspace; the cross-workspace dirty state stays surfaced by the existing otherDirty Alert. Forward can_write via a new onCanWriteChange callback so the resource banner hides Discard in read-only mode (matching the trigger editors). - useTriggerDraftSync: drop the now-unused path arg from maybeRestore and update all 11 trigger editor call sites. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix(frontend): deep-clone fallback in UserDraft.discard to avoid baseline aliasing When a caller passed a live $state proxy as `fallback` (Variable/Resource editors handed `initialStates[selected]` to the banner's Discard), the handle's draft cell ended up sharing the same proxy as the caller's baseline. Subsequent form edits mutated both sides in lock-step and the dirty check kept reporting equal, so the banner never reappeared and the Update button stayed disabled until the drawer was reopened. Cloning the fallback inside `discard` (via `snapshotDraftValue`) gives the handle a fresh tree and decouples the two reactive graphs. Trigger editors already cloned at their call site (resetToDeployed); this just makes the API self-contained for all callers. Also switch the variable form's "Audit log for each access" alert from warning to info — it's informational, not a warning. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix(frontend): honor disabled prop in LocalDraftBanner's diff drawer The banner's `disabled` prop hid the inline Discard button but the diff drawer's "Discard changes" action was still wired unconditionally, so a read-only user could bypass the hidden inline action via Show diff. Gate the diff-drawer button on the same flag so both surfaces agree. Flagged by cubic and Codex on PR #9335. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
102 lines
3.0 KiB
Svelte
102 lines
3.0 KiB
Svelte
<script lang="ts">
|
|
import { Button } from '$lib/components/common'
|
|
import DiffDrawer from '$lib/components/DiffDrawer.svelte'
|
|
import { classes } from '$lib/components/common/alert/model'
|
|
import { type Value } from '$lib/utils'
|
|
import { AlertCircle } from 'lucide-svelte'
|
|
import { twMerge } from 'tailwind-merge'
|
|
import { slide } from 'svelte/transition'
|
|
|
|
interface Props {
|
|
/** Whether there are unsaved local changes relative to the deployed baseline. */
|
|
show: boolean
|
|
/** Deployed/backend baseline config (the "original" side of the diff). */
|
|
getDeployed: () => unknown
|
|
/** Current form config — the local autosave (the "current" side of the diff). */
|
|
getCurrent: () => unknown
|
|
/** Drop the local changes and reset the form to the deployed baseline. */
|
|
onDiscard: () => void | Promise<void>
|
|
/** When true (e.g. no write access), hide the Discard action. */
|
|
disabled?: boolean
|
|
/** Diff drawer title. */
|
|
title?: string
|
|
}
|
|
|
|
let {
|
|
show,
|
|
getDeployed,
|
|
getCurrent,
|
|
onDiscard,
|
|
disabled = false,
|
|
title = 'Deployed <> Local changes'
|
|
}: Props = $props()
|
|
|
|
let diffDrawer: DiffDrawer | undefined = $state()
|
|
|
|
function showDiff() {
|
|
const deployed = getDeployed()
|
|
if (deployed == null) return
|
|
// Snapshot both sides at click time. They are typically Svelte `$state`
|
|
// proxies (resource/variable's `initialStates[ws]` and the draft handle's
|
|
// cell); without snapshot the diff drawer would re-read them reactively
|
|
// and update as the user keeps typing behind it.
|
|
const original = $state.snapshot(deployed) as Value
|
|
const current = $state.snapshot(getCurrent()) as Value
|
|
diffDrawer?.openDrawer()
|
|
// Mirror the inline Discard's `disabled` gate inside the diff drawer —
|
|
// otherwise a read-only user could still trigger onDiscard via the
|
|
// drawer button even though we hid the banner's inline action.
|
|
diffDrawer?.setDiff({
|
|
mode: 'simple',
|
|
original,
|
|
current,
|
|
title,
|
|
button: disabled
|
|
? undefined
|
|
: {
|
|
text: 'Discard changes',
|
|
onClick: async () => {
|
|
await onDiscard()
|
|
diffDrawer?.closeDrawer()
|
|
}
|
|
}
|
|
})
|
|
}
|
|
</script>
|
|
|
|
<DiffDrawer bind:this={diffDrawer} />
|
|
|
|
{#if show}
|
|
<div
|
|
transition:slide|local={{ duration: 120 }}
|
|
class={twMerge(
|
|
'flex flex-row items-center justify-between gap-2 px-4 py-1',
|
|
classes.warning.bgClass,
|
|
'!border-0 !rounded-none'
|
|
)}
|
|
>
|
|
<div class="flex flex-row items-center gap-2 min-w-0">
|
|
<AlertCircle class={classes.warning.iconClass} size={16} />
|
|
<span class={twMerge('text-xs font-semibold truncate', classes.warning.titleClass)}>
|
|
You have unsaved changes
|
|
</span>
|
|
</div>
|
|
<div class="flex flex-row items-center gap-2 shrink-0">
|
|
<Button
|
|
unifiedSize="sm"
|
|
variant="subtle"
|
|
btnClasses={classes.warning.titleClass}
|
|
on:click={showDiff}>Show diff</Button
|
|
>
|
|
{#if !disabled}
|
|
<Button
|
|
unifiedSize="sm"
|
|
variant="subtle"
|
|
btnClasses={classes.warning.titleClass}
|
|
on:click={onDiscard}>Discard</Button
|
|
>
|
|
{/if}
|
|
</div>
|
|
</div>
|
|
{/if}
|