diff --git a/packages/builder/src/builderStore/dataBinding.js b/packages/builder/src/builderStore/dataBinding.js index 679a77ca2b..79785f4e8d 100644 --- a/packages/builder/src/builderStore/dataBinding.js +++ b/packages/builder/src/builderStore/dataBinding.js @@ -2,6 +2,7 @@ import { cloneDeep } from "lodash/fp" import { get } from "svelte/store" import { backendUiStore, store } from "builderStore" import { findAllMatchingComponents, findComponentPath } from "./storeUtils" +import { makePropSafe } from "@budibase/string-templates" import { TableNames } from "../constants" // Regex to match all instances of template strings @@ -106,7 +107,9 @@ export const getContextBindings = (rootComponent, componentId) => { contextBindings.push({ type: "context", - runtimeBinding: `${component._id}.${runtimeBoundKey}`, + runtimeBinding: `${makePropSafe(component._id)}.${makePropSafe( + runtimeBoundKey + )}`, readableBinding: `${component._instanceName}.${table.name}.${key}`, fieldSchema, providerId: component._id, @@ -167,7 +170,7 @@ export const getComponentBindings = rootComponent => { return { type: "instance", providerId: component._id, - runtimeBinding: `${component._id}`, + runtimeBinding: `${makePropSafe(component._id)}`, readableBinding: `${component._instanceName}`, } }) @@ -199,43 +202,52 @@ export const getSchemaForDatasource = datasource => { } /** - * Converts a readable data binding into a runtime data binding + * utility function for the readableToRuntimeBinding and runtimeToReadableBinding. */ -export function readableToRuntimeBinding(bindableProperties, textWithBindings) { +function bindingReplacement(bindableProperties, textWithBindings, convertTo) { + const convertFrom = + convertTo === "runtimeBinding" ? "readableBinding" : "runtimeBinding" if (typeof textWithBindings !== "string") { return textWithBindings } + const convertFromProps = bindableProperties + .map(el => el[convertFrom]) + .sort((a, b) => { + return b.length - a.length + }) const boundValues = textWithBindings.match(CAPTURE_VAR_INSIDE_TEMPLATE) || [] let result = textWithBindings - boundValues.forEach(boundValue => { - const binding = bindableProperties.find(({ readableBinding }) => { - return boundValue === `{{ ${readableBinding} }}` - }) - if (binding) { - result = result.replace(boundValue, `{{ ${binding.runtimeBinding} }}`) + for (let boundValue of boundValues) { + let newBoundValue = boundValue + for (let from of convertFromProps) { + if (newBoundValue.includes(from)) { + const binding = bindableProperties.find(el => el[convertFrom] === from) + newBoundValue = newBoundValue.replace(from, binding[convertTo]) + } } - }) + result = result.replace(boundValue, newBoundValue) + } return result } +/** + * Converts a readable data binding into a runtime data binding + */ +export function readableToRuntimeBinding(bindableProperties, textWithBindings) { + return bindingReplacement( + bindableProperties, + textWithBindings, + "runtimeBinding" + ) +} + /** * Converts a runtime data binding into a readable data binding */ export function runtimeToReadableBinding(bindableProperties, textWithBindings) { - if (typeof textWithBindings !== "string") { - return textWithBindings - } - const boundValues = textWithBindings.match(CAPTURE_VAR_INSIDE_TEMPLATE) || [] - let result = textWithBindings - boundValues.forEach(boundValue => { - const binding = bindableProperties.find(({ runtimeBinding }) => { - return boundValue === `{{ ${runtimeBinding} }}` - }) - // Show invalid bindings as invalid rather than a long ID - result = result.replace( - boundValue, - `{{ ${binding?.readableBinding ?? "Invalid binding"} }}` - ) - }) - return result + return bindingReplacement( + bindableProperties, + textWithBindings, + "readableBinding" + ) } diff --git a/packages/builder/src/builderStore/store/screenTemplates/rowDetailScreen.js b/packages/builder/src/builderStore/store/screenTemplates/rowDetailScreen.js index 0a9148eaf8..fd54405875 100644 --- a/packages/builder/src/builderStore/store/screenTemplates/rowDetailScreen.js +++ b/packages/builder/src/builderStore/store/screenTemplates/rowDetailScreen.js @@ -2,6 +2,7 @@ import sanitizeUrl from "./utils/sanitizeUrl" import { rowListUrl } from "./rowListScreen" import { Screen } from "./utils/Screen" import { Component } from "./utils/Component" +import { makePropSafe } from "@budibase/string-templates" import { makeMainContainer, makeBreadcrumbContainer, @@ -12,7 +13,7 @@ import { export default function(tables) { return tables.map(table => { const heading = table.primaryDisplay - ? `{{ data.${table.primaryDisplay} }}` + ? `{{ data.${makePropSafe(table.primaryDisplay)} }}` : null return { name: `${table.name} - Detail`, @@ -60,8 +61,8 @@ function generateTitleContainer(table, title, providerId) { onClick: [ { parameters: { - rowId: `{{ ${providerId}._id }}`, - revId: `{{ ${providerId}._rev }}`, + rowId: `{{ ${makePropSafe(providerId)}._id }}`, + revId: `{{ ${makePropSafe(providerId)}._rev }}`, tableId: table._id, }, "##eventHandlerType": "Delete Row", diff --git a/packages/builder/src/components/automation/SetupPanel/GenericBindingPopover.svelte b/packages/builder/src/components/automation/SetupPanel/GenericBindingPopover.svelte index a326f60a08..24b6042940 100644 --- a/packages/builder/src/components/automation/SetupPanel/GenericBindingPopover.svelte +++ b/packages/builder/src/components/automation/SetupPanel/GenericBindingPopover.svelte @@ -3,6 +3,7 @@ import { TextArea, Label, + Input, Heading, Body, Spacer, @@ -10,6 +11,9 @@ Popover, } from "@budibase/bbui" import { createEventDispatcher } from "svelte" + import { isValid } from "@budibase/string-templates" + import { handlebarsCompletions } from "constants/completions" + const dispatch = createEventDispatcher() export let value = "" @@ -18,9 +22,14 @@ export let align export let popover = null + let helpers = handlebarsCompletions() let getCaretPosition + let validity = true + let search = "" $: categories = Object.entries(groupBy("category", bindings)) + $: value && checkValid() + $: searchRgx = new RegExp(search, "ig") function onClickBinding(binding) { const position = getCaretPosition() @@ -34,18 +43,27 @@ value += toAdd } } + + function checkValid() { + validity = isValid(value) + }
Available bindings + + +
{#each categories as [categoryName, bindings]} {categoryName} - {#each bindings as binding} + {#each bindings.filter(binding => + binding.label.match(searchRgx) + ) as binding}
onClickBinding(binding)}> {binding.label} {binding.type} @@ -56,6 +74,17 @@
{/each} {/each} + Helpers + + {#each helpers.filter(helper => helper.label.match(searchRgx) || helper.description.match(searchRgx)) as helper} +
onClickBinding(helper)}> + {helper.label} +
+
+ {@html helper.description || ''} +
+
+ {/each}
@@ -70,11 +99,20 @@ bind:getCaretPosition bind:value placeholder="Add options from the left, type text, or do both" /> + {#if !validity} +

+ Current Handlebars syntax is invalid, please check the guide + here + for more details. +

+ {/if}
Learn more about binding - +
@@ -152,4 +190,14 @@ align-items: center; margin-top: var(--spacing-m); } + + .syntax-error { + color: var(--red); + font-size: 12px; + } + + .syntax-error a { + color: var(--red); + text-decoration: underline; + } diff --git a/packages/builder/src/components/backend/DataTable/modals/EditRoles.svelte b/packages/builder/src/components/backend/DataTable/modals/EditRoles.svelte index 981c1297e6..d8f094b3cb 100644 --- a/packages/builder/src/components/backend/DataTable/modals/EditRoles.svelte +++ b/packages/builder/src/components/backend/DataTable/modals/EditRoles.svelte @@ -9,7 +9,7 @@ let permissions = [] let selectedRole = {} let errors = [] - let builtInRoles = ['Admin', 'Power', 'Basic', 'Public'] + let builtInRoles = ["Admin", "Power", "Basic", "Public"] $: selectedRoleId = selectedRole._id $: otherRoles = $backendUiStore.roles.filter( role => role._id !== selectedRoleId @@ -103,7 +103,11 @@ {/each} {#if selectedRole} - + + {#if context} Columns
    - {#each context as { readableBinding }} + {#each context.filter(context => + context.readableBinding.match(searchRgx) + ) as { readableBinding }}
  • addToText(readableBinding)}> {readableBinding}
  • {/each}
{/if} + {#if instance} Components +
    - {#each instance as { readableBinding }} + {#each instance.filter(instance => + instance.readableBinding.match(searchRgx) + ) as { readableBinding }}
  • addToText(readableBinding)}> {readableBinding}
  • {/each}
{/if} + + Helpers + +
    + {#each helpers.filter(helper => helper.label.match(searchRgx) || helper.description.match(searchRgx)) as helper} +
  • addToText(helper.text)}> +
    + +
    + {@html helper.description} +
    +
    +
  • + {/each} +