Files
windmill/frontend/src/lib/svelte5UtilsKit.svelte.ts
T
Diego ImbertandRuben Fiszel ebb3048ca0 feat: mount session list pages in process instead of iframes (#11289)
* feat: mount session list pages in process instead of iframes

* fix: seed filter defaults from the query, not the cached search params

* fix: type trigger list rows from the generated trigger types

* fix: judge hosted lists by the operating workspace's rules and keep role-gated filters

* fix: keep the user folders filter key for every role, hidden where it does not apply

* fix: drop the user folders filter for users it is not offered to

* fix: wait for a known user before dropping the user folders filter

* style: tint trigger rows for every kind and align the schedules footer wording

* fix: stamp edited_at when a schedule is updated

---------

Co-authored-by: Ruben Fiszel <ruben@windmill.dev>
2026-09-24 15:14:59 +02:00

129 lines
3.9 KiB
TypeScript

// SvelteKit-specific utilities
// This file should only be imported in SvelteKit apps as it depends on $app/environment
import { z } from 'zod'
import { serializeParam } from '$lib/utils/serializeParam'
import { untrack } from 'svelte'
import { useHostedPage } from '$lib/components/hostedPage'
export type SearchParamsResult<S extends z.ZodType> =
S extends z.ZodObject<infer Shape>
? { -readonly [K in keyof Shape]: z.infer<Shape[K]> } & Record<string, unknown>
: z.infer<S> & Record<string, unknown>
/** Parse a raw string from the URL back to a typed value guided by the zod field schema. */
function deserializeParam(raw: string, fieldSchema: z.ZodType): unknown {
// Unwrap nullable / optional / default wrappers to get the inner type
let inner: z.ZodType = fieldSchema
while (
inner instanceof (z as any).ZodNullable ||
inner instanceof (z as any).ZodOptional ||
inner instanceof (z as any).ZodDefault
) {
inner = (inner as any)._def.innerType ?? (inner as any)._def.type
}
if (inner instanceof (z as any).ZodNumber) {
const n = Number(raw)
return isNaN(n) ? null : n
}
if (inner instanceof (z as any).ZodBoolean) {
if (raw === 'true') return true
if (raw === 'false') return false
return null
}
// ZodString, ZodEnum, ZodLiteral, ZodAny, or unknown → try as plain string first,
// fall back to JSON parse for non-string schemas
if (
inner instanceof (z as any).ZodString ||
inner instanceof (z as any).ZodEnum ||
inner instanceof (z as any).ZodLiteral
) {
return raw
}
// For any other type (object, array, …) try JSON
try {
return JSON.parse(raw)
} catch {
return raw
}
}
/**
* Returns a reactive object whose properties are synced to the browser's URL search params.
* Reading a property returns the current value from the URL (reactive).
* Writing a property updates the URL via history.replaceState (no navigation).
*
* Primitive types (string / number / boolean) are serialized as plain text.
* Everything else is JSON-serialized.
*/
export function useSearchParams<S extends z.ZodType>(schema: S): SearchParamsResult<S> {
const shape: Record<string, z.ZodType> = (schema as any).shape ?? {}
const keys = Object.keys(shape)
const hosted = useHostedPage()
const currentSearch = () => (hosted ? hosted.search : window.location.search)
// Reactive snapshot of search params - one $state cell per key
const values: Record<string, unknown> = $state(
Object.fromEntries(
keys.map((k) => {
const raw = new URLSearchParams(untrack(currentSearch)).get(k)
const parsed = raw != null ? deserializeParam(raw, shape[k]) : null
return [k, parsed]
})
)
)
function syncFromUrl() {
const sp = new URLSearchParams(currentSearch())
for (const k of keys) {
const raw = sp.get(k)
const parsed = raw != null ? deserializeParam(raw, shape[k]) : null
;(values as any)[k] = parsed
}
}
// Keep in sync when the user navigates back/forward, or the host re-points the page
$effect(() => {
if (hosted) {
hosted.search
untrack(syncFromUrl)
return
}
window.addEventListener('popstate', syncFromUrl)
return () => window.removeEventListener('popstate', syncFromUrl)
})
// Build the proxy object: reads come from $state, writes go to the URL + $state
const proxy: Record<string, unknown> = {}
for (const key of keys) {
Object.defineProperty(proxy, key, {
get() {
return (values as any)[key]
},
set(v: unknown) {
;(values as any)[key] = v
const sp = new URLSearchParams(untrack(currentSearch))
if (v == null) {
sp.delete(key)
} else {
sp.set(key, serializeParam(v))
}
if (hosted) {
hosted.setSearch(sp.size ? `?${sp}` : '')
return
}
const hash = window.location.hash
const newUrl = sp.toString()
? `${window.location.pathname}?${sp}${hash}`
: `${window.location.pathname}${hash}`
history.replaceState(history.state, '', newUrl)
},
enumerable: true,
configurable: true
})
}
return proxy as SearchParamsResult<S>
}