Merge remote-tracking branch 'origin/develop' into feature/qr-barcode-reader

This commit is contained in:
Dean 2022-10-10 09:08:48 +01:00
commit 91e63ca2c9
73 changed files with 1498 additions and 691 deletions

View File

@ -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

View File

@ -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
@ -60,3 +60,6 @@ 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)
### 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.

81
docs/DEV-SETUP-WINDOWS.md Normal file
View File

@ -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.

View File

@ -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

View File

@ -1,5 +1,5 @@
{
"version": "2.0.14-alpha.4",
"version": "2.0.24-alpha.3",
"npmClient": "yarn",
"packages": [
"packages/*"

View File

@ -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",

View File

@ -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") {

View File

@ -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",

View File

@ -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", () => {

View File

@ -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",

View File

@ -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),
}
}

View File

@ -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/")

View File

@ -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) => {

View File

@ -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}
/>
</div>
{/key}

View File

@ -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}
/>
</Drawer>

View File

@ -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,

View File

@ -0,0 +1,13 @@
<script>
import { ActionButton } from "@budibase/bbui"
const eject = () => {
document.dispatchEvent(
new KeyboardEvent("keydown", { key: "e", ctrlKey: true })
)
}
</script>
<div>
<ActionButton secondary on:click={eject}>Eject block</ActionButton>
</div>

View File

@ -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}
/>
</div>
{#if info}
<div class="text">{@html info}</div>
{/if}
</div>
<style>
@ -123,4 +128,9 @@
.control {
position: relative;
}
.text {
margin-top: var(--spectrum-global-dimension-size-65);
font-size: var(--spectrum-global-dimension-font-size-75);
color: var(--grey-6);
}
</style>

View File

@ -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 @@
<DrawerBindableCombobox
{value}
{bindings}
{placeholder}
on:change
options={urlOptions}
appendBindingsAsOptions={false}

View File

@ -98,11 +98,21 @@
`./components/${$selectedComponent?._id}/new`
)
// Register handler to send custom to the preview
$: store.actions.preview.registerEventHandler((name, payload) => {
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 {

View File

@ -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
</MenuItem>
{#if isBlock}
<MenuItem
icon="Export"
keyBind="Ctrl+E"
on:click={() => keyboardEvent("e", true)}
>
Eject block
</MenuItem>
{/if}
<MenuItem
icon="ChevronUp"
keyBind="Ctrl+!ArrowUp"

View File

@ -7,7 +7,9 @@
import ConfirmDialog from "components/common/ConfirmDialog.svelte"
let confirmDeleteDialog
let confirmEjectDialog
let componentToDelete
let componentToEject
const keyHandlers = {
["^ArrowUp"]: async component => {
@ -29,6 +31,10 @@
store.actions.components.copy(component)
await store.actions.components.paste(component, "below")
},
["^e"]: component => {
componentToEject = component
confirmEjectDialog.show()
},
["^Enter"]: () => {
$goto("./new")
},
@ -124,3 +130,10 @@
okText="Delete Component"
onOk={() => store.actions.components.delete(componentToDelete)}
/>
<ConfirmDialog
bind:this={confirmEjectDialog}
title="Eject block"
body={`Ejecting a block breaks it down into multiple components and cannot be undone. Are you sure you want to eject "${componentToEject?._instanceName}"?`}
onOk={() => store.actions.components.requestEjectBlock(componentToEject?._id)}
okText="Eject block"
/>

View File

@ -4,6 +4,7 @@
import { store } from "builderStore"
import PropertyControl from "components/design/settings/controls/PropertyControl.svelte"
import ResetFieldsButton from "components/design/settings/controls/ResetFieldsButton.svelte"
import EjectBlockButton from "components/design/settings/controls/EjectBlockButton.svelte"
import { getComponentForSetting } from "components/design/settings/componentSettings"
export let componentDefinition
@ -12,20 +13,29 @@
export let componentBindings
export let isScreen = false
$: sections = getSections(componentDefinition)
$: sections = getSections(componentInstance, componentDefinition, isScreen)
const getSections = definition => {
const getSections = (instance, definition, isScreen) => {
const settings = definition?.settings ?? []
const generalSettings = settings.filter(setting => !setting.section)
const customSections = settings.filter(setting => setting.section)
return [
let sections = [
{
name: "General",
info: componentDefinition?.info,
settings: generalSettings,
},
...(customSections || []),
]
// Filter out settings which shouldn't be rendered
sections.forEach(section => {
section.settings.forEach(setting => {
setting.visible = canRenderControl(instance, setting, isScreen)
})
section.visible = section.settings.some(setting => setting.visible)
})
return sections
}
const updateSetting = async (key, value) => {
@ -36,7 +46,7 @@
}
}
const canRenderControl = (setting, isScreen) => {
const canRenderControl = (instance, setting, isScreen) => {
// Prevent rendering on click setting for screens
if (setting?.type === "event" && isScreen) {
return false
@ -51,6 +61,7 @@
if (setting.dependsOn) {
let dependantSetting = setting.dependsOn
let dependantValue = null
let invert = !!setting.dependsOn.invert
if (typeof setting.dependsOn === "object") {
dependantSetting = setting.dependsOn.setting
dependantValue = setting.dependsOn.value
@ -62,7 +73,7 @@
// If no specific value is depended upon, check if a value exists at all
// for the dependent setting
if (dependantValue == null) {
const currentValue = componentInstance[dependantSetting]
const currentValue = instance[dependantSetting]
if (currentValue === false) {
return false
}
@ -73,7 +84,11 @@
}
// Otherwise check the value matches
return componentInstance[dependantSetting] === dependantValue
if (invert) {
return instance[dependantSetting] !== dependantValue
} else {
return instance[dependantSetting] === dependantValue
}
}
return true
@ -81,60 +96,54 @@
</script>
{#each sections as section, idx (section.name)}
<DetailSummary name={section.name} collapsible={false}>
{#if idx === 0 && !componentInstance._component.endsWith("/layout") && !isScreen}
<PropertyControl
control={Input}
label="Name"
key="_instanceName"
value={componentInstance._instanceName}
onChange={val => updateSetting("_instanceName", val)}
/>
{/if}
{#each section.settings as setting (setting.key)}
{#if canRenderControl(setting, isScreen)}
{#if section.visible}
<DetailSummary name={section.name} collapsible={false}>
{#if idx === 0 && !componentInstance._component.endsWith("/layout") && !isScreen}
<PropertyControl
type={setting.type}
control={getComponentForSetting(setting)}
label={setting.label}
key={setting.key}
value={componentInstance[setting.key]}
defaultValue={setting.defaultValue}
nested={setting.nested}
onChange={val => updateSetting(setting.key, val)}
highlighted={$store.highlightedSettingKey === setting.key}
props={{
// Generic settings
placeholder: setting.placeholder || null,
// Select settings
options: setting.options || [],
// Number fields
min: setting.min || null,
max: setting.max || null,
}}
{bindings}
{componentBindings}
{componentInstance}
{componentDefinition}
control={Input}
label="Name"
key="_instanceName"
value={componentInstance._instanceName}
onChange={val => updateSetting("_instanceName", val)}
/>
{/if}
{/each}
{#if idx === 0 && componentDefinition?.component?.endsWith("/fieldgroup")}
<ResetFieldsButton {componentInstance} />
{/if}
{#if section?.info}
<div class="text">
{@html section.info}
</div>
{/if}
</DetailSummary>
{/each}
{#each section.settings as setting (setting.key)}
{#if setting.visible}
<PropertyControl
type={setting.type}
control={getComponentForSetting(setting)}
label={setting.label}
key={setting.key}
value={componentInstance[setting.key]}
defaultValue={setting.defaultValue}
nested={setting.nested}
onChange={val => updateSetting(setting.key, val)}
highlighted={$store.highlightedSettingKey === setting.key}
info={setting.info}
props={{
// Generic settings
placeholder: setting.placeholder || null,
<style>
.text {
font-size: var(--spectrum-global-dimension-font-size-75);
color: var(--grey-6);
}
</style>
// Select settings
options: setting.options || [],
// Number fields
min: setting.min || null,
max: setting.max || null,
}}
{bindings}
{componentBindings}
{componentInstance}
{componentDefinition}
/>
{/if}
{/each}
{#if idx === 0 && componentDefinition?.component?.endsWith("/fieldgroup")}
<ResetFieldsButton {componentInstance} />
{/if}
{#if idx === 0 && componentDefinition?.block}
<EjectBlockButton />
{/if}
</DetailSummary>
{/if}
{/each}

View File

@ -5,7 +5,8 @@
"children": [
"tableblock",
"cardsblock",
"repeaterblock"
"repeaterblock",
"formblock"
]
},
{

View File

@ -38,7 +38,7 @@
let duplicateScreen = Helpers.cloneDeep(screen)
delete duplicateScreen._id
delete duplicateScreen._rev
makeComponentUnique(duplicateScreen.props)
duplicateScreen.props = makeComponentUnique(duplicateScreen.props)
// Attach the new name and URL
duplicateScreen.routing.route = sanitizeUrl(screenUrl)

View File

@ -1,6 +1,6 @@
{
"name": "@budibase/cli",
"version": "2.0.14-alpha.4",
"version": "2.0.24-alpha.3",
"description": "Budibase CLI, for developers, self hosting and migrations.",
"main": "src/index.js",
"bin": {
@ -26,9 +26,9 @@
"outputPath": "build"
},
"dependencies": {
"@budibase/backend-core": "2.0.14-alpha.4",
"@budibase/string-templates": "2.0.14-alpha.4",
"@budibase/types": "2.0.14-alpha.4",
"@budibase/backend-core": "2.0.24-alpha.3",
"@budibase/string-templates": "2.0.24-alpha.3",
"@budibase/types": "2.0.24-alpha.3",
"axios": "0.21.2",
"chalk": "4.1.0",
"cli-progress": "3.11.2",

View File

@ -3493,7 +3493,6 @@
},
"s3upload": {
"name": "S3 File Upload",
"info": "This component can't be used with S3 datasources that use custom endpoints.",
"icon": "UploadToCloud",
"styles": [
"size"
@ -3514,7 +3513,8 @@
{
"type": "dataSource/s3",
"label": "S3 Datasource",
"key": "datasourceId"
"key": "datasourceId",
"info": "This component can't be used with S3 datasources that use custom endpoints"
},
{
"type": "text",
@ -3552,7 +3552,6 @@
},
"dataprovider": {
"name": "Data Provider",
"info": "Pagination is only available for data stored in tables.",
"icon": "Data",
"illegalChildren": [
"section"
@ -3598,7 +3597,8 @@
"type": "boolean",
"label": "Paginate",
"key": "paginate",
"defaultValue": true
"defaultValue": true,
"info": "Pagination is only available for data stored in tables"
}
],
"context": {
@ -3640,7 +3640,6 @@
],
"hasChildren": true,
"showEmptyState": false,
"info": "Row selection is only compatible with internal or SQL tables",
"settings": [
{
"type": "dataProvider",
@ -3697,7 +3696,8 @@
"type": "boolean",
"label": "Allow row selection",
"key": "allowSelectRows",
"defaultValue": false
"defaultValue": false,
"info": "Row selection is only compatible with internal or SQL tables"
},
{
"type": "boolean",
@ -3738,13 +3738,13 @@
"size"
],
"hasChildren": false,
"info": "Your data provider will be automatically filtered to the given date range.",
"settings": [
{
"type": "dataProvider",
"label": "Provider",
"key": "dataProvider",
"required": true
"required": true,
"info": "Your data provider will be automatically filtered to the given date range."
},
{
"type": "field",
@ -3879,7 +3879,6 @@
"styles": [
"size"
],
"info": "Only the first 3 search columns will be used.",
"settings": [
{
"type": "text",
@ -3896,7 +3895,8 @@
"type": "searchfield",
"label": "Search Columns",
"key": "searchColumns",
"placeholder": "Choose search columns"
"placeholder": "Choose search columns",
"info": "Only the first 5 search columns will be used"
},
{
"type": "filter",
@ -3943,7 +3943,6 @@
{
"section": true,
"name": "Table",
"info": "Row selection is only compatible with internal or SQL tables",
"settings": [
{
"type": "number",
@ -3977,7 +3976,8 @@
{
"type": "boolean",
"label": "Allow row selection",
"key": "allowSelectRows"
"key": "allowSelectRows",
"info": "Row selection is only compatible with internal or SQL tables"
},
{
"type": "boolean",
@ -4044,7 +4044,6 @@
"styles": [
"size"
],
"info": "Only the first 3 search columns will be used.",
"settings": [
{
"type": "text",
@ -4061,7 +4060,8 @@
"type": "searchfield",
"label": "Search Columns",
"key": "searchColumns",
"placeholder": "Choose search columns"
"placeholder": "Choose search columns",
"info": "Only the first 5 search columns will be used"
},
{
"type": "filter",
@ -4208,6 +4208,7 @@
}
},
"repeaterblock": {
"block": true,
"name": "Repeater block",
"icon": "ViewList",
"illegalChildren": [
@ -4445,5 +4446,145 @@
"required": true
}
]
},
"formblock": {
"name": "Form Block",
"icon": "Form",
"styles": ["size"],
"block": true,
"info": "Form blocks are only compatible with internal or SQL tables",
"settings": [
{
"type": "select",
"label": "Type",
"key": "actionType",
"options": ["Create", "Update", "View"],
"defaultValue": "Create"
},
{
"type": "table",
"label": "Table",
"key": "dataSource"
},
{
"type": "text",
"label": "Row ID",
"key": "rowId",
"nested": true,
"dependsOn": {
"setting": "actionType",
"value": "Create",
"invert": true
}
},
{
"type": "text",
"label": "Title",
"key": "title",
"nested": true
},
{
"type": "select",
"label": "Size",
"key": "size",
"options": [
{
"label": "Medium",
"value": "spectrum--medium"
},
{
"label": "Large",
"value": "spectrum--large"
}
],
"defaultValue": "spectrum--medium"
},
{
"section": true,
"name": "Fields",
"settings": [
{
"type": "multifield",
"label": "Fields",
"key": "fields"
},
{
"type": "select",
"label": "Field labels",
"key": "labelPosition",
"defaultValue": "left",
"options": [
{
"label": "Left",
"value": "left"
},
{
"label": "Above",
"value": "above"
}
]
},
{
"type": "boolean",
"label": "Disabled",
"key": "disabled",
"defaultValue": false,
"dependsOn": {
"setting": "actionType",
"value": "View",
"invert": true
}
}
]
},
{
"section": true,
"name": "Buttons",
"settings": [
{
"type": "boolean",
"label": "Show save button",
"key": "showSaveButton",
"defaultValue": true,
"dependsOn": {
"setting": "actionType",
"value": "View",
"invert": true
}
},
{
"type": "boolean",
"label": "Show delete button",
"key": "showDeleteButton",
"defaultValue": false,
"dependsOn": {
"setting": "actionType",
"value": "Update"
}
},
{
"type": "url",
"label": "Navigate after button press",
"key": "actionUrl",
"placeholder": "Choose a screen",
"dependsOn": {
"setting": "actionType",
"value": "View",
"invert": true
}
}
]
}
],
"context": [
{
"type": "form",
"suffix": "form"
},
{
"type": "schema",
"suffix": "repeater"
}
]
}
}

View File

@ -1,6 +1,6 @@
{
"name": "@budibase/client",
"version": "2.0.14-alpha.4",
"version": "2.0.24-alpha.3",
"license": "MPL-2.0",
"module": "dist/budibase-client.js",
"main": "dist/budibase-client.js",
@ -19,9 +19,9 @@
"dev:builder": "rollup -cw"
},
"dependencies": {
"@budibase/bbui": "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/frontend-core": "2.0.24-alpha.3",
"@budibase/string-templates": "2.0.24-alpha.3",
"@spectrum-css/button": "^3.0.3",
"@spectrum-css/card": "^3.0.3",
"@spectrum-css/divider": "^1.0.3",

View File

@ -1,5 +1,7 @@
import { createAPIClient } from "@budibase/frontend-core"
import { notificationStore, authStore, devToolsStore } from "../stores"
import { notificationStore } from "../stores/notification.js"
import { authStore } from "../stores/auth.js"
import { devToolsStore } from "../stores/devTools.js"
import { get } from "svelte/store"
export const API = createAPIClient({

View File

@ -1,12 +1,92 @@
<script>
import { getContext, setContext } from "svelte"
import { getContext, onDestroy, onMount, setContext } from "svelte"
import { builderStore } from "stores/builder.js"
import { blockStore } from "stores/blocks.js"
const component = getContext("component")
const { styleable } = getContext("sdk")
// We need to set a block context to know we're inside a block, but also
// to be able to reference the actual component ID of the block from
// any depth
setContext("block", { id: $component.id })
let structureLookupMap = {}
const registerBlockComponent = (id, order, parentId, instance) => {
// Ensure child array exists
if (!structureLookupMap[parentId]) {
structureLookupMap[parentId] = {}
}
// Add this instance in this order, overwriting any existing instance in
// this order in case of repeaters
structureLookupMap[parentId][order] = instance
}
const eject = () => {
// Start the new structure with the root component
let definition = structureLookupMap[$component.id][0]
// Copy styles from block to root component
definition._styles = {
...definition._styles,
normal: {
...definition._styles?.normal,
...$component.styles?.normal,
},
custom:
definition._styles?.custom || "" + $component.styles?.custom || "",
}
// Create component tree
attachChildren(definition, structureLookupMap)
builderStore.actions.ejectBlock($component.id, definition)
}
const attachChildren = (rootComponent, map) => {
// Transform map into children array
let id = rootComponent._id
const children = Object.entries(map[id] || {}).map(([order, instance]) => ({
order,
instance,
}))
if (!children.length) {
return
}
// Sort children by order
children.sort((a, b) => (a.order < b.order ? -1 : 1))
// Attach all children of this component
rootComponent._children = children.map(x => x.instance)
// Recurse for each child
rootComponent._children.forEach(child => {
attachChildren(child, map)
})
}
setContext("block", {
// We need to set a block context to know we're inside a block, but also
// to be able to reference the actual component ID of the block from
// any depth
id: $component.id,
// We register block components with their raw props so that we can eject
// blocks later on
registerComponent: registerBlockComponent,
})
onMount(() => {
// We register and unregister blocks to the block store when inside the
// builder preview to allow for block ejection
if ($builderStore.inBuilder) {
blockStore.actions.registerBlock($component.id, { eject })
}
})
onDestroy(() => {
if ($builderStore.inBuilder) {
blockStore.actions.unregisterBlock($component.id)
}
})
</script>
<slot />
<div use:styleable={$component.styles}>
<slot />
</div>

View File

@ -1,17 +1,21 @@
<script>
import { getContext } from "svelte"
import { generate } from "shortid"
import { builderStore } from "../stores/builder.js"
import Component from "components/Component.svelte"
export let type
export let props
export let styles
export let context
export let order = 0
export let containsSlot = false
// ID is only exposed as a prop so that it can be bound to from parent
// block components
export let id
const component = getContext("component")
const block = getContext("block")
const rand = generate()
@ -21,13 +25,22 @@
$: instance = {
_component: `@budibase/standard-components/${type}`,
_id: id,
_instanceName: type[0].toUpperCase() + type.slice(1),
_styles: {
normal: {
...styles,
},
...styles,
normal: styles?.normal || {},
},
_containsSlot: containsSlot,
...props,
}
// Register this block component if we're inside the builder so it can be
// ejected later
$: {
if ($builderStore.inBuilder) {
block.registerComponent(id, order ?? 0, $component?.id, instance)
}
}
</script>
<Component {instance} isBlock>

View File

@ -16,7 +16,6 @@
themeStore,
appStore,
devToolsStore,
environmentStore,
} from "stores"
import NotificationDisplay from "components/overlay/NotificationDisplay.svelte"
import ConfirmationDisplay from "components/overlay/ConfirmationDisplay.svelte"
@ -48,8 +47,6 @@
!$builderStore.inBuilder &&
$devToolsStore.enabled &&
!$routeStore.queryParams?.peek
$: objectStoreUrl = $environmentStore.cloud ? "https://cdn.budi.live" : ""
$: pluginsUrl = `${objectStoreUrl}/plugins`
// Handle no matching route
$: {
@ -95,8 +92,7 @@
<svelte:head>
{#if $builderStore.usedPlugins?.length}
{#each $builderStore.usedPlugins as plugin (plugin.hash)}
<script
src={`${pluginsUrl}/${plugin.jsUrl}?r=${plugin.hash || ""}`}></script>
<script src={`${plugin.jsUrl}?r=${plugin.hash || ""}`}></script>
{/each}
{/if}
</svelte:head>

View File

@ -2,7 +2,6 @@
import { getContext } from "svelte"
import Block from "components/Block.svelte"
import BlockComponent from "components/BlockComponent.svelte"
import { Heading } from "@budibase/bbui"
import { makePropSafe as safe } from "@budibase/string-templates"
import { enrichSearchColumns, enrichFilter } from "utils/blocks.js"
@ -31,9 +30,7 @@
export let cardButtonOnClick
export let linkColumn
const { fetchDatasourceSchema, styleable } = getContext("sdk")
const context = getContext("context")
const component = getContext("component")
const { fetchDatasourceSchema } = getContext("sdk")
let formId
let dataProviderId
@ -84,163 +81,132 @@
{#if schemaLoaded}
<Block>
<div class="card-list" use:styleable={$component.styles}>
<BlockComponent
type="form"
bind:id={formId}
props={{ dataSource, disableValidation: true }}
>
{#if title || enrichedSearchColumns?.length || showTitleButton}
<div class="header" class:mobile={$context.device.mobile}>
<div class="title">
<Heading>{title || ""}</Heading>
</div>
<div class="controls">
{#if enrichedSearchColumns?.length}
<div
class="search"
style="--cols:{enrichedSearchColumns?.length}"
>
{#each enrichedSearchColumns as column}
<BlockComponent
type={column.componentType}
props={{
field: column.name,
placeholder: column.name,
text: column.name,
autoWidth: true,
}}
/>
{/each}
</div>
{/if}
{#if showTitleButton}
<BlockComponent
type="button"
props={{
onClick: titleButtonAction,
text: titleButtonText,
type: "cta",
}}
/>
{/if}
</div>
</div>
{/if}
<BlockComponent
type="form"
bind:id={formId}
props={{ dataSource, disableValidation: true }}
>
{#if title || enrichedSearchColumns?.length || showTitleButton}
<BlockComponent
type="dataprovider"
bind:id={dataProviderId}
type="container"
props={{
dataSource,
filter: enrichedFilter,
sortColumn,
sortOrder,
paginate,
limit,
direction: "row",
hAlign: "stretch",
vAlign: "middle",
gap: "M",
wrap: true,
}}
styles={{
normal: {
"margin-bottom": "20px",
},
}}
order={0}
>
<BlockComponent
type="repeater"
bind:id={repeaterId}
context="repeater"
type="heading"
props={{
text: title,
}}
order={0}
/>
<BlockComponent
type="container"
props={{
dataProvider: `{{ literal ${safe(dataProviderId)} }}`,
direction: "row",
hAlign: "stretch",
vAlign: "top",
hAlign: "left",
vAlign: "middle",
gap: "M",
noRowsMessage: "No rows found",
}}
styles={{
display: "grid",
"grid-template-columns": `repeat(auto-fill, minmax(min(${cardWidth}px, 100%), 1fr))`,
wrap: true,
}}
order={1}
>
<BlockComponent
type="spectrumcard"
props={{
title: cardTitle,
subtitle: cardSubtitle,
description: cardDescription,
imageURL: cardImageURL,
horizontal: cardHorizontal,
showButton: showCardButton,
buttonText: cardButtonText,
buttonOnClick: cardButtonOnClick,
linkURL: fullCardURL,
linkPeek: cardPeek,
}}
styles={{
width: "auto",
}}
/>
{#if enrichedSearchColumns?.length}
{#each enrichedSearchColumns as column, idx}
<BlockComponent
type={column.componentType}
props={{
field: column.name,
placeholder: column.name,
text: column.name,
autoWidth: true,
}}
order={idx}
styles={{
normal: {
width: "192px",
},
}}
/>
{/each}
{/if}
{#if showTitleButton}
<BlockComponent
type="button"
props={{
onClick: titleButtonAction,
text: titleButtonText,
type: "cta",
}}
order={enrichedSearchColumns?.length ?? 0}
/>
{/if}
</BlockComponent>
</BlockComponent>
{/if}
<BlockComponent
type="dataprovider"
bind:id={dataProviderId}
props={{
dataSource,
filter: enrichedFilter,
sortColumn,
sortOrder,
paginate,
limit,
}}
order={1}
>
<BlockComponent
type="repeater"
bind:id={repeaterId}
context="repeater"
props={{
dataProvider: `{{ literal ${safe(dataProviderId)} }}`,
direction: "row",
hAlign: "stretch",
vAlign: "top",
gap: "M",
noRowsMessage: "No rows found",
}}
styles={{
custom: `display: grid;\ngrid-template-columns: repeat(auto-fill, minmax(min(${cardWidth}px, 100%), 1fr));`,
}}
order={0}
>
<BlockComponent
type="spectrumcard"
props={{
title: cardTitle,
subtitle: cardSubtitle,
description: cardDescription,
imageURL: cardImageURL,
horizontal: cardHorizontal,
showButton: showCardButton,
buttonText: cardButtonText,
buttonOnClick: cardButtonOnClick,
linkURL: fullCardURL,
linkPeek: cardPeek,
}}
styles={{
normal: {
width: "auto",
},
}}
order={0}
/>
</BlockComponent>
</BlockComponent>
</div>
</BlockComponent>
</Block>
{/if}
<style>
.header {
display: flex;
flex-direction: row;
justify-content: space-between;
align-items: center;
gap: 20px;
margin-bottom: 20px;
}
.title {
overflow: hidden;
}
.title :global(.spectrum-Heading) {
flex: 1 1 auto;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
.controls {
flex: 0 1 auto;
display: flex;
flex-direction: row;
justify-content: flex-end;
align-items: center;
gap: 20px;
}
.controls :global(.spectrum-InputGroup .spectrum-InputGroup-input) {
width: 100%;
}
.search {
flex: 0 1 auto;
gap: 10px;
max-width: 100%;
display: grid;
grid-template-columns: repeat(var(--cols), minmax(120px, 200px));
}
.search :global(.spectrum-InputGroup) {
min-width: 0;
}
/* Mobile styles */
.mobile {
flex-direction: column;
justify-content: flex-start;
align-items: stretch;
}
.mobile .controls {
flex-direction: column-reverse;
justify-content: flex-start;
align-items: stretch;
}
.mobile .search {
display: flex;
flex-direction: column;
justify-content: flex-start;
align-items: stretch;
position: relative;
width: 100%;
}
</style>

View File

@ -0,0 +1,239 @@
<script>
import { getContext } from "svelte"
import BlockComponent from "../../BlockComponent.svelte"
import Block from "../../Block.svelte"
import Placeholder from "../Placeholder.svelte"
import { makePropSafe as safe } from "@budibase/string-templates"
export let actionType
export let dataSource
export let size
export let disabled
export let fields
export let labelPosition
export let title
export let showSaveButton
export let showDeleteButton
export let rowId
export let actionUrl
const { fetchDatasourceSchema, builderStore } = getContext("sdk")
const FieldTypeToComponentMap = {
string: "stringfield",
number: "numberfield",
options: "optionsfield",
array: "multifieldselect",
boolean: "booleanfield",
longform: "longformfield",
datetime: "datetimefield",
attachment: "attachmentfield",
link: "relationshipfield",
json: "jsonfield",
}
let schema
let formId
let providerId
let repeaterId
$: fetchSchema(dataSource)
$: onSave = [
{
"##eventHandlerType": "Save Row",
parameters: {
providerId: formId,
tableId: dataSource?.tableId,
},
},
{
"##eventHandlerType": "Close Screen Modal",
},
{
"##eventHandlerType": "Navigate To",
parameters: {
url: actionUrl,
},
},
]
$: onDelete = [
{
"##eventHandlerType": "Delete Row",
parameters: {
confirm: true,
tableId: dataSource?.tableId,
rowId: `{{ ${safe(repeaterId)}.${safe("_id")} }}`,
revId: `{{ ${safe(repeaterId)}.${safe("_rev")} }}`,
},
},
{
"##eventHandlerType": "Close Screen Modal",
},
{
"##eventHandlerType": "Navigate To",
parameters: {
url: actionUrl,
},
},
]
$: filter = [
{
field: "_id",
operator: "equal",
type: "string",
value: rowId,
valueType: "Binding",
},
]
// If we're using an "update" form, use the real data provider. If we're
// using a create form, we just want a fake array so that our repeater
// will actually render the form, but data doesn't matter.
$: dataProvider =
actionType !== "Create"
? `{{ literal ${safe(providerId)} }}`
: { rows: [{}] }
$: renderDeleteButton = showDeleteButton && actionType === "Update"
$: renderSaveButton = showSaveButton && actionType !== "View"
$: renderButtons = renderDeleteButton || renderSaveButton
$: renderHeader = renderButtons || title
const fetchSchema = async () => {
schema = (await fetchDatasourceSchema(dataSource)) || {}
}
const getComponentForField = field => {
if (!field || !schema?.[field]) {
return null
}
const type = schema[field].type
return FieldTypeToComponentMap[type]
}
</script>
<Block>
{#if fields?.length}
<BlockComponent
type="dataprovider"
context="provider"
bind:id={providerId}
props={{
dataSource,
filter,
limit: rowId ? 1 : $builderStore.inBuilder ? 1 : 0,
paginate: false,
}}
>
<BlockComponent
type="repeater"
context="repeater"
bind:id={repeaterId}
props={{
dataProvider,
noRowsMessage: "We couldn't find a row to display",
}}
>
<BlockComponent
type="form"
props={{
actionType: actionType === "Create" ? "Create" : "Update",
dataSource,
size,
disabled: disabled || actionType === "View",
}}
context="form"
bind:id={formId}
>
<BlockComponent
type="container"
props={{
direction: "column",
hAlign: "stretch",
vAlign: "top",
gap: "M",
}}
>
{#if renderHeader}
<BlockComponent
type="container"
props={{
direction: "row",
hAlign: "stretch",
vAlign: "center",
gap: "M",
wrap: true,
}}
order={0}
>
<BlockComponent
type="heading"
props={{ text: title || "" }}
order={0}
/>
{#if renderButtons}
<BlockComponent
type="container"
props={{
direction: "row",
hAlign: "stretch",
vAlign: "center",
gap: "M",
wrap: true,
}}
order={1}
>
{#if renderDeleteButton}
<BlockComponent
type="button"
props={{
text: "Delete",
onClick: onDelete,
quiet: true,
type: "secondary",
}}
order={0}
/>
{/if}
{#if renderSaveButton}
<BlockComponent
type="button"
props={{
text: "Save",
onClick: onSave,
type: "cta",
}}
order={1}
/>
{/if}
</BlockComponent>
{/if}
</BlockComponent>
{/if}
<BlockComponent
type="fieldgroup"
props={{ labelPosition }}
order={1}
>
{#each fields as field, idx}
{#if getComponentForField(field)}
<BlockComponent
type={getComponentForField(field)}
props={{
field,
label: field,
placeholder: field,
disabled,
}}
order={idx}
/>
{/if}
{/each}
</BlockComponent>
</BlockComponent>
</BlockComponent>
</BlockComponent>
</BlockComponent>
{:else}
<Placeholder
text="Choose your table and add some fields to your form to get started"
/>
{/if}
</Block>

View File

@ -17,45 +17,43 @@
export let vAlign
export let gap
let providerId
const component = getContext("component")
const { styleable } = getContext("sdk")
let providerId
</script>
<Block>
<div use:styleable={$component.styles}>
<BlockComponent
type="dataprovider"
context="provider"
bind:id={providerId}
props={{
dataSource,
filter,
sortColumn,
sortOrder,
limit,
paginate,
}}
>
{#if $component.empty}
<Placeholder />
{:else}
<BlockComponent
type="repeater"
context="repeater"
props={{
dataProvider: `{{ literal ${safe(providerId)} }}`,
noRowsMessage,
direction,
hAlign,
vAlign,
gap,
}}
>
<slot />
</BlockComponent>
{/if}
</BlockComponent>
</div>
<BlockComponent
type="dataprovider"
context="provider"
bind:id={providerId}
props={{
dataSource,
filter,
sortColumn,
sortOrder,
limit,
paginate,
}}
>
{#if $component.empty}
<Placeholder />
{:else}
<BlockComponent
type="repeater"
context="repeater"
containsSlot
props={{
dataProvider: `{{ literal ${safe(providerId)} }}`,
noRowsMessage,
direction,
hAlign,
vAlign,
gap,
}}
>
<slot />
</BlockComponent>
{/if}
</BlockComponent>
</Block>

View File

@ -2,7 +2,6 @@
import { getContext } from "svelte"
import Block from "components/Block.svelte"
import BlockComponent from "components/BlockComponent.svelte"
import { Heading } from "@budibase/bbui"
import { makePropSafe as safe } from "@budibase/string-templates"
import { enrichSearchColumns, enrichFilter } from "utils/blocks.js"
@ -29,9 +28,7 @@
export let titleButtonURL
export let titleButtonPeek
const { fetchDatasourceSchema, styleable } = getContext("sdk")
const context = getContext("context")
const component = getContext("component")
const { fetchDatasourceSchema } = getContext("sdk")
let formId
let dataProviderId
@ -64,145 +61,116 @@
{#if schemaLoaded}
<Block>
<div class={size} use:styleable={$component.styles}>
<BlockComponent
type="form"
bind:id={formId}
props={{ dataSource, disableValidation: true, editAutoColumns: true }}
>
{#if title || enrichedSearchColumns?.length || showTitleButton}
<div class="header" class:mobile={$context.device.mobile}>
<div class="title">
<Heading>{title || ""}</Heading>
</div>
<div class="controls">
{#if enrichedSearchColumns?.length}
<div
class="search"
style="--cols:{enrichedSearchColumns?.length}"
>
{#each enrichedSearchColumns as column}
<BlockComponent
type={column.componentType}
props={{
field: column.name,
placeholder: column.name,
text: column.name,
autoWidth: true,
}}
/>
{/each}
</div>
{/if}
{#if showTitleButton}
<BlockComponent
type="button"
props={{
onClick: titleButtonAction,
text: titleButtonText,
type: "cta",
}}
/>
{/if}
</div>
</div>
{/if}
<BlockComponent
type="form"
bind:id={formId}
props={{
dataSource,
disableValidation: true,
editAutoColumns: true,
size,
}}
>
{#if title || enrichedSearchColumns?.length || showTitleButton}
<BlockComponent
type="dataprovider"
bind:id={dataProviderId}
type="container"
props={{
dataSource,
filter: enrichedFilter,
sortColumn,
sortOrder,
paginate,
limit: rowCount,
direction: "row",
hAlign: "stretch",
vAlign: "middle",
gap: "M",
wrap: true,
}}
styles={{
normal: {
"margin-bottom": "20px",
},
}}
order={0}
>
<BlockComponent
type="table"
context="table"
type="heading"
props={{
dataProvider: `{{ literal ${safe(dataProviderId)} }}`,
columns: tableColumns,
showAutoColumns,
rowCount,
quiet,
compact,
allowSelectRows,
size,
linkRows,
linkURL,
linkColumn,
linkPeek,
text: title,
}}
order={0}
/>
<BlockComponent
type="container"
props={{
direction: "row",
hAlign: "left",
vAlign: "center",
gap: "M",
wrap: true,
}}
order={1}
>
{#if enrichedSearchColumns?.length}
{#each enrichedSearchColumns as column, idx}
<BlockComponent
type={column.componentType}
props={{
field: column.name,
placeholder: column.name,
text: column.name,
autoWidth: true,
}}
styles={{
normal: {
width: "192px",
},
}}
order={idx}
/>
{/each}
{/if}
{#if showTitleButton}
<BlockComponent
type="button"
props={{
onClick: titleButtonAction,
text: titleButtonText,
type: "cta",
}}
order={enrichedSearchColumns?.length ?? 0}
/>
{/if}
</BlockComponent>
</BlockComponent>
{/if}
<BlockComponent
type="dataprovider"
bind:id={dataProviderId}
props={{
dataSource,
filter: enrichedFilter,
sortColumn,
sortOrder,
paginate,
limit: rowCount,
}}
order={1}
>
<BlockComponent
type="table"
context="table"
props={{
dataProvider: `{{ literal ${safe(dataProviderId)} }}`,
columns: tableColumns,
showAutoColumns,
rowCount,
quiet,
compact,
allowSelectRows,
size,
linkRows,
linkURL,
linkColumn,
linkPeek,
}}
/>
</BlockComponent>
</div>
</BlockComponent>
</Block>
{/if}
<style>
.header {
display: flex;
flex-direction: row;
justify-content: space-between;
align-items: center;
gap: 20px;
margin-bottom: 20px;
}
.title {
overflow: hidden;
}
.title :global(.spectrum-Heading) {
flex: 1 1 auto;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
.controls {
flex: 0 1 auto;
display: flex;
flex-direction: row;
justify-content: flex-end;
align-items: center;
gap: 20px;
}
.controls :global(.spectrum-InputGroup .spectrum-InputGroup-input) {
width: 100%;
}
.search {
flex: 0 1 auto;
gap: 10px;
max-width: 100%;
display: grid;
grid-template-columns: repeat(var(--cols), minmax(120px, 200px));
}
.search :global(.spectrum-InputGroup) {
min-width: 0;
}
/* Mobile styles */
.mobile {
flex-direction: column;
justify-content: flex-start;
align-items: stretch;
}
.mobile .controls {
flex-direction: column-reverse;
justify-content: flex-start;
align-items: stretch;
}
.mobile .search {
display: flex;
flex-direction: column;
justify-content: flex-start;
align-items: stretch;
position: relative;
width: 100%;
}
</style>

View File

@ -1,3 +1,4 @@
export { default as tableblock } from "./TableBlock.svelte"
export { default as cardsblock } from "./CardsBlock.svelte"
export { default as repeaterblock } from "./RepeaterBlock.svelte"
export { default as formblock } from "./FormBlock.svelte"

View File

@ -1,37 +1,39 @@
export class ApexOptionsBuilder {
formatters = {
["Default"]: val => (isNaN(val) ? val : Math.round(val * 100) / 100),
["Thousands"]: val => `${Math.round(val / 1000)}K`,
["Millions"]: val => `${Math.round(val / 1000000)}M`,
}
options = {
series: [],
legend: {
show: false,
position: "top",
horizontalAlign: "right",
showForSingleSeries: true,
showForNullSeries: true,
showForZeroSeries: true,
},
chart: {
toolbar: {
constructor() {
this.formatters = {
["Default"]: val => (isNaN(val) ? val : Math.round(val * 100) / 100),
["Thousands"]: val => `${Math.round(val / 1000)}K`,
["Millions"]: val => `${Math.round(val / 1000000)}M`,
}
this.options = {
series: [],
legend: {
show: false,
position: "top",
horizontalAlign: "right",
showForSingleSeries: true,
showForNullSeries: true,
showForZeroSeries: true,
},
zoom: {
enabled: false,
chart: {
toolbar: {
show: false,
},
zoom: {
enabled: false,
},
},
},
xaxis: {
labels: {
formatter: this.formatters.Default,
xaxis: {
labels: {
formatter: this.formatters.Default,
},
},
},
yaxis: {
labels: {
formatter: this.formatters.Default,
yaxis: {
labels: {
formatter: this.formatters.Default,
},
},
},
}
}
setOption(path, value) {

View File

@ -48,36 +48,7 @@
// Fetches the form schema from this form's dataSource
const fetchSchema = async dataSource => {
if (!dataSource) {
schema = {}
}
// If the datasource is a query, then we instead use a schema of the query
// parameters rather than the output schema
else if (
dataSource.type === "query" &&
dataSource._id &&
actionType === "Create"
) {
try {
const query = await API.fetchQueryDefinition(dataSource._id)
let paramSchema = {}
const params = query.parameters || []
params.forEach(param => {
paramSchema[param.name] = { ...param, type: "string" }
})
schema = paramSchema
} catch (error) {
schema = {}
}
}
// For all other cases, just grab the normal schema
else {
const dataSourceSchema = await fetchDatasourceSchema(dataSource)
schema = dataSourceSchema || {}
}
schema = (await fetchDatasourceSchema(dataSource)) || {}
if (!loaded) {
loaded = true
}
@ -95,7 +66,7 @@
$: initialValues = getInitialValues(actionType, dataSource, $context)
$: resetKey = Helpers.hashString(
JSON.stringify(initialValues) + JSON.stringify(schema)
JSON.stringify(initialValues) + JSON.stringify(schema) + disabled
)
</script>

View File

@ -1,9 +1,10 @@
import ClientApp from "./components/ClientApp.svelte"
import {
componentStore,
builderStore,
appStore,
devToolsStore,
blockStore,
componentStore,
environmentStore,
} from "./stores"
import loadSpectrumIcons from "@budibase/bbui/spectrum-icons-rollup.js"
@ -50,6 +51,17 @@ const loadBudibase = async () => {
const enableDevTools = !get(builderStore).inBuilder && get(appStore).isDevApp
devToolsStore.actions.setEnabled(enableDevTools)
// Register handler for runtime events from the builder
window.handleBuilderRuntimeEvent = (name, payload) => {
if (!window["##BUDIBASE_IN_BUILDER##"]) {
return
}
if (name === "eject-block") {
const block = blockStore.actions.getBlock(payload)
block?.eject()
}
}
// Register any custom components
if (window["##BUDIBASE_CUSTOM_COMPONENTS##"]) {
window["##BUDIBASE_CUSTOM_COMPONENTS##"].forEach(component => {

View File

@ -0,0 +1,34 @@
import { get, writable } from "svelte/store"
const createBlockStore = () => {
const store = writable({})
const registerBlock = (id, instance) => {
store.update(state => ({
...state,
[id]: instance,
}))
}
const unregisterBlock = id => {
store.update(state => {
delete state[id]
return state
})
}
const getBlock = id => {
return get(store)[id]
}
return {
subscribe: store.subscribe,
actions: {
registerBlock,
unregisterBlock,
getBlock,
},
}
}
export const blockStore = createBlockStore()

View File

@ -85,6 +85,9 @@ const createBuilderStore = () => {
highlightSetting: setting => {
dispatchEvent("highlight-setting", { setting })
},
ejectBlock: (id, definition) => {
dispatchEvent("eject-block", { id, definition })
},
updateUsedPlugin: (name, hash) => {
// Check if we used this plugin
const used = get(store)?.usedPlugins?.find(x => x.name === name)

View File

@ -17,6 +17,7 @@ export { devToolsStore } from "./devTools"
export { componentStore } from "./components"
export { uploadStore } from "./uploads.js"
export { rowSelectionStore } from "./rowSelection.js"
export { blockStore } from "./blocks.js"
export { environmentStore } from "./environment"
// Context stores are layered and duplicated, so it is not a singleton

View File

@ -16,7 +16,7 @@ import JSONArrayFetch from "@budibase/frontend-core/src/fetch/JSONArrayFetch.js"
*/
export const fetchDatasourceSchema = async (
datasource,
options = { enrichRelationships: false }
options = { enrichRelationships: false, formSchema: false }
) => {
const handler = {
table: TableFetch,
@ -34,7 +34,17 @@ export const fetchDatasourceSchema = async (
// Get the datasource definition and then schema
const definition = await instance.getDefinition(datasource)
let schema = instance.getSchema(datasource, definition)
// Get the normal schema as long as we aren't wanting a form schema
let schema
if (datasource?.type !== "query" || !options?.formSchema) {
schema = instance.getSchema(datasource, definition)
} else if (definition.parameters?.length) {
schema = {}
definition.parameters.forEach(param => {
schema[param.name] = { ...param, type: "string" }
})
}
if (!schema) {
return null
}

View File

@ -1,12 +1,12 @@
{
"name": "@budibase/frontend-core",
"version": "2.0.14-alpha.4",
"version": "2.0.24-alpha.3",
"description": "Budibase frontend core libraries used in builder and client",
"author": "Budibase",
"license": "MPL-2.0",
"svelte": "src/index.js",
"dependencies": {
"@budibase/bbui": "2.0.14-alpha.4",
"@budibase/bbui": "2.0.24-alpha.3",
"lodash": "^4.17.21",
"svelte": "^3.46.2"
}

View File

@ -14,52 +14,52 @@ import { convertJSONSchemaToTableSchema } from "../utils/json"
* For other types of datasource, this class is overridden and extended.
*/
export default class DataFetch {
// API client
API = null
// Feature flags
featureStore = writable({
supportsSearch: false,
supportsSort: false,
supportsPagination: false,
})
// Config
options = {
datasource: null,
limit: 10,
// Search config
filter: null,
query: null,
// Sorting config
sortColumn: null,
sortOrder: "ascending",
sortType: null,
// Pagination config
paginate: true,
}
// State of the fetch
store = writable({
rows: [],
info: null,
schema: null,
loading: false,
loaded: false,
query: null,
pageNumber: 0,
cursor: null,
cursors: [],
})
/**
* Constructs a new DataFetch instance.
* @param opts the fetch options
*/
constructor(opts) {
// API client
this.API = null
// Feature flags
this.featureStore = writable({
supportsSearch: false,
supportsSort: false,
supportsPagination: false,
})
// Config
this.options = {
datasource: null,
limit: 10,
// Search config
filter: null,
query: null,
// Sorting config
sortColumn: null,
sortOrder: "ascending",
sortType: null,
// Pagination config
paginate: true,
}
// State of the fetch
this.store = writable({
rows: [],
info: null,
schema: null,
loading: false,
loaded: false,
query: null,
pageNumber: 0,
cursor: null,
cursors: [],
})
// Merge options with their default values
this.API = opts?.API
this.options = {

View File

@ -121,7 +121,12 @@ export const buildLuceneQuery = filter => {
query.allOr = true
return
}
if (type === "datetime" && !isHbs) {
if (
type === "datetime" &&
!isHbs &&
operator !== "empty" &&
operator !== "notEmpty"
) {
// Ensure date value is a valid date and parse into correct format
if (!value) {
return

View File

@ -1,6 +1,6 @@
{
"name": "@budibase/sdk",
"version": "2.0.14-alpha.4",
"version": "2.0.24-alpha.3",
"description": "Budibase Public API SDK",
"author": "Budibase",
"license": "MPL-2.0",

View File

@ -1,7 +1,7 @@
{
"name": "@budibase/server",
"email": "hi@budibase.com",
"version": "2.0.14-alpha.4",
"version": "2.0.24-alpha.3",
"description": "Budibase Web Server",
"main": "src/index.ts",
"repository": {
@ -77,11 +77,11 @@
"license": "GPL-3.0",
"dependencies": {
"@apidevtools/swagger-parser": "10.0.3",
"@budibase/backend-core": "2.0.14-alpha.4",
"@budibase/client": "2.0.14-alpha.4",
"@budibase/pro": "2.0.14-alpha.4",
"@budibase/string-templates": "2.0.14-alpha.4",
"@budibase/types": "2.0.14-alpha.4",
"@budibase/backend-core": "2.0.24-alpha.3",
"@budibase/client": "2.0.24-alpha.3",
"@budibase/pro": "2.0.24-alpha.3",
"@budibase/string-templates": "2.0.24-alpha.3",
"@budibase/types": "2.0.24-alpha.3",
"@bull-board/api": "3.7.0",
"@bull-board/koa": "3.9.4",
"@elastic/elasticsearch": "7.10.0",

View File

@ -32,7 +32,7 @@ const {
import { USERS_TABLE_SCHEMA } from "../../constants"
import { removeAppFromUserRoles } from "../../utilities/workerRequests"
import { clientLibraryPath, stringToReadStream } from "../../utilities"
import { getAllLocks } from "../../utilities/redis"
import { getLocksById } from "../../utilities/redis"
import {
updateClientLibrary,
backupClientLibrary,
@ -45,11 +45,11 @@ import { cleanupAutomations } from "../../automations/utils"
import { context } from "@budibase/backend-core"
import { checkAppMetadata } from "../../automations/logging"
import { getUniqueRows } from "../../utilities/usageQuota/rows"
import { quotas } from "@budibase/pro"
import { quotas, groups } from "@budibase/pro"
import { errors, events, migrations } from "@budibase/backend-core"
import { App, Layout, Screen, MigrationType } from "@budibase/types"
import { BASE_LAYOUT_PROP_IDS } from "../../constants/layouts"
import { groups } from "@budibase/pro"
import { enrichPluginURLs } from "../../utilities/plugins"
const URL_REGEX_SLASH = /\/|\\/g
@ -171,16 +171,16 @@ export const fetch = async (ctx: any) => {
const all = ctx.query && ctx.query.status === AppStatus.ALL
const apps = await getAllApps({ dev, all })
const appIds = apps
.filter((app: any) => app.status === "development")
.map((app: any) => app.appId)
// get the locks for all the dev apps
if (dev || all) {
const locks = await getAllLocks()
const locks = await getLocksById(appIds)
for (let app of apps) {
if (app.status !== "development") {
continue
}
const lock = locks.find((lock: any) => lock.appId === app.appId)
const lock = locks[app.appId]
if (lock) {
app.lockedBy = lock.user
app.lockedBy = lock
} else {
// make sure its definitely not present
delete app.lockedBy
@ -208,10 +208,13 @@ export const fetchAppDefinition = async (ctx: any) => {
export const fetchAppPackage = async (ctx: any) => {
const db = context.getAppDB()
const application = await db.get(DocumentType.APP_METADATA)
let application = await db.get(DocumentType.APP_METADATA)
const layouts = await getLayouts()
let screens = await getScreens()
// Enrich plugin URLs
application.usedPlugins = enrichPluginURLs(application.usedPlugins)
// Only filter screens if the user is not a builder
if (!(ctx.user.builder && ctx.user.builder.global)) {
const userRoleId = getUserRoleId(ctx)

View File

@ -68,6 +68,7 @@ exports.buildSchemaFromDb = async function (ctx) {
datasource.entities = tables
}
setDefaultDisplayColumns(datasource)
const dbResp = await db.put(datasource)
datasource._rev = dbResp.rev
@ -78,6 +79,24 @@ exports.buildSchemaFromDb = async function (ctx) {
ctx.body = response
}
/**
* Make sure all datasource entities have a display name selected
*/
const setDefaultDisplayColumns = datasource => {
//
for (let entity of Object.values(datasource.entities)) {
if (entity.primaryDisplay) {
continue
}
const notAutoColumn = Object.values(entity.schema).find(
schema => !schema.autocolumn
)
if (notAutoColumn) {
entity.primaryDisplay = notAutoColumn.name
}
}
}
/**
* Check for variables that have been updated or removed and invalidate them.
*/
@ -155,6 +174,7 @@ exports.save = async function (ctx) {
const { tables, error } = await buildSchemaHelper(datasource)
schemaError = error
datasource.entities = tables
setDefaultDisplayColumns(datasource)
}
const dbResp = await db.put(datasource)
@ -238,19 +258,6 @@ const buildSchemaHelper = async datasource => {
const connector = new Connector(datasource.config)
await connector.buildSchema(datasource._id, datasource.entities)
// make sure they all have a display name selected
for (let entity of Object.values(datasource.entities ?? {})) {
if (entity.primaryDisplay) {
continue
}
const notAutoColumn = Object.values(entity.schema).find(
schema => !schema.autocolumn
)
if (notAutoColumn) {
entity.primaryDisplay = notAutoColumn.name
}
}
const errors = connector.schemaErrors
let error = null
if (errors && Object.keys(errors).length > 0) {

View File

@ -103,7 +103,7 @@ exports.revert = async ctx => {
target: appId,
})
try {
if (!env.isTest()) {
if (!env.isCypress()) {
// in-memory db stalls on rollback
await replication.rollback()
}

View File

@ -52,14 +52,19 @@ export async function read(ctx: any, next: any) {
}
export async function update(ctx: any, next: any) {
ctx.request.body = await addRev(fixRow(ctx.request.body, ctx.params))
const { tableId } = ctx.params
ctx.request.body = await addRev(fixRow(ctx.request.body, ctx.params), tableId)
await rowController.save(ctx)
await next()
}
export async function destroy(ctx: any, next: any) {
const { tableId } = ctx.params
// set the body as expected, with the _id and _rev fields
ctx.request.body = await addRev(fixRow({ _id: ctx.params.rowId }, ctx.params))
ctx.request.body = await addRev(
fixRow({ _id: ctx.params.rowId }, ctx.params),
tableId
)
await rowController.destroy(ctx)
// destroy controller doesn't currently return the row as the body, need to adjust this
// in the public API to be correct

View File

@ -22,7 +22,7 @@ export async function addRev(
}
/**
* Performs a case insensitive search on the provided documents, using the
* Performs a case in-sensitive search on the provided documents, using the
* provided key and value. This will be a string based search, using the
* startsWith function.
*/

View File

@ -240,6 +240,10 @@ async function execute(
const { rows, pagination, extra } = await quotas.addQuery(runFn, {
datasourceId: datasource._id,
})
// remove the raw from execution incase transformer being used to hide data
if (extra?.raw) {
delete extra.raw
}
if (opts && opts.rowsOnly) {
ctx.body = rows
} else {

View File

@ -145,7 +145,7 @@ class QueryBuilder {
* @param options The preprocess options
* @returns {string|*}
*/
preprocess(value, { escape, lowercase, wrap } = {}) {
preprocess(value, { escape, lowercase, wrap, type } = {}) {
const hasVersion = !!this.version
// Determine if type needs wrapped
const originalType = typeof value
@ -157,8 +157,11 @@ class QueryBuilder {
if (escape && originalType === "string") {
value = `${value}`.replace(/[ #+\-&|!(){}\]^"~*?:\\]/g, "\\$&")
}
// Wrap in quotes
if (hasVersion && wrap) {
if (originalType === "string" && !isNaN(value) && !type) {
value = `"${value}"`
} else if (hasVersion && wrap) {
value = originalType === "number" ? value : `"${value}"`
}
return value
@ -253,6 +256,7 @@ class QueryBuilder {
value = builder.preprocess(value, {
escape: true,
lowercase: true,
type: "string",
})
return `${key}:${value}*`
})
@ -281,6 +285,7 @@ class QueryBuilder {
value = builder.preprocess(value, {
escape: true,
lowercase: true,
type: "fuzzy",
})
return `${key}:${value}~`
})

View File

@ -1,3 +1,5 @@
import { enrichPluginURLs } from "../../../utilities/plugins"
require("svelte/register")
const send = require("koa-send")
@ -107,12 +109,13 @@ export const serveApp = async function (ctx: any) {
if (!env.isJest()) {
const App = require("./templates/BudibaseApp.svelte").default
const plugins = enrichPluginURLs(appInfo.usedPlugins)
const { head, html, css } = App.render({
title: appInfo.name,
production: env.isProd(),
appId,
clientLibPath: clientLibraryPath(appId, appInfo.version, ctx),
usedPlugins: appInfo.usedPlugins,
usedPlugins: plugins,
})
const appHbs = loadHandlebarsFile(`${__dirname}/templates/app.hbs`)

View File

@ -88,9 +88,7 @@
<!-- But before loadBudibase is called -->
{#if usedPlugins?.length}
{#each usedPlugins as plugin}
<script
type="application/javascript"
src={`/plugins/${plugin.jsUrl}`}></script>
<script type="application/javascript" src={plugin.jsUrl}></script>
{/each}
{/if}
<script type="application/javascript">

View File

@ -56,6 +56,16 @@
return
}
// If this is a custom event, try and handle it
if (parsed.runtimeEvent) {
const { name, payload } = parsed
if (window.handleBuilderRuntimeEvent) {
window.handleBuilderRuntimeEvent(name, payload)
}
return
}
// Otherwise this is a full reload message
// Extract data from message
const {
selectedComponentId,

View File

@ -14,8 +14,10 @@ import {
fixAutoColumnSubType,
} from "../../../utilities/rowProcessor"
import { runStaticFormulaChecks } from "./bulkFormula"
import { Table } from "../../../definitions/common"
import { Table } from "@budibase/types"
import { quotas } from "@budibase/pro"
import { isEqual } from "lodash"
import { cloneDeep } from "lodash/fp"
function checkAutoColumns(table: Table, oldTable: Table) {
if (!table.schema) {
@ -123,10 +125,16 @@ export async function save(ctx: any) {
if (updatedRows && updatedRows.length !== 0) {
await db.bulkDocs(updatedRows)
}
const result = await db.put(tableToSave)
let result = await db.put(tableToSave)
tableToSave._rev = result.rev
const savedTable = cloneDeep(tableToSave)
tableToSave = await tableSaveFunctions.after(tableToSave)
// the table may be updated as part of the table save after functionality - need to write it
if (!isEqual(savedTable, tableToSave)) {
result = await db.put(tableToSave)
tableToSave._rev = result.rev
}
// has to run after, make sure it has _id
await runStaticFormulaChecks(tableToSave, { oldTable, deletion: null })
return tableToSave

View File

@ -247,7 +247,7 @@ class TableSaveFunctions {
// after saving
async after(table: any) {
table = await handleSearchIndexes(table)
await handleDataImport(this.user, table, this.dataImport)
table = await handleDataImport(this.user, table, this.dataImport)
return table
}

View File

@ -1,7 +1,7 @@
jest.mock("../../../utilities/redis", () => ({
init: jest.fn(),
getAllLocks: () => {
return []
getLocksById: () => {
return {}
},
doesUserHaveLock: () => {
return true

View File

@ -173,4 +173,24 @@ describe("internal search", () => {
}, PARAMS)
checkLucene(response, `*:* AND NOT column:(a AND b AND c)`, PARAMS)
})
it("test equal without version query", async () => {
PARAMS.version = null
const response = await search.paginatedSearch({
equal: {
"column": "1",
}
}, PARAMS)
const query = response.rows[0].query
const json = JSON.parse(query)
if (PARAMS.sort) {
expect(json.sort).toBe(`${PARAMS.sort}<${PARAMS.sortType}>`)
}
if (PARAMS.bookmark) {
expect(json.bookmark).toBe(PARAMS.bookmark)
}
expect(json.include_docs).toBe(true)
expect(json.q).toBe(`(*:* AND column:"1") AND tableId:${PARAMS.tableId}`)
})
})

View File

@ -252,7 +252,7 @@ class Orchestrator {
let loopStepNumber: any = undefined
let loopSteps: LoopStep[] | undefined = []
let metadata
let wasLoopStep = false
// check if this is a recurring automation,
if (isProdAppID(this._appId) && isRecurring(automation)) {
metadata = await this.getMetadata()
@ -267,6 +267,7 @@ class Orchestrator {
let input,
iterations = 1,
iterationCount = 0
if (step.stepId === LOOP_STEP_ID) {
loopStep = step
loopStepNumber = stepCount
@ -277,10 +278,8 @@ class Orchestrator {
input = await processObject(loopStep.inputs, this._context)
iterations = getLoopIterations(loopStep as LoopStep, input)
}
for (let index = 0; index < iterations; index++) {
let originalStepInput = cloneDeep(step.inputs)
// Handle if the user has set a max iteration count or if it reaches the max limit set by us
if (loopStep && input.binding) {
let newInput = await processObject(
@ -313,7 +312,6 @@ class Orchestrator {
} else {
item = loopStep.inputs.binding
}
this._context.steps[loopStepNumber] = {
currentItem: item[index],
}
@ -331,6 +329,16 @@ class Orchestrator {
innerValue,
`steps.${loopStepNumber}`
)
} else if (typeof value === "object") {
for (let [innerObject, innerValue] of Object.entries(
originalStepInput[key][innerKey]
)) {
originalStepInput[key][innerKey][innerObject] =
automationUtils.substituteLoopStep(
innerValue,
`steps.${loopStepNumber}`
)
}
}
}
} else {
@ -386,6 +394,7 @@ class Orchestrator {
let stepFn = await this.getStepFunctionality(step.stepId)
let inputs = await processObject(originalStepInput, this._context)
inputs = automationUtils.cleanInputValues(inputs, step.schema.inputs)
try {
// appId is always passed
const outputs = await stepFn({
@ -394,6 +403,7 @@ class Orchestrator {
emitter: this._emitter,
context: this._context,
})
this._context.steps[stepCount] = outputs
// if filter causes us to stop execution don't break the loop, set a var
// so that we can finish iterating through the steps and record that it stopped
@ -419,6 +429,7 @@ class Orchestrator {
console.error(`Automation error - ${step.stepId} - ${err}`)
return err
}
if (loopStep) {
iterationCount++
if (index === iterations - 1) {
@ -429,6 +440,13 @@ class Orchestrator {
}
}
// Delete the step after the loop step as it's irrelevant, since information is included
// in the loop step
if (wasLoopStep) {
this._context.steps.splice(loopStepNumber + 1, 1)
wasLoopStep = false
}
if (loopSteps && loopSteps.length) {
let tempOutput = {
success: true,
@ -441,9 +459,10 @@ class Orchestrator {
outputs: tempOutput,
inputs: step.inputs,
})
this._context.steps[loopStepNumber] = tempOutput
this._context.steps.splice(loopStepNumber, 0, tempOutput)
loopSteps = undefined
wasLoopStep = true
}
}

View File

@ -0,0 +1,21 @@
const env = require("../environment")
const { plugins: ProPlugins } = require("@budibase/pro")
const { objectStore } = require("@budibase/backend-core")
exports.enrichPluginURLs = plugins => {
if (!plugins || !plugins.length) {
return []
}
return plugins.map(plugin => {
const cloud = !env.SELF_HOSTED
const bucket = objectStore.ObjectStoreBuckets.PLUGINS
const jsFileName = "plugin.min.js"
// In self host we need to prefix the path, as the bucket name is not part
// of the bucket path. In cloud, it's already part of the bucket path.
let jsUrl = cloud ? "https://cdn.budi.live/" : `/${bucket}/`
jsUrl += ProPlugins.getBucketPath(plugin.name)
jsUrl += jsFileName
return { ...plugin, jsUrl }
})
}

View File

@ -34,12 +34,8 @@ exports.doesUserHaveLock = async (devAppId, user) => {
return expected === userId
}
exports.getAllLocks = async () => {
const locks = await devAppClient.scan()
return locks.map(lock => ({
appId: lock.key,
user: lock.value,
}))
exports.getLocksById = async appIds => {
return await devAppClient.bulkGet(appIds)
}
exports.updateLock = async (devAppId, user) => {

View File

@ -1094,12 +1094,12 @@
resolved "https://registry.yarnpkg.com/@bcoe/v8-coverage/-/v8-coverage-0.2.3.tgz#75a2e8b51cb758a7553d6804a5932d7aace75c39"
integrity sha512-0hYQ8SB4Db5zvZB4axdMHGwEaQjkZzFjQiN9LVYvIFB2nSUHW9tYpxWriPrWDASIxiaXax83REcLxuSdnGPZtw==
"@budibase/backend-core@2.0.14-alpha.4":
version "2.0.14-alpha.4"
resolved "https://registry.yarnpkg.com/@budibase/backend-core/-/backend-core-2.0.14-alpha.4.tgz#213446a9e04385fb38ab8785742cdb8b4eaf2cac"
integrity sha512-ma1Ipst4AQVi4sx+ULs3bX9xT4eHqavCvM/BXtZzV23SOTdjJTi9wX2KWbIvAUT5xo2NgN0uRf5zf7B6CBRuXw==
"@budibase/backend-core@2.0.24-alpha.3":
version "2.0.24-alpha.3"
resolved "https://registry.yarnpkg.com/@budibase/backend-core/-/backend-core-2.0.24-alpha.3.tgz#49a92082d1ca6bed0eb82519a6c02d7d14ead751"
integrity sha512-Zm/ddRDMzMuXCoEXZa0CzA/B1SnpQ+yjZCNAPH7Y4yYTIKfeE/DQ6COLlUixQxVMAUN1L5+GXML+px6fRigA5w==
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"
@ -1180,13 +1180,13 @@
svelte-flatpickr "^3.2.3"
svelte-portal "^1.0.0"
"@budibase/pro@2.0.14-alpha.4":
version "2.0.14-alpha.4"
resolved "https://registry.yarnpkg.com/@budibase/pro/-/pro-2.0.14-alpha.4.tgz#9796271365f4ffb875655f86df84a2b4111b793a"
integrity sha512-I5QIv04hfoUPEYHPkCD5L/5WU7cJurmfhjy62ZMTNbJzFzhIGDzQlOjM0MnQO5kJvGv1NWl2PQvSty1rYzXGsw==
"@budibase/pro@2.0.24-alpha.3":
version "2.0.24-alpha.3"
resolved "https://registry.yarnpkg.com/@budibase/pro/-/pro-2.0.24-alpha.3.tgz#16605029663b07d0e3bcf0c08893878b542b6752"
integrity sha512-gBdqJzvtEAMJkhB2YTAyVYKoCANA25F1wT/K7kQtTOy5LrjDmcXah7CZoetfZQGbsKxUYEAZwJzxLSXmIxvWmQ==
dependencies:
"@budibase/backend-core" "2.0.14-alpha.4"
"@budibase/types" "2.0.14-alpha.4"
"@budibase/backend-core" "2.0.24-alpha.3"
"@budibase/types" "2.0.24-alpha.3"
"@koa/router" "8.0.8"
joi "17.6.0"
node-fetch "^2.6.1"
@ -1209,10 +1209,10 @@
svelte-apexcharts "^1.0.2"
svelte-flatpickr "^3.1.0"
"@budibase/types@2.0.14-alpha.4":
version "2.0.14-alpha.4"
resolved "https://registry.yarnpkg.com/@budibase/types/-/types-2.0.14-alpha.4.tgz#37b1ff362d2551fd978e8c8153a4cec357eda0b0"
integrity sha512-l4dfMh5it1N56nc+jCtdXklrZA1cMt6WSVBoKCXBedZGpguJr18Wo/cfn1YWryuUMuSlpFVDTrlQQXCZ+ku92g==
"@budibase/types@2.0.24-alpha.3":
version "2.0.24-alpha.3"
resolved "https://registry.yarnpkg.com/@budibase/types/-/types-2.0.24-alpha.3.tgz#84eec5f991a2cfaf48d968b07a075b343f847289"
integrity sha512-f9PhtqzmqPI76ITXttuvxsvqMUJtkrDYf/4MHlI2v5ssNL9r0C/hbQEXllff3L3JqViEHWxkKFmfvfnDTV8rRQ==
"@bull-board/api@3.7.0":
version "3.7.0"

View File

@ -1,6 +1,6 @@
{
"name": "@budibase/string-templates",
"version": "2.0.14-alpha.4",
"version": "2.0.24-alpha.3",
"description": "Handlebars wrapper for Budibase templating.",
"main": "src/index.cjs",
"module": "dist/bundle.mjs",

View File

@ -1,6 +1,6 @@
{
"name": "@budibase/types",
"version": "2.0.14-alpha.4",
"version": "2.0.24-alpha.3",
"description": "Budibase types",
"main": "dist/index.js",
"types": "dist/index.d.ts",

View File

@ -1,7 +1,7 @@
{
"name": "@budibase/worker",
"email": "hi@budibase.com",
"version": "2.0.14-alpha.4",
"version": "2.0.24-alpha.3",
"description": "Budibase background service",
"main": "src/index.ts",
"repository": {
@ -36,10 +36,10 @@
"author": "Budibase",
"license": "GPL-3.0",
"dependencies": {
"@budibase/backend-core": "2.0.14-alpha.4",
"@budibase/pro": "2.0.14-alpha.4",
"@budibase/string-templates": "2.0.14-alpha.4",
"@budibase/types": "2.0.14-alpha.4",
"@budibase/backend-core": "2.0.24-alpha.3",
"@budibase/pro": "2.0.24-alpha.3",
"@budibase/string-templates": "2.0.24-alpha.3",
"@budibase/types": "2.0.24-alpha.3",
"@koa/router": "8.0.8",
"@sentry/node": "6.17.7",
"@techpass/passport-openidconnect": "0.3.2",

View File

@ -291,12 +291,12 @@
resolved "https://registry.yarnpkg.com/@bcoe/v8-coverage/-/v8-coverage-0.2.3.tgz#75a2e8b51cb758a7553d6804a5932d7aace75c39"
integrity sha512-0hYQ8SB4Db5zvZB4axdMHGwEaQjkZzFjQiN9LVYvIFB2nSUHW9tYpxWriPrWDASIxiaXax83REcLxuSdnGPZtw==
"@budibase/backend-core@2.0.14-alpha.4":
version "2.0.14-alpha.4"
resolved "https://registry.yarnpkg.com/@budibase/backend-core/-/backend-core-2.0.14-alpha.4.tgz#213446a9e04385fb38ab8785742cdb8b4eaf2cac"
integrity sha512-ma1Ipst4AQVi4sx+ULs3bX9xT4eHqavCvM/BXtZzV23SOTdjJTi9wX2KWbIvAUT5xo2NgN0uRf5zf7B6CBRuXw==
"@budibase/backend-core@2.0.24-alpha.3":
version "2.0.24-alpha.3"
resolved "https://registry.yarnpkg.com/@budibase/backend-core/-/backend-core-2.0.24-alpha.3.tgz#49a92082d1ca6bed0eb82519a6c02d7d14ead751"
integrity sha512-Zm/ddRDMzMuXCoEXZa0CzA/B1SnpQ+yjZCNAPH7Y4yYTIKfeE/DQ6COLlUixQxVMAUN1L5+GXML+px6fRigA5w==
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"
@ -327,21 +327,21 @@
uuid "8.3.2"
zlib "1.0.5"
"@budibase/pro@2.0.14-alpha.4":
version "2.0.14-alpha.4"
resolved "https://registry.yarnpkg.com/@budibase/pro/-/pro-2.0.14-alpha.4.tgz#9796271365f4ffb875655f86df84a2b4111b793a"
integrity sha512-I5QIv04hfoUPEYHPkCD5L/5WU7cJurmfhjy62ZMTNbJzFzhIGDzQlOjM0MnQO5kJvGv1NWl2PQvSty1rYzXGsw==
"@budibase/pro@2.0.24-alpha.3":
version "2.0.24-alpha.3"
resolved "https://registry.yarnpkg.com/@budibase/pro/-/pro-2.0.24-alpha.3.tgz#16605029663b07d0e3bcf0c08893878b542b6752"
integrity sha512-gBdqJzvtEAMJkhB2YTAyVYKoCANA25F1wT/K7kQtTOy5LrjDmcXah7CZoetfZQGbsKxUYEAZwJzxLSXmIxvWmQ==
dependencies:
"@budibase/backend-core" "2.0.14-alpha.4"
"@budibase/types" "2.0.14-alpha.4"
"@budibase/backend-core" "2.0.24-alpha.3"
"@budibase/types" "2.0.24-alpha.3"
"@koa/router" "8.0.8"
joi "17.6.0"
node-fetch "^2.6.1"
"@budibase/types@2.0.14-alpha.4":
version "2.0.14-alpha.4"
resolved "https://registry.yarnpkg.com/@budibase/types/-/types-2.0.14-alpha.4.tgz#37b1ff362d2551fd978e8c8153a4cec357eda0b0"
integrity sha512-l4dfMh5it1N56nc+jCtdXklrZA1cMt6WSVBoKCXBedZGpguJr18Wo/cfn1YWryuUMuSlpFVDTrlQQXCZ+ku92g==
"@budibase/types@2.0.24-alpha.3":
version "2.0.24-alpha.3"
resolved "https://registry.yarnpkg.com/@budibase/types/-/types-2.0.24-alpha.3.tgz#84eec5f991a2cfaf48d968b07a075b343f847289"
integrity sha512-f9PhtqzmqPI76ITXttuvxsvqMUJtkrDYf/4MHlI2v5ssNL9r0C/hbQEXllff3L3JqViEHWxkKFmfvfnDTV8rRQ==
"@cspotcode/source-map-consumer@0.8.0":
version "0.8.0"

View File

@ -1,8 +1,10 @@
#!/bin/bash
if [[ $TARGETARCH == arm* ]] ;
then
echo "INSTALLING ARM64 MINIO"
wget https://dl.min.io/server/minio/release/linux-arm64/minio
else
echo "INSTALLING AMD64 MINIO"
wget https://dl.min.io/server/minio/release/linux-amd64/minio
fi
chmod +x minio

View File

@ -18,6 +18,11 @@ git clone https://$PERSONAL_ACCESS_TOKEN@github.com/Budibase/budibase-pro.git
if [[ -d "budibase-pro" ]]; then
cd budibase-pro
if [[ -z "${BRANCH}" ]]; then
echo Using GITHUB_REF_NAME: $GITHUB_REF_NAME
export BRANCH=$GITHUB_REF_NAME
fi
# Try to checkout the matching pro branch
git checkout $BRANCH