diff --git a/lerna.json b/lerna.json index 18c131fe56..afcb33918b 100644 --- a/lerna.json +++ b/lerna.json @@ -1,6 +1,6 @@ { "$schema": "node_modules/lerna/schemas/lerna-schema.json", - "version": "3.4.7", + "version": "3.4.9", "npmClient": "yarn", "concurrency": 20, "command": { diff --git a/packages/backend-core/src/utils/utils.ts b/packages/backend-core/src/utils/utils.ts index 30cf55b149..7f2e25b6d4 100644 --- a/packages/backend-core/src/utils/utils.ts +++ b/packages/backend-core/src/utils/utils.ts @@ -247,3 +247,7 @@ export function hasCircularStructure(json: any) { } return false } + +export function urlHasProtocol(url: string): boolean { + return !!url.match(/^.+:\/\/.+$/) +} diff --git a/packages/bbui/src/Typography/Body.svelte b/packages/bbui/src/Typography/Body.svelte index 2123eeee95..06664c9033 100644 --- a/packages/bbui/src/Typography/Body.svelte +++ b/packages/bbui/src/Typography/Body.svelte @@ -1,11 +1,11 @@ -

+ import { onMount } from "svelte" + import { Input, Label } from "@budibase/bbui" + import { previewStore, selectedScreen } from "@/stores/builder" + import type { ComponentContext } from "@budibase/types" + + export let baseRoute = "" + + let testValue: string | undefined + + $: routeParams = baseRoute.match(/:[a-zA-Z]+/g) || [] + $: hasUrlParams = routeParams.length > 0 + $: placeholder = getPlaceholder(baseRoute) + $: baseInput = createBaseInput(baseRoute) + $: updateTestValueFromContext($previewStore.selectedComponentContext) + $: if ($selectedScreen) { + testValue = "" + } + + const getPlaceholder = (route: string) => { + const trimmed = route.replace(/\/$/, "") + if (trimmed.startsWith("/:")) { + return "1" + } + const segments = trimmed.split("/").slice(2) + let count = 1 + return segments + .map(segment => (segment.startsWith(":") ? count++ : segment)) + .join("/") + } + + // This function is needed to repopulate the test value from componentContext + // when a user navigates to another component and then back again + const updateTestValueFromContext = (context: ComponentContext | null) => { + if (context?.url && !testValue) { + const { wild, ...urlParams } = context.url + const queryParams = context.query + if (Object.values(urlParams).some(v => Boolean(v))) { + let value = baseRoute + .split("/") + .slice(2) + .map(segment => + segment.startsWith(":") + ? urlParams[segment.slice(1)] || "" + : segment + ) + .join("/") + const qs = new URLSearchParams(queryParams).toString() + if (qs) { + value += `?${qs}` + } + testValue = value + } + } + } + + const createBaseInput = (baseRoute: string) => { + return baseRoute === "/" || baseRoute.split("/")[1]?.startsWith(":") + ? "/" + : `/${baseRoute.split("/")[1]}/` + } + + const onVariableChange = (e: CustomEvent) => { + previewStore.setUrlTestData({ route: baseRoute, testValue: e.detail }) + } + + onMount(() => { + previewStore.requestComponentContext() + }) + + +{#if hasUrlParams} +

+
+ +
+
+
+ +
+
+ +
+
+
+{/if} + + diff --git a/packages/builder/src/pages/builder/app/[application]/design/[screenId]/[componentId]/_components/Screen/GeneralPanel.svelte b/packages/builder/src/pages/builder/app/[application]/design/[screenId]/[componentId]/_components/Screen/GeneralPanel.svelte index 3a6e7a702c..31479bc820 100644 --- a/packages/builder/src/pages/builder/app/[application]/design/[screenId]/[componentId]/_components/Screen/GeneralPanel.svelte +++ b/packages/builder/src/pages/builder/app/[application]/design/[screenId]/[componentId]/_components/Screen/GeneralPanel.svelte @@ -15,6 +15,7 @@ import ButtonActionEditor from "@/components/design/settings/controls/ButtonActionEditor/ButtonActionEditor.svelte" import { getBindableProperties } from "@/dataBinding" import BarButtonList from "@/components/design/settings/controls/BarButtonList.svelte" + import URLVariableTestInput from "@/components/design/settings/controls/URLVariableTestInput.svelte" $: bindings = getBindableProperties($selectedScreen, null) $: screenSettings = getScreenSettings($selectedScreen) @@ -93,6 +94,13 @@ ], }, }, + { + key: "urlTest", + control: URLVariableTestInput, + props: { + baseRoute: screen.routing?.route, + }, + }, ] return settings diff --git a/packages/builder/src/stores/builder/preview.ts b/packages/builder/src/stores/builder/preview.ts index ef38b099cf..0fef91d6b9 100644 --- a/packages/builder/src/stores/builder/preview.ts +++ b/packages/builder/src/stores/builder/preview.ts @@ -1,9 +1,8 @@ import { get } from "svelte/store" import { BudiStore } from "../BudiStore" -import { PreviewDevice } from "@budibase/types" +import { PreviewDevice, ComponentContext } from "@budibase/types" type PreviewEventHandler = (name: string, payload?: any) => void -type ComponentContext = Record interface PreviewState { previewDevice: PreviewDevice @@ -86,6 +85,10 @@ export class PreviewStore extends BudiStore { this.sendEvent("builder-state", data) } + setUrlTestData(data: Record) { + this.sendEvent("builder-url-test-data", data) + } + requestComponentContext() { this.sendEvent("request-context") } diff --git a/packages/client/src/components/ClientApp.svelte b/packages/client/src/components/ClientApp.svelte index b1ee03e84a..d10ec2c997 100644 --- a/packages/client/src/components/ClientApp.svelte +++ b/packages/client/src/components/ClientApp.svelte @@ -29,6 +29,7 @@ import UserBindingsProvider from "./context/UserBindingsProvider.svelte" import DeviceBindingsProvider from "./context/DeviceBindingsProvider.svelte" import StateBindingsProvider from "./context/StateBindingsProvider.svelte" + import TestUrlBindingsProvider from "./context/TestUrlBindingsProvider.svelte" import RowSelectionProvider from "./context/RowSelectionProvider.svelte" import QueryParamsProvider from "./context/QueryParamsProvider.svelte" import SettingsBar from "./preview/SettingsBar.svelte" @@ -169,108 +170,110 @@ - - - - {#key $builderStore.selectedComponentId} - {#if $builderStore.inBuilder} - - {/if} - {/key} - - -
- -
- {#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