Files
windmill/frontend/src/lib/svelte5UtilsKit.svelte.ts
T
Diego Imbertandclaude[bot] 4a21babe79 feat: Unified filters and new runs page (#8027)
* RunsPage redesign v0

* nit

* Remove manualdatepicker

* remove shadow

* ui nits

* nit scrollbar bg

* prettier cards

* nit

* Remove code

* command/meta multi select

* Shift select

* RightClickPopover

* nit

* Ctrl A

* nit card

* DropdownMenu

* nit

* count hint

* fix stuck keys

* opacity UX

* error toasts pickhubscript

* Improve UX

* fix undefined error

* keyboard nav

* nit batch rerun fixes

* nit fix scroll / height

* Batch reruns actions + nits

* nit

* Cancel selected jobs

* Cancel / re-run all filtered jobs

* Go to job / flow / script action

* nit

* add batch actions back

* nit

* nit

* bar on splitpane hover

* nit

* New Timeframe system

* reset btn

* nit fixes

* dead code

* nits

* typecheck

* naming clarity

* Update frontend/src/lib/components/RightClickPopover.svelte

Co-authored-by: claude[bot] <209825114+claude[bot]@users.noreply.github.com>

* unnecessary json stringify

* dedup 'the'

* Code deletion to prepare for changes

* filter types

* ui

* fix bug with maxTs

* stuck with melt

* GenericDropdown

* filters onclick

* iterate

* iter

* add all filters

* Descriptions

* focus position

* stash

* TaggedTextInput works much much better

* placeholder

* currentTag suggestion

* improve

* nit

* Keyboard nav

* buildRunsFilterSearchbarSchema

* nit naming

* assignObjInPlace

* Escaping + pretty dates

* nit empty

* fix cursor

* nit space

* Filter filtering

* escape pasted value

* nit

* escape spaces

* nit undefined

* add space at end if right arrow

* escape all spaces

* arrow skips escape chars

* escape \ too

* delete whole escaped characters

* double space to escape tag

* code refactor

* Ensure cursor visible

* fix keyboard nav

* safety

* filterSchemaRecToZodSchema

* URL Sync

* fix readonly

* fix typing

* start replacing old filter logic

* use new filter impl

* nit

* nit reactivity

* nit fix

* no more localStorage

* Add back status and kind toggles

* Nit fix

* style nit

* focus at end on click

* clearn btn + fixes

* fix broken date uri

* nit

* useSyncedTimeframe

* negative filter button

* negative filters helpers rust

* Negated filters backed

* nit

* highlight

* New useSearchParams

* Accept comma separated list

* nit allowNegative

* openapi update

* Fix trigger kind list/negation not working

* nit oipenpai

* Presets

* DebouncedTempValue

* remove presets from list when already applied

* UI nit improvements

* allowMultiple

* hint

* validateFilterInstance fn

* nit fix

* error highlights

* nit ux selecting negative list

* nit

* on clear btn

* SimpleEditor for JSON

* nit

* flop

* Pass presets as param

* nit delete

* preventCursorMoveOnNextSync

* responsive layout

* Escape \n

* Inline calendar input

* mm/dd or dd/mm depending on US or not

* onClickBehavior

* infiniteRange

* other nits

* Wiring with runs filter

* formatDateRange better

* inits on right page

* style

* min hour support

* Time input

* use our components

* Improve SKILL.md

* dd mm yyyy numeric input

* TimeframeSelect with new date picker

* fixes

* ensure date is in view when value changes externally

* fixes

* nit select all on focus

* select year + nits

* nit layout shift

* nit negative when starting with !

* nit

* SelectDropdown uses GenericDropdown now

* Fix blank select dropdown rendering bug

* icons

* Reset btn + shorter date range formatting

* overflow fix

* unnecessary absolute

* fix clear btn overlap

* Update routes for new filters (assets, schedule, resource, variables)

* update openapi

* Impl for other pages

* ui nits

* nit fixes

* Fix columns filter

* super nits

---------

Co-authored-by: claude[bot] <209825114+claude[bot]@users.noreply.github.com>
2026-02-23 17:53:09 +00:00

122 lines
3.8 KiB
TypeScript

// SvelteKit-specific utilities
// This file should only be imported in SvelteKit apps as it depends on $app/environment
import { z } from 'zod'
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>
/** Serialize a value to a URL search param string. Primitives are written as-is; anything else is JSON. */
function serializeParam(value: unknown): string {
if (typeof value === 'string') return value
if (typeof value === 'number') return String(value)
if (typeof value === 'boolean') return String(value)
return JSON.stringify(value)
}
/** 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)
// 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(window.location.search).get(k)
const parsed = raw != null ? deserializeParam(raw, shape[k]) : null
return [k, parsed]
})
)
)
function syncFromUrl() {
const sp = new URLSearchParams(window.location.search)
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
$effect(() => {
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(window.location.search)
if (v == null) {
sp.delete(key)
} else {
sp.set(key, serializeParam(v))
}
const newUrl = sp.toString()
? `${window.location.pathname}?${sp}`
: window.location.pathname
history.replaceState(history.state, '', newUrl)
},
enumerable: true,
configurable: true
})
}
return proxy as SearchParamsResult<S>
}