diff --git a/lerna.json b/lerna.json
index a8276de8cc..a47d8fe604 100644
--- a/lerna.json
+++ b/lerna.json
@@ -1,5 +1,5 @@
{
- "version": "2.0.24-alpha.0",
+ "version": "2.0.24-alpha.3",
"npmClient": "yarn",
"packages": [
"packages/*"
diff --git a/packages/backend-core/package.json b/packages/backend-core/package.json
index 90b645ff2c..8df964912e 100644
--- a/packages/backend-core/package.json
+++ b/packages/backend-core/package.json
@@ -1,6 +1,6 @@
{
"name": "@budibase/backend-core",
- "version": "2.0.24-alpha.0",
+ "version": "2.0.24-alpha.3",
"description": "Budibase backend core libraries used in server and worker",
"main": "dist/src/index.js",
"types": "dist/src/index.d.ts",
@@ -20,7 +20,7 @@
"test:watch": "jest --watchAll"
},
"dependencies": {
- "@budibase/types": "2.0.24-alpha.0",
+ "@budibase/types": "2.0.24-alpha.3",
"@shopify/jest-koa-mocks": "5.0.1",
"@techpass/passport-openidconnect": "0.3.2",
"aws-sdk": "2.1030.0",
diff --git a/packages/backend-core/src/redis/index.ts b/packages/backend-core/src/redis/index.ts
index 206110366f..62e718d8ad 100644
--- a/packages/backend-core/src/redis/index.ts
+++ b/packages/backend-core/src/redis/index.ts
@@ -214,6 +214,31 @@ export = class RedisWrapper {
}
}
+ async bulkGet(keys: string[]) {
+ const db = this._db
+ const prefixedKeys = keys.map(key => addDbPrefix(db, key))
+ let response = await this.getClient().mget(prefixedKeys)
+ if (Array.isArray(response)) {
+ let final: any = {}
+ let count = 0
+ for (let result of response) {
+ if (result) {
+ let parsed
+ try {
+ parsed = JSON.parse(result)
+ } catch (err) {
+ parsed = result
+ }
+ final[keys[count]] = parsed
+ }
+ count++
+ }
+ return final
+ } else {
+ throw new Error(`Invalid response: ${response}`)
+ }
+ }
+
async store(key: string, value: any, expirySeconds: number | null = null) {
const db = this._db
if (typeof value === "object") {
diff --git a/packages/bbui/package.json b/packages/bbui/package.json
index 803396fb35..0583237a45 100644
--- a/packages/bbui/package.json
+++ b/packages/bbui/package.json
@@ -1,7 +1,7 @@
{
"name": "@budibase/bbui",
"description": "A UI solution used in the different Budibase projects.",
- "version": "2.0.24-alpha.0",
+ "version": "2.0.24-alpha.3",
"license": "MPL-2.0",
"svelte": "src/index.js",
"module": "dist/bbui.es.js",
@@ -38,7 +38,7 @@
],
"dependencies": {
"@adobe/spectrum-css-workflow-icons": "^1.2.1",
- "@budibase/string-templates": "2.0.24-alpha.0",
+ "@budibase/string-templates": "2.0.24-alpha.3",
"@spectrum-css/actionbutton": "^1.0.1",
"@spectrum-css/actiongroup": "^1.0.1",
"@spectrum-css/avatar": "^3.0.2",
diff --git a/packages/builder/cypress/integration/queryLevelTransformers.spec.js b/packages/builder/cypress/integration/queryLevelTransformers.spec.js
index 2b74e0c2e5..d16f8075f9 100644
--- a/packages/builder/cypress/integration/queryLevelTransformers.spec.js
+++ b/packages/builder/cypress/integration/queryLevelTransformers.spec.js
@@ -1,5 +1,5 @@
import filterTests from "../support/filterTests"
-const interact = require('../support/interact')
+const interact = require("../support/interact")
filterTests(["smoke", "all"], () => {
context("Query Level Transformers", () => {
diff --git a/packages/builder/package.json b/packages/builder/package.json
index 687b571b66..900b33fb88 100644
--- a/packages/builder/package.json
+++ b/packages/builder/package.json
@@ -1,6 +1,6 @@
{
"name": "@budibase/builder",
- "version": "2.0.24-alpha.0",
+ "version": "2.0.24-alpha.3",
"license": "GPL-3.0",
"private": true,
"scripts": {
@@ -71,10 +71,10 @@
}
},
"dependencies": {
- "@budibase/bbui": "2.0.24-alpha.0",
- "@budibase/client": "2.0.24-alpha.0",
- "@budibase/frontend-core": "2.0.24-alpha.0",
- "@budibase/string-templates": "2.0.24-alpha.0",
+ "@budibase/bbui": "2.0.24-alpha.3",
+ "@budibase/client": "2.0.24-alpha.3",
+ "@budibase/frontend-core": "2.0.24-alpha.3",
+ "@budibase/string-templates": "2.0.24-alpha.3",
"@sentry/browser": "5.19.1",
"@spectrum-css/page": "^3.0.1",
"@spectrum-css/vars": "^3.0.1",
diff --git a/packages/builder/src/builderStore/componentUtils.js b/packages/builder/src/builderStore/componentUtils.js
index e7787dea72..54997df38f 100644
--- a/packages/builder/src/builderStore/componentUtils.js
+++ b/packages/builder/src/builderStore/componentUtils.js
@@ -185,43 +185,42 @@ export const makeComponentUnique = component => {
// Replace component ID
const oldId = component._id
const newId = Helpers.uuid()
- component._id = newId
+ let definition = JSON.stringify(component)
- if (component._children?.length) {
- let children = JSON.stringify(component._children)
+ // Replace all instances of this ID in HBS bindings
+ definition = definition.replace(new RegExp(oldId, "g"), newId)
- // Replace all instances of this ID in child HBS bindings
- children = children.replace(new RegExp(oldId, "g"), newId)
+ // Replace all instances of this ID in JS bindings
+ const bindings = findHBSBlocks(definition)
+ bindings.forEach(binding => {
+ // JSON.stringify will have escaped double quotes, so we need
+ // to account for that
+ let sanitizedBinding = binding.replace(/\\"/g, '"')
- // Replace all instances of this ID in child JS bindings
- const bindings = findHBSBlocks(children)
- bindings.forEach(binding => {
- // JSON.stringify will have escaped double quotes, so we need
- // to account for that
- let sanitizedBinding = binding.replace(/\\"/g, '"')
+ // Check if this is a valid JS binding
+ let js = decodeJSBinding(sanitizedBinding)
+ if (js != null) {
+ // Replace ID inside JS binding
+ js = js.replace(new RegExp(oldId, "g"), newId)
- // Check if this is a valid JS binding
- let js = decodeJSBinding(sanitizedBinding)
- if (js != null) {
- // Replace ID inside JS binding
- js = js.replace(new RegExp(oldId, "g"), newId)
+ // Create new valid JS binding
+ let newBinding = encodeJSBinding(js)
- // Create new valid JS binding
- let newBinding = encodeJSBinding(js)
+ // Replace escaped double quotes
+ newBinding = newBinding.replace(/"/g, '\\"')
- // Replace escaped double quotes
- newBinding = newBinding.replace(/"/g, '\\"')
+ // Insert new JS back into binding.
+ // A single string replace here is better than a regex as
+ // the binding contains special characters, and we only need
+ // to replace a single instance.
+ definition = definition.replace(binding, newBinding)
+ }
+ })
- // Insert new JS back into binding.
- // A single string replace here is better than a regex as
- // the binding contains special characters, and we only need
- // to replace a single instance.
- children = children.replace(binding, newBinding)
- }
- })
-
- // Recurse on all children
- component._children = JSON.parse(children)
- component._children.forEach(makeComponentUnique)
+ // Recurse on all children
+ component = JSON.parse(definition)
+ return {
+ ...component,
+ _children: component._children?.map(makeComponentUnique),
}
}
diff --git a/packages/builder/src/builderStore/dataBinding.js b/packages/builder/src/builderStore/dataBinding.js
index c83daa7807..536692eecc 100644
--- a/packages/builder/src/builderStore/dataBinding.js
+++ b/packages/builder/src/builderStore/dataBinding.js
@@ -169,7 +169,12 @@ export const getComponentBindableProperties = (asset, componentId) => {
/**
* Gets all data provider components above a component.
*/
-export const getContextProviderComponents = (asset, componentId, type) => {
+export const getContextProviderComponents = (
+ asset,
+ componentId,
+ type,
+ options = { includeSelf: false }
+) => {
if (!asset || !componentId) {
return []
}
@@ -177,7 +182,9 @@ export const getContextProviderComponents = (asset, componentId, type) => {
// Get the component tree leading up to this component, ignoring the component
// itself
const path = findComponentPath(asset.props, componentId)
- path.pop()
+ if (!options?.includeSelf) {
+ path.pop()
+ }
// Filter by only data provider components
return path.filter(component => {
@@ -798,6 +805,17 @@ export const buildFormSchema = component => {
if (!component) {
return schema
}
+
+ // If this is a form block, simply use the fields setting
+ if (component._component.endsWith("formblock")) {
+ let schema = {}
+ component.fields?.forEach(field => {
+ schema[field] = { type: "string" }
+ })
+ return schema
+ }
+
+ // Otherwise find all field component children
const settings = getComponentSettings(component._component)
const fieldSetting = settings.find(
setting => setting.key === "field" && setting.type.startsWith("field/")
diff --git a/packages/builder/src/builderStore/store/frontend.js b/packages/builder/src/builderStore/store/frontend.js
index aefdba9fb2..c90ab10c9a 100644
--- a/packages/builder/src/builderStore/store/frontend.js
+++ b/packages/builder/src/builderStore/store/frontend.js
@@ -330,6 +330,16 @@ export const getFrontendStore = () => {
return state
})
},
+ sendEvent: (name, payload) => {
+ const { previewEventHandler } = get(store)
+ previewEventHandler?.(name, payload)
+ },
+ registerEventHandler: handler => {
+ store.update(state => {
+ state.previewEventHandler = handler
+ return state
+ })
+ },
},
layouts: {
select: layoutId => {
@@ -611,7 +621,7 @@ export const getFrontendStore = () => {
// Make new component unique if copying
if (!cut) {
- makeComponentUnique(componentToPaste)
+ componentToPaste = makeComponentUnique(componentToPaste)
}
newComponentId = componentToPaste._id
@@ -891,6 +901,50 @@ export const getFrontendStore = () => {
component[name] = value
})
},
+ requestEjectBlock: componentId => {
+ store.actions.preview.sendEvent("eject-block", componentId)
+ },
+ handleEjectBlock: async (componentId, ejectedDefinition) => {
+ let nextSelectedComponentId
+
+ await store.actions.screens.patch(screen => {
+ const block = findComponent(screen.props, componentId)
+ const parent = findComponentParent(screen.props, componentId)
+
+ // Sanity check
+ if (!block || !parent?._children?.length) {
+ return false
+ }
+
+ // Attach block children back into ejected definition, using the
+ // _containsSlot flag to know where to insert them
+ const slotContainer = findAllMatchingComponents(
+ ejectedDefinition,
+ x => x._containsSlot
+ )[0]
+ if (slotContainer) {
+ delete slotContainer._containsSlot
+ slotContainer._children = [
+ ...(slotContainer._children || []),
+ ...(block._children || []),
+ ]
+ }
+
+ // Replace block with ejected definition
+ ejectedDefinition = makeComponentUnique(ejectedDefinition)
+ const index = parent._children.findIndex(x => x._id === componentId)
+ parent._children[index] = ejectedDefinition
+ nextSelectedComponentId = ejectedDefinition._id
+ })
+
+ // Select new root component
+ if (nextSelectedComponentId) {
+ store.update(state => {
+ state.selectedComponentId = nextSelectedComponentId
+ return state
+ })
+ }
+ },
},
links: {
save: async (url, title) => {
diff --git a/packages/builder/src/components/design/settings/controls/ButtonActionEditor/ButtonActionDrawer.svelte b/packages/builder/src/components/design/settings/controls/ButtonActionEditor/ButtonActionDrawer.svelte
index 69d5fe60b4..ef7c81233b 100644
--- a/packages/builder/src/components/design/settings/controls/ButtonActionEditor/ButtonActionDrawer.svelte
+++ b/packages/builder/src/components/design/settings/controls/ButtonActionEditor/ButtonActionDrawer.svelte
@@ -21,6 +21,7 @@
export let key
export let actions
export let bindings = []
+ export let nested
$: showAvailableActions = !actions?.length
@@ -187,6 +188,7 @@
this={selectedActionComponent}
parameters={selectedAction.parameters}
bindings={allBindings}
+ {nested}
/>
{/key}
diff --git a/packages/builder/src/components/design/settings/controls/ButtonActionEditor/ButtonActionEditor.svelte b/packages/builder/src/components/design/settings/controls/ButtonActionEditor/ButtonActionEditor.svelte
index f8fb385eb3..6a23ba8cbd 100644
--- a/packages/builder/src/components/design/settings/controls/ButtonActionEditor/ButtonActionEditor.svelte
+++ b/packages/builder/src/components/design/settings/controls/ButtonActionEditor/ButtonActionEditor.svelte
@@ -12,6 +12,7 @@
export let value = []
export let name
export let bindings
+ export let nested
let drawer
let tmpValue
@@ -90,6 +91,7 @@
eventType={name}
{bindings}
{key}
+ {nested}
/>
diff --git a/packages/builder/src/components/design/settings/controls/ButtonActionEditor/actions/SaveRow.svelte b/packages/builder/src/components/design/settings/controls/ButtonActionEditor/actions/SaveRow.svelte
index 174962d824..433f4bb3c2 100644
--- a/packages/builder/src/components/design/settings/controls/ButtonActionEditor/actions/SaveRow.svelte
+++ b/packages/builder/src/components/design/settings/controls/ButtonActionEditor/actions/SaveRow.svelte
@@ -10,11 +10,13 @@
export let parameters
export let bindings = []
+ export let nested
$: formComponents = getContextProviderComponents(
$currentAsset,
$store.selectedComponentId,
- "form"
+ "form",
+ { includeSelf: nested }
)
$: schemaComponents = getContextProviderComponents(
$currentAsset,
diff --git a/packages/builder/src/components/design/settings/controls/EjectBlockButton.svelte b/packages/builder/src/components/design/settings/controls/EjectBlockButton.svelte
new file mode 100644
index 0000000000..e19d4b584b
--- /dev/null
+++ b/packages/builder/src/components/design/settings/controls/EjectBlockButton.svelte
@@ -0,0 +1,13 @@
+
+
+
diff --git a/packages/builder/src/components/design/settings/controls/PropertyControl.svelte b/packages/builder/src/components/design/settings/controls/PropertyControl.svelte
index 3927e0b3a5..70b88c41e5 100644
--- a/packages/builder/src/components/design/settings/controls/PropertyControl.svelte
+++ b/packages/builder/src/components/design/settings/controls/PropertyControl.svelte
@@ -20,6 +20,7 @@
export let componentBindings = []
export let nested = false
export let highlighted = false
+ export let info = null
$: nullishValue = value == null || value === ""
$: allBindings = getAllBindings(bindings, componentBindings, nested)
@@ -94,11 +95,15 @@
bindings={allBindings}
name={key}
text={label}
+ {nested}
{key}
{type}
{...props}
/>
+ {#if info}
+ {@html info}
+ {/if}
diff --git a/packages/builder/src/components/design/settings/controls/URLSelect.svelte b/packages/builder/src/components/design/settings/controls/URLSelect.svelte
index dc2fa7ad89..a07c2190da 100644
--- a/packages/builder/src/components/design/settings/controls/URLSelect.svelte
+++ b/packages/builder/src/components/design/settings/controls/URLSelect.svelte
@@ -4,6 +4,7 @@
export let value
export let bindings
+ export let placeholder
$: urlOptions = $store.screens
.map(screen => screen.routing?.route)
@@ -13,6 +14,7 @@
{
+ iframe?.contentWindow.postMessage(
+ JSON.stringify({
+ name,
+ payload,
+ isBudibaseEvent: true,
+ runtimeEvent: true,
+ })
+ )
+ })
+
// Update the iframe with the builder info to render the correct preview
const refreshContent = message => {
- if (iframe) {
- iframe.contentWindow.postMessage(message)
- }
+ iframe?.contentWindow.postMessage(message)
}
const receiveMessage = message => {
@@ -198,6 +208,9 @@
block: "center",
})
}
+ } else if (type === "eject-block") {
+ const { id, definition } = data
+ await store.actions.components.handleEjectBlock(id, definition)
} else if (type === "reload-plugin") {
await store.actions.components.refreshDefinitions()
} else {
diff --git a/packages/builder/src/pages/builder/app/[application]/design/[screenId]/components/[componentId]/_components/navigation/ComponentDropdownMenu.svelte b/packages/builder/src/pages/builder/app/[application]/design/[screenId]/components/[componentId]/_components/navigation/ComponentDropdownMenu.svelte
index c19cba1aac..aeaa577455 100644
--- a/packages/builder/src/pages/builder/app/[application]/design/[screenId]/components/[componentId]/_components/navigation/ComponentDropdownMenu.svelte
+++ b/packages/builder/src/pages/builder/app/[application]/design/[screenId]/components/[componentId]/_components/navigation/ComponentDropdownMenu.svelte
@@ -4,7 +4,9 @@
export let component
+ $: definition = store.actions.components.getDefinition(component?._component)
$: noPaste = !$store.componentToPaste
+ $: isBlock = definition?.block === true
const keyboardEvent = (key, ctrlKey = false) => {
document.dispatchEvent(
@@ -30,6 +32,15 @@
>
Delete
+ {#if isBlock}
+
+ {/if}