mirror of https://github.com/Budibase/budibase.git
72 changed files with 527 additions and 521 deletions
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,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,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 |
@ -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" |
|||
@ -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,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,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,5 +0,0 @@ |
|||
<script> |
|||
import { WorkflowBuilder } from "components/workflow" |
|||
</script> |
|||
|
|||
<WorkflowBuilder /> |
|||
Loading…
Reference in new issue