mirror of
https://github.com/windmill-labs/windmill.git
synced 2026-10-13 16:02:59 +00:00
fix: route a workspace switch through the editor's unsaved-changes prompt
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 5.5
parent
0c770dd5fd
commit
147dd09330
@@ -28,7 +28,6 @@
|
||||
import { page } from '$app/state'
|
||||
import MultiplayerMenu from './MultiplayerMenu.svelte'
|
||||
import { isCloudHosted } from '$lib/cloud'
|
||||
import { workspaceAIClients } from '../copilot/lib'
|
||||
import { twMerge } from 'tailwind-merge'
|
||||
import {
|
||||
ambiguousWorkspaceNames,
|
||||
@@ -63,7 +62,6 @@
|
||||
// Read before switching: the switch swaps the stores these read out from under us.
|
||||
const landOnHome = landsOnHome(id)
|
||||
const href = workspaceHref(id)
|
||||
workspaceAIClients.init(id)
|
||||
// On the sessions page, the page's family reconcile swaps out a chat that
|
||||
// doesn't belong to the new workspace's family.
|
||||
await switchWorkspaceAndPage(id, { landOnHome, href })
|
||||
|
||||
@@ -8,7 +8,6 @@
|
||||
} from '$lib/stores'
|
||||
import { switchWorkspaceAndPage } from './workspaceSwitchUrl'
|
||||
import { base } from '$lib/base'
|
||||
import { workspaceAIClients } from '$lib/components/copilot/lib'
|
||||
import WorkspaceFamilyPicker from '$lib/components/sessions/WorkspaceFamilyPicker.svelte'
|
||||
import WorkspaceScopeTrigger from '$lib/components/WorkspaceScopeTrigger.svelte'
|
||||
import {
|
||||
@@ -51,7 +50,6 @@
|
||||
|
||||
function switchWorkspaceDirect(id: string) {
|
||||
if ($workspaceStore === id) return
|
||||
workspaceAIClients.init(id)
|
||||
void switchWorkspaceAndPage(id)
|
||||
}
|
||||
|
||||
|
||||
@@ -1,8 +1,18 @@
|
||||
import { page } from '$app/state'
|
||||
import { replaceState } from '$app/navigation'
|
||||
import { goto } from '$lib/navigation'
|
||||
import { AppService, FlowService, JobService, ScriptService } from '$lib/gen'
|
||||
import { get } from 'svelte/store'
|
||||
import {
|
||||
AppService,
|
||||
FlowService,
|
||||
JobService,
|
||||
ScriptService,
|
||||
type UserDraftItemKind
|
||||
} from '$lib/gen'
|
||||
import { switchWorkspace } from '$lib/storeUtils'
|
||||
import { workspaceStore } from '$lib/stores'
|
||||
import { UserDraftDbSyncer } from '$lib/userDraftDbSyncer.svelte'
|
||||
import { workspaceAIClients } from '$lib/components/copilot/lib'
|
||||
|
||||
type ItemExists = (workspace: string, param: string) => Promise<boolean>
|
||||
|
||||
@@ -34,31 +44,70 @@ const jobExists: ItemExists = async (workspace, id) => {
|
||||
}
|
||||
}
|
||||
|
||||
type ItemPage = {
|
||||
param: string
|
||||
exists: ItemExists
|
||||
// Set on editors, whose auto-save-off edits are parked under this draft kind.
|
||||
draftKind?: UserDraftItemKind
|
||||
}
|
||||
|
||||
// Pages showing one workspace item, keyed by route id, with the check that the item named
|
||||
// by the route param exists in a given workspace.
|
||||
const ITEM_PAGES: Record<string, { param: string; exists: ItemExists }> = {
|
||||
'/(root)/(logged)/scripts/edit/[...path]': { param: 'path', exists: scriptExists },
|
||||
const ITEM_PAGES: Record<string, ItemPage> = {
|
||||
'/(root)/(logged)/scripts/edit/[...path]': {
|
||||
param: 'path',
|
||||
exists: scriptExists,
|
||||
draftKind: 'script'
|
||||
},
|
||||
'/(root)/(logged)/scripts/get/[...hash]': { param: 'hash', exists: scriptHashOrPathExists },
|
||||
'/(root)/(logged)/flows/edit/[...path]': { param: 'path', exists: flowExists },
|
||||
'/(root)/(logged)/flows/edit/[...path]': { param: 'path', exists: flowExists, draftKind: 'flow' },
|
||||
'/(root)/(logged)/flows/get/[...path]': { param: 'path', exists: flowExists },
|
||||
'/(root)/(logged)/apps/edit/[...path]': { param: 'path', exists: appExists },
|
||||
'/(root)/(logged)/apps/edit/[...path]': { param: 'path', exists: appExists, draftKind: 'app' },
|
||||
'/(root)/(logged)/apps/get/[...path]': { param: 'path', exists: appExists },
|
||||
'/(root)/(logged)/apps_raw/edit/[...path]': { param: 'path', exists: appExists },
|
||||
'/(root)/(logged)/apps_raw/edit/[...path]': {
|
||||
param: 'path',
|
||||
exists: appExists,
|
||||
draftKind: 'raw_app'
|
||||
},
|
||||
'/(root)/(logged)/apps_raw/get/[...path]': { param: 'path', exists: appExists },
|
||||
'/(root)/(logged)/run/[...run]': { param: 'run', exists: jobExists }
|
||||
}
|
||||
|
||||
async function itemPageMissingIn(workspace: string): Promise<boolean> {
|
||||
function currentItemPage(): { itemPage: ItemPage; param: string } | undefined {
|
||||
const itemPage = page.route.id ? ITEM_PAGES[page.route.id] : undefined
|
||||
const param = itemPage ? page.params[itemPage.param] : undefined
|
||||
if (!itemPage || !param) return false
|
||||
return itemPage && param ? { itemPage, param } : undefined
|
||||
}
|
||||
|
||||
async function itemPageMissingIn(workspace: string): Promise<boolean> {
|
||||
const current = currentItemPage()
|
||||
if (!current) return false
|
||||
try {
|
||||
return !(await itemPage.exists(workspace, param))
|
||||
return !(await current.itemPage.exists(workspace, current.param))
|
||||
} catch {
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
// Staying on an editor would load the target workspace's copy without a pathname change, so
|
||||
// its unsaved-changes guard (which only fires on one) would never ask about parked edits.
|
||||
function editorHasUnsavedEdits(): boolean {
|
||||
const current = currentItemPage()
|
||||
const workspace = get(workspaceStore)
|
||||
if (!current?.itemPage.draftKind || !workspace) return false
|
||||
const byHash = current.itemPage.draftKind === 'script' && page.url.searchParams.get('hash')
|
||||
return UserDraftDbSyncer.hasUnsavedDisabledChanges({
|
||||
workspace,
|
||||
itemKind: current.itemPage.draftKind,
|
||||
path: byHash ? '' : current.param
|
||||
})
|
||||
}
|
||||
|
||||
function switchTo(id: string) {
|
||||
workspaceAIClients.init(id)
|
||||
switchWorkspace(id)
|
||||
}
|
||||
|
||||
// Workspace switch shared by the sidebar workspace pickers. An item page stays open when
|
||||
// its item also exists in `id`, and goes home otherwise.
|
||||
export async function switchWorkspaceAndPage(
|
||||
@@ -72,14 +121,20 @@ export async function switchWorkspaceAndPage(
|
||||
href?: string
|
||||
}
|
||||
): Promise<void> {
|
||||
if (opts?.landOnHome || (await itemPageMissingIn(id))) {
|
||||
const unsavedEdits = editorHasUnsavedEdits()
|
||||
if (opts?.landOnHome || unsavedEdits || (await itemPageMissingIn(id))) {
|
||||
// Leave before switching: an item page still mounted when the store changes
|
||||
// refetches its item in `id` and toasts the 404.
|
||||
await goto('/')
|
||||
switchWorkspace(id)
|
||||
// The param carries the switch through the editor's unsaved-changes prompt: that
|
||||
// cancels this navigation and, on discard, replays this URL, which the logged layout
|
||||
// applies. On cancel nothing is switched.
|
||||
const from = page.route.id
|
||||
await goto(`/?workspace=${encodeURIComponent(id)}`)
|
||||
if (unsavedEdits && page.route.id === from) return
|
||||
switchTo(id)
|
||||
return
|
||||
}
|
||||
switchWorkspace(id)
|
||||
switchTo(id)
|
||||
if (opts?.href) {
|
||||
await goto(opts.href)
|
||||
} else if (page.url.searchParams.get('workspace')) {
|
||||
|
||||
Reference in New Issue
Block a user