From 7adfb0310d01514e1f2e031c54d05931dcdbd396 Mon Sep 17 00:00:00 2001 From: Ruben Fiszel Date: Tue, 20 Jan 2026 13:19:29 +0000 Subject: [PATCH] feat(raw-apps): enable hash-based routing with URL sync for shareable URLs (#7624) --- .../components/raw_apps/RawAppPreview.svelte | 58 +++++++++++-- frontend/src/lib/components/raw_apps/utils.ts | 86 +++++++++++++++---- 2 files changed, 120 insertions(+), 24 deletions(-) diff --git a/frontend/src/lib/components/raw_apps/RawAppPreview.svelte b/frontend/src/lib/components/raw_apps/RawAppPreview.svelte index 4ef2c45f99..e45ea3b20b 100644 --- a/frontend/src/lib/components/raw_apps/RawAppPreview.svelte +++ b/frontend/src/lib/components/raw_apps/RawAppPreview.svelte @@ -3,6 +3,7 @@ import RawAppBackgroundRunner from './RawAppBackgroundRunner.svelte' import type { Runnable } from './rawAppPolicy' import { htmlContent } from './utils' + import { onMount } from 'svelte' interface Props { workspace: string @@ -15,13 +16,58 @@ let { workspace, user, secret, path, runnables }: Props = $props() let iframe = $state() as HTMLIFrameElement | undefined + + // Get initial hash from parent URL to pass to iframe + let initialHash = $state('') + + onMount(() => { + initialHash = window.location.hash || '' + }) + + // Use blob URL instead of srcDoc to give the iframe a proper origin. + // srcDoc iframes have "null" origin which breaks URL constructor in routers. + let blobUrl = $derived.by(() => { + if (!secret) return undefined + const baseUrl = typeof window !== 'undefined' ? window.location.origin : '' + const html = htmlContent(workspace, secret, { ctx: user, workspace }, baseUrl, initialHash) + const blob = new Blob([html], { type: 'text/html' }) + return URL.createObjectURL(blob) + }) + + // Cleanup blob URL when it changes or component unmounts + $effect(() => { + const url = blobUrl + return () => { + if (url) URL.revokeObjectURL(url) + } + }) + + // Listen for hash changes from iframe and update parent URL + $effect(() => { + function handleMessage(event: MessageEvent) { + console.log('[Parent] Received message:', event.data) + if (event.data?.type === 'windmill:hashchange') { + const newHash = event.data.hash || '' + console.log('[Parent] Updating hash to:', newHash) + // Update parent URL without triggering navigation + if (window.location.hash !== newHash) { + history.replaceState(null, '', newHash || window.location.pathname) + } + } + } + + window.addEventListener('message', handleMessage) + return () => window.removeEventListener('message', handleMessage) + }) - +{#if blobUrl} + +{/if} diff --git a/frontend/src/lib/components/raw_apps/utils.ts b/frontend/src/lib/components/raw_apps/utils.ts index 453f8e62af..f57a7e3a32 100644 --- a/frontend/src/lib/components/raw_apps/utils.ts +++ b/frontend/src/lib/components/raw_apps/utils.ts @@ -15,24 +15,74 @@ export type RawApp = { files: string[] } -export function htmlContent(workspace: string, secret: string | undefined, ctx: any) { - return ` - - - - - App Preview - - - - -
- - - - ` +export function htmlContent( + workspace: string, + secret: string | undefined, + ctx: any, + baseUrl: string = '', + initialHash: string = '' +) { + return ` + + + + App Preview + + + + +
+ + +` } function removeStaticFields(schema: Schema, fields: Record): Schema {