diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml
index aaf8489019..6316bf1837 100644
--- a/.github/workflows/release.yml
+++ b/.github/workflows/release.yml
@@ -1,6 +1,6 @@
name: Budibase Release
-on:
+on:
push:
branches:
- master
@@ -15,20 +15,20 @@ env:
POSTHOG_TOKEN: ${{ secrets.POSTHOG_TOKEN }}
POSTHOG_URL: ${{ secrets.POSTHOG_URL }}
SENTRY_DSN: ${{ secrets.SENTRY_DSN }}
-
+
jobs:
release:
- runs-on: ubuntu-latest
+ runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2
- uses: actions/setup-node@v1
with:
node-version: 12.x
- - run: yarn
- - run: yarn bootstrap
- - run: yarn lint
- - run: yarn build
+ - run: yarn
+ - run: yarn bootstrap
+ - run: yarn lint
+ - run: yarn build
- run: yarn test
- name: Configure AWS Credentials
@@ -41,11 +41,11 @@ jobs:
- name: Publish budibase packages to NPM
env:
NPM_TOKEN: ${{ secrets.NPM_TOKEN }}
- run: |
+ run: |
# setup the username and email. I tend to use 'GitHub Actions Bot' with no email by default
git config user.name "Budibase Release Bot"
git config user.email "<>"
- echo //registry.npmjs.org/:_authToken=${NPM_TOKEN} >> .npmrc
+ echo //registry.npmjs.org/:_authToken=${NPM_TOKEN} >> .npmrc
yarn release
- name: 'Get Previous tag'
@@ -86,4 +86,3 @@ jobs:
charts_dir: docs
env:
CR_TOKEN: "${{ secrets.GITHUB_TOKEN }}"
-
\ No newline at end of file
diff --git a/LICENSE b/LICENSE
index a6bd926020..9c0a8c22a0 100644
--- a/LICENSE
+++ b/LICENSE
@@ -5,8 +5,7 @@ Each Budibase package has its own license:
builder: GPLv3
server: GPLv3
client: MPLv2.0
-standard-components: MPLv2.0
-You can consider Budibase to be GPLv3 licensed.
+You can consider Budibase to be GPLv3 licensed.
The apps that you build with Budibase do not fall under GPLv3 - hence why our components and client library are licensed differently.
diff --git a/SECURITY.md b/SECURITY.md
new file mode 100644
index 0000000000..e414f48cb8
--- /dev/null
+++ b/SECURITY.md
@@ -0,0 +1,15 @@
+# Security Policy
+
+## Versions
+
+As an open source product, we will only patch the latest major version for security vulnerabilities. Previous versions of budibase will not be retroactively patched.
+
+## Disclosing
+
+You can get in touch with us regarding a vulnerability via email at community@budibase.com.
+
+You can also disclose via huntr.dev. If you believe you have found a vulnerability, please disclose it on huntr and let us know.
+
+https://huntr.dev/bounties/disclose
+
+This will enable us to review the vulnerability and potentially reward you for your work.
diff --git a/hosting/docker-compose.yaml b/hosting/docker-compose.yaml
index 560b273668..18b93fdf61 100644
--- a/hosting/docker-compose.yaml
+++ b/hosting/docker-compose.yaml
@@ -51,6 +51,7 @@ services:
INTERNAL_API_KEY: ${INTERNAL_API_KEY}
REDIS_URL: redis-service:6379
REDIS_PASSWORD: ${REDIS_PASSWORD}
+ ACCOUNT_PORTAL_URL: https://portal.budi.live
volumes:
- ./logs:/logs
depends_on:
@@ -107,7 +108,7 @@ services:
depends_on:
- couchdb-service
command: ["sh","-c","sleep 10 && $${PUT_CALL}/_users && $${PUT_CALL}/_replicator; fg;"]
-
+
redis-service:
restart: always
image: redis
@@ -116,7 +117,7 @@ services:
- "${REDIS_PORT}:6379"
volumes:
- redis_data:/data
-
+
watchtower-service:
image: containrrr/watchtower
ports:
diff --git a/hosting/kubernetes/budibase/templates/worker-service-deployment.yaml b/hosting/kubernetes/budibase/templates/worker-service-deployment.yaml
index 703d59c075..6c165872c8 100644
--- a/hosting/kubernetes/budibase/templates/worker-service-deployment.yaml
+++ b/hosting/kubernetes/budibase/templates/worker-service-deployment.yaml
@@ -87,6 +87,8 @@ spec:
{{ end }}
- name: SELF_HOSTED
value: {{ .Values.globals.selfHosted | quote }}
+ - name: ACCOUNT_PORTAL_URL
+ value: {{ .Values.globals.accountPortalUrl | quote }}
image: budibase/worker
imagePullPolicy: Always
name: bbworker
diff --git a/hosting/kubernetes/budibase/values.yaml b/hosting/kubernetes/budibase/values.yaml
index 30594f95e3..1113842c8b 100644
--- a/hosting/kubernetes/budibase/values.yaml
+++ b/hosting/kubernetes/budibase/values.yaml
@@ -44,7 +44,7 @@ ingress:
nginx: true
certificateArn: ""
className: ""
- annotations:
+ annotations:
kubernetes.io/ingress.class: nginx
hosts:
- host: # change if using custom domain
@@ -55,7 +55,7 @@ ingress:
service:
name: proxy-service
port:
- number: 10000
+ number: 10000
resources: {}
# We usually recommend not to specify default resources and to leave this as a conscious
@@ -86,9 +86,10 @@ globals:
budibaseEnv: PRODUCTION
enableAnalytics: false
posthogToken: ""
- sentryDSN: ""
+ sentryDSN: ""
logLevel: info
selfHosted: 1
+ accountPortalUrL: ""
createSecrets: true # creates an internal API key, JWT secrets and redis password for you
# if createSecrets is set to false, you can hard-code your secrets here
@@ -120,7 +121,7 @@ services:
password: "" # only change if pointing to existing couch server
port: 5984
storage: 100Mi
-
+
redis:
enabled: true # disable if using external redis
port: 6379
@@ -128,15 +129,15 @@ services:
url: "" # only change if pointing to existing redis cluster and enabled: false
password: "budibase" # recommended to override if using built-in redis
storage: 100Mi
-
+
objectStore:
minio: true
browser: true
port: 9000
replicaCount: 1
accessKey: "" # AWS_ACCESS_KEY if using S3 or existing minio access key
- secretKey: "" # AWS_SECRET_ACCESS_KEY if using S3 or existing minio secret
- region: "" # AWS_REGION if using S3 or existing minio secret
- url: "" # only change if pointing to existing minio cluster and minio: false
+ secretKey: "" # AWS_SECRET_ACCESS_KEY if using S3 or existing minio secret
+ region: "" # AWS_REGION if using S3 or existing minio secret
+ url: "" # only change if pointing to existing minio cluster and minio: false
storage: 100Mi
diff --git a/hosting/kubernetes/envoy/envoy.yaml b/hosting/kubernetes/envoy/envoy.yaml
index 0b4c9204eb..4bf751b3a3 100644
--- a/hosting/kubernetes/envoy/envoy.yaml
+++ b/hosting/kubernetes/envoy/envoy.yaml
@@ -28,12 +28,12 @@ static_resources:
- match: { prefix: "/builder" }
route:
cluster: app-service
-
+
- match: { prefix: "/app_" }
route:
cluster: app-service
- # special case for worker admin API
+ # special cases for worker admin (deprecated), global and system API
- match: { prefix: "/api/global/" }
route:
cluster: worker-service
@@ -50,13 +50,13 @@ static_resources:
route:
cluster: app-service
- # special case for when API requests are made, can just forward, not to minio
+ # special case for when API requests are made, can just forward, not to minio
- match: { prefix: "/api/" }
route:
cluster: app-service
- match: { prefix: "/worker/" }
- route:
+ route:
cluster: worker-service
prefix_rewrite: "/"
@@ -85,7 +85,7 @@ static_resources:
- lb_endpoints:
- endpoint:
address:
- socket_address:
+ socket_address:
address: app-service.budibase.svc.cluster.local
port_value: 4002
@@ -113,7 +113,7 @@ static_resources:
- lb_endpoints:
- endpoint:
address:
- socket_address:
+ socket_address:
address: worker-service.budibase.svc.cluster.local
port_value: 4001
diff --git a/lerna.json b/lerna.json
index 93e1890431..9b2b1cac6d 100644
--- a/lerna.json
+++ b/lerna.json
@@ -1,5 +1,5 @@
{
- "version": "0.9.139",
+ "version": "0.9.140-alpha.0",
"npmClient": "yarn",
"packages": [
"packages/*"
diff --git a/package.json b/package.json
index 7e329faafa..3df577ca58 100644
--- a/package.json
+++ b/package.json
@@ -20,7 +20,6 @@
"setup": "node ./hosting/scripts/setup.js && yarn && yarn bootstrap && yarn build && yarn dev",
"bootstrap": "lerna link && lerna bootstrap",
"build": "lerna run build",
- "initialise": "lerna run initialise",
"publishdev": "lerna run publishdev",
"publishnpm": "yarn build && lerna publish --force-publish",
"release": "yarn build && lerna publish patch --yes --force-publish",
@@ -49,6 +48,8 @@
"release:helm": "./scripts/release_helm_chart.sh",
"multi:enable": "lerna run multi:enable",
"multi:disable": "lerna run multi:disable",
+ "selfhost:enable": "lerna run selfhost:enable",
+ "selfhost:disable": "lerna run selfhost:disable",
"postinstall": "husky install"
}
}
diff --git a/packages/auth/package.json b/packages/auth/package.json
index 32dc672a71..448b408742 100644
--- a/packages/auth/package.json
+++ b/packages/auth/package.json
@@ -1,6 +1,6 @@
{
"name": "@budibase/auth",
- "version": "0.9.139",
+ "version": "0.9.140-alpha.0",
"description": "Authentication middlewares for budibase builder and apps",
"main": "src/index.js",
"author": "Budibase",
diff --git a/packages/auth/scripts/jestSetup.js b/packages/auth/scripts/jestSetup.js
index 07648f693f..93dbf3fd5a 100644
--- a/packages/auth/scripts/jestSetup.js
+++ b/packages/auth/scripts/jestSetup.js
@@ -1,5 +1,6 @@
const env = require("../src/environment")
+env._set("SELF_HOSTED", "1")
env._set("NODE_ENV", "jest")
env._set("JWT_SECRET", "test-jwtsecret")
env._set("LOG_LEVEL", "silent")
diff --git a/packages/auth/src/cache/user.js b/packages/auth/src/cache/user.js
index e0936182a7..51bed0210e 100644
--- a/packages/auth/src/cache/user.js
+++ b/packages/auth/src/cache/user.js
@@ -1,9 +1,42 @@
const redis = require("../redis/authRedis")
const { getTenantId, lookupTenantId, getGlobalDB } = require("../tenancy")
+const env = require("../environment")
+const accounts = require("../cloud/accounts")
const EXPIRY_SECONDS = 3600
-exports.getUser = async (userId, tenantId = null) => {
+/**
+ * The default populate user function
+ */
+const populateFromDB = async (userId, tenantId) => {
+ const user = await getGlobalDB(tenantId).get(userId)
+ user.budibaseAccess = true
+
+ if (!env.SELF_HOSTED) {
+ const account = await accounts.getAccount(user.email)
+ if (account) {
+ user.account = account
+ user.accountPortalAccess = true
+ }
+ }
+
+ return user
+}
+
+/**
+ * Get the requested user by id.
+ * Use redis cache to first read the user.
+ * If not present fallback to loading the user directly and re-caching.
+ * @param {*} userId the id of the user to get
+ * @param {*} tenantId the tenant of the user to get
+ * @param {*} populateUser function to provide the user for re-caching. default to couch db
+ * @returns
+ */
+exports.getUser = async (
+ userId,
+ tenantId = null,
+ populateUser = populateFromDB
+) => {
if (!tenantId) {
try {
tenantId = getTenantId()
@@ -15,7 +48,7 @@ exports.getUser = async (userId, tenantId = null) => {
// try cache
let user = await client.get(userId)
if (!user) {
- user = await getGlobalDB(tenantId).get(userId)
+ user = await populateUser(userId, tenantId)
client.store(userId, user, EXPIRY_SECONDS)
}
if (user && !user.tenantId && tenantId) {
diff --git a/packages/auth/src/cloud/accounts.js b/packages/auth/src/cloud/accounts.js
new file mode 100644
index 0000000000..a102df8920
--- /dev/null
+++ b/packages/auth/src/cloud/accounts.js
@@ -0,0 +1,22 @@
+const API = require("./api")
+const env = require("../environment")
+
+const api = new API(env.ACCOUNT_PORTAL_URL)
+
+// TODO: Authorization
+
+exports.getAccount = async email => {
+ const payload = {
+ email,
+ }
+ const response = await api.post(`/api/accounts/search`, {
+ body: payload,
+ })
+ const json = await response.json()
+
+ if (response.status !== 200) {
+ throw Error(`Error getting account by email ${email}`, json)
+ }
+
+ return json[0]
+}
diff --git a/packages/auth/src/cloud/api.js b/packages/auth/src/cloud/api.js
new file mode 100644
index 0000000000..ffa785d02a
--- /dev/null
+++ b/packages/auth/src/cloud/api.js
@@ -0,0 +1,44 @@
+const fetch = require("node-fetch")
+class API {
+ constructor(host) {
+ this.host = host
+ }
+
+ apiCall =
+ method =>
+ async (url = "", options = {}) => {
+ if (!options.headers) {
+ options.headers = {}
+ }
+
+ if (!options.headers["Content-Type"]) {
+ options.headers = {
+ "Content-Type": "application/json",
+ Accept: "application/json",
+ ...options.headers,
+ }
+ }
+
+ let json = options.headers["Content-Type"] === "application/json"
+
+ const requestOptions = {
+ method: method,
+ body: json ? JSON.stringify(options.body) : options.body,
+ headers: options.headers,
+ // TODO: See if this is necessary
+ credentials: "include",
+ }
+
+ const resp = await fetch(`${this.host}${url}`, requestOptions)
+
+ return resp
+ }
+
+ post = this.apiCall("POST")
+ get = this.apiCall("GET")
+ patch = this.apiCall("PATCH")
+ del = this.apiCall("DELETE")
+ put = this.apiCall("PUT")
+}
+
+module.exports = API
diff --git a/packages/auth/src/db/utils.js b/packages/auth/src/db/utils.js
index 65f530fc7b..a1a831523e 100644
--- a/packages/auth/src/db/utils.js
+++ b/packages/auth/src/db/utils.js
@@ -35,10 +35,6 @@ exports.APP_PREFIX = DocumentTypes.APP + SEPARATOR
exports.APP_DEV = exports.APP_DEV_PREFIX = DocumentTypes.APP_DEV + SEPARATOR
exports.SEPARATOR = SEPARATOR
-function isDevApp(app) {
- return app.appId.startsWith(exports.APP_DEV_PREFIX)
-}
-
/**
* If creating DB allDocs/query params with only a single top level ID this can be used, this
* is usually the case as most of our docs are top level e.g. tables, automations, users and so on.
@@ -62,6 +58,18 @@ function getDocParams(docType, docId = null, otherProps = {}) {
}
}
+exports.isDevAppID = appId => {
+ return appId.startsWith(exports.APP_DEV_PREFIX)
+}
+
+exports.isProdAppID = appId => {
+ return appId.startsWith(exports.APP_PREFIX) && !exports.isDevAppID(appId)
+}
+
+function isDevApp(app) {
+ return exports.isDevAppID(app.appId)
+}
+
/**
* Given an app ID this will attempt to retrieve the tenant ID from it.
* @return {null|string} The tenant ID found within the app ID.
diff --git a/packages/auth/src/environment.js b/packages/auth/src/environment.js
index cddd7ab98a..bae5c65a1b 100644
--- a/packages/auth/src/environment.js
+++ b/packages/auth/src/environment.js
@@ -20,6 +20,8 @@ module.exports = {
MINIO_URL: process.env.MINIO_URL,
INTERNAL_API_KEY: process.env.INTERNAL_API_KEY,
MULTI_TENANCY: process.env.MULTI_TENANCY,
+ ACCOUNT_PORTAL_URL: process.env.ACCOUNT_PORTAL_URL,
+ SELF_HOSTED: !!parseInt(process.env.SELF_HOSTED),
isTest,
_set(key, value) {
process.env[key] = value
diff --git a/packages/auth/src/middleware/authenticated.js b/packages/auth/src/middleware/authenticated.js
index e3705a9a24..944f3ee9d9 100644
--- a/packages/auth/src/middleware/authenticated.js
+++ b/packages/auth/src/middleware/authenticated.js
@@ -21,7 +21,10 @@ function finalise(
* The tenancy modules should not be used here and it should be assumed that the tenancy context
* has not yet been populated.
*/
-module.exports = (noAuthPatterns = [], opts = { publicAllowed: false }) => {
+module.exports = (
+ noAuthPatterns = [],
+ opts = { publicAllowed: false, populateUser: null }
+) => {
const noAuthOptions = noAuthPatterns ? buildMatcherRegex(noAuthPatterns) : []
return async (ctx, next) => {
let publicEndpoint = false
@@ -46,7 +49,15 @@ module.exports = (noAuthPatterns = [], opts = { publicAllowed: false }) => {
error = "No session found"
} else {
try {
- user = await getUser(userId, session.tenantId)
+ if (opts && opts.populateUser) {
+ user = await getUser(
+ userId,
+ session.tenantId,
+ opts.populateUser(ctx)
+ )
+ } else {
+ user = await getUser(userId, session.tenantId)
+ }
delete user.password
authenticated = true
} catch (err) {
diff --git a/packages/auth/src/redis/utils.js b/packages/auth/src/redis/utils.js
index 09b4905298..6befecd9ba 100644
--- a/packages/auth/src/redis/utils.js
+++ b/packages/auth/src/redis/utils.js
@@ -8,11 +8,13 @@ const REDIS_PASSWORD = !env.REDIS_PASSWORD ? "budibase" : env.REDIS_PASSWORD
exports.Databases = {
PW_RESETS: "pwReset",
+ VERIFICATIONS: "verification",
INVITATIONS: "invitation",
DEV_LOCKS: "devLocks",
DEBOUNCE: "debounce",
SESSIONS: "session",
USER_CACHE: "users",
+ FLAGS: "flags",
}
exports.SEPARATOR = SEPARATOR
diff --git a/packages/auth/src/tenancy/tenancy.js b/packages/auth/src/tenancy/tenancy.js
index 6e18ea7154..ebd573496c 100644
--- a/packages/auth/src/tenancy/tenancy.js
+++ b/packages/auth/src/tenancy/tenancy.js
@@ -63,6 +63,7 @@ exports.tryAddTenant = async (tenantId, userId, email) => {
}
if (emailDoc) {
emailDoc.tenantId = tenantId
+ emailDoc.userId = userId
promises.push(db.put(emailDoc))
}
if (tenants.tenantIds.indexOf(tenantId) === -1) {
diff --git a/packages/bbui/package.json b/packages/bbui/package.json
index 408494b199..123d168fee 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": "0.9.139",
+ "version": "0.9.140-alpha.0",
"license": "AGPL-3.0",
"svelte": "src/index.js",
"module": "dist/bbui.es.js",
diff --git a/packages/bbui/src/ActionButton/ActionButton.svelte b/packages/bbui/src/ActionButton/ActionButton.svelte
index 83f71d385b..b518ac3d92 100644
--- a/packages/bbui/src/ActionButton/ActionButton.svelte
+++ b/packages/bbui/src/ActionButton/ActionButton.svelte
@@ -12,6 +12,7 @@
export let dataCy = null
export let size = "M"
export let active = false
+ export let fullWidth = false
function longPress(element) {
if (!longPressable) return
@@ -40,6 +41,7 @@
class:spectrum-ActionButton--quiet={quiet}
class:spectrum-ActionButton--emphasized={emphasized}
class:is-selected={selected}
+ class:fullWidth
class="spectrum-ActionButton spectrum-ActionButton--size{size}"
class:active
{disabled}
@@ -71,6 +73,9 @@
diff --git a/packages/bbui/src/Modal/ModalContent.svelte b/packages/bbui/src/Modal/ModalContent.svelte
index bba72e6093..678a813a61 100644
--- a/packages/bbui/src/Modal/ModalContent.svelte
+++ b/packages/bbui/src/Modal/ModalContent.svelte
@@ -46,8 +46,10 @@
{#if showDivider}
@@ -120,4 +122,9 @@
.close-icon :global(svg) {
margin-right: 0;
}
+
+ .header-spacing {
+ display: flex;
+ justify-content: space-between;
+ }
diff --git a/packages/bbui/src/ProgressCircle/ProgressCircle.svelte b/packages/bbui/src/ProgressCircle/ProgressCircle.svelte
index 9c8181ec7c..0428263346 100644
--- a/packages/bbui/src/ProgressCircle/ProgressCircle.svelte
+++ b/packages/bbui/src/ProgressCircle/ProgressCircle.svelte
@@ -13,7 +13,7 @@
}
}
- export let value = false
+ export let value = null
export let minValue = 0
export let maxValue = 100
@@ -42,7 +42,7 @@
diff --git a/packages/bbui/src/SideNavigation/Item.svelte b/packages/bbui/src/SideNavigation/Item.svelte
index f50270dfbd..dfebdb46a6 100644
--- a/packages/bbui/src/SideNavigation/Item.svelte
+++ b/packages/bbui/src/SideNavigation/Item.svelte
@@ -13,6 +13,7 @@
class="spectrum-SideNav-item"
class:is-selected={selected}
class:is-disabled={disabled}
+ on:click
>
{#if heading}
diff --git a/packages/bbui/src/Table/Table.svelte b/packages/bbui/src/Table/Table.svelte
index b6b61a8d04..11284b8917 100644
--- a/packages/bbui/src/Table/Table.svelte
+++ b/packages/bbui/src/Table/Table.svelte
@@ -3,6 +3,7 @@
import "@spectrum-css/table/dist/index-vars.css"
import CellRenderer from "./CellRenderer.svelte"
import SelectEditRenderer from "./SelectEditRenderer.svelte"
+ import { cloneDeep } from "lodash"
/**
* The expected schema is our normal couch schemas for our tables.
@@ -197,7 +198,7 @@
const editRow = (e, row) => {
e.stopPropagation()
- dispatch("editrow", row)
+ dispatch("editrow", cloneDeep(row))
}
const toggleSelectRow = row => {
diff --git a/packages/builder/assets/discord.svg b/packages/builder/assets/discord.svg
new file mode 100644
index 0000000000..3efe1ec110
--- /dev/null
+++ b/packages/builder/assets/discord.svg
@@ -0,0 +1,10 @@
+
diff --git a/packages/builder/assets/integromat.png b/packages/builder/assets/integromat.png
new file mode 100644
index 0000000000..1fbbe63e74
Binary files /dev/null and b/packages/builder/assets/integromat.png differ
diff --git a/packages/builder/assets/n8n.png b/packages/builder/assets/n8n.png
new file mode 100644
index 0000000000..b9dad93e5a
Binary files /dev/null and b/packages/builder/assets/n8n.png differ
diff --git a/packages/builder/assets/slack.svg b/packages/builder/assets/slack.svg
new file mode 100644
index 0000000000..d0a7c176f9
--- /dev/null
+++ b/packages/builder/assets/slack.svg
@@ -0,0 +1,33 @@
+
+
+
diff --git a/packages/builder/assets/zapier.png b/packages/builder/assets/zapier.png
new file mode 100644
index 0000000000..3805331440
Binary files /dev/null and b/packages/builder/assets/zapier.png differ
diff --git a/packages/builder/cypress/integration/createAutomation.spec.js b/packages/builder/cypress/integration/createAutomation.spec.js
index e82eeff670..0620a15e25 100644
--- a/packages/builder/cypress/integration/createAutomation.spec.js
+++ b/packages/builder/cypress/integration/createAutomation.spec.js
@@ -7,26 +7,42 @@ context("Create a automation", () => {
// https://on.cypress.io/interacting-with-elements
it("should create a automation", () => {
cy.createTestTableWithData()
-
+ cy.wait(2000)
cy.contains("Automate").click()
cy.get("[data-cy='new-screen'] > .spectrum-Icon").click()
- cy.get(".spectrum-Dialog-grid").within(() => {
+ cy.get(".modal-inner-wrapper").within(() => {
cy.get("input").type("Add Row")
+ cy.contains("Row Created").click()
+ cy.wait(500)
cy.get(".spectrum-Button--cta").click()
})
- // Add trigger
- cy.contains("Trigger").click()
- cy.contains("Row Created").click()
- cy.get(".setup").within(() => {
- cy.get(".spectrum-Picker-label").click()
- cy.contains("dog").click()
- })
+ // Setup trigger
+ cy.contains("Setup").click()
+ cy.get(".spectrum-Picker-label").click()
+ cy.wait(500)
+ cy.contains("dog").click()
// Create action
- cy.contains("Action").click()
- cy.contains("Create Row").click()
- cy.get(".setup").within(() => {
+ cy.contains("Add Action").click()
+ cy.get(".modal-inner-wrapper").within(() => {
+ cy.wait(1000)
+ cy.contains("Create Row").trigger('mouseover').click().click()
+ cy.get(".spectrum-Button--cta").click()
+ })
+ cy.contains("Setup").click()
+ cy.get(".spectrum-Picker-label").click()
+ cy.contains("dog").click()
+ cy.get(".spectrum-Textfield-input")
+ .first()
+ .type("goodboy")
+ cy.get(".spectrum-Textfield-input")
+ .eq(1)
+ .type("11")
+
+ cy.contains("Run test").click()
+ cy.get(".modal-inner-wrapper").within(() => {
+ cy.wait(1000)
cy.get(".spectrum-Picker-label").click()
cy.contains("dog").click()
cy.get(".spectrum-Textfield-input")
@@ -35,19 +51,12 @@ context("Create a automation", () => {
cy.get(".spectrum-Textfield-input")
.eq(1)
.type("11")
+ cy.get(".spectrum-Textfield-input")
+ .eq(2)
+ .type("123456")
+ cy.get(".spectrum-Textfield-input")
+ .eq(3)
+ .type("123456")
})
-
- // Save
- cy.contains("Save Automation").click()
-
- // Activate Automation
- cy.get("[data-cy=activate-automation]").click()
- })
-
- it("should add row when a new row is added", () => {
- cy.contains("Data").click()
- cy.addRow(["Rover", 15])
- cy.reload()
- cy.contains("goodboy").should("have.text", "goodboy")
})
})
diff --git a/packages/builder/cypress/setup.js b/packages/builder/cypress/setup.js
index 4ad8e5287d..1a6f1d5b2b 100644
--- a/packages/builder/cypress/setup.js
+++ b/packages/builder/cypress/setup.js
@@ -23,6 +23,7 @@ process.env.MINIO_SECRET_KEY = "budibase"
process.env.COUCH_DB_USER = "budibase"
process.env.COUCH_DB_PASSWORD = "budibase"
process.env.INTERNAL_API_KEY = "budibase"
+process.env.ALLOW_DEV_AUTOMATIONS = 1
// Stop info logs polluting test outputs
process.env.LOG_LEVEL = "error"
diff --git a/packages/builder/cypress/support/commands.js b/packages/builder/cypress/support/commands.js
index 261b840577..ea6ca81e66 100644
--- a/packages/builder/cypress/support/commands.js
+++ b/packages/builder/cypress/support/commands.js
@@ -34,12 +34,11 @@ Cypress.Commands.add("createApp", name => {
cy.get(".spectrum-Modal")
.within(() => {
cy.get("input").eq(0).type(name).should("have.value", name).blur()
- cy.contains("Create app").click()
+ cy.get(".spectrum-ButtonGroup").contains("Create app").click()
})
.then(() => {
- cy.get(".selected > .content", {
- timeout: 20000,
- }).should("be.visible")
+ cy.expandBudibaseConnection()
+ cy.get(".nav-item.selected > .content").should("be.visible")
})
})
@@ -83,6 +82,7 @@ Cypress.Commands.add("createTable", tableName => {
Cypress.Commands.add("addColumn", (tableName, columnName, type) => {
// Select Table
+ cy.selectTable(tableName)
cy.contains(".nav-item", tableName).click()
cy.contains("Create column").click()
@@ -161,3 +161,15 @@ Cypress.Commands.add("createScreen", (screenName, route) => {
cy.get(".spectrum-Button--cta").click()
})
})
+
+Cypress.Commands.add("expandBudibaseConnection", () => {
+ if (Cypress.$(".nav-item > .content > .opened").length === 0) {
+ // expand the Budibase DB connection string
+ cy.get(".icon.arrow").eq(0).click()
+ }
+})
+
+Cypress.Commands.add("selectTable", tableName => {
+ cy.expandBudibaseConnection()
+ cy.contains(".nav-item", tableName).click()
+})
diff --git a/packages/builder/package.json b/packages/builder/package.json
index 15ebb5e586..fb4d050392 100644
--- a/packages/builder/package.json
+++ b/packages/builder/package.json
@@ -1,6 +1,6 @@
{
"name": "@budibase/builder",
- "version": "0.9.139",
+ "version": "0.9.140-alpha.0",
"license": "AGPL-3.0",
"private": true,
"scripts": {
@@ -65,10 +65,10 @@
}
},
"dependencies": {
- "@budibase/bbui": "^0.9.139",
- "@budibase/client": "^0.9.139",
+ "@budibase/bbui": "^0.9.140-alpha.0",
+ "@budibase/client": "^0.9.140-alpha.0",
"@budibase/colorpicker": "1.1.2",
- "@budibase/string-templates": "^0.9.139",
+ "@budibase/string-templates": "^0.9.140-alpha.0",
"@sentry/browser": "5.19.1",
"@spectrum-css/page": "^3.0.1",
"@spectrum-css/vars": "^3.0.1",
diff --git a/packages/builder/src/builderStore/dataBinding.js b/packages/builder/src/builderStore/dataBinding.js
index 00eaaf0249..903512d0fb 100644
--- a/packages/builder/src/builderStore/dataBinding.js
+++ b/packages/builder/src/builderStore/dataBinding.js
@@ -1,6 +1,10 @@
import { cloneDeep } from "lodash/fp"
import { get } from "svelte/store"
-import { findComponent, findComponentPath } from "./storeUtils"
+import {
+ findComponent,
+ findComponentPath,
+ findAllMatchingComponents,
+} from "./storeUtils"
import { store } from "builderStore"
import { tables as tablesStore, queries as queriesStores } from "stores/backend"
import { makePropSafe } from "@budibase/string-templates"
@@ -18,7 +22,9 @@ export const getBindableProperties = (asset, componentId) => {
const userBindings = getUserBindings()
const urlBindings = getUrlBindings(asset)
const deviceBindings = getDeviceBindings()
+ const stateBindings = getStateBindings()
return [
+ ...stateBindings,
...deviceBindings,
...urlBindings,
...contextBindings,
@@ -256,6 +262,22 @@ const getDeviceBindings = () => {
return bindings
}
+/**
+ * Gets all state bindings that are globally available.
+ */
+const getStateBindings = () => {
+ let bindings = []
+ if (get(store).clientFeatures?.state) {
+ const safeState = makePropSafe("state")
+ bindings = getAllStateVariables().map(key => ({
+ type: "context",
+ runtimeBinding: `${safeState}.${makePropSafe(key)}`,
+ readableBinding: `State.${key}`,
+ }))
+ }
+ return bindings
+}
+
/**
* Gets all bindable properties from URL parameters.
*/
@@ -458,3 +480,49 @@ export function runtimeToReadableBinding(bindableProperties, textWithBindings) {
"readableBinding"
)
}
+
+/**
+ * Returns an array of the keys of any state variables which are set anywhere
+ * in the app.
+ */
+export const getAllStateVariables = () => {
+ let allComponents = []
+
+ // Find all onClick settings in all layouts
+ get(store).layouts.forEach(layout => {
+ const components = findAllMatchingComponents(
+ layout.props,
+ c => c.onClick != null
+ )
+ allComponents = allComponents.concat(components || [])
+ })
+
+ // Find all onClick settings in all screens
+ get(store).screens.forEach(screen => {
+ const components = findAllMatchingComponents(
+ screen.props,
+ c => c.onClick != null
+ )
+ allComponents = allComponents.concat(components || [])
+ })
+
+ // Add state bindings for all state actions
+ let bindingSet = new Set()
+ allComponents.forEach(component => {
+ if (!Array.isArray(component.onClick)) {
+ return
+ }
+ component.onClick.forEach(action => {
+ if (
+ action["##eventHandlerType"] === "Update State" &&
+ action.parameters?.type === "set" &&
+ action.parameters?.key &&
+ action.parameters?.value
+ ) {
+ bindingSet.add(action.parameters.key)
+ }
+ })
+ })
+
+ return Array.from(bindingSet)
+}
diff --git a/packages/builder/src/builderStore/store/automation/Automation.js b/packages/builder/src/builderStore/store/automation/Automation.js
index a9dce88258..dcbb747e38 100644
--- a/packages/builder/src/builderStore/store/automation/Automation.js
+++ b/packages/builder/src/builderStore/store/automation/Automation.js
@@ -13,6 +13,10 @@ export default class Automation {
return this.automation.definition.trigger
}
+ addTestData(data) {
+ this.automation.testData = data
+ }
+
addBlock(block) {
// Make sure to add trigger if doesn't exist
if (!this.hasTrigger() && block.type === "TRIGGER") {
diff --git a/packages/builder/src/builderStore/store/automation/index.js b/packages/builder/src/builderStore/store/automation/index.js
index c372f27bb7..e60553070b 100644
--- a/packages/builder/src/builderStore/store/automation/index.js
+++ b/packages/builder/src/builderStore/store/automation/index.js
@@ -17,7 +17,6 @@ const automationActions = store => ({
state.blockDefinitions = {
TRIGGER: jsonResponses[1].trigger,
ACTION: jsonResponses[1].action,
- LOGIC: jsonResponses[1].logic,
}
// if previously selected find the new obj and select it
if (selected) {
@@ -46,21 +45,24 @@ const automationActions = store => ({
return state
})
},
- save: async ({ automation }) => {
+ save: async automation => {
const UPDATE_AUTOMATION_URL = `/api/automations`
const response = await api.put(UPDATE_AUTOMATION_URL, automation)
const json = await response.json()
store.update(state => {
+ const newAutomation = json.automation
const existingIdx = state.automations.findIndex(
existing => existing._id === automation._id
)
- state.automations.splice(existingIdx, 1, json.automation)
- state.automations = state.automations
- store.actions.select(json.automation)
- return state
+ if (existingIdx !== -1) {
+ state.automations.splice(existingIdx, 1, newAutomation)
+ state.automations = [...state.automations]
+ store.actions.select(newAutomation)
+ return state
+ }
})
},
- delete: async ({ automation }) => {
+ delete: async automation => {
const { _id, _rev } = automation
const DELETE_AUTOMATION_URL = `/api/automations/${_id}/${_rev}`
await api.delete(DELETE_AUTOMATION_URL)
@@ -70,16 +72,24 @@ const automationActions = store => ({
existing => existing._id === _id
)
state.automations.splice(existingIdx, 1)
- state.automations = state.automations
+ state.automations = [...state.automations]
state.selectedAutomation = null
state.selectedBlock = null
return state
})
},
- trigger: async ({ automation }) => {
+ trigger: async automation => {
const { _id } = automation
- const TRIGGER_AUTOMATION_URL = `/api/automations/${_id}/trigger`
- return await api.post(TRIGGER_AUTOMATION_URL)
+ return await api.post(`/api/automations/${_id}/trigger`)
+ },
+ test: async (automation, testData) => {
+ const { _id } = automation
+ const response = await api.post(`/api/automations/${_id}/test`, testData)
+ const json = await response.json()
+ store.update(state => {
+ state.selectedAutomation.testResults = json
+ return state
+ })
},
select: automation => {
store.update(state => {
@@ -88,6 +98,12 @@ const automationActions = store => ({
return state
})
},
+ addTestDataToAutomation: data => {
+ store.update(state => {
+ state.selectedAutomation.addTestData(data)
+ return state
+ })
+ },
addBlockToAutomation: block => {
store.update(state => {
const newBlock = state.selectedAutomation.addBlock(cloneDeep(block))
@@ -129,7 +145,6 @@ export const getAutomationStore = () => {
blockDefinitions: {
TRIGGER: [],
ACTION: [],
- LOGIC: [],
},
selectedAutomation: null,
}
diff --git a/packages/builder/src/builderStore/store/frontend.js b/packages/builder/src/builderStore/store/frontend.js
index 192ade9e5d..603fa88b09 100644
--- a/packages/builder/src/builderStore/store/frontend.js
+++ b/packages/builder/src/builderStore/store/frontend.js
@@ -41,6 +41,9 @@ const INITIAL_FRONTEND_STATE = {
spectrumThemes: false,
intelligentLoading: false,
deviceAwareness: false,
+ state: false,
+ customThemes: false,
+ devicePreview: false,
},
currentFrontEndType: "none",
selectedScreenId: "",
@@ -53,6 +56,8 @@ const INITIAL_FRONTEND_STATE = {
routes: {},
clientLibPath: "",
theme: "",
+ customTheme: {},
+ previewDevice: "desktop",
}
export const getFrontendStore = () => {
@@ -77,6 +82,7 @@ export const getFrontendStore = () => {
layouts,
screens,
theme: application.theme || "spectrum--light",
+ customTheme: application.customTheme,
hasAppPackage: true,
appInstance: application.instance,
clientLibPath,
@@ -110,6 +116,22 @@ export const getFrontendStore = () => {
}
},
},
+ customTheme: {
+ save: async customTheme => {
+ const appId = get(store).appId
+ const response = await api.put(`/api/applications/${appId}`, {
+ customTheme,
+ })
+ if (response.status === 200) {
+ store.update(state => {
+ state.customTheme = customTheme
+ return state
+ })
+ } else {
+ throw new Error("Error updating theme")
+ }
+ },
+ },
routing: {
fetch: async () => {
const response = await api.get("/api/routing")
@@ -210,6 +232,12 @@ export const getFrontendStore = () => {
await store.actions.layouts.save(selectedAsset)
}
},
+ setDevice: device => {
+ store.update(state => {
+ state.previewDevice = device
+ return state
+ })
+ },
},
layouts: {
select: layoutId => {
diff --git a/packages/builder/src/components/automation/AutomationBuilder/AutomationBuilder.svelte b/packages/builder/src/components/automation/AutomationBuilder/AutomationBuilder.svelte
index 62faff3caa..7ce77a58e3 100644
--- a/packages/builder/src/components/automation/AutomationBuilder/AutomationBuilder.svelte
+++ b/packages/builder/src/components/automation/AutomationBuilder/AutomationBuilder.svelte
@@ -1,10 +1,8 @@
{#if automation}
-
{/if}
diff --git a/packages/builder/src/components/automation/AutomationBuilder/BlockList.svelte b/packages/builder/src/components/automation/AutomationBuilder/BlockList.svelte
deleted file mode 100644
index af5c9e449e..0000000000
--- a/packages/builder/src/components/automation/AutomationBuilder/BlockList.svelte
+++ /dev/null
@@ -1,108 +0,0 @@
-
-
-
- {#each tabs as tab, idx}
-
-
onChangeTab(idx)}
- >
- {tab.label}
-
-
- {/each}
-
- (selectedIndex = null)}
- bind:this={popover}
- {anchor}
- align="left"
->
-
- {#each blocks as [stepId, blockDefinition]}
- addBlockToAutomation(stepId, blockDefinition)}
- />
- {/each}
-
-
-
-
-
-
-
diff --git a/packages/builder/src/components/automation/AutomationBuilder/FlowChart/ActionModal.svelte b/packages/builder/src/components/automation/AutomationBuilder/FlowChart/ActionModal.svelte
new file mode 100644
index 0000000000..b822973b62
--- /dev/null
+++ b/packages/builder/src/components/automation/AutomationBuilder/FlowChart/ActionModal.svelte
@@ -0,0 +1,135 @@
+
+
+ {
+ blockComplete = true
+ addBlockToAutomation()
+ }}
+>
+ Select an app or event.
+
+ Apps
+
+
+ {#each Object.entries(external) as [idx, action]}
+
selectAction(action)}
+ >
+
+
+
+ {idx.charAt(0).toUpperCase() + idx.slice(1)}
+
+
+ {/each}
+
+
+ Actions
+
+
+ {#each Object.entries(internal) as [idx, action]}
+
selectAction(action)}
+ >
+
+
+
+ {action.name}
+
+
+ {/each}
+
+
+
+
+
diff --git a/packages/builder/src/components/automation/AutomationBuilder/FlowChart/Arrow.svelte b/packages/builder/src/components/automation/AutomationBuilder/FlowChart/Arrow.svelte
index 931490b197..5aec39abeb 100644
--- a/packages/builder/src/components/automation/AutomationBuilder/FlowChart/Arrow.svelte
+++ b/packages/builder/src/components/automation/AutomationBuilder/FlowChart/Arrow.svelte
@@ -6,6 +6,7 @@
xmlns="http://www.w3.org/2000/svg"
>
diff --git a/packages/builder/src/components/automation/AutomationBuilder/FlowChart/ExternalActions.js b/packages/builder/src/components/automation/AutomationBuilder/FlowChart/ExternalActions.js
new file mode 100644
index 0000000000..843445a3c2
--- /dev/null
+++ b/packages/builder/src/components/automation/AutomationBuilder/FlowChart/ExternalActions.js
@@ -0,0 +1,11 @@
+import DiscordLogo from "assets/discord.svg"
+import ZapierLogo from "assets/zapier.png"
+import IntegromatLogo from "assets/integromat.png"
+import SlackLogo from "assets/slack.svg"
+
+export const externalActions = {
+ zapier: { name: "zapier", icon: ZapierLogo },
+ discord: { name: "discord", icon: DiscordLogo },
+ slack: { name: "slack", icon: SlackLogo },
+ integromat: { name: "integromat", icon: IntegromatLogo },
+}
diff --git a/packages/builder/src/components/automation/AutomationBuilder/FlowChart/FlowChart.svelte b/packages/builder/src/components/automation/AutomationBuilder/FlowChart/FlowChart.svelte
index e960271b87..c05a103fac 100644
--- a/packages/builder/src/components/automation/AutomationBuilder/FlowChart/FlowChart.svelte
+++ b/packages/builder/src/components/automation/AutomationBuilder/FlowChart/FlowChart.svelte
@@ -1,12 +1,23 @@
-{#if !blocks.length}Add a trigger to your automation to get started{/if}
-
- {#each blocks as block, idx (block.id)}
-
-
- {#if idx !== blocks.length - 1}
-
- {/if}
+
+
+
+
+
{automation.name}
+
+
+
+
+
+
+
{
+ testDataModal.show()
+ }}
+ icon="MultipleCheck"
+ size="S">Run test
+
+
- {/each}
-
+ {#each blocks as block, idx (block.id)}
+
+
+ {#if idx !== blocks.length - 1}
+
+
+
+ {/if}
+
+ {/each}
+
+
+ Are you sure you wish to delete the automation
+ {automation.name}?
+ This action cannot be undone.
+
+
+
+
+
+
diff --git a/packages/builder/src/components/automation/AutomationBuilder/FlowChart/FlowItem.svelte b/packages/builder/src/components/automation/AutomationBuilder/FlowChart/FlowItem.svelte
index 439db62639..f077ac35d7 100644
--- a/packages/builder/src/components/automation/AutomationBuilder/FlowChart/FlowItem.svelte
+++ b/packages/builder/src/components/automation/AutomationBuilder/FlowChart/FlowItem.svelte
@@ -1,86 +1,203 @@
onSelect(block)}
+ on:click={() => {
+ onSelect(block)
+ }}
>
-
- {#if block.type === "TRIGGER"}
-
- When this happens...
- {:else if block.type === "ACTION"}
-
- Do this...
- {:else if block.type === "LOGIC"}
-
- Only continue if...
- {/if}
-
- {#if block.type === "TRIGGER"}Trigger{:else}Step {blockIdx + 1}{/if}
+
+
{
+ blockComplete = !blockComplete
+ }}
+ class="splitHeader"
+ >
+
+ {#if externalActions[block.stepId]}
+
+ {:else}
+
+ {/if}
+
+ {#if isTrigger}
+ When this happens:
+ {:else}
+ Do this:
+ {/if}
+
+ {block?.name?.toUpperCase() || ""}
+
+
+ {#if testResult}
+
resultsModal.show()}>
+ View response
+
+ {/if}
- {#if block.type !== "TRIGGER" || allowDeleteTrigger}
-
- {/if}
-
-
-
-
-
+
+ {#if !blockComplete}
+
+
+
+
+
+ {#if setupToggled}
+
+ {#if lastStep}
+
+ {/if}
+
+ {/if}
+
+
+ {/if}
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/packages/builder/src/components/automation/AutomationBuilder/FlowChart/ResultsModal.svelte b/packages/builder/src/components/automation/AutomationBuilder/FlowChart/ResultsModal.svelte
new file mode 100644
index 0000000000..7dfdff20a7
--- /dev/null
+++ b/packages/builder/src/components/automation/AutomationBuilder/FlowChart/ResultsModal.svelte
@@ -0,0 +1,114 @@
+
+
+
+
+
+ {#if isTrigger || testResult[0].outputs.success}
+
+
+
+ {:else}
+
+
+
+ {/if}
+
+
+
+ {
+ inputToggled = !inputToggled
+ }}
+ class="toggle splitHeader"
+ >
+
+
+ {#if inputToggled}
+
+ {:else}
+
+ {/if}
+
+
+ {#if inputToggled}
+
+
+
+ {/if}
+
+ {
+ outputToggled = !outputToggled
+ }}
+ class="toggle splitHeader"
+ >
+
+
+ {#if outputToggled}
+
+ {:else}
+
+ {/if}
+
+
+ {#if outputToggled}
+
+
+
+ {/if}
+
+
+
diff --git a/packages/builder/src/components/automation/AutomationBuilder/FlowChart/TestDataModal.svelte b/packages/builder/src/components/automation/AutomationBuilder/FlowChart/TestDataModal.svelte
new file mode 100644
index 0000000000..8caba9d351
--- /dev/null
+++ b/packages/builder/src/components/automation/AutomationBuilder/FlowChart/TestDataModal.svelte
@@ -0,0 +1,91 @@
+
+
+ {
+ automationStore.actions.addTestDataToAutomation(testData)
+ automationStore.actions.test(
+ $automationStore.selectedAutomation?.automation,
+ testData
+ )
+ }}
+ cancelText="Cancel"
+>
+
+
+
+
+
+
+
+
+
diff --git a/packages/builder/src/components/automation/AutomationPanel/AutomationList.svelte b/packages/builder/src/components/automation/AutomationPanel/AutomationList.svelte
index f70366773a..79de4bfbe6 100644
--- a/packages/builder/src/components/automation/AutomationPanel/AutomationList.svelte
+++ b/packages/builder/src/components/automation/AutomationPanel/AutomationList.svelte
@@ -6,7 +6,6 @@
import EditAutomationPopover from "./EditAutomationPopover.svelte"
$: selectedAutomationId = $automationStore.selectedAutomation?.automation?._id
-
onMount(() => {
automationStore.actions.fetch()
})
diff --git a/packages/builder/src/components/automation/AutomationPanel/AutomationPanel.svelte b/packages/builder/src/components/automation/AutomationPanel/AutomationPanel.svelte
index dbcbc33db0..0c975eab18 100644
--- a/packages/builder/src/components/automation/AutomationPanel/AutomationPanel.svelte
+++ b/packages/builder/src/components/automation/AutomationPanel/AutomationPanel.svelte
@@ -2,7 +2,9 @@
import AutomationList from "./AutomationList.svelte"
import CreateAutomationModal from "./CreateAutomationModal.svelte"
import { Icon, Modal, Tabs, Tab } from "@budibase/bbui"
- let modal
+
+ export let modal
+ export let webhookModal
@@ -11,7 +13,7 @@
diff --git a/packages/builder/src/components/automation/AutomationPanel/CreateAutomationModal.svelte b/packages/builder/src/components/automation/AutomationPanel/CreateAutomationModal.svelte
index dfcfc2ab95..7700a4a1c2 100644
--- a/packages/builder/src/components/automation/AutomationPanel/CreateAutomationModal.svelte
+++ b/packages/builder/src/components/automation/AutomationPanel/CreateAutomationModal.svelte
@@ -3,12 +3,14 @@
import { database } from "stores/backend"
import { automationStore } from "builderStore"
import { notifications } from "@budibase/bbui"
- import { Icon, Input, ModalContent } from "@budibase/bbui"
+ import { Input, ModalContent, Layout, Body, Icon } from "@budibase/bbui"
import analytics from "analytics"
let name
+ let selectedTrigger
+ let triggerVal
+ export let webhookModal
- $: valid = !!name
$: instanceId = $database._id
async function createAutomation() {
@@ -16,41 +18,96 @@
name,
instanceId,
})
+ const newBlock = $automationStore.selectedAutomation.constructBlock(
+ "TRIGGER",
+ triggerVal.stepId,
+ triggerVal
+ )
+
+ automationStore.actions.addBlockToAutomation(newBlock)
+ if (triggerVal.stepId === "WEBHOOK") {
+ webhookModal.show
+ }
+
+ await automationStore.actions.save(
+ $automationStore.selectedAutomation?.automation
+ )
+
notifications.success(`Automation ${name} created.`)
+
$goto(`./${$automationStore.selectedAutomation.automation._id}`)
analytics.captureEvent("Automation Created", { name })
}
+ $: triggers = Object.entries($automationStore.blockDefinitions.TRIGGER)
+
+ const selectTrigger = trigger => {
+ triggerVal = trigger
+ selectedTrigger = trigger.name
+ }
+ Please name your automation, then select a trigger. Every automation must
+ start with a trigger.
+
-
-
- Learn about automations
-
+
+
+ Triggers
+
+
+ {#each triggers as [idx, trigger]}
+
selectTrigger(trigger)}
+ >
+
+
+
+ {trigger.name}
+
+
+ {/each}
+
+
diff --git a/packages/builder/src/components/automation/AutomationPanel/EditAutomationPopover.svelte b/packages/builder/src/components/automation/AutomationPanel/EditAutomationPopover.svelte
index a99c11e9e1..fc12b60676 100644
--- a/packages/builder/src/components/automation/AutomationPanel/EditAutomationPopover.svelte
+++ b/packages/builder/src/components/automation/AutomationPanel/EditAutomationPopover.svelte
@@ -1,20 +1,17 @@
+
+
+
+
+
+
+ Learn about automations
+
+
+
+
+
diff --git a/packages/builder/src/components/automation/SetupPanel/AutomationBlockSetup.svelte b/packages/builder/src/components/automation/SetupPanel/AutomationBlockSetup.svelte
index d1c5d104d3..adc22e5daf 100644
--- a/packages/builder/src/components/automation/SetupPanel/AutomationBlockSetup.svelte
+++ b/packages/builder/src/components/automation/SetupPanel/AutomationBlockSetup.svelte
@@ -2,8 +2,16 @@
import TableSelector from "./TableSelector.svelte"
import RowSelector from "./RowSelector.svelte"
import SchemaSetup from "./SchemaSetup.svelte"
- import { Button, Input, Select, Label } from "@budibase/bbui"
+ import {
+ Button,
+ Input,
+ Select,
+ Label,
+ ActionButton,
+ Drawer,
+ } from "@budibase/bbui"
import { automationStore } from "builderStore"
+ import { tables } from "stores/backend"
import WebhookDisplay from "../Shared/WebhookDisplay.svelte"
import DrawerBindableInput from "../../common/bindings/DrawerBindableInput.svelte"
import AutomationBindingPanel from "../../common/bindings/ServerBindingPanel.svelte"
@@ -12,16 +20,47 @@
import QueryParamSelector from "./QueryParamSelector.svelte"
import CronBuilder from "./CronBuilder.svelte"
import Editor from "components/integration/QueryEditor.svelte"
+ import { debounce } from "lodash"
+ import ModalBindableInput from "components/common/bindings/ModalBindableInput.svelte"
+ import FilterDrawer from "components/design/PropertiesPanel/PropertyControls/FilterEditor/FilterDrawer.svelte"
+ // need the client lucene builder to convert to the structure API expects
+ import { buildLuceneQuery } from "../../../../../client/src/utils/lucene"
export let block
export let webhookModal
- $: inputs = Object.entries(block.schema?.inputs?.properties || {})
+ export let testData
+ export let schemaProperties
+ export let isTestModal = false
+ let drawer
+ let tempFilters = lookForFilters(schemaProperties) || []
+ let fillWidth = true
$: stepId = block.stepId
$: bindings = getAvailableBindings(
- block,
+ block || $automationStore.selectedBlock,
$automationStore.selectedAutomation?.automation?.definition
)
+ $: inputData = testData ? testData : block.inputs
+ $: tableId = inputData ? inputData.tableId : null
+ $: table = tableId
+ ? $tables.list.find(table => table._id === inputData.tableId)
+ : { schema: {} }
+ $: schemaFields = table ? Object.values(table.schema) : []
+
+ const onChange = debounce(
+ async function (e, key) {
+ if (isTestModal) {
+ testData[key] = e.detail
+ } else {
+ block.inputs[key] = e.detail
+ await automationStore.actions.save(
+ $automationStore.selectedAutomation?.automation
+ )
+ }
+ },
+ isTestModal ? 0 : 800
+ )
+
function getAvailableBindings(block, automation) {
if (!block || !automation) {
return []
@@ -52,64 +91,158 @@
}
return bindings
}
+
+ function lookForFilters(properties) {
+ if (!properties) {
+ return []
+ }
+ let filters
+ const inputs = testData ? testData : block.inputs
+ for (let [key, field] of properties) {
+ // need to look for the builder definition (keyed separately, see saveFilters)
+ const defKey = `${key}-def`
+ if (field.customType === "filters" && inputs?.[defKey]) {
+ filters = inputs[defKey]
+ break
+ }
+ }
+ return filters || []
+ }
+
+ function saveFilters(key) {
+ const filters = buildLuceneQuery(tempFilters)
+ const defKey = `${key}-def`
+ inputData[key] = filters
+ inputData[defKey] = tempFilters
+ onChange({ detail: filters }, key)
+ // need to store the builder definition in the automation
+ onChange({ detail: tempFilters }, defKey)
+ drawer.hide()
+ }
-
{block.name}
- {#each inputs as [key, value]}
+ {#each schemaProperties as [key, value]}
-
+
{#if value.type === "string" && value.enum}
{/each}
@@ -132,9 +265,7 @@
grid-gap: 5px;
}
- .block-label {
- font-weight: 600;
- font-size: var(--font-size-s);
- color: var(--grey-7);
+ .test :global(.drawer) {
+ width: 10000px !important;
}
diff --git a/packages/builder/src/components/automation/SetupPanel/CronBuilder.svelte b/packages/builder/src/components/automation/SetupPanel/CronBuilder.svelte
index 810e452742..1b410cd86a 100644
--- a/packages/builder/src/components/automation/SetupPanel/CronBuilder.svelte
+++ b/packages/builder/src/components/automation/SetupPanel/CronBuilder.svelte
@@ -1,7 +1,13 @@
-
+
diff --git a/packages/builder/src/components/automation/SetupPanel/RowSelector.svelte b/packages/builder/src/components/automation/SetupPanel/RowSelector.svelte
index 99f41908e7..1d54c86b4a 100644
--- a/packages/builder/src/components/automation/SetupPanel/RowSelector.svelte
+++ b/packages/builder/src/components/automation/SetupPanel/RowSelector.svelte
@@ -1,14 +1,27 @@
table.name}
getOptionValue={table => table._id}
@@ -30,21 +44,52 @@
{#each schemaFields as [field, schema]}
{#if !schema.autocolumn}
- {#if schemaHasOptions(schema)}
+ {#if schemaHasOptions(schema) && schema.type !== "array"}
onChange(e, field)}
label={field}
+ value={value[field]}
+ options={schema.constraints.inclusion}
+ />
+ {:else if schema.type === "datetime"}
+ onChange(e, field)}
+ />
+ {:else if schema.type === "boolean"}
+ onChange(e, field)}
+ />
+ {:else if schema.type === "array"}
+
{:else if schema.type === "string" || schema.type === "number"}
- (value[field] = e.detail)}
- label={field}
- type="string"
- {bindings}
- />
+ {#if $automationStore.selectedAutomation.automation.testData}
+ onChange(e, field)}
+ {bindings}
+ />
+ {:else}
+ onChange(e, field)}
+ label={field}
+ type="string"
+ {bindings}
+ fillWidth={true}
+ />
+ {/if}
{/if}
{/if}
{/each}
diff --git a/packages/builder/src/components/automation/SetupPanel/SchemaSetup.svelte b/packages/builder/src/components/automation/SetupPanel/SchemaSetup.svelte
index 1257563ff8..730de6270a 100644
--- a/packages/builder/src/components/automation/SetupPanel/SchemaSetup.svelte
+++ b/packages/builder/src/components/automation/SetupPanel/SchemaSetup.svelte
@@ -1,11 +1,18 @@
@@ -68,7 +76,10 @@
/>
(value[field.name] = e.target.value)}
+ on:change={e => {
+ value[field.name] = e.detail
+ dispatch("change", value)
+ }}
options={typeOptions}
/>
.root {
- position: relative;
max-width: 100%;
- overflow-x: auto;
/* so we can show the "+" button beside the "fields" label*/
top: -26px;
}
@@ -103,7 +112,6 @@
/*grid-template-rows: auto auto;
grid-template-columns: auto;*/
position: relative;
- overflow: hidden;
}
.field :global(select) {
diff --git a/packages/builder/src/components/automation/SetupPanel/SetupPanel.svelte b/packages/builder/src/components/automation/SetupPanel/SetupPanel.svelte
deleted file mode 100644
index 3ba59f36a6..0000000000
--- a/packages/builder/src/components/automation/SetupPanel/SetupPanel.svelte
+++ /dev/null
@@ -1,96 +0,0 @@
-
-
-
- Setup
- setAutomationLive(!automationLive)}
- dataCy="activate-automation"
- text="Live"
- />
-
-{#if $automationStore.selectedBlock}
-
-{:else if automation}
- {automation.name}
-
-{/if}
-
-
-
-
-
-
diff --git a/packages/builder/src/components/automation/SetupPanel/TableSelector.svelte b/packages/builder/src/components/automation/SetupPanel/TableSelector.svelte
index 4f9ac05a06..ceb28a37ca 100644
--- a/packages/builder/src/components/automation/SetupPanel/TableSelector.svelte
+++ b/packages/builder/src/components/automation/SetupPanel/TableSelector.svelte
@@ -1,11 +1,20 @@
table.name}
diff --git a/packages/builder/src/components/automation/Shared/CreateWebhookModal.svelte b/packages/builder/src/components/automation/Shared/CreateWebhookModal.svelte
index 04656c1e2e..ac63ba6a47 100644
--- a/packages/builder/src/components/automation/Shared/CreateWebhookModal.svelte
+++ b/packages/builder/src/components/automation/Shared/CreateWebhookModal.svelte
@@ -18,10 +18,7 @@
onMount(async () => {
if (!automation?.definition?.trigger?.inputs.schemaUrl) {
// save the automation initially
- await automationStore.actions.save({
- instanceId,
- automation,
- })
+ await automationStore.actions.save(automation)
}
interval = setInterval(async () => {
await automationStore.actions.fetch()
diff --git a/packages/builder/src/components/backend/DataTable/modals/ExportModal.svelte b/packages/builder/src/components/backend/DataTable/modals/ExportModal.svelte
index 0b63c0bd4d..740027a8fd 100644
--- a/packages/builder/src/components/backend/DataTable/modals/ExportModal.svelte
+++ b/packages/builder/src/components/backend/DataTable/modals/ExportModal.svelte
@@ -18,12 +18,10 @@
let exportFormat = FORMATS[0].key
async function exportView() {
- const filename = `export.${exportFormat}`
download(
`/api/views/export?view=${encodeURIComponent(
view
- )}&format=${exportFormat}`,
- filename
+ )}&format=${exportFormat}`
)
}
diff --git a/packages/builder/src/components/backend/DatasourceNavigator/DatasourceNavigator.svelte b/packages/builder/src/components/backend/DatasourceNavigator/DatasourceNavigator.svelte
index 42ea30dbb0..84c737eb67 100644
--- a/packages/builder/src/components/backend/DatasourceNavigator/DatasourceNavigator.svelte
+++ b/packages/builder/src/components/backend/DatasourceNavigator/DatasourceNavigator.svelte
@@ -9,7 +9,10 @@
import TableNavigator from "components/backend/TableNavigator/TableNavigator.svelte"
import ICONS from "./icons"
+ let openDataSources = []
+
function selectDatasource(datasource) {
+ toggleNode(datasource)
datasources.select(datasource._id)
$goto(`./datasource/${datasource._id}`)
}
@@ -19,6 +22,15 @@
$goto(`./datasource/${query.datasourceId}/${query._id}`)
}
+ function toggleNode(datasource) {
+ const isOpen = openDataSources.includes(datasource._id)
+ if (isOpen) {
+ openDataSources = openDataSources.filter(id => datasource._id !== id)
+ } else {
+ openDataSources = [...openDataSources, datasource._id]
+ }
+ }
+
onMount(() => {
datasources.fetch()
queries.fetch()
@@ -31,8 +43,11 @@
0}
text={datasource.name}
+ opened={openDataSources.includes(datasource._id)}
selected={$datasources.selected === datasource._id}
+ withArrow={true}
on:click={() => selectDatasource(datasource)}
+ on:iconClick={() => toggleNode(datasource)}
>
-
+ {#if openDataSources.includes(datasource._id)}
+
+ {/if}
{#each $queries.list.filter(query => query.datasourceId === datasource._id) as query}
onClickQuery(query)}
>
diff --git a/packages/builder/src/components/common/ConfigChecklist.svelte b/packages/builder/src/components/common/ConfigChecklist.svelte
index 83e6a41063..05f3f7c719 100644
--- a/packages/builder/src/components/common/ConfigChecklist.svelte
+++ b/packages/builder/src/components/common/ConfigChecklist.svelte
@@ -7,16 +7,29 @@
ProgressCircle,
} from "@budibase/bbui"
import { admin } from "stores/portal"
+ import { goto } from "@roxi/routify"
+ import { onMount } from "svelte"
- const MESSAGES = {
- apps: "Create your first app",
- smtp: "Set up email",
- adminUser: "Create your first user",
- sso: "Set up single sign-on",
- }
+ let width = window.innerWidth
+ $: side = width < 500 ? "right" : "left"
+
+ const resizeObserver = new ResizeObserver(entries => {
+ if (entries?.[0]) {
+ width = entries[0].contentRect?.width
+ }
+ })
+
+ onMount(() => {
+ const doc = document.documentElement
+ resizeObserver.observe(doc)
+
+ return () => {
+ resizeObserver.unobserve(doc)
+ }
+ })
-
+
@@ -28,9 +41,12 @@
{#each Object.keys($admin.checklist) as checklistItem, idx}