mirror of
https://github.com/windmill-labs/windmill.git
synced 2026-09-21 00:02:30 +00:00
feat: two-action out-of-date prompt; taking the latest moves into the diff drawer
Four buttons made the prompt hard to read. It keeps "See what changed" and a red "Use latest" (it replaces the draft); closing it is keeping the draft. "Take latest, keep my edits" moves to the diff drawer's header, offered only while the draft is behind, so the user takes the latest with the diff in front of them. Scripts, flows and raw apps pass the action through their diff drawer; the classic app editor has no drawer wired to the prompt and loses it. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Fable 5.1
parent
ceae36a85b
commit
504c360d66
@@ -50,6 +50,7 @@
|
||||
* way to tell what their draft is being compared against. */
|
||||
deployedLabel?: string
|
||||
versions?: DiffVersionOption[]
|
||||
onTakeLatest?: () => void | Promise<void>
|
||||
draft: DiffData | undefined
|
||||
current: DiffData
|
||||
path?: string
|
||||
@@ -112,6 +113,18 @@
|
||||
}
|
||||
}
|
||||
|
||||
let takingLatest = $state(false)
|
||||
async function takeLatest() {
|
||||
if (!data || data.mode !== 'normal' || !data.onTakeLatest || takingLatest) return
|
||||
takingLatest = true
|
||||
try {
|
||||
await data.onTakeLatest()
|
||||
diffViewer?.closeDrawer()
|
||||
} finally {
|
||||
takingLatest = false
|
||||
}
|
||||
}
|
||||
|
||||
export function setDiff(
|
||||
diff:
|
||||
| {
|
||||
@@ -120,6 +133,7 @@
|
||||
deployedLabel?: string
|
||||
versions?: DiffVersionOption[]
|
||||
loadVersion?: (id: string) => Promise<Value | undefined>
|
||||
onTakeLatest?: () => void | Promise<void>
|
||||
draft?: Value | undefined
|
||||
current: Value
|
||||
defaultDiffType?: 'deployed' | 'draft'
|
||||
@@ -134,7 +148,16 @@
|
||||
}
|
||||
) {
|
||||
if (diff.mode === 'normal') {
|
||||
const { deployed, deployedLabel, versions, loadVersion, draft, current, button } = diff
|
||||
const {
|
||||
deployed,
|
||||
deployedLabel,
|
||||
versions,
|
||||
loadVersion,
|
||||
onTakeLatest,
|
||||
draft,
|
||||
current,
|
||||
button
|
||||
} = diff
|
||||
versionLoader = loadVersion
|
||||
headLabel = deployedLabel
|
||||
selectedVersion = versions?.find((v) => v.isHead)?.id
|
||||
@@ -143,6 +166,7 @@
|
||||
deployed: !deployed.draft_only ? prepareDiff(deployed) : undefined,
|
||||
deployedLabel,
|
||||
versions,
|
||||
onTakeLatest,
|
||||
draft: draft ? prepareDiff(draft) : undefined,
|
||||
current: prepareDiff(current),
|
||||
path: draft?.path || deployed?.path,
|
||||
@@ -279,6 +303,11 @@
|
||||
{/if}
|
||||
</div>
|
||||
{#snippet actions()}
|
||||
{#if data?.mode === 'normal' && data.onTakeLatest}
|
||||
<Button unifiedSize="sm" variant="default" loading={takingLatest} onClick={takeLatest}>
|
||||
Take latest, keep my edits
|
||||
</Button>
|
||||
{/if}
|
||||
{#if data?.mode === 'normal'}
|
||||
<Button
|
||||
unifiedSize="sm"
|
||||
|
||||
@@ -123,6 +123,7 @@
|
||||
* route re-seeds from `draft_path` so the topbar shows the pending name,
|
||||
* this stays where the item actually is. */
|
||||
userDraftPath = '',
|
||||
onTakeLatest = undefined,
|
||||
pathStoreInit = undefined,
|
||||
newFlow,
|
||||
selectedId,
|
||||
@@ -1072,6 +1073,7 @@
|
||||
deployed: deployedValue ?? savedFlow,
|
||||
deployedLabel,
|
||||
versions: await deployedVersionOptions(),
|
||||
onTakeLatest,
|
||||
loadVersion: async (id) => {
|
||||
const v = await FlowService.getFlowVersion({
|
||||
workspace: opWorkspace!,
|
||||
|
||||
@@ -119,6 +119,7 @@
|
||||
fullyLoaded = true,
|
||||
initialPath = $bindable(''),
|
||||
userDraftPath = '',
|
||||
onTakeLatest = undefined,
|
||||
autosaveWorkspace = undefined,
|
||||
autosavePath = undefined,
|
||||
template = $bindable('script'),
|
||||
@@ -887,6 +888,7 @@
|
||||
deployed,
|
||||
deployedLabel: deployedVersionLabel(deployed),
|
||||
versions: await deployedVersionOptions(headHash),
|
||||
onTakeLatest,
|
||||
loadVersion: async (hash) => {
|
||||
const v = await ScriptService.getScriptByHash({ workspace: opWorkspace!, hash })
|
||||
return replaceFalseWithUndefined({
|
||||
|
||||
@@ -54,9 +54,6 @@
|
||||
deployedBy?: string | undefined
|
||||
/** Discard the draft and reload deployed (same as "Reset to deployed"). */
|
||||
onLoadLatestDeploy?: () => void | Promise<void>
|
||||
/** Move the draft's base to the head and keep its content (see
|
||||
* StaleDraftModal). Omit where the route cannot set the base. */
|
||||
onTakeLatest?: () => void | Promise<void>
|
||||
/** Opens the editor's Deployed↔Current diff from the stale prompt, so the
|
||||
* choice between keeping and discarding is informed. Omit where the editor
|
||||
* has no diff drawer; the action is then not rendered. */
|
||||
@@ -87,7 +84,6 @@
|
||||
deployedBy = undefined,
|
||||
onLoadLatestDeploy,
|
||||
onViewDiff,
|
||||
onTakeLatest,
|
||||
onBeforeRelocate,
|
||||
enabled = true
|
||||
}: Props = $props()
|
||||
@@ -185,7 +181,6 @@
|
||||
{deployedBy}
|
||||
{onLoadLatestDeploy}
|
||||
{onViewDiff}
|
||||
{onTakeLatest}
|
||||
/>
|
||||
{/if}
|
||||
<ConfirmationModal
|
||||
|
||||
@@ -3,9 +3,10 @@
|
||||
* The prompt for a draft that is behind: someone deployed a newer version
|
||||
* of the item after the draft forked from it. Opened on every load while
|
||||
* that holds (the parent computes it; see DraftEditorModals), it names the
|
||||
* two versions and offers the four ways out: look at the diff, keep going,
|
||||
* take the latest as the new base while keeping the edits, or drop the
|
||||
* draft for the latest deploy.
|
||||
* two versions and offers two ways out: look at the diff, or drop the draft
|
||||
* for the latest deploy. Closing it is "keep editing". Taking the latest as
|
||||
* the new base while keeping the edits lives in the diff drawer, where the
|
||||
* user can see what they are taking.
|
||||
*
|
||||
* Open-state is bindable so the parent can dismiss programmatically
|
||||
* (e.g. after the load-latest-deploy callback completes).
|
||||
@@ -38,10 +39,6 @@
|
||||
* to see what actually differs — and after a rename the difference is
|
||||
* often only the path. Omitted where the editor has no diff drawer. */
|
||||
onViewDiff?: () => void | Promise<void>
|
||||
/** Moves the draft's base to the head and keeps its content: the one way
|
||||
* to acknowledge the newer version without discarding edits. The route
|
||||
* owns it because the base lives in a per-kind field of the value. */
|
||||
onTakeLatest?: () => void | Promise<void>
|
||||
}
|
||||
|
||||
let {
|
||||
@@ -53,25 +50,10 @@
|
||||
deployedHeadVersion = undefined,
|
||||
deployedBy = undefined,
|
||||
onLoadLatestDeploy,
|
||||
onViewDiff,
|
||||
onTakeLatest
|
||||
onViewDiff
|
||||
}: Props = $props()
|
||||
|
||||
let loading = $state(false)
|
||||
let takingLatest = $state(false)
|
||||
|
||||
async function takeLatest() {
|
||||
if (takingLatest || !onTakeLatest) return
|
||||
takingLatest = true
|
||||
try {
|
||||
await onTakeLatest()
|
||||
isOpen = false
|
||||
} catch (e: any) {
|
||||
sendUserToast(`Could not take the latest version: ${e?.body ?? e?.message ?? e}`, true)
|
||||
} finally {
|
||||
takingLatest = false
|
||||
}
|
||||
}
|
||||
|
||||
// Scripts are versioned by hash, the other kinds by a numeric version id;
|
||||
// the diff picker renders them the same way.
|
||||
@@ -142,19 +124,11 @@
|
||||
{:else}
|
||||
<div></div>
|
||||
{/if}
|
||||
<div class="flex gap-2">
|
||||
<Button variant="default" unifiedSize="sm" on:click={() => (isOpen = false)}>
|
||||
Keep editing my draft
|
||||
</Button>
|
||||
{#if onTakeLatest}
|
||||
<Button variant="default" unifiedSize="sm" loading={takingLatest} on:click={takeLatest}>
|
||||
Take latest, keep my edits
|
||||
</Button>
|
||||
{/if}
|
||||
<Button variant="accent" unifiedSize="sm" {loading} on:click={loadLatestDeploy}>
|
||||
Load latest deploy
|
||||
</Button>
|
||||
</div>
|
||||
<!-- Red: this replaces the draft with the latest deploy. Closing the modal
|
||||
is "keep editing", so it needs no button of its own. -->
|
||||
<Button variant="accent" destructive unifiedSize="sm" {loading} on:click={loadLatestDeploy}>
|
||||
Use latest
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</Modal2>
|
||||
|
||||
@@ -27,6 +27,10 @@ export type DiffDrawerDiff =
|
||||
/** Loads one version's payload. Returning `undefined` leaves the current
|
||||
* comparison in place rather than blanking the diff. */
|
||||
loadVersion?: (id: string) => Promise<Value | undefined>
|
||||
/** Moves the draft's base to the head and keeps its content. Passed only
|
||||
* while the draft is behind; rendered as a header action so the user
|
||||
* takes the latest with the diff in front of them. */
|
||||
onTakeLatest?: () => void | Promise<void>
|
||||
draft?: Value | undefined
|
||||
current: Value
|
||||
defaultDiffType?: 'deployed' | 'draft'
|
||||
|
||||
@@ -14,6 +14,9 @@ export type FlowBuilderProps = {
|
||||
* draft's `draft_path` so the topbar shows the pending name, so it can't be used
|
||||
* to resolve what is actually deployed. */
|
||||
userDraftPath?: string
|
||||
/** Moves the draft's base to the deployed head and keeps its content; offered
|
||||
* in the diff drawer while the draft is behind. */
|
||||
onTakeLatest?: () => void | Promise<void>
|
||||
pathStoreInit?: string | undefined
|
||||
newFlow: boolean
|
||||
selectedId: string | undefined
|
||||
|
||||
@@ -136,6 +136,9 @@
|
||||
* deployed" guard: deploying is refused with a confirmation while it is not
|
||||
* the head. Undefined for a draft-only app. */
|
||||
version?: number | undefined
|
||||
/** Moves the draft's base to the deployed head and keeps its content;
|
||||
* offered in the diff drawer while the draft is behind. */
|
||||
onTakeLatest?: () => void | Promise<void>
|
||||
// See ScriptBuilderProps — same indicator semantics.
|
||||
loadedFromDraft?: boolean
|
||||
othersDraftsCount?: number
|
||||
@@ -208,7 +211,8 @@
|
||||
onRestore,
|
||||
onSavedNewAppPath,
|
||||
condensedHeader = false,
|
||||
version = undefined
|
||||
version = undefined,
|
||||
onTakeLatest = undefined
|
||||
}: Props = $props()
|
||||
|
||||
// Workspace this editor operates on: the session's acting workspace when
|
||||
@@ -2303,6 +2307,7 @@
|
||||
bind:summary
|
||||
bind:pendingDraftPath
|
||||
{version}
|
||||
{onTakeLatest}
|
||||
{onRestore}
|
||||
{onSavedNewAppPath}
|
||||
{policy}
|
||||
|
||||
@@ -109,6 +109,9 @@
|
||||
}
|
||||
| undefined
|
||||
version?: number | undefined
|
||||
/** Moves the draft's base to the deployed head and keeps its content;
|
||||
* offered in the diff drawer while the draft is behind. */
|
||||
onTakeLatest?: () => void | Promise<void>
|
||||
newApp: boolean
|
||||
newPath?: string
|
||||
/** Initial labels for the app, threaded from the loaded app data. */
|
||||
@@ -179,6 +182,7 @@
|
||||
diffDrawer = undefined,
|
||||
savedApp = $bindable(undefined),
|
||||
version = $bindable(undefined),
|
||||
onTakeLatest = undefined,
|
||||
newApp,
|
||||
newPath = '',
|
||||
labels: initialLabels = undefined,
|
||||
@@ -445,6 +449,7 @@
|
||||
mode: 'normal',
|
||||
deployed: deployedValue ?? stripRawAppDiffNoise(savedApp),
|
||||
versions: await deployedVersionOptions(),
|
||||
onTakeLatest,
|
||||
loadVersion: async (id) => {
|
||||
const v = await AppService.getAppByVersion({ workspace: opWorkspace!, id: Number(id) })
|
||||
// Same normalization as `syncWithDeployed`, so switching versions doesn't
|
||||
|
||||
@@ -36,6 +36,10 @@ export interface ScriptBuilderProps {
|
||||
* stop/restart pair is a no-op on a non-live entry.
|
||||
*/
|
||||
userDraftPath?: string
|
||||
/** Moves the draft's base to the deployed head and keeps its content; offered
|
||||
* in the diff drawer while the draft is behind. The route owns it because
|
||||
* the base lives in a per-kind field of the value. */
|
||||
onTakeLatest?: () => void | Promise<void>
|
||||
/**
|
||||
* Workspace + path the AutosaveIndicator watches for sync state. Default
|
||||
* (undefined) falls back to `$workspaceStore` / `userDraftPath` — the
|
||||
|
||||
@@ -465,13 +465,6 @@
|
||||
{draftBaseVersion}
|
||||
{deployedHeadVersion}
|
||||
{deployedBy}
|
||||
onTakeLatest={async () => {
|
||||
const head = deployedHeadVersion != null ? Number(deployedHeadVersion) : undefined
|
||||
if (!app?.value || head == null || !$workspaceStore) return
|
||||
;(app.value as App).parent_version = head
|
||||
draftBaseVersion = String(head)
|
||||
await UserDraft.forcePersist('app', path, { workspace: $workspaceStore })
|
||||
}}
|
||||
onLoadLatestDeploy={async () => {
|
||||
if (!$workspaceStore) return
|
||||
await runResetToDeployed({
|
||||
|
||||
@@ -582,14 +582,6 @@
|
||||
{draftBaseVersion}
|
||||
{deployedHeadVersion}
|
||||
{deployedBy}
|
||||
onTakeLatest={() => {
|
||||
const head = deployedHeadVersion != null ? Number(deployedHeadVersion) : undefined
|
||||
if (head == null) return
|
||||
// The bundle carries `parentVersion`, so this alone re-persists the draft.
|
||||
parentVersion = head
|
||||
if (deployedBaseline) deployedBaseline = { ...deployedBaseline, parent_version: head }
|
||||
draftBaseVersion = String(head)
|
||||
}}
|
||||
onViewDiff={() => rawAppEditor?.openDiffDrawer()}
|
||||
onLoadLatestDeploy={async () => {
|
||||
// stopSync-bracketed; see /scripts/edit's restoreDeployed for the race.
|
||||
@@ -630,6 +622,17 @@
|
||||
{diffDrawer}
|
||||
newApp={isNewApp}
|
||||
version={parentVersion}
|
||||
onTakeLatest={draftBaseVersion &&
|
||||
deployedHeadVersion &&
|
||||
draftBaseVersion !== deployedHeadVersion
|
||||
? () => {
|
||||
const head = Number(deployedHeadVersion)
|
||||
// The bundle carries `parentVersion`, so this alone re-persists the draft.
|
||||
parentVersion = head
|
||||
if (deployedBaseline) deployedBaseline = { ...deployedBaseline, parent_version: head }
|
||||
draftBaseVersion = String(head)
|
||||
}
|
||||
: undefined}
|
||||
onDeploy={({ version }) => {
|
||||
// The version just written is the new head, and the base the next
|
||||
// autosave should carry.
|
||||
|
||||
@@ -528,13 +528,6 @@
|
||||
{draftBaseVersion}
|
||||
deployedHeadVersion={version != null ? String(version) : undefined}
|
||||
{deployedBy}
|
||||
onTakeLatest={async () => {
|
||||
const head = version
|
||||
if (!draftSync.draft || head == null || !$workspaceStore) return
|
||||
draftSync.draft = { ...draftSync.draft, version_id: head }
|
||||
draftBaseVersion = String(head)
|
||||
await UserDraft.forcePersist('flow', flowDraftPath, { workspace: $workspaceStore })
|
||||
}}
|
||||
onViewDiff={() => flowBuilder?.openDiffDrawer()}
|
||||
onBeforeRelocate={() => flowBuilder?.saveDraft()}
|
||||
onLoadLatestDeploy={async () => {
|
||||
@@ -558,6 +551,15 @@
|
||||
</div>
|
||||
{:else if renderEditor}
|
||||
<FlowBuilder
|
||||
onTakeLatest={draftBaseVersion && version != null && draftBaseVersion !== String(version)
|
||||
? async () => {
|
||||
const head = version
|
||||
if (!draftSync.draft || head == null || !$workspaceStore) return
|
||||
draftSync.draft = { ...draftSync.draft, version_id: head }
|
||||
draftBaseVersion = String(head)
|
||||
await UserDraft.forcePersist('flow', flowDraftPath, { workspace: $workspaceStore })
|
||||
}
|
||||
: undefined}
|
||||
onDeploy={(e) => {
|
||||
// stopSync-bracketed immediate delete; see /scripts/edit's restoreDeployed.
|
||||
if ($workspaceStore) {
|
||||
|
||||
@@ -489,16 +489,6 @@
|
||||
draftBaseVersion={draftBaseHash}
|
||||
deployedHeadVersion={deployedHeadHash}
|
||||
{deployedBy}
|
||||
onTakeLatest={async () => {
|
||||
const head = deployedHeadHash
|
||||
if (!draftSync.draft || !head || !$workspaceStore) return
|
||||
draftSync.draft = { ...draftSync.draft, parent_hash: head }
|
||||
// The baseline mirrors the draft's base so an unedited draft still
|
||||
// compares equal and the autosave can discard it.
|
||||
if (deployedBaseline) deployedBaseline = { ...deployedBaseline, parent_hash: head }
|
||||
draftBaseHash = head
|
||||
await UserDraft.forcePersist('script', draftPath, { workspace: $workspaceStore })
|
||||
}}
|
||||
onViewDiff={() => scriptBuilder?.openDiffDrawer()}
|
||||
onBeforeRelocate={() => scriptBuilder?.saveDraft()}
|
||||
onLoadLatestDeploy={async () => {
|
||||
@@ -520,6 +510,18 @@
|
||||
bind:this={scriptBuilder}
|
||||
{initialPath}
|
||||
userDraftPath={draftPath}
|
||||
onTakeLatest={draftBaseHash && deployedHeadHash && draftBaseHash !== deployedHeadHash
|
||||
? async () => {
|
||||
const head = deployedHeadHash
|
||||
if (!draftSync.draft || !head || !$workspaceStore) return
|
||||
draftSync.draft = { ...draftSync.draft, parent_hash: head }
|
||||
// The baseline mirrors the draft's base so an unedited draft still
|
||||
// compares equal and the autosave can discard it.
|
||||
if (deployedBaseline) deployedBaseline = { ...deployedBaseline, parent_hash: head }
|
||||
draftBaseHash = head
|
||||
await UserDraft.forcePersist('script', draftPath, { workspace: $workspaceStore })
|
||||
}
|
||||
: undefined}
|
||||
bind:script={draftSync.draft}
|
||||
template={builderTemplate}
|
||||
{lockedLanguage}
|
||||
|
||||
Reference in New Issue
Block a user