diff --git a/packages/builder/package.json b/packages/builder/package.json
index 8e426deb45..65072a9f0f 100644
--- a/packages/builder/package.json
+++ b/packages/builder/package.json
@@ -38,16 +38,19 @@
]
},
"dependencies": {
+ "@beyonk/svelte-notifications": "^2.0.3",
"@budibase/bbui": "^0.3.5",
"@budibase/client": "^0.0.32",
"@nx-js/compiler-util": "^2.0.0",
"codemirror": "^5.51.0",
"date-fns": "^1.29.0",
+ "deepmerge": "^4.2.2",
"feather-icons": "^4.21.0",
"flatpickr": "^4.5.7",
"lodash": "^4.17.13",
"logrocket": "^1.0.6",
"lunr": "^2.3.5",
+ "mustache": "^4.0.1",
"safe-buffer": "^5.1.2",
"shortid": "^2.2.8",
"string_decoder": "^1.2.0",
diff --git a/packages/builder/src/App.svelte b/packages/builder/src/App.svelte
index 000c5c0e16..b19b8df5c7 100644
--- a/packages/builder/src/App.svelte
+++ b/packages/builder/src/App.svelte
@@ -7,6 +7,7 @@
import AppNotification, {
showAppNotification,
} from "components/common/AppNotification.svelte"
+ import { NotificationDisplay } from "@beyonk/svelte-notifications"
function showErrorBanner() {
showAppNotification({
@@ -26,4 +27,7 @@
+
+
+
diff --git a/packages/builder/src/budibase.css b/packages/builder/src/budibase.css
index 0d74ed7221..283a761c3b 100644
--- a/packages/builder/src/budibase.css
+++ b/packages/builder/src/budibase.css
@@ -77,7 +77,8 @@
}
.budibase__input {
- width: 250px;
+ width: 100%;
+ max-width: 250px;
height: 35px;
border-radius: 3px;
border: 1px solid #DBDBDB;
diff --git a/packages/builder/src/builderStore/api.js b/packages/builder/src/builderStore/api.js
index 7440fd7031..04dcce6cb9 100644
--- a/packages/builder/src/builderStore/api.js
+++ b/packages/builder/src/builderStore/api.js
@@ -18,10 +18,12 @@ const post = apiCall("POST")
const get = apiCall("GET")
const patch = apiCall("PATCH")
const del = apiCall("DELETE")
+const put = apiCall("PUT")
export default {
post,
get,
patch,
delete: del,
+ put,
}
diff --git a/packages/builder/src/builderStore/index.js b/packages/builder/src/builderStore/index.js
index 8ba017a7c8..2af3a66667 100644
--- a/packages/builder/src/builderStore/index.js
+++ b/packages/builder/src/builderStore/index.js
@@ -1,9 +1,11 @@
import { getStore } from "./store"
import { getBackendUiStore } from "./store/backend"
+import { getWorkflowStore } from "./store/workflow/"
import LogRocket from "logrocket"
export const store = getStore()
export const backendUiStore = getBackendUiStore()
+export const workflowStore = getWorkflowStore()
export const initialise = async () => {
try {
diff --git a/packages/builder/src/builderStore/store/index.js b/packages/builder/src/builderStore/store/index.js
index f94cea2399..87694d846f 100644
--- a/packages/builder/src/builderStore/store/index.js
+++ b/packages/builder/src/builderStore/store/index.js
@@ -156,7 +156,6 @@ const createScreen = store => (screenName, route, layoutComponentName) => {
description: "",
url: "",
_css: "",
- uiFunctions: "",
props: createProps(rootComponent).props,
}
diff --git a/packages/builder/src/builderStore/store/workflow/Workflow.js b/packages/builder/src/builderStore/store/workflow/Workflow.js
new file mode 100644
index 0000000000..d9c1ee249f
--- /dev/null
+++ b/packages/builder/src/builderStore/store/workflow/Workflow.js
@@ -0,0 +1,95 @@
+import mustache from "mustache"
+import blockDefinitions from "components/workflow/WorkflowPanel/blockDefinitions"
+import { generate } from "shortid"
+
+/**
+ * Class responsible for the traversing of the workflow definition.
+ * Workflow definitions are stored in linked lists.
+ */
+export default class Workflow {
+ constructor(workflow) {
+ this.workflow = workflow
+ }
+
+ hasTrigger() {
+ return this.workflow.definition.trigger
+ }
+
+ addBlock(block) {
+ // Make sure to add trigger if doesn't exist
+ if (!this.hasTrigger() && block.type === "TRIGGER") {
+ this.workflow.definition.trigger = { id: generate(), ...block }
+ return
+ }
+
+ this.workflow.definition.steps.push({
+ id: generate(),
+ ...block,
+ })
+ }
+
+ updateBlock(updatedBlock, id) {
+ const { steps, trigger } = this.workflow.definition
+
+ if (trigger && trigger.id === id) {
+ this.workflow.definition.trigger = null
+ return
+ }
+
+ const stepIdx = steps.findIndex(step => step.id === id)
+ if (stepIdx < 0) throw new Error("Block not found.")
+ steps.splice(stepIdx, 1, updatedBlock)
+ }
+
+ deleteBlock(id) {
+ const { steps, trigger } = this.workflow.definition
+
+ if (trigger && trigger.id === id) {
+ this.workflow.definition.trigger = null
+ return
+ }
+
+ const stepIdx = steps.findIndex(step => step.id === id)
+ if (stepIdx < 0) throw new Error("Block not found.")
+ steps.splice(stepIdx, 1)
+ }
+
+ createUiTree() {
+ if (!this.workflow.definition) return []
+ return Workflow.buildUiTree(this.workflow.definition)
+ }
+
+ static buildUiTree(definition) {
+ const steps = []
+ if (definition.trigger) steps.push(definition.trigger)
+
+ return [...steps, ...definition.steps].map(step => {
+ // The client side display definition for the block
+ const definition = blockDefinitions[step.type][step.actionId]
+ if (!definition) {
+ throw new Error(
+ `No block definition exists for the chosen block. Check there's an entry in the block definitions for ${step.actionId}`
+ )
+ }
+
+ if (!definition.params) {
+ throw new Error(
+ `Blocks should always have parameters. Ensure that the block definition is correct for ${step.actionId}`
+ )
+ }
+
+ const tagline = definition.tagline || ""
+ const args = step.args || {}
+
+ return {
+ id: step.id,
+ type: step.type,
+ params: step.params,
+ args,
+ heading: step.actionId,
+ body: mustache.render(tagline, args),
+ name: definition.name,
+ }
+ })
+ }
+}
diff --git a/packages/builder/src/builderStore/store/workflow/index.js b/packages/builder/src/builderStore/store/workflow/index.js
new file mode 100644
index 0000000000..8d5e63b197
--- /dev/null
+++ b/packages/builder/src/builderStore/store/workflow/index.js
@@ -0,0 +1,106 @@
+import { writable } from "svelte/store"
+import api from "../../api"
+import Workflow from "./Workflow"
+
+const workflowActions = store => ({
+ fetch: async instanceId => {
+ const WORKFLOWS_URL = `/api/${instanceId}/workflows`
+ const workflowResponse = await api.get(WORKFLOWS_URL)
+ const json = await workflowResponse.json()
+ store.update(state => {
+ state.workflows = json
+ return state
+ })
+ },
+ create: async ({ instanceId, name }) => {
+ const workflow = {
+ name,
+ definition: {
+ steps: [],
+ },
+ }
+ const CREATE_WORKFLOW_URL = `/api/${instanceId}/workflows`
+ const response = await api.post(CREATE_WORKFLOW_URL, workflow)
+ const json = await response.json()
+ store.update(state => {
+ state.workflows = state.workflows.concat(json.workflow)
+ state.currentWorkflow = new Workflow(json.workflow)
+ return state
+ })
+ },
+ save: async ({ instanceId, workflow }) => {
+ const UPDATE_WORKFLOW_URL = `/api/${instanceId}/workflows`
+ const response = await api.put(UPDATE_WORKFLOW_URL, workflow)
+ const json = await response.json()
+ store.update(state => {
+ const existingIdx = state.workflows.findIndex(
+ existing => existing._id === workflow._id
+ )
+ state.workflows.splice(existingIdx, 1, json.workflow)
+ state.workflows = state.workflows
+ state.currentWorkflow = new Workflow(json.workflow)
+ return state
+ })
+ },
+ update: async ({ instanceId, workflow }) => {
+ const UPDATE_WORKFLOW_URL = `/api/${instanceId}/workflows`
+ const response = await api.put(UPDATE_WORKFLOW_URL, workflow)
+ const json = await response.json()
+ store.update(state => {
+ const existingIdx = state.workflows.findIndex(
+ existing => existing._id === workflow._id
+ )
+ state.workflows.splice(existingIdx, 1, json.workflow)
+ state.workflows = state.workflows
+ return state
+ })
+ },
+ delete: async ({ instanceId, workflow }) => {
+ const { _id, _rev } = workflow
+ const DELETE_WORKFLOW_URL = `/api/${instanceId}/workflows/${_id}/${_rev}`
+ await api.delete(DELETE_WORKFLOW_URL)
+
+ store.update(state => {
+ const existingIdx = state.workflows.findIndex(
+ existing => existing._id === _id
+ )
+ state.workflows.splice(existingIdx, 1)
+ state.workflows = state.workflows
+ state.currentWorkflow = null
+ return state
+ })
+ },
+ select: workflow => {
+ store.update(state => {
+ state.currentWorkflow = new Workflow(workflow)
+ state.selectedWorkflowBlock = null
+ return state
+ })
+ },
+ addBlockToWorkflow: block => {
+ store.update(state => {
+ state.currentWorkflow.addBlock(block)
+ state.selectedWorkflowBlock = block
+ return state
+ })
+ },
+ deleteWorkflowBlock: block => {
+ store.update(state => {
+ state.currentWorkflow.deleteBlock(block.id)
+ state.selectedWorkflowBlock = null
+ return state
+ })
+ },
+})
+
+export const getWorkflowStore = () => {
+ const INITIAL_WORKFLOW_STATE = {
+ workflows: [],
+ }
+
+ const store = writable(INITIAL_WORKFLOW_STATE)
+
+ store.actions = workflowActions(store)
+
+ return store
+}
diff --git a/packages/builder/src/builderStore/store/workflow/tests/Workflow.spec.js b/packages/builder/src/builderStore/store/workflow/tests/Workflow.spec.js
new file mode 100644
index 0000000000..fd14404a5f
--- /dev/null
+++ b/packages/builder/src/builderStore/store/workflow/tests/Workflow.spec.js
@@ -0,0 +1,57 @@
+import Workflow from "../Workflow";
+import TEST_WORKFLOW from "./testWorkflow";
+
+const TEST_BLOCK = {
+ id: "VFWeZcIPx",
+ name: "Update UI State",
+ tagline: "Update {{path}} to {{value}}",
+ icon: "ri-refresh-line",
+ description: "Update your User Interface with some data.",
+ environment: "CLIENT",
+ params: {
+ path: "string",
+ value: "longText",
+ },
+ args: {
+ path: "foo",
+ value: "started...",
+ },
+ actionId: "SET_STATE",
+ type: "ACTION",
+}
+
+describe("Workflow Data Object", () => {
+ let workflow
+
+ beforeEach(() => {
+ workflow = new Workflow({ ...TEST_WORKFLOW });
+ });
+
+ it("adds a workflow block to the workflow", () => {
+ workflow.addBlock(TEST_BLOCK);
+ expect(workflow.workflow.definition)
+ })
+
+ it("updates a workflow block with new attributes", () => {
+ const firstBlock = workflow.workflow.definition.steps[0];
+ const updatedBlock = {
+ ...firstBlock,
+ name: "UPDATED"
+ };
+ workflow.updateBlock(updatedBlock, firstBlock.id);
+ expect(workflow.workflow.definition.steps[0]).toEqual(updatedBlock)
+ })
+
+ it("deletes a workflow block successfully", () => {
+ const { steps } = workflow.workflow.definition
+ const originalLength = steps.length
+
+ const lastBlock = steps[steps.length - 1];
+ workflow.deleteBlock(lastBlock.id);
+ expect(workflow.workflow.definition.steps.length).toBeLessThan(originalLength);
+ })
+
+ it("builds a tree that gets rendered in the flowchart builder", () => {
+ expect(Workflow.buildUiTree(TEST_WORKFLOW.definition)).toMatchSnapshot();
+ })
+})
diff --git a/packages/builder/src/builderStore/store/workflow/tests/__snapshots__/Workflow.spec.js.snap b/packages/builder/src/builderStore/store/workflow/tests/__snapshots__/Workflow.spec.js.snap
new file mode 100644
index 0000000000..732764a082
--- /dev/null
+++ b/packages/builder/src/builderStore/store/workflow/tests/__snapshots__/Workflow.spec.js.snap
@@ -0,0 +1,49 @@
+// Jest Snapshot v1, https://goo.gl/fbAQLP
+
+exports[`Workflow Data Object builds a tree that gets rendered in the flowchart builder 1`] = `
+Array [
+ Object {
+ "args": Object {
+ "time": 3000,
+ },
+ "body": "Delay for 3000 milliseconds",
+ "heading": "DELAY",
+ "id": "zJQcZUgDS",
+ "name": "Delay",
+ "params": Object {
+ "time": "number",
+ },
+ "type": "LOGIC",
+ },
+ Object {
+ "args": Object {
+ "path": "foo",
+ "value": "finished",
+ },
+ "body": "Update foo to finished",
+ "heading": "SET_STATE",
+ "id": "3RSTO7BMB",
+ "name": "Update UI State",
+ "params": Object {
+ "path": "string",
+ "value": "longText",
+ },
+ "type": "ACTION",
+ },
+ Object {
+ "args": Object {
+ "path": "foo",
+ "value": "started...",
+ },
+ "body": "Update foo to started...",
+ "heading": "SET_STATE",
+ "id": "VFWeZcIPx",
+ "name": "Update UI State",
+ "params": Object {
+ "path": "string",
+ "value": "longText",
+ },
+ "type": "ACTION",
+ },
+]
+`;
diff --git a/packages/builder/src/builderStore/store/workflow/tests/testWorkflow.js b/packages/builder/src/builderStore/store/workflow/tests/testWorkflow.js
new file mode 100644
index 0000000000..90c4b17924
--- /dev/null
+++ b/packages/builder/src/builderStore/store/workflow/tests/testWorkflow.js
@@ -0,0 +1,63 @@
+export default {
+ _id: "53b6148c65d1429c987e046852d11611",
+ _rev: "4-02c6659734934895812fa7be0215ee59",
+ name: "Test Workflow",
+ definition: {
+ steps: [
+ {
+ id: "VFWeZcIPx",
+ name: "Update UI State",
+ tagline: "Update {{path}} to {{value}}",
+ icon: "ri-refresh-line",
+ description: "Update your User Interface with some data.",
+ environment: "CLIENT",
+ params: {
+ path: "string",
+ value: "longText",
+ },
+ args: {
+ path: "foo",
+ value: "started...",
+ },
+ actionId: "SET_STATE",
+ type: "ACTION",
+ },
+ {
+ id: "zJQcZUgDS",
+ name: "Delay",
+ icon: "ri-time-fill",
+ tagline: "Delay for {{time}} milliseconds",
+ description: "Delay the workflow until an amount of time has passed.",
+ environment: "CLIENT",
+ params: {
+ time: "number",
+ },
+ args: {
+ time: 3000,
+ },
+ actionId: "DELAY",
+ type: "LOGIC",
+ },
+ {
+ id: "3RSTO7BMB",
+ name: "Update UI State",
+ tagline: "Update {{path}} to {{value}}",
+ icon: "ri-refresh-line",
+ description: "Update your User Interface with some data.",
+ environment: "CLIENT",
+ params: {
+ path: "string",
+ value: "longText",
+ },
+ args: {
+ path: "foo",
+ value: "finished",
+ },
+ actionId: "SET_STATE",
+ type: "ACTION",
+ },
+ ],
+ },
+ type: "workflow",
+ live: true,
+}
diff --git a/packages/builder/src/components/common/binding.js b/packages/builder/src/components/common/binding.js
deleted file mode 100644
index af04397327..0000000000
--- a/packages/builder/src/components/common/binding.js
+++ /dev/null
@@ -1,31 +0,0 @@
-import { isString } from "lodash/fp"
-
-import {
- BB_STATE_BINDINGPATH,
- BB_STATE_FALLBACK,
- BB_STATE_BINDINGSOURCE,
- isBound,
- parseBinding,
-} from "@budibase/client/src/state/parseBinding"
-
-export const isBinding = isBound
-
-export const setBinding = ({ path, fallback, source }, binding = {}) => {
- if (isNonEmptyString(path)) binding[BB_STATE_BINDINGPATH] = path
- if (isNonEmptyString(fallback)) binding[BB_STATE_FALLBACK] = fallback
- binding[BB_STATE_BINDINGSOURCE] = source || "store"
- return binding
-}
-
-export const getBinding = val => {
- const binding = parseBinding(val)
- return binding
- ? binding
- : {
- path: "",
- source: "store",
- fallback: "",
- }
-}
-
-const isNonEmptyString = s => isString(s) && s.length > 0
diff --git a/packages/builder/src/components/common/eventHandlers.js b/packages/builder/src/components/common/eventHandlers.js
index 9dac01eb86..2e7a62ac3e 100644
--- a/packages/builder/src/components/common/eventHandlers.js
+++ b/packages/builder/src/components/common/eventHandlers.js
@@ -1,13 +1,8 @@
import { eventHandlers } from "../../../../client/src/state/eventHandlers"
-import { writable } from "svelte/store"
export { EVENT_TYPE_MEMBER_NAME } from "../../../../client/src/state/eventHandlers"
-export const allHandlers = user => {
- const store = writable({
- _bbuser: user,
- })
-
- const handlersObj = eventHandlers(store)
+export const allHandlers = () => {
+ const handlersObj = eventHandlers()
const handlers = Object.keys(handlersObj).map(name => ({
name,
diff --git a/packages/builder/src/components/database/ModelDataTable/ModelDataTable.svelte b/packages/builder/src/components/database/ModelDataTable/ModelDataTable.svelte
index 9e6ddc5042..8a16d5d0b1 100644
--- a/packages/builder/src/components/database/ModelDataTable/ModelDataTable.svelte
+++ b/packages/builder/src/components/database/ModelDataTable/ModelDataTable.svelte
@@ -43,13 +43,6 @@
)
}
- async function selectRecord(record) {
- return await api.loadRecord(record.key, {
- appname: $store.appname,
- instanceId: $backendUiStore.selectedDatabase._id,
- })
- }
-
const ITEMS_PER_PAGE = 10
// Internal headers we want to hide from the user
const INTERNAL_HEADERS = ["_id", "_rev", "modelId", "type"]
diff --git a/packages/builder/src/components/database/ModelDataTable/api.js b/packages/builder/src/components/database/ModelDataTable/api.js
index c723676388..cb98879567 100644
--- a/packages/builder/src/components/database/ModelDataTable/api.js
+++ b/packages/builder/src/components/database/ModelDataTable/api.js
@@ -1,6 +1,6 @@
import api from "builderStore/api"
-export async function createUser(user, appId, instanceId) {
+export async function createUser(user, instanceId) {
const CREATE_USER_URL = `/api/${instanceId}/users`
const response = await api.post(CREATE_USER_URL, user)
return await response.json()
@@ -28,7 +28,7 @@ export async function saveRecord(record, instanceId, modelId) {
}
export async function fetchDataForView(viewName, instanceId) {
- const FETCH_RECORDS_URL = `/api/${instanceId}/${viewName}/records`
+ const FETCH_RECORDS_URL = `/api/${instanceId}/views/${viewName}`
const response = await api.get(FETCH_RECORDS_URL)
return await response.json()
diff --git a/packages/builder/src/components/database/ModelDataTable/modals/CreateEditModel/CreateEditModel.svelte b/packages/builder/src/components/database/ModelDataTable/modals/CreateEditModel/CreateEditModel.svelte
index 97bb799c45..bd302df5b7 100644
--- a/packages/builder/src/components/database/ModelDataTable/modals/CreateEditModel/CreateEditModel.svelte
+++ b/packages/builder/src/components/database/ModelDataTable/modals/CreateEditModel/CreateEditModel.svelte
@@ -35,7 +35,7 @@
}
-
+
{#if !showFieldView}
Create / Edit Model
@@ -43,7 +43,7 @@
Create / Edit Field
{/if}
-
+
{#if !showFieldView}
Settings
diff --git a/packages/builder/src/components/database/ModelDataTable/modals/CreateEditModel/FieldView.svelte b/packages/builder/src/components/database/ModelDataTable/modals/CreateEditModel/FieldView.svelte
index 46f8bd9189..2c3443ae56 100644
--- a/packages/builder/src/components/database/ModelDataTable/modals/CreateEditModel/FieldView.svelte
+++ b/packages/builder/src/components/database/ModelDataTable/modals/CreateEditModel/FieldView.svelte
@@ -64,7 +64,6 @@
{:else if type === 'datetime'}
-
diff --git a/packages/builder/src/components/database/ModelDataTable/modals/CreateUser.svelte b/packages/builder/src/components/database/ModelDataTable/modals/CreateUser.svelte
index 842703af1c..e81659f716 100644
--- a/packages/builder/src/components/database/ModelDataTable/modals/CreateUser.svelte
+++ b/packages/builder/src/components/database/ModelDataTable/modals/CreateUser.svelte
@@ -7,14 +7,15 @@
let username
let password
+ let accessLevelId
- $: valid = username && password
+ $: valid = username && password && accessLevelId
$: instanceId = $backendUiStore.selectedDatabase._id
$: appId = $store.appId
async function createUser() {
- const user = { name: username, username, password }
- const response = await api.createUser(user, appId, instanceId)
+ const user = { name: username, username, password, accessLevelId }
+ const response = await api.createUser(user, instanceId)
backendUiStore.actions.users.create(response)
onClosed()
}
@@ -30,6 +31,14 @@
+
+
+
+