1
0
Fork 0
mirror of synced 2024-07-04 05:50:57 +12:00

Merge pull request #626 from Budibase/rename-workflow-automation

Rename workflow -> automation
This commit is contained in:
Michael Drury 2020-09-22 15:27:33 +01:00 committed by GitHub
commit 13c368481c
72 changed files with 527 additions and 522 deletions

View file

@ -4,6 +4,6 @@ node_modules_win
package-lock.json
release/
dist/
cypress/screenshots
cypress/videos
routify
cypress/videos
cypress/screenshots

View file

@ -1,27 +1,27 @@
context("Create a workflow", () => {
context("Create a automation", () => {
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!"
"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 workflow", () => {
it("should create a automation", () => {
cy.createTestTableWithData()
cy.contains("workflow").click()
cy.contains("Create New Workflow").click()
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-workflow-component]").click()
cy.get("[data-cy=add-automation-component]").click()
cy.get("[data-cy=RECORD_SAVED]").click()
cy.get("[data-cy=workflow-block-setup]").within(() => {
cy.get("[data-cy=automation-block-setup]").within(() => {
cy.get("select")
.first()
.select("dog")
@ -29,7 +29,7 @@ context("Create a workflow", () => {
// Create action
cy.get("[data-cy=SAVE_RECORD]").click()
cy.get("[data-cy=workflow-block-setup]").within(() => {
cy.get("[data-cy=automation-block-setup]").within(() => {
cy.get("select")
.first()
.select("dog")
@ -42,10 +42,10 @@ context("Create a workflow", () => {
})
// Save
cy.contains("Save Workflow").click()
cy.contains("Save Automation").click()
// Activate Workflow
cy.get("[data-cy=activate-workflow]").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")
})

View file

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

View file

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

View file

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

View file

@ -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 <b>{{time}}</b> 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
)
})
})

View file

@ -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",

View file

@ -1,126 +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,
type: "workflow",
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
}

View file

@ -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 <b>{{time}}</b> 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
)
})
})

View file

@ -1,48 +1,48 @@
<script>
import { afterUpdate } from "svelte"
import { workflowStore, backendUiStore } from "builderStore"
import { automationStore, backendUiStore } from "builderStore"
import { notifier } from "builderStore/store/notifications"
import Flowchart from "./flowchart/FlowChart.svelte"
$: workflow = $workflowStore.selectedWorkflow?.workflow
$: workflowLive = workflow?.live
$: automation = $automationStore.selectedAutomation?.automation
$: automationLive = automation?.live
$: instanceId = $backendUiStore.selectedDatabase._id
function onSelect(block) {
workflowStore.update(state => {
automationStore.update(state => {
state.selectedBlock = block
return state
})
}
function setWorkflowLive(live) {
workflow.live = live
workflowStore.actions.save({ instanceId, workflow })
function setAutomationLive(live) {
automation.live = live
automationStore.actions.save({ instanceId, automation })
if (live) {
notifier.info(`Workflow ${workflow.name} enabled.`)
notifier.info(`Automation ${automation.name} enabled.`)
} else {
notifier.danger(`Workflow ${workflow.name} disabled.`)
notifier.danger(`Automation ${automation.name} disabled.`)
}
}
</script>
<section>
<Flowchart {workflow} {onSelect} />
<Flowchart {automation} {onSelect} />
</section>
<footer>
{#if workflow}
{#if automation}
<button
class:highlighted={workflowLive}
class:hoverable={workflowLive}
class:highlighted={automationLive}
class:hoverable={automationLive}
class="stop-button hoverable">
<i class="ri-stop-fill" on:click={() => setWorkflowLive(false)} />
<i class="ri-stop-fill" on:click={() => setAutomationLive(false)} />
</button>
<button
class:highlighted={!workflowLive}
class:hoverable={!workflowLive}
class:highlighted={!automationLive}
class:hoverable={!automationLive}
class="play-button hoverable"
data-cy="activate-workflow"
on:click={() => setWorkflowLive(true)}>
data-cy="activate-automation"
on:click={() => setAutomationLive(true)}>
<i class="ri-play-fill" />
</button>
{/if}

View file

@ -4,17 +4,17 @@
import { flip } from "svelte/animate"
import { fade, fly } from "svelte/transition"
export let workflow
export let automation
export let onSelect
let blocks
$: {
blocks = []
if (workflow) {
if (workflow.definition.trigger) {
blocks.push(workflow.definition.trigger)
if (automation) {
if (automation.definition.trigger) {
blocks.push(automation.definition.trigger)
}
blocks = blocks.concat(workflow.definition.steps || [])
blocks = blocks.concat(automation.definition.steps || [])
}
}
</script>

View file

@ -1,13 +1,13 @@
<script>
import { workflowStore } from "builderStore"
import WorkflowBlockTagline from "./WorkflowBlockTagline.svelte"
import { automationStore } from "builderStore"
import AutomationBlockTagline from "./AutomationBlockTagline.svelte"
export let onSelect
export let block
let selected
$: selected = $workflowStore.selectedBlock?.id === block.id
$: steps = $workflowStore.selectedWorkflow?.workflow?.definition?.steps ?? []
$: selected = $automationStore.selectedBlock?.id === block.id
$: steps = $automationStore.selectedAutomation?.automation?.definition?.steps ?? []
$: blockIdx = steps.findIndex(step => step.id === block.id)
</script>
@ -32,7 +32,7 @@
</header>
<hr />
<p>
<WorkflowBlockTagline {block} />
<AutomationBlockTagline {block} />
</p>
</div>

View file

@ -2,17 +2,17 @@
import Modal from "svelte-simple-modal"
import { notifier } from "builderStore/store/notifications"
import { onMount, getContext } from "svelte"
import { backendUiStore, workflowStore } from "builderStore"
import CreateWorkflowModal from "./CreateWorkflowModal.svelte"
import { backendUiStore, automationStore } from "builderStore"
import CreateAutomationModal from "./CreateAutomationModal.svelte"
import { Button } from "@budibase/bbui"
const { open, close } = getContext("simple-modal")
$: selectedWorkflowId = $workflowStore.selectedWorkflow?.workflow?._id
$: selectedAutomationId = $automationStore.selectedAutomation?.automation?._id
function newWorkflow() {
function newAutomation() {
open(
CreateWorkflowModal,
CreateAutomationModal,
{
onClosed: close,
},
@ -21,20 +21,20 @@
}
onMount(() => {
workflowStore.actions.fetch()
automationStore.actions.fetch()
})
</script>
<section>
<Button purple wide on:click={newWorkflow}>Create New Workflow</Button>
<Button purple wide on:click={newAutomation}>Create New Automation</Button>
<ul>
{#each $workflowStore.workflows as workflow}
{#each $automationStore.automations as automation}
<li
class="workflow-item"
class:selected={workflow._id === selectedWorkflowId}
on:click={() => workflowStore.actions.select(workflow)}>
<i class="ri-stackshare-line" class:live={workflow.live} />
{workflow.name}
class="automation-item"
class:selected={automation._id === selectedAutomationId}
on:click={() => automationStore.actions.select(automation)}>
<i class="ri-stackshare-line" class:live={automation.live} />
{automation.name}
</li>
{/each}
</ul>
@ -68,7 +68,7 @@
font-size: 14px;
}
.workflow-item {
.automation-item {
display: flex;
border-radius: 5px;
padding-left: 12px;
@ -78,21 +78,21 @@
color: var(--ink);
}
.workflow-item i {
.automation-item i {
font-size: 24px;
margin-right: 10px;
}
.workflow-item:hover {
.automation-item:hover {
cursor: pointer;
background: var(--grey-1);
}
.workflow-item.selected {
.automation-item.selected {
background: var(--grey-2);
}
.new-workflow-button {
.new-automation-button {
cursor: pointer;
border: 1px solid var(--grey-4);
border-radius: 3px;
@ -108,7 +108,7 @@
transition: all 2ms;
}
.new-workflow-button:hover {
.new-automation-button:hover {
background: var(--grey-1);
}

View file

@ -1,5 +1,5 @@
<script>
import { store, backendUiStore, workflowStore } from "builderStore"
import { store, backendUiStore, automationStore } from "builderStore"
import { notifier } from "builderStore/store/notifications"
import ActionButton from "components/common/ActionButton.svelte"
import { Input } from "@budibase/bbui"
@ -12,19 +12,19 @@
$: instanceId = $backendUiStore.selectedDatabase._id
$: appId = $store.appId
async function createWorkflow() {
await workflowStore.actions.create({
async function createAutomation() {
await automationStore.actions.create({
name,
instanceId,
})
onClosed()
notifier.success(`Workflow ${name} created.`)
notifier.success(`Automation ${name} created.`)
}
</script>
<header>
<i class="ri-stackshare-line" />
Create Workflow
Create Automation
</header>
<div>
<Input bind:value={name} label="Name" />
@ -32,10 +32,10 @@
<footer>
<a href="https://docs.budibase.com">
<i class="ri-information-line" />
Learn about workflows
Learn about automations
</a>
<ActionButton secondary on:click={onClosed}>Cancel</ActionButton>
<ActionButton disabled={!valid} on:click={createWorkflow}>Save</ActionButton>
<ActionButton disabled={!valid} on:click={createAutomation}>Save</ActionButton>
</footer>
<style>

View file

@ -1,22 +1,22 @@
<script>
import { workflowStore } from "builderStore"
import WorkflowList from "./WorkflowList/WorkflowList.svelte"
import { automationStore } from "builderStore"
import AutomationList from "./AutomationList/AutomationList.svelte"
import BlockList from "./BlockList/BlockList.svelte"
let selectedTab = "WORKFLOWS"
let selectedTab = "AUTOMATIONS"
</script>
<header>
<span
data-cy="workflow-list"
class="hoverable workflow-header"
class:selected={selectedTab === 'WORKFLOWS'}
on:click={() => (selectedTab = 'WORKFLOWS')}>
Workflows
data-cy="automation-list"
class="hoverable automation-header"
class:selected={selectedTab === 'AUTOMATIONS'}
on:click={() => (selectedTab = 'AUTOMATIONS')}>
Automations
</span>
{#if $workflowStore.selectedWorkflow}
{#if $automationStore.selectedAutomation}
<span
data-cy="add-workflow-component"
data-cy="add-automation-component"
class="hoverable"
class:selected={selectedTab === 'ADD'}
on:click={() => (selectedTab = 'ADD')}>
@ -24,8 +24,8 @@
</span>
{/if}
</header>
{#if selectedTab === 'WORKFLOWS'}
<WorkflowList />
{#if selectedTab === 'AUTOMATIONS'}
<AutomationList />
{:else if selectedTab === 'ADD'}
<BlockList />
{/if}
@ -40,7 +40,7 @@
margin-bottom: 20px;
}
.workflow-header {
.automation-header {
margin-right: 20px;
}

View file

@ -1,12 +1,12 @@
<script>
import { workflowStore } from "builderStore"
import { automationStore } from "builderStore"
export let blockDefinition
export let stepId
export let blockType
function addBlockToWorkflow() {
workflowStore.actions.addBlockToWorkflow({
function addBlockToAutomation() {
automationStore.actions.addBlockToAutomation({
...blockDefinition,
args: blockDefinition.args || {},
stepId,
@ -16,20 +16,20 @@
</script>
<div
class="workflow-block hoverable"
on:click={addBlockToWorkflow}
class="automation-block hoverable"
on:click={addBlockToAutomation}
data-cy={stepId}>
<div>
<i class={blockDefinition.icon} />
</div>
<div class="workflow-text">
<div class="automation-text">
<h4>{blockDefinition.name}</h4>
<p>{blockDefinition.description}</p>
</div>
</div>
<style>
.workflow-block {
.automation-block {
display: grid;
grid-template-columns: 20px auto;
align-items: center;
@ -38,11 +38,11 @@
border-radius: var(--border-radius-m);
}
.workflow-block:hover {
.automation-block:hover {
background-color: var(--grey-1);
}
.workflow-text {
.automation-text {
margin-left: 16px;
}

View file

@ -1,14 +1,14 @@
<script>
import { workflowStore } from "builderStore"
import WorkflowBlock from "./WorkflowBlock.svelte"
import { automationStore } from "builderStore"
import AutomationBlock from "./AutomationBlock.svelte"
import FlatButtonGroup from "components/userInterface/FlatButtonGroup.svelte"
let selectedTab = "TRIGGER"
let buttonProps = []
$: blocks = Object.entries($workflowStore.blockDefinitions[selectedTab])
$: blocks = Object.entries($automationStore.blockDefinitions[selectedTab])
$: {
if ($workflowStore.selectedWorkflow.hasTrigger()) {
if ($automationStore.selectedAutomation.hasTrigger()) {
buttonProps = [
{ value: "ACTION", text: "Action" },
{ value: "LOGIC", text: "Logic" },
@ -33,7 +33,7 @@
<FlatButtonGroup value={selectedTab} {buttonProps} onChange={onChangeTab} />
<div id="blocklist">
{#each blocks as [stepId, blockDefinition]}
<WorkflowBlock {blockDefinition} {stepId} blockType={selectedTab} />
<AutomationBlock {blockDefinition} {stepId} blockType={selectedTab} />
{/each}
</div>
</section>

View file

@ -0,0 +1,3 @@
export { default as AutomationPanel } from "./AutomationPanel.svelte"
export { default as BlockList } from "./BlockList/BlockList.svelte"
export { default as AutomationList } from "./AutomationList/AutomationList.svelte"

View file

@ -2,25 +2,25 @@
import ModelSelector from "./ParamInputs/ModelSelector.svelte"
import RecordSelector from "./ParamInputs/RecordSelector.svelte"
import { Input, TextArea, Select, Label } from "@budibase/bbui"
import { workflowStore } from "builderStore"
import { automationStore } from "builderStore"
import BindableInput from "../../userInterface/BindableInput.svelte"
export let block
$: inputs = Object.entries(block.schema?.inputs?.properties || {})
$: bindings = getAvailableBindings(
block,
$workflowStore.selectedWorkflow?.workflow?.definition
$automationStore.selectedAutomation?.automation?.definition
)
function getAvailableBindings(block, workflow) {
if (!block || !workflow) {
function getAvailableBindings(block, automation) {
if (!block || !automation) {
return []
}
// Find previous steps to the selected one
let allSteps = [...workflow.steps]
if (workflow.trigger) {
allSteps = [workflow.trigger, ...allSteps]
let allSteps = [...automation.steps]
if (automation.trigger) {
allSteps = [automation.trigger, ...allSteps]
}
const blockIdx = allSteps.findIndex(step => step.id === block.id)
@ -44,7 +44,7 @@
}
</script>
<div class="container" data-cy="workflow-block-setup">
<div class="container" data-cy="automation-block-setup">
<div class="block-label">{block.name}</div>
{#each inputs as [key, value]}
<div class="bb-margin-xl block-field">

View file

@ -1,5 +1,5 @@
<script>
import { store, backendUiStore, workflowStore } from "builderStore"
import { store, backendUiStore, automationStore } from "builderStore"
import { notifier } from "builderStore/store/notifications"
import ActionButton from "components/common/ActionButton.svelte"
@ -10,32 +10,32 @@
$: valid = !!name
$: instanceId = $backendUiStore.selectedDatabase._id
async function deleteWorkflow() {
await workflowStore.actions.delete({
async function deleteAutomation() {
await automationStore.actions.delete({
instanceId,
workflow: $workflowStore.selectedWorkflow.workflow,
automation: $automationStore.selectedAutomation.automation,
})
onClosed()
notifier.danger("Workflow deleted.")
notifier.danger("Automation deleted.")
}
</script>
<header>
<i class="ri-stackshare-line" />
Delete Workflow
Delete Automation
</header>
<div>
<p>
Are you sure you want to delete this workflow? This action can't be undone.
Are you sure you want to delete this automation? This action can't be undone.
</p>
</div>
<footer>
<a href="https://docs.budibase.com">
<i class="ri-information-line" />
Learn about workflows
Learn about automations
</a>
<ActionButton on:click={onClosed}>Cancel</ActionButton>
<ActionButton alert on:click={deleteWorkflow}>Delete</ActionButton>
<ActionButton alert on:click={deleteAutomation}>Delete</ActionButton>
</footer>
<style>

View file

@ -1,49 +1,49 @@
<script>
import { getContext } from "svelte"
import { backendUiStore, workflowStore } from "builderStore"
import { backendUiStore, automationStore } from "builderStore"
import { notifier } from "builderStore/store/notifications"
import WorkflowBlockSetup from "./WorkflowBlockSetup.svelte"
import DeleteWorkflowModal from "./DeleteWorkflowModal.svelte"
import AutomationBlockSetup from "./AutomationBlockSetup.svelte"
import DeleteAutomationModal from "./DeleteAutomationModal.svelte"
import { Button, Input, Label } from "@budibase/bbui"
const { open, close } = getContext("simple-modal")
let selectedTab = "SETUP"
$: workflow = $workflowStore.selectedWorkflow?.workflow
$: automation = $automationStore.selectedAutomation?.automation
$: allowDeleteBlock =
$workflowStore.selectedBlock?.type !== "TRIGGER" ||
!workflow?.definition?.steps?.length
$automationStore.selectedBlock?.type !== "TRIGGER" ||
!automation?.definition?.steps?.length
function deleteWorkflow() {
function deleteAutomation() {
open(
DeleteWorkflowModal,
DeleteAutomationModal,
{ onClosed: close },
{ styleContent: { padding: "0" } }
)
}
function deleteWorkflowBlock() {
workflowStore.actions.deleteWorkflowBlock($workflowStore.selectedBlock)
function deleteAutomationBlock() {
automationStore.actions.deleteAutomationBlock($automationStore.selectedBlock)
}
async function testWorkflow() {
const result = await workflowStore.actions.trigger({
workflow: $workflowStore.selectedWorkflow.workflow,
async function testAutomation() {
const result = await automationStore.actions.trigger({
automation: $automationStore.selectedAutomation.automation,
})
if (result.status === 200) {
notifier.success(`Workflow ${workflow.name} triggered successfully.`)
notifier.success(`Automation ${automation.name} triggered successfully.`)
} else {
notifier.danger(`Failed to trigger workflow ${workflow.name}.`)
notifier.danger(`Failed to trigger automation ${automation.name}.`)
}
}
async function saveWorkflow() {
await workflowStore.actions.save({
async function saveAutomation() {
await automationStore.actions.save({
instanceId: $backendUiStore.selectedDatabase._id,
workflow,
automation,
})
notifier.success(`Workflow ${workflow.name} saved.`)
notifier.success(`Automation ${automation.name} saved.`)
}
</script>
@ -56,38 +56,38 @@
Setup
</span>
</header>
{#if $workflowStore.selectedBlock}
<WorkflowBlockSetup bind:block={$workflowStore.selectedBlock} />
{#if $automationStore.selectedBlock}
<AutomationBlockSetup bind:block={$automationStore.selectedBlock} />
<div class="buttons">
<Button green wide data-cy="save-workflow-setup" on:click={saveWorkflow}>
Save Workflow
<Button green wide data-cy="save-automation-setup" on:click={saveAutomation}>
Save Automation
</Button>
<Button
disabled={!allowDeleteBlock}
red
wide
on:click={deleteWorkflowBlock}>
on:click={deleteAutomationBlock}>
Delete Block
</Button>
</div>
{:else if $workflowStore.selectedWorkflow}
{:else if $automationStore.selectedAutomation}
<div class="panel">
<div class="panel-body">
<div class="block-label">
Workflow
<b>{workflow.name}</b>
Automation
<b>{automation.name}</b>
</div>
</div>
<Button secondary wide on:click={testWorkflow}>Test Workflow</Button>
<Button secondary wide on:click={testAutomation}>Test Automation</Button>
<div class="buttons">
<Button
green
wide
data-cy="save-workflow-setup"
on:click={saveWorkflow}>
Save Workflow
data-cy="save-automation-setup"
on:click={saveAutomation}>
Save Automation
</Button>
<Button red wide on:click={deleteWorkflow}>Delete Workflow</Button>
<Button red wide on:click={deleteAutomation}>Delete Automation</Button>
</div>
</div>
{/if}

View file

@ -0,0 +1,3 @@
export { default as AutomationBuilder } from "./AutomationBuilder/AutomationBuilder.svelte"
export { default as SetupPanel } from "./SetupPanel/SetupPanel.svelte"
export { default as AutomationPanel } from "./AutomationPanel/AutomationPanel.svelte"

View file

@ -1,7 +1,7 @@
<script>
import { writable } from "svelte/store"
import { store, workflowStore, backendUiStore } from "builderStore"
import { store, automationStore, backendUiStore } from "builderStore"
import { string, object } from "yup"
import api, { get } from "builderStore/api"
import Form from "@svelteschool/svelte-forms"
@ -133,7 +133,7 @@
if (applicationPkg.ok) {
backendUiStore.actions.reset()
await store.setPackage(pkg)
workflowStore.actions.fetch()
automationStore.actions.fetch()
} else {
throw new Error(pkg)
}

View file

@ -3,7 +3,7 @@
import { find, map, keys, reduce, keyBy } from "lodash/fp"
import { pipe } from "components/common/core"
import { EVENT_TYPE_MEMBER_NAME } from "components/common/eventHandlers"
import { store, workflowStore } from "builderStore"
import { store, automationStore } from "builderStore"
import { ArrowDownIcon } from "components/common/Icons/"
import { createEventDispatcher } from "svelte"
@ -18,14 +18,14 @@
</script>
<div class="handler-option">
{#if parameter.name === 'workflow'}
{#if parameter.name === 'automation'}
<span>{parameter.name}</span>
{/if}
{#if parameter.name === 'workflow'}
{#if parameter.name === 'automation'}
<Select on:change bind:value={parameter.value}>
<option value="" />
{#each $workflowStore.workflows.filter(wf => wf.live) as workflow}
<option value={workflow._id}>{workflow.name}</option>
{#each $automationStore.automations.filter(wf => wf.live) as automation}
<option value={automation._id}>{automation.name}</option>
{/each}
</Select>
{:else if parameter.name === 'url'}

View file

@ -1,3 +0,0 @@
export { default as WorkflowPanel } from "./WorkflowPanel.svelte"
export { default as BlockList } from "./BlockList/BlockList.svelte"
export { default as WorkflowList } from "./WorkflowList/WorkflowList.svelte"

View file

@ -1,3 +0,0 @@
export { default as WorkflowBuilder } from "./WorkflowBuilder/WorkflowBuilder.svelte"
export { default as SetupPanel } from "./SetupPanel/SetupPanel.svelte"
export { default as WorkflowPanel } from "./WorkflowPanel/WorkflowPanel.svelte"

View file

@ -1,6 +1,6 @@
<script>
import Modal from "svelte-simple-modal"
import { store, workflowStore, backendUiStore } from "builderStore"
import { store, automationStore, backendUiStore } from "builderStore"
import SettingsLink from "components/settings/Link.svelte"
import { get } from "builderStore/api"
@ -21,17 +21,17 @@
if (res.ok) {
backendUiStore.actions.reset()
await store.setPackage(pkg)
workflowStore.actions.fetch()
await automationStore.actions.fetch()
return pkg
} else {
throw new Error(pkg)
}
}
// handles navigation between frontend, backend, workflow.
// handles navigation between frontend, backend, automation.
// this remembers your last place on each of the sections
// e.g. if one of your screens is selected on front end, then
// you browse to backend, when you click fronend, you will be
// you browse to backend, when you click frontend, you will be
// brought back to the same screen
const topItemNavigate = path => () => {
const activeTopNav = $layout.children.find(c => $isActive(c.path))

View file

@ -1,18 +1,19 @@
<!-- routify:options index=3 -->
<script>
import { workflowStore } from "builderStore"
import { WorkflowPanel, SetupPanel } from "components/workflow"
import { automationStore } from "builderStore"
import { AutomationPanel, SetupPanel } from "components/automation"
</script>
<div class="root">
<div class="nav">
<div class="inner">
<WorkflowPanel />
<AutomationPanel />
</div>
</div>
<div class="content">
<slot />
</div>
{#if $workflowStore.selectedWorkflow}
{#if $automationStore.selectedAutomation}
<div class="nav">
<div class="inner">
<SetupPanel />

View file

@ -0,0 +1,5 @@
<script>
import { AutomationBuilder } from "components/automation"
</script>
<AutomationBuilder />

View file

@ -1,3 +1,4 @@
<!-- routify:options index=1 -->
<script>
import { getContext } from "svelte"
import { store, backendUiStore } from "builderStore"

View file

@ -1,3 +1,4 @@
<!-- routify:options index=1 -->
<script>
import { store, backendUiStore } from "builderStore"
import { goto } from "@sveltech/routify"

View file

@ -1,5 +0,0 @@
<script>
import { WorkflowBuilder } from "components/workflow"
</script>
<WorkflowBuilder />

View file

@ -1,8 +1,8 @@
const CouchDB = require("../../db")
const newid = require("../../db/newid")
const actions = require("../../workflows/actions")
const logic = require("../../workflows/logic")
const triggers = require("../../workflows/triggers")
const actions = require("../../automations/actions")
const logic = require("../../automations/logic")
const triggers = require("../../automations/triggers")
/*************************
* *
@ -10,12 +10,12 @@ const triggers = require("../../workflows/triggers")
* *
*************************/
function cleanWorkflowInputs(workflow) {
if (workflow == null) {
return workflow
function cleanAutomationInputs(automation) {
if (automation == null) {
return automation
}
let steps = workflow.definition.steps
let trigger = workflow.definition.trigger
let steps = automation.definition.steps
let trigger = automation.definition.trigger
let allSteps = [...steps, trigger]
for (let step of allSteps) {
if (step == null) {
@ -27,25 +27,25 @@ function cleanWorkflowInputs(workflow) {
}
}
}
return workflow
return automation
}
exports.create = async function(ctx) {
const db = new CouchDB(ctx.user.instanceId)
let workflow = ctx.request.body
let automation = ctx.request.body
workflow._id = newid()
automation._id = newid()
workflow.type = "workflow"
workflow = cleanWorkflowInputs(workflow)
const response = await db.post(workflow)
workflow._rev = response.rev
automation.type = "automation"
automation = cleanAutomationInputs(automation)
const response = await db.post(automation)
automation._rev = response.rev
ctx.status = 200
ctx.body = {
message: "Workflow created successfully",
workflow: {
...workflow,
message: "Automation created successfully",
automation: {
...automation,
...response,
},
}
@ -53,17 +53,17 @@ exports.create = async function(ctx) {
exports.update = async function(ctx) {
const db = new CouchDB(ctx.user.instanceId)
let workflow = ctx.request.body
let automation = ctx.request.body
workflow = cleanWorkflowInputs(workflow)
const response = await db.put(workflow)
workflow._rev = response.rev
automation = cleanAutomationInputs(automation)
const response = await db.put(automation)
automation._rev = response.rev
ctx.status = 200
ctx.body = {
message: `Workflow ${workflow._id} updated successfully.`,
workflow: {
...workflow,
message: `Automation ${automation._id} updated successfully.`,
automation: {
...automation,
_rev: response.rev,
_id: response.id,
},
@ -73,7 +73,7 @@ exports.update = async function(ctx) {
exports.fetch = async function(ctx) {
const db = new CouchDB(ctx.user.instanceId)
const response = await db.query(`database/by_type`, {
key: ["workflow"],
key: ["automation"],
include_docs: true,
})
ctx.body = response.rows.map(row => row.doc)
@ -117,14 +117,14 @@ module.exports.getDefinitionList = async function(ctx) {
exports.trigger = async function(ctx) {
const db = new CouchDB(ctx.user.instanceId)
let workflow = await db.get(ctx.params.id)
await triggers.externalTrigger(workflow, {
let automation = await db.get(ctx.params.id)
await triggers.externalTrigger(automation, {
...ctx.request.body,
instanceId: ctx.user.instanceId,
})
ctx.status = 200
ctx.body = {
message: `Workflow ${workflow._id} has been triggered.`,
workflow,
message: `Automation ${automation._id} has been triggered.`,
automation,
}
}

View file

@ -31,9 +31,9 @@ exports.create = async function(ctx) {
emit([doc.type], doc._id)
}.toString(),
},
by_workflow_trigger: {
by_automation_trigger: {
map: function(doc) {
if (doc.type === "workflow") {
if (doc.type === "automation") {
const trigger = doc.definition.trigger
if (trigger) {
emit([trigger.event], trigger)

View file

@ -188,7 +188,7 @@ exports.destroy = async function(ctx) {
}
ctx.body = await db.remove(ctx.params.recordId, ctx.params.revId)
ctx.status = 200
// for workflows
// for automations
ctx.record = record
emitEvent(`record:delete`, ctx, record)
}

View file

@ -12,7 +12,7 @@ const controller = {
!name.startsWith("all") &&
name !== "by_type" &&
name !== "by_username" &&
name !== "by_workflow_trigger"
name !== "by_automation_trigger"
) {
response.push({
name,

View file

@ -16,7 +16,7 @@ const {
viewRoutes,
staticRoutes,
componentRoutes,
workflowRoutes,
automationRoutes,
accesslevelRoutes,
apiKeysRoutes,
} = require("./routes")
@ -84,8 +84,8 @@ router.use(userRoutes.allowedMethods())
router.use(instanceRoutes.routes())
router.use(instanceRoutes.allowedMethods())
router.use(workflowRoutes.routes())
router.use(workflowRoutes.allowedMethods())
router.use(automationRoutes.routes())
router.use(automationRoutes.allowedMethods())
router.use(deployRoutes.routes())
router.use(deployRoutes.allowedMethods())

View file

@ -1,5 +1,5 @@
const Router = require("@koa/router")
const controller = require("../controllers/workflow")
const controller = require("../controllers/automation")
const authorized = require("../../middleware/authorized")
const joiValidator = require("../../middleware/joi-validator")
const { BUILDER } = require("../../utilities/accessLevels")
@ -30,7 +30,7 @@ function generateValidator(existing = false) {
_id: existing ? Joi.string().required() : Joi.string(),
_rev: existing ? Joi.string().required() : Joi.string(),
name: Joi.string().required(),
type: Joi.string().valid("workflow").required(),
type: Joi.string().valid("automation").required(),
definition: Joi.object({
steps: Joi.array().required().items(generateStepSchema(["ACTION", "LOGIC"])),
trigger: generateStepSchema(["TRIGGER"]),
@ -40,40 +40,40 @@ function generateValidator(existing = false) {
router
.get(
"/api/workflows/trigger/list",
"/api/automations/trigger/list",
authorized(BUILDER),
controller.getTriggerList
)
.get(
"/api/workflows/action/list",
"/api/automations/action/list",
authorized(BUILDER),
controller.getActionList
)
.get(
"/api/workflows/logic/list",
"/api/automations/logic/list",
authorized(BUILDER),
controller.getLogicList
)
.get(
"/api/workflows/definitions/list",
"/api/automations/definitions/list",
authorized(BUILDER),
controller.getDefinitionList
)
.get("/api/workflows", authorized(BUILDER), controller.fetch)
.get("/api/workflows/:id", authorized(BUILDER), controller.find)
.get("/api/automations", authorized(BUILDER), controller.fetch)
.get("/api/automations/:id", authorized(BUILDER), controller.find)
.put(
"/api/workflows",
"/api/automations",
authorized(BUILDER),
generateValidator(true),
controller.update
)
.post(
"/api/workflows",
"/api/automations",
authorized(BUILDER),
generateValidator(false),
controller.create
)
.post("/api/workflows/:id/trigger", controller.trigger)
.delete("/api/workflows/:id/:rev", authorized(BUILDER), controller.destroy)
.post("/api/automations/:id/trigger", controller.trigger)
.delete("/api/automations/:id/:rev", authorized(BUILDER), controller.destroy)
module.exports = router

View file

@ -9,7 +9,7 @@ const recordRoutes = require("./record")
const viewRoutes = require("./view")
const staticRoutes = require("./static")
const componentRoutes = require("./component")
const workflowRoutes = require("./workflow")
const automationRoutes = require("./automation")
const accesslevelRoutes = require("./accesslevel")
const deployRoutes = require("./deploy")
const apiKeysRoutes = require("./apikeys")
@ -27,7 +27,7 @@ module.exports = {
viewRoutes,
staticRoutes,
componentRoutes,
workflowRoutes,
automationRoutes,
accesslevelRoutes,
apiKeysRoutes,
}

View file

@ -14,9 +14,9 @@ const {
const { delay } = require("./testUtils")
const MAX_RETRIES = 4
const TEST_WORKFLOW = {
_id: "Test Workflow",
name: "My Workflow",
const TEST_AUTOMATION = {
_id: "Test Automation",
name: "My Automation",
pageId: "123123123",
screenId: "kasdkfldsafkl",
live: true,
@ -28,20 +28,20 @@ const TEST_WORKFLOW = {
steps: [
],
},
type: "workflow",
type: "automation",
}
let ACTION_DEFINITIONS = {}
let TRIGGER_DEFINITIONS = {}
let LOGIC_DEFINITIONS = {}
describe("/workflows", () => {
describe("/automations", () => {
let request
let server
let app
let instance
let workflow
let workflowId
let automation
let automationId
beforeAll(async () => {
({ request, server } = await supertest())
@ -50,7 +50,7 @@ describe("/workflows", () => {
})
beforeEach(async () => {
if (workflow) await destroyDocument(workflow.id)
if (automation) await destroyDocument(automation.id)
instance = await createInstance(request, app._id)
})
@ -58,18 +58,18 @@ describe("/workflows", () => {
server.close()
})
const createWorkflow = async () => {
workflow = await insertDocument(instance._id, {
type: "workflow",
...TEST_WORKFLOW
const createAutomation = async () => {
automation = await insertDocument(instance._id, {
type: "automation",
...TEST_AUTOMATION
})
workflow = { ...workflow, ...TEST_WORKFLOW }
automation = { ...automation, ...TEST_AUTOMATION }
}
describe("get definitions", () => {
it("returns a list of definitions for actions", async () => {
const res = await request
.get(`/api/workflows/action/list`)
.get(`/api/automations/action/list`)
.set(defaultHeaders(app._id, instance._id))
.expect('Content-Type', /json/)
.expect(200)
@ -80,7 +80,7 @@ describe("/workflows", () => {
it("returns a list of definitions for triggers", async () => {
const res = await request
.get(`/api/workflows/trigger/list`)
.get(`/api/automations/trigger/list`)
.set(defaultHeaders(app._id, instance._id))
.expect('Content-Type', /json/)
.expect(200)
@ -91,7 +91,7 @@ describe("/workflows", () => {
it("returns a list of definitions for actions", async () => {
const res = await request
.get(`/api/workflows/logic/list`)
.get(`/api/automations/logic/list`)
.set(defaultHeaders(app._id, instance._id))
.expect('Content-Type', /json/)
.expect(200)
@ -102,7 +102,7 @@ describe("/workflows", () => {
it("returns all of the definitions in one", async () => {
const res = await request
.get(`/api/workflows/definitions/list`)
.get(`/api/automations/definitions/list`)
.set(defaultHeaders(app._id, instance._id))
.expect('Content-Type', /json/)
.expect(200)
@ -114,7 +114,7 @@ describe("/workflows", () => {
})
describe("create", () => {
it("should setup the workflow fully", () => {
it("should setup the automation fully", () => {
let trigger = TRIGGER_DEFINITIONS["RECORD_SAVED"]
trigger.id = "wadiawdo34"
let saveAction = ACTION_DEFINITIONS["SAVE_RECORD"]
@ -124,51 +124,51 @@ describe("/workflows", () => {
}
saveAction.id = "awde444wk"
TEST_WORKFLOW.definition.steps.push(saveAction)
TEST_WORKFLOW.definition.trigger = trigger
TEST_AUTOMATION.definition.steps.push(saveAction)
TEST_AUTOMATION.definition.trigger = trigger
})
it("returns a success message when the workflow is successfully created", async () => {
it("returns a success message when the automation is successfully created", async () => {
const res = await request
.post(`/api/workflows`)
.post(`/api/automations`)
.set(defaultHeaders(app._id, instance._id))
.send(TEST_WORKFLOW)
.send(TEST_AUTOMATION)
.expect('Content-Type', /json/)
.expect(200)
expect(res.body.message).toEqual("Workflow created successfully")
expect(res.body.workflow.name).toEqual("My Workflow")
expect(res.body.workflow._id).not.toEqual(null)
workflowId = res.body.workflow._id
expect(res.body.message).toEqual("Automation created successfully")
expect(res.body.automation.name).toEqual("My Automation")
expect(res.body.automation._id).not.toEqual(null)
automationId = res.body.automation._id
})
it("should apply authorization to endpoint", async () => {
await builderEndpointShouldBlockNormalUsers({
request,
method: "POST",
url: `/api/workflows`,
url: `/api/automations`,
instanceId: instance._id,
appId: app._id,
body: TEST_WORKFLOW
body: TEST_AUTOMATION
})
})
})
describe("trigger", () => {
it("trigger the workflow successfully", async () => {
it("trigger the automation successfully", async () => {
let model = await createModel(request, app._id, instance._id)
TEST_WORKFLOW.definition.trigger.inputs.modelId = model._id
TEST_WORKFLOW.definition.steps[0].inputs.record.modelId = model._id
await createWorkflow()
TEST_AUTOMATION.definition.trigger.inputs.modelId = model._id
TEST_AUTOMATION.definition.steps[0].inputs.record.modelId = model._id
await createAutomation()
const res = await request
.post(`/api/workflows/${workflow._id}/trigger`)
.post(`/api/automations/${automation._id}/trigger`)
.send({ name: "Test" })
.set(defaultHeaders(app._id, instance._id))
.expect('Content-Type', /json/)
.expect(200)
expect(res.body.message).toEqual(`Workflow ${workflow._id} has been triggered.`)
expect(res.body.workflow.name).toEqual(TEST_WORKFLOW.name)
// wait for workflow to complete in background
expect(res.body.message).toEqual(`Automation ${automation._id} has been triggered.`)
expect(res.body.automation.name).toEqual(TEST_AUTOMATION.name)
// wait for automation to complete in background
for (let tries = 0; tries < MAX_RETRIES; tries++) {
let elements = await getAllFromModel(request, app._id, instance._id, model._id)
// don't test it unless there are values to test
@ -185,42 +185,42 @@ describe("/workflows", () => {
})
describe("update", () => {
it("updates a workflows data", async () => {
await createWorkflow()
workflow._id = workflow.id
workflow._rev = workflow.rev
workflow.name = "Updated Name"
workflow.type = "workflow"
it("updates a automations data", async () => {
await createAutomation()
automation._id = automation.id
automation._rev = automation.rev
automation.name = "Updated Name"
automation.type = "automation"
const res = await request
.put(`/api/workflows`)
.put(`/api/automations`)
.set(defaultHeaders(app._id, instance._id))
.send(workflow)
.send(automation)
.expect('Content-Type', /json/)
.expect(200)
expect(res.body.message).toEqual("Workflow Test Workflow updated successfully.")
expect(res.body.workflow.name).toEqual("Updated Name")
expect(res.body.message).toEqual("Automation Test Automation updated successfully.")
expect(res.body.automation.name).toEqual("Updated Name")
})
})
describe("fetch", () => {
it("return all the workflows for an instance", async () => {
await createWorkflow()
it("return all the automations for an instance", async () => {
await createAutomation()
const res = await request
.get(`/api/workflows`)
.get(`/api/automations`)
.set(defaultHeaders(app._id, instance._id))
.expect('Content-Type', /json/)
.expect(200)
expect(res.body[0]).toEqual(expect.objectContaining(TEST_WORKFLOW))
expect(res.body[0]).toEqual(expect.objectContaining(TEST_AUTOMATION))
})
it("should apply authorization to endpoint", async () => {
await builderEndpointShouldBlockNormalUsers({
request,
method: "GET",
url: `/api/workflows`,
url: `/api/automations`,
instanceId: instance._id,
appId: app._id,
})
@ -228,23 +228,23 @@ describe("/workflows", () => {
})
describe("destroy", () => {
it("deletes a workflow by its ID", async () => {
await createWorkflow()
it("deletes a automation by its ID", async () => {
await createAutomation()
const res = await request
.delete(`/api/workflows/${workflow.id}/${workflow.rev}`)
.delete(`/api/automations/${automation.id}/${automation.rev}`)
.set(defaultHeaders(app._id, instance._id))
.expect('Content-Type', /json/)
.expect(200)
expect(res.body.id).toEqual(TEST_WORKFLOW._id)
expect(res.body.id).toEqual(TEST_AUTOMATION._id)
})
it("should apply authorization to endpoint", async () => {
await createWorkflow()
await createAutomation()
await builderEndpointShouldBlockNormalUsers({
request,
method: "DELETE",
url: `/api/workflows/${workflow.id}/${workflow._rev}`,
url: `/api/automations/${automation.id}/${automation._rev}`,
instanceId: instance._id,
appId: app._id,
})

View file

@ -6,7 +6,7 @@ const http = require("http")
const api = require("./api")
const env = require("./environment")
const eventEmitter = require("./events")
const workflows = require("./workflows/index")
const automations = require("./automations/index")
const Sentry = require("@sentry/node")
const app = new Koa()
@ -50,5 +50,5 @@ process.on("SIGINT", () => process.exit(1))
module.exports = server.listen(env.PORT || 4001, () => {
console.log(`Budibase running on ${JSON.stringify(server.address())}`)
workflows.init()
automations.init()
})

View file

@ -21,11 +21,10 @@ function runWorker(job) {
* This module is built purely to kick off the worker farm and manage the inputs/outputs
*/
module.exports.init = function() {
triggers.workflowQueue.process(async job => {
triggers.automationQueue.process(async job => {
if (environment.BUDIBASE_ENVIRONMENT === "PRODUCTION") {
await runWorker(job)
} else {
console.log("Testing standard thread")
await singleThread(job)
}
})

View file

@ -4,7 +4,7 @@ module.exports.definition = {
name: "Delay",
icon: "ri-time-fill",
tagline: "Delay for {{inputs.time}} milliseconds",
description: "Delay the workflow until an amount of time has passed",
description: "Delay the automation until an amount of time has passed",
stepId: "DELAY",
inputs: {},
schema: {

View file

@ -16,7 +16,7 @@ module.exports.definition = {
name: "Filter",
tagline: "{{inputs.field}} {{inputs.condition}} {{inputs.value}}",
icon: "ri-git-branch-line",
description: "Filter any workflows which do not meet certain conditions",
description: "Filter any automations which do not meet certain conditions",
type: "LOGIC",
stepId: "FILTER",
inputs: {

View file

@ -45,18 +45,18 @@ function recurseMustache(inputs, context) {
}
/**
* The workflow orchestrator is a class responsible for executing workflows.
* It handles the context of the workflow and makes sure each step gets the correct
* The automation orchestrator is a class responsible for executing automations.
* It handles the context of the automation and makes sure each step gets the correct
* inputs and handles any outputs.
*/
class Orchestrator {
constructor(workflow, triggerOutput) {
constructor(automation, triggerOutput) {
this._instanceId = triggerOutput.instanceId
// remove from context
delete triggerOutput.instanceId
// step zero is never used as the mustache is zero indexed for customer facing
this._context = { steps: [{}], trigger: triggerOutput }
this._workflow = workflow
this._automation = automation
}
async getStepFunctionality(type, stepId) {
@ -67,14 +67,14 @@ class Orchestrator {
step = logic.getLogic(stepId)
}
if (step == null) {
throw `Cannot find workflow step by name ${stepId}`
throw `Cannot find automation step by name ${stepId}`
}
return step
}
async execute() {
let workflow = this._workflow
for (let step of workflow.definition.steps) {
let automation = this._automation
for (let step of automation.definition.steps) {
let stepFn = await this.getStepFunctionality(step.type, step.stepId)
step.inputs = recurseMustache(step.inputs, this._context)
// instanceId is always passed
@ -93,11 +93,11 @@ class Orchestrator {
// callback is required for worker-farm to state that the worker thread has completed
module.exports = async (job, cb = null) => {
try {
const workflowOrchestrator = new Orchestrator(
job.data.workflow,
const automationOrchestrator = new Orchestrator(
job.data.automation,
job.data.event
)
await workflowOrchestrator.execute()
await automationOrchestrator.execute()
if (cb) {
cb()
}

View file

@ -2,7 +2,7 @@ const CouchDB = require("../db")
const emitter = require("../events/index")
const InMemoryQueue = require("./queue/inMemoryQueue")
let workflowQueue = new InMemoryQueue()
let automationQueue = new InMemoryQueue()
const FAKE_STRING = "TEST"
const FAKE_BOOL = false
@ -76,28 +76,31 @@ const BUILTIN_DEFINITIONS = {
},
}
async function queueRelevantRecordWorkflows(event, eventType) {
async function queueRelevantRecordAutomations(event, eventType) {
if (event.instanceId == null) {
throw `No instanceId specified for ${eventType} - check event emitters.`
}
const db = new CouchDB(event.instanceId)
const workflowsToTrigger = await db.query("database/by_workflow_trigger", {
key: [eventType],
include_docs: true,
})
const automationsToTrigger = await db.query(
"database/by_automation_trigger",
{
key: [eventType],
include_docs: true,
}
)
const workflows = workflowsToTrigger.rows.map(wf => wf.doc)
for (let workflow of workflows) {
let workflowDef = workflow.definition
let workflowTrigger = workflowDef ? workflowDef.trigger : {}
const automations = automationsToTrigger.rows.map(wf => wf.doc)
for (let automation of automations) {
let automationDef = automation.definition
let automationTrigger = automationDef ? automationDef.trigger : {}
if (
!workflow.live ||
!workflowTrigger.inputs ||
workflowTrigger.inputs.modelId !== event.record.modelId
!automation.live ||
!automationTrigger.inputs ||
automationTrigger.inputs.modelId !== event.record.modelId
) {
continue
}
workflowQueue.add({ workflow, event })
automationQueue.add({ automation, event })
}
}
@ -105,18 +108,18 @@ emitter.on("record:save", async function(event) {
if (!event || !event.record || !event.record.modelId) {
return
}
await queueRelevantRecordWorkflows(event, "record:save")
await queueRelevantRecordAutomations(event, "record:save")
})
emitter.on("record:delete", async function(event) {
if (!event || !event.record || !event.record.modelId) {
return
}
await queueRelevantRecordWorkflows(event, "record:delete")
await queueRelevantRecordAutomations(event, "record:delete")
})
async function fillRecordOutput(workflow, params) {
let triggerSchema = workflow.definition.trigger
async function fillRecordOutput(automation, params) {
let triggerSchema = automation.definition.trigger
let modelId = triggerSchema.inputs.modelId
const db = new CouchDB(params.instanceId)
try {
@ -147,19 +150,19 @@ async function fillRecordOutput(workflow, params) {
return params
}
module.exports.externalTrigger = async function(workflow, params) {
module.exports.externalTrigger = async function(automation, params) {
// TODO: replace this with allowing user in builder to input values in future
if (
workflow.definition != null &&
workflow.definition.trigger != null &&
workflow.definition.trigger.inputs.modelId != null
automation.definition != null &&
automation.definition.trigger != null &&
automation.definition.trigger.inputs.modelId != null
) {
params = await fillRecordOutput(workflow, params)
params = await fillRecordOutput(automation, params)
}
workflowQueue.add({ workflow, event: params })
automationQueue.add({ automation, event: params })
}
module.exports.workflowQueue = workflowQueue
module.exports.automationQueue = automationQueue
module.exports.BUILTIN_DEFINITIONS = BUILTIN_DEFINITIONS

View file

@ -2,7 +2,7 @@ const EventEmitter = require("events").EventEmitter
/**
* keeping event emitter in one central location as it might be used for things other than
* workflows (what it was for originally) - having a central emitter will be useful in the
* automations (what it was for originally) - having a central emitter will be useful in the
* future.
*/

View file

@ -2,7 +2,7 @@
module.exports.READ_MODEL = "read-model"
module.exports.WRITE_MODEL = "write-model"
module.exports.READ_VIEW = "read-view"
module.exports.EXECUTE_WORKFLOW = "execute-workflow"
module.exports.EXECUTE_AUTOMATION = "execute-automation"
module.exports.USER_MANAGEMENT = "user-management"
module.exports.BUILDER = "builder"
module.exports.LIST_USERS = "list-users"

View file

@ -1,6 +1,6 @@
const viewController = require("../api/controllers/view")
const modelController = require("../api/controllers/model")
const workflowController = require("../api/controllers/workflow")
const automationController = require("../api/controllers/automation")
const accessLevels = require("./accessLevels")
// this has been broken out to reduce risk of circular dependency from utilities, no enums defined here
@ -26,13 +26,13 @@ const generatePowerUserPermissions = async instanceId => {
await viewController.fetch(fetchViewsCtx)
const views = fetchViewsCtx.body
const fetchWorkflowsCtx = {
const fetchAutomationsCtx = {
user: {
instanceId,
},
}
await workflowController.fetch(fetchWorkflowsCtx)
const workflows = fetchWorkflowsCtx.body
await automationController.fetch(fetchAutomationsCtx)
const automations = fetchAutomationsCtx.body
const readModelPermissions = models.map(m => ({
itemId: m._id,
@ -49,16 +49,16 @@ const generatePowerUserPermissions = async instanceId => {
name: accessLevels.READ_VIEW,
}))
const executeWorkflowPermissions = workflows.map(w => ({
const executeAutomationPermissions = automations.map(w => ({
itemId: w._id,
name: accessLevels.EXECUTE_WORKFLOW,
name: accessLevels.EXECUTE_AUTOMATION,
}))
return [
...readModelPermissions,
...writeModelPermissions,
...viewPermissions,
...executeWorkflowPermissions,
...executeAutomationPermissions,
{ name: accessLevels.LIST_USERS },
]
}