- {#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 4d2bd0e1e2..f055e7fc15 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"
@@ -179,6 +180,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/server/src/api/controllers/static/index.ts b/packages/server/src/api/controllers/static/index.ts
index 97406d677d..6b8ecda0d9 100644
--- a/packages/server/src/api/controllers/static/index.ts
+++ b/packages/server/src/api/controllers/static/index.ts
@@ -339,10 +339,13 @@ export const getSignedUploadURL = async function (
ctx.throw(400, "bucket and key values are required")
}
try {
+ let endpoint = datasource?.config?.endpoint
+ if (endpoint && !utils.urlHasProtocol(endpoint)) {
+ endpoint = `https://${endpoint}`
+ }
const s3 = new S3({
region: awsRegion,
- endpoint: datasource?.config?.endpoint || undefined,
-
+ endpoint: endpoint,
credentials: {
accessKeyId: datasource?.config?.accessKeyId as string,
secretAccessKey: datasource?.config?.secretAccessKey as string,
@@ -350,8 +353,8 @@ export const getSignedUploadURL = async function (
})
const params = { Bucket: bucket, Key: key }
signedUrl = await getSignedUrl(s3, new PutObjectCommand(params))
- if (datasource?.config?.endpoint) {
- publicUrl = `${datasource.config.endpoint}/${bucket}/${key}`
+ if (endpoint) {
+ publicUrl = `${endpoint}/${bucket}/${key}`
} else {
publicUrl = `https://${bucket}.s3.${awsRegion}.amazonaws.com/${key}`
}
diff --git a/packages/server/src/integrations/s3.ts b/packages/server/src/integrations/s3.ts
index 488c22835a..88d00724f1 100644
--- a/packages/server/src/integrations/s3.ts
+++ b/packages/server/src/integrations/s3.ts
@@ -7,7 +7,7 @@ import {
ConnectionInfo,
} from "@budibase/types"
-import { S3 } from "@aws-sdk/client-s3"
+import { S3, S3ClientConfig } from "@aws-sdk/client-s3"
import csv from "csvtojson"
import stream from "stream"
@@ -157,13 +157,20 @@ const SCHEMA: Integration = {
}
class S3Integration implements IntegrationBase {
- private readonly config: S3Config
- private client
+ private readonly config: S3ClientConfig
+ private client: S3
constructor(config: S3Config) {
- this.config = config
- if (this.config.endpoint) {
- this.config.s3ForcePathStyle = true
+ this.config = {
+ forcePathStyle: config.s3ForcePathStyle || true,
+ credentials: {
+ accessKeyId: config.accessKeyId,
+ secretAccessKey: config.secretAccessKey,
+ },
+ region: config.region,
+ }
+ if (config.endpoint) {
+ this.config.forcePathStyle = true
} else {
delete this.config.endpoint
}
@@ -176,7 +183,9 @@ class S3Integration implements IntegrationBase {
connected: false,
}
try {
- await this.client.listBuckets()
+ await this.client.listBuckets({
+ MaxBuckets: 1,
+ })
response.connected = true
} catch (e: any) {
response.error = e.message as string
@@ -253,7 +262,7 @@ class S3Integration implements IntegrationBase {
.on("error", () => {
csvError = true
})
- fileStream.on("finish", () => {
+ fileStream.on("end", () => {
resolve(response)
})
}).catch(err => {
diff --git a/packages/server/src/sdk/app/datasources/datasources.ts b/packages/server/src/sdk/app/datasources/datasources.ts
index 84e1601152..d87ef69205 100644
--- a/packages/server/src/sdk/app/datasources/datasources.ts
+++ b/packages/server/src/sdk/app/datasources/datasources.ts
@@ -120,7 +120,7 @@ export function areRESTVariablesValid(datasource: Datasource) {
export function checkDatasourceTypes(schema: Integration, config: any) {
for (let key of Object.keys(config)) {
- if (!schema.datasource[key]) {
+ if (!schema.datasource?.[key]) {
continue
}
const type = schema.datasource[key].type
@@ -149,7 +149,9 @@ async function enrichDatasourceWithValues(
) as Datasource
processed.entities = entities
const definition = await getDefinition(processed.source)
- processed.config = checkDatasourceTypes(definition!, processed.config)
+ if (definition) {
+ processed.config = checkDatasourceTypes(definition, processed.config)
+ }
return {
datasource: processed,
envVars: env as Record,
diff --git a/packages/types/src/sdk/datasources.ts b/packages/types/src/sdk/datasources.ts
index 1f2a4d2127..349e9fbe0e 100644
--- a/packages/types/src/sdk/datasources.ts
+++ b/packages/types/src/sdk/datasources.ts
@@ -157,7 +157,7 @@ export interface Integration {
friendlyName: string
type?: string
iconUrl?: string
- datasource: DatasourceConfig
+ datasource?: DatasourceConfig
query: {
[key: string]: QueryDefinition
}
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