diff --git a/.github/workflows/budibase_ci.yml b/.github/workflows/budibase_ci.yml index f4c17b64ea..e03768a420 100644 --- a/.github/workflows/budibase_ci.yml +++ b/.github/workflows/budibase_ci.yml @@ -16,7 +16,7 @@ jobs: strategy: matrix: - node-version: [10.x] + node-version: [12.x] steps: - uses: actions/checkout@v2 diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 2cfe328312..290f614e20 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -15,7 +15,7 @@ jobs: strategy: matrix: os: [macos-latest, ubuntu-latest, windows-latest] - node-version: [10.x] + node-version: [12.x] steps: - uses: actions/checkout@v2 diff --git a/packages/builder/.gitignore b/packages/builder/.gitignore index 50269a9125..2e8ed59c47 100644 --- a/packages/builder/.gitignore +++ b/packages/builder/.gitignore @@ -4,6 +4,6 @@ node_modules_win package-lock.json release/ dist/ -cypress/screenshots -cypress/videos routify +cypress/videos +cypress/screenshots diff --git a/packages/builder/cypress/integration/createAutomation.spec.js b/packages/builder/cypress/integration/createAutomation.spec.js new file mode 100644 index 0000000000..3acd5a7f7a --- /dev/null +++ b/packages/builder/cypress/integration/createAutomation.spec.js @@ -0,0 +1,59 @@ +context("Create a automation", () => { + before(() => { + cy.server() + cy.visit("localhost:4001/_builder") + + cy.createApp( + "Automation Test App", + "This app is used to test that automations do in fact work!" + ) + }) + + // https://on.cypress.io/interacting-with-elements + it("should create a automation", () => { + cy.createTestTableWithData() + + cy.contains("automate").click() + cy.contains("Create New Automation").click() + cy.get("input").type("Add Record") + cy.contains("Save").click() + + // Add trigger + cy.get("[data-cy=add-automation-component]").click() + cy.get("[data-cy=RECORD_SAVED]").click() + cy.get("[data-cy=automation-block-setup]").within(() => { + cy.get("select") + .first() + .select("dog") + }) + + // Create action + cy.get("[data-cy=SAVE_RECORD]").click() + cy.get("[data-cy=automation-block-setup]").within(() => { + cy.get("select") + .first() + .select("dog") + cy.get("input") + .first() + .type("goodboy") + cy.get("input") + .eq(1) + .type("11") + }) + + // Save + cy.contains("Save Automation").click() + + // Activate Automation + cy.get("[data-cy=activate-automation]").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() + cy.addRecord(["Rover", 15]) + cy.reload() + cy.contains("goodboy").should("have.text", "goodboy") + }) +}) diff --git a/packages/builder/cypress/integration/createWorkflow.spec.js b/packages/builder/cypress/integration/createWorkflow.spec.js deleted file mode 100644 index 833777f106..0000000000 --- a/packages/builder/cypress/integration/createWorkflow.spec.js +++ /dev/null @@ -1,52 +0,0 @@ -context("Create a workflow", () => { - 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!" - ) - }) - - // 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() - - // 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") - - // 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") - }) - - 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") - }) -}) diff --git a/packages/builder/cypress/videos/createApp.spec.js.mp4 b/packages/builder/cypress/videos/createApp.spec.js.mp4 deleted file mode 100644 index 4e13dbe91b..0000000000 Binary files a/packages/builder/cypress/videos/createApp.spec.js.mp4 and /dev/null differ diff --git a/packages/builder/cypress/videos/createBinding.spec.js.mp4 b/packages/builder/cypress/videos/createBinding.spec.js.mp4 deleted file mode 100644 index 6cbfc19b9d..0000000000 Binary files a/packages/builder/cypress/videos/createBinding.spec.js.mp4 and /dev/null differ diff --git a/packages/builder/cypress/videos/createComponents.spec.js.mp4 b/packages/builder/cypress/videos/createComponents.spec.js.mp4 deleted file mode 100644 index 8b5fcbe188..0000000000 Binary files a/packages/builder/cypress/videos/createComponents.spec.js.mp4 and /dev/null differ diff --git a/packages/builder/cypress/videos/createTable.spec.js.mp4 b/packages/builder/cypress/videos/createTable.spec.js.mp4 deleted file mode 100644 index 162b21a8b5..0000000000 Binary files a/packages/builder/cypress/videos/createTable.spec.js.mp4 and /dev/null differ diff --git a/packages/builder/cypress/videos/createUser.spec.js.mp4 b/packages/builder/cypress/videos/createUser.spec.js.mp4 deleted file mode 100644 index cae887ed6c..0000000000 Binary files a/packages/builder/cypress/videos/createUser.spec.js.mp4 and /dev/null differ diff --git a/packages/builder/cypress/videos/createView.spec.js.mp4 b/packages/builder/cypress/videos/createView.spec.js.mp4 deleted file mode 100644 index e24101c222..0000000000 Binary files a/packages/builder/cypress/videos/createView.spec.js.mp4 and /dev/null differ diff --git a/packages/builder/cypress/videos/createWorkflow.spec.js.mp4 b/packages/builder/cypress/videos/createWorkflow.spec.js.mp4 deleted file mode 100644 index 22fd8541c7..0000000000 Binary files a/packages/builder/cypress/videos/createWorkflow.spec.js.mp4 and /dev/null differ diff --git a/packages/builder/cypress/videos/screens.spec.js.mp4 b/packages/builder/cypress/videos/screens.spec.js.mp4 deleted file mode 100644 index 812238057d..0000000000 Binary files a/packages/builder/cypress/videos/screens.spec.js.mp4 and /dev/null differ diff --git a/packages/builder/package.json b/packages/builder/package.json index 33071aff67..d46504f918 100644 --- a/packages/builder/package.json +++ b/packages/builder/package.json @@ -100,7 +100,7 @@ "jest": "^24.8.0", "ncp": "^2.0.0", "rimraf": "^3.0.2", - "rollup": "^1.12.0", + "rollup": "^2.11.2", "rollup-plugin-alias": "^1.5.2", "rollup-plugin-commonjs": "^10.0.0", "rollup-plugin-copy": "^3.0.0", @@ -110,10 +110,10 @@ "rollup-plugin-node-globals": "^1.4.0", "rollup-plugin-node-resolve": "^5.2.0", "rollup-plugin-svelte": "^5.0.3", - "rollup-plugin-terser": "^4.0.4", + "rollup-plugin-terser": "^7.0.2", "rollup-plugin-url": "^2.2.2", "start-server-and-test": "^1.11.0", - "svelte": "3.23.x", + "svelte": "^3.24.1", "svelte-jester": "^1.0.6" }, "gitHead": "115189f72a850bfb52b65ec61d932531bf327072" diff --git a/packages/builder/src/builderStore/index.js b/packages/builder/src/builderStore/index.js index 6a72690524..fff862703e 100644 --- a/packages/builder/src/builderStore/index.js +++ b/packages/builder/src/builderStore/index.js @@ -1,11 +1,11 @@ import { getStore } from "./store" import { getBackendUiStore } from "./store/backend" -import { getWorkflowStore } from "./store/workflow/" +import { getAutomationStore } from "./store/automation/" import analytics from "../analytics" export const store = getStore() export const backendUiStore = getBackendUiStore() -export const workflowStore = getWorkflowStore() +export const automationStore = getAutomationStore() export const initialise = async () => { try { diff --git a/packages/builder/src/builderStore/store/workflow/Workflow.js b/packages/builder/src/builderStore/store/automation/Automation.js similarity index 54% rename from packages/builder/src/builderStore/store/workflow/Workflow.js rename to packages/builder/src/builderStore/store/automation/Automation.js index 8ac6664e91..cbdcabeccc 100644 --- a/packages/builder/src/builderStore/store/workflow/Workflow.js +++ b/packages/builder/src/builderStore/store/automation/Automation.js @@ -1,59 +1,59 @@ import { generate } from "shortid" /** - * Class responsible for the traversing of the workflow definition. - * Workflow definitions are stored in linked lists. + * Class responsible for the traversing of the automation definition. + * Automation definitions are stored in linked lists. */ -export default class Workflow { - constructor(workflow) { - this.workflow = workflow +export default class Automation { + constructor(automation) { + this.automation = automation } hasTrigger() { - return this.workflow.definition.trigger + return this.automation.definition.trigger } addBlock(block) { // Make sure to add trigger if doesn't exist if (!this.hasTrigger() && block.type === "TRIGGER") { const trigger = { id: generate(), ...block } - this.workflow.definition.trigger = trigger + this.automation.definition.trigger = trigger return trigger } const newBlock = { id: generate(), ...block } - this.workflow.definition.steps = [ - ...this.workflow.definition.steps, + this.automation.definition.steps = [ + ...this.automation.definition.steps, newBlock, ] return newBlock } updateBlock(updatedBlock, id) { - const { steps, trigger } = this.workflow.definition + const { steps, trigger } = this.automation.definition if (trigger && trigger.id === id) { - this.workflow.definition.trigger = updatedBlock + this.automation.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 + this.automation.definition.steps = steps } deleteBlock(id) { - const { steps, trigger } = this.workflow.definition + const { steps, trigger } = this.automation.definition if (trigger && trigger.id === id) { - this.workflow.definition.trigger = null + this.automation.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) - this.workflow.definition.steps = steps + this.automation.definition.steps = steps } } diff --git a/packages/builder/src/builderStore/store/automation/index.js b/packages/builder/src/builderStore/store/automation/index.js new file mode 100644 index 0000000000..e1db07053e --- /dev/null +++ b/packages/builder/src/builderStore/store/automation/index.js @@ -0,0 +1,126 @@ +import { writable } from "svelte/store" +import api from "../../api" +import Automation from "./Automation" +import { cloneDeep } from "lodash/fp" + +const automationActions = store => ({ + fetch: async () => { + const responses = await Promise.all([ + api.get(`/api/automations`), + api.get(`/api/automations/definitions/list`), + ]) + const jsonResponses = await Promise.all(responses.map(x => x.json())) + store.update(state => { + state.automations = jsonResponses[0] + state.blockDefinitions = { + TRIGGER: jsonResponses[1].trigger, + ACTION: jsonResponses[1].action, + LOGIC: jsonResponses[1].logic, + } + return state + }) + }, + create: async ({ name }) => { + const automation = { + name, + type: "automation", + definition: { + steps: [], + }, + } + const CREATE_AUTOMATION_URL = `/api/automations` + const response = await api.post(CREATE_AUTOMATION_URL, automation) + const json = await response.json() + store.update(state => { + state.automations = [...state.automations, json.automation] + store.actions.select(json.automation) + return state + }) + }, + 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 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 + }) + }, + delete: async ({ automation }) => { + const { _id, _rev } = automation + const DELETE_AUTOMATION_URL = `/api/automations/${_id}/${_rev}` + await api.delete(DELETE_AUTOMATION_URL) + + store.update(state => { + const existingIdx = state.automations.findIndex( + existing => existing._id === _id + ) + state.automations.splice(existingIdx, 1) + state.automations = state.automations + state.selectedAutomation = null + state.selectedBlock = null + return state + }) + }, + trigger: async ({ automation }) => { + const { _id } = automation + const TRIGGER_AUTOMATION_URL = `/api/automations/${_id}/trigger` + return await api.post(TRIGGER_AUTOMATION_URL) + }, + select: automation => { + store.update(state => { + state.selectedAutomation = new Automation(cloneDeep(automation)) + state.selectedBlock = null + return state + }) + }, + addBlockToAutomation: block => { + store.update(state => { + const newBlock = state.selectedAutomation.addBlock(cloneDeep(block)) + state.selectedBlock = newBlock + return state + }) + }, + deleteAutomationBlock: block => { + store.update(state => { + const idx = state.selectedAutomation.automation.definition.steps.findIndex( + x => x.id === block.id + ) + state.selectedAutomation.deleteBlock(block.id) + + // Select next closest step + const steps = state.selectedAutomation.automation.definition.steps + let nextSelectedBlock + if (steps[idx] != null) { + nextSelectedBlock = steps[idx] + } else if (steps[idx - 1] != null) { + nextSelectedBlock = steps[idx - 1] + } else { + nextSelectedBlock = + state.selectedAutomation.automation.definition.trigger || null + } + state.selectedBlock = nextSelectedBlock + return state + }) + }, +}) + +export const getAutomationStore = () => { + const INITIAL_AUTOMATION_STATE = { + automations: [], + blockDefinitions: { + TRIGGER: [], + ACTION: [], + LOGIC: [], + }, + selectedAutomation: null, + } + const store = writable(INITIAL_AUTOMATION_STATE) + store.actions = automationActions(store) + return store +} diff --git a/packages/builder/src/builderStore/store/automation/tests/Automation.spec.js b/packages/builder/src/builderStore/store/automation/tests/Automation.spec.js new file mode 100644 index 0000000000..8378310c2e --- /dev/null +++ b/packages/builder/src/builderStore/store/automation/tests/Automation.spec.js @@ -0,0 +1,48 @@ +import Automation from "../Automation" +import TEST_AUTOMATION from "./testAutomation" + +const TEST_BLOCK = { + id: "AUXJQGZY7", + name: "Delay", + icon: "ri-time-fill", + tagline: "Delay for {{time}} milliseconds", + description: "Delay the automation until an amount of time has passed.", + params: { time: "number" }, + type: "LOGIC", + args: { time: "5000" }, + stepId: "DELAY", +} + +describe("Automation Data Object", () => { + let automation + + beforeEach(() => { + automation = new Automation({ ...TEST_AUTOMATION }) + }) + + it("adds a automation block to the automation", () => { + automation.addBlock(TEST_BLOCK) + expect(automation.automation.definition) + }) + + it("updates a automation block with new attributes", () => { + const firstBlock = automation.automation.definition.steps[0] + const updatedBlock = { + ...firstBlock, + name: "UPDATED", + } + automation.updateBlock(updatedBlock, firstBlock.id) + expect(automation.automation.definition.steps[0]).toEqual(updatedBlock) + }) + + it("deletes a automation block successfully", () => { + const { steps } = automation.automation.definition + const originalLength = steps.length + + const lastBlock = steps[steps.length - 1] + automation.deleteBlock(lastBlock.id) + expect(automation.automation.definition.steps.length).toBeLessThan( + originalLength + ) + }) +}) diff --git a/packages/builder/src/builderStore/store/workflow/tests/testWorkflow.js b/packages/builder/src/builderStore/store/automation/tests/testAutomation.js similarity index 97% rename from packages/builder/src/builderStore/store/workflow/tests/testWorkflow.js rename to packages/builder/src/builderStore/store/automation/tests/testAutomation.js index 96999277eb..e75560c1f0 100644 --- a/packages/builder/src/builderStore/store/workflow/tests/testWorkflow.js +++ b/packages/builder/src/builderStore/store/automation/tests/testAutomation.js @@ -1,5 +1,5 @@ export default { - name: "Test workflow", + name: "Test automation", definition: { steps: [ { @@ -68,7 +68,7 @@ export default { stepId: "RECORD_SAVED", }, }, - type: "workflow", + type: "automation", ok: true, id: "b384f861f4754e1693835324a7fcca62", rev: "1-aa1c2cbd868ef02e26f8fad531dd7e37", diff --git a/packages/builder/src/builderStore/store/backend.js b/packages/builder/src/builderStore/store/backend.js index 3835f44d64..bbfbcd4726 100644 --- a/packages/builder/src/builderStore/store/backend.js +++ b/packages/builder/src/builderStore/store/backend.js @@ -87,7 +87,7 @@ export const getBackendUiStore = () => { state.models = state.models.filter( existing => existing._id !== model._id ) - state.selectedModel = state.models[0] || {} + state.selectedModel = {} return state }) }, diff --git a/packages/builder/src/builderStore/store/workflow/index.js b/packages/builder/src/builderStore/store/workflow/index.js deleted file mode 100644 index 6a4db7afaf..0000000000 --- a/packages/builder/src/builderStore/store/workflow/index.js +++ /dev/null @@ -1,125 +0,0 @@ -import { writable } from "svelte/store" -import api from "../../api" -import Workflow from "./Workflow" -import { cloneDeep } from "lodash/fp" - -const workflowActions = store => ({ - fetch: async () => { - 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 = jsonResponses[0] - state.blockDefinitions = { - TRIGGER: jsonResponses[1].trigger, - ACTION: jsonResponses[1].action, - LOGIC: jsonResponses[1].logic, - } - return state - }) - }, - create: async ({ name }) => { - const workflow = { - name, - definition: { - steps: [], - }, - } - const CREATE_WORKFLOW_URL = `/api/workflows` - const response = await api.post(CREATE_WORKFLOW_URL, workflow) - const json = await response.json() - store.update(state => { - state.workflows = [...state.workflows, json.workflow] - store.actions.select(json.workflow) - return state - }) - }, - save: 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 - }) - }, - delete: async ({ workflow }) => { - const { _id, _rev } = workflow - const DELETE_WORKFLOW_URL = `/api/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.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.selectedWorkflow = new Workflow(cloneDeep(workflow)) - state.selectedBlock = null - return state - }) - }, - addBlockToWorkflow: block => { - store.update(state => { - const newBlock = state.selectedWorkflow.addBlock(cloneDeep(block)) - state.selectedBlock = newBlock - return state - }) - }, - deleteWorkflowBlock: block => { - store.update(state => { - 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 - }) - }, -}) - -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 deleted file mode 100644 index 4d270ea611..0000000000 --- a/packages/builder/src/builderStore/store/workflow/tests/Workflow.spec.js +++ /dev/null @@ -1,48 +0,0 @@ -import Workflow from "../Workflow" -import TEST_WORKFLOW from "./testWorkflow" - -const TEST_BLOCK = { - 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 }) - }) - - 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 - ) - }) -}) diff --git a/packages/builder/src/components/workflow/WorkflowBuilder/WorkflowBuilder.svelte b/packages/builder/src/components/automation/AutomationBuilder/AutomationBuilder.svelte similarity index 58% rename from packages/builder/src/components/workflow/WorkflowBuilder/WorkflowBuilder.svelte rename to packages/builder/src/components/automation/AutomationBuilder/AutomationBuilder.svelte index b6dabc55fd..ad49776605 100644 --- a/packages/builder/src/components/workflow/WorkflowBuilder/WorkflowBuilder.svelte +++ b/packages/builder/src/components/automation/AutomationBuilder/AutomationBuilder.svelte @@ -1,49 +1,48 @@
- +