make alerts live, more hooks, app notifications

This commit is contained in:
Martin McKeaveney 2020-05-28 23:31:55 +01:00
parent 6f0a84dd38
commit 7a3b368399
22 changed files with 208 additions and 92 deletions

View File

@ -38,6 +38,7 @@
] ]
}, },
"dependencies": { "dependencies": {
"@beyonk/svelte-notifications": "^2.0.3",
"@budibase/client": "^0.0.32", "@budibase/client": "^0.0.32",
"@nx-js/compiler-util": "^2.0.0", "@nx-js/compiler-util": "^2.0.0",
"codemirror": "^5.51.0", "codemirror": "^5.51.0",

View File

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

View File

@ -7,6 +7,8 @@
import AppNotification, { import AppNotification, {
showAppNotification, showAppNotification,
} from "components/common/AppNotification.svelte" } from "components/common/AppNotification.svelte"
import { NotificationDisplay } from '@beyonk/svelte-notifications'
function showErrorBanner() { function showErrorBanner() {
showAppNotification({ showAppNotification({
@ -24,8 +26,7 @@
$basepath = "/_builder" $basepath = "/_builder"
</script> </script>
<AppNotification /> <NotificationDisplay />
<Modal> <Modal>
<Router {routes} /> <Router {routes} />
</Modal> </Modal>

View File

@ -77,7 +77,8 @@
} }
.budibase__input { .budibase__input {
width: 250px; width: 100%;
max-width: 250px;
height: 35px; height: 35px;
border-radius: 3px; border-radius: 3px;
border: 1px solid #DBDBDB; border: 1px solid #DBDBDB;

View File

@ -1,6 +1,7 @@
import mustache from "mustache" import mustache from "mustache"
// TODO: tidy up import // TODO: tidy up import
import blockDefinitions from "../../../pages/[application]/workflow/WorkflowPanel/blockDefinitions" import blockDefinitions from "../../../pages/[application]/workflow/WorkflowPanel/blockDefinitions"
import { generate } from "shortid"
/** /**
* Class responsible for the traversing of the workflow definition. * Class responsible for the traversing of the workflow definition.
@ -14,7 +15,10 @@ export default class Workflow {
addBlock(block) { addBlock(block) {
let node = this.workflow.definition let node = this.workflow.definition
while (node.next) node = node.next while (node.next) node = node.next
node.next = block node.next = {
id: generate(),
...block
}
} }
updateBlock(updatedBlock, id) { updateBlock(updatedBlock, id) {
@ -70,7 +74,7 @@ export default class Workflow {
type: block.type, type: block.type,
params: block.params, params: block.params,
args, args,
heading: block.actionId, heading: definition.actionId,
body: mustache.render(tagline, args), body: mustache.render(tagline, args),
}) })

View File

@ -82,7 +82,7 @@ const workflowActions = store => ({
}, },
deleteWorkflowBlock: block => { deleteWorkflowBlock: block => {
store.update(state => { store.update(state => {
state.currentWorkflow.deleteBlock(block._id) state.currentWorkflow.deleteBlock(block.id)
state.selectedWorkflowBlock = null state.selectedWorkflowBlock = null
return state return state
}) })

View File

@ -32,6 +32,7 @@
$: { $: {
events = Object.keys(component) events = Object.keys(component)
// TODO: use real events
.filter(propName => ["onChange", "onClick", "onLoad"].includes(propName)) .filter(propName => ["onChange", "onClick", "onLoad"].includes(propName))
.map(propName => ({ .map(propName => ({
name: propName, name: propName,

View File

@ -9,7 +9,7 @@
EVENT_TYPE_MEMBER_NAME, EVENT_TYPE_MEMBER_NAME,
allHandlers, allHandlers,
} from "components/common/eventHandlers" } from "components/common/eventHandlers"
import { store } from "builderStore" import { store, workflowStore } from "builderStore"
import StateBindingOptions from "../PropertyCascader/StateBindingOptions.svelte" import StateBindingOptions from "../PropertyCascader/StateBindingOptions.svelte"
import { ArrowDownIcon } from "components/common/Icons/" import { ArrowDownIcon } from "components/common/Icons/"
@ -22,18 +22,26 @@
<div class="handler-option"> <div class="handler-option">
<span>{parameter.name}</span> <span>{parameter.name}</span>
<div class="handler-input"> <div class="handler-input">
<Input on:change={onChange} value={parameter.value} /> {#if parameter.name === 'workflow'}
<button on:click={() => (isOpen = !isOpen)}> <select class="budibase__input" {onChange} value={parameter.value}>
<div class="icon" style={`transform: rotate(${isOpen ? 0 : 90}deg);`}> {#each $workflowStore.workflows as workflow}
<ArrowDownIcon size={36} /> <option value={workflow._id}>{workflow.name}</option>
</div> {/each}
</button> </select>
{#if isOpen} {:else}
<StateBindingOptions <Input on:change={onChange} value={parameter.value} />
onSelect={option => { <button on:click={() => (isOpen = !isOpen)}>
onChange(option) <div class="icon" style={`transform: rotate(${isOpen ? 0 : 90}deg);`}>
isOpen = false <ArrowDownIcon size={36} />
}} /> </div>
</button>
{#if isOpen}
<StateBindingOptions
onSelect={option => {
onChange(option)
isOpen = false
}} />
{/if}
{/if} {/if}
</div> </div>
</div> </div>

View File

@ -56,11 +56,10 @@
on:click={() => $goto(`/settings`)}> on:click={() => $goto(`/settings`)}>
<SettingsIcon /> <SettingsIcon />
</span> </span>
<span <span class:active={false} class="topnavitemright">
class:active={false} <a href={`/${application}`} target="_blank">
class="topnavitemright" <PreviewIcon />
on:click={() => (location = `/${application}`)}> </a>
<PreviewIcon />
</span> </span>
</div> </div>
</div> </div>
@ -84,6 +83,11 @@
flex-direction: column; flex-direction: column;
} }
a {
text-transform: none;
color: var(--ink-lighter);
}
.top-nav { .top-nav {
flex: 0 0 auto; flex: 0 0 auto;
height: 60px; height: 60px;

View File

@ -1,5 +1,6 @@
<script> <script>
import { store, backendUiStore, workflowStore } from "builderStore" import { store, backendUiStore, workflowStore } from "builderStore"
import { notifier } from '@beyonk/svelte-notifications'
import api from "builderStore/api" import api from "builderStore/api"
import ActionButton from "components/common/ActionButton.svelte" import ActionButton from "components/common/ActionButton.svelte"
@ -16,6 +17,7 @@
workflow: $workflowStore.currentWorkflow.workflow, workflow: $workflowStore.currentWorkflow.workflow,
}) })
onClosed() onClosed()
notifier.danger("Workflow deleted.")
} }
</script> </script>

View File

@ -1,13 +1,14 @@
<script> <script>
import { onMount, getContext } from "svelte" import { onMount, getContext } from "svelte"
import { backendUiStore, workflowStore } from "builderStore" import { backendUiStore, workflowStore } from "builderStore"
import { notifier } from "@beyonk/svelte-notifications";
import api from "builderStore/api" import api from "builderStore/api"
import WorkflowBlockSetup from "./WorkflowBlockSetup.svelte" import WorkflowBlockSetup from "./WorkflowBlockSetup.svelte"
import DeleteWorkflowModal from "./DeleteWorkflowModal.svelte" import DeleteWorkflowModal from "./DeleteWorkflowModal.svelte"
const { open, close } = getContext("simple-modal") const { open, close } = getContext("simple-modal")
$: workflow = $workflowStore.currentWorkflow $: workflow = $workflowStore.currentWorkflow && $workflowStore.currentWorkflow.workflow
$: workflowBlock = $workflowStore.selectedWorkflowBlock $: workflowBlock = $workflowStore.selectedWorkflowBlock
function deleteWorkflow() { function deleteWorkflow() {
@ -21,8 +22,8 @@
} }
function deleteWorkflowBlock() { function deleteWorkflowBlock() {
// TODO: implement, need to put IDs against workflow blocks
workflowStore.actions.deleteWorkflowBlock(workflowBlock) workflowStore.actions.deleteWorkflowBlock(workflowBlock)
notifier.info("Workflow block deleted.");
} }
</script> </script>

View File

@ -1,5 +1,5 @@
<script> <script>
import { backendUiStore } from "builderStore" import { backendUiStore, store } from "builderStore"
export let workflowBlock export let workflowBlock
@ -8,6 +8,7 @@
$: workflowParams = workflowBlock.params $: workflowParams = workflowBlock.params
? Object.entries(workflowBlock.params) ? Object.entries(workflowBlock.params)
: [] : []
$: components = Object.values($store.components).filter(comp => comp.name)
// $: workflowArgs = workflowBlock.args ? Object.keys(workflowBlock.args) : [] // $: workflowArgs = workflowBlock.args ? Object.keys(workflowBlock.args) : []
</script> </script>
@ -18,7 +19,15 @@
<div class="uk-margin block-field"> <div class="uk-margin block-field">
<label class="uk-form-label">{parameter}</label> <label class="uk-form-label">{parameter}</label>
<div class="uk-form-controls"> <div class="uk-form-controls">
{#if type === 'number'} {#if Array.isArray(type)}
<select
class="budibase__input"
bind:value={workflowBlock.args[parameter]}>
{#each type as option}
<option value={option}>{option}</option>
{/each}
</select>
{:else if type === 'number'}
<input <input
type="number" type="number"
class="budibase__input" class="budibase__input"
@ -32,11 +41,11 @@
{/each} {/each}
</select> </select>
{:else if type === 'component'} {:else if type === 'component'}
<!-- <select> <select class="budibase__input">
{#each $store.components as question} {#each components as component}
<option value={question}>{question.text}</option> <option value={component.id}>{component.name}</option>
{/each} {/each}
</select> --> </select>
{:else if type === 'string'} {:else if type === 'string'}
<input <input
type="text" type="text"

View File

@ -1,17 +1,23 @@
<script> <script>
import { onMount } from "svelte" import { onMount } from "svelte"
import { workflowStore, backendUiStore } from "builderStore" import { workflowStore, backendUiStore } from "builderStore"
import { notifier } from "@beyonk/svelte-notifications"
import Flowchart from "./svelte-flows/Flowchart.svelte" import Flowchart from "./svelte-flows/Flowchart.svelte"
import api from "builderStore/api" import api from "builderStore/api"
let canvas let selectedWorkflow
let workflow
let uiTree let uiTree
let instanceId = $backendUiStore.selectedDatabase._id let instanceId = $backendUiStore.selectedDatabase._id
$: workflow = $workflowStore.currentWorkflow // TODO: better naming
$: selectedWorkflow = $workflowStore.currentWorkflow
$: if (workflow) uiTree = workflow ? workflow.createUiTree() : [] $: workflowLive = selectedWorkflow && selectedWorkflow.workflow.live
$: if (selectedWorkflow)
uiTree = selectedWorkflow ? selectedWorkflow.createUiTree() : []
$: instanceId = $backendUiStore.selectedDatabase._id
function onDelete(block) { function onDelete(block) {
// TODO finish // TODO finish
@ -24,17 +30,37 @@
return state return state
}) })
} }
function setWorkflowLive(live) {
const { workflow } = selectedWorkflow
workflow.live = live
workflowStore.actions.save({ instanceId, workflow })
if (live) {
notifier.info(`Workflow ${workflow.name} enabled.`)
} else {
notifier.danger(`Workflow ${workflow.name} disabled.`)
}
}
</script> </script>
<section> <section>
<Flowchart blocks={uiTree} {onSelect} on:delete={onDelete} /> <Flowchart blocks={uiTree} {onSelect} on:delete={onDelete} />
<footer> <footer>
<button class="stop-button hoverable"> {#if selectedWorkflow}
<i class="ri-stop-fill" /> <button
</button> class:highlighted={workflowLive}
<button class="play-button hoverable"> class:hoverable={workflowLive}
<i class="ri-play-fill" /> class="stop-button hoverable">
</button> <i class="ri-stop-fill" on:click={() => setWorkflowLive(false)} />
</button>
<button
class:highlighted={!workflowLive}
class:hoverable={!workflowLive}
class="play-button hoverable"
on:click={() => setWorkflowLive(true)}>
<i class="ri-play-fill" />
</button>
{/if}
</footer> </footer>
</section> </section>
@ -61,11 +87,11 @@
margin-right: 24px; margin-right: 24px;
} }
.play-button:hover { .play-button.highlighted {
background: var(--primary); background: var(--primary);
} }
.stop-button:hover { .stop-button.highlighted {
background: var(--coral); background: var(--coral);
} }
</style> </style>

View File

@ -0,0 +1,9 @@
<svg
width="9"
height="75"
viewBox="0 0 9 75"
fill="none"
xmlns="http://www.w3.org/2000/svg">
<path d="M5.0625 70H9L4.5 75L0 70H3.9375V65H5.0625V70Z" fill="#ADAEC4" />
<rect x="4" width="1" height="65" fill="#ADAEC4" />
</svg>

After

Width:  |  Height:  |  Size: 241 B

View File

@ -1,5 +1,6 @@
<script> <script>
import FlowItem from "./FlowItem.svelte" import FlowItem from "./FlowItem.svelte"
import Arrow from "./Arrow.svelte";
export let blocks = [] export let blocks = []
export let onSelect export let onSelect
@ -9,7 +10,7 @@
{#each blocks as block, idx} {#each blocks as block, idx}
<FlowItem {onSelect} {block} /> <FlowItem {onSelect} {block} />
{#if idx !== blocks.length - 1} {#if idx !== blocks.length - 1}
<i class="ri-arrow-down-line" /> <Arrow />
{/if} {/if}
{/each} {/each}
</section> </section>

View File

@ -1,16 +1,27 @@
<script> <script>
import { fade } from "svelte/transition"
export let onSelect export let onSelect
export let block export let block
function selectBlock() { function selectBlock() {
onSelect(block) onSelect(block)
} }
console.log(block)
</script> </script>
<div class={`${block.type} hoverable`} on:click={selectBlock}> <div transition:fade class={`${block.type} hoverable`} on:click={selectBlock}>
<header>{block.heading}</header> <header>
{#if block.type === 'TRIGGER'}
<i class="ri-lightbulb-fill" />
When this happens...
{:else if block.type === 'ACTION'}
<i class="ri-flashlight-fill" />
Do this...
{:else if block.type === 'LOGIC'}
<i class="ri-pause-fill" />
Only continue if...
{/if}
</header>
<hr /> <hr />
<p> <p>
{@html block.body} {@html block.body}
@ -21,7 +32,6 @@
div { div {
width: 320px; width: 320px;
padding: 20px; padding: 20px;
margin-bottom: 60px;
border-radius: 5px; border-radius: 5px;
transition: 0.3s all; transition: 0.3s all;
box-shadow: 0 4px 30px 0 rgba(57, 60, 68, 0.08); box-shadow: 0 4px 30px 0 rgba(57, 60, 68, 0.08);
@ -30,6 +40,18 @@
color: var(--white); color: var(--white);
} }
header {
font-size: 16px;
font-weight: 500;
display: flex;
align-items: center;
}
header i {
font-size: 20px;
margin-right: 5px;
}
.ACTION { .ACTION {
background-color: var(--white); background-color: var(--white);
color: var(--font); color: var(--font);

View File

@ -52,16 +52,21 @@
grid-gap: 5px; grid-gap: 5px;
grid-auto-flow: column; grid-auto-flow: column;
grid-auto-columns: 1fr 1fr 1fr; grid-auto-columns: 1fr 1fr 1fr;
margin-bottom: 10px;
} }
.subtabs span { .subtabs span {
transition: 0.3s all;
text-align: center; text-align: center;
color: var(--font); color: var(--dark-grey);
font-weight: 500; font-weight: 500;
padding: 10px;
} }
.subtabs span.selected { .subtabs span.selected {
border-bottom: 4px solid var(--primary); background: var(--dark-grey);
color: var(--white);
border-radius: 2px;
} }
.subtabs span:not(.selected) { .subtabs span:not(.selected) {

View File

@ -1,5 +1,6 @@
<script> <script>
import { store, backendUiStore, workflowStore } from "builderStore" import { store, backendUiStore, workflowStore } from "builderStore"
import { notifier } from '@beyonk/svelte-notifications'
import api from "builderStore/api" import api from "builderStore/api"
import ActionButton from "components/common/ActionButton.svelte" import ActionButton from "components/common/ActionButton.svelte"
@ -17,6 +18,7 @@
instanceId, instanceId,
}) })
onClosed() onClosed()
notifier.success(`Workflow ${name} created.`)
} }
</script> </script>

View File

@ -1,5 +1,6 @@
<script> <script>
import Modal from "svelte-simple-modal" import Modal from "svelte-simple-modal"
import { notifier } from "@beyonk/svelte-notifications";
import { onMount, getContext } from "svelte" import { onMount, getContext } from "svelte"
import { backendUiStore, workflowStore } from "builderStore" import { backendUiStore, workflowStore } from "builderStore"
import api from "builderStore/api" import api from "builderStore/api"
@ -8,7 +9,7 @@
const { open, close } = getContext("simple-modal") const { open, close } = getContext("simple-modal")
$: currentWorkflowId = $: currentWorkflowId =
$workflowStore.currentWorkflow && $workflowStore.currentWorkflow._id $workflowStore.currentWorkflow && $workflowStore.currentWorkflow.workflow._id
function newWorkflow() { function newWorkflow() {
open( open(
@ -24,12 +25,14 @@
workflowStore.actions.fetch($backendUiStore.selectedDatabase._id) workflowStore.actions.fetch($backendUiStore.selectedDatabase._id)
}) })
function saveWorkflow() { async function saveWorkflow() {
const workflow = $workflowStore.currentWorkflow.workflow
// TODO: Clean up args // TODO: Clean up args
workflowStore.actions.save({ await workflowStore.actions.save({
instanceId: $backendUiStore.selectedDatabase._id, instanceId: $backendUiStore.selectedDatabase._id,
workflow: $workflowStore.currentWorkflow.workflow, workflow
}) })
notifier.success(`Workflow ${workflow.name} saved.`);
} }
</script> </script>
@ -82,7 +85,7 @@
display: flex; display: flex;
align-items: center; align-items: center;
border-radius: 3px; border-radius: 3px;
height: 40px; height: 32px;
font-weight: 500; font-weight: 500;
} }

View File

@ -5,17 +5,6 @@
import api from "builderStore/api" import api from "builderStore/api"
import blockDefinitions from "./blockDefinitions" import blockDefinitions from "./blockDefinitions"
const WORKFLOW_TABS = [
{
name: "Workflows",
key: "WORKFLOWS",
},
{
name: "Add",
key: "ADD",
},
]
let selectedTab = "WORKFLOWS" let selectedTab = "WORKFLOWS"
let definitions = [] let definitions = []
</script> </script>
@ -57,15 +46,4 @@
span:not(.selected) { span:not(.selected) {
color: var(--dark-grey); color: var(--dark-grey);
} }
.delete-workflow-button {
font-family: Roboto;
width: 100%;
border: solid 1px #f2f2f2;
border-radius: 2px;
background: var(--white);
height: 32px;
font-size: 12px;
font-weight: 500;
}
</style> </style>

View File

@ -2,7 +2,7 @@ const ACTION = {
SET_STATE: { SET_STATE: {
name: "Update UI State", name: "Update UI State",
tagline: "Update <b>{{path}}</b> to <b>{{value}}</b>", tagline: "Update <b>{{path}}</b> to <b>{{value}}</b>",
icon: "", icon: "ri-refresh-line",
description: "Update your User Interface with some data.", description: "Update your User Interface with some data.",
environment: "CLIENT", environment: "CLIENT",
params: { params: {
@ -38,8 +38,8 @@ const ACTION = {
}, },
}, },
FIND_RECORD: { FIND_RECORD: {
description: "Delete a record from your database.", description: "Find a record in your database.",
icon: "ri-delete-bin-line", icon: "ri-search-line",
name: "Find Record", name: "Find Record",
environment: "SERVER", environment: "SERVER",
params: { params: {
@ -59,7 +59,7 @@ const ACTION = {
}, },
SEND_EMAIL: { SEND_EMAIL: {
description: "Send an email.", description: "Send an email.",
tagline: "Send email to {{to}}", tagline: "Send email to <b>{{to}}</b>",
icon: "ri-mail-open-fill", icon: "ri-mail-open-fill",
name: "Send Email", name: "Send Email",
environment: "SERVER", environment: "SERVER",
@ -73,9 +73,9 @@ const ACTION = {
} }
const TRIGGER = { const TRIGGER = {
SAVE_RECORD: { RECORD_SAVED: {
name: "Record Saved", name: "Record Saved",
icon: "ri-delete-bin-line", icon: "ri-save-line",
tagline: "Record is added to {{model}}", tagline: "Record is added to {{model}}",
description: "Save a record to your database.", description: "Save a record to your database.",
environment: "SERVER", environment: "SERVER",
@ -83,39 +83,72 @@ const TRIGGER = {
model: "model", model: "model",
}, },
}, },
RECORD_DELETED: {
name: "Record Deleted",
icon: "ri-delete-bin-line",
tagline: "Record is deleted from <b>{{model}}</b>",
description: "Fired when a record is deleted from your database.",
environment: "SERVER",
params: {
model: "model"
},
},
CLICK: { CLICK: {
name: "Click", name: "Click",
icon: "ri-cursor-line", icon: "ri-cursor-line",
tagline: "{{component}} is clicked",
description: "Trigger when you click on an element in the UI.", description: "Trigger when you click on an element in the UI.",
environment: "CLIENT",
params: {
component: "component"
}
}, },
LOAD: { LOAD: {
name: "Load", name: "Load",
icon: "ri-loader-line", icon: "ri-loader-line",
tagline: "{{component}} is loaded",
description: "Trigger an element has finished loading.", description: "Trigger an element has finished loading.",
environment: "CLIENT",
params: {
component: "component"
}
}, },
INPUT: { INPUT: {
name: "Input", name: "Input",
icon: "ri-text", icon: "ri-text",
description: "Trigger when you environment into an input box.", tagline: "Text entered into {{component}",
description: "Trigger when you type into an input box.",
environment: "CLIENT",
params: {
component: "component"
}
}, },
} }
const LOGIC = { const LOGIC = {
FILTER: { FILTER: {
name: "Filter", name: "Filter",
tagline: "{{key}} {{condition}} {{value}}", tagline: "{{field}} <b>{{condition}}</b> {{value}}",
icon: "ri-git-branch-line", icon: "ri-git-branch-line",
description: "Filter any workflows which do not meet certain conditions.", description: "Filter any workflows which do not meet certain conditions.",
environment: "CLIENT", environment: "CLIENT",
params: { params: {
if: "string", field: "string",
condition: [
"equals"
],
value: "string"
}, },
}, },
DELAY: { DELAY: {
name: "Delay", name: "Delay",
icon: "ri-git-branch-line", icon: "ri-time-fill",
tagline: "Delay for <b>{{time}}</b> milliseconds",
description: "Delay the workflow until an amount of time has passed.", description: "Delay the workflow until an amount of time has passed.",
environment: "CLIENT", environment: "CLIENT",
params: {
time: "number",
},
}, },
} }

View File

@ -1,7 +1,7 @@
import get from "lodash/fp/get" import get from "lodash/fp/get"
/** /**
* The workflow orhestrator is a class responsible for executing workflows. * The workflow orchestrator is a class responsible for executing workflows.
* It relies on the strategy pattern, which allows composable behaviour to be * It relies on the strategy pattern, which allows composable behaviour to be
* passed into its execute() function. This allows custom execution behaviour based * passed into its execute() function. This allows custom execution behaviour based
* on where the orchestrator is run. * on where the orchestrator is run.
@ -30,6 +30,7 @@ export default class Orchestrator {
// Execute a workflow from a running budibase app // Execute a workflow from a running budibase app
export const clientStrategy = { export const clientStrategy = {
delay: ms => new Promise(resolve => setTimeout(resolve, ms)),
context: {}, context: {},
bindContextArgs: function(args, api) { bindContextArgs: function(args, api) {
const mappedArgs = { ...args } const mappedArgs = { ...args }
@ -80,6 +81,10 @@ export const clientStrategy = {
SET_STATE: block.args, SET_STATE: block.args,
} }
} }
if (block.actionId === "DELAY") {
await this.delay(block.args.time)
}
} }
// this workflow block gets executed on the server // this workflow block gets executed on the server
@ -102,6 +107,6 @@ export const clientStrategy = {
console.log("workflowContext", this.context) console.log("workflowContext", this.context)
// TODO: clean this up, don't pass all those args // TODO: clean this up, don't pass all those args
this.run({ workflow: workflow.next, instanceId, api }) await this.run({ workflow: workflow.next, instanceId, api })
}, },
} }