Merge branch 'master' into feature/client-feedback-component
This commit is contained in:
commit
e411ac5b3d
|
@ -66,6 +66,7 @@
|
||||||
"@budibase/bbui": "^1.54.0",
|
"@budibase/bbui": "^1.54.0",
|
||||||
"@budibase/client": "^0.5.3",
|
"@budibase/client": "^0.5.3",
|
||||||
"@budibase/colorpicker": "^1.0.1",
|
"@budibase/colorpicker": "^1.0.1",
|
||||||
|
"@budibase/string-templates": "^0.5.3",
|
||||||
"@budibase/svelte-ag-grid": "^0.0.16",
|
"@budibase/svelte-ag-grid": "^0.0.16",
|
||||||
"@sentry/browser": "5.19.1",
|
"@sentry/browser": "5.19.1",
|
||||||
"@svelteschool/svelte-forms": "^0.7.0",
|
"@svelteschool/svelte-forms": "^0.7.0",
|
||||||
|
@ -75,7 +76,6 @@
|
||||||
"deepmerge": "^4.2.2",
|
"deepmerge": "^4.2.2",
|
||||||
"fast-sort": "^2.2.0",
|
"fast-sort": "^2.2.0",
|
||||||
"lodash": "^4.17.13",
|
"lodash": "^4.17.13",
|
||||||
"mustache": "^4.0.1",
|
|
||||||
"posthog-js": "1.4.5",
|
"posthog-js": "1.4.5",
|
||||||
"remixicon": "^2.5.0",
|
"remixicon": "^2.5.0",
|
||||||
"shortid": "^2.2.15",
|
"shortid": "^2.2.15",
|
||||||
|
|
|
@ -1,8 +1,8 @@
|
||||||
export const CAPTURE_VAR_INSIDE_MUSTACHE = /{{([^}]+)}}/g
|
export const CAPTURE_VAR_INSIDE_TEMPLATE = /{{([^}]+)}}/g
|
||||||
|
|
||||||
export function readableToRuntimeBinding(bindableProperties, textWithBindings) {
|
export function readableToRuntimeBinding(bindableProperties, textWithBindings) {
|
||||||
// Find all instances of mustasche
|
// Find all instances of template strings
|
||||||
const boundValues = textWithBindings.match(CAPTURE_VAR_INSIDE_MUSTACHE)
|
const boundValues = textWithBindings.match(CAPTURE_VAR_INSIDE_TEMPLATE)
|
||||||
|
|
||||||
let result = textWithBindings
|
let result = textWithBindings
|
||||||
// Replace readableBindings with runtimeBindings
|
// Replace readableBindings with runtimeBindings
|
||||||
|
@ -22,7 +22,7 @@ export function runtimeToReadableBinding(bindableProperties, textWithBindings) {
|
||||||
let temp = textWithBindings
|
let temp = textWithBindings
|
||||||
const boundValues =
|
const boundValues =
|
||||||
(typeof textWithBindings === "string" &&
|
(typeof textWithBindings === "string" &&
|
||||||
textWithBindings.match(CAPTURE_VAR_INSIDE_MUSTACHE)) ||
|
textWithBindings.match(CAPTURE_VAR_INSIDE_TEMPLATE)) ||
|
||||||
[]
|
[]
|
||||||
|
|
||||||
// Replace runtimeBindings with readableBindings:
|
// Replace runtimeBindings with readableBindings:
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
export function uuid() {
|
export function uuid() {
|
||||||
// always want to make this start with a letter, as this makes it
|
// always want to make this start with a letter, as this makes it
|
||||||
// easier to use with mustache bindings in the client
|
// easier to use with template string bindings in the client
|
||||||
return "cxxxxxxxxxxxx4xxxyxxxxxxxxxxxxxxx".replace(/[xy]/g, c => {
|
return "cxxxxxxxxxxxx4xxxyxxxxxxxxxxxxxxx".replace(/[xy]/g, c => {
|
||||||
const r = (Math.random() * 16) | 0,
|
const r = (Math.random() * 16) | 0,
|
||||||
v = c == "x" ? r : (r & 0x3) | 0x8
|
v = c == "x" ? r : (r & 0x3) | 0x8
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
<script>
|
<script>
|
||||||
import mustache from "mustache"
|
import { processStringSync } from "@budibase/string-templates"
|
||||||
import { get } from "lodash/fp"
|
import { get } from "lodash/fp"
|
||||||
import { backendUiStore } from "builderStore"
|
import { backendUiStore } from "builderStore"
|
||||||
|
|
||||||
|
@ -54,8 +54,8 @@
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
// Fill in bindings with mustache
|
// Fill in bindings with templating library
|
||||||
return mustache.render(formattedTagline, { inputs })
|
return processStringSync(formattedTagline, { inputs })
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
|
|
@ -11,9 +11,7 @@
|
||||||
on:click
|
on:click
|
||||||
class:big={subtitle != null}
|
class:big={subtitle != null}
|
||||||
{...$$restProps}>
|
{...$$restProps}>
|
||||||
{#if icon}
|
{#if icon}<i class={icon} />{/if}
|
||||||
<i class={icon} />
|
|
||||||
{/if}
|
|
||||||
<div class="content">
|
<div class="content">
|
||||||
<div class="title">{title}</div>
|
<div class="title">{title}</div>
|
||||||
{#if subtitle != null}
|
{#if subtitle != null}
|
||||||
|
|
|
@ -1,12 +1,6 @@
|
||||||
<script>
|
<script>
|
||||||
import groupBy from "lodash/fp/groupBy"
|
import groupBy from "lodash/fp/groupBy"
|
||||||
import {
|
import { Button, TextArea, Drawer, Heading, Spacer } from "@budibase/bbui"
|
||||||
Button,
|
|
||||||
TextArea,
|
|
||||||
Drawer,
|
|
||||||
Heading,
|
|
||||||
Spacer,
|
|
||||||
} from "@budibase/bbui"
|
|
||||||
import { createEventDispatcher } from "svelte"
|
import { createEventDispatcher } from "svelte"
|
||||||
|
|
||||||
const dispatch = createEventDispatcher()
|
const dispatch = createEventDispatcher()
|
||||||
|
@ -38,7 +32,9 @@
|
||||||
<Heading extraSmall>Tables</Heading>
|
<Heading extraSmall>Tables</Heading>
|
||||||
<ul>
|
<ul>
|
||||||
{#each context as { readableBinding }}
|
{#each context as { readableBinding }}
|
||||||
<li on:click={() => addToText(readableBinding)}>{readableBinding}</li>
|
<li on:click={() => addToText(readableBinding)}>
|
||||||
|
{readableBinding}
|
||||||
|
</li>
|
||||||
{/each}
|
{/each}
|
||||||
</ul>
|
</ul>
|
||||||
{/if}
|
{/if}
|
||||||
|
@ -46,7 +42,9 @@
|
||||||
<Heading extraSmall>Components</Heading>
|
<Heading extraSmall>Components</Heading>
|
||||||
<ul>
|
<ul>
|
||||||
{#each instance as { readableBinding }}
|
{#each instance as { readableBinding }}
|
||||||
<li on:click={() => addToText(readableBinding)}>{readableBinding}</li>
|
<li on:click={() => addToText(readableBinding)}>
|
||||||
|
{readableBinding}
|
||||||
|
</li>
|
||||||
{/each}
|
{/each}
|
||||||
</ul>
|
</ul>
|
||||||
{/if}
|
{/if}
|
||||||
|
|
|
@ -1,9 +1,11 @@
|
||||||
<script context="module">
|
<script context="module">
|
||||||
import iconData from "./icons.js"
|
import iconData from "./icons.js"
|
||||||
|
|
||||||
const categories = Object.keys(iconData)
|
|
||||||
const icons = Object.keys(iconData).reduce((acc, cat) => [...acc, ...Object.keys(iconData[cat])], [])
|
|
||||||
|
|
||||||
|
const categories = Object.keys(iconData)
|
||||||
|
const icons = Object.keys(iconData).reduce(
|
||||||
|
(acc, cat) => [...acc, ...Object.keys(iconData[cat])],
|
||||||
|
[]
|
||||||
|
)
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
|
@ -285,4 +287,4 @@
|
||||||
.page-btn:hover {
|
.page-btn:hover {
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|
|
@ -19,7 +19,7 @@
|
||||||
export let onChange = () => {}
|
export let onChange = () => {}
|
||||||
|
|
||||||
let bindingDrawer
|
let bindingDrawer
|
||||||
|
|
||||||
let temporaryBindableValue = value
|
let temporaryBindableValue = value
|
||||||
let bindableProperties = []
|
let bindableProperties = []
|
||||||
let anchor
|
let anchor
|
||||||
|
@ -103,20 +103,24 @@
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<Drawer bind:this={bindingDrawer} title="Bindings">
|
<Drawer bind:this={bindingDrawer} title="Bindings">
|
||||||
<div slot="description"><Body extraSmall grey>Add the objects on the left to enrich your text.</Body></div>
|
<div slot="description">
|
||||||
|
<Body extraSmall grey>
|
||||||
|
Add the objects on the left to enrich your text.
|
||||||
|
</Body>
|
||||||
|
</div>
|
||||||
<heading slot="buttons">
|
<heading slot="buttons">
|
||||||
<Button thin blue on:click={handleClose}>Save</Button>
|
<Button thin blue on:click={handleClose}>Save</Button>
|
||||||
</heading>
|
</heading>
|
||||||
<div slot="body">
|
<div slot="body">
|
||||||
<BindingPanel {...handlevalueKey(value)}
|
<BindingPanel
|
||||||
close={handleClose}
|
{...handlevalueKey(value)}
|
||||||
on:update={e => (temporaryBindableValue = e.detail)}
|
close={handleClose}
|
||||||
{bindableProperties} />
|
on:update={e => (temporaryBindableValue = e.detail)}
|
||||||
|
{bindableProperties} />
|
||||||
</div>
|
</div>
|
||||||
</Drawer>
|
</Drawer>
|
||||||
|
|
||||||
<style>
|
<style>
|
||||||
|
|
||||||
.property-control {
|
.property-control {
|
||||||
position: relative;
|
position: relative;
|
||||||
display: flex;
|
display: flex;
|
||||||
|
|
|
@ -1,5 +1,12 @@
|
||||||
<script>
|
<script>
|
||||||
import { Button, Icon, DropdownMenu, Spacer, Heading, Drawer } from "@budibase/bbui"
|
import {
|
||||||
|
Button,
|
||||||
|
Icon,
|
||||||
|
DropdownMenu,
|
||||||
|
Spacer,
|
||||||
|
Heading,
|
||||||
|
Drawer,
|
||||||
|
} from "@budibase/bbui"
|
||||||
import { createEventDispatcher } from "svelte"
|
import { createEventDispatcher } from "svelte"
|
||||||
import { store, backendUiStore, currentAsset } from "builderStore"
|
import { store, backendUiStore, currentAsset } from "builderStore"
|
||||||
import { notifier } from "builderStore/store/notifications"
|
import { notifier } from "builderStore/store/notifications"
|
||||||
|
@ -91,34 +98,34 @@
|
||||||
</div>
|
</div>
|
||||||
{#if value.type === 'query'}
|
{#if value.type === 'query'}
|
||||||
<i class="ri-settings-5-line" on:click={drawer.show} />
|
<i class="ri-settings-5-line" on:click={drawer.show} />
|
||||||
<Drawer title={'Query'}>
|
<Drawer title={'Query'}>
|
||||||
<div slot="buttons">
|
<div slot="buttons">
|
||||||
<Button
|
<Button
|
||||||
blue
|
blue
|
||||||
thin
|
thin
|
||||||
on:click={() => {
|
on:click={() => {
|
||||||
notifier.success('Query parameters saved.')
|
notifier.success('Query parameters saved.')
|
||||||
handleSelected(value)
|
handleSelected(value)
|
||||||
drawer.hide()
|
drawer.hide()
|
||||||
}}>
|
}}>
|
||||||
Save
|
Save
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
<div class="drawer-contents" slot="body">
|
<div class="drawer-contents" slot="body">
|
||||||
<IntegrationQueryEditor
|
<IntegrationQueryEditor
|
||||||
query={value}
|
query={value}
|
||||||
schema={fetchDatasourceSchema(value)}
|
schema={fetchDatasourceSchema(value)}
|
||||||
editable={false} />
|
editable={false} />
|
||||||
<Spacer large />
|
<Spacer large />
|
||||||
{#if value.parameters.length > 0}
|
{#if value.parameters.length > 0}
|
||||||
<ParameterBuilder
|
<ParameterBuilder
|
||||||
bind:customParams={value.queryParams}
|
bind:customParams={value.queryParams}
|
||||||
parameters={queries.find(query => query._id === value._id).parameters}
|
parameters={queries.find(query => query._id === value._id).parameters}
|
||||||
bindings={queryBindableProperties} />
|
bindings={queryBindableProperties} />
|
||||||
{/if}
|
{/if}
|
||||||
</div>
|
</div>
|
||||||
</Drawer>
|
</Drawer>
|
||||||
{/if}
|
{/if}
|
||||||
<DropdownMenu bind:this={dropdownRight} anchor={anchorRight}>
|
<DropdownMenu bind:this={dropdownRight} anchor={anchorRight}>
|
||||||
<div class="dropdown">
|
<div class="dropdown">
|
||||||
<div class="title">
|
<div class="title">
|
||||||
|
|
|
@ -5324,11 +5324,6 @@ ms@2.1.2, ms@^2.1.1:
|
||||||
resolved "https://registry.yarnpkg.com/ms/-/ms-2.1.2.tgz#d09d1f357b443f493382a8eb3ccd183872ae6009"
|
resolved "https://registry.yarnpkg.com/ms/-/ms-2.1.2.tgz#d09d1f357b443f493382a8eb3ccd183872ae6009"
|
||||||
integrity sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==
|
integrity sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==
|
||||||
|
|
||||||
mustache@^4.0.1:
|
|
||||||
version "4.0.1"
|
|
||||||
resolved "https://registry.yarnpkg.com/mustache/-/mustache-4.0.1.tgz#d99beb031701ad433338e7ea65e0489416c854a2"
|
|
||||||
integrity sha512-yL5VE97+OXn4+Er3THSmTdCFCtx5hHWzrolvH+JObZnUYwuaG7XV+Ch4fR2cIrcYI0tFHxS7iyFYl14bW8y2sA==
|
|
||||||
|
|
||||||
nan@^2.12.1:
|
nan@^2.12.1:
|
||||||
version "2.14.2"
|
version "2.14.2"
|
||||||
resolved "https://registry.yarnpkg.com/nan/-/nan-2.14.2.tgz#f5376400695168f4cc694ac9393d0c9585eeea19"
|
resolved "https://registry.yarnpkg.com/nan/-/nan-2.14.2.tgz#f5376400695168f4cc694ac9393d0c9585eeea19"
|
||||||
|
|
|
@ -9,8 +9,8 @@
|
||||||
"dev:builder": "rollup -cw"
|
"dev:builder": "rollup -cw"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
|
"@budibase/string-templates": "^0.5.3",
|
||||||
"deep-equal": "^2.0.1",
|
"deep-equal": "^2.0.1",
|
||||||
"mustache": "^4.0.1",
|
|
||||||
"regexparam": "^1.3.0",
|
"regexparam": "^1.3.0",
|
||||||
"shortid": "^2.2.15",
|
"shortid": "^2.2.15",
|
||||||
"svelte-spa-router": "^3.0.5"
|
"svelte-spa-router": "^3.0.5"
|
||||||
|
|
|
@ -17,11 +17,14 @@
|
||||||
const componentStore = writable({})
|
const componentStore = writable({})
|
||||||
setContext("component", componentStore)
|
setContext("component", componentStore)
|
||||||
|
|
||||||
|
// Enrich component props
|
||||||
|
let enrichedProps
|
||||||
|
$: enrichComponentProps(definition, $dataContext, $bindingStore)
|
||||||
|
|
||||||
// Extract component definition info
|
// Extract component definition info
|
||||||
$: constructor = getComponentConstructor(definition._component)
|
$: constructor = getComponentConstructor(definition._component)
|
||||||
$: children = definition._children
|
$: children = definition._children
|
||||||
$: id = definition._id
|
$: id = definition._id
|
||||||
$: enrichedProps = enrichProps(definition, $dataContext, $bindingStore)
|
|
||||||
$: styles = definition._styles
|
$: styles = definition._styles
|
||||||
|
|
||||||
// Allow component selection in the builder preview if we're previewing a
|
// Allow component selection in the builder preview if we're previewing a
|
||||||
|
@ -30,7 +33,11 @@
|
||||||
$builderStore.previewType === "layout" || screenslotContext
|
$builderStore.previewType === "layout" || screenslotContext
|
||||||
|
|
||||||
// Update component context
|
// Update component context
|
||||||
$: componentStore.set({ id, children: children.length, styles: { ...styles, id, allowSelection } })
|
$: componentStore.set({
|
||||||
|
id,
|
||||||
|
children: children.length,
|
||||||
|
styles: { ...styles, id, allowSelection },
|
||||||
|
})
|
||||||
|
|
||||||
// Gets the component constructor for the specified component
|
// Gets the component constructor for the specified component
|
||||||
const getComponentConstructor = component => {
|
const getComponentConstructor = component => {
|
||||||
|
@ -39,6 +46,11 @@
|
||||||
return name === "screenslot" ? Router : ComponentLibrary[name]
|
return name === "screenslot" ? Router : ComponentLibrary[name]
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Enriches any string component props using handlebars
|
||||||
|
const enrichComponentProps = async (definition, context, bindingStore) => {
|
||||||
|
enrichedProps = await enrichProps(definition, context, bindingStore)
|
||||||
|
}
|
||||||
|
|
||||||
// Returns a unique key to let svelte know when to remount components.
|
// Returns a unique key to let svelte know when to remount components.
|
||||||
// If a component is selected we want to remount it every time any props
|
// If a component is selected we want to remount it every time any props
|
||||||
// change.
|
// change.
|
||||||
|
@ -48,7 +60,7 @@
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
{#if constructor}
|
{#if constructor && enrichedProps}
|
||||||
<svelte:component this={constructor} {...enrichedProps}>
|
<svelte:component this={constructor} {...enrichedProps}>
|
||||||
{#if children && children.length}
|
{#if children && children.length}
|
||||||
{#each children as child (getChildKey(child._id))}
|
{#each children as child (getChildKey(child._id))}
|
||||||
|
|
|
@ -5,26 +5,31 @@ import { saveRow, deleteRow, executeQuery, triggerAutomation } from "../api"
|
||||||
const saveRowHandler = async (action, context) => {
|
const saveRowHandler = async (action, context) => {
|
||||||
let draft = context[`${action.parameters.contextPath}_draft`]
|
let draft = context[`${action.parameters.contextPath}_draft`]
|
||||||
if (action.parameters.fields) {
|
if (action.parameters.fields) {
|
||||||
Object.entries(action.parameters.fields).forEach(([key, entry]) => {
|
for (let [key, entry] of Object.entries(action.parameters.fields)) {
|
||||||
draft[key] = enrichDataBinding(entry.value, context)
|
draft[key] = await enrichDataBinding(entry.value, context)
|
||||||
})
|
}
|
||||||
}
|
}
|
||||||
await saveRow(draft)
|
await saveRow(draft)
|
||||||
}
|
}
|
||||||
|
|
||||||
const deleteRowHandler = async (action, context) => {
|
const deleteRowHandler = async (action, context) => {
|
||||||
const { tableId, revId, rowId } = action.parameters
|
const { tableId, revId, rowId } = action.parameters
|
||||||
|
const [enrichTable, enrichRow, enrichRev] = await Promise.all([
|
||||||
|
enrichDataBinding(tableId, context),
|
||||||
|
enrichDataBinding(rowId, context),
|
||||||
|
enrichDataBinding(revId, context),
|
||||||
|
])
|
||||||
await deleteRow({
|
await deleteRow({
|
||||||
tableId: enrichDataBinding(tableId, context),
|
tableId: enrichTable,
|
||||||
rowId: enrichDataBinding(rowId, context),
|
rowId: enrichRow,
|
||||||
revId: enrichDataBinding(revId, context),
|
revId: enrichRev,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
const triggerAutomationHandler = async (action, context) => {
|
const triggerAutomationHandler = async (action, context) => {
|
||||||
const params = {}
|
const params = {}
|
||||||
for (let field in action.parameters.fields) {
|
for (let field in action.parameters.fields) {
|
||||||
params[field] = enrichDataBinding(
|
params[field] = await enrichDataBinding(
|
||||||
action.parameters.fields[field].value,
|
action.parameters.fields[field].value,
|
||||||
context
|
context
|
||||||
)
|
)
|
||||||
|
|
|
@ -5,7 +5,7 @@ import { enrichButtonActions } from "./buttonActions"
|
||||||
* Enriches component props.
|
* Enriches component props.
|
||||||
* Data bindings are enriched, and button actions are enriched.
|
* Data bindings are enriched, and button actions are enriched.
|
||||||
*/
|
*/
|
||||||
export const enrichProps = (props, dataContexts, dataBindings) => {
|
export const enrichProps = async (props, dataContexts, dataBindings) => {
|
||||||
// Exclude all private props that start with an underscore
|
// Exclude all private props that start with an underscore
|
||||||
let validProps = {}
|
let validProps = {}
|
||||||
Object.entries(props)
|
Object.entries(props)
|
||||||
|
@ -24,7 +24,7 @@ export const enrichProps = (props, dataContexts, dataBindings) => {
|
||||||
}
|
}
|
||||||
|
|
||||||
// Enrich all data bindings in top level props
|
// Enrich all data bindings in top level props
|
||||||
let enrichedProps = enrichDataBindings(validProps, context)
|
let enrichedProps = await enrichDataBindings(validProps, context)
|
||||||
|
|
||||||
// Enrich button actions if they exist
|
// Enrich button actions if they exist
|
||||||
if (props._component.endsWith("/button") && enrichedProps.onClick) {
|
if (props._component.endsWith("/button") && enrichedProps.onClick) {
|
||||||
|
|
|
@ -1,46 +1,30 @@
|
||||||
import mustache from "mustache"
|
import { processString } from "@budibase/string-templates"
|
||||||
|
|
||||||
// this is a much more liberal version of mustache's escape function
|
// Regex to test inputs with to see if they are likely candidates for template strings
|
||||||
// ...just ignoring < and > to prevent tags from user input
|
const looksLikeTemplate = /{{.*}}/
|
||||||
// original version here https://github.com/janl/mustache.js/blob/4b7908f5c9fec469a11cfaed2f2bed23c84e1c5c/mustache.js#L78
|
|
||||||
const entityMap = {
|
|
||||||
"<": "<",
|
|
||||||
">": ">",
|
|
||||||
}
|
|
||||||
mustache.escape = text => {
|
|
||||||
if (text == null || typeof text !== "string") {
|
|
||||||
return text
|
|
||||||
}
|
|
||||||
return text.replace(/[<>]/g, function fromEntityMap(s) {
|
|
||||||
return entityMap[s] || s
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
// Regex to test inputs with to see if they are likely candidates for mustache
|
|
||||||
const looksLikeMustache = /{{.*}}/
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Enriches a given input with a row from the database.
|
* Enriches a given input with a row from the database.
|
||||||
*/
|
*/
|
||||||
export const enrichDataBinding = (input, context) => {
|
export const enrichDataBinding = async (input, context) => {
|
||||||
// Only accept string inputs
|
// Only accept string inputs
|
||||||
if (!input || typeof input !== "string") {
|
if (!input || typeof input !== "string") {
|
||||||
return input
|
return input
|
||||||
}
|
}
|
||||||
// Do a fast regex check if this looks like a mustache string
|
// Do a fast regex check if this looks like a template string
|
||||||
if (!looksLikeMustache.test(input)) {
|
if (!looksLikeTemplate.test(input)) {
|
||||||
return input
|
return input
|
||||||
}
|
}
|
||||||
return mustache.render(input, context)
|
return processString(input, context)
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Enriches each prop in a props object
|
* Enriches each prop in a props object
|
||||||
*/
|
*/
|
||||||
export const enrichDataBindings = (props, context) => {
|
export const enrichDataBindings = async (props, context) => {
|
||||||
let enrichedProps = {}
|
let enrichedProps = {}
|
||||||
Object.entries(props).forEach(([key, value]) => {
|
for (let [key, value] of Object.entries(props)) {
|
||||||
enrichedProps[key] = enrichDataBinding(value, context)
|
enrichedProps[key] = await enrichDataBinding(value, context)
|
||||||
})
|
}
|
||||||
return enrichedProps
|
return enrichedProps
|
||||||
}
|
}
|
||||||
|
|
|
@ -51,6 +51,7 @@
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@elastic/elasticsearch": "^7.10.0",
|
"@elastic/elasticsearch": "^7.10.0",
|
||||||
"@budibase/client": "^0.5.3",
|
"@budibase/client": "^0.5.3",
|
||||||
|
"@budibase/string-templates": "^0.5.3",
|
||||||
"@koa/router": "^8.0.0",
|
"@koa/router": "^8.0.0",
|
||||||
"@sendgrid/mail": "^7.1.1",
|
"@sendgrid/mail": "^7.1.1",
|
||||||
"@sentry/node": "^5.19.2",
|
"@sentry/node": "^5.19.2",
|
||||||
|
@ -67,7 +68,6 @@
|
||||||
"electron-util": "^0.14.2",
|
"electron-util": "^0.14.2",
|
||||||
"fix-path": "^3.0.0",
|
"fix-path": "^3.0.0",
|
||||||
"fs-extra": "^8.1.0",
|
"fs-extra": "^8.1.0",
|
||||||
"handlebars": "^4.7.6",
|
|
||||||
"jimp": "^0.16.1",
|
"jimp": "^0.16.1",
|
||||||
"joi": "^17.2.1",
|
"joi": "^17.2.1",
|
||||||
"jsonschema": "^1.4.0",
|
"jsonschema": "^1.4.0",
|
||||||
|
@ -82,7 +82,6 @@
|
||||||
"lodash": "^4.17.13",
|
"lodash": "^4.17.13",
|
||||||
"mongodb": "^3.6.3",
|
"mongodb": "^3.6.3",
|
||||||
"mssql": "^6.2.3",
|
"mssql": "^6.2.3",
|
||||||
"mustache": "^4.0.1",
|
|
||||||
"node-fetch": "^2.6.0",
|
"node-fetch": "^2.6.0",
|
||||||
"open": "^7.3.0",
|
"open": "^7.3.0",
|
||||||
"pg": "^8.5.1",
|
"pg": "^8.5.1",
|
||||||
|
|
|
@ -29,7 +29,7 @@ const {
|
||||||
createLoginScreen,
|
createLoginScreen,
|
||||||
} = require("../../constants/screens")
|
} = require("../../constants/screens")
|
||||||
const { cloneDeep } = require("lodash/fp")
|
const { cloneDeep } = require("lodash/fp")
|
||||||
const { recurseMustache } = require("../../utilities/mustache")
|
const { processObject } = require("@budibase/string-templates")
|
||||||
const { getAllApps } = require("../../utilities")
|
const { getAllApps } = require("../../utilities")
|
||||||
const { USERS_TABLE_SCHEMA } = require("../../constants")
|
const { USERS_TABLE_SCHEMA } = require("../../constants")
|
||||||
const {
|
const {
|
||||||
|
@ -232,8 +232,7 @@ const createEmptyAppPackage = async (ctx, app) => {
|
||||||
let screensAndLayouts = []
|
let screensAndLayouts = []
|
||||||
for (let layout of BASE_LAYOUTS) {
|
for (let layout of BASE_LAYOUTS) {
|
||||||
const cloned = cloneDeep(layout)
|
const cloned = cloneDeep(layout)
|
||||||
cloned.title = app.name
|
screensAndLayouts.push(await processObject(cloned, app))
|
||||||
screensAndLayouts.push(recurseMustache(cloned, app))
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const homeScreen = createHomeScreen(app)
|
const homeScreen = createHomeScreen(app)
|
||||||
|
|
|
@ -7,7 +7,7 @@ const fs = require("fs-extra")
|
||||||
const uuid = require("uuid")
|
const uuid = require("uuid")
|
||||||
const AWS = require("aws-sdk")
|
const AWS = require("aws-sdk")
|
||||||
const { prepareUpload } = require("../deploy/utils")
|
const { prepareUpload } = require("../deploy/utils")
|
||||||
const handlebars = require("handlebars")
|
const { processString } = require("@budibase/string-templates")
|
||||||
const {
|
const {
|
||||||
budibaseAppsDir,
|
budibaseAppsDir,
|
||||||
budibaseTempDir,
|
budibaseTempDir,
|
||||||
|
@ -176,11 +176,8 @@ exports.serveApp = async function(ctx) {
|
||||||
objectStoreUrl: objectStoreUrl(),
|
objectStoreUrl: objectStoreUrl(),
|
||||||
})
|
})
|
||||||
|
|
||||||
const template = handlebars.compile(
|
const appHbs = fs.readFileSync(`${__dirname}/templates/app.hbs`, "utf8")
|
||||||
fs.readFileSync(`${__dirname}/templates/app.hbs`, "utf8")
|
ctx.body = await processString(appHbs, {
|
||||||
)
|
|
||||||
|
|
||||||
ctx.body = template({
|
|
||||||
head,
|
head,
|
||||||
body: html,
|
body: html,
|
||||||
style: css.code,
|
style: css.code,
|
||||||
|
|
|
@ -22,7 +22,9 @@
|
||||||
<title>{title}</title>
|
<title>{title}</title>
|
||||||
<link rel="icon" type="image/png" href={favicon} />
|
<link rel="icon" type="image/png" href={favicon} />
|
||||||
<link rel="stylesheet" href="https://rsms.me/inter/inter.css" />
|
<link rel="stylesheet" href="https://rsms.me/inter/inter.css" />
|
||||||
<link href="https://cdn.jsdelivr.net/npm/remixicon@2.5.0/fonts/remixicon.css" rel="stylesheet">
|
<link
|
||||||
|
href="https://cdn.jsdelivr.net/npm/remixicon@2.5.0/fonts/remixicon.css"
|
||||||
|
rel="stylesheet" />
|
||||||
<style>
|
<style>
|
||||||
html,
|
html,
|
||||||
body {
|
body {
|
||||||
|
|
|
@ -1,10 +1,11 @@
|
||||||
const CouchDB = require("../db")
|
const CouchDB = require("../db")
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* When values are input to the system generally they will be of type string as this is required for mustache. This can
|
* When values are input to the system generally they will be of type string as this is required for template strings.
|
||||||
* generate some odd scenarios as the Schema of the automation requires a number but the builder might supply a string
|
* This can generate some odd scenarios as the Schema of the automation requires a number but the builder might supply
|
||||||
* with mustache syntax to get the number from the rest of the context. To support this the server has to make sure that
|
* a string with template syntax to get the number from the rest of the context. To support this the server has to
|
||||||
* the post mustache statement can be cast into the correct type, this function does this for numbers and booleans.
|
* make sure that the post template statement can be cast into the correct type, this function does this for numbers
|
||||||
|
* and booleans.
|
||||||
*
|
*
|
||||||
* @param {object} inputs An object of inputs, please note this will not recurse down into any objects within, it simply
|
* @param {object} inputs An object of inputs, please note this will not recurse down into any objects within, it simply
|
||||||
* cleanses the top level inputs, however it can be used by recursively calling it deeper into the object structures if
|
* cleanses the top level inputs, however it can be used by recursively calling it deeper into the object structures if
|
||||||
|
@ -54,7 +55,7 @@ module.exports.cleanInputValues = (inputs, schema) => {
|
||||||
*
|
*
|
||||||
* @param {string} appId The instance which the Table/Table is contained under.
|
* @param {string} appId The instance which the Table/Table is contained under.
|
||||||
* @param {string} tableId The ID of the Table/Table which the schema is to be retrieved for.
|
* @param {string} tableId The ID of the Table/Table which the schema is to be retrieved for.
|
||||||
* @param {object} row The input row structure which requires clean-up after having been through mustache statements.
|
* @param {object} row The input row structure which requires clean-up after having been through template statements.
|
||||||
* @returns {Promise<Object>} The cleaned up rows object, will should now have all the required primitive types.
|
* @returns {Promise<Object>} The cleaned up rows object, will should now have all the required primitive types.
|
||||||
*/
|
*/
|
||||||
module.exports.cleanUpRow = async (appId, tableId, row) => {
|
module.exports.cleanUpRow = async (appId, tableId, row) => {
|
||||||
|
@ -66,11 +67,11 @@ module.exports.cleanUpRow = async (appId, tableId, row) => {
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* A utility function for the cleanUpRow, which can be used if only the row ID is known (not the table ID) to clean
|
* A utility function for the cleanUpRow, which can be used if only the row ID is known (not the table ID) to clean
|
||||||
* up a row after mustache statements have been replaced. This is specifically useful for the update row action.
|
* up a row after template statements have been replaced. This is specifically useful for the update row action.
|
||||||
*
|
*
|
||||||
* @param {string} appId The instance which the Table/Table is contained under.
|
* @param {string} appId The instance which the Table/Table is contained under.
|
||||||
* @param {string} rowId The ID of the row from which the tableId will be extracted, to get the Table/Table schema.
|
* @param {string} rowId The ID of the row from which the tableId will be extracted, to get the Table/Table schema.
|
||||||
* @param {object} row The input row structure which requires clean-up after having been through mustache statements.
|
* @param {object} row The input row structure which requires clean-up after having been through template statements.
|
||||||
* @returns {Promise<Object>} The cleaned up rows object, which will now have all the required primitive types.
|
* @returns {Promise<Object>} The cleaned up rows object, which will now have all the required primitive types.
|
||||||
*/
|
*/
|
||||||
module.exports.cleanUpRowById = async (appId, rowId, row) => {
|
module.exports.cleanUpRowById = async (appId, rowId, row) => {
|
||||||
|
|
|
@ -1,13 +1,8 @@
|
||||||
const handlebars = require("handlebars")
|
|
||||||
const actions = require("./actions")
|
const actions = require("./actions")
|
||||||
const logic = require("./logic")
|
const logic = require("./logic")
|
||||||
const automationUtils = require("./automationUtils")
|
const automationUtils = require("./automationUtils")
|
||||||
const AutomationEmitter = require("../events/AutomationEmitter")
|
const AutomationEmitter = require("../events/AutomationEmitter")
|
||||||
const { recurseMustache } = require("../utilities/mustache")
|
const { processObject } = require("@budibase/string-templates")
|
||||||
|
|
||||||
handlebars.registerHelper("object", value => {
|
|
||||||
return new handlebars.SafeString(JSON.stringify(value))
|
|
||||||
})
|
|
||||||
|
|
||||||
const FILTER_STEP_ID = logic.BUILTIN_DEFINITIONS.FILTER.stepId
|
const FILTER_STEP_ID = logic.BUILTIN_DEFINITIONS.FILTER.stepId
|
||||||
|
|
||||||
|
@ -24,7 +19,7 @@ class Orchestrator {
|
||||||
// remove from context
|
// remove from context
|
||||||
delete triggerOutput.appId
|
delete triggerOutput.appId
|
||||||
delete triggerOutput.metadata
|
delete triggerOutput.metadata
|
||||||
// step zero is never used as the mustache is zero indexed for customer facing
|
// step zero is never used as the template string is zero indexed for customer facing
|
||||||
this._context = { steps: [{}], trigger: triggerOutput }
|
this._context = { steps: [{}], trigger: triggerOutput }
|
||||||
this._automation = automation
|
this._automation = automation
|
||||||
// create an emitter which has the chain count for this automation run in it, so it can block
|
// create an emitter which has the chain count for this automation run in it, so it can block
|
||||||
|
@ -49,7 +44,7 @@ class Orchestrator {
|
||||||
let automation = this._automation
|
let automation = this._automation
|
||||||
for (let step of automation.definition.steps) {
|
for (let step of automation.definition.steps) {
|
||||||
let stepFn = await this.getStepFunctionality(step.type, step.stepId)
|
let stepFn = await this.getStepFunctionality(step.type, step.stepId)
|
||||||
step.inputs = recurseMustache(step.inputs, this._context)
|
step.inputs = await processObject(step.inputs, this._context)
|
||||||
step.inputs = automationUtils.cleanInputValues(
|
step.inputs = automationUtils.cleanInputValues(
|
||||||
step.inputs,
|
step.inputs,
|
||||||
step.schema.inputs
|
step.schema.inputs
|
||||||
|
|
|
@ -9,7 +9,7 @@ const { rowEmission, tableEmission } = require("./utils")
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Extending the standard emitter to some syntactic sugar and standardisation to the emitted event.
|
* Extending the standard emitter to some syntactic sugar and standardisation to the emitted event.
|
||||||
* This is specifically quite important for mustache used in automations.
|
* This is specifically quite important for template strings used in automations.
|
||||||
*/
|
*/
|
||||||
class BudibaseEmitter extends EventEmitter {
|
class BudibaseEmitter extends EventEmitter {
|
||||||
emitRow(eventName, appId, row, table = null) {
|
emitRow(eventName, appId, row, table = null) {
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
const { existsSync, readFile, writeFile, ensureDir } = require("fs-extra")
|
const { existsSync, readFile, writeFile, ensureDir } = require("fs-extra")
|
||||||
const { join, resolve } = require("./centralPath")
|
const { join, resolve } = require("./centralPath")
|
||||||
const handlebars = require("handlebars")
|
const { processString } = require("@budibase/string-templates")
|
||||||
const uuid = require("uuid")
|
const uuid = require("uuid")
|
||||||
|
|
||||||
module.exports = async opts => {
|
module.exports = async opts => {
|
||||||
|
@ -31,8 +31,7 @@ const createDevEnvFile = async opts => {
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
opts.cookieKey1 = opts.cookieKey1 || uuid.v4()
|
opts.cookieKey1 = opts.cookieKey1 || uuid.v4()
|
||||||
const envTemplate = handlebars.compile(template)
|
const config = await processString(template, opts)
|
||||||
const config = envTemplate(opts)
|
|
||||||
await writeFile(destConfigFile, config, { flag: "w+" })
|
await writeFile(destConfigFile, config, { flag: "w+" })
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,73 +0,0 @@
|
||||||
const handlebars = require("handlebars")
|
|
||||||
|
|
||||||
handlebars.registerHelper("object", value => {
|
|
||||||
return new handlebars.SafeString(JSON.stringify(value))
|
|
||||||
})
|
|
||||||
|
|
||||||
/**
|
|
||||||
* When running mustache statements to execute on the context of the automation it possible user's may input mustache
|
|
||||||
* in a few different forms, some of which are invalid but are logically valid. An example of this would be the mustache
|
|
||||||
* statement "{{steps[0].revision}}" here it is obvious the user is attempting to access an array or object using array
|
|
||||||
* like operators. These are not supported by Mustache and therefore the statement will fail. This function will clean up
|
|
||||||
* the mustache statement so it instead reads as "{{steps.0.revision}}" which is valid and will work. It may also be expanded
|
|
||||||
* to include any other mustache statement cleanup that has been deemed necessary for the system.
|
|
||||||
*
|
|
||||||
* @param {string} string The string which *may* contain mustache statements, it is OK if it does not contain any.
|
|
||||||
* @returns {string} The string that was input with cleaned up mustache statements as required.
|
|
||||||
*/
|
|
||||||
function cleanMustache(string) {
|
|
||||||
let charToReplace = {
|
|
||||||
"[": ".",
|
|
||||||
"]": "",
|
|
||||||
}
|
|
||||||
let regex = new RegExp(/{{[^}}]*}}/g)
|
|
||||||
let matches = string.match(regex)
|
|
||||||
if (matches == null) {
|
|
||||||
return string
|
|
||||||
}
|
|
||||||
for (let match of matches) {
|
|
||||||
let baseIdx = string.indexOf(match)
|
|
||||||
for (let key of Object.keys(charToReplace)) {
|
|
||||||
let idxChar = match.indexOf(key)
|
|
||||||
if (idxChar !== -1) {
|
|
||||||
string =
|
|
||||||
string.slice(baseIdx, baseIdx + idxChar) +
|
|
||||||
charToReplace[key] +
|
|
||||||
string.slice(baseIdx + idxChar + 1)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return string
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Given an input object this will recurse through all props to try and update
|
|
||||||
* any handlebars/mustache statements within.
|
|
||||||
* @param {object|array} inputs The input structure which is to be recursed, it is important to note that
|
|
||||||
* if the structure contains any cycles then this will fail.
|
|
||||||
* @param {object} context The context that handlebars should fill data from.
|
|
||||||
* @returns {object|array} The structure input, as fully updated as possible.
|
|
||||||
*/
|
|
||||||
function recurseMustache(inputs, context) {
|
|
||||||
// JSON stringify will fail if there are any cycles, stops infinite recursion
|
|
||||||
try {
|
|
||||||
JSON.stringify(inputs)
|
|
||||||
} catch (err) {
|
|
||||||
throw "Unable to process inputs to JSON, cannot recurse"
|
|
||||||
}
|
|
||||||
for (let key of Object.keys(inputs)) {
|
|
||||||
let val = inputs[key]
|
|
||||||
if (typeof val === "string") {
|
|
||||||
val = cleanMustache(inputs[key])
|
|
||||||
const template = handlebars.compile(val)
|
|
||||||
inputs[key] = template(context)
|
|
||||||
}
|
|
||||||
// this covers objects and arrays
|
|
||||||
else if (typeof val === "object") {
|
|
||||||
inputs[key] = recurseMustache(inputs[key], context)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return inputs
|
|
||||||
}
|
|
||||||
|
|
||||||
exports.recurseMustache = recurseMustache
|
|
|
@ -228,6 +228,16 @@
|
||||||
lodash "^4.17.19"
|
lodash "^4.17.19"
|
||||||
to-fast-properties "^2.0.0"
|
to-fast-properties "^2.0.0"
|
||||||
|
|
||||||
|
"@budibase/client@^0.5.3":
|
||||||
|
version "0.5.3"
|
||||||
|
resolved "https://registry.yarnpkg.com/@budibase/client/-/client-0.5.3.tgz#d2406b9a5b25ac446ba0f776b0ef3a38777a131a"
|
||||||
|
integrity sha512-pv8pMH5vxgvIAEl+2zjp1ScWAtqVWqeH65e9EDqX6oVK2AsnJe9r0HxywOHN5mCgOFxou972+39c6fYR9/enyw==
|
||||||
|
dependencies:
|
||||||
|
deep-equal "^2.0.1"
|
||||||
|
mustache "^4.0.1"
|
||||||
|
regexparam "^1.3.0"
|
||||||
|
svelte-spa-router "^3.0.5"
|
||||||
|
|
||||||
"@cnakazawa/watch@^1.0.3":
|
"@cnakazawa/watch@^1.0.3":
|
||||||
version "1.0.4"
|
version "1.0.4"
|
||||||
resolved "https://registry.yarnpkg.com/@cnakazawa/watch/-/watch-1.0.4.tgz#f864ae85004d0fcab6f50be9141c4da368d1656a"
|
resolved "https://registry.yarnpkg.com/@cnakazawa/watch/-/watch-1.0.4.tgz#f864ae85004d0fcab6f50be9141c4da368d1656a"
|
||||||
|
@ -3588,18 +3598,6 @@ growly@^1.3.0:
|
||||||
resolved "https://registry.yarnpkg.com/growly/-/growly-1.3.0.tgz#f10748cbe76af964b7c96c93c6bcc28af120c081"
|
resolved "https://registry.yarnpkg.com/growly/-/growly-1.3.0.tgz#f10748cbe76af964b7c96c93c6bcc28af120c081"
|
||||||
integrity sha1-8QdIy+dq+WS3yWyTxrzCivEgwIE=
|
integrity sha1-8QdIy+dq+WS3yWyTxrzCivEgwIE=
|
||||||
|
|
||||||
handlebars@^4.7.6:
|
|
||||||
version "4.7.6"
|
|
||||||
resolved "https://registry.yarnpkg.com/handlebars/-/handlebars-4.7.6.tgz#d4c05c1baf90e9945f77aa68a7a219aa4a7df74e"
|
|
||||||
integrity sha512-1f2BACcBfiwAfStCKZNrUCgqNZkGsAT7UM3kkYtXuLo0KnaVfjKOyf7PRzB6++aK9STyT1Pd2ZCPe3EGOXleXA==
|
|
||||||
dependencies:
|
|
||||||
minimist "^1.2.5"
|
|
||||||
neo-async "^2.6.0"
|
|
||||||
source-map "^0.6.1"
|
|
||||||
wordwrap "^1.0.0"
|
|
||||||
optionalDependencies:
|
|
||||||
uglify-js "^3.1.4"
|
|
||||||
|
|
||||||
har-schema@^2.0.0:
|
har-schema@^2.0.0:
|
||||||
version "2.0.0"
|
version "2.0.0"
|
||||||
resolved "https://registry.yarnpkg.com/har-schema/-/har-schema-2.0.0.tgz#a94c2224ebcac04782a0d9035521f24735b7ec92"
|
resolved "https://registry.yarnpkg.com/har-schema/-/har-schema-2.0.0.tgz#a94c2224ebcac04782a0d9035521f24735b7ec92"
|
||||||
|
|
|
@ -8,13 +8,12 @@
|
||||||
export let size = "fa-lg"
|
export let size = "fa-lg"
|
||||||
export let color = "#f00"
|
export let color = "#f00"
|
||||||
$: styles = {
|
$: styles = {
|
||||||
...$component.styles,
|
...$component.styles,
|
||||||
normal: {
|
normal: {
|
||||||
...$component.styles.normal,
|
...$component.styles.normal,
|
||||||
color
|
color,
|
||||||
|
},
|
||||||
}
|
}
|
||||||
}
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<i use:styleable={styles}
|
<i use:styleable={styles} class="{icon} {size}" />
|
||||||
class="{icon} {size}" />
|
|
||||||
|
|
|
@ -28,7 +28,7 @@
|
||||||
{#each rows as row}
|
{#each rows as row}
|
||||||
<DataProvider {row}>
|
<DataProvider {row}>
|
||||||
{#if $component.children === 0}
|
{#if $component.children === 0}
|
||||||
<p>Add some components too.</p>
|
<p>Add some components too.</p>
|
||||||
{:else}
|
{:else}
|
||||||
<slot />
|
<slot />
|
||||||
{/if}
|
{/if}
|
||||||
|
@ -47,4 +47,4 @@
|
||||||
border: #ccc 1px solid;
|
border: #ccc 1px solid;
|
||||||
padding: var(--spacing-m);
|
padding: var(--spacing-m);
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|
|
@ -0,0 +1,12 @@
|
||||||
|
{
|
||||||
|
"globals": {
|
||||||
|
"emit": true,
|
||||||
|
"key": true
|
||||||
|
},
|
||||||
|
"env": {
|
||||||
|
"node": true
|
||||||
|
},
|
||||||
|
"extends": ["eslint:recommended"],
|
||||||
|
"rules": {
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,2 @@
|
||||||
|
dist/
|
||||||
|
node_modules/
|
|
@ -0,0 +1,194 @@
|
||||||
|
/*
|
||||||
|
* For a detailed explanation regarding each configuration property, visit:
|
||||||
|
* https://jestjs.io/docs/en/configuration.html
|
||||||
|
*/
|
||||||
|
|
||||||
|
module.exports = {
|
||||||
|
// All imported modules in your tests should be mocked automatically
|
||||||
|
// automock: false,
|
||||||
|
|
||||||
|
// Stop running tests after `n` failures
|
||||||
|
// bail: 0,
|
||||||
|
|
||||||
|
// The directory where Jest should store its cached dependency information
|
||||||
|
// cacheDirectory: "/tmp/jest_rs",
|
||||||
|
|
||||||
|
// Automatically clear mock calls and instances between every test
|
||||||
|
clearMocks: true,
|
||||||
|
|
||||||
|
// Indicates whether the coverage information should be collected while executing the test
|
||||||
|
// collectCoverage: false,
|
||||||
|
|
||||||
|
// An array of glob patterns indicating a set of files for which coverage information should be collected
|
||||||
|
// collectCoverageFrom: undefined,
|
||||||
|
|
||||||
|
// The directory where Jest should output its coverage files
|
||||||
|
coverageDirectory: "coverage",
|
||||||
|
|
||||||
|
// An array of regexp pattern strings used to skip coverage collection
|
||||||
|
// coveragePathIgnorePatterns: [
|
||||||
|
// "/node_modules/"
|
||||||
|
// ],
|
||||||
|
|
||||||
|
// Indicates which provider should be used to instrument code for coverage
|
||||||
|
coverageProvider: "v8",
|
||||||
|
|
||||||
|
// A list of reporter names that Jest uses when writing coverage reports
|
||||||
|
// coverageReporters: [
|
||||||
|
// "json",
|
||||||
|
// "text",
|
||||||
|
// "lcov",
|
||||||
|
// "clover"
|
||||||
|
// ],
|
||||||
|
|
||||||
|
// An object that configures minimum threshold enforcement for coverage results
|
||||||
|
// coverageThreshold: undefined,
|
||||||
|
|
||||||
|
// A path to a custom dependency extractor
|
||||||
|
// dependencyExtractor: undefined,
|
||||||
|
|
||||||
|
// Make calling deprecated APIs throw helpful error messages
|
||||||
|
// errorOnDeprecated: false,
|
||||||
|
|
||||||
|
// Force coverage collection from ignored files using an array of glob patterns
|
||||||
|
// forceCoverageMatch: [],
|
||||||
|
|
||||||
|
// A path to a module which exports an async function that is triggered once before all test suites
|
||||||
|
// globalSetup: undefined,
|
||||||
|
|
||||||
|
// A path to a module which exports an async function that is triggered once after all test suites
|
||||||
|
// globalTeardown: undefined,
|
||||||
|
|
||||||
|
// A set of global variables that need to be available in all test environments
|
||||||
|
// globals: {},
|
||||||
|
|
||||||
|
// The maximum amount of workers used to run your tests. Can be specified as % or a number. E.g. maxWorkers: 10% will use 10% of your CPU amount + 1 as the maximum worker number. maxWorkers: 2 will use a maximum of 2 workers.
|
||||||
|
// maxWorkers: "50%",
|
||||||
|
|
||||||
|
// An array of directory names to be searched recursively up from the requiring module's location
|
||||||
|
// moduleDirectories: [
|
||||||
|
// "node_modules"
|
||||||
|
// ],
|
||||||
|
|
||||||
|
// An array of file extensions your modules use
|
||||||
|
// moduleFileExtensions: [
|
||||||
|
// "js",
|
||||||
|
// "json",
|
||||||
|
// "jsx",
|
||||||
|
// "ts",
|
||||||
|
// "tsx",
|
||||||
|
// "node"
|
||||||
|
// ],
|
||||||
|
|
||||||
|
// A map from regular expressions to module names or to arrays of module names that allow to stub out resources with a single module
|
||||||
|
// moduleNameMapper: {},
|
||||||
|
|
||||||
|
// An array of regexp pattern strings, matched against all module paths before considered 'visible' to the module loader
|
||||||
|
// modulePathIgnorePatterns: [],
|
||||||
|
|
||||||
|
// Activates notifications for test results
|
||||||
|
// notify: false,
|
||||||
|
|
||||||
|
// An enum that specifies notification mode. Requires { notify: true }
|
||||||
|
// notifyMode: "failure-change",
|
||||||
|
|
||||||
|
// A preset that is used as a base for Jest's configuration
|
||||||
|
// preset: undefined,
|
||||||
|
|
||||||
|
// Run tests from one or more projects
|
||||||
|
// projects: undefined,
|
||||||
|
|
||||||
|
// Use this configuration option to add custom reporters to Jest
|
||||||
|
// reporters: undefined,
|
||||||
|
|
||||||
|
// Automatically reset mock state between every test
|
||||||
|
// resetMocks: false,
|
||||||
|
|
||||||
|
// Reset the module registry before running each individual test
|
||||||
|
// resetModules: false,
|
||||||
|
|
||||||
|
// A path to a custom resolver
|
||||||
|
// resolver: undefined,
|
||||||
|
|
||||||
|
// Automatically restore mock state between every test
|
||||||
|
// restoreMocks: false,
|
||||||
|
|
||||||
|
// The root directory that Jest should scan for tests and modules within
|
||||||
|
// rootDir: undefined,
|
||||||
|
|
||||||
|
// A list of paths to directories that Jest should use to search for files in
|
||||||
|
// roots: [
|
||||||
|
// "<rootDir>"
|
||||||
|
// ],
|
||||||
|
|
||||||
|
// Allows you to use a custom runner instead of Jest's default test runner
|
||||||
|
// runner: "jest-runner",
|
||||||
|
|
||||||
|
// The paths to modules that run some code to configure or set up the testing environment before each test
|
||||||
|
// setupFiles: [],
|
||||||
|
|
||||||
|
// A list of paths to modules that run some code to configure or set up the testing framework before each test
|
||||||
|
// setupFilesAfterEnv: [],
|
||||||
|
|
||||||
|
// The number of seconds after which a test is considered as slow and reported as such in the results.
|
||||||
|
// slowTestThreshold: 5,
|
||||||
|
|
||||||
|
// A list of paths to snapshot serializer modules Jest should use for snapshot testing
|
||||||
|
// snapshotSerializers: [],
|
||||||
|
|
||||||
|
// The test environment that will be used for testing
|
||||||
|
testEnvironment: "node",
|
||||||
|
|
||||||
|
// Options that will be passed to the testEnvironment
|
||||||
|
// testEnvironmentOptions: {},
|
||||||
|
|
||||||
|
// Adds a location field to test results
|
||||||
|
// testLocationInResults: false,
|
||||||
|
|
||||||
|
// The glob patterns Jest uses to detect test files
|
||||||
|
// testMatch: [
|
||||||
|
// "**/__tests__/**/*.[jt]s?(x)",
|
||||||
|
// "**/?(*.)+(spec|test).[tj]s?(x)"
|
||||||
|
// ],
|
||||||
|
|
||||||
|
// An array of regexp pattern strings that are matched against all test paths, matched tests are skipped
|
||||||
|
// testPathIgnorePatterns: [
|
||||||
|
// "/node_modules/"
|
||||||
|
// ],
|
||||||
|
|
||||||
|
// The regexp pattern or array of patterns that Jest uses to detect test files
|
||||||
|
// testRegex: [],
|
||||||
|
|
||||||
|
// This option allows the use of a custom results processor
|
||||||
|
// testResultsProcessor: undefined,
|
||||||
|
|
||||||
|
// This option allows use of a custom test runner
|
||||||
|
// testRunner: "jasmine2",
|
||||||
|
|
||||||
|
// This option sets the URL for the jsdom environment. It is reflected in properties such as location.href
|
||||||
|
// testURL: "http://localhost",
|
||||||
|
|
||||||
|
// Setting this value to "fake" allows the use of fake timers for functions such as "setTimeout"
|
||||||
|
// timers: "real",
|
||||||
|
|
||||||
|
// A map from regular expressions to paths to transformers
|
||||||
|
// transform: undefined,
|
||||||
|
|
||||||
|
// An array of regexp pattern strings that are matched against all source file paths, matched files will skip transformation
|
||||||
|
// transformIgnorePatterns: [
|
||||||
|
// "/node_modules/",
|
||||||
|
// "\\.pnp\\.[^\\/]+$"
|
||||||
|
// ],
|
||||||
|
|
||||||
|
// An array of regexp pattern strings that are matched against all modules before the module loader will automatically return a mock for them
|
||||||
|
// unmockedModulePathPatterns: undefined,
|
||||||
|
|
||||||
|
// Indicates whether each individual test should be reported during the run
|
||||||
|
// verbose: undefined,
|
||||||
|
|
||||||
|
// An array of regexp patterns that are matched against all source file paths before re-running tests in watch mode
|
||||||
|
// watchPathIgnorePatterns: [],
|
||||||
|
|
||||||
|
// Whether to use watchman for file crawling
|
||||||
|
// watchman: true,
|
||||||
|
}
|
|
@ -0,0 +1,31 @@
|
||||||
|
{
|
||||||
|
"name": "@budibase/string-templates",
|
||||||
|
"version": "0.5.3",
|
||||||
|
"description": "Handlebars wrapper for Budibase templating.",
|
||||||
|
"main": "dist/bundle.js",
|
||||||
|
"module": "dist/bundle.js",
|
||||||
|
"license": "AGPL-3.0",
|
||||||
|
"types": "dist/index.d.ts",
|
||||||
|
"scripts": {
|
||||||
|
"build": "tsc && rollup -c",
|
||||||
|
"dev:builder": "tsc && rollup -cw",
|
||||||
|
"test": "jest"
|
||||||
|
},
|
||||||
|
"dependencies": {
|
||||||
|
"handlebars": "^4.7.6",
|
||||||
|
"handlebars-helpers": "^0.10.0",
|
||||||
|
"handlebars-utils": "^1.0.6",
|
||||||
|
"helper-date": "^1.0.1",
|
||||||
|
"lodash": "^4.17.20"
|
||||||
|
},
|
||||||
|
"devDependencies": {
|
||||||
|
"@rollup/plugin-json": "^4.1.0",
|
||||||
|
"jest": "^26.6.3",
|
||||||
|
"rollup": "^2.36.2",
|
||||||
|
"rollup-plugin-commonjs": "^10.1.0",
|
||||||
|
"rollup-plugin-node-builtins": "^2.1.2",
|
||||||
|
"rollup-plugin-node-globals": "^1.4.0",
|
||||||
|
"rollup-plugin-node-resolve": "^5.2.0",
|
||||||
|
"typescript": "^4.1.3"
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,28 @@
|
||||||
|
import commonjs from "rollup-plugin-commonjs"
|
||||||
|
import resolve from "rollup-plugin-node-resolve"
|
||||||
|
import builtins from "rollup-plugin-node-builtins"
|
||||||
|
import globals from "rollup-plugin-node-globals"
|
||||||
|
import json from "@rollup/plugin-json"
|
||||||
|
|
||||||
|
export default {
|
||||||
|
input: "src/index.js",
|
||||||
|
output: [
|
||||||
|
{
|
||||||
|
sourcemap: true,
|
||||||
|
format: "umd",
|
||||||
|
file: "./dist/bundle.js",
|
||||||
|
name: "string-templates",
|
||||||
|
exports: "named",
|
||||||
|
},
|
||||||
|
],
|
||||||
|
plugins: [
|
||||||
|
resolve({
|
||||||
|
preferBuiltins: true,
|
||||||
|
browser: true,
|
||||||
|
}),
|
||||||
|
commonjs(),
|
||||||
|
globals(),
|
||||||
|
builtins(),
|
||||||
|
json(),
|
||||||
|
],
|
||||||
|
}
|
|
@ -0,0 +1,19 @@
|
||||||
|
class Helper {
|
||||||
|
constructor(name, fn) {
|
||||||
|
this.name = name
|
||||||
|
this.fn = fn
|
||||||
|
}
|
||||||
|
|
||||||
|
register(handlebars) {
|
||||||
|
// wrap the function so that no helper can cause handlebars to break
|
||||||
|
handlebars.registerHelper(this.name, value => {
|
||||||
|
return this.fn(value) || value
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
unregister(handlebars) {
|
||||||
|
handlebars.unregisterHelper(this.name)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = Helper
|
|
@ -0,0 +1,21 @@
|
||||||
|
module.exports.HelperFunctionBuiltin = [
|
||||||
|
"#if",
|
||||||
|
"#unless",
|
||||||
|
"#each",
|
||||||
|
"#with",
|
||||||
|
"lookup",
|
||||||
|
"log",
|
||||||
|
"blockHelperMissing",
|
||||||
|
"each",
|
||||||
|
"helperMissing",
|
||||||
|
"if",
|
||||||
|
"unless",
|
||||||
|
"log",
|
||||||
|
"lookup",
|
||||||
|
"with",
|
||||||
|
]
|
||||||
|
|
||||||
|
module.exports.HelperFunctionNames = {
|
||||||
|
OBJECT: "object",
|
||||||
|
ALL: "all",
|
||||||
|
}
|
|
@ -0,0 +1,56 @@
|
||||||
|
const helpers = require("handlebars-helpers")
|
||||||
|
const dateHelper = require("helper-date")
|
||||||
|
const { HelperFunctionBuiltin } = require("./constants")
|
||||||
|
|
||||||
|
/**
|
||||||
|
* full list of supported helpers can be found here:
|
||||||
|
* https://github.com/helpers/handlebars-helpers
|
||||||
|
*/
|
||||||
|
|
||||||
|
const EXTERNAL_FUNCTION_COLLECTIONS = [
|
||||||
|
"math",
|
||||||
|
"array",
|
||||||
|
"number",
|
||||||
|
"url",
|
||||||
|
"string",
|
||||||
|
"markdown",
|
||||||
|
]
|
||||||
|
|
||||||
|
const DATE_NAME = "date"
|
||||||
|
|
||||||
|
exports.registerAll = handlebars => {
|
||||||
|
handlebars.registerHelper(DATE_NAME, dateHelper)
|
||||||
|
let externalNames = []
|
||||||
|
for (let collection of EXTERNAL_FUNCTION_COLLECTIONS) {
|
||||||
|
// collect information about helper
|
||||||
|
let hbsHelperInfo = helpers[collection]()
|
||||||
|
for (let entry of Object.entries(hbsHelperInfo)) {
|
||||||
|
const name = entry[0]
|
||||||
|
// skip built in functions and ones seen already
|
||||||
|
if (
|
||||||
|
HelperFunctionBuiltin.indexOf(name) !== -1 ||
|
||||||
|
externalNames.indexOf(name) !== -1
|
||||||
|
) {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
externalNames.push(name)
|
||||||
|
}
|
||||||
|
// attach it to our handlebars instance
|
||||||
|
helpers[collection]({
|
||||||
|
handlebars,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
// add date external functionality
|
||||||
|
externalNames.push(DATE_NAME)
|
||||||
|
exports.externalHelperNames = externalNames
|
||||||
|
}
|
||||||
|
|
||||||
|
exports.unregisterAll = handlebars => {
|
||||||
|
handlebars.unregisterHelper(DATE_NAME)
|
||||||
|
for (let name of module.exports.externalHelperNames) {
|
||||||
|
handlebars.unregisterHelper(name)
|
||||||
|
}
|
||||||
|
exports.externalHelperNames = []
|
||||||
|
}
|
||||||
|
|
||||||
|
exports.externalHelperNames = []
|
|
@ -0,0 +1,53 @@
|
||||||
|
const Helper = require("./Helper")
|
||||||
|
const { SafeString } = require("handlebars")
|
||||||
|
const externalHandlebars = require("./external")
|
||||||
|
const { HelperFunctionNames, HelperFunctionBuiltin } = require("./constants")
|
||||||
|
|
||||||
|
const HTML_SWAPS = {
|
||||||
|
"<": "<",
|
||||||
|
">": ">",
|
||||||
|
}
|
||||||
|
|
||||||
|
const HELPERS = [
|
||||||
|
// external helpers
|
||||||
|
new Helper(HelperFunctionNames.OBJECT, value => {
|
||||||
|
return new SafeString(JSON.stringify(value))
|
||||||
|
}),
|
||||||
|
// this help is applied to all statements
|
||||||
|
new Helper(HelperFunctionNames.ALL, value => {
|
||||||
|
// null/undefined values produce bad results
|
||||||
|
if (value == null) {
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
let text = new SafeString(unescape(value).replace(/&/g, "&"))
|
||||||
|
if (text == null || typeof text !== "string") {
|
||||||
|
return text
|
||||||
|
}
|
||||||
|
return text.replace(/[<>]/g, tag => {
|
||||||
|
return HTML_SWAPS[tag] || tag
|
||||||
|
})
|
||||||
|
}),
|
||||||
|
]
|
||||||
|
|
||||||
|
module.exports.HelperNames = () => {
|
||||||
|
return Object.values(HelperFunctionNames).concat(
|
||||||
|
HelperFunctionBuiltin,
|
||||||
|
externalHandlebars.externalHelperNames
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports.registerAll = handlebars => {
|
||||||
|
for (let helper of HELPERS) {
|
||||||
|
helper.register(handlebars)
|
||||||
|
}
|
||||||
|
// register imported helpers
|
||||||
|
externalHandlebars.registerAll(handlebars)
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports.unregisterAll = handlebars => {
|
||||||
|
for (let helper of HELPERS) {
|
||||||
|
helper.unregister(handlebars)
|
||||||
|
}
|
||||||
|
// unregister all imported helpers
|
||||||
|
externalHandlebars.unregisterAll(handlebars)
|
||||||
|
}
|
|
@ -0,0 +1,105 @@
|
||||||
|
const handlebars = require("handlebars")
|
||||||
|
const { registerAll } = require("./helpers/index")
|
||||||
|
const processors = require("./processors")
|
||||||
|
const { cloneDeep } = require("lodash/fp")
|
||||||
|
const { removeNull } = require("./utilities")
|
||||||
|
|
||||||
|
const hbsInstance = handlebars.create()
|
||||||
|
registerAll(hbsInstance)
|
||||||
|
|
||||||
|
/**
|
||||||
|
* utility function to check if the object is valid
|
||||||
|
*/
|
||||||
|
function testObject(object) {
|
||||||
|
// JSON stringify will fail if there are any cycles, stops infinite recursion
|
||||||
|
try {
|
||||||
|
JSON.stringify(object)
|
||||||
|
} catch (err) {
|
||||||
|
throw "Unable to process inputs to JSON, cannot recurse"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Given an input object this will recurse through all props to try and update any handlebars statements within.
|
||||||
|
* @param {object|array} object The input structure which is to be recursed, it is important to note that
|
||||||
|
* if the structure contains any cycles then this will fail.
|
||||||
|
* @param {object} context The context that handlebars should fill data from.
|
||||||
|
* @returns {Promise<object|array>} The structure input, as fully updated as possible.
|
||||||
|
*/
|
||||||
|
module.exports.processObject = async (object, context) => {
|
||||||
|
testObject(object)
|
||||||
|
for (let key of Object.keys(object)) {
|
||||||
|
if (object[key] != null) {
|
||||||
|
let val = object[key]
|
||||||
|
if (typeof val === "string") {
|
||||||
|
object[key] = await module.exports.processString(object[key], context)
|
||||||
|
} else if (typeof val === "object") {
|
||||||
|
object[key] = await module.exports.processObject(object[key], context)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return object
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* This will process a single handlebars containing string. If the string passed in has no valid handlebars statements
|
||||||
|
* then nothing will occur.
|
||||||
|
* @param {string} string The template string which is the filled from the context object.
|
||||||
|
* @param {object} context An object of information which will be used to enrich the string.
|
||||||
|
* @returns {Promise<string>} The enriched string, all templates should have been replaced if they can be.
|
||||||
|
*/
|
||||||
|
module.exports.processString = async (string, context) => {
|
||||||
|
// TODO: carry out any async calls before carrying out async call
|
||||||
|
return module.exports.processStringSync(string, context)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Given an input object this will recurse through all props to try and update any handlebars statements within. This is
|
||||||
|
* a pure sync call and therefore does not have the full functionality of the async call.
|
||||||
|
* @param {object|array} object The input structure which is to be recursed, it is important to note that
|
||||||
|
* if the structure contains any cycles then this will fail.
|
||||||
|
* @param {object} context The context that handlebars should fill data from.
|
||||||
|
* @returns {object|array} The structure input, as fully updated as possible.
|
||||||
|
*/
|
||||||
|
module.exports.processObjectSync = (object, context) => {
|
||||||
|
testObject(object)
|
||||||
|
for (let key of Object.keys(object)) {
|
||||||
|
let val = object[key]
|
||||||
|
if (typeof val === "string") {
|
||||||
|
object[key] = module.exports.processStringSync(object[key], context)
|
||||||
|
} else if (typeof val === "object") {
|
||||||
|
object[key] = module.exports.processObjectSync(object[key], context)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return object
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* This will process a single handlebars containing string. If the string passed in has no valid handlebars statements
|
||||||
|
* then nothing will occur. This is a pure sync call and therefore does not have the full functionality of the async call.
|
||||||
|
* @param {string} string The template string which is the filled from the context object.
|
||||||
|
* @param {object} context An object of information which will be used to enrich the string.
|
||||||
|
* @returns {string} The enriched string, all templates should have been replaced if they can be.
|
||||||
|
*/
|
||||||
|
module.exports.processStringSync = (string, context) => {
|
||||||
|
const clonedContext = removeNull(cloneDeep(context))
|
||||||
|
// remove any null/undefined properties
|
||||||
|
if (typeof string !== "string") {
|
||||||
|
throw "Cannot process non-string types."
|
||||||
|
}
|
||||||
|
let template
|
||||||
|
string = processors.preprocess(string)
|
||||||
|
// this does not throw an error when template can't be fulfilled, have to try correct beforehand
|
||||||
|
template = hbsInstance.compile(string)
|
||||||
|
return processors.postprocess(template(clonedContext))
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Errors can occur if a user of this library attempts to use a helper that has not been added to the system, these errors
|
||||||
|
* can be captured to alert the user of the mistake.
|
||||||
|
* @param {function} handler a function which will be called every time an error occurs when processing a handlebars
|
||||||
|
* statement.
|
||||||
|
*/
|
||||||
|
module.exports.errorEvents = handler => {
|
||||||
|
hbsInstance.registerHelper("helperMissing", handler)
|
||||||
|
}
|
|
@ -0,0 +1,26 @@
|
||||||
|
const { FIND_HBS_REGEX } = require("../utilities")
|
||||||
|
const preprocessor = require("./preprocessor")
|
||||||
|
const postprocessor = require("./postprocessor")
|
||||||
|
|
||||||
|
function process(string, processors) {
|
||||||
|
for (let processor of processors) {
|
||||||
|
// re-run search each time incase previous processor updated/removed a match
|
||||||
|
let regex = new RegExp(FIND_HBS_REGEX)
|
||||||
|
let matches = string.match(regex)
|
||||||
|
if (matches == null) {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
for (let match of matches) {
|
||||||
|
string = processor.process(string, match)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return string
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports.preprocess = string => {
|
||||||
|
return process(string, preprocessor.processors)
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports.postprocess = string => {
|
||||||
|
return process(string, postprocessor.processors)
|
||||||
|
}
|
|
@ -0,0 +1,9 @@
|
||||||
|
/* eslint-disable no-unused-vars */
|
||||||
|
class Postprocessor {
|
||||||
|
constructor(name, fn) {
|
||||||
|
this.name = name
|
||||||
|
this.fn = fn
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports.processors = []
|
|
@ -0,0 +1,88 @@
|
||||||
|
const { HelperNames } = require("../helpers")
|
||||||
|
const { swapStrings, isAlphaNumeric } = require("../utilities")
|
||||||
|
|
||||||
|
const PreprocessorNames = {
|
||||||
|
SWAP_TO_DOT: "swap-to-dot-notation",
|
||||||
|
HANDLE_SPACES: "handle-spaces-in-properties",
|
||||||
|
FINALISE: "finalise",
|
||||||
|
}
|
||||||
|
|
||||||
|
/* eslint-disable no-unused-vars */
|
||||||
|
class Preprocessor {
|
||||||
|
constructor(name, fn) {
|
||||||
|
this.name = name
|
||||||
|
this.fn = fn
|
||||||
|
}
|
||||||
|
|
||||||
|
process(fullString, statement) {
|
||||||
|
const output = this.fn(statement)
|
||||||
|
const idx = fullString.indexOf(statement)
|
||||||
|
return swapStrings(fullString, idx, statement.length, output)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports.processors = [
|
||||||
|
new Preprocessor(PreprocessorNames.SWAP_TO_DOT, statement => {
|
||||||
|
let startBraceIdx = statement.indexOf("[")
|
||||||
|
let lastIdx = 0
|
||||||
|
while (startBraceIdx !== -1) {
|
||||||
|
// if the character previous to the literal specifier is alpha-numeric this should happen
|
||||||
|
if (isAlphaNumeric(statement.charAt(startBraceIdx - 1))) {
|
||||||
|
statement = swapStrings(statement, startBraceIdx + lastIdx, 1, ".[")
|
||||||
|
}
|
||||||
|
lastIdx = startBraceIdx + 1
|
||||||
|
startBraceIdx = statement.substring(lastIdx + 1).indexOf("[")
|
||||||
|
}
|
||||||
|
return statement
|
||||||
|
}),
|
||||||
|
|
||||||
|
new Preprocessor(PreprocessorNames.HANDLE_SPACES, 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)
|
||||||
|
if (insideStatement.charAt(0) === " ") {
|
||||||
|
insideStatement = insideStatement.slice(1)
|
||||||
|
}
|
||||||
|
if (insideStatement.charAt(insideStatement.length - 1) === " ") {
|
||||||
|
insideStatement = insideStatement.slice(0, insideStatement.length - 1)
|
||||||
|
}
|
||||||
|
const possibleHelper = insideStatement.split(" ")[0]
|
||||||
|
if (HelperNames().some(option => possibleHelper === option)) {
|
||||||
|
insideStatement = `(${insideStatement})`
|
||||||
|
}
|
||||||
|
return `{{ all ${insideStatement} }}`
|
||||||
|
}),
|
||||||
|
]
|
|
@ -0,0 +1,23 @@
|
||||||
|
const ALPHA_NUMERIC_REGEX = /^[A-Za-z0-9]+$/g
|
||||||
|
|
||||||
|
module.exports.FIND_HBS_REGEX = /{{([^{}])+}}/g
|
||||||
|
|
||||||
|
module.exports.isAlphaNumeric = char => {
|
||||||
|
return char.match(ALPHA_NUMERIC_REGEX)
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports.swapStrings = (string, start, length, swap) => {
|
||||||
|
return string.slice(0, start) + swap + string.slice(start + length)
|
||||||
|
}
|
||||||
|
|
||||||
|
// removes null and undefined
|
||||||
|
module.exports.removeNull = obj => {
|
||||||
|
return Object.fromEntries(
|
||||||
|
Object.entries(obj)
|
||||||
|
.filter(entry => entry[1] != null)
|
||||||
|
.map(([key, value]) => [
|
||||||
|
key,
|
||||||
|
value === Object(value) ? module.exports.removeNull(value) : value,
|
||||||
|
])
|
||||||
|
)
|
||||||
|
}
|
|
@ -0,0 +1,84 @@
|
||||||
|
const {
|
||||||
|
processObject,
|
||||||
|
processString,
|
||||||
|
} = require("../src/index")
|
||||||
|
|
||||||
|
describe("Test that the string processing works correctly", () => {
|
||||||
|
it("should process a basic template string", async () => {
|
||||||
|
const output = await processString("templating is {{ adjective }}", {
|
||||||
|
adjective: "easy"
|
||||||
|
})
|
||||||
|
expect(output).toBe("templating is easy")
|
||||||
|
})
|
||||||
|
|
||||||
|
it("should process a literal template", async () => {
|
||||||
|
const output = await processString("derp is {{{ adjective }}}", {
|
||||||
|
adjective: "derp"
|
||||||
|
})
|
||||||
|
expect(output).toBe("derp is derp")
|
||||||
|
})
|
||||||
|
|
||||||
|
it("should fail gracefully when wrong type passed in", async () => {
|
||||||
|
let error = null
|
||||||
|
try {
|
||||||
|
await processString(null, null)
|
||||||
|
} catch (err) {
|
||||||
|
error = err
|
||||||
|
}
|
||||||
|
expect(error).not.toBeNull()
|
||||||
|
})
|
||||||
|
|
||||||
|
it("confirm that null properties are handled correctly", async () => {
|
||||||
|
const output = await processString("hello {{ name }} I am {{ name2 }}", {
|
||||||
|
name: undefined,
|
||||||
|
name2: null,
|
||||||
|
})
|
||||||
|
expect(output).toBe("hello I am ")
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
describe("Test that the object processing works correctly", () => {
|
||||||
|
it("should be able to process an object with some template strings", async () => {
|
||||||
|
const output = await processObject({
|
||||||
|
first: "thing is {{ adjective }}",
|
||||||
|
second: "thing is bad",
|
||||||
|
third: "we are {{ adjective }} {{ noun }}",
|
||||||
|
}, {
|
||||||
|
adjective: "easy",
|
||||||
|
noun: "people",
|
||||||
|
})
|
||||||
|
expect(output.first).toBe("thing is easy")
|
||||||
|
expect(output.second).toBe("thing is bad")
|
||||||
|
expect(output.third).toBe("we are easy people")
|
||||||
|
})
|
||||||
|
|
||||||
|
it("should be able to handle arrays of string templates", async () => {
|
||||||
|
const output = await processObject(["first {{ noun }}", "second {{ noun }}"], {
|
||||||
|
noun: "person"
|
||||||
|
})
|
||||||
|
expect(output[0]).toBe("first person")
|
||||||
|
expect(output[1]).toBe("second person")
|
||||||
|
})
|
||||||
|
|
||||||
|
it("should fail gracefully when object passed in has cycles", async () => {
|
||||||
|
let error = null
|
||||||
|
try {
|
||||||
|
const innerObj = { a: "thing {{ a }}" }
|
||||||
|
innerObj.b = innerObj
|
||||||
|
await processObject(innerObj, { a: 1 })
|
||||||
|
} catch (err) {
|
||||||
|
error = err
|
||||||
|
}
|
||||||
|
expect(error).not.toBeNull()
|
||||||
|
})
|
||||||
|
|
||||||
|
it("should fail gracefully when wrong type is passed in", async () => {
|
||||||
|
let error = null
|
||||||
|
try {
|
||||||
|
await processObject(null, null)
|
||||||
|
} catch (err) {
|
||||||
|
error = err
|
||||||
|
}
|
||||||
|
expect(error).not.toBeNull()
|
||||||
|
})
|
||||||
|
})
|
|
@ -0,0 +1,51 @@
|
||||||
|
const {
|
||||||
|
processString,
|
||||||
|
} = require("../src/index")
|
||||||
|
|
||||||
|
describe("Handling context properties with spaces in their name", () => {
|
||||||
|
it("should allow through literal specifiers", async () => {
|
||||||
|
const output = await processString("test {{ [test thing] }}", {
|
||||||
|
"test thing": 1
|
||||||
|
})
|
||||||
|
expect(output).toBe("test 1")
|
||||||
|
})
|
||||||
|
|
||||||
|
it("should convert to dot notation where required", async () => {
|
||||||
|
const output = await processString("test {{ test[0] }}", {
|
||||||
|
test: [2]
|
||||||
|
})
|
||||||
|
expect(output).toBe("test 2")
|
||||||
|
})
|
||||||
|
|
||||||
|
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 }}", {
|
||||||
|
"person name": "Mike",
|
||||||
|
})
|
||||||
|
expect(output).toBe("hello my name is Mike")
|
||||||
|
})
|
||||||
|
|
||||||
|
it("should be able to handle an object with layers that requires escaping", async () => {
|
||||||
|
const output = await processString("testcase {{ testing.test case }}", {
|
||||||
|
testing: {
|
||||||
|
"test case": 1
|
||||||
|
}
|
||||||
|
})
|
||||||
|
expect(output).toBe("testcase 1")
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
describe("attempt some complex problems", () => {
|
||||||
|
it("should be able to handle a very complex handlebars statement", async () => {
|
||||||
|
const context = {
|
||||||
|
"New Repeater": {
|
||||||
|
"Get Actors": {
|
||||||
|
"first_name": "Bob",
|
||||||
|
"last_name": "Bobert"
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
const hbs = "{{ New Repeater.Get Actors.first_name }} {{ New Repeater.Get Actors.last_name }}"
|
||||||
|
const output = await processString(hbs, context)
|
||||||
|
expect(output).toBe("Bob Bobert")
|
||||||
|
})
|
||||||
|
})
|
|
@ -0,0 +1,13 @@
|
||||||
|
const {
|
||||||
|
processString,
|
||||||
|
} = require("../src/index")
|
||||||
|
|
||||||
|
describe("test the custom helpers we have applied", () => {
|
||||||
|
it("should be able to use the object helper", async () => {
|
||||||
|
const output = await processString("object is {{ object obj }}", {
|
||||||
|
obj: { a: 1 },
|
||||||
|
})
|
||||||
|
expect(output).toBe("object is {\"a\":1}")
|
||||||
|
})
|
||||||
|
|
||||||
|
})
|
|
@ -0,0 +1,33 @@
|
||||||
|
const { processString } = require("../src/index")
|
||||||
|
|
||||||
|
describe("specific test case for whether or not full app template can still be rendered", () => {
|
||||||
|
it("should be able to render the app template", async () => {
|
||||||
|
const template =
|
||||||
|
`<!doctype html>
|
||||||
|
<html>
|
||||||
|
<head>
|
||||||
|
{{{head}}}
|
||||||
|
</head>
|
||||||
|
<script>
|
||||||
|
window["##BUDIBASE_APP_ID##"] = "{{appId}}"
|
||||||
|
</script>
|
||||||
|
{{{body}}}
|
||||||
|
</html>`
|
||||||
|
const context = {
|
||||||
|
appId: "App1",
|
||||||
|
head: "<title>App</title>",
|
||||||
|
body: "<body><p>App things</p></body>"
|
||||||
|
}
|
||||||
|
const output = await processString(template, context)
|
||||||
|
expect(output).toBe(`<!doctype html>
|
||||||
|
<html>
|
||||||
|
<head>
|
||||||
|
<title>App</title>
|
||||||
|
</head>
|
||||||
|
<script>
|
||||||
|
window["##BUDIBASE_APP_ID##"] = "App1"
|
||||||
|
</script>
|
||||||
|
<body><p>App things</p></body>
|
||||||
|
</html>`)
|
||||||
|
})
|
||||||
|
})
|
|
@ -0,0 +1,10 @@
|
||||||
|
{
|
||||||
|
"include": ["src/**/*"],
|
||||||
|
|
||||||
|
"compilerOptions": {
|
||||||
|
"allowJs": true,
|
||||||
|
"declaration": true,
|
||||||
|
"emitDeclarationOnly": true,
|
||||||
|
"outDir": "dist"
|
||||||
|
}
|
||||||
|
}
|
File diff suppressed because it is too large
Load Diff
|
@ -0,0 +1,12 @@
|
||||||
|
{
|
||||||
|
"globals": {
|
||||||
|
"emit": true,
|
||||||
|
"key": true
|
||||||
|
},
|
||||||
|
"env": {
|
||||||
|
"node": true
|
||||||
|
},
|
||||||
|
"extends": ["eslint:recommended"],
|
||||||
|
"rules": {
|
||||||
|
}
|
||||||
|
}
|
|
@ -35,6 +35,10 @@ const SYMLINK_PATHS = [
|
||||||
symlink: `${devDir}/budibase-client.js.map`,
|
symlink: `${devDir}/budibase-client.js.map`,
|
||||||
destination: resolve("packages/client/dist/budibase-client.js.map"),
|
destination: resolve("packages/client/dist/budibase-client.js.map"),
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
symlink: `${devDir}/@budibase/string-templates`,
|
||||||
|
destination: resolve("packages/string-templates"),
|
||||||
|
},
|
||||||
]
|
]
|
||||||
|
|
||||||
SYMLINK_PATHS.forEach(sym => {
|
SYMLINK_PATHS.forEach(sym => {
|
||||||
|
|
Loading…
Reference in New Issue