move workflow to array data structure

This commit is contained in:
Martin McKeaveney 2020-06-01 10:41:28 +01:00
parent 5c1aa00fd0
commit 15ba1a5a12
36 changed files with 257 additions and 232 deletions

View File

@ -152,7 +152,7 @@ export default {
{
find: "builderStore",
replacement: path.resolve(projectRootDir, "src/builderStore"),
}
},
],
customResolver,
}),

View File

@ -7,8 +7,7 @@
import AppNotification, {
showAppNotification,
} from "components/common/AppNotification.svelte"
import { NotificationDisplay } from '@beyonk/svelte-notifications'
import { NotificationDisplay } from "@beyonk/svelte-notifications"
function showErrorBanner() {
showAppNotification({

View File

@ -13,82 +13,109 @@ export default class Workflow {
}
isEmpty() {
return !this.workflow.definition.next
// return this.workflow.definition.next
return this.workflow.length > 0
}
addBlock(block) {
let node = this.workflow.definition
while (node.next) node = node.next
node.next = {
// Make sure to add trigger if doesn't exist
this.workflow.definition.steps.push({
id: generate(),
...block
}
...block,
})
}
updateBlock(updatedBlock, id) {
let block = this.workflow.definition
const { steps, trigger } = this.workflow.definition
while (block.id !== id) block = block.next
if (!block) throw new Error("Block not found.")
// if the block is a trigger do X
block = updatedBlock
// if step
const stepIdx = steps.findIndex(step => step.id === id)
// while (block.id !== id) block = block.next
if (stepIdx < 0) throw new Error("Block not found.")
steps.splice(stepIdx, 1, updatedBlock)
}
deleteBlock(id) {
let previous = null
let block = this.workflow.definition
const { steps, trigger } = this.workflow.definition
// iterate through the blocks
while (block.id !== id) {
previous = block
block = block.next
}
const stepIdx = steps.findIndex(step => step.id === id)
// delete the block matching your id
if (!block.next) {
delete previous.next
} else {
previous.next = block.next
}
if (stepIdx < 0) throw new Error("Block not found.")
steps.splice(stepIdx, 1)
}
createUiTree() {
if (!this.workflow.definition.next) return []
return Workflow.buildUiTree(this.workflow.definition.next)
if (!this.workflow.definition) return []
return Workflow.buildUiTree(this.workflow.definition)
}
static buildUiTree(block, tree = []) {
if (!block) return tree
static buildUiTree(definition) {
return definition.steps.map(step => {
// The client side display definition for the block
const definition = blockDefinitions[step.type][step.actionId]
if (!definition) {
throw new Error(
`No block definition exists for the chosen block. Check there's an entry in the block definitions for ${step.actionId}`
)
}
// The client side display definition for the block
const definition = blockDefinitions[block.type][block.actionId]
if (!definition) {
throw new Error(
`No block definition exists for the chosen block. Check there's an entry in the block definitions for ${block.actionId}`
)
}
if (!definition.params) {
throw new Error(
`Blocks should always have parameters. Ensure that the block definition is correct for ${step.actionId}`
)
}
if (!definition.params) {
throw new Error(
`Blocks should always have parameters. Ensure that the block definition is correct for ${block.actionId}`
)
}
const tagline = definition.tagline || ""
const args = step.args || {}
const tagline = definition.tagline || ""
const args = block.args || {}
// all the fields the workflow block needs to render in the UI
tree.push({
id: block.id,
type: block.type,
params: block.params,
args,
heading: block.actionId,
body: mustache.render(tagline, args),
name: definition.name
return {
id: step.id,
type: step.type,
params: step.params,
args,
heading: step.actionId,
body: mustache.render(tagline, args),
name: definition.name,
}
})
return this.buildUiTree(block.next, tree)
}
// static buildUiTree(block, tree = []) {
// if (!block) return tree
// // The client side display definition for the block
// const definition = blockDefinitions[block.type][block.actionId]
// if (!definition) {
// throw new Error(
// `No block definition exists for the chosen block. Check there's an entry in the block definitions for ${block.actionId}`
// )
// }
// if (!definition.params) {
// throw new Error(
// `Blocks should always have parameters. Ensure that the block definition is correct for ${block.actionId}`
// )
// }
// const tagline = definition.tagline || ""
// const args = block.args || {}
// // all the fields the workflow block needs to render in the UI
// tree.push({
// id: block.id,
// type: block.type,
// params: block.params,
// args,
// heading: block.actionId,
// body: mustache.render(tagline, args),
// name: definition.name
// })
// return this.buildUiTree(block.next, tree)
// }
}

View File

@ -13,7 +13,8 @@ const workflowActions = store => ({
})
},
create: async ({ instanceId, name }) => {
const workflow = { name, definition: {} }
// TODO: set these defaults in the backend
const workflow = { name, definition: { trigger: {}, steps: [] } }
const CREATE_WORKFLOW_URL = `/api/${instanceId}/workflows`
const response = await api.post(CREATE_WORKFLOW_URL, workflow)
const json = await response.json()

View File

@ -116,7 +116,7 @@
stylesheetLinks,
selectedComponentType,
selectedComponentId,
frontendDefinition: JSON.stringify(frontendDefinition)
frontendDefinition: JSON.stringify(frontendDefinition),
})} />
{/if}
</div>

View File

@ -17,14 +17,16 @@
export let onChange
let isOpen = false
</script>
<div class="handler-option">
<span>{parameter.name}</span>
<div class="handler-input">
{#if parameter.name === 'workflow'}
<select class="budibase__input" on:change={onChange} bind:value={parameter.value}>
<select
class="budibase__input"
on:change={onChange}
bind:value={parameter.value}>
{#each $workflowStore.workflows as workflow}
<option value={workflow._id}>{workflow.name}</option>
{/each}

View File

@ -1,6 +1,6 @@
<script>
import { store, backendUiStore, workflowStore } from "builderStore"
import { notifier } from '@beyonk/svelte-notifications'
import { notifier } from "@beyonk/svelte-notifications"
import api from "builderStore/api"
import ActionButton from "components/common/ActionButton.svelte"

View File

@ -1,6 +1,6 @@
<script>
import { store } from "builderStore"
import deepmerge from "deepmerge";
import deepmerge from "deepmerge"
export let value

View File

@ -1,4 +1,3 @@
<script>
import { backendUiStore } from "builderStore"

View File

@ -1,7 +1,7 @@
<script>
import { backendUiStore, store } from "builderStore"
import ComponentSelector from "./ParamInputs/ComponentSelector.svelte";
import ModelSelector from "./ParamInputs/ModelSelector.svelte";
import ComponentSelector from "./ParamInputs/ComponentSelector.svelte"
import ModelSelector from "./ParamInputs/ModelSelector.svelte"
export let workflowBlock
@ -12,9 +12,7 @@
: []
</script>
<label class="uk-form-label">
{workflowBlock.type}: {workflowBlock.name}
</label>
<label class="uk-form-label">{workflowBlock.type}: {workflowBlock.name}</label>
{#each workflowParams as [parameter, type]}
<div class="uk-margin block-field">
<label class="uk-form-label">{parameter}</label>

View File

@ -1,6 +1,6 @@
<script>
import FlowItem from "./FlowItem.svelte"
import Arrow from "./Arrow.svelte";
import Arrow from "./Arrow.svelte"
export let blocks = []
export let onSelect

View File

@ -12,7 +12,10 @@
$: definitions = Object.entries(blockDefinitions[selectedTab])
$: {
if (!$workflowStore.currentWorkflow.isEmpty() && selectedTab === "TRIGGER") {
if (
!$workflowStore.currentWorkflow.isEmpty() &&
selectedTab === "TRIGGER"
) {
selectedTab = "ACTION"
}
}

View File

@ -1,6 +1,6 @@
<script>
import { store, backendUiStore, workflowStore } from "builderStore"
import { notifier } from '@beyonk/svelte-notifications'
import { notifier } from "@beyonk/svelte-notifications"
import api from "builderStore/api"
import ActionButton from "components/common/ActionButton.svelte"

View File

@ -1,6 +1,6 @@
<script>
import Modal from "svelte-simple-modal"
import { notifier } from "@beyonk/svelte-notifications";
import { notifier } from "@beyonk/svelte-notifications"
import { onMount, getContext } from "svelte"
import { backendUiStore, workflowStore } from "builderStore"
import api from "builderStore/api"
@ -9,7 +9,8 @@
const { open, close } = getContext("simple-modal")
$: currentWorkflowId =
$workflowStore.currentWorkflow && $workflowStore.currentWorkflow.workflow._id
$workflowStore.currentWorkflow &&
$workflowStore.currentWorkflow.workflow._id
function newWorkflow() {
open(
@ -30,9 +31,9 @@
// TODO: Clean up args
await workflowStore.actions.save({
instanceId: $backendUiStore.selectedDatabase._id,
workflow
workflow,
})
notifier.success(`Workflow ${workflow.name} saved.`);
notifier.success(`Workflow ${workflow.name} saved.`)
}
</script>

View File

@ -94,7 +94,7 @@ const TRIGGER = {
description: "Fired when a record is deleted from your database.",
environment: "SERVER",
params: {
model: "model"
model: "model",
},
},
// CLICK: {
@ -138,10 +138,8 @@ const LOGIC = {
environment: "CLIENT",
params: {
filter: "string",
condition: [
"equals",
],
value: "string"
condition: ["equals"],
value: "string",
},
},
DELAY: {

View File

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

View File

@ -0,0 +1,29 @@
import { get } from "svelte/store"
import { setState } from "../../state/setState"
import { appStore } from "../../state/store"
const delay = ms => new Promise(resolve => setTimeout(resolve, ms))
export default {
SET_STATE: ({ context, args, id }) => {
// get props from the workflow context if required
setState(...Object.values(args))
// update the context with the data
context = {
...context,
[id]: args,
}
},
NAVIGATE: ({ context, args, id }) => {},
DELAY: async ({ context, args }) => await delay(args.time),
FILTER: (context, args) => {
const { field, condition, value } = args
switch (condition) {
case "equals":
if (field !== value) return
break
default:
return
}
},
}

View File

@ -3,7 +3,7 @@ import Orchestrator, { clientStrategy } from "./orchestrator"
export const triggerWorkflow = api => ({ workflow }) => {
const workflowOrchestrator = new Orchestrator(
api,
"inst_60dd510_700f7dc06735403e81d5af91072d7241"
"inst_ad75c7f_4f3e7d5d80a74b17a5187a18e2aba85e"
)
workflowOrchestrator.strategy = clientStrategy

View File

@ -1,7 +1,7 @@
import { get } from "svelte/store";
import { setState } from "../../state/setState";
import mustache from "mustache";
import { appStore } from "../../state/store";
import { get } from "svelte/store"
import mustache from "mustache"
import { appStore } from "../../state/store"
import clientActions from "./actions"
/**
* The workflow orchestrator is a class responsible for executing workflows.
@ -17,7 +17,7 @@ export default class Orchestrator {
}
set strategy(strategy) {
this._strategy = strategy({ api: this.api, instanceId: this.instanceId });
this._strategy = strategy({ api: this.api, instanceId: this.instanceId })
}
async execute(workflowId) {
@ -32,85 +32,58 @@ export default class Orchestrator {
// Execute a workflow from a running budibase app
export const clientStrategy = ({ api, instanceId }) => ({
delay: ms => new Promise(resolve => setTimeout(resolve, ms)),
context: {},
bindContextArgs: function(args) {
const mappedArgs = { ...args }
console.log("original args", args)
// bind the workflow action args to the workflow context, if required
for (let arg in args) {
const argValue = args[arg]
// We don't want to render mustache templates on non-strings
if (typeof argValue !== "string") continue
// Means that it's bound to state or workflow context
console.log(argValue, get(appStore));
mappedArgs[arg] = mustache.render(argValue, {
context: this.context,
state: get(appStore)
});
state: get(appStore),
})
}
console.log(mappedArgs)
return mappedArgs
},
run: async function(workflow) {
const block = workflow.next
for (let block of workflow.steps) {
console.log("Executing workflow block", block)
console.log("Executing workflow block", block)
// This code gets run in the browser
if (block.environment === "CLIENT") {
const action = clientActions[block.actionId]
await action({
context: this.context,
args: this.bindContextArgs(block.args),
id: block.id,
})
}
if (!block) return
// this workflow block gets executed on the server
if (block.environment === "SERVER") {
const EXECUTE_WORKFLOW_URL = `/api/${instanceId}/workflows/action`
const response = await api.post({
url: EXECUTE_WORKFLOW_URL,
body: {
action: block.actionId,
args: this.bindContextArgs(block.args, api),
},
})
// This code gets run in the browser
if (block.environment === "CLIENT") {
if (block.actionId === "SET_STATE") {
// get props from the workflow context if required
setState(...Object.values(this.bindContextArgs(block.args)))
// update the context with the data
this.context = {
...this.context,
SET_STATE: block.args,
[block.actionId]: response,
}
}
if (block.actionId === "NAVIGATE") {
}
if (block.actionId === "DELAY") {
await this.delay(block.args.time)
}
if (block.actionId === "FILTER") {
const { field, condition, value } = block.args;
switch (condition) {
case "equals":
if (field !== value) return;
break;
default:
return;
}
}
console.log("workflowContext", this.context)
}
// this workflow block gets executed on the server
if (block.environment === "SERVER") {
const EXECUTE_WORKFLOW_URL = `/api/${instanceId}/workflows/action`
const response = await api.post({
url: EXECUTE_WORKFLOW_URL,
body: {
action: block.actionId,
args: this.bindContextArgs(block.args, api),
},
})
this.context = {
...this.context,
[block.actionId]: response,
}
}
console.log("workflowContext", this.context)
await this.run(workflow.next)
},
})

View File

@ -8,7 +8,7 @@ export const createApp = ({
componentLibraries,
frontendDefinition,
user,
window
window,
}) => {
let routeTo
let currentUrl
@ -38,7 +38,7 @@ export const createApp = ({
routeTo = screenRouter({
screens: frontendDefinition.screens,
onScreenSelected,
appRootPath: frontendDefinition.appRootPath
appRootPath: frontendDefinition.appRootPath,
})
const fallbackPath = window.location.pathname.replace(
frontendDefinition.appRootPath,

View File

@ -39,7 +39,7 @@ export const loadBudibase = async opts => {
componentLibraries: componentLibraryModules,
frontendDefinition,
user,
window
window,
})
const route = _window.location

View File

@ -42,7 +42,7 @@ export const attachChildren = initialiseOpts => (htmlElement, options) => {
parentNode: treeNode,
ComponentConstructor,
htmlElement,
anchor
anchor,
})
for (let childNode of childNodesThisIteration) {

View File

@ -1,12 +1,12 @@
import { appStore } from "../state/store"
import mustache from "mustache";
import mustache from "mustache"
export const prepareRenderComponent = ({
ComponentConstructor,
htmlElement,
anchor,
props,
parentNode
parentNode,
}) => {
const parentContext = (parentNode && parentNode.context) || {}
@ -42,14 +42,16 @@ export const prepareRenderComponent = ({
// make this node listen to the store
if (thisNode.stateBound) {
const unsubscribe = appStore.subscribe(state => {
const storeBoundProps = { ...initialProps._bb.props };
const storeBoundProps = { ...initialProps._bb.props }
for (let prop in storeBoundProps) {
if (typeof storeBoundProps[prop] === "string") {
storeBoundProps[prop] = mustache.render(storeBoundProps[prop], { state });
storeBoundProps[prop] = mustache.render(storeBoundProps[prop], {
state,
})
}
}
thisNode.component.$set(storeBoundProps);
});
}
thisNode.component.$set(storeBoundProps)
})
thisNode.unsubscribe = unsubscribe
}
}

View File

@ -1,5 +1,5 @@
import regexparam from "regexparam"
import { routerStore } from "../state/store";
import { routerStore } from "../state/store"
import { initRouteStore } from "../state/store"
// TODO: refactor
@ -43,8 +43,8 @@ export const screenRouter = ({ screens, onScreenSelected, appRootPath }) => {
}
routerStore.update(state => {
state["##routeParams"] = params;
return state;
state["##routeParams"] = params
return state
})
const screenIndex = current !== -1 ? current : fallback

View File

@ -51,7 +51,7 @@ export const bbFactory = ({
componentLibraries,
treeNode,
onScreenSlotRendered,
setupState
setupState,
}
return {

View File

@ -1,7 +1,7 @@
import { setState } from "./setState"
import { getState } from "./getState"
import { isArray, isUndefined } from "lodash/fp"
import { appStore } from "./store";
import { appStore } from "./store"
import { createApi } from "../api"
@ -21,7 +21,7 @@ export const eventHandlers = (store, rootPath, routeTo) => {
const api = createApi({
rootPath,
setState,
getState: (path, fallback) => getState(path, fallback)
getState: (path, fallback) => getState(path, fallback),
})
const setStateHandler = ({ path, value }) => setState(path, value)
@ -29,7 +29,7 @@ export const eventHandlers = (store, rootPath, routeTo) => {
return {
"Set State": handler(["path", "value"], setStateHandler),
"Navigate To": handler(["url"], param => routeTo(param && param.url)),
"Trigger Workflow": handler(["workflow"], api.triggerWorkflow)
"Trigger Workflow": handler(["workflow"], api.triggerWorkflow),
}
}

View File

@ -1,10 +1,10 @@
// import { isUndefined, isObject } from "lodash/fp"
import { get } from "svelte/store";
import getOr from "lodash/fp/getOr";
import { appStore } from "./store";
import { get } from "svelte/store"
import getOr from "lodash/fp/getOr"
import { appStore } from "./store"
export const getState = (path, fallback) => {
if (!path || path.length === 0) return fallback
return getOr(fallback, path, get(appStore));
}
return getOr(fallback, path, get(appStore))
}

View File

@ -1,11 +1,11 @@
import set from "lodash/fp/set";
import { appStore } from "./store";
import set from "lodash/fp/set"
import { appStore } from "./store"
export const setState = (path, value) => {
if (!path || path.length === 0) return
appStore.update(state => {
state = set(path, value, state);
state = set(path, value, state)
return state
})
}

View File

@ -5,8 +5,8 @@ import {
} from "./eventHandlers"
import { bbFactory } from "./bbComponentApi"
import mustache from "mustache"
import { get } from "svelte/store";
import { appStore } from "./store";
import { get } from "svelte/store"
import { appStore } from "./store"
const doNothing = () => {}
doNothing.isPlaceholder = true
@ -55,9 +55,9 @@ export const createStateManager = ({
// TODO: remove
const unsubscribe = appStore.subscribe(state => {
console.log("store updated", state);
return state;
});
console.log("store updated", state)
return state
})
// const unsubscribe = store.subscribe(
// onStoreStateUpdated({
@ -84,20 +84,18 @@ const onStoreStateUpdated = ({
getCurrentState,
componentLibraries,
onScreenSlotRendered,
setupState
setupState,
}) => state => {
// fire the state update event to re-render anything bound to this
// setCurrentState(state)
// setCurrentState(state)
// attachChildren({
// componentLibraries,
// treeNode: createTreeNode(),
// onScreenSlotRendered,
// setupState,
// getCurrentState,
// })(document.querySelector("#app"), { hydrate: true, force: true })
// fire the state update event to re-render anything bound to this
// setCurrentState(state)
// setCurrentState(state)
// attachChildren({
// componentLibraries,
// treeNode: createTreeNode(),
// onScreenSlotRendered,
// setupState,
// getCurrentState,
// })(document.querySelector("#app"), { hydrate: true, force: true })
// // the original array gets changed by components' destroy()
// // so we make a clone and check if they are still in the original
// const nodesWithBoundChildren_clone = [...nodesWithCodeBoundChildren]
@ -161,19 +159,14 @@ const onStoreStateUpdated = ({
// node.component.$set(newProps)
// }
const _setup = ({
handlerTypes,
getCurrentState,
bb,
store
}) => node => {
const _setup = ({ handlerTypes, getCurrentState, bb, store }) => node => {
const props = node.props
const context = node.context || {}
const initialProps = { ...props }
// const storeBoundProps = []
const currentStoreState = get(appStore)
console.log("node", node);
console.log("node", node)
// console.log("node", node);
// console.log("nodeComponent", node.component);
@ -185,12 +178,12 @@ const _setup = ({
// const binding = parseBinding(propValue)
// TODO: better binding stuff
const isBound = typeof propValue === "string" && propValue.startsWith("{{");
const isBound = typeof propValue === "string" && propValue.startsWith("{{")
if (isBound) {
initialProps[propName] = mustache.render(propValue, {
state: currentStoreState,
context
context,
})
if (!node.stateBound) {
@ -230,10 +223,11 @@ const _setup = ({
const resolvedParams = {}
for (let paramName in handlerInfo.parameters) {
const paramValue = handlerInfo.parameters[paramName]
resolvedParams[paramName] = () => mustache.render(paramValue, {
state: getCurrentState(),
context,
})
resolvedParams[paramName] = () =>
mustache.render(paramValue, {
state: getCurrentState(),
context,
})
// const paramBinding = parseBinding(paramValue)
// if (!paramBinding) {
// resolvedParams[paramName] = () => paramValue

View File

@ -1,16 +1,9 @@
import { writable } from "svelte/store";
import { writable } from "svelte/store"
const appStore = writable({});
appStore.actions = {
const appStore = writable({})
appStore.actions = {}
};
const routerStore = writable({})
routerStore.actions = {}
const routerStore = writable({});
routerStore.actions = {
}
export {
appStore,
routerStore
}
export { appStore, routerStore }

View File

@ -182,4 +182,4 @@ const maketestlib = window => ({
set(opts.props)
opts.target.appendChild(node)
},
})
})

View File

@ -37,8 +37,8 @@ exports.create = async function(ctx) {
emit([trigger.event], trigger)
}
}
}.toString()
}
}.toString(),
},
},
})

View File

@ -47,7 +47,7 @@ exports.save = async function(ctx) {
ctx.eventEmitter.emit(`record:save`, {
record,
instanceId: ctx.params.instanceId
instanceId: ctx.params.instanceId,
})
ctx.body = record
ctx.status = 200

View File

@ -8,19 +8,19 @@ module.exports = async function createUser(user) {
instanceId: "inst_60dd510_700f7dc06735403e81d5af91072d7241",
},
request: {
body: user
body: user,
},
}
try {
const response = await userController.create(ctx)
return {
user: response
user: response,
}
} catch (err) {
console.error(err);
console.error(err)
return {
user: null
user: null,
}
}
}

View File

@ -1,27 +1,33 @@
const EventEmitter = require("events").EventEmitter
const CouchDB = require("../db");
const CouchDB = require("../db")
const emitter = new EventEmitter()
function determineWorkflowsToTrigger(instanceId, event) {
const db = new CouchDB(instanceId);
async function determineWorkflowsToTrigger(instanceId, event) {
const db = new CouchDB(instanceId)
const workflowsToTrigger = await db.query("database/by_workflow_trigger", {
key: [event]
key: [event],
})
return workflowsToTrigger.rows;
return workflowsToTrigger.rows
}
emitter.on("record:save", async function(event) {
const workflowsToTrigger = await determineWorkflowsToTrigger(instanceId, "record:save")
const workflowsToTrigger = await determineWorkflowsToTrigger(
instanceId,
"record:save"
)
for (let workflow of workflowsToTrigger) {
// SERVER SIDE STUFF!!
}
})
emitter.on("record:delete", function(event) {
const workflowsToTrigger = await determineWorkflowsToTrigger(instanceId, "record:delete")
emitter.on("record:delete", async function(event) {
const workflowsToTrigger = await determineWorkflowsToTrigger(
instanceId,
"record:delete"
)
for (let workflow of workflowsToTrigger) {
// SERVER SIDE STUFF!!

View File

@ -17,7 +17,7 @@ const WORKFLOW_SCHEMA = {
type: "object",
properties: {
triggers: { type: "array" },
steps: { type: "array" }
steps: { type: "array" },
// next: {
// type: "object",
// properties: {