Rewrite workflow editing state for better UI sync
This commit is contained in:
parent
b886c8f342
commit
494d38029f
|
@ -1,4 +1,3 @@
|
|||
import mustache from "mustache"
|
||||
import { generate } from "shortid"
|
||||
|
||||
/**
|
||||
|
@ -6,9 +5,8 @@ import { generate } from "shortid"
|
|||
* Workflow definitions are stored in linked lists.
|
||||
*/
|
||||
export default class Workflow {
|
||||
constructor(workflow, blockDefinitions) {
|
||||
constructor(workflow) {
|
||||
this.workflow = workflow
|
||||
this.blockDefinitions = blockDefinitions
|
||||
}
|
||||
|
||||
hasTrigger() {
|
||||
|
@ -22,23 +20,26 @@ export default class Workflow {
|
|||
return
|
||||
}
|
||||
|
||||
this.workflow.definition.steps.push({
|
||||
id: generate(),
|
||||
...block,
|
||||
})
|
||||
const newBlock = { id: generate(), ...block }
|
||||
this.workflow.definition.steps = [
|
||||
...this.workflow.definition.steps,
|
||||
newBlock,
|
||||
]
|
||||
return newBlock
|
||||
}
|
||||
|
||||
updateBlock(updatedBlock, id) {
|
||||
const { steps, trigger } = this.workflow.definition
|
||||
|
||||
if (trigger && trigger.id === id) {
|
||||
this.workflow.definition.trigger = null
|
||||
this.workflow.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
|
||||
}
|
||||
|
||||
deleteBlock(id) {
|
||||
|
@ -52,46 +53,6 @@ export default class Workflow {
|
|||
const stepIdx = steps.findIndex(step => step.id === id)
|
||||
if (stepIdx < 0) throw new Error("Block not found.")
|
||||
steps.splice(stepIdx, 1)
|
||||
}
|
||||
|
||||
createUiTree() {
|
||||
if (!this.workflow.definition) return []
|
||||
return Workflow.buildUiTree(this.workflow.definition, this.blockDefinitions)
|
||||
}
|
||||
|
||||
static buildUiTree(definition, blockDefinitions) {
|
||||
const steps = []
|
||||
if (definition.trigger) {
|
||||
steps.push(definition.trigger)
|
||||
}
|
||||
|
||||
return [...steps, ...definition.steps].map(step => {
|
||||
// The client side display definition for the block
|
||||
const definition = blockDefinitions[step.type][step.stepId]
|
||||
if (!definition) {
|
||||
throw new Error(
|
||||
`No block definition exists for the chosen block. Check there's an entry in the block definitions for ${step.stepId}`
|
||||
)
|
||||
}
|
||||
|
||||
if (!definition.params) {
|
||||
throw new Error(
|
||||
`Blocks should always have parameters. Ensure that the block definition is correct for ${step.stepId}`
|
||||
)
|
||||
}
|
||||
|
||||
const tagline = definition.tagline || ""
|
||||
const args = step.args || {}
|
||||
|
||||
return {
|
||||
id: step.id,
|
||||
type: step.type,
|
||||
params: step.params,
|
||||
args,
|
||||
heading: step.stepId,
|
||||
body: mustache.render(tagline, args),
|
||||
name: definition.name,
|
||||
}
|
||||
})
|
||||
this.workflow.definition.steps = steps
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
import { writable } from "svelte/store"
|
||||
import api from "../../api"
|
||||
import Workflow from "./Workflow"
|
||||
import { cloneDeep } from "lodash/fp"
|
||||
|
||||
const workflowActions = store => ({
|
||||
fetch: async () => {
|
||||
|
@ -32,11 +33,8 @@ const workflowActions = store => ({
|
|||
const response = await api.post(CREATE_WORKFLOW_URL, workflow)
|
||||
const json = await response.json()
|
||||
store.update(state => {
|
||||
state.workflows = state.workflows.concat(json.workflow)
|
||||
state.currentWorkflow = new Workflow(
|
||||
json.workflow,
|
||||
state.blockDefinitions
|
||||
)
|
||||
state.workflows = [...state.workflows, json.workflow]
|
||||
store.actions.select(json.workflow)
|
||||
return state
|
||||
})
|
||||
},
|
||||
|
@ -50,23 +48,7 @@ const workflowActions = store => ({
|
|||
)
|
||||
state.workflows.splice(existingIdx, 1, json.workflow)
|
||||
state.workflows = state.workflows
|
||||
state.currentWorkflow = new Workflow(
|
||||
json.workflow,
|
||||
state.blockDefinitions
|
||||
)
|
||||
return state
|
||||
})
|
||||
},
|
||||
update: 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
|
||||
})
|
||||
},
|
||||
|
@ -81,37 +63,34 @@ const workflowActions = store => ({
|
|||
)
|
||||
state.workflows.splice(existingIdx, 1)
|
||||
state.workflows = state.workflows
|
||||
state.currentWorkflow = null
|
||||
state.selectedWorkflow = null
|
||||
state.selectedBlock = null
|
||||
return state
|
||||
})
|
||||
},
|
||||
select: workflow => {
|
||||
store.update(state => {
|
||||
state.currentWorkflow = new Workflow(workflow, state.blockDefinitions)
|
||||
state.selectedWorkflowBlock = null
|
||||
state.selectedWorkflow = new Workflow(cloneDeep(workflow))
|
||||
state.selectedBlock = null
|
||||
return state
|
||||
})
|
||||
},
|
||||
addBlockToWorkflow: block => {
|
||||
store.update(state => {
|
||||
state.currentWorkflow.addBlock(block)
|
||||
const steps = state.currentWorkflow.workflow.definition.steps
|
||||
state.selectedWorkflowBlock = steps.length
|
||||
? steps[steps.length - 1]
|
||||
: state.currentWorkflow.workflow.definition.trigger
|
||||
const newBlock = state.selectedWorkflow.addBlock(block)
|
||||
state.selectedBlock = newBlock
|
||||
return state
|
||||
})
|
||||
},
|
||||
deleteWorkflowBlock: block => {
|
||||
store.update(state => {
|
||||
console.log(state.currentWorkflow.workflow)
|
||||
const idx = state.currentWorkflow.workflow.definition.steps.findIndex(
|
||||
const idx = state.selectedWorkflow.workflow.definition.steps.findIndex(
|
||||
x => x.id === block.id
|
||||
)
|
||||
state.currentWorkflow.deleteBlock(block.id)
|
||||
state.selectedWorkflow.deleteBlock(block.id)
|
||||
|
||||
// Select next closest step
|
||||
const steps = state.currentWorkflow.workflow.definition.steps
|
||||
const steps = state.selectedWorkflow.workflow.definition.steps
|
||||
let nextSelectedBlock
|
||||
if (steps[idx] != null) {
|
||||
nextSelectedBlock = steps[idx]
|
||||
|
@ -119,9 +98,9 @@ const workflowActions = store => ({
|
|||
nextSelectedBlock = steps[idx - 1]
|
||||
} else {
|
||||
nextSelectedBlock =
|
||||
state.currentWorkflow.workflow.definition.trigger || null
|
||||
state.selectedWorkflow.workflow.definition.trigger || null
|
||||
}
|
||||
state.selectedWorkflowBlock = nextSelectedBlock
|
||||
state.selectedBlock = nextSelectedBlock
|
||||
return state
|
||||
})
|
||||
},
|
||||
|
@ -135,8 +114,8 @@ export const getWorkflowStore = () => {
|
|||
ACTION: [],
|
||||
LOGIC: [],
|
||||
},
|
||||
selectedWorkflow: null,
|
||||
}
|
||||
|
||||
const store = writable(INITIAL_WORKFLOW_STATE)
|
||||
store.actions = workflowActions(store)
|
||||
return store
|
||||
|
|
|
@ -13,7 +13,7 @@
|
|||
async function deleteWorkflow() {
|
||||
await workflowStore.actions.delete({
|
||||
instanceId,
|
||||
workflow: $workflowStore.currentWorkflow.workflow,
|
||||
workflow: $workflowStore.selectedWorkflow.workflow,
|
||||
})
|
||||
onClosed()
|
||||
notifier.danger("Workflow deleted.")
|
||||
|
|
|
@ -2,13 +2,15 @@
|
|||
import { backendUiStore } from "builderStore"
|
||||
|
||||
export let value
|
||||
let modelId = value ? value._id : ""
|
||||
$: value = $backendUiStore.models.find(x => x._id === modelId)
|
||||
</script>
|
||||
|
||||
<div class="block-field">
|
||||
<select class="budibase__input" bind:value>
|
||||
<select class="budibase__input" bind:value={modelId}>
|
||||
<option value="" />
|
||||
{#each $backendUiStore.models as model}
|
||||
<option value={model}>{model.name}</option>
|
||||
<option value={model._id}>{model.name}</option>
|
||||
{/each}
|
||||
</select>
|
||||
</div>
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
<script>
|
||||
import { fade } from "svelte/transition"
|
||||
import { onMount, getContext } from "svelte"
|
||||
import { getContext } from "svelte"
|
||||
import { backendUiStore, workflowStore } from "builderStore"
|
||||
import { notifier } from "builderStore/store/notifications"
|
||||
import WorkflowBlockSetup from "./WorkflowBlockSetup.svelte"
|
||||
|
@ -9,40 +9,22 @@
|
|||
|
||||
const { open, close } = getContext("simple-modal")
|
||||
|
||||
const ACCESS_LEVELS = [
|
||||
{
|
||||
name: "Admin",
|
||||
key: "ADMIN",
|
||||
canExecute: true,
|
||||
editable: false,
|
||||
},
|
||||
{
|
||||
name: "Power User",
|
||||
key: "POWER_USER",
|
||||
canExecute: true,
|
||||
editable: false,
|
||||
},
|
||||
]
|
||||
|
||||
let selectedTab = "SETUP"
|
||||
let testResult
|
||||
|
||||
$: workflow =
|
||||
$workflowStore.currentWorkflow && $workflowStore.currentWorkflow.workflow
|
||||
$: workflowBlock = $workflowStore.selectedWorkflowBlock
|
||||
$workflowStore.selectedWorkflow && $workflowStore.selectedWorkflow.workflow
|
||||
|
||||
function deleteWorkflow() {
|
||||
open(
|
||||
DeleteWorkflowModal,
|
||||
{
|
||||
onClosed: close,
|
||||
},
|
||||
{ onClosed: close },
|
||||
{ styleContent: { padding: "0" } }
|
||||
)
|
||||
}
|
||||
|
||||
function deleteWorkflowBlock() {
|
||||
workflowStore.actions.deleteWorkflowBlock(workflowBlock)
|
||||
workflowStore.actions.deleteWorkflowBlock($workflowStore.selectedBlock)
|
||||
notifier.info("Workflow block deleted.")
|
||||
}
|
||||
|
||||
|
@ -51,7 +33,6 @@
|
|||
}
|
||||
|
||||
async function saveWorkflow() {
|
||||
const workflow = $workflowStore.currentWorkflow.workflow
|
||||
await workflowStore.actions.save({
|
||||
instanceId: $backendUiStore.selectedDatabase._id,
|
||||
workflow,
|
||||
|
@ -71,7 +52,7 @@
|
|||
}}>
|
||||
Setup
|
||||
</span>
|
||||
{#if !workflowBlock}
|
||||
{#if !$workflowStore.selectedBlock}
|
||||
<span
|
||||
class="test-tab"
|
||||
class:selected={selectedTab === 'TEST'}
|
||||
|
@ -95,8 +76,8 @@
|
|||
</div>
|
||||
{/if}
|
||||
{#if selectedTab === 'SETUP'}
|
||||
{#if workflowBlock}
|
||||
<WorkflowBlockSetup {workflowBlock} />
|
||||
{#if $workflowStore.selectedBlock}
|
||||
<WorkflowBlockSetup bind:block={$workflowStore.selectedBlock} />
|
||||
<div class="buttons">
|
||||
<Button
|
||||
green
|
||||
|
@ -107,25 +88,10 @@
|
|||
</Button>
|
||||
<Button red wide on:click={deleteWorkflowBlock}>Delete Block</Button>
|
||||
</div>
|
||||
{:else if $workflowStore.currentWorkflow}
|
||||
{:else if $workflowStore.selectedWorkflow}
|
||||
<div class="panel">
|
||||
<div class="panel-body">
|
||||
<div class="block-label">Workflow: {workflow.name}</div>
|
||||
<div class="config-item">
|
||||
<Label small forAttr={'useraccess'}>User Access</Label>
|
||||
<div class="access-levels">
|
||||
|
||||
{#each ACCESS_LEVELS as level}
|
||||
<span class="access-level">
|
||||
<label>{level.name}</label>
|
||||
<input
|
||||
type="checkbox"
|
||||
disabled={!level.editable}
|
||||
bind:checked={level.canExecute} />
|
||||
</span>
|
||||
{/each}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="buttons">
|
||||
<Button
|
||||
|
@ -181,10 +147,6 @@
|
|||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
.config-item {
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
header > span {
|
||||
color: var(--grey-5);
|
||||
margin-right: 20px;
|
||||
|
@ -205,13 +167,6 @@
|
|||
gap: 12px;
|
||||
}
|
||||
|
||||
.access-level {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
margin-top: 20px;
|
||||
}
|
||||
|
||||
.access-level label {
|
||||
font-weight: normal;
|
||||
color: var(--ink);
|
||||
|
|
|
@ -1,51 +1,43 @@
|
|||
<script>
|
||||
import { backendUiStore, store } from "builderStore"
|
||||
import ComponentSelector from "./ParamInputs/ComponentSelector.svelte"
|
||||
import ModelSelector from "./ParamInputs/ModelSelector.svelte"
|
||||
import RecordSelector from "./ParamInputs/RecordSelector.svelte"
|
||||
import { Input, TextArea, Select } from "@budibase/bbui"
|
||||
|
||||
export let workflowBlock
|
||||
export let block
|
||||
|
||||
let params
|
||||
|
||||
$: workflowParams = workflowBlock.params
|
||||
? Object.entries(workflowBlock.params)
|
||||
: []
|
||||
$: params = block.params ? Object.entries(block.params) : []
|
||||
</script>
|
||||
|
||||
<div class="container">
|
||||
{#each workflowParams as [parameter, type]}
|
||||
{#each params as [parameter, type]}
|
||||
<div class="block-field">
|
||||
<label class="label">{parameter}</label>
|
||||
{#if Array.isArray(type)}
|
||||
<Select bind:value={workflowBlock.args[parameter]} thin secondary>
|
||||
<Select bind:value={block.args[parameter]} thin secondary>
|
||||
{#each type as option}
|
||||
<option value={option}>{option}</option>
|
||||
{/each}
|
||||
</Select>
|
||||
{:else if type === 'component'}
|
||||
<ComponentSelector bind:value={workflowBlock.args[parameter]} />
|
||||
<ComponentSelector bind:value={block.args[parameter]} />
|
||||
{:else if type === 'accessLevel'}
|
||||
<Select bind:value={workflowBlock.args[parameter]} thin secondary>
|
||||
<Select bind:value={block.args[parameter]} thin secondary>
|
||||
<option value="ADMIN">Admin</option>
|
||||
<option value="POWER_USER">Power User</option>
|
||||
</Select>
|
||||
{:else if type === 'password'}
|
||||
<Input
|
||||
type="password"
|
||||
thin
|
||||
bind:value={workflowBlock.args[parameter]} />
|
||||
<Input type="password" thin bind:value={block.args[parameter]} />
|
||||
{:else if type === 'number'}
|
||||
<Input type="number" thin bind:value={workflowBlock.args[parameter]} />
|
||||
<Input type="number" thin bind:value={block.args[parameter]} />
|
||||
{:else if type === 'longText'}
|
||||
<TextArea type="text" thin bind:value={workflowBlock.args[parameter]} />
|
||||
<TextArea type="text" thin bind:value={block.args[parameter]} />
|
||||
{:else if type === 'model'}
|
||||
<ModelSelector bind:value={workflowBlock.args[parameter]} />
|
||||
<ModelSelector bind:value={block.args[parameter]} />
|
||||
{:else if type === 'record'}
|
||||
<RecordSelector value={workflowBlock.args[parameter]} />
|
||||
<RecordSelector value={block.args[parameter]} />
|
||||
{:else if type === 'string'}
|
||||
<Input type="text" thin bind:value={workflowBlock.args[parameter]} />
|
||||
<Input type="text" thin bind:value={block.args[parameter]} />
|
||||
{/if}
|
||||
</div>
|
||||
{/each}
|
||||
|
|
|
@ -1,30 +1,24 @@
|
|||
<script>
|
||||
import { onMount } from "svelte"
|
||||
import { afterUpdate } from "svelte"
|
||||
import { workflowStore, backendUiStore } from "builderStore"
|
||||
import { notifier } from "builderStore/store/notifications"
|
||||
import Flowchart from "./flowchart/FlowChart.svelte"
|
||||
|
||||
let selectedWorkflow
|
||||
let uiTree
|
||||
let instanceId = $backendUiStore.selectedDatabase._id
|
||||
|
||||
$: selectedWorkflow = $workflowStore.currentWorkflow
|
||||
|
||||
$: workflowLive = selectedWorkflow && selectedWorkflow.workflow.live
|
||||
|
||||
$: uiTree = selectedWorkflow ? selectedWorkflow.createUiTree() : []
|
||||
let section
|
||||
|
||||
$: workflow =
|
||||
$workflowStore.selectedWorkflow && $workflowStore.selectedWorkflow.workflow
|
||||
$: workflowLive = workflow && workflow.live
|
||||
$: instanceId = $backendUiStore.selectedDatabase._id
|
||||
|
||||
function onSelect(block) {
|
||||
workflowStore.update(state => {
|
||||
state.selectedWorkflowBlock = block
|
||||
state.selectedBlock = block
|
||||
return state
|
||||
})
|
||||
}
|
||||
|
||||
function setWorkflowLive(live) {
|
||||
const { workflow } = selectedWorkflow
|
||||
workflow.live = live
|
||||
workflowStore.actions.save({ instanceId, workflow })
|
||||
if (live) {
|
||||
|
@ -33,13 +27,17 @@
|
|||
notifier.danger(`Workflow ${workflow.name} disabled.`)
|
||||
}
|
||||
}
|
||||
|
||||
afterUpdate(() => {
|
||||
section.scrollTo(0, section.scrollHeight)
|
||||
})
|
||||
</script>
|
||||
|
||||
<section>
|
||||
<Flowchart blocks={uiTree} {onSelect} />
|
||||
<section bind:this={section}>
|
||||
<Flowchart {workflow} {onSelect} />
|
||||
</section>
|
||||
<footer>
|
||||
{#if selectedWorkflow}
|
||||
{#if workflow}
|
||||
<button
|
||||
class:highlighted={workflowLive}
|
||||
class:hoverable={workflowLive}
|
||||
|
|
|
@ -1,12 +1,25 @@
|
|||
<script>
|
||||
import FlowItem from "./FlowItem.svelte"
|
||||
import Arrow from "./Arrow.svelte"
|
||||
import { flip } from "svelte/animate"
|
||||
import { fade, fly } from "svelte/transition"
|
||||
|
||||
export let blocks = []
|
||||
export let workflow
|
||||
export let onSelect
|
||||
let blocks
|
||||
|
||||
$: {
|
||||
blocks = []
|
||||
if (workflow) {
|
||||
if (workflow.definition.trigger) {
|
||||
blocks.push(workflow.definition.trigger)
|
||||
}
|
||||
blocks = blocks.concat(workflow.definition.steps || [])
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<section class="canvas">
|
||||
<section>
|
||||
{#each blocks as block, idx (block.id)}
|
||||
<FlowItem {onSelect} {block} />
|
||||
{#if idx !== blocks.length - 1}
|
||||
|
@ -19,9 +32,6 @@
|
|||
section {
|
||||
position: absolute;
|
||||
padding: 20px 40px;
|
||||
}
|
||||
|
||||
.canvas {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
flex-direction: column;
|
||||
|
|
|
@ -1,18 +1,19 @@
|
|||
<script>
|
||||
import mustache from "mustache"
|
||||
import { fade } from "svelte/transition"
|
||||
import { workflowStore } from "builderStore"
|
||||
|
||||
export let onSelect
|
||||
export let block
|
||||
let selected = false
|
||||
let selected
|
||||
|
||||
$: selected =
|
||||
$workflowStore.selectedBlock != null &&
|
||||
$workflowStore.selectedBlock.id === block.id
|
||||
|
||||
function selectBlock() {
|
||||
onSelect(block)
|
||||
}
|
||||
|
||||
$: selected =
|
||||
$workflowStore.selectedWorkflowBlock != null &&
|
||||
$workflowStore.selectedWorkflowBlock.id === block.id
|
||||
</script>
|
||||
|
||||
<div
|
||||
|
@ -34,7 +35,7 @@
|
|||
</header>
|
||||
<hr />
|
||||
<p>
|
||||
{@html block.body}
|
||||
{@html mustache.render(block.tagline, block.args)}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
|
|
|
@ -8,7 +8,7 @@
|
|||
$: blocks = Object.entries($workflowStore.blockDefinitions[selectedTab])
|
||||
|
||||
$: {
|
||||
if ($workflowStore.currentWorkflow.hasTrigger()) {
|
||||
if ($workflowStore.selectedWorkflow.hasTrigger()) {
|
||||
buttonProps = [
|
||||
{ value: "ACTION", text: "Action" },
|
||||
{ value: "LOGIC", text: "Logic" },
|
||||
|
|
|
@ -8,9 +8,9 @@
|
|||
|
||||
const { open, close } = getContext("simple-modal")
|
||||
|
||||
$: currentWorkflowId =
|
||||
$workflowStore.currentWorkflow &&
|
||||
$workflowStore.currentWorkflow.workflow._id
|
||||
$: selectedWorkflowId =
|
||||
$workflowStore.selectedWorkflow &&
|
||||
$workflowStore.selectedWorkflow.workflow._id
|
||||
|
||||
function newWorkflow() {
|
||||
open(
|
||||
|
@ -33,7 +33,7 @@
|
|||
{#each $workflowStore.workflows as workflow}
|
||||
<li
|
||||
class="workflow-item"
|
||||
class:selected={workflow._id === currentWorkflowId}
|
||||
class:selected={workflow._id === selectedWorkflowId}
|
||||
on:click={() => workflowStore.actions.select(workflow)}>
|
||||
<i class="ri-stackshare-line" class:live={workflow.live} />
|
||||
{workflow.name}
|
||||
|
|
|
@ -14,7 +14,7 @@
|
|||
on:click={() => (selectedTab = 'WORKFLOWS')}>
|
||||
Workflows
|
||||
</span>
|
||||
{#if $workflowStore.currentWorkflow}
|
||||
{#if $workflowStore.selectedWorkflow}
|
||||
<span
|
||||
data-cy="add-workflow-component"
|
||||
class="hoverable"
|
||||
|
|
Loading…
Reference in New Issue