Branching UX updates, fix for looping results and general failure results for automations. Added fix for stacking currentItem loop bindings

This commit is contained in:
Dean 2024-10-11 16:20:26 +01:00
parent 185fd557dd
commit 9382ca4c0b
17 changed files with 1736 additions and 548 deletions

View File

@ -13,10 +13,8 @@
import { admin, licensing } from "stores/portal"
import { externalActions } from "./ExternalActions"
import { TriggerStepID, ActionStepID } from "constants/backend/automations"
import { checkForCollectStep } from "helpers/utils"
export let blockIdx
export let lastStep
export let block
export let modal
let syncAutomationsEnabled = $licensing.syncAutomationsEnabled
@ -29,7 +27,15 @@
ActionStepID.TRIGGER_AUTOMATION_RUN,
]
$: collectBlockExists = checkForCollectStep($selectedAutomation)
$: blockRef = $automationStore.blocks?.[block.id]
$: lastStep = blockRef?.terminating
$: pathSteps = block.id
? automationStore.actions.getPathSteps(blockRef.pathTo, $selectedAutomation)
: []
$: collectBlockExists = pathSteps?.some(
step => step.stepId === ActionStepID.COLLECT
)
const disabled = () => {
return {
@ -100,9 +106,14 @@
action.stepId,
action
)
await automationStore.actions.addBlockToAutomation(newBlock, blockIdx + 1)
await automationStore.actions.addBlockToAutomation(
newBlock,
blockRef ? blockRef.pathTo : block.pathTo
)
modal.hide()
} catch (error) {
console.error(error)
notifications.error("Error saving automation")
}
}

View File

@ -0,0 +1,239 @@
<script>
import FilterBuilder from "components/design/settings/controls/FilterEditor/FilterBuilder.svelte"
import {
Drawer,
DrawerContent,
ActionButton,
Icon,
Layout,
Body,
Divider,
TooltipPosition,
TooltipType,
Button,
} from "@budibase/bbui"
import PropField from "components/automation/SetupPanel/PropField.svelte"
import AutomationBindingPanel from "components/common/bindings/ServerBindingPanel.svelte"
import FlowItemHeader from "./FlowItemHeader.svelte"
import FlowItemActions from "./FlowItemActions.svelte"
import { automationStore, selectedAutomation } from "stores/builder"
import { QueryUtils } from "@budibase/frontend-core"
import { cloneDeep } from "lodash/fp"
import { createEventDispatcher } from "svelte"
const dispatch = createEventDispatcher()
export let pathTo
export let branchIdx
export let step
export let isLast
export let bindings
let drawer
let condition
let open = true
$: branch = step.inputs?.branches?.[branchIdx]
$: editableConditionUI = cloneDeep(branch.conditionUI || {})
$: condition = QueryUtils.buildQuery(editableConditionUI)
// Parse all the bindings into fields for the condition builder
$: schemaFields = bindings.map(binding => {
return {
name: `{{${binding.runtimeBinding}}}`,
displayName: `${binding.category} - ${binding.display.name}`,
type: "string",
}
})
$: branchBlockRef = {
branchNode: true,
pathTo: (pathTo || []).concat({ branchIdx }),
}
</script>
<Drawer bind:this={drawer} title="Branch condition" forceModal>
<Button
cta
slot="buttons"
on:click={() => {
drawer.hide()
dispatch("change", {
conditionUI: editableConditionUI,
condition,
})
}}
>
Save
</Button>
<DrawerContent slot="body">
<FilterBuilder
filters={editableConditionUI}
{bindings}
{schemaFields}
datasource={{ type: "custom" }}
panel={AutomationBindingPanel}
on:change={e => {
editableConditionUI = e.detail
}}
allowOnEmpty={false}
builderType={"condition"}
docsURL={null}
/>
</DrawerContent>
</Drawer>
<div class="flow-item">
<div class={`block branch-node hoverable`} class:selected={false}>
<FlowItemHeader
{open}
itemName={branch.name}
block={step}
deleteStep={async () => {
await automationStore.actions.deleteBranch(
branchBlockRef.pathTo,
$selectedAutomation
)
}}
on:update={async e => {
let stepUpdate = cloneDeep(step)
let branchUpdate = stepUpdate.inputs?.branches.find(
stepBranch => stepBranch.id == branch.id
)
branchUpdate.name = e.detail
const updatedAuto = automationStore.actions.updateStep(
pathTo,
$selectedAutomation,
stepUpdate
)
await automationStore.actions.save(updatedAuto)
}}
on:toggle={() => (open = !open)}
>
<div slot="custom-actions" class="branch-actions">
<Icon
on:click={() => {
automationStore.actions.branchLeft(
branchBlockRef.pathTo,
$selectedAutomation,
step
)
}}
tooltip={"Move left"}
tooltipType={TooltipType.Info}
tooltipPosition={TooltipPosition.Top}
hoverable
disabled={branchIdx == 0}
name="ArrowLeft"
/>
<Icon
on:click={() => {
automationStore.actions.branchRight(
branchBlockRef.pathTo,
$selectedAutomation,
step
)
}}
tooltip={"Move right"}
tooltipType={TooltipType.Info}
tooltipPosition={TooltipPosition.Top}
hoverable
disabled={isLast}
name="ArrowRight"
/>
</div>
</FlowItemHeader>
{#if open}
<Divider noMargin />
<div class="blockSection">
<!-- Content body for possible slot -->
<Layout noPadding>
<PropField label="Only run when">
<ActionButton fullWidth on:click={drawer.show}>
{editableConditionUI?.groups?.length
? "Update condition"
: "Add condition"}
</ActionButton>
</PropField>
<div class="footer">
<Icon name="Info" />
<Body size="S">
Only the first branch which matches it's condition will run
</Body>
</div>
</Layout>
</div>
{/if}
</div>
<div class="separator" />
<FlowItemActions block={branchBlockRef} />
{#if step.inputs.children[branch.id]?.length}
<div class="separator" />
{/if}
</div>
<style>
.branch-actions {
display: flex;
gap: var(--spacing-l);
}
.footer {
display: flex;
align-items: center;
gap: var(--spacing-m);
}
.flow-item {
display: flex;
flex-direction: column;
align-items: center;
}
.block-options {
justify-content: flex-end;
align-items: center;
display: flex;
gap: var(--spacing-m);
}
.center-items {
display: flex;
align-items: center;
}
.splitHeader {
display: flex;
justify-content: space-between;
align-items: center;
}
.iconAlign {
padding: 0 0 0 var(--spacing-m);
display: inline-block;
}
.block {
width: 480px;
font-size: 16px;
background-color: var(--background);
border: 1px solid var(--spectrum-global-color-gray-300);
border-radius: 4px 4px 4px 4px;
}
.blockSection {
padding: var(--spacing-xl);
}
.separator {
width: 1px;
height: 25px;
border-left: 1px dashed var(--grey-4);
color: var(--grey-4);
align-self: center;
}
.blockTitle {
display: flex;
align-items: center;
gap: var(--spacing-s);
}
</style>

View File

@ -5,37 +5,82 @@
automationHistoryStore,
} from "stores/builder"
import ConfirmDialog from "components/common/ConfirmDialog.svelte"
import FlowItem from "./FlowItem.svelte"
import TestDataModal from "./TestDataModal.svelte"
import { flip } from "svelte/animate"
import { fly } from "svelte/transition"
import { Icon, notifications, Modal, Toggle } from "@budibase/bbui"
import { ActionStepID } from "constants/backend/automations"
import UndoRedoControl from "components/common/UndoRedoControl.svelte"
import StepNode from "./StepNode.svelte"
// Test test test
import { FIELDS } from "constants/backend"
import { tables } from "stores/builder"
import { AutomationEventType } from "@budibase/types"
import { writable } from "svelte/store"
import { setContext } from "svelte"
const test = writable({
someupdate: () => {
console.log("updated")
},
})
import { memo } from "@budibase/frontend-core"
import { sdk } from "@budibase/shared-core"
import { migrateReferencesInObject } from "dataBinding"
import { cloneDeep } from "lodash/fp"
import { onMount } from "svelte"
export let automation
const memoAutomation = memo(automation)
let testDataModal
let confirmDeleteDialog
let scrolling = false
let blockRefs = {}
let treeEle
$: blocks = getBlocks(automation).filter(x => x.stepId !== ActionStepID.LOOP)
$: isRowAction = sdk.automations.isRowAction(automation)
// Memo auto
$: memoAutomation.set(automation)
// Parse the automation tree state
$: refresh($memoAutomation)
$: blocks = getBlocks($memoAutomation).filter(
x => x.stepId !== ActionStepID.LOOP
)
$: isRowAction = sdk.automations.isRowAction($memoAutomation)
const refresh = auto => {
automationStore.update(state => {
return {
...state,
blocks: {},
}
})
// Traverse the automation and build metadata
automationStore.actions.traverse(auto)
blockRefs = $automationStore.blocks
// Build global automation bindings.
const environmentBindings =
automationStore.actions.buildEnvironmentBindings()
// Push common bindings globally
automationStore.update(state => ({
...state,
bindings: [...environmentBindings],
}))
// Parse the steps for references to sequential binding
const updatedAuto = cloneDeep(auto)
// Parse and migrate all bindings
Object.values(blockRefs)
.filter(blockRef => {
// Pulls out all distinct terminating nodes
return blockRef.terminating
})
.forEach(blockRef => {
automationStore.actions
.getPathSteps(blockRef.pathTo, updatedAuto)
.forEach((step, idx, steps) => {
migrateReferencesInObject({
obj: step,
originalIndex: idx,
steps,
})
})
})
}
const getBlocks = automation => {
let blocks = []
@ -62,19 +107,21 @@
scrolling = false
}
}
onMount(() => {
// Ensure the trigger element is centered in the view on load.
const triggerBlock = treeEle?.querySelector(".block.TRIGGER")
triggerBlock?.scrollIntoView({
behavior: "instant",
block: "nearest",
inline: "center",
})
})
</script>
<!-- svelte-ignore a11y-click-events-have-key-events -->
<!-- svelte-ignore a11y-no-static-element-interactions -->
<div class="header" class:scrolling>
<button
on:click={() => {
automationStore.actions.traverse($selectedAutomation)
console.log($automationStore)
}}
>
TEST
</button>
<div class="header-left">
<UndoRedoControl store={automationHistoryStore} />
</div>
@ -117,32 +164,21 @@
</div>
<div class="canvas" on:scroll={handleScroll}>
<div class="content">
<!--
Separate out the Trigger node?
-->
<div class="root">
{#each blocks as block, idx (block.id)}
<StepNode
step={blocks[idx]}
stepIdx={idx}
isLast={blocks?.length - 1 === idx}
{automation}
/>
{/each}
</div>
<!-- {#each blocks as block, idx (block.id)}
<div
class="block"
animate:flip={{ duration: 500 }}
in:fly={{ x: 500, duration: 500 }}
out:fly|local={{ x: 500, duration: 500 }}
>
{#if block.stepId !== ActionStepID.LOOP}
<FlowItem {testDataModal} {block} {idx} />
<div class="tree">
<div class="root" bind:this={treeEle}>
{#if Object.keys(blockRefs).length}
{#each blocks as block, idx (block.id)}
<StepNode
step={blocks[idx]}
stepIdx={idx}
isLast={blocks?.length - 1 === idx}
automation={$memoAutomation}
blocks={blockRefs}
/>
{/each}
{/if}
</div>
{/each} -->
</div>
</div>
</div>
<ConfirmDialog
@ -193,15 +229,7 @@
box-sizing: border-box;
}
/* .block {
display: flex;
flex-direction: column;
justify-content: flex-start;
align-items: center;
} */
.content {
flex-grow: 1;
padding: 23px 23px 80px;
box-sizing: border-box;
/* overflow-x: hidden; */
@ -212,7 +240,11 @@
border-bottom: var(--border-light);
z-index: 1;
}
.tree {
justify-content: center;
display: inline-flex;
min-width: 100%;
}
.header {
z-index: 1;
display: flex;

View File

@ -11,45 +11,47 @@
Layout,
Detail,
Modal,
Button,
notifications,
Label,
AbsTooltip,
InlineAlert,
} from "@budibase/bbui"
import AutomationBlockSetup from "../../SetupPanel/AutomationBlockSetup.svelte"
import CreateWebhookModal from "components/automation/Shared/CreateWebhookModal.svelte"
import ActionModal from "./ActionModal.svelte"
import FlowItemHeader from "./FlowItemHeader.svelte"
import RoleSelect from "components/design/settings/controls/RoleSelect.svelte"
import { ActionStepID, TriggerStepID } from "constants/backend/automations"
import { AutomationStepType } from "@budibase/types"
import FlowItemActions from "./FlowItemActions.svelte"
export let block
export let blockRef
export let testDataModal
export let idx
export let isLast
export let automation
export let bindings
let selected
let webhookModal
let actionModal
let open = true
let showLooping = false
let role
$: collectBlockExists = $selectedAutomation.definition.steps.some(
$: pathSteps = loadSteps(blockRef)
const loadSteps = blockRef => {
return blockRef
? automationStore.actions.getPathSteps(blockRef.pathTo, automation)
: []
}
$: collectBlockExists = pathSteps.some(
step => step.stepId === ActionStepID.COLLECT
)
$: automationId = $selectedAutomation?._id
$: isTrigger = block.type === "TRIGGER"
$: automationId = automation?._id
$: isTrigger = block.type === AutomationStepType.TRIGGER
$: lastStep = blockRef?.terminating
$: steps = $selectedAutomation?.definition?.steps ?? []
$: blockIdx = steps.findIndex(step => step.id === block.id)
$: lastStep = !isTrigger && blockIdx + 1 === steps.length
$: totalBlocks = $selectedAutomation?.definition?.steps.length + 1
$: loopBlock = $selectedAutomation?.definition.steps.find(
x => x.blockToLoop === block.id
)
$: loopBlock = pathSteps.find(x => x.blockToLoop === block.id)
$: isAppAction = block?.stepId === TriggerStepID.APP
$: isAppAction && setPermissions(role)
$: isAppAction && getPermissions(automationId)
@ -79,23 +81,13 @@
}
}
async function removeLooping() {
try {
await automationStore.actions.deleteAutomationBlock(loopBlock)
} catch (error) {
notifications.error("Error saving automation")
}
async function deleteStep() {
await automationStore.actions.deleteAutomationBlock(blockRef.pathTo)
}
async function deleteStep() {
try {
if (loopBlock) {
await automationStore.actions.deleteAutomationBlock(loopBlock)
}
await automationStore.actions.deleteAutomationBlock(block, blockIdx)
} catch (error) {
notifications.error("Error saving automation")
}
async function removeLooping() {
let loopBlockRef = $automationStore.blocks[blockRef.looped]
await automationStore.actions.deleteAutomationBlock(loopBlockRef.pathTo)
}
async function addLooping() {
@ -106,128 +98,143 @@
loopDefinition
)
loopBlock.blockToLoop = block.id
await automationStore.actions.addBlockToAutomation(loopBlock, blockIdx)
await automationStore.actions.addBlockToAutomation(
loopBlock,
blockRef.pathTo
)
}
</script>
<!-- svelte-ignore a11y-click-events-have-key-events -->
<!-- svelte-ignore a11y-no-static-element-interactions -->
<div class={`block ${block.type} hoverable`} class:selected on:click={() => {}}>
{#if loopBlock}
<div class="blockSection">
<div
on:click={() => {
showLooping = !showLooping
}}
class="splitHeader"
>
<div class="center-items">
<svg
width="28px"
height="28px"
class="spectrum-Icon"
style="color:var(--spectrum-global-color-gray-700);"
focusable="false"
>
<use xlink:href="#spectrum-icon-18-Reuse" />
</svg>
<div class="iconAlign">
<Detail size="S">Looping</Detail>
{#if block.stepId !== "LOOP"}
<!-- svelte-ignore a11y-click-events-have-key-events -->
<!-- svelte-ignore a11y-no-static-element-interactions -->
<div
id={`block-${block.id}`}
class={`block ${block.type} hoverable`}
class:selected
on:click={() => {}}
>
{#if loopBlock}
<div class="blockSection">
<div
on:click={() => {
showLooping = !showLooping
}}
class="splitHeader"
>
<div class="center-items">
<svg
width="28px"
height="28px"
class="spectrum-Icon"
style="color:var(--spectrum-global-color-gray-700);"
focusable="false"
>
<use xlink:href="#spectrum-icon-18-Reuse" />
</svg>
<div class="iconAlign">
<Detail size="S">Looping</Detail>
</div>
</div>
</div>
<div class="blockTitle">
<AbsTooltip type="negative" text="Remove looping">
<Icon on:click={removeLooping} hoverable name="DeleteOutline" />
</AbsTooltip>
<div class="blockTitle">
<AbsTooltip type="negative" text="Remove looping">
<Icon on:click={removeLooping} hoverable name="DeleteOutline" />
</AbsTooltip>
<div style="margin-left: 10px;" on:click={() => {}}>
<Icon hoverable name={showLooping ? "ChevronDown" : "ChevronUp"} />
<div style="margin-left: 10px;" on:click={() => {}}>
<Icon
hoverable
name={showLooping ? "ChevronDown" : "ChevronUp"}
/>
</div>
</div>
</div>
</div>
</div>
<Divider noMargin />
{#if !showLooping}
<Divider noMargin />
{#if !showLooping}
<div class="blockSection">
<Layout noPadding gap="S">
<AutomationBlockSetup
schemaProperties={Object.entries(
$automationStore.blockDefinitions.ACTION.LOOP.schema.inputs
.properties
)}
{webhookModal}
block={loopBlock}
{automation}
{bindings}
/>
</Layout>
</div>
<Divider noMargin />
{/if}
{/if}
<FlowItemHeader
{open}
{block}
{testDataModal}
{idx}
{addLooping}
{deleteStep}
on:toggle={() => (open = !open)}
on:update={async e => {
const newName = e.detail
if (newName.length === 0) {
await automationStore.actions.deleteAutomationName(block.id)
} else {
await automationStore.actions.saveAutomationName(block.id, newName)
}
}}
/>
{#if open}
<Divider noMargin />
<div class="blockSection">
<Layout noPadding gap="S">
{#if isAppAction}
<div>
<Label>Role</Label>
<RoleSelect bind:value={role} />
</div>
{/if}
<AutomationBlockSetup
schemaProperties={Object.entries(
$automationStore.blockDefinitions.ACTION.LOOP.schema.inputs
.properties
block?.schema?.inputs?.properties || {}
)}
{block}
{webhookModal}
block={loopBlock}
{automation}
{bindings}
/>
{#if isTrigger && triggerInfo}
<InlineAlert
header={triggerInfo.type}
message={`This trigger is tied to the "${triggerInfo.rowAction.name}" row action in your ${triggerInfo.table.name} table`}
/>
{/if}
</Layout>
</div>
<Divider noMargin />
{/if}
</div>
{#if !collectBlockExists || !lastStep}
<div class="separator" />
<FlowItemActions
{block}
on:branch={() => {
automationStore.actions.branchAutomation(
$automationStore.blocks[block.id].pathTo,
$selectedAutomation
)
}}
/>
{#if !lastStep}
<div class="separator" />
{/if}
{/if}
<FlowItemHeader
{open}
{block}
{testDataModal}
{idx}
{addLooping}
{deleteStep}
on:toggle={() => (open = !open)}
/>
{#if open}
<Divider noMargin />
<div class="blockSection">
<Layout noPadding gap="S">
{#if isAppAction}
<div>
<Label>Role</Label>
<RoleSelect bind:value={role} />
</div>
{/if}
<AutomationBlockSetup
schemaProperties={Object.entries(
block?.schema?.inputs?.properties || {}
)}
{block}
{webhookModal}
/>
{#if isTrigger && triggerInfo}
<InlineAlert
header={triggerInfo.type}
message={`This trigger is tied to the "${triggerInfo.rowAction.name}" row action in your ${triggerInfo.table.name} table`}
/>
{/if}
{#if lastStep}
<Button on:click={() => testDataModal.show()} cta>
Finish and test automation
</Button>
{/if}
</Layout>
</div>
{/if}
</div>
{#if !collectBlockExists || !lastStep}
<div class="separator" />
<!-- Need to break out the separators -->
<FlowItemActions on:addStep={actionModal.show()} />
<!-- <Icon
on:click={() => actionModal.show()}
hoverable
name="AddCircle"
size="S"
/> -->
{#if isTrigger ? !isLast || totalBlocks > 1 : blockIdx !== totalBlocks - 2}
<div class="separator" />
{/if}
{/if}
<Modal bind:this={actionModal} width="30%">
<ActionModal modal={actionModal} {lastStep} {blockIdx} />
</Modal>
<Modal bind:this={webhookModal} width="30%">
<CreateWebhookModal />
</Modal>

View File

@ -0,0 +1,49 @@
<script>
import { Icon, TooltipPosition, TooltipType, Modal } from "@budibase/bbui"
import { createEventDispatcher } from "svelte"
import ActionModal from "./ActionModal.svelte"
export let block
const dispatch = createEventDispatcher()
let actionModal
</script>
<Modal bind:this={actionModal} width="30%">
<ActionModal modal={actionModal} {block} />
</Modal>
<div class="action-bar">
{#if !block.branchNode}
<Icon
hoverable
name="Branch3"
on:click={() => {
dispatch("branch")
}}
tooltipType={TooltipType.Info}
tooltipPosition={TooltipPosition.Left}
tooltip={"Create branch"}
/>
{/if}
<Icon
hoverable
name="AddCircle"
on:click={() => {
actionModal.show()
}}
tooltipType={TooltipType.Info}
tooltipPosition={TooltipPosition.Right}
tooltip={"Add a step"}
/>
</div>
<style>
.action-bar {
background-color: var(--background);
border-radius: 4px 4px 4px 4px;
display: flex;
gap: var(--spacing-m);
padding: var(--spacing-m);
}
</style>

View File

@ -10,10 +10,11 @@
export let showTestStatus = false
export let testResult
export let isTrigger
export let idx
export let addLooping
export let deleteStep
export let enableNaming = true
export let itemName
let validRegex = /^[A-Za-z0-9_\s]+$/
let typing = false
let editing = false
@ -21,10 +22,11 @@
$: stepNames = $selectedAutomation?.definition.stepNames
$: allSteps = $selectedAutomation?.definition.steps || []
$: automationName = stepNames?.[block.id] || block?.name || ""
$: automationName = itemName || stepNames?.[block.id] || block?.name || ""
$: automationNameError = getAutomationNameError(automationName)
$: status = updateStatus(testResult)
$: isHeaderTrigger = isTrigger || block.type === "TRIGGER"
$: isBranch = block.stepId === "BRANCH"
$: {
if (!testResult) {
@ -33,9 +35,9 @@
)?.[0]
}
}
$: loopBlock = $selectedAutomation?.definition.steps.find(
x => x.blockToLoop === block?.id
)
$: blockRef = $automationStore.blocks[block.id]
$: isLooped = blockRef?.looped
async function onSelect(block) {
await automationStore.update(state => {
@ -84,30 +86,18 @@
return null
}
const saveName = async () => {
if (automationNameError || block.name === automationName) {
return
}
if (automationName.length === 0) {
await automationStore.actions.deleteAutomationName(block.id)
} else {
await automationStore.actions.saveAutomationName(block.id, automationName)
}
}
const startEditing = () => {
editing = true
typing = true
}
const stopEditing = async () => {
const stopEditing = () => {
editing = false
typing = false
if (automationNameError) {
automationName = stepNames[block.id] || block?.name
} else {
await saveName()
dispatch("update", automationName)
}
}
</script>
@ -118,7 +108,6 @@
class:typing={typing && !automationNameError && editing}
class:typing-error={automationNameError && editing}
class="blockSection"
on:click={() => dispatch("toggle")}
>
<div class="splitHeader">
<div class="center-items">
@ -144,16 +133,14 @@
{#if isHeaderTrigger}
<Body size="XS"><b>Trigger</b></Body>
{:else}
<div style="margin-left: 2px;">
<Body size="XS"><b>Step {idx}</b></Body>
</div>
<Body size="XS"><b>{isBranch ? "Branch" : "Step"}</b></Body>
{/if}
{#if enableNaming}
<input
class="input-text"
disabled={!enableNaming}
placeholder="Enter step name"
placeholder={`Enter ${isBranch ? "branch" : "step"} name`}
name="name"
autocomplete="off"
value={automationName}
@ -208,8 +195,9 @@
onSelect(block)
}}
>
<slot name="custom-actions" />
{#if !showTestStatus}
{#if !isHeaderTrigger && !loopBlock && (block?.features?.[Features.LOOPING] || !block.features)}
{#if !isHeaderTrigger && !isLooped && !isBranch && (block?.features?.[Features.LOOPING] || !block.features)}
<AbsTooltip type="info" text="Add looping">
<Icon on:click={addLooping} hoverable name="RotateCW" />
</AbsTooltip>
@ -220,6 +208,9 @@
</AbsTooltip>
{/if}
{/if}
{#if !showTestStatus && !isHeaderTrigger}
<span class="action-spacer" />
{/if}
{#if !showTestStatus}
<Icon
on:click={e => {
@ -245,6 +236,9 @@
</div>
<style>
.action-spacer {
border-left: 1px solid var(--spectrum-global-color-gray-300);
}
.status-container {
display: flex;
align-items: center;
@ -298,6 +292,8 @@
font-size: var(--spectrum-alias-font-size-default);
font-family: var(--font-sans);
text-overflow: ellipsis;
padding-left: 0px;
border: 0px;
}
input:focus {

View File

@ -0,0 +1,177 @@
<script>
import FlowItem from "./FlowItem.svelte"
import BranchNode from "./BranchNode.svelte"
import { AutomationActionStepId } from "@budibase/types"
import { ActionButton } from "@budibase/bbui"
import { automationStore } from "stores/builder"
import { cloneDeep } from "lodash"
export let step = {}
export let stepIdx
export let automation
export let blocks
export let isLast = false
$: blockRef = blocks?.[step.id]
$: pathToCurrentNode = blockRef?.pathTo
$: isBranch = step.stepId === AutomationActionStepId.BRANCH
$: branches = step.inputs?.branches
// All bindings available to this point
$: availableBindings = automationStore.actions.getPathBindings(
step.id,
automation
)
// Combine all bindings for the step
$: bindings = [...availableBindings, ...($automationStore.bindings || [])]
</script>
{#if isBranch}
<div class="split-branch-btn">
<ActionButton
icon="AddCircle"
on:click={() => {
automationStore.actions.branchAutomation(pathToCurrentNode, automation)
}}
>
Add additional branch
</ActionButton>
</div>
<div class="branched">
{#each branches as branch, bIdx}
{@const leftMost = bIdx === 0}
{@const rightMost = branches?.length - 1 === bIdx}
<div class="branch-wrap">
<div
class="branch"
class:left={leftMost}
class:right={rightMost}
class:middle={!leftMost && !rightMost}
>
<div class="branch-node">
<BranchNode
{step}
{bindings}
pathTo={pathToCurrentNode}
branchIdx={bIdx}
isLast={rightMost}
on:change={e => {
const updatedBranch = { ...branch, ...e.detail }
if (!step?.inputs?.branches?.[bIdx]) {
console.error(`Cannot load target branch: ${bIdx}`)
return
}
let branchStepUpdate = cloneDeep(step)
branchStepUpdate.inputs.branches[bIdx] = updatedBranch
const updated = automationStore.actions.updateStep(
blockRef?.pathTo,
automation,
branchStepUpdate
)
automationStore.actions.save(updated)
}}
/>
</div>
<!-- Branch steps -->
{#each step.inputs?.children[branch.id] || [] as bStep, sIdx}
<!-- Recursive StepNode -->
<svelte:self
step={bStep}
stepIdx={sIdx}
branchIdx={bIdx}
isLast={blockRef.terminating}
pathTo={pathToCurrentNode}
{automation}
{blocks}
/>
{/each}
</div>
</div>
{/each}
</div>
{:else}
<!--Drop Zone-->
<div class="block">
<FlowItem
block={step}
idx={stepIdx}
{blockRef}
{isLast}
{automation}
{bindings}
/>
</div>
<!--Drop Zone-->
{/if}
<style>
.branch-wrap {
width: inherit;
}
.branch {
display: flex;
align-items: center;
flex-direction: column;
position: relative;
width: inherit;
}
.branched {
display: flex;
gap: 64px;
}
.branch::before {
height: 64px;
border-left: 1px dashed var(--grey-4);
border-top: 1px dashed var(--grey-4);
content: "";
color: var(--grey-4);
width: 50%;
position: absolute;
left: 50%;
top: -16px;
}
.branch.left::before {
color: var(--grey-4);
width: calc(50% + 62px);
}
.branch.middle::after {
height: 64px;
border-top: 1px dashed var(--grey-4);
content: "";
color: var(--grey-4);
width: calc(50% + 62px);
position: absolute;
left: 50%;
top: -16px;
}
.branch.right::before {
left: 0px;
border-right: 1px dashed var(--grey-4);
border-left: none;
}
.branch.middle::before {
left: 0px;
border-right: 1px dashed var(--grey-4);
border-left: none;
}
.branch-node {
margin-top: 48px;
}
.split-branch-btn {
z-index: 2;
}
</style>

View File

@ -3,6 +3,8 @@
import FlowItemHeader from "./FlowChart/FlowItemHeader.svelte"
import { ActionStepID } from "constants/backend/automations"
import { JsonView } from "@zerodevx/svelte-json-view"
import { automationStore } from "stores/builder"
import { AutomationActionStepId } from "@budibase/types"
export let automation
export let testResults
@ -28,21 +30,27 @@
}
}
$: filteredResults = prepTestResults(testResults)
const getBranchName = (step, id) => {
if (!step || !id) {
return
}
return step.inputs.branches.find(branch => branch.id === id)?.name
}
$: filteredResults = prepTestResults(testResults)
$: {
if (testResults.message) {
blocks = automation?.definition?.trigger
? [automation.definition.trigger]
: []
} else if (automation) {
blocks = []
if (automation.definition.trigger) {
blocks.push(automation.definition.trigger)
}
blocks = blocks
.concat(automation.definition.steps || [])
.filter(x => x.stepId !== ActionStepID.LOOP)
const terminatingStep = filteredResults.at(-1)
const terminatingBlockRef = $automationStore.blocks[terminatingStep.id]
const pathSteps = automationStore.actions.getPathSteps(
terminatingBlockRef.pathTo,
automation
)
blocks = [...pathSteps].filter(x => x.stepId !== ActionStepID.LOOP)
} else if (filteredResults) {
blocks = filteredResults || []
// make sure there is an ID for each block being displayed
@ -60,6 +68,9 @@
{#if block.stepId !== ActionStepID.LOOP}
<FlowItemHeader
enableNaming={false}
itemName={block.stepId === AutomationActionStepId.BRANCH
? getBranchName(block, filteredResults?.[idx].outputs?.branchId)
: null}
open={!!openBlocks[block.id]}
on:toggle={() => (openBlocks[block.id] = !openBlocks[block.id])}
isTrigger={idx === 0}

View File

@ -21,8 +21,8 @@
} from "@budibase/bbui"
import CreateWebhookModal from "components/automation/Shared/CreateWebhookModal.svelte"
import { automationStore, selectedAutomation, tables } from "stores/builder"
import { environment, licensing } from "stores/portal"
import { automationStore, tables } from "stores/builder"
import { environment } from "stores/portal"
import WebhookDisplay from "../Shared/WebhookDisplay.svelte"
import {
BindingSidePanel,
@ -46,10 +46,7 @@
} from "components/common/CodeEditor"
import FilterBuilder from "components/design/settings/controls/FilterEditor/FilterBuilder.svelte"
import { QueryUtils, Utils, search, memo } from "@budibase/frontend-core"
import {
getSchemaForDatasourcePlus,
getEnvironmentBindings,
} from "dataBinding"
import { getSchemaForDatasourcePlus } from "dataBinding"
import { TriggerStepID, ActionStepID } from "constants/backend/automations"
import { onMount } from "svelte"
import { writable } from "svelte/store"
@ -110,7 +107,8 @@
$memoBlock.id,
automation
)
$: environmentBindings = buildEnvironmentBindings($memoEnvVariables)
$: environmentBindings =
automationStore.actions.buildEnvironmentBindings($memoEnvVariables)
$: bindings = [...automationBindings, ...environmentBindings]
$: getInputData(testData, $memoBlock.inputs)
@ -145,21 +143,6 @@
? [hbAutocomplete([...bindingsToCompletions(bindings, codeMode)])]
: []
const buildEnvironmentBindings = () => {
if ($licensing.environmentVariablesEnabled) {
return getEnvironmentBindings().map(binding => {
return {
...binding,
display: {
...binding.display,
rank: 98,
},
}
})
}
return []
}
const getInputData = (testData, blockInputs) => {
// Test data is not cloned for reactivity
let newInputData = testData || cloneDeep(blockInputs)
@ -529,9 +512,6 @@
})
*/
const onChange = Utils.sequential(async update => {
if (1 == 1) {
console.error("ABORT UPDATE")
}
const request = cloneDeep(update)
// Process app trigger updates
if (isTrigger && !isTestModal) {
@ -576,8 +556,8 @@
...newTestData,
...request,
}
// TO DO - uncomment
// await automationStore.actions.addTestDataToAutomation(newTestData)
await automationStore.actions.addTestDataToAutomation(newTestData)
} else {
const data = { schema, ...request }
await automationStore.actions.updateBlockInputs(block, data)

View File

@ -12,8 +12,10 @@
export let bindings = []
export let panel = ClientBindingPanel
export let allowBindings = true
export let allowOnEmpty
export let datasource
export let showFilterEmptyDropdown
export let builderType
export let docsURL
</script>
<CoreFilterBuilder
@ -26,7 +28,9 @@
{schemaFields}
{datasource}
{allowBindings}
{showFilterEmptyDropdown}
{bindings}
{allowOnEmpty}
{builderType}
{docsURL}
on:change
/>

View File

@ -1469,3 +1469,31 @@ export const updateReferencesInObject = ({
}
}
}
// Migrate references
// Switch all bindings to reference their ids
export const migrateReferencesInObject = ({ obj, label = "steps", steps }) => {
const stepIndexRegex = new RegExp(`{{\\s*${label}\\.(\\d+)\\.`, "g")
const updateActionStep = (str, index, replaceWith) =>
str.replace(`{{ ${label}.${index}.`, `{{ ${label}.${replaceWith}.`)
for (const key in obj) {
if (typeof obj[key] === "string") {
let matches
while ((matches = stepIndexRegex.exec(obj[key])) !== null) {
const referencedStep = parseInt(matches[1])
obj[key] = updateActionStep(
obj[key],
referencedStep,
steps[referencedStep]?.id
)
}
} else if (typeof obj[key] === "object" && obj[key] !== null) {
migrateReferencesInObject({
obj: obj[key],
steps,
})
}
}
}

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,177 @@
<script>
import { Input, Icon, Drawer, Button } from "@budibase/bbui"
import { isJSBinding } from "@budibase/string-templates"
import { createEventDispatcher } from "svelte"
export let filter
export let disabled = false
export let bindings = []
export let panel
export let drawerTitle
export let toReadable
export let toRuntime
const dispatch = createEventDispatcher()
let bindingDrawer
let fieldValue
$: fieldValue = filter?.field
$: readableValue = toReadable ? toReadable(bindings, fieldValue) : fieldValue
$: drawerValue = toDrawerValue(fieldValue)
$: isJS = isJSBinding(fieldValue)
const drawerOnChange = e => {
drawerValue = e.detail
}
const onChange = e => {
fieldValue = e.detail
dispatch("change", {
field: toRuntime ? toRuntime(bindings, fieldValue) : fieldValue,
})
}
const onConfirmBinding = () => {
dispatch("change", {
field: toRuntime ? toRuntime(bindings, drawerValue) : drawerValue,
})
}
/**
* Converts arrays into strings. The CodeEditor expects a string or encoded JS
*
* @param{string} fieldValue
*/
const toDrawerValue = fieldValue => {
return Array.isArray(fieldValue) ? fieldValue.join(",") : readableValue
}
</script>
<div>
<Drawer
on:drawerHide
on:drawerShow
bind:this={bindingDrawer}
title={drawerTitle || ""}
forceModal
>
<Button
cta
slot="buttons"
on:click={() => {
onConfirmBinding()
bindingDrawer.hide()
}}
>
Confirm
</Button>
<svelte:component
this={panel}
slot="body"
value={drawerValue}
allowJS
allowHelpers
allowHBS
on:change={drawerOnChange}
{bindings}
/>
</Drawer>
<div class="field-wrap" class:bindings={true}>
<div class="field">
<Input
disabled={filter.noValue}
readonly={true}
value={isJS ? "(JavaScript function)" : readableValue}
on:change={onChange}
/>
</div>
<div class="binding-control">
{#if !disabled}
<!-- svelte-ignore a11y-click-events-have-key-events -->
<!-- svelte-ignore a11y-no-static-element-interactions -->
<div
class="icon binding"
on:click={() => {
bindingDrawer.show()
}}
>
<Icon size="S" name="FlashOn" />
</div>
{/if}
</div>
</div>
</div>
<style>
.field-wrap {
display: flex;
}
.field {
flex: 1;
}
.field-wrap.bindings .field :global(.spectrum-Form-itemField),
.field-wrap.bindings .field :global(input),
.field-wrap.bindings .field :global(.spectrum-Picker) {
border-top-right-radius: 0px;
border-bottom-right-radius: 0px;
}
.field-wrap.bindings
.field
:global(.spectrum-InputGroup.spectrum-Datepicker) {
min-width: unset;
border-radius: 0px;
}
.field-wrap.bindings
.field
:global(
.spectrum-InputGroup.spectrum-Datepicker
.spectrum-Textfield-input.spectrum-InputGroup-input
) {
width: 100%;
}
.binding-control .icon {
border: 1px solid
var(
--spectrum-textfield-m-border-color,
var(--spectrum-alias-border-color)
);
border-left: 0px;
border-top-right-radius: 4px;
border-bottom-right-radius: 4px;
justify-content: center;
align-items: center;
display: flex;
flex-direction: row;
box-sizing: border-box;
width: 31px;
color: var(--spectrum-alias-text-color);
background-color: var(--spectrum-global-color-gray-75);
transition: background-color
var(--spectrum-global-animation-duration-100, 130ms),
box-shadow var(--spectrum-global-animation-duration-100, 130ms),
border-color var(--spectrum-global-animation-duration-100, 130ms);
height: calc(var(--spectrum-alias-item-height-m));
}
.binding-control .icon.binding {
color: var(--yellow);
}
.binding-control .icon:hover {
cursor: pointer;
background-color: var(--spectrum-global-color-gray-50);
border-color: var(--spectrum-alias-border-color-hover);
color: var(--spectrum-alias-text-color-hover);
}
.binding-control .icon.binding:hover {
color: var(--yellow);
}
</style>

View File

@ -16,6 +16,7 @@
import { QueryUtils, Constants } from "@budibase/frontend-core"
import { getContext, createEventDispatcher } from "svelte"
import FilterField from "./FilterField.svelte"
import ConditionField from "./ConditionField.svelte"
const dispatch = createEventDispatcher()
const {
@ -32,8 +33,10 @@
export let datasource
export let behaviourFilters = false
export let allowBindings = false
export let allowOnEmpty = true
export let builderType = "filter"
export let docsURL = "https://docs.budibase.com/docs/searchfilter-data"
// Review
export let bindings
export let panel
export let toReadable
@ -91,6 +94,10 @@
}
const getValidOperatorsForType = filter => {
if (builderType === "condition") {
return [OperatorOptions.Equals, OperatorOptions.NotEquals]
}
if (!filter?.field && !filter?.name) {
return []
}
@ -210,6 +217,9 @@
} else if (addFilter) {
targetGroup.filters.push({
valueType: FilterValueType.VALUE,
...(builderType === "condition"
? { operator: OperatorOptions.Equals.value }
: {}),
})
} else if (group) {
editable.groups[groupIdx] = {
@ -274,7 +284,7 @@
placeholder={false}
/>
</span>
<span>of the following filter groups:</span>
<span>of the following {builderType} groups:</span>
</div>
{/if}
{#if editableFilters?.groups?.length}
@ -303,7 +313,7 @@
placeholder={false}
/>
</span>
<span>of the following filters are matched:</span>
<span>of the following {builderType}s are matched:</span>
</div>
<div class="group-actions">
<Icon
@ -334,20 +344,39 @@
<div class="filters">
{#each group.filters as filter, filterIdx}
<div class="filter">
<Select
value={filter.field}
options={fieldOptions}
on:change={e => {
const updated = { ...filter, field: e.detail }
onFieldChange(updated)
onFilterFieldUpdate(updated, groupIdx, filterIdx)
}}
placeholder="Column"
/>
{#if builderType === "filter"}
<Select
value={filter.field}
options={fieldOptions}
on:change={e => {
const updated = { ...filter, field: e.detail }
onFieldChange(updated)
onFilterFieldUpdate(updated, groupIdx, filterIdx)
}}
placeholder="Column"
/>
{:else}
<ConditionField
placeholder="Value"
{filter}
drawerTitle={"Edit Binding"}
{bindings}
{panel}
{toReadable}
{toRuntime}
on:change={e => {
const updated = {
...filter,
field: e.detail.field,
}
delete updated.valueType
onFilterFieldUpdate(updated, groupIdx, filterIdx)
}}
/>
{/if}
<Select
value={filter.operator}
disabled={!filter.field}
disabled={!filter.field && builderType === "filter"}
options={getValidOperatorsForType(filter)}
on:change={e => {
const updated = { ...filter, operator: e.detail }
@ -356,9 +385,11 @@
}}
placeholder={false}
/>
<FilterField
placeholder="Value"
drawerTitle={builderType === "condition"
? "Edit binding"
: null}
{allowBindings}
{filter}
{schemaFields}
@ -396,7 +427,7 @@
<div class="filters-footer">
<Layout noPadding>
{#if behaviourFilters && editableFilters?.groups?.length}
{#if behaviourFilters && allowOnEmpty && editableFilters?.groups?.length}
<div class="empty-filter">
<span>Return</span>
<span class="empty-filter-picker">
@ -413,7 +444,7 @@
placeholder={false}
/>
</span>
<span>when all filters are empty</span>
<span>when all {builderType}s are empty</span>
</div>
{/if}
<div class="add-group">
@ -427,17 +458,16 @@
})
}}
>
Add filter group
Add {builderType} group
</Button>
<a
href="https://docs.budibase.com/docs/searchfilter-data"
target="_blank"
>
<Icon
name="HelpOutline"
color="var(--spectrum-global-color-gray-600)"
/>
</a>
{#if docsURL}
<a href={docsURL} target="_blank">
<Icon
name="HelpOutline"
color="var(--spectrum-global-color-gray-600)"
/>
</a>
{/if}
</div>
</Layout>
</div>

View File

@ -21,6 +21,7 @@
export let allowBindings = false
export let schemaFields
export let panel
export let drawerTitle
export let toReadable
export let toRuntime
@ -133,7 +134,7 @@
on:drawerHide
on:drawerShow
bind:this={bindingDrawer}
title={filter.field}
title={drawerTitle || filter.field}
forceModal
>
<Button

View File

@ -308,7 +308,10 @@ class Orchestrator {
)
}
private async executeSteps(steps: AutomationStep[]): Promise<void> {
private async executeSteps(
steps: AutomationStep[],
pathIdx?: number
): Promise<void> {
return tracer.trace(
"Orchestrator.executeSteps",
{ resource: "automation" },
@ -324,10 +327,17 @@ class Orchestrator {
while (stepIndex < steps.length) {
const step = steps[stepIndex]
if (step.stepId === AutomationActionStepId.BRANCH) {
await this.executeBranchStep(step)
// stepIndex for current step context offset
// pathIdx relating to the full list of steps in the run
await this.executeBranchStep(step, stepIndex + (pathIdx || 0))
stepIndex++
} else if (step.stepId === AutomationActionStepId.LOOP) {
stepIndex = await this.executeLoopStep(step, steps, stepIndex)
stepIndex = await this.executeLoopStep(
step,
steps,
stepIndex,
pathIdx
)
} else {
if (!this.stopped) {
await this.executeStep(step)
@ -350,11 +360,14 @@ class Orchestrator {
private async executeLoopStep(
loopStep: LoopStep,
steps: AutomationStep[],
currentIndex: number
stepIdx: number,
pathIdx?: number
): Promise<number> {
await processObject(loopStep.inputs, this.context)
const iterations = getLoopIterations(loopStep)
let stepToLoopIndex = currentIndex + 1
let stepToLoopIndex = stepIdx + 1
let pathStepIdx = (pathIdx || stepIdx) + 1
let iterationCount = 0
let shouldCleanup = true
@ -365,7 +378,7 @@ class Orchestrator {
)
} catch (err) {
this.updateContextAndOutput(
stepToLoopIndex,
pathStepIdx,
steps[stepToLoopIndex],
{},
{
@ -385,7 +398,7 @@ class Orchestrator {
(loopStep.inputs.iterations && loopStepIndex === maxIterations)
) {
this.updateContextAndOutput(
stepToLoopIndex,
pathStepIdx,
steps[stepToLoopIndex],
{
items: this.loopStepOutputs,
@ -412,7 +425,7 @@ class Orchestrator {
if (isFailure) {
this.updateContextAndOutput(
loopStepIndex,
pathStepIdx,
steps[stepToLoopIndex],
{
items: this.loopStepOutputs,
@ -427,11 +440,11 @@ class Orchestrator {
break
}
this.context.steps[currentIndex + 1] = {
this.context.steps[pathStepIdx] = {
currentItem: this.getCurrentLoopItem(loopStep, loopStepIndex),
}
stepToLoopIndex = currentIndex + 1
stepToLoopIndex = stepIdx + 1
await this.executeStep(steps[stepToLoopIndex], stepToLoopIndex)
iterationCount++
@ -451,7 +464,7 @@ class Orchestrator {
}
// Loop Step clean up
this.executionOutput.steps.splice(currentIndex + 1, 0, {
this.executionOutput.steps.splice(pathStepIdx, 0, {
id: steps[stepToLoopIndex].id,
stepId: steps[stepToLoopIndex].stepId,
outputs: tempOutput,
@ -471,7 +484,10 @@ class Orchestrator {
return stepToLoopIndex + 1
}
private async executeBranchStep(branchStep: BranchStep): Promise<void> {
private async executeBranchStep(
branchStep: BranchStep,
pathIdx?: number
): Promise<void> {
const { branches, children } = branchStep.inputs
for (const branch of branches) {
@ -479,6 +495,7 @@ class Orchestrator {
if (condition) {
const branchStatus = {
status: `${branch.name} branch taken`,
branchId: `${branch.id}`,
success: true,
}
@ -490,8 +507,9 @@ class Orchestrator {
)
this.context.steps[this.context.steps.length] = branchStatus
const branchSteps = children?.[branch.name] || []
await this.executeSteps(branchSteps)
const branchSteps = children?.[branch.id] || []
// A final +1 to accomodate the branch step itself
await this.executeSteps(branchSteps, (pathIdx || 0) + 1)
return
}
}

View File

@ -116,6 +116,7 @@ export type BranchStepInputs = {
}
export type Branch = {
id: any
name: string
condition: SearchFilters
}