mirror of
https://github.com/windmill-labs/windmill.git
synced 2026-10-13 00:03:00 +00:00
fix: go home on workspace switch when the open item is missing there
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
ea0b947644
commit
0c770dd5fd
@@ -35,6 +35,16 @@
|
||||
|
||||
let aRef: HTMLAnchorElement | undefined = $state(undefined)
|
||||
let buttonRef: HTMLButtonElement | undefined = $state(undefined)
|
||||
|
||||
// Svelte delegates `onclick` to the app root, where SvelteKit's link handler runs first
|
||||
// and follows `href` regardless. Listening on the anchor itself lets `preventDefault()`
|
||||
// in `onClick` cancel the navigation.
|
||||
function anchorClick(node: HTMLAnchorElement) {
|
||||
const handler = onClick
|
||||
if (!handler) return
|
||||
node.addEventListener('click', handler)
|
||||
return () => node.removeEventListener('click', handler)
|
||||
}
|
||||
</script>
|
||||
|
||||
{#if href}
|
||||
@@ -57,7 +67,7 @@
|
||||
{target}
|
||||
onfocusin={onFocusIn}
|
||||
onfocusout={onFocusOut}
|
||||
onclick={onClick}
|
||||
{@attach anchorClick}
|
||||
>
|
||||
{@render children?.()}
|
||||
</a>
|
||||
|
||||
@@ -22,11 +22,10 @@
|
||||
import { MenuItem, Tooltip } from '$lib/components/meltComponents'
|
||||
import { EXECUTIONS_HINT } from './executionsHint'
|
||||
import WorkspaceIcon from '$lib/components/workspace/WorkspaceIcon.svelte'
|
||||
import { fixupUrlAfterWorkspaceSwitch } from './workspaceSwitchUrl'
|
||||
import { switchWorkspaceAndPage } from './workspaceSwitchUrl'
|
||||
import { goto } from '$lib/navigation'
|
||||
import { base } from '$lib/base'
|
||||
import { page } from '$app/state'
|
||||
import { switchWorkspace } from '$lib/storeUtils'
|
||||
import MultiplayerMenu from './MultiplayerMenu.svelte'
|
||||
import { isCloudHosted } from '$lib/cloud'
|
||||
import { workspaceAIClients } from '../copilot/lib'
|
||||
@@ -61,14 +60,13 @@
|
||||
if ($workspaceStore === id) {
|
||||
return
|
||||
}
|
||||
// Read before switchWorkspace: it swaps the stores this reads out from under us.
|
||||
// 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)
|
||||
switchWorkspace(id)
|
||||
// The sessions page needs no navigation here: the item's link navigation
|
||||
// (workspaceHref) keeps the route, and the page's family reconcile swaps
|
||||
// out a chat that doesn't belong to the new workspace's family.
|
||||
await fixupUrlAfterWorkspaceSwitch(id, { landOnHome })
|
||||
// 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 })
|
||||
}
|
||||
|
||||
// Operator page access is granted per workspace, so a switch lands on home rather than
|
||||
@@ -345,11 +343,7 @@
|
||||
</MenuItem>
|
||||
{/if}
|
||||
{#if canForkHere}
|
||||
<MenuItem
|
||||
class={itemClass}
|
||||
onClick={() => (globalForkModal.val = { opened: true })}
|
||||
{item}
|
||||
>
|
||||
<MenuItem class={itemClass} onClick={() => (globalForkModal.val = { opened: true })} {item}>
|
||||
<Plus size={16} />
|
||||
Workspace fork
|
||||
</MenuItem>
|
||||
|
||||
@@ -6,8 +6,7 @@
|
||||
userStore,
|
||||
superadmin
|
||||
} from '$lib/stores'
|
||||
import { switchWorkspace } from '$lib/storeUtils'
|
||||
import { fixupUrlAfterWorkspaceSwitch } from './workspaceSwitchUrl'
|
||||
import { switchWorkspaceAndPage } from './workspaceSwitchUrl'
|
||||
import { base } from '$lib/base'
|
||||
import { workspaceAIClients } from '$lib/components/copilot/lib'
|
||||
import WorkspaceFamilyPicker from '$lib/components/sessions/WorkspaceFamilyPicker.svelte'
|
||||
@@ -53,8 +52,7 @@
|
||||
function switchWorkspaceDirect(id: string) {
|
||||
if ($workspaceStore === id) return
|
||||
workspaceAIClients.init(id)
|
||||
switchWorkspace(id)
|
||||
void fixupUrlAfterWorkspaceSwitch(id)
|
||||
void switchWorkspaceAndPage(id)
|
||||
}
|
||||
|
||||
function openForkModal() {
|
||||
|
||||
@@ -1,34 +1,90 @@
|
||||
import { page } from '$app/state'
|
||||
import { replaceState } from '$app/navigation'
|
||||
import { goto } from '$lib/navigation'
|
||||
import { AppService, FlowService, JobService, ScriptService } from '$lib/gen'
|
||||
import { switchWorkspace } from '$lib/storeUtils'
|
||||
|
||||
// Post-workspace-switch URL fixup shared by the sidebar workspace pickers.
|
||||
// Item-scoped pages would show a wrong-workspace (or missing) item after a
|
||||
// switch — go home instead. Otherwise, really rewrite a ?workspace= param in
|
||||
// the address bar (mutating page.url is a no-op there): left stale, it gets
|
||||
// re-applied on reload or when exiting session mode restores the route,
|
||||
// silently switching the workspace back.
|
||||
const EDIT_PAGES = [
|
||||
'/scripts/edit/',
|
||||
'/flows/edit/',
|
||||
'/apps/edit/',
|
||||
'/apps_raw/edit/',
|
||||
'/scripts/get/',
|
||||
'/flows/get/',
|
||||
'/apps/get/',
|
||||
'/apps_raw/get/'
|
||||
]
|
||||
type ItemExists = (workspace: string, param: string) => Promise<boolean>
|
||||
|
||||
export async function fixupUrlAfterWorkspaceSwitch(
|
||||
const appExists: ItemExists = (workspace, path) => AppService.existsApp({ workspace, path })
|
||||
const flowExists: ItemExists = (workspace, path) =>
|
||||
FlowService.existsFlowByPath({ workspace, path })
|
||||
const scriptExists: ItemExists = (workspace, path) =>
|
||||
ScriptService.existsScriptByPath({ workspace, path })
|
||||
|
||||
// The script page's param is a hash, a path, or a hub path.
|
||||
const scriptHashOrPathExists: ItemExists = async (workspace, hashOrPath) => {
|
||||
if (hashOrPath.startsWith('hub/')) return true
|
||||
if (await scriptExists(workspace, hashOrPath)) return true
|
||||
try {
|
||||
await ScriptService.getScriptByHash({ workspace, hash: hashOrPath })
|
||||
return true
|
||||
} catch {
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
// A one-row lookup, where getJob would download the job's args and result.
|
||||
const jobExists: ItemExists = async (workspace, id) => {
|
||||
try {
|
||||
await JobService.getRootJobId({ workspace, id })
|
||||
return true
|
||||
} catch {
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
// 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 },
|
||||
'/(root)/(logged)/scripts/get/[...hash]': { param: 'hash', exists: scriptHashOrPathExists },
|
||||
'/(root)/(logged)/flows/edit/[...path]': { param: 'path', exists: flowExists },
|
||||
'/(root)/(logged)/flows/get/[...path]': { param: 'path', exists: flowExists },
|
||||
'/(root)/(logged)/apps/edit/[...path]': { param: 'path', exists: appExists },
|
||||
'/(root)/(logged)/apps/get/[...path]': { param: 'path', exists: appExists },
|
||||
'/(root)/(logged)/apps_raw/edit/[...path]': { param: 'path', exists: appExists },
|
||||
'/(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> {
|
||||
const itemPage = page.route.id ? ITEM_PAGES[page.route.id] : undefined
|
||||
const param = itemPage ? page.params[itemPage.param] : undefined
|
||||
if (!itemPage || !param) return false
|
||||
try {
|
||||
return !(await itemPage.exists(workspace, param))
|
||||
} catch {
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
// 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(
|
||||
id: string,
|
||||
// Decided by the caller so the rule stays with the picker that has the context
|
||||
// for it — a picker rendered outside the sidebar drives its own page and must
|
||||
// not be navigated away from.
|
||||
opts?: { landOnHome?: boolean }
|
||||
opts?: {
|
||||
// Decided by the caller so the rule stays with the picker that has the context
|
||||
// for it — a picker rendered outside the sidebar drives its own page and must
|
||||
// not be navigated away from.
|
||||
landOnHome?: boolean
|
||||
// Where to go when staying on the page, for a picker whose items link there.
|
||||
href?: string
|
||||
}
|
||||
): Promise<void> {
|
||||
if (EDIT_PAGES.some((p) => page.route.id?.includes(p) ?? false) || opts?.landOnHome) {
|
||||
if (opts?.landOnHome || (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)
|
||||
return
|
||||
}
|
||||
switchWorkspace(id)
|
||||
if (opts?.href) {
|
||||
await goto(opts.href)
|
||||
} else if (page.url.searchParams.get('workspace')) {
|
||||
// A stale ?workspace= param is re-applied on reload or when exiting session mode
|
||||
// restores the route, silently switching the workspace back.
|
||||
const url = new URL(window.location.href)
|
||||
url.searchParams.set('workspace', id)
|
||||
replaceState(url, page.state)
|
||||
|
||||
Reference in New Issue
Block a user