fix: isolate SvelteKit-specific imports for library usage

Split SvelteKit-specific code into separate files to allow
windmill-components to be used as a library in non-SvelteKit
contexts (e.g., windmill-react-sdk):

- Split logout.ts into logout.ts and logoutKit.ts
- Split svelte5Utils.svelte.ts into svelte5Utils.svelte.ts and
  svelte5UtilsKit.svelte.ts (for runed/kit useSearchParams)
- Fix triggers/utils.ts type-only import resolution
- Update FlowRestartButton to use callback instead of direct navigation
- Update all route files to import from logoutKit

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
Ruben Fiszel
2026-01-21 00:25:32 +00:00
parent 2a64c208a1
commit 203f6785c4
14 changed files with 83 additions and 69 deletions
@@ -5,7 +5,6 @@
import { FlowService, JobService, type FlowVersion } from '$lib/gen'
import { workspaceStore } from '$lib/stores'
import { emptyString, sendUserToast } from '$lib/utils'
import { goto } from '$lib/navigation'
interface Props {
jobId: string
@@ -17,7 +16,10 @@
enterpriseOnly?: boolean
variant?: 'default' | 'accent'
unifiedSize?: 'xs' | 'sm' | 'md' | 'lg'
/** Called when flow is restarted. If not provided, will navigate to the new run using goto (requires SvelteKit) */
onRestart?: (stepId: string, branchOrIterationN: number, flowVersion?: number) => void
/** Called when flow restart completes with the new job ID. Used for navigation in non-SvelteKit contexts */
onRestartComplete?: (newJobId: string) => void
}
let {
@@ -30,7 +32,8 @@
enterpriseOnly = false,
variant = 'default',
unifiedSize = 'md',
onRestart
onRestart,
onRestartComplete
}: Props = $props()
let branchOrIterationN = $state(0)
@@ -49,7 +52,7 @@
flow_version: flowVersion
}
})
await goto('/run/' + run + '?workspace=' + $workspaceStore)
onRestartComplete?.(run)
}
async function loadFlowVersions() {
+2 -1
View File
@@ -44,7 +44,8 @@
import ToggleButton from '$lib/components/common/toggleButton-v2/ToggleButton.svelte'
import Select from '$lib/components/select/Select.svelte'
import AnimatedPane from '$lib/components/splitPanes/AnimatedPane.svelte'
import { useSearchParams, StaleWhileLoading } from '$lib/svelte5Utils.svelte'
import { useSearchParams } from '$lib/svelte5UtilsKit.svelte'
import { StaleWhileLoading } from '$lib/svelte5Utils.svelte'
interface Props {
/** Initial path from route params (e.g., /runs/u/user/script) */
@@ -28,7 +28,7 @@
} from '$lib/stores'
import { twMerge } from 'tailwind-merge'
import { USER_SETTINGS_HASH } from './settings'
import { logout } from '$lib/logout'
import { logout } from '$lib/logoutKit'
import DarkModeObserver from '../DarkModeObserver.svelte'
import BarsStaggered from '../icons/BarsStaggered.svelte'
import { Menu, Menubar, MenuItem } from '$lib/components/meltComponents'
@@ -1,6 +1,6 @@
<script lang="ts">
import { goto } from '$lib/navigation'
import { logout } from '$lib/logout'
import { logout } from '$lib/logoutKit'
import {
userStore,
usageStore,
@@ -14,7 +14,7 @@ import type {
} from '$lib/gen/types.gen'
import type { Writable } from 'svelte/store'
import SchedulePollIcon from '../icons/SchedulePollIcon.svelte'
import { type TriggerKind } from '$lib/components/triggers'
import type { TriggerKind } from '../triggers'
import { saveScheduleFromCfg } from '$lib/components/flows/scheduleUtils'
import { saveHttpRouteFromCfg } from './http/utils'
import { saveWebsocketTriggerFromCfg } from './websocket/utils'
@@ -14,7 +14,7 @@
type CompletedJob
} from '$lib/gen'
import { validateUsername } from '$lib/utils'
import { logoutWithRedirect } from '$lib/logout'
import { logoutWithRedirect } from '$lib/logoutKit'
import { page } from '$app/stores'
import { usersWorkspaceStore, workspaceStore } from '$lib/stores'
import CenteredModal from '$lib/components/CenteredModal.svelte'
+3 -24
View File
@@ -1,33 +1,12 @@
import { goto } from '$lib/navigation'
import { UserService } from '$lib/gen'
import { clearStores } from './storeUtils'
import { sendUserToast } from './toast'
export async function logoutWithRedirect(rd?: string): Promise<void> {
console.log('logoutWithRedirect', rd)
await clearUser()
const splitted = rd?.split('?')[0]
if (rd && rd != '/' && splitted != '/user/login' && splitted != '/user/logout') {
const error = document.cookie.includes('token')
? `error=${encodeURIComponent('You have been logged out because your session has expired.')}&`
: ''
console.log('login redirect with error', error, rd)
goto(`/user/login?${error}${rd ? 'rd=' + encodeURIComponent(rd) : ''}`, { replaceState: true })
} else {
console.log('login redirect vanilla')
goto('/user/login', { replaceState: true })
}
}
export async function logout(): Promise<void> {
await clearUser()
goto(`/user/login`)
sendUserToast('you have been logged out')
}
// Note: logout and logoutWithRedirect have been moved to logoutKit.ts
// as they depend on SvelteKit navigation
export async function clearUser() {
try {
clearStores()
await UserService.logout()
} catch (error) { }
} catch (error) {}
}
+28
View File
@@ -0,0 +1,28 @@
// SvelteKit-specific logout utilities
// These functions depend on $lib/navigation which requires SvelteKit
import { goto } from '$lib/navigation'
import { clearUser } from './logout'
import { sendUserToast } from './toast'
export async function logoutWithRedirect(rd?: string): Promise<void> {
console.log('logoutWithRedirect', rd)
await clearUser()
const splitted = rd?.split('?')[0]
if (rd && rd != '/' && splitted != '/user/login' && splitted != '/user/logout') {
const error = document.cookie.includes('token')
? `error=${encodeURIComponent('You have been logged out because your session has expired.')}&`
: ''
console.log('login redirect with error', error, rd)
goto(`/user/login?${error}${rd ? 'rd=' + encodeURIComponent(rd) : ''}`, { replaceState: true })
} else {
console.log('login redirect vanilla')
goto('/user/login', { replaceState: true })
}
}
export async function logout(): Promise<void> {
await clearUser()
goto(`/user/login`)
sendUserToast('you have been logged out')
}
+1 -1
View File
@@ -1,7 +1,7 @@
import { goto as svelteGoto } from '$app/navigation'
import { base as svelteBase } from '$app/paths'
export function goto(path, options = {}) {
export function goto(path: string, options = {}) {
if (svelteBase == '' || path.startsWith('?')) {
return svelteGoto(path, options)
} else {
+1 -33
View File
@@ -2,10 +2,8 @@
import { untrack } from 'svelte'
import { deepEqual } from 'fast-equals'
import { type StateStore } from './utils'
import type { StateStore } from './utils'
import { resource, watch, type ResourceReturn } from 'runed'
import * as runed from 'runed/kit'
import type z from 'zod'
export function withProps<Component, Props>(component: Component, props: Props) {
const ret = $state({
@@ -190,36 +188,6 @@ export class ChangeOnDeepInequality<T> {
}
}
// The original from runed has a weird behavior with dedup reads causing duplicate effect runs
// (Every field has to be derived to avoid it : https://runed.dev/docs/utilities/use-search-params)
export function useSearchParams<S extends z.ZodType>(
schema: S,
options?: runed.SearchParamsOptions
): runed.ReturnUseSearchParams<S> {
let params = runed.useSearchParams(schema, options)
let keys = Object.keys((schema as any).shape ?? {})
let obj = { ...params }
for (const key of keys) {
// Somehow using $derived does not trigger reactivity sometimes ...
// (e.g: filters.arg in RunsPage.svelte updates in the URL but does not trigger reactivity)
let derivedVal = $state(params[key])
Object.defineProperty(obj, key, {
get: () => {
if (typeof derivedVal === 'string') return decodeURIComponent(derivedVal)
return derivedVal
},
set: (v) => {
const val = typeof v === 'string' ? encodeURIComponent(v) : v
params[key] = val
derivedVal = val
},
enumerable: true,
configurable: true
})
}
return obj
}
// Prevents flickering when data is unloaded (undefined) then reloaded quickly
// But still becomes undefined if data is not reloaded within the timeout
// so the user has feedback that the data is not available anymore.
@@ -0,0 +1,35 @@
// SvelteKit-specific utilities
// This file should only be imported in SvelteKit apps as it depends on $app/environment
import * as runed from 'runed/kit'
import type z from 'zod'
// The original from runed has a weird behavior with dedup reads causing duplicate effect runs
// (Every field has to be derived to avoid it : https://runed.dev/docs/utilities/use-search-params)
export function useSearchParams<S extends z.ZodType>(
schema: S,
options?: runed.SearchParamsOptions
): runed.ReturnUseSearchParams<S> {
let params = runed.useSearchParams(schema, options)
let keys = Object.keys((schema as any).shape ?? {})
let obj = { ...params }
for (const key of keys) {
// Somehow using $derived does not trigger reactivity sometimes ...
// (e.g: filters.arg in RunsPage.svelte updates in the URL but does not trigger reactivity)
let derivedVal = $state(params[key])
Object.defineProperty(obj, key, {
get: () => {
if (typeof derivedVal === 'string') return decodeURIComponent(derivedVal)
return derivedVal
},
set: (v) => {
const val = typeof v === 'string' ? encodeURIComponent(v) : v
params[key] = val
derivedVal = val
},
enumerable: true,
configurable: true
})
}
return obj
}
@@ -3,7 +3,7 @@
import { base } from '$app/paths'
import { page } from '$app/stores'
import { sendUserToast } from '$lib/toast'
import { logout, logoutWithRedirect } from '$lib/logout'
import { logout, logoutWithRedirect } from '$lib/logoutKit'
import { UserService, type WorkspaceInvite, WorkspaceService } from '$lib/gen'
import {
superadmin,
+1 -1
View File
@@ -2,7 +2,7 @@
import { goto } from '$lib/navigation'
import { page } from '$app/stores'
import { UserService, WorkspaceService } from '$lib/gen'
import { logoutWithRedirect } from '$lib/logout'
import { logoutWithRedirect } from '$lib/logoutKit'
import { userStore, usersWorkspaceStore, workspaceStore } from '$lib/stores'
import { getUserExt } from '$lib/user'
import { sendUserToast } from '$lib/toast'
@@ -7,7 +7,7 @@
import { userStore, usersWorkspaceStore, workspaceStore } from '$lib/stores'
import { getUserExt } from '$lib/user'
import { logoutWithRedirect } from '$lib/logout'
import { logoutWithRedirect } from '$lib/logoutKit'
import { parseQueryParams } from '$lib/utils'
import { page } from '$app/state'
import { isCloudHosted } from '$lib/cloud'