diff --git a/docs/DEV-SETUP-DEBIAN.md b/docs/DEV-SETUP-DEBIAN.md
index 88a124708c..9edd8286cb 100644
--- a/docs/DEV-SETUP-DEBIAN.md
+++ b/docs/DEV-SETUP-DEBIAN.md
@@ -1,12 +1,15 @@
## Dev Environment on Debian 11
-### Install Node
+### Install NVM & Node 14
+NVM documentation: https://github.com/nvm-sh/nvm#installing-and-updating
-Budibase requires a recent version of node (14+):
+Install NVM
```
-curl -sL https://deb.nodesource.com/setup_16.x | sudo bash -
-apt -y install nodejs
-node -v
+curl -o- https://raw.githubusercontent.com/nvm-sh/nvm/v0.39.0/install.sh | bash
+```
+Install Node 14
+```
+nvm install 14
```
### Install npm requirements
@@ -31,7 +34,7 @@ This setup process was tested on Debian 11 (bullseye) with version numbers show
- Docker: 20.10.5
- Docker-Compose: 1.29.2
-- Node: v16.15.1
+- Node: v14.20.1
- Yarn: 1.22.19
- Lerna: 5.1.4
diff --git a/docs/DEV-SETUP-MACOSX.md b/docs/DEV-SETUP-MACOSX.md
index c5990e58da..d9e2dcad6a 100644
--- a/docs/DEV-SETUP-MACOSX.md
+++ b/docs/DEV-SETUP-MACOSX.md
@@ -11,7 +11,7 @@ through brew.
### Install Node
-Budibase requires a recent version of node (14+):
+Budibase requires a recent version of node 14:
```
brew install node npm
node -v
@@ -38,7 +38,7 @@ This setup process was tested on Mac OSX 12 (Monterey) with version numbers show
- Docker: 20.10.14
- Docker-Compose: 2.6.0
-- Node: 18.3.0
+- Node: 14.20.1
- Yarn: 1.22.19
- Lerna: 5.1.4
@@ -59,4 +59,7 @@ The dev version will be available on port 10000 i.e.
http://127.0.0.1:10000/builder/admin
| **NOTE**: If you are working on a M1 Apple Silicon, you will need to uncomment `# platform: linux/amd64` line in
-[hosting/docker-compose-dev.yaml](../hosting/docker-compose.dev.yaml)
\ No newline at end of file
+[hosting/docker-compose-dev.yaml](../hosting/docker-compose.dev.yaml)
+
+### Troubleshooting
+If there are errors with the `yarn setup` command, you can try installing nvm and node 14. This is the same as the instructions for Debian 11.
diff --git a/docs/DEV-SETUP-WINDOWS.md b/docs/DEV-SETUP-WINDOWS.md
new file mode 100644
index 0000000000..c5608b7567
--- /dev/null
+++ b/docs/DEV-SETUP-WINDOWS.md
@@ -0,0 +1,81 @@
+## Dev Environment on Windows 10/11 (WSL2)
+
+
+### Install WSL with Ubuntu LTS
+
+Enable WSL 2 on Windows 10/11 for docker support.
+```
+wsl --set-default-version 2
+```
+Install Ubuntu LTS.
+```
+wsl --install Ubuntu
+```
+
+Or follow the instruction here:
+https://learn.microsoft.com/en-us/windows/wsl/install
+
+### Install Docker in windows
+Download the installer from docker and install it.
+
+Check this url for more detailed instructions:
+https://docs.docker.com/desktop/install/windows-install/
+
+You should follow the next steps from within the Ubuntu terminal.
+
+### Install NVM & Node 14
+NVM documentation: https://github.com/nvm-sh/nvm#installing-and-updating
+
+Install NVM
+```
+curl -o- https://raw.githubusercontent.com/nvm-sh/nvm/v0.39.0/install.sh | bash
+```
+Install Node 14
+```
+nvm install 14
+```
+
+
+### Install npm requirements
+
+```
+npm install -g yarn jest lerna
+```
+
+### Clone the repo
+```
+git clone https://github.com/Budibase/budibase.git
+```
+
+### Check Versions
+
+This setup process was tested on Windows 11 with version numbers show below. Your mileage may vary using anything else.
+
+- Docker: 20.10.7
+- Docker-Compose: 2.10.2
+- Node: v14.20.1
+- Yarn: 1.22.19
+- Lerna: 5.5.4
+
+### Build
+
+```
+cd budibase
+yarn setup
+```
+The yarn setup command runs several build steps i.e.
+```
+node ./hosting/scripts/setup.js && yarn && yarn bootstrap && yarn build && yarn dev
+```
+So this command will actually run the application in dev mode. It creates .env files under `./packages/server` and `./packages/worker` and runs docker containers for each service via docker-compose.
+
+The dev version will be available on port 10000 i.e.
+
+http://127.0.0.1:10000/builder/admin
+
+### Working with the code
+Here are the instructions to work on the application from within Visual Studio Code (in Windows) through the WSL. All the commands and files are within the Ubuntu system and it should run as if you were working on a Linux machine.
+
+https://code.visualstudio.com/docs/remote/wsl
+
+Note you will be able to run the application from within the WSL terminal and you will be able to access the application from the a browser in Windows.
\ No newline at end of file
diff --git a/hosting/single/Dockerfile b/hosting/single/Dockerfile
index f34290f627..58796f0362 100644
--- a/hosting/single/Dockerfile
+++ b/hosting/single/Dockerfile
@@ -19,8 +19,8 @@ ADD packages/worker .
RUN node /pinVersions.js && yarn && yarn build && /cleanup.sh
FROM couchdb:3.2.1
-# TARGETARCH can be amd64 or arm e.g. docker build --build-arg TARGETARCH=amd64
-ARG TARGETARCH=amd64
+ARG TARGETARCH
+ENV TARGETARCH $TARGETARCH
#TARGETBUILD can be set to single (for single docker image) or aas (for azure app service)
# e.g. docker build --build-arg TARGETBUILD=aas ....
ARG TARGETBUILD=single
diff --git a/lerna.json b/lerna.json
index 469bc51be7..a47d8fe604 100644
--- a/lerna.json
+++ b/lerna.json
@@ -1,5 +1,5 @@
{
- "version": "2.0.14-alpha.4",
+ "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 76e724b77d..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.14-alpha.4",
+ "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.14-alpha.4",
+ "@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 01e1a5f17e..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.14-alpha.4",
+ "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.14-alpha.4",
+ "@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 c838f05b8c..900b33fb88 100644
--- a/packages/builder/package.json
+++ b/packages/builder/package.json
@@ -1,6 +1,6 @@
{
"name": "@budibase/builder",
- "version": "2.0.14-alpha.4",
+ "version": "2.0.24-alpha.3",
"license": "GPL-3.0",
"private": true,
"scripts": {
@@ -71,10 +71,10 @@
}
},
"dependencies": {
- "@budibase/bbui": "2.0.14-alpha.4",
- "@budibase/client": "2.0.14-alpha.4",
- "@budibase/frontend-core": "2.0.14-alpha.4",
- "@budibase/string-templates": "2.0.14-alpha.4",
+ "@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 7456ec5691..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 => {
@@ -396,19 +403,17 @@ export const getUserBindings = () => {
bindings = keys.reduce((acc, key) => {
const fieldSchema = schema[key]
- if (fieldSchema.type !== "link") {
- acc.push({
- type: "context",
- runtimeBinding: `${safeUser}.${makePropSafe(key)}`,
- readableBinding: `Current User.${key}`,
- // Field schema and provider are required to construct relationship
- // datasource options, based on bindable properties
- fieldSchema,
- providerId: "user",
- category: "Current User",
- icon: "User",
- })
- }
+ acc.push({
+ type: "context",
+ runtimeBinding: `${safeUser}.${makePropSafe(key)}`,
+ readableBinding: `Current User.${key}`,
+ // Field schema and provider are required to construct relationship
+ // datasource options, based on bindable properties
+ fieldSchema,
+ providerId: "user",
+ category: "Current User",
+ icon: "User",
+ })
return acc
}, [])
@@ -800,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}