- {#if showDevTools}
-
+
+
+
+
+ {#key $builderStore.selectedComponentId}
+ {#if $builderStore.inBuilder}
+
{/if}
+ {/key}
-
- {#if permissionError}
-
-
-
- {@html ErrorSVG}
-
- You don't have permission to use this app
-
-
- Ask your administrator to grant you access
-
-
-
- {:else if !$screenStore.activeLayout}
-
-
-
- {@html ErrorSVG}
-
- Something went wrong rendering your app
-
-
- Get in touch with support if this issue
- persists
-
-
-
- {:else if embedNoScreens}
-
-
-
- {@html ErrorSVG}
-
- This Budibase app is not publicly accessible
-
-
-
- {:else}
-
- {#key $screenStore.activeLayout._id}
-
- {/key}
-
-
-
-
-
-
+
+
+
+
+ {#if showDevTools}
+
{/if}
- {#if showDevTools}
-
+
+ {#if permissionError}
+
+
+
+ {@html ErrorSVG}
+
+ You don't have permission to use this app
+
+
+ Ask your administrator to grant you access
+
+
+
+ {:else if !$screenStore.activeLayout}
+
+
+
+ {@html ErrorSVG}
+
+ Something went wrong rendering your app
+
+
+ Get in touch with support if this issue
+ persists
+
+
+
+ {:else if embedNoScreens}
+
+
+
+ {@html ErrorSVG}
+
+ This Budibase app is not publicly accessible
+
+
+
+ {:else}
+
+ {#key $screenStore.activeLayout._id}
+
+ {/key}
+
+
+
+
+
+
+ {/if}
+
+ {#if showDevTools}
+
+ {/if}
+
+
+ {#if !$builderStore.inBuilder && $featuresStore.logoEnabled}
+
{/if}
- {#if !$builderStore.inBuilder && $featuresStore.logoEnabled}
-
+
+ {#if $appStore.isDevApp}
+
+ {/if}
+ {#if $builderStore.inBuilder || $devToolsStore.allowSelection}
+
+ {/if}
+ {#if $builderStore.inBuilder}
+
+
+
{/if}
-
-
- {#if $appStore.isDevApp}
-
- {/if}
- {#if $builderStore.inBuilder || $devToolsStore.allowSelection}
-
- {/if}
- {#if $builderStore.inBuilder}
-
-
-
- {/if}
-
-
+
+
diff --git a/packages/client/src/components/context/TestUrlBindingsProvider.svelte b/packages/client/src/components/context/TestUrlBindingsProvider.svelte
new file mode 100644
index 0000000000..15894ee032
--- /dev/null
+++ b/packages/client/src/components/context/TestUrlBindingsProvider.svelte
@@ -0,0 +1,8 @@
+
+
+
+
+
diff --git a/packages/client/src/index.js b/packages/client/src/index.js
new file mode 100644
index 0000000000..f19c46a452
--- /dev/null
+++ b/packages/client/src/index.js
@@ -0,0 +1,142 @@
+import ClientApp from "./components/ClientApp.svelte"
+import UpdatingApp from "./components/UpdatingApp.svelte"
+import {
+ builderStore,
+ appStore,
+ blockStore,
+ componentStore,
+ environmentStore,
+ dndStore,
+ eventStore,
+ hoverStore,
+ stateStore,
+ routeStore,
+} from "./stores"
+import loadSpectrumIcons from "@budibase/bbui/spectrum-icons-vite.js"
+import { get } from "svelte/store"
+import { initWebsocket } from "./websocket.js"
+
+// Provide svelte and svelte/internal as globals for custom components
+import * as svelte from "svelte"
+import * as internal from "svelte/internal"
+
+window.svelte_internal = internal
+window.svelte = svelte
+
+// Initialise spectrum icons
+loadSpectrumIcons()
+
+let app
+
+const loadBudibase = async () => {
+ // Update builder store with any builder flags
+ builderStore.set({
+ ...get(builderStore),
+ inBuilder: !!window["##BUDIBASE_IN_BUILDER##"],
+ layout: window["##BUDIBASE_PREVIEW_LAYOUT##"],
+ screen: window["##BUDIBASE_PREVIEW_SCREEN##"],
+ selectedComponentId: window["##BUDIBASE_SELECTED_COMPONENT_ID##"],
+ previewId: window["##BUDIBASE_PREVIEW_ID##"],
+ theme: window["##BUDIBASE_PREVIEW_THEME##"],
+ customTheme: window["##BUDIBASE_PREVIEW_CUSTOM_THEME##"],
+ previewDevice: window["##BUDIBASE_PREVIEW_DEVICE##"],
+ navigation: window["##BUDIBASE_PREVIEW_NAVIGATION##"],
+ hiddenComponentIds: window["##BUDIBASE_HIDDEN_COMPONENT_IDS##"],
+ usedPlugins: window["##BUDIBASE_USED_PLUGINS##"],
+ location: window["##BUDIBASE_LOCATION##"],
+ snippets: window["##BUDIBASE_SNIPPETS##"],
+ componentErrors: window["##BUDIBASE_COMPONENT_ERRORS##"],
+ })
+
+ // Set app ID - this window flag is set by both the preview and the real
+ // server rendered app HTML
+ appStore.actions.setAppId(window["##BUDIBASE_APP_ID##"])
+
+ // Set the flag used to determine if the app is being loaded via an iframe
+ appStore.actions.setAppEmbedded(
+ window["##BUDIBASE_APP_EMBEDDED##"] === "true"
+ )
+
+ if (window.MIGRATING_APP) {
+ new UpdatingApp({
+ target: window.document.body,
+ })
+ return
+ }
+
+ // Fetch environment info
+ if (!get(environmentStore)?.loaded) {
+ await environmentStore.actions.fetchEnvironment()
+ }
+
+ // Register handler for runtime events from the builder
+ window.handleBuilderRuntimeEvent = (type, data) => {
+ if (!window["##BUDIBASE_IN_BUILDER##"]) {
+ return
+ }
+ if (type === "event-completed") {
+ eventStore.actions.resolveEvent(data)
+ } else if (type === "eject-block") {
+ const block = blockStore.actions.getBlock(data)
+ block?.eject()
+ } else if (type === "dragging-new-component") {
+ const { dragging, component } = data
+ if (dragging) {
+ const definition =
+ componentStore.actions.getComponentDefinition(component)
+ dndStore.actions.startDraggingNewComponent({ component, definition })
+ } else {
+ dndStore.actions.reset()
+ }
+ } else if (type === "request-context") {
+ const { selectedComponentInstance, screenslotInstance } =
+ get(componentStore)
+ const instance = selectedComponentInstance || screenslotInstance
+ const context = instance?.getDataContext()
+ let stringifiedContext = null
+ try {
+ stringifiedContext = JSON.stringify(context)
+ } catch (error) {
+ // Ignore - invalid context
+ }
+ eventStore.actions.dispatchEvent("provide-context", {
+ context: stringifiedContext,
+ })
+ } else if (type === "hover-component") {
+ hoverStore.actions.hoverComponent(data, false)
+ } else if (type === "builder-meta") {
+ builderStore.actions.setMetadata(data)
+ } else if (type === "builder-state") {
+ const [[key, value]] = Object.entries(data)
+ stateStore.actions.setValue(key, value)
+ } else if (type === "builder-url-test-data") {
+ const { route, testValue } = data
+ routeStore.actions.setTestUrlParams(route, testValue)
+ }
+ }
+
+ // Register any custom components
+ if (window["##BUDIBASE_CUSTOM_COMPONENTS##"]) {
+ window["##BUDIBASE_CUSTOM_COMPONENTS##"].forEach(component => {
+ componentStore.actions.registerCustomComponent(component)
+ })
+ }
+
+ // Make a callback available for custom component bundles to register
+ // themselves at runtime
+ window.registerCustomComponent =
+ componentStore.actions.registerCustomComponent
+
+ // Initialise websocket
+ initWebsocket()
+
+ // Create app if one hasn't been created yet
+ if (!app) {
+ app = new ClientApp({
+ target: window.document.body,
+ })
+ }
+}
+
+// Attach to window so the HTML template can call this when it loads
+window.loadBudibase = loadBudibase
diff --git a/packages/client/src/index.ts b/packages/client/src/index.ts
index 443648eb08..8d8fc80ed1 100644
--- a/packages/client/src/index.ts
+++ b/packages/client/src/index.ts
@@ -10,6 +10,7 @@ import {
eventStore,
hoverStore,
stateStore,
+ routeStore,
} from "@/stores"
import { get } from "svelte/store"
import { initWebsocket } from "@/websocket"
@@ -188,6 +189,9 @@ const loadBudibase = async () => {
} else if (type === "builder-state") {
const [[key, value]] = Object.entries(data)
stateStore.actions.setValue(key, value)
+ } else if (type === "builder-url-test-data") {
+ const { route, testValue } = data
+ routeStore.actions.setTestUrlParams(route, testValue)
}
}
diff --git a/packages/client/src/stores/routes.ts b/packages/client/src/stores/routes.ts
index e6cb6288d2..5158205b0a 100644
--- a/packages/client/src/stores/routes.ts
+++ b/packages/client/src/stores/routes.ts
@@ -119,7 +119,39 @@ const createRouteStore = () => {
const base = window.location.href.split("#")[0]
return `${base}#${relativeURL}`
}
+ const setTestUrlParams = (route: string, testValue: string) => {
+ if (route === "/") {
+ return
+ }
+ const [pathPart, queryPart] = testValue.split("?")
+ const routeSegments = route.split("/").filter(Boolean)
+
+ // If first segment happens to be a parameter (e.g. /:foo), include it
+ const startIndex = routeSegments[0]?.startsWith(":") ? 0 : 1
+ const segments = routeSegments.slice(startIndex)
+ const testSegments = pathPart.split("/")
+
+ const params: Record
= {}
+ segments.forEach((segment, index) => {
+ if (segment.startsWith(":") && index < testSegments.length) {
+ params[segment.slice(1)] = testSegments[index]
+ }
+ })
+
+ const queryParams: Record = {}
+ if (queryPart) {
+ queryPart.split("&").forEach(param => {
+ const [key, value] = param.split("=")
+ if (key && value) {
+ queryParams[key] = value
+ }
+ })
+ }
+
+ setQueryParams({ ...queryParams })
+ store.update(state => ({ ...state, testUrlParams: params }))
+ }
return {
subscribe: store.subscribe,
actions: {
@@ -130,6 +162,7 @@ const createRouteStore = () => {
setQueryParams,
setActiveRoute,
setRouterLoaded,
+ setTestUrlParams,
},
}
}
diff --git a/packages/types/src/ui/stores/preview.ts b/packages/types/src/ui/stores/preview.ts
index 4d09366ff5..d9f5f2ac46 100644
--- a/packages/types/src/ui/stores/preview.ts
+++ b/packages/types/src/ui/stores/preview.ts
@@ -1 +1,2 @@
export type PreviewDevice = "desktop" | "tablet" | "mobile"
+export type ComponentContext = Record