diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index eb878d2b54..2cfe328312 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -4,8 +4,6 @@ on: # Trigger the workflow on push with tags, # but only for the master branch push: - branches: - - master tags: - 'v*' diff --git a/lerna.json b/lerna.json index 5d5fb16e81..be61b1ef96 100644 --- a/lerna.json +++ b/lerna.json @@ -1,5 +1,5 @@ { - "version": "0.1.19", + "version": "0.1.21", "npmClient": "yarn", "packages": [ "packages/*" diff --git a/packages/builder/cypress/integration/createWorkflow.spec.js b/packages/builder/cypress/integration/createWorkflow.spec.js index e7fe34b630..833777f106 100644 --- a/packages/builder/cypress/integration/createWorkflow.spec.js +++ b/packages/builder/cypress/integration/createWorkflow.spec.js @@ -1,46 +1,52 @@ -context('Create a workflow', () => { +context("Create a workflow", () => { + before(() => { + cy.server() + cy.visit("localhost:4001/_builder") - before(() => { - cy.server() - cy.visit('localhost:4001/_builder') + cy.createApp( + "Workflow Test App", + "This app is used to test that workflows do in fact work!" + ) + }) - cy.createApp('Workflow Test App', 'This app is used to test that workflows do in fact work!') - }) + // https://on.cypress.io/interacting-with-elements + it("should create a workflow", () => { + cy.createTestTableWithData() - // https://on.cypress.io/interacting-with-elements - it('should create a workflow', () => { - cy.createTestTableWithData() + cy.contains("workflow").click() + cy.contains("Create New Workflow").click() + cy.get("input").type("Add Record") + cy.contains("Save").click() - cy.contains('workflow').click() - cy.contains('Create New Workflow').click() - cy.get('input').type('Add Record') - cy.contains('Save').click() + // Add trigger + cy.get("[data-cy=add-workflow-component]").click() + cy.get("[data-cy=RECORD_SAVED]").click() + cy.get(".budibase__input").select("dog") - // Add trigger - cy.get('[data-cy=add-workflow-component]').click() - cy.get('[data-cy=RECORD_SAVED]').click() - cy.get('.budibase__input').select('dog') + // Create action + cy.get("[data-cy=SAVE_RECORD]").click() + cy.get(".budibase__input").select("dog") + cy.get(".container input") + .first() + .type("goodboy") + cy.get(".container input") + .eq(1) + .type("11") - // Create action - cy.get('[data-cy=SAVE_RECORD]').click() - cy.get('.container input').first().type('goodboy') - cy.get('.container input').eq(1).type('11') + // Save + cy.contains("Save Workflow").click() - // Save - cy.contains('Save Workflow').click() + // Activate Workflow + cy.get("[data-cy=activate-workflow]").click() + cy.contains("Add Record").should("be.visible") + cy.get(".stop-button.highlighted").should("be.visible") + }) - // Activate Workflow - cy.get('[data-cy=activate-workflow]').click() - cy.contains("Add Record").should("be.visible") - cy.get(".stop-button.highlighted").should("be.visible") - }) + it("should add record when a new record is added", () => { + cy.contains("backend").click() - it('should add record when a new record is added', () => { - cy.contains('backend').click() - - cy.addRecord(["Rover", 15]) - cy.reload() - cy.contains('goodboy').should('have.text', 'goodboy') - - }) -}) \ No newline at end of file + cy.addRecord(["Rover", 15]) + cy.reload() + cy.contains("goodboy").should("have.text", "goodboy") + }) +}) diff --git a/packages/builder/cypress/videos/createWorkflow.spec.js.mp4 b/packages/builder/cypress/videos/createWorkflow.spec.js.mp4 index 52116ef9e9..22fd8541c7 100644 Binary files a/packages/builder/cypress/videos/createWorkflow.spec.js.mp4 and b/packages/builder/cypress/videos/createWorkflow.spec.js.mp4 differ diff --git a/packages/builder/package.json b/packages/builder/package.json index 740f6f7d61..524025ba5b 100644 --- a/packages/builder/package.json +++ b/packages/builder/package.json @@ -1,6 +1,6 @@ { "name": "@budibase/builder", - "version": "0.1.19", + "version": "0.1.21", "license": "AGPL-3.0", "private": true, "scripts": { @@ -63,8 +63,8 @@ } }, "dependencies": { - "@budibase/bbui": "^1.32.0", - "@budibase/client": "^0.1.19", + "@budibase/bbui": "^1.33.0", + "@budibase/client": "^0.1.21", "@budibase/colorpicker": "^1.0.1", "@fortawesome/fontawesome-free": "^5.14.0", "@sentry/browser": "5.19.1", diff --git a/packages/builder/rollup.config.js b/packages/builder/rollup.config.js index 3d73d968f3..ffeacf9e52 100644 --- a/packages/builder/rollup.config.js +++ b/packages/builder/rollup.config.js @@ -234,7 +234,7 @@ export default { // Watch the `dist` directory and refresh the // browser on changes when not in production - !production && livereload(outputpath), + !production && livereload({ watch: outputpath, delay: 500 }), // If we're building for production (npm run build // instead of npm run dev), minify diff --git a/packages/builder/src/builderStore/store/workflow/Workflow.js b/packages/builder/src/builderStore/store/workflow/Workflow.js index d9c1ee249f..8ac6664e91 100644 --- a/packages/builder/src/builderStore/store/workflow/Workflow.js +++ b/packages/builder/src/builderStore/store/workflow/Workflow.js @@ -1,5 +1,3 @@ -import mustache from "mustache" -import blockDefinitions from "components/workflow/WorkflowPanel/blockDefinitions" import { generate } from "shortid" /** @@ -18,27 +16,31 @@ export default class Workflow { 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 + const trigger = { id: generate(), ...block } + this.workflow.definition.trigger = trigger + return trigger } - this.workflow.definition.steps.push({ - id: generate(), - ...block, - }) + const newBlock = { id: generate(), ...block } + this.workflow.definition.steps = [ + ...this.workflow.definition.steps, + newBlock, + ] + return newBlock } updateBlock(updatedBlock, id) { const { steps, trigger } = this.workflow.definition if (trigger && trigger.id === id) { - this.workflow.definition.trigger = null + this.workflow.definition.trigger = updatedBlock return } const stepIdx = steps.findIndex(step => step.id === id) if (stepIdx < 0) throw new Error("Block not found.") steps.splice(stepIdx, 1, updatedBlock) + this.workflow.definition.steps = steps } deleteBlock(id) { @@ -52,44 +54,6 @@ export default class Workflow { 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, - } - }) + this.workflow.definition.steps = steps } } diff --git a/packages/builder/src/builderStore/store/workflow/index.js b/packages/builder/src/builderStore/store/workflow/index.js index 9d50f85661..6a4db7afaf 100644 --- a/packages/builder/src/builderStore/store/workflow/index.js +++ b/packages/builder/src/builderStore/store/workflow/index.js @@ -1,14 +1,22 @@ import { writable } from "svelte/store" import api from "../../api" import Workflow from "./Workflow" +import { cloneDeep } from "lodash/fp" const workflowActions = store => ({ fetch: async () => { - const WORKFLOWS_URL = `/api/workflows` - const workflowResponse = await api.get(WORKFLOWS_URL) - const json = await workflowResponse.json() + const responses = await Promise.all([ + api.get(`/api/workflows`), + api.get(`/api/workflows/definitions/list`), + ]) + const jsonResponses = await Promise.all(responses.map(x => x.json())) store.update(state => { - state.workflows = json + state.workflows = jsonResponses[0] + state.blockDefinitions = { + TRIGGER: jsonResponses[1].trigger, + ACTION: jsonResponses[1].action, + LOGIC: jsonResponses[1].logic, + } return state }) }, @@ -23,8 +31,8 @@ const workflowActions = store => ({ 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) + state.workflows = [...state.workflows, json.workflow] + store.actions.select(json.workflow) return state }) }, @@ -38,20 +46,7 @@ const workflowActions = store => ({ ) state.workflows.splice(existingIdx, 1, json.workflow) state.workflows = state.workflows - state.currentWorkflow = new Workflow(json.workflow) - return state - }) - }, - update: async ({ workflow }) => { - const UPDATE_WORKFLOW_URL = `/api/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 + store.actions.select(json.workflow) return state }) }, @@ -66,28 +61,49 @@ const workflowActions = store => ({ ) state.workflows.splice(existingIdx, 1) state.workflows = state.workflows - state.currentWorkflow = null + state.selectedWorkflow = null + state.selectedBlock = null return state }) }, + trigger: async ({ workflow }) => { + const { _id } = workflow + const TRIGGER_WORKFLOW_URL = `/api/workflows/${_id}/trigger` + return await api.post(TRIGGER_WORKFLOW_URL) + }, select: workflow => { store.update(state => { - state.currentWorkflow = new Workflow(workflow) - state.selectedWorkflowBlock = null + state.selectedWorkflow = new Workflow(cloneDeep(workflow)) + state.selectedBlock = null return state }) }, addBlockToWorkflow: block => { store.update(state => { - state.currentWorkflow.addBlock(block) - state.selectedWorkflowBlock = block + const newBlock = state.selectedWorkflow.addBlock(cloneDeep(block)) + state.selectedBlock = newBlock return state }) }, deleteWorkflowBlock: block => { store.update(state => { - state.currentWorkflow.deleteBlock(block.id) - state.selectedWorkflowBlock = null + const idx = state.selectedWorkflow.workflow.definition.steps.findIndex( + x => x.id === block.id + ) + state.selectedWorkflow.deleteBlock(block.id) + + // Select next closest step + const steps = state.selectedWorkflow.workflow.definition.steps + let nextSelectedBlock + if (steps[idx] != null) { + nextSelectedBlock = steps[idx] + } else if (steps[idx - 1] != null) { + nextSelectedBlock = steps[idx - 1] + } else { + nextSelectedBlock = + state.selectedWorkflow.workflow.definition.trigger || null + } + state.selectedBlock = nextSelectedBlock return state }) }, @@ -96,11 +112,14 @@ const workflowActions = store => ({ export const getWorkflowStore = () => { const INITIAL_WORKFLOW_STATE = { workflows: [], + blockDefinitions: { + TRIGGER: [], + ACTION: [], + LOGIC: [], + }, + selectedWorkflow: null, } - 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 index fd14404a5f..4d270ea611 100644 --- a/packages/builder/src/builderStore/store/workflow/tests/Workflow.spec.js +++ b/packages/builder/src/builderStore/store/workflow/tests/Workflow.spec.js @@ -1,57 +1,48 @@ -import Workflow from "../Workflow"; -import TEST_WORKFLOW from "./testWorkflow"; +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", + id: "AUXJQGZY7", + name: "Delay", + icon: "ri-time-fill", + tagline: "Delay for {{time}} milliseconds", + description: "Delay the workflow until an amount of time has passed.", + params: { time: "number" }, + type: "LOGIC", + args: { time: "5000" }, + stepId: "DELAY", } describe("Workflow Data Object", () => { let workflow beforeEach(() => { - workflow = new Workflow({ ...TEST_WORKFLOW }); - }); + workflow = new Workflow({ ...TEST_WORKFLOW }) + }) it("adds a workflow block to the workflow", () => { - workflow.addBlock(TEST_BLOCK); + workflow.addBlock(TEST_BLOCK) expect(workflow.workflow.definition) }) it("updates a workflow block with new attributes", () => { - const firstBlock = workflow.workflow.definition.steps[0]; + const firstBlock = workflow.workflow.definition.steps[0] const updatedBlock = { ...firstBlock, - name: "UPDATED" - }; - workflow.updateBlock(updatedBlock, firstBlock.id); + 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 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(); + const lastBlock = steps[steps.length - 1] + workflow.deleteBlock(lastBlock.id) + expect(workflow.workflow.definition.steps.length).toBeLessThan( + originalLength + ) }) }) 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 deleted file mode 100644 index 732764a082..0000000000 --- a/packages/builder/src/builderStore/store/workflow/tests/__snapshots__/Workflow.spec.js.snap +++ /dev/null @@ -1,49 +0,0 @@ -// 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 index 90c4b17924..96999277eb 100644 --- a/packages/builder/src/builderStore/store/workflow/tests/testWorkflow.js +++ b/packages/builder/src/builderStore/store/workflow/tests/testWorkflow.js @@ -1,63 +1,78 @@ export default { - _id: "53b6148c65d1429c987e046852d11611", - _rev: "4-02c6659734934895812fa7be0215ee59", - name: "Test Workflow", + 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", + id: "ANBDINAPS", + description: "Send an email.", + tagline: "Send email to {{to}}", + icon: "ri-mail-open-fill", + name: "Send Email", params: { - path: "string", - value: "longText", + to: "string", + from: "string", + subject: "longText", + text: "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, + text: "A user was created!", + subject: "New Budibase User", + from: "budimaster@budibase.com", + to: "test@test.com", }, - 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", + stepId: "SEND_EMAIL", }, ], + trigger: { + id: "iRzYMOqND", + name: "Record Saved", + event: "record:save", + icon: "ri-save-line", + tagline: "Record is added to {{model.name}}", + description: "Fired when a record is saved to your database.", + params: { model: "model" }, + type: "TRIGGER", + args: { + model: { + type: "model", + views: {}, + name: "users", + schema: { + name: { + type: "string", + constraints: { + type: "string", + length: { maximum: 123 }, + presence: { allowEmpty: false }, + }, + name: "name", + }, + age: { + type: "number", + constraints: { + type: "number", + presence: { allowEmpty: false }, + numericality: { + greaterThanOrEqualTo: "", + lessThanOrEqualTo: "", + }, + }, + name: "age", + }, + }, + _id: "c6b4e610cd984b588837bca27188a451", + _rev: "7-b8aa1ce0b53e88928bb88fc11bdc0aff", + }, + }, + stepId: "RECORD_SAVED", + }, }, type: "workflow", - live: true, + ok: true, + id: "b384f861f4754e1693835324a7fcca62", + rev: "1-aa1c2cbd868ef02e26f8fad531dd7e37", + live: false, + _id: "b384f861f4754e1693835324a7fcca62", + _rev: "108-4116829ec375e0481d0ecab9e83a2caf", } diff --git a/packages/builder/src/components/common/DatePicker.svelte b/packages/builder/src/components/common/DatePicker.svelte index ded25b5d87..ec9d41a17b 100644 --- a/packages/builder/src/components/common/DatePicker.svelte +++ b/packages/builder/src/components/common/DatePicker.svelte @@ -12,6 +12,5 @@
-
diff --git a/packages/builder/src/components/database/DataTable/ModelDataTable.svelte b/packages/builder/src/components/database/DataTable/ModelDataTable.svelte index af99b2bada..0804b5e8da 100644 --- a/packages/builder/src/components/database/DataTable/ModelDataTable.svelte +++ b/packages/builder/src/components/database/DataTable/ModelDataTable.svelte @@ -36,14 +36,14 @@ } } - $: paginatedData = data - ? data.slice( + $: sort = $backendUiStore.sort + $: sorted = sort ? fsort(data)[sort.direction](sort.column) : data + $: paginatedData = sorted + ? sorted.slice( currentPage * ITEMS_PER_PAGE, currentPage * ITEMS_PER_PAGE + ITEMS_PER_PAGE ) : [] - $: sort = $backendUiStore.sort - $: sorted = sort ? fsort(data)[sort.direction](sort.column) : data $: headers = Object.keys($backendUiStore.selectedModel.schema) .sort() @@ -78,10 +78,10 @@ - {#if sorted.length === 0} + {#if paginatedData.length === 0}
No Data.
{/if} - {#each sorted as row} + {#each paginatedData as row} @@ -100,7 +100,7 @@ diff --git a/packages/builder/src/components/database/DataTable/Table.svelte b/packages/builder/src/components/database/DataTable/Table.svelte index 47be4eab37..d3fdc63046 100644 --- a/packages/builder/src/components/database/DataTable/Table.svelte +++ b/packages/builder/src/components/database/DataTable/Table.svelte @@ -26,15 +26,15 @@ $: columns = schema ? Object.keys(schema) : [] + $: sort = $backendUiStore.sort + $: sorted = sort ? fsort(data)[sort.direction](sort.column) : data $: paginatedData = - data && data.length - ? data.slice( + sorted && sorted.length + ? sorted.slice( currentPage * ITEMS_PER_PAGE, currentPage * ITEMS_PER_PAGE + ITEMS_PER_PAGE ) : [] - $: sort = $backendUiStore.sort - $: sorted = sort ? fsort(data)[sort.direction](sort.column) : data
@@ -68,7 +68,7 @@
diff --git a/packages/builder/src/components/database/DataTable/popovers/Filter.svelte b/packages/builder/src/components/database/DataTable/popovers/Filter.svelte index 1cf8e9acab..be40a66291 100644 --- a/packages/builder/src/components/database/DataTable/popovers/Filter.svelte +++ b/packages/builder/src/components/database/DataTable/popovers/Filter.svelte @@ -71,7 +71,16 @@ } function addFilter() { - view.filters = [...view.filters, {}] + view.filters.push({}) + view.filters = view.filters + } + + function isMultipleChoice(field) { + return ( + viewModel.schema[field].constraints && + viewModel.schema[field].constraints.inclusion && + viewModel.schema[field].constraints.inclusion.length + ) } @@ -108,10 +117,18 @@ {/each} - + {#if filter.key && isMultipleChoice(filter.key)} + + {:else} + + {/if} removeFilter(idx)} /> {/each} diff --git a/packages/builder/src/components/userInterface/EventsEditor/EventPropertyControl.svelte b/packages/builder/src/components/userInterface/EventsEditor/EventPropertyControl.svelte index c27d1aaa3d..52986b0efd 100644 --- a/packages/builder/src/components/userInterface/EventsEditor/EventPropertyControl.svelte +++ b/packages/builder/src/components/userInterface/EventsEditor/EventPropertyControl.svelte @@ -12,7 +12,7 @@ - + - import { Input, Select } from "@budibase/bbui" + import { Input, DataList, Select } from "@budibase/bbui" import { find, map, keys, reduce, keyBy } from "lodash/fp" import { pipe } from "components/common/core" import { EVENT_TYPE_MEMBER_NAME } from "components/common/eventHandlers" @@ -29,12 +29,12 @@ {/each} {:else if parameter.name === 'url'} - + {:else} - import { Select, Label } from "@budibase/bbui" + import { DataList, Label } from "@budibase/bbui" import { store } from "builderStore" export let parameters @@ -7,12 +7,12 @@
- +
diff --git a/packages/builder/src/components/workflow/SetupPanel/WorkflowBlockSetup.svelte b/packages/builder/src/components/workflow/SetupPanel/WorkflowBlockSetup.svelte index 35ab01b544..5e889a32d0 100644 --- a/packages/builder/src/components/workflow/SetupPanel/WorkflowBlockSetup.svelte +++ b/packages/builder/src/components/workflow/SetupPanel/WorkflowBlockSetup.svelte @@ -1,55 +1,45 @@ - -{#each workflowParams as [parameter, type]} -
- - {#if Array.isArray(type)} - - {:else if type === 'component'} - - {:else if type === 'accessLevel'} - - {:else if type === 'password'} - - {:else if type === 'number'} - - {:else if type === 'longText'} -