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