Adding error checking to our handlebars syntax inputs as well as making all handlebars helpers available due to space pre-processor being removed.
This commit is contained in:
parent
b9fb8d6c73
commit
9acd30a4cb
|
@ -2,6 +2,7 @@ import { cloneDeep } from "lodash/fp"
|
||||||
import { get } from "svelte/store"
|
import { get } from "svelte/store"
|
||||||
import { backendUiStore, store } from "builderStore"
|
import { backendUiStore, store } from "builderStore"
|
||||||
import { findAllMatchingComponents, findComponentPath } from "./storeUtils"
|
import { findAllMatchingComponents, findComponentPath } from "./storeUtils"
|
||||||
|
import { makePropSafe } from "@budibase/string-templates"
|
||||||
|
|
||||||
// Regex to match all instances of template strings
|
// Regex to match all instances of template strings
|
||||||
const CAPTURE_VAR_INSIDE_TEMPLATE = /{{([^}]+)}}/g
|
const CAPTURE_VAR_INSIDE_TEMPLATE = /{{([^}]+)}}/g
|
||||||
|
@ -105,7 +106,7 @@ export const getContextBindings = (rootComponent, componentId) => {
|
||||||
|
|
||||||
contextBindings.push({
|
contextBindings.push({
|
||||||
type: "context",
|
type: "context",
|
||||||
runtimeBinding: `${component._id}.${runtimeBoundKey}`,
|
runtimeBinding: `${makePropSafe(component._id)}.${makePropSafe(runtimeBoundKey)}`,
|
||||||
readableBinding: `${component._instanceName}.${table.name}.${key}`,
|
readableBinding: `${component._instanceName}.${table.name}.${key}`,
|
||||||
fieldSchema,
|
fieldSchema,
|
||||||
providerId: component._id,
|
providerId: component._id,
|
||||||
|
@ -135,7 +136,7 @@ export const getComponentBindings = rootComponent => {
|
||||||
return {
|
return {
|
||||||
type: "instance",
|
type: "instance",
|
||||||
providerId: component._id,
|
providerId: component._id,
|
||||||
runtimeBinding: `${component._id}`,
|
runtimeBinding: `${makePropSafe(component._id)}`,
|
||||||
readableBinding: `${component._instanceName}`,
|
readableBinding: `${component._instanceName}`,
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
|
@ -2,6 +2,7 @@ import sanitizeUrl from "./utils/sanitizeUrl"
|
||||||
import { rowListUrl } from "./rowListScreen"
|
import { rowListUrl } from "./rowListScreen"
|
||||||
import { Screen } from "./utils/Screen"
|
import { Screen } from "./utils/Screen"
|
||||||
import { Component } from "./utils/Component"
|
import { Component } from "./utils/Component"
|
||||||
|
import { makePropSafe } from "@budibase/string-templates"
|
||||||
import {
|
import {
|
||||||
makeMainContainer,
|
makeMainContainer,
|
||||||
makeBreadcrumbContainer,
|
makeBreadcrumbContainer,
|
||||||
|
@ -12,7 +13,7 @@ import {
|
||||||
export default function(tables) {
|
export default function(tables) {
|
||||||
return tables.map(table => {
|
return tables.map(table => {
|
||||||
const heading = table.primaryDisplay
|
const heading = table.primaryDisplay
|
||||||
? `{{ data.${table.primaryDisplay} }}`
|
? `{{ data.${makePropSafe(table.primaryDisplay)} }}`
|
||||||
: null
|
: null
|
||||||
return {
|
return {
|
||||||
name: `${table.name} - Detail`,
|
name: `${table.name} - Detail`,
|
||||||
|
@ -60,8 +61,8 @@ function generateTitleContainer(table, title, providerId) {
|
||||||
onClick: [
|
onClick: [
|
||||||
{
|
{
|
||||||
parameters: {
|
parameters: {
|
||||||
rowId: `{{ ${providerId}._id }}`,
|
rowId: `{{ ${makePropSafe(providerId)}._id }}`,
|
||||||
revId: `{{ ${providerId}._rev }}`,
|
revId: `{{ ${makePropSafe(providerId)}._rev }}`,
|
||||||
tableId: table._id,
|
tableId: table._id,
|
||||||
},
|
},
|
||||||
"##eventHandlerType": "Delete Row",
|
"##eventHandlerType": "Delete Row",
|
||||||
|
|
|
@ -10,6 +10,7 @@
|
||||||
Popover,
|
Popover,
|
||||||
} from "@budibase/bbui"
|
} from "@budibase/bbui"
|
||||||
import { createEventDispatcher } from "svelte"
|
import { createEventDispatcher } from "svelte"
|
||||||
|
import { isValid } from "@budibase/string-templates"
|
||||||
const dispatch = createEventDispatcher()
|
const dispatch = createEventDispatcher()
|
||||||
|
|
||||||
export let value = ""
|
export let value = ""
|
||||||
|
@ -19,8 +20,10 @@
|
||||||
export let popover = null
|
export let popover = null
|
||||||
|
|
||||||
let getCaretPosition
|
let getCaretPosition
|
||||||
|
let validity = true
|
||||||
|
|
||||||
$: categories = Object.entries(groupBy("category", bindings))
|
$: categories = Object.entries(groupBy("category", bindings))
|
||||||
|
$: value && checkValid()
|
||||||
|
|
||||||
function onClickBinding(binding) {
|
function onClickBinding(binding) {
|
||||||
const position = getCaretPosition()
|
const position = getCaretPosition()
|
||||||
|
@ -34,6 +37,10 @@
|
||||||
value += toAdd
|
value += toAdd
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function checkValid() {
|
||||||
|
validity = isValid(value)
|
||||||
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<Popover {anchor} {align} bind:this={popover}>
|
<Popover {anchor} {align} bind:this={popover}>
|
||||||
|
@ -70,11 +77,16 @@
|
||||||
bind:getCaretPosition
|
bind:getCaretPosition
|
||||||
bind:value
|
bind:value
|
||||||
placeholder="Add options from the left, type text, or do both" />
|
placeholder="Add options from the left, type text, or do both" />
|
||||||
|
{#if !validity}
|
||||||
|
<p class="syntax-error">Current Handlebars syntax is invalid, please check the guide
|
||||||
|
<a href="https://handlebarsjs.com/guide/">here</a> for more details.
|
||||||
|
</p>
|
||||||
|
{/if}
|
||||||
<div class="controls">
|
<div class="controls">
|
||||||
<a href="https://docs.budibase.com/design/binding">
|
<a href="https://docs.budibase.com/design/binding">
|
||||||
<Body small>Learn more about binding</Body>
|
<Body small>Learn more about binding</Body>
|
||||||
</a>
|
</a>
|
||||||
<Button on:click={popover.hide} primary>Done</Button>
|
<Button on:click={popover.hide} disabled={!validity} primary>Done</Button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
@ -152,4 +164,14 @@
|
||||||
align-items: center;
|
align-items: center;
|
||||||
margin-top: var(--spacing-m);
|
margin-top: var(--spacing-m);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.syntax-error {
|
||||||
|
color: var(--red);
|
||||||
|
font-size: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.syntax-error a {
|
||||||
|
color: var(--red);
|
||||||
|
text-decoration: underline;
|
||||||
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|
|
@ -41,7 +41,7 @@
|
||||||
|
|
||||||
.icon {
|
.icon {
|
||||||
right: 2px;
|
right: 2px;
|
||||||
top: 2px;
|
top: 26px;
|
||||||
bottom: 2px;
|
bottom: 2px;
|
||||||
position: absolute;
|
position: absolute;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
|
|
|
@ -2,12 +2,29 @@
|
||||||
import groupBy from "lodash/fp/groupBy"
|
import groupBy from "lodash/fp/groupBy"
|
||||||
import { Button, TextArea, Drawer, Heading, Spacer } from "@budibase/bbui"
|
import { Button, TextArea, Drawer, Heading, Spacer } from "@budibase/bbui"
|
||||||
import { createEventDispatcher } from "svelte"
|
import { createEventDispatcher } from "svelte"
|
||||||
|
import { isValid } from "@budibase/string-templates"
|
||||||
|
import {getBindableProperties, readableToRuntimeBinding} from "builderStore/dataBinding"
|
||||||
|
import { currentAsset, store } from "../../../builderStore"
|
||||||
|
|
||||||
const dispatch = createEventDispatcher()
|
const dispatch = createEventDispatcher()
|
||||||
export let bindableProperties
|
export let bindableProperties
|
||||||
export let value = ""
|
export let value = ""
|
||||||
export let bindingDrawer
|
export let bindingDrawer
|
||||||
|
|
||||||
|
let validity = true
|
||||||
|
|
||||||
|
$: value && checkValid()
|
||||||
|
$: bindableProperties = getBindableProperties(
|
||||||
|
$currentAsset.props,
|
||||||
|
$store.selectedComponentId
|
||||||
|
)
|
||||||
|
|
||||||
|
function checkValid() {
|
||||||
|
// TODO: need to convert the value to the runtime binding
|
||||||
|
const runtimeBinding = readableToRuntimeBinding(bindableProperties, value)
|
||||||
|
validity = isValid(runtimeBinding)
|
||||||
|
}
|
||||||
|
|
||||||
function addToText(readableBinding) {
|
function addToText(readableBinding) {
|
||||||
value = `${value || ""}{{ ${readableBinding} }}`
|
value = `${value || ""}{{ ${readableBinding} }}`
|
||||||
}
|
}
|
||||||
|
@ -55,6 +72,11 @@
|
||||||
bind:value
|
bind:value
|
||||||
placeholder="Add text, or click the objects on the left to add them to the
|
placeholder="Add text, or click the objects on the left to add them to the
|
||||||
textbox." />
|
textbox." />
|
||||||
|
{#if !validity}
|
||||||
|
<p class="syntax-error">Current Handlebars syntax is invalid, please check the guide
|
||||||
|
<a href="https://handlebarsjs.com/guide/">here</a> for more details.
|
||||||
|
</p>
|
||||||
|
{/if}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
@ -108,4 +130,15 @@
|
||||||
height: 40vh;
|
height: 40vh;
|
||||||
overflow-y: auto;
|
overflow-y: auto;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.syntax-error {
|
||||||
|
padding-top: var(--spacing-m);
|
||||||
|
color: var(--red);
|
||||||
|
font-size: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.syntax-error a {
|
||||||
|
color: var(--red);
|
||||||
|
text-decoration: underline;
|
||||||
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|
|
@ -87,6 +87,7 @@ module.exports.processStringSync = (string, context) => {
|
||||||
if (typeof string !== "string") {
|
if (typeof string !== "string") {
|
||||||
throw "Cannot process non-string types."
|
throw "Cannot process non-string types."
|
||||||
}
|
}
|
||||||
|
|
||||||
let template
|
let template
|
||||||
string = processors.preprocess(string)
|
string = processors.preprocess(string)
|
||||||
// this does not throw an error when template can't be fulfilled, have to try correct beforehand
|
// this does not throw an error when template can't be fulfilled, have to try correct beforehand
|
||||||
|
@ -95,11 +96,26 @@ module.exports.processStringSync = (string, context) => {
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Errors can occur if a user of this library attempts to use a helper that has not been added to the system, these errors
|
* Simple utility function which makes sure that a templating property has been wrapped in literal specifiers correctly.
|
||||||
* can be captured to alert the user of the mistake.
|
* @param {string} property The property which is to be wrapped.
|
||||||
* @param {function} handler a function which will be called every time an error occurs when processing a handlebars
|
* @returns {string} The wrapped property ready to be added to a templating string.
|
||||||
* statement.
|
|
||||||
*/
|
*/
|
||||||
module.exports.errorEvents = handler => {
|
module.exports.makePropSafe = property => {
|
||||||
hbsInstance.registerHelper("helperMissing", handler)
|
return `[${property}]`.replace("[[", "[").replace("]]", "]")
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Checks whether or not a template string contains totally valid syntax (simply tries running it)
|
||||||
|
* @param string The string to test for valid syntax - this may contain no templates and will be considered valid.
|
||||||
|
* @returns {boolean} Whether or not the input string is valid.
|
||||||
|
*/
|
||||||
|
module.exports.isValid = string => {
|
||||||
|
// don't really need a real context to check if its valid
|
||||||
|
const context = {}
|
||||||
|
try {
|
||||||
|
hbsInstance.compile(processors.preprocess(string, false))(context)
|
||||||
|
return true
|
||||||
|
} catch (err) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -17,8 +17,14 @@ function process(string, processors) {
|
||||||
return string
|
return string
|
||||||
}
|
}
|
||||||
|
|
||||||
module.exports.preprocess = string => {
|
module.exports.preprocess = (string, finalise = true) => {
|
||||||
return process(string, preprocessor.processors)
|
let processors = preprocessor.processors
|
||||||
|
// the pre-processor finalisation stops handlebars from ever throwing an error
|
||||||
|
// might want to pre-process for other benefits but still want to see errors
|
||||||
|
if (!finalise) {
|
||||||
|
processors = processors.filter(processor => processor.name !== preprocessor.PreprocessorNames.FINALISE)
|
||||||
|
}
|
||||||
|
return process(string, processors)
|
||||||
}
|
}
|
||||||
|
|
||||||
module.exports.postprocess = string => {
|
module.exports.postprocess = string => {
|
||||||
|
|
|
@ -37,42 +37,7 @@ module.exports.processors = [
|
||||||
return statement
|
return statement
|
||||||
}),
|
}),
|
||||||
|
|
||||||
new Preprocessor(PreprocessorNames.HANDLE_SPACES, statement => {
|
new Preprocessor(PreprocessorNames.FINALISE, statement => {
|
||||||
// exclude helpers and brackets, regex will only find double brackets
|
|
||||||
const exclusions = HelperNames()
|
|
||||||
// find all the parts split by spaces
|
|
||||||
const splitBySpaces = statement
|
|
||||||
.split(" ")
|
|
||||||
.filter(el => el !== "{{" && el !== "}}")
|
|
||||||
// remove braces if they are found and weren't spaced out
|
|
||||||
splitBySpaces[0] = splitBySpaces[0].replace("{", "")
|
|
||||||
splitBySpaces[splitBySpaces.length - 1] = splitBySpaces[
|
|
||||||
splitBySpaces.length - 1
|
|
||||||
].replace("}", "")
|
|
||||||
// remove the excluded elements
|
|
||||||
const propertyParts = splitBySpaces.filter(
|
|
||||||
part => exclusions.indexOf(part) === -1
|
|
||||||
)
|
|
||||||
// rebuild to get the full property
|
|
||||||
const fullProperty = propertyParts.join(" ")
|
|
||||||
// now work out the dot notation layers and split them up
|
|
||||||
const propertyLayers = fullProperty.split(".")
|
|
||||||
// find the layers which need to be wrapped and wrap them
|
|
||||||
for (let layer of propertyLayers) {
|
|
||||||
if (layer.indexOf(" ") !== -1) {
|
|
||||||
statement = swapStrings(
|
|
||||||
statement,
|
|
||||||
statement.indexOf(layer),
|
|
||||||
layer.length,
|
|
||||||
`[${layer}]`
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
// remove the edge case of double brackets being entered (in-case user already has specified)
|
|
||||||
return statement.replace(/\[\[/g, "[").replace(/]]/g, "]")
|
|
||||||
}),
|
|
||||||
|
|
||||||
new Preprocessor(Preprocessor.FINALISE, statement => {
|
|
||||||
let insideStatement = statement.slice(2, statement.length - 2)
|
let insideStatement = statement.slice(2, statement.length - 2)
|
||||||
if (insideStatement.charAt(0) === " ") {
|
if (insideStatement.charAt(0) === " ") {
|
||||||
insideStatement = insideStatement.slice(1)
|
insideStatement = insideStatement.slice(1)
|
||||||
|
@ -87,3 +52,5 @@ module.exports.processors = [
|
||||||
return `{{ all ${insideStatement} }}`
|
return `{{ all ${insideStatement} }}`
|
||||||
}),
|
}),
|
||||||
]
|
]
|
||||||
|
|
||||||
|
module.exports.PreprocessorNames = PreprocessorNames
|
||||||
|
|
|
@ -1,6 +1,8 @@
|
||||||
const {
|
const {
|
||||||
processObject,
|
processObject,
|
||||||
processString,
|
processString,
|
||||||
|
isValid,
|
||||||
|
makePropSafe,
|
||||||
} = require("../src/index")
|
} = require("../src/index")
|
||||||
|
|
||||||
describe("Test that the string processing works correctly", () => {
|
describe("Test that the string processing works correctly", () => {
|
||||||
|
@ -82,3 +84,20 @@ describe("Test that the object processing works correctly", () => {
|
||||||
expect(error).not.toBeNull()
|
expect(error).not.toBeNull()
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
|
describe("check the utility functions", () => {
|
||||||
|
it("should return false for an invalid template string", () => {
|
||||||
|
const valid = isValid("{{ table1.thing prop }}")
|
||||||
|
expect(valid).toBe(false)
|
||||||
|
})
|
||||||
|
|
||||||
|
it("should state this template is valid", () => {
|
||||||
|
const valid = isValid("{{ thing }}")
|
||||||
|
expect(valid).toBe(true)
|
||||||
|
})
|
||||||
|
|
||||||
|
it("should make a property safe", () => {
|
||||||
|
const property = makePropSafe("thing")
|
||||||
|
expect(property).toEqual("[thing]")
|
||||||
|
})
|
||||||
|
})
|
|
@ -18,14 +18,14 @@ describe("Handling context properties with spaces in their name", () => {
|
||||||
})
|
})
|
||||||
|
|
||||||
it("should be able to handle a property with a space in its name", async () => {
|
it("should be able to handle a property with a space in its name", async () => {
|
||||||
const output = await processString("hello my name is {{ person name }}", {
|
const output = await processString("hello my name is {{ [person name] }}", {
|
||||||
"person name": "Mike",
|
"person name": "Mike",
|
||||||
})
|
})
|
||||||
expect(output).toBe("hello my name is Mike")
|
expect(output).toBe("hello my name is Mike")
|
||||||
})
|
})
|
||||||
|
|
||||||
it("should be able to handle an object with layers that requires escaping", async () => {
|
it("should be able to handle an object with layers that requires escaping", async () => {
|
||||||
const output = await processString("testcase {{ testing.test case }}", {
|
const output = await processString("testcase {{ testing.[test case] }}", {
|
||||||
testing: {
|
testing: {
|
||||||
"test case": 1
|
"test case": 1
|
||||||
}
|
}
|
||||||
|
@ -44,8 +44,19 @@ describe("attempt some complex problems", () => {
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
const hbs = "{{ New Repeater.Get Actors.first_name }} {{ New Repeater.Get Actors.last_name }}"
|
const hbs = "{{ [New Repeater].[Get Actors].[first_name] }} {{ [New Repeater].[Get Actors].[last_name] }}"
|
||||||
const output = await processString(hbs, context)
|
const output = await processString(hbs, context)
|
||||||
expect(output).toBe("Bob Bobert")
|
expect(output).toBe("Bob Bobert")
|
||||||
})
|
})
|
||||||
|
|
||||||
|
it("should be able to process an odd string produced by builder", async () => {
|
||||||
|
const context = {
|
||||||
|
"c306d140d7e854f388bae056db380a0eb": {
|
||||||
|
"test prop": "test",
|
||||||
|
}
|
||||||
|
}
|
||||||
|
const hbs = "null{{ [c306d140d7e854f388bae056db380a0eb].[test prop] }}"
|
||||||
|
const output = await processString(hbs, context)
|
||||||
|
expect(output).toBe("nulltest")
|
||||||
|
})
|
||||||
})
|
})
|
Loading…
Reference in New Issue