Merge pull request #1124 from Budibase/url-context

Bindable URL parameters
This commit is contained in:
Andrew Kingston 2021-02-16 16:32:23 +00:00 committed by GitHub
commit f01e9998b8
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
29 changed files with 1058 additions and 1146 deletions

View File

@ -1,5 +1,5 @@
{
"version": "0.7.7",
"version": "0.7.8",
"npmClient": "yarn",
"packages": [
"packages/*"

View File

@ -1,6 +1,6 @@
{
"name": "@budibase/builder",
"version": "0.7.7",
"version": "0.7.8",
"license": "AGPL-3.0",
"private": true,
"scripts": {
@ -64,9 +64,9 @@
},
"dependencies": {
"@budibase/bbui": "^1.58.5",
"@budibase/client": "^0.7.7",
"@budibase/client": "^0.7.8",
"@budibase/colorpicker": "1.0.1",
"@budibase/string-templates": "^0.7.7",
"@budibase/string-templates": "^0.7.8",
"@budibase/svelte-ag-grid": "^0.0.16",
"@sentry/browser": "5.19.1",
"@svelteschool/svelte-forms": "0.7.0",

View File

@ -11,21 +11,24 @@ const CAPTURE_VAR_INSIDE_TEMPLATE = /{{([^}]+)}}/g
/**
* Gets all bindable data context fields and instance fields.
*/
export const getBindableProperties = (rootComponent, componentId) => {
return getContextBindings(rootComponent, componentId)
export const getBindableProperties = (asset, componentId) => {
const contextBindings = getContextBindings(asset, componentId)
const userBindings = getUserBindings()
const urlBindings = getUrlBindings(asset, componentId)
return [...contextBindings, ...userBindings, ...urlBindings]
}
/**
* Gets all data provider components above a component.
*/
export const getDataProviderComponents = (rootComponent, componentId) => {
if (!rootComponent || !componentId) {
export const getDataProviderComponents = (asset, componentId) => {
if (!asset || !componentId) {
return []
}
// Get the component tree leading up to this component, ignoring the component
// itself
const path = findComponentPath(rootComponent, componentId)
const path = findComponentPath(asset.props, componentId)
path.pop()
// Filter by only data provider components
@ -38,18 +41,14 @@ export const getDataProviderComponents = (rootComponent, componentId) => {
/**
* Gets all data provider components above a component.
*/
export const getActionProviderComponents = (
rootComponent,
componentId,
actionType
) => {
if (!rootComponent || !componentId) {
export const getActionProviderComponents = (asset, componentId, actionType) => {
if (!asset || !componentId) {
return []
}
// Get the component tree leading up to this component, ignoring the component
// itself
const path = findComponentPath(rootComponent, componentId)
const path = findComponentPath(asset.props, componentId)
path.pop()
// Filter by only data provider components
@ -92,13 +91,12 @@ export const getDatasourceForProvider = component => {
}
/**
* Gets all bindable data contexts. These are fields of schemas of data contexts
* provided by data provider components, such as lists or row detail components.
* Gets all bindable data properties from component data contexts.
*/
export const getContextBindings = (rootComponent, componentId) => {
const getContextBindings = (asset, componentId) => {
// Extract any components which provide data contexts
const dataProviders = getDataProviderComponents(rootComponent, componentId)
let contextBindings = []
const dataProviders = getDataProviderComponents(asset, componentId)
let bindings = []
// Create bindings for each data provider
dataProviders.forEach(component => {
@ -109,7 +107,7 @@ export const getContextBindings = (rootComponent, componentId) => {
// Forms are an edge case which do not need table schemas
if (isForm) {
schema = buildFormSchema(component)
tableName = "Schema"
tableName = "Fields"
} else {
if (!datasource) {
return
@ -143,7 +141,7 @@ export const getContextBindings = (rootComponent, componentId) => {
runtimeBoundKey = `${key}_first`
}
contextBindings.push({
bindings.push({
type: "context",
runtimeBinding: `${makePropSafe(component._id)}.${makePropSafe(
runtimeBoundKey
@ -157,7 +155,14 @@ export const getContextBindings = (rootComponent, componentId) => {
})
})
// Add logged in user bindings
return bindings
}
/**
* Gets all bindable properties from the logged in user.
*/
const getUserBindings = () => {
let bindings = []
const tables = get(backendUiStore).tables
const userTable = tables.find(table => table._id === TableNames.USERS)
const schema = {
@ -176,7 +181,7 @@ export const getContextBindings = (rootComponent, componentId) => {
runtimeBoundKey = `${key}_first`
}
contextBindings.push({
bindings.push({
type: "context",
runtimeBinding: `user.${runtimeBoundKey}`,
readableBinding: `Current User.${key}`,
@ -187,7 +192,26 @@ export const getContextBindings = (rootComponent, componentId) => {
})
})
return contextBindings
return bindings
}
/**
* Gets all bindable properties from URL parameters.
*/
const getUrlBindings = asset => {
const url = asset?.routing?.route ?? ""
const split = url.split("/")
let params = []
split.forEach(part => {
if (part.startsWith(":") && part.length > 1) {
params.push(part.replace(/:/g, "").replace(/\?/g, ""))
}
})
return params.map(param => ({
type: "context",
runtimeBinding: `url.${param}`,
readableBinding: `URL.${param}`,
}))
}
/**

View File

@ -0,0 +1,84 @@
<script>
import { Icon, Input, Drawer, Body, Button } from "@budibase/bbui"
import {
readableToRuntimeBinding,
runtimeToReadableBinding,
} from "builderStore/dataBinding"
import BindingPanel from "components/design/PropertiesPanel/BindingPanel.svelte"
import { createEventDispatcher } from "svelte"
const dispatch = createEventDispatcher()
export let value = ""
export let bindings = []
let bindingDrawer
let tempValue = value
$: readableValue = runtimeToReadableBinding(bindings, value)
const handleClose = () => {
onChange(tempValue)
bindingDrawer.hide()
}
const onChange = value => {
dispatch("change", readableToRuntimeBinding(bindings, value))
}
</script>
<div class="control">
<Input
thin
value={readableValue}
on:change={event => onChange(event.target.value)}
placeholder="/screen" />
<div class="icon" on:click={bindingDrawer.show}>
<Icon name="lightning" />
</div>
</div>
<Drawer bind:this={bindingDrawer} title="Bindings">
<div slot="description">
<Body extraSmall grey>
Add the objects on the left to enrich your text.
</Body>
</div>
<heading slot="buttons">
<Button thin blue on:click={handleClose}>Save</Button>
</heading>
<div slot="body">
<BindingPanel
value={readableValue}
close={handleClose}
on:update={event => (tempValue = event.detail)}
bindableProperties={bindings} />
</div>
</Drawer>
<style>
.control {
flex: 1;
margin-left: var(--spacing-l);
position: relative;
}
.icon {
right: 2px;
top: 2px;
bottom: 2px;
position: absolute;
align-items: center;
display: flex;
box-sizing: border-box;
padding-left: 7px;
border-left: 1px solid var(--grey-4);
background-color: var(--grey-2);
border-top-right-radius: var(--border-radius-m);
border-bottom-right-radius: var(--border-radius-m);
color: var(--grey-7);
font-size: 14px;
}
.icon:hover {
color: var(--ink);
cursor: pointer;
}
</style>

View File

@ -24,7 +24,7 @@
$: value && checkValid()
$: bindableProperties = getBindableProperties(
$currentAsset.props,
$currentAsset,
$store.selectedComponentId
)
$: dispatch("update", value)

View File

@ -47,7 +47,7 @@
type: "query",
}))
$: bindableProperties = getBindableProperties(
$currentAsset.props,
$currentAsset,
$store.selectedComponentId
)
$: queryBindableProperties = bindableProperties.map(property => ({

View File

@ -10,7 +10,7 @@
export let parameters
$: dataProviderComponents = getDataProviderComponents(
$currentAsset.props,
$currentAsset,
$store.selectedComponentId
)
$: {

View File

@ -10,7 +10,7 @@
ds => ds._id === parameters.datasourceId
)
$: bindableProperties = getBindableProperties(
$currentAsset.props,
$currentAsset,
$store.selectedComponentId
).map(property => ({
...property,

View File

@ -1,18 +1,23 @@
<script>
import { DataList, Label } from "@budibase/bbui"
import { allScreens } from "builderStore"
import { Label } from "@budibase/bbui"
import { getBindableProperties } from "builderStore/dataBinding"
import { currentAsset, store } from "builderStore"
import DrawerBindableInput from "components/common/DrawerBindableInput.svelte"
export let parameters
let bindingDrawer
let tempValue = parameters.url
$: bindings = getBindableProperties($currentAsset, $store.selectedComponentId)
</script>
<div class="root">
<Label size="m" color="dark">Screen</Label>
<DataList secondary bind:value={parameters.url}>
<option value="" />
{#each $allScreens as screen}
<option value={screen.routing.route}>{screen.props._instanceName}</option>
{/each}
</DataList>
<DrawerBindableInput
value={parameters.url}
on:change={value => (parameters.url = value.detail)}
{bindings} />
</div>
<style>

View File

@ -6,7 +6,7 @@
export let parameters
$: dataProviders = getDataProviderComponents(
$currentAsset.props,
$currentAsset,
$store.selectedComponentId
)
</script>

View File

@ -25,7 +25,7 @@
const emptyField = () => ({ name: "", value: "" })
$: bindableProperties = getBindableProperties(
$currentAsset.props,
$currentAsset,
$store.selectedComponentId
)

View File

@ -11,7 +11,7 @@
export let parameters
$: dataProviderComponents = getDataProviderComponents(
$currentAsset.props,
$currentAsset,
$store.selectedComponentId
)
$: providerComponent = dataProviderComponents.find(

View File

@ -6,7 +6,7 @@
export let parameters
$: actionProviders = getActionProviderComponents(
$currentAsset.props,
$currentAsset,
$store.selectedComponentId,
"ValidateForm"
)

View File

@ -24,7 +24,7 @@
let valid
$: bindableProperties = getBindableProperties(
$currentAsset.props,
$currentAsset,
$store.selectedComponentId
)
$: safeValue = getSafeValue(value, props.defaultValue, bindableProperties)

View File

@ -1,80 +0,0 @@
<script>
import { DataList } from "@budibase/bbui"
import { createEventDispatcher } from "svelte"
import { store, allScreens, currentAsset } from "builderStore"
import { getBindableProperties } from "builderStore/dataBinding"
export let value = ""
$: urls = getUrls($allScreens, $currentAsset, $store.selectedComponentId)
// Update value on blur
const dispatch = createEventDispatcher()
const handleBlur = () => dispatch("change", value)
// Get all valid screen URL, as well as detail screens which can be used in
// the current data context
const getUrls = (screens, asset, componentId) => {
// Get all screens which aren't detail screens
let urls = screens
.filter(screen => !screen.props._component.endsWith("/rowdetail"))
.map(screen => ({
name: screen.props._instanceName,
url: screen.routing.route,
sort: screen.props._component,
}))
// Add detail screens enriched with the current data context
const bindableProperties = getBindableProperties(asset.props, componentId)
screens
.filter(screen => screen.props._component.endsWith("/rowdetail"))
.forEach(detailScreen => {
// Find any _id bindings that match the detail screen's table
const binding = bindableProperties.find(p => {
return (
p.type === "context" &&
p.runtimeBinding.endsWith("._id") &&
p.tableId === detailScreen.props.table
)
})
if (binding) {
urls.push({
name: detailScreen.props._instanceName,
url: detailScreen.routing.route.replace(
":id",
`{{ ${binding.runtimeBinding} }}`
),
sort: detailScreen.props._component,
})
}
})
return urls
}
</script>
<div>
<DataList
editable
secondary
extraThin
on:blur={handleBlur}
on:change
bind:value>
<option value="" />
{#each urls as url}
<option value={url.url}>{url.name}</option>
{/each}
</DataList>
</div>
<style>
div {
flex: 1 1 auto;
display: flex;
flex-direction: row;
}
div :global(> div) {
flex: 1 1 auto;
}
</style>

View File

@ -18,7 +18,6 @@
import MultiFieldSelect from "./PropertyControls/MultiFieldSelect.svelte"
import SchemaSelect from "./PropertyControls/SchemaSelect.svelte"
import EventsEditor from "./PropertyControls/EventsEditor"
import ScreenSelect from "./PropertyControls/ScreenSelect.svelte"
import DetailScreenSelect from "./PropertyControls/DetailScreenSelect.svelte"
import { IconSelect } from "./PropertyControls/IconSelect"
import ColorPicker from "./PropertyControls/ColorPicker.svelte"
@ -62,7 +61,6 @@
text: Input,
select: OptionSelect,
datasource: DatasourceSelect,
screen: ScreenSelect,
detailScreen: DetailScreenSelect,
boolean: Checkbox,
number: Input,

File diff suppressed because it is too large Load Diff

View File

@ -1,6 +1,6 @@
{
"name": "@budibase/client",
"version": "0.7.7",
"version": "0.7.8",
"license": "MPL-2.0",
"main": "dist/budibase-client.js",
"module": "dist/budibase-client.js",
@ -9,14 +9,14 @@
"dev:builder": "rollup -cw"
},
"dependencies": {
"@budibase/string-templates": "^0.7.7",
"@budibase/string-templates": "^0.7.8",
"deep-equal": "^2.0.1",
"regexparam": "^1.3.0",
"shortid": "^2.2.15",
"svelte-spa-router": "^3.0.5"
},
"devDependencies": {
"@budibase/standard-components": "^0.7.7",
"@budibase/standard-components": "^0.7.8",
"@rollup/plugin-commonjs": "^16.0.0",
"@rollup/plugin-node-resolve": "^10.0.0",
"fs-extra": "^8.1.0",

View File

@ -1,5 +1,5 @@
<script>
import { getContext, setContext } from "svelte"
import { getContext } from "svelte"
import Router from "svelte-spa-router"
import { routeStore } from "../store"
import Screen from "./Screen.svelte"
@ -10,12 +10,10 @@
// Only wrap this as an array to take advantage of svelte keying,
// to ensure the svelte-spa-router is fully remounted when route config
// changes
$: configs = [
{
$: config = {
routes: getRouterConfig($routeStore.routes),
id: $routeStore.routeSessionId,
},
]
}
const getRouterConfig = routes => {
let config = {}
@ -33,11 +31,11 @@
}
</script>
{#each configs as config (config.id)}
{#key config.id}
<div use:styleable={$component.styles}>
<Router on:routeLoading={onRouteLoading} routes={config.routes} />
</div>
{/each}
{/key}
<style>
div {

View File

@ -2,6 +2,7 @@
import { fade } from "svelte/transition"
import { screenStore, routeStore } from "../store"
import Component from "./Component.svelte"
import Provider from "./Provider.svelte"
// Keep route params up to date
export let params = {}
@ -12,16 +13,16 @@
// Redirect to home layout if no matching route
$: screenDefinition == null && routeStore.actions.navigate("/")
// Make a screen array so we can use keying to properly re-render each screen
$: screens = screenDefinition ? [screenDefinition] : []
</script>
{#each screens as screen (screen._id)}
<!-- Ensure to fully remount when screen changes -->
{#key screenDefinition?._id}
<Provider key="url" data={params}>
<div in:fade>
<Component definition={screen} />
<Component definition={screenDefinition} />
</div>
{/each}
</Provider>
{/key}
<style>
div {

View File

@ -44,7 +44,7 @@ export const enrichProps = async (props, context) => {
let enrichedProps = await enrichDataBindings(validProps, totalContext)
// Enrich button actions if they exist
if (props._component.endsWith("/button") && enrichedProps.onClick) {
if (props._component?.endsWith("/button") && enrichedProps.onClick) {
enrichedProps.onClick = enrichButtonActions(
enrichedProps.onClick,
totalContext

View File

@ -1,7 +1,7 @@
{
"name": "@budibase/server",
"email": "hi@budibase.com",
"version": "0.7.7",
"version": "0.7.8",
"description": "Budibase Web Server",
"main": "src/electron.js",
"repository": {
@ -50,8 +50,8 @@
"author": "Budibase",
"license": "AGPL-3.0-or-later",
"dependencies": {
"@budibase/client": "^0.7.7",
"@budibase/string-templates": "^0.7.7",
"@budibase/client": "^0.7.8",
"@budibase/string-templates": "^0.7.8",
"@elastic/elasticsearch": "7.10.0",
"@koa/router": "8.0.0",
"@sendgrid/mail": "7.1.1",

View File

@ -176,9 +176,10 @@
"key": "subheading"
},
{
"type": "screen",
"type": "text",
"label": "Link URL",
"key": "destinationUrl"
"key": "destinationUrl",
"placeholder": "/screen"
}
]
},
@ -209,9 +210,10 @@
"key": "linkText"
},
{
"type": "screen",
"label": "Link Url",
"key": "linkUrl"
"type": "text",
"label": "Link URL",
"key": "linkUrl",
"placeholder": "/screen"
},
{
"type": "color",
@ -378,9 +380,10 @@
"key": "text"
},
{
"type": "screen",
"type": "text",
"label": "URL",
"key": "url"
"key": "url",
"placeholder": "/screen"
},
{
"type": "boolean",
@ -451,9 +454,10 @@
"key": "linkText"
},
{
"type": "screen",
"type": "text",
"label": "Link URL",
"key": "linkUrl"
"key": "linkUrl",
"placeholder": "/screen"
},
{
"type": "color",

View File

@ -35,7 +35,7 @@
"keywords": [
"svelte"
],
"version": "0.7.7",
"version": "0.7.8",
"license": "MIT",
"gitHead": "1a80b09fd093f2599a68f7db72ad639dd50922dd",
"dependencies": {

View File

@ -1,59 +0,0 @@
<script>
import { Label, Multiselect } from "@budibase/bbui"
import { capitalise } from "./helpers"
import { getContext } from "svelte"
const { API } = getContext("sdk")
export let schema = {}
export let linkedRows = []
export let showLabel = true
export let secondary
let linkedTable
let allRows = []
$: label = capitalise(schema.name)
$: linkedTableId = schema.tableId
$: fetchRows(linkedTableId)
$: fetchTable(linkedTableId)
async function fetchTable(id) {
if (id != null) {
linkedTable = await API.fetchTableDefinition(id)
}
}
async function fetchRows(id) {
if (id != null) {
allRows = await API.fetchTableData(id)
}
}
function getPrettyName(row) {
return row[(linkedTable && linkedTable.primaryDisplay) || "_id"]
}
</script>
{#if linkedTable != null}
{#if linkedTable.primaryDisplay == null}
{#if showLabel}
<Label extraSmall grey>{label}</Label>
{/if}
<Label small black>
Please choose a display column for the
<b>{linkedTable.name}</b>
table.
</Label>
{:else}
<Multiselect
{secondary}
bind:value={linkedRows}
label={showLabel ? label : null}
placeholder="Choose some options">
{#each allRows as row}
<option value={row._id}>{getPrettyName(row)}</option>
{/each}
</Multiselect>
{/if}
{/if}

View File

@ -18,16 +18,16 @@
let table
let fieldMap = {}
// Checks if the closest data context matches the model for this forms
// datasource, and use it as the initial form values if so
// Returns the closes data context which isn't a built in context
const getInitialValues = context => {
return context && context.tableId === datasource?.tableId ? context : {}
if (["user", "url"].includes(context.closestComponentId)) {
return {}
}
return context[`${context.closestComponentId}`] || {}
}
// Use the closest data context as the initial form values if it matches
const initialValues = getInitialValues(
$context[`${$context.closestComponentId}`]
)
// Use the closest data context as the initial form values
const initialValues = getInitialValues($context)
// Form state contains observable data about the form
const formState = writable({ values: initialValues, errors: {}, valid: true })

View File

@ -24,7 +24,7 @@
$: fetchTable(linkedTableId)
const fetchTable = async id => {
if (id != null) {
if (id) {
const result = await API.fetchTableDefinition(id)
if (!result.error) {
tableDefinition = result
@ -33,7 +33,7 @@
}
const fetchRows = async id => {
if (id != null) {
if (id) {
const rows = await API.fetchTableData(id)
options = rows && !rows.error ? rows : []
}

View File

@ -1,6 +1,6 @@
{
"name": "@budibase/string-templates",
"version": "0.7.7",
"version": "0.7.8",
"description": "Handlebars wrapper for Budibase templating.",
"main": "src/index.js",
"module": "src/index.js",

View File

@ -1,7 +1,7 @@
{
"name": "@budibase/deployment",
"email": "hi@budibase.com",
"version": "0.7.7",
"version": "0.7.8",
"description": "Budibase Deployment Server",
"main": "src/index.js",
"repository": {