Merge pull request #626 from Budibase/rename-workflow-automation
Rename workflow -> automation
This commit is contained in:
commit
91edce8e9d
|
@ -4,6 +4,6 @@ node_modules_win
|
|||
package-lock.json
|
||||
release/
|
||||
dist/
|
||||
cypress/screenshots
|
||||
cypress/videos
|
||||
routify
|
||||
cypress/videos
|
||||
cypress/screenshots
|
||||
|
|
|
@ -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")
|
||||
})
|
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
|
@ -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 {
|
||||
|
|
|
@ -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
|
||||
}
|
||||
}
|
|
@ -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
|
||||
}
|
|
@ -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
|
||||
)
|
||||
})
|
||||
})
|
|
@ -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",
|
|
@ -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
|
||||
}
|
|
@ -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
|
||||
)
|
||||
})
|
||||
})
|
|
@ -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}
|
Before Width: | Height: | Size: 290 B After Width: | Height: | Size: 290 B |
|
@ -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>
|
|
@ -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>
|
||||
|
|
@ -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);
|
||||
}
|
||||
|
|
@ -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>
|
|
@ -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;
|
||||
}
|
||||
|
|
@ -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;
|
||||
}
|
||||
|
|
@ -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>
|
|
@ -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"
|
|
@ -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">
|
|
@ -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>
|
|
@ -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}
|
|
@ -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"
|
|
@ -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)
|
||||
}
|
||||
|
|
|
@ -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'}
|
||||
|
|
|
@ -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"
|
|
@ -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"
|
|
@ -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))
|
||||
|
|
|
@ -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 />
|
|
@ -0,0 +1,5 @@
|
|||
<script>
|
||||
import { AutomationBuilder } from "components/automation"
|
||||
</script>
|
||||
|
||||
<AutomationBuilder />
|
|
@ -1,3 +1,4 @@
|
|||
<!-- routify:options index=1 -->
|
||||
<script>
|
||||
import { getContext } from "svelte"
|
||||
import { store, backendUiStore } from "builderStore"
|
||||
|
|
|
@ -1,3 +1,4 @@
|
|||
<!-- routify:options index=1 -->
|
||||
<script>
|
||||
import { store, backendUiStore } from "builderStore"
|
||||
import { goto } from "@sveltech/routify"
|
||||
|
|
|
@ -1,5 +0,0 @@
|
|||
<script>
|
||||
import { WorkflowBuilder } from "components/workflow"
|
||||
</script>
|
||||
|
||||
<WorkflowBuilder />
|
|
@ -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,
|
||||
}
|
||||
}
|
|
@ -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)
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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())
|
||||
|
|
|
@ -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
|
|
@ -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,
|
||||
}
|
||||
|
|
|
@ -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,
|
||||
})
|
|
@ -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()
|
||||
})
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
})
|
|
@ -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: {
|
|
@ -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: {
|
|
@ -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()
|
||||
}
|
|
@ -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", {
|
||||
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
|
|
@ -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.
|
||||
*/
|
||||
|
||||
|
|
|
@ -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"
|
||||
|
|
|
@ -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 },
|
||||
]
|
||||
}
|
||||
|
|
Loading…
Reference in New Issue