url bindings test values

This commit is contained in:
Peter Clement 2025-02-11 11:28:14 +00:00
parent 73de5ad1cf
commit c26d9f4e13
7 changed files with 228 additions and 92 deletions

View File

@ -0,0 +1,94 @@
<script>
import { onMount } from "svelte"
import { Input } from "@budibase/bbui"
import { previewStore } from "@/stores/builder"
export let baseRoute = ""
export let testValue = ""
// Extract route parameters (anything starting with :)
$: routeParams = baseRoute.match(/:[a-zA-Z]+/g) || []
$: {
if ($previewStore.selectedComponentContext?.url?.testValue !== undefined) {
testValue = $previewStore.selectedComponentContext.url.testValue
}
}
const onVariableChange = e => {
previewStore.updateUrl({ route: baseRoute, testValue: e.detail })
previewStore.requestComponentContext()
}
onMount(() => {
previewStore.requestComponentContext()
})
</script>
<div class="url-test-section">
<div class="label">URL Variable Testing</div>
<div class="url-pattern">Pattern: {baseRoute}</div>
<div class="url-test-container">
<div class="base-input">
<Input disabled={true} value={`/${baseRoute.split("/")[1]}/`} />
</div>
<div class="variable-input">
<Input
value={testValue}
on:change={onVariableChange}
placeholder={routeParams.length
? `e.g. ${routeParams.map(p => p.slice(1)).join("/")}`
: "Add test values"}
/>
</div>
</div>
</div>
<style>
.url-test-section {
width: 100%;
margin-top: var(--spacing-xl);
}
.label {
font-size: var(--spectrum-global-dimension-font-size-75);
font-weight: 500;
margin-bottom: var(--spacing-s);
}
.url-pattern {
font-size: var(--spectrum-global-dimension-font-size-75);
color: var(--spectrum-global-color-gray-700);
margin-bottom: var(--spacing-xs);
}
.url-test-container {
display: flex;
width: 100%;
}
.base-input {
width: 40%;
margin-right: -1px;
}
.base-input :global(.spectrum-Textfield-input) {
border-top-right-radius: 0;
border-bottom-right-radius: 0;
background-color: var(--spectrum-global-color-gray-200);
color: var(--spectrum-global-color-gray-600);
}
.variable-input {
flex: 1;
}
.variable-input :global(.spectrum-Textfield-input) {
border-top-left-radius: 0;
border-bottom-left-radius: 0;
}
/* Override input styles to make them look connected */
.url-test-container :global(.spectrum-Textfield:focus-within) {
z-index: 1;
}
</style>

View File

@ -15,9 +15,11 @@
import ButtonActionEditor from "@/components/design/settings/controls/ButtonActionEditor/ButtonActionEditor.svelte" import ButtonActionEditor from "@/components/design/settings/controls/ButtonActionEditor/ButtonActionEditor.svelte"
import { getBindableProperties } from "@/dataBinding" import { getBindableProperties } from "@/dataBinding"
import BarButtonList from "@/components/design/settings/controls/BarButtonList.svelte" import BarButtonList from "@/components/design/settings/controls/BarButtonList.svelte"
import URLVariableTestInput from "@/components/design/settings/controls/URLVariableTestInput.svelte"
$: bindings = getBindableProperties($selectedScreen, null) $: bindings = getBindableProperties($selectedScreen, null)
$: screenSettings = getScreenSettings($selectedScreen) $: screenSettings = getScreenSettings($selectedScreen)
let urlTestValue = ""
let errors = {} let errors = {}
@ -93,6 +95,14 @@
], ],
}, },
}, },
{
key: "urlTest",
control: URLVariableTestInput,
props: {
baseRoute: screen.routing?.route,
testValue: urlTestValue,
},
},
] ]
return settings return settings

View File

@ -86,6 +86,10 @@ export class PreviewStore extends BudiStore<PreviewState> {
this.sendEvent("builder-state", data) this.sendEvent("builder-state", data)
} }
updateUrl(data: Record<string, any>) {
this.sendEvent("builder-test-url", data)
}
requestComponentContext() { requestComponentContext() {
this.sendEvent("request-context") this.sendEvent("request-context")
} }

View File

@ -29,6 +29,7 @@
import UserBindingsProvider from "components/context/UserBindingsProvider.svelte" import UserBindingsProvider from "components/context/UserBindingsProvider.svelte"
import DeviceBindingsProvider from "components/context/DeviceBindingsProvider.svelte" import DeviceBindingsProvider from "components/context/DeviceBindingsProvider.svelte"
import StateBindingsProvider from "components/context/StateBindingsProvider.svelte" import StateBindingsProvider from "components/context/StateBindingsProvider.svelte"
import TestUrlBindingsProvider from "components/context/TestUrlBindingsProvider.svelte"
import RowSelectionProvider from "components/context/RowSelectionProvider.svelte" import RowSelectionProvider from "components/context/RowSelectionProvider.svelte"
import QueryParamsProvider from "components/context/QueryParamsProvider.svelte" import QueryParamsProvider from "components/context/QueryParamsProvider.svelte"
import SettingsBar from "components/preview/SettingsBar.svelte" import SettingsBar from "components/preview/SettingsBar.svelte"
@ -168,107 +169,109 @@
<StateBindingsProvider> <StateBindingsProvider>
<RowSelectionProvider> <RowSelectionProvider>
<QueryParamsProvider> <QueryParamsProvider>
<SnippetsProvider> <TestUrlBindingsProvider>
<!-- Settings bar can be rendered outside of device preview --> <SnippetsProvider>
<!-- Key block needs to be outside the if statement or it breaks --> <!-- Settings bar can be rendered outside of device preview -->
{#key $builderStore.selectedComponentId} <!-- Key block needs to be outside the if statement or it breaks -->
{#if $builderStore.inBuilder} {#key $builderStore.selectedComponentId}
<SettingsBar /> {#if $builderStore.inBuilder}
{/if} <SettingsBar />
{/key}
<!-- Clip boundary for selection indicators -->
<div
id="clip-root"
class:preview={$builderStore.inBuilder}
class:tablet-preview={$builderStore.previewDevice ===
"tablet"}
class:mobile-preview={$builderStore.previewDevice ===
"mobile"}
>
<!-- Actual app -->
<div id="app-root">
{#if showDevTools}
<DevToolsHeader />
{/if} {/if}
{/key}
<div id="app-body"> <!-- Clip boundary for selection indicators -->
{#if permissionError} <div
<div class="error"> id="clip-root"
<Layout justifyItems="center" gap="S"> class:preview={$builderStore.inBuilder}
<!-- eslint-disable-next-line svelte/no-at-html-tags --> class:tablet-preview={$builderStore.previewDevice ===
{@html ErrorSVG} "tablet"}
<Heading size="L"> class:mobile-preview={$builderStore.previewDevice ===
You don't have permission to use this app "mobile"}
</Heading> >
<Body size="S"> <!-- Actual app -->
Ask your administrator to grant you access <div id="app-root">
</Body> {#if showDevTools}
</Layout> <DevToolsHeader />
</div>
{:else if !$screenStore.activeLayout}
<div class="error">
<Layout justifyItems="center" gap="S">
<!-- eslint-disable-next-line svelte/no-at-html-tags -->
{@html ErrorSVG}
<Heading size="L">
Something went wrong rendering your app
</Heading>
<Body size="S">
Get in touch with support if this issue
persists
</Body>
</Layout>
</div>
{:else if embedNoScreens}
<div class="error">
<Layout justifyItems="center" gap="S">
<!-- eslint-disable-next-line svelte/no-at-html-tags -->
{@html ErrorSVG}
<Heading size="L">
This Budibase app is not publicly accessible
</Heading>
</Layout>
</div>
{:else}
<CustomThemeWrapper>
{#key $screenStore.activeLayout._id}
<Component
isLayout
instance={$screenStore.activeLayout.props}
/>
{/key}
<!-- Layers on top of app -->
<NotificationDisplay />
<ConfirmationDisplay />
<PeekScreenDisplay />
</CustomThemeWrapper>
{/if} {/if}
{#if showDevTools} <div id="app-body">
<DevTools /> {#if permissionError}
<div class="error">
<Layout justifyItems="center" gap="S">
<!-- eslint-disable-next-line svelte/no-at-html-tags -->
{@html ErrorSVG}
<Heading size="L">
You don't have permission to use this app
</Heading>
<Body size="S">
Ask your administrator to grant you access
</Body>
</Layout>
</div>
{:else if !$screenStore.activeLayout}
<div class="error">
<Layout justifyItems="center" gap="S">
<!-- eslint-disable-next-line svelte/no-at-html-tags -->
{@html ErrorSVG}
<Heading size="L">
Something went wrong rendering your app
</Heading>
<Body size="S">
Get in touch with support if this issue
persists
</Body>
</Layout>
</div>
{:else if embedNoScreens}
<div class="error">
<Layout justifyItems="center" gap="S">
<!-- eslint-disable-next-line svelte/no-at-html-tags -->
{@html ErrorSVG}
<Heading size="L">
This Budibase app is not publicly accessible
</Heading>
</Layout>
</div>
{:else}
<CustomThemeWrapper>
{#key $screenStore.activeLayout._id}
<Component
isLayout
instance={$screenStore.activeLayout.props}
/>
{/key}
<!-- Layers on top of app -->
<NotificationDisplay />
<ConfirmationDisplay />
<PeekScreenDisplay />
</CustomThemeWrapper>
{/if}
{#if showDevTools}
<DevTools />
{/if}
</div>
{#if !$builderStore.inBuilder && $featuresStore.logoEnabled}
<FreeFooter />
{/if} {/if}
</div> </div>
{#if !$builderStore.inBuilder && $featuresStore.logoEnabled} <!-- Preview and dev tools utilities -->
<FreeFooter /> {#if $appStore.isDevApp}
<SelectionIndicator />
{/if}
{#if $builderStore.inBuilder || $devToolsStore.allowSelection}
<HoverIndicator />
{/if}
{#if $builderStore.inBuilder}
<DNDHandler />
<GridDNDHandler />
{/if} {/if}
</div> </div>
</SnippetsProvider>
<!-- Preview and dev tools utilities --> </TestUrlBindingsProvider>
{#if $appStore.isDevApp}
<SelectionIndicator />
{/if}
{#if $builderStore.inBuilder || $devToolsStore.allowSelection}
<HoverIndicator />
{/if}
{#if $builderStore.inBuilder}
<DNDHandler />
<GridDNDHandler />
{/if}
</div>
</SnippetsProvider>
</QueryParamsProvider> </QueryParamsProvider>
</RowSelectionProvider> </RowSelectionProvider>
</StateBindingsProvider> </StateBindingsProvider>

View File

@ -0,0 +1,8 @@
<script>
import Provider from "./Provider.svelte"
import { routeStore } from "stores"
</script>
<Provider key="url" data={$routeStore.testUrlParams}>
<slot />
</Provider>

View File

@ -10,6 +10,7 @@ import {
eventStore, eventStore,
hoverStore, hoverStore,
stateStore, stateStore,
routeStore,
} from "./stores" } from "./stores"
import loadSpectrumIcons from "@budibase/bbui/spectrum-icons-vite.js" import loadSpectrumIcons from "@budibase/bbui/spectrum-icons-vite.js"
import { get } from "svelte/store" import { get } from "svelte/store"
@ -108,6 +109,9 @@ const loadBudibase = async () => {
} else if (type === "builder-state") { } else if (type === "builder-state") {
const [[key, value]] = Object.entries(data) const [[key, value]] = Object.entries(data)
stateStore.actions.setValue(key, value) stateStore.actions.setValue(key, value)
} else if (type === "builder-test-url") {
const { route, testValue } = data
routeStore.actions.setTestUrlParams(route, testValue)
} }
} }

View File

@ -119,7 +119,19 @@ const createRouteStore = () => {
const base = window.location.href.split("#")[0] const base = window.location.href.split("#")[0]
return `${base}#${relativeURL}` return `${base}#${relativeURL}`
} }
const setTestUrlParams = (route: string, testValue: string) => {
const routeSegments = route.split("/").slice(2)
const testSegments = testValue.split("/")
const params: Record<string, string> = {}
routeSegments.forEach((segment, index) => {
if (segment.startsWith(":") && index < testSegments.length) {
params[segment.slice(1)] = testSegments[index]
}
})
store.update(state => ({ ...state, testUrlParams: params }))
}
return { return {
subscribe: store.subscribe, subscribe: store.subscribe,
actions: { actions: {
@ -130,6 +142,7 @@ const createRouteStore = () => {
setQueryParams, setQueryParams,
setActiveRoute, setActiveRoute,
setRouterLoaded, setRouterLoaded,
setTestUrlParams,
}, },
} }
} }