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 { admin, licensing } from "stores/portal"
import { externalActions } from "./ExternalActions" import { externalActions } from "./ExternalActions"
import { TriggerStepID, ActionStepID } from "constants/backend/automations" import { TriggerStepID, ActionStepID } from "constants/backend/automations"
import { checkForCollectStep } from "helpers/utils"
export let blockIdx export let block
export let lastStep
export let modal export let modal
let syncAutomationsEnabled = $licensing.syncAutomationsEnabled let syncAutomationsEnabled = $licensing.syncAutomationsEnabled
@ -29,7 +27,15 @@
ActionStepID.TRIGGER_AUTOMATION_RUN, 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 = () => { const disabled = () => {
return { return {
@ -100,9 +106,14 @@
action.stepId, action.stepId,
action action
) )
await automationStore.actions.addBlockToAutomation(newBlock, blockIdx + 1)
await automationStore.actions.addBlockToAutomation(
newBlock,
blockRef ? blockRef.pathTo : block.pathTo
)
modal.hide() modal.hide()
} catch (error) { } catch (error) {
console.error(error)
notifications.error("Error saving automation") 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, automationHistoryStore,
} from "stores/builder" } from "stores/builder"
import ConfirmDialog from "components/common/ConfirmDialog.svelte" import ConfirmDialog from "components/common/ConfirmDialog.svelte"
import FlowItem from "./FlowItem.svelte"
import TestDataModal from "./TestDataModal.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 { Icon, notifications, Modal, Toggle } from "@budibase/bbui"
import { ActionStepID } from "constants/backend/automations" import { ActionStepID } from "constants/backend/automations"
import UndoRedoControl from "components/common/UndoRedoControl.svelte" import UndoRedoControl from "components/common/UndoRedoControl.svelte"
import StepNode from "./StepNode.svelte" import StepNode from "./StepNode.svelte"
import { memo } from "@budibase/frontend-core"
// 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 { sdk } from "@budibase/shared-core" import { sdk } from "@budibase/shared-core"
import { migrateReferencesInObject } from "dataBinding"
import { cloneDeep } from "lodash/fp"
import { onMount } from "svelte"
export let automation export let automation
const memoAutomation = memo(automation)
let testDataModal let testDataModal
let confirmDeleteDialog let confirmDeleteDialog
let scrolling = false let scrolling = false
let blockRefs = {}
let treeEle
$: blocks = getBlocks(automation).filter(x => x.stepId !== ActionStepID.LOOP) // Memo auto
$: isRowAction = sdk.automations.isRowAction(automation) $: 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 => { const getBlocks = automation => {
let blocks = [] let blocks = []
@ -62,19 +107,21 @@
scrolling = false 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> </script>
<!-- svelte-ignore a11y-click-events-have-key-events --> <!-- svelte-ignore a11y-click-events-have-key-events -->
<!-- svelte-ignore a11y-no-static-element-interactions --> <!-- svelte-ignore a11y-no-static-element-interactions -->
<div class="header" class:scrolling> <div class="header" class:scrolling>
<button
on:click={() => {
automationStore.actions.traverse($selectedAutomation)
console.log($automationStore)
}}
>
TEST
</button>
<div class="header-left"> <div class="header-left">
<UndoRedoControl store={automationHistoryStore} /> <UndoRedoControl store={automationHistoryStore} />
</div> </div>
@ -117,32 +164,21 @@
</div> </div>
<div class="canvas" on:scroll={handleScroll}> <div class="canvas" on:scroll={handleScroll}>
<div class="content"> <div class="content">
<!-- <div class="tree">
Separate out the Trigger node? <div class="root" bind:this={treeEle}>
--> {#if Object.keys(blockRefs).length}
<div class="root"> {#each blocks as block, idx (block.id)}
{#each blocks as block, idx (block.id)} <StepNode
<StepNode step={blocks[idx]}
step={blocks[idx]} stepIdx={idx}
stepIdx={idx} isLast={blocks?.length - 1 === idx}
isLast={blocks?.length - 1 === idx} automation={$memoAutomation}
{automation} blocks={blockRefs}
/> />
{/each} {/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} />
{/if} {/if}
</div> </div>
{/each} --> </div>
</div> </div>
</div> </div>
<ConfirmDialog <ConfirmDialog
@ -193,15 +229,7 @@
box-sizing: border-box; box-sizing: border-box;
} }
/* .block {
display: flex;
flex-direction: column;
justify-content: flex-start;
align-items: center;
} */
.content { .content {
flex-grow: 1;
padding: 23px 23px 80px; padding: 23px 23px 80px;
box-sizing: border-box; box-sizing: border-box;
/* overflow-x: hidden; */ /* overflow-x: hidden; */
@ -212,7 +240,11 @@
border-bottom: var(--border-light); border-bottom: var(--border-light);
z-index: 1; z-index: 1;
} }
.tree {
justify-content: center;
display: inline-flex;
min-width: 100%;
}
.header { .header {
z-index: 1; z-index: 1;
display: flex; display: flex;

View File

@ -11,45 +11,47 @@
Layout, Layout,
Detail, Detail,
Modal, Modal,
Button,
notifications,
Label, Label,
AbsTooltip, AbsTooltip,
InlineAlert, InlineAlert,
} from "@budibase/bbui" } from "@budibase/bbui"
import AutomationBlockSetup from "../../SetupPanel/AutomationBlockSetup.svelte" import AutomationBlockSetup from "../../SetupPanel/AutomationBlockSetup.svelte"
import CreateWebhookModal from "components/automation/Shared/CreateWebhookModal.svelte" import CreateWebhookModal from "components/automation/Shared/CreateWebhookModal.svelte"
import ActionModal from "./ActionModal.svelte"
import FlowItemHeader from "./FlowItemHeader.svelte" import FlowItemHeader from "./FlowItemHeader.svelte"
import RoleSelect from "components/design/settings/controls/RoleSelect.svelte" import RoleSelect from "components/design/settings/controls/RoleSelect.svelte"
import { ActionStepID, TriggerStepID } from "constants/backend/automations" import { ActionStepID, TriggerStepID } from "constants/backend/automations"
import { AutomationStepType } from "@budibase/types"
import FlowItemActions from "./FlowItemActions.svelte" import FlowItemActions from "./FlowItemActions.svelte"
export let block export let block
export let blockRef
export let testDataModal export let testDataModal
export let idx export let idx
export let isLast export let automation
export let bindings
let selected let selected
let webhookModal let webhookModal
let actionModal
let open = true let open = true
let showLooping = false let showLooping = false
let role 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 step => step.stepId === ActionStepID.COLLECT
) )
$: automationId = $selectedAutomation?._id $: automationId = automation?._id
$: isTrigger = block.type === "TRIGGER" $: isTrigger = block.type === AutomationStepType.TRIGGER
$: lastStep = blockRef?.terminating
$: steps = $selectedAutomation?.definition?.steps ?? [] $: loopBlock = pathSteps.find(x => x.blockToLoop === block.id)
$: 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
)
$: isAppAction = block?.stepId === TriggerStepID.APP $: isAppAction = block?.stepId === TriggerStepID.APP
$: isAppAction && setPermissions(role) $: isAppAction && setPermissions(role)
$: isAppAction && getPermissions(automationId) $: isAppAction && getPermissions(automationId)
@ -79,23 +81,13 @@
} }
} }
async function removeLooping() { async function deleteStep() {
try { await automationStore.actions.deleteAutomationBlock(blockRef.pathTo)
await automationStore.actions.deleteAutomationBlock(loopBlock)
} catch (error) {
notifications.error("Error saving automation")
}
} }
async function deleteStep() { async function removeLooping() {
try { let loopBlockRef = $automationStore.blocks[blockRef.looped]
if (loopBlock) { await automationStore.actions.deleteAutomationBlock(loopBlockRef.pathTo)
await automationStore.actions.deleteAutomationBlock(loopBlock)
}
await automationStore.actions.deleteAutomationBlock(block, blockIdx)
} catch (error) {
notifications.error("Error saving automation")
}
} }
async function addLooping() { async function addLooping() {
@ -106,128 +98,143 @@
loopDefinition loopDefinition
) )
loopBlock.blockToLoop = block.id loopBlock.blockToLoop = block.id
await automationStore.actions.addBlockToAutomation(loopBlock, blockIdx) await automationStore.actions.addBlockToAutomation(
loopBlock,
blockRef.pathTo
)
} }
</script> </script>
<!-- svelte-ignore a11y-click-events-have-key-events --> {#if block.stepId !== "LOOP"}
<!-- svelte-ignore a11y-no-static-element-interactions --> <!-- svelte-ignore a11y-click-events-have-key-events -->
<div class={`block ${block.type} hoverable`} class:selected on:click={() => {}}> <!-- svelte-ignore a11y-no-static-element-interactions -->
{#if loopBlock} <div
<div class="blockSection"> id={`block-${block.id}`}
<div class={`block ${block.type} hoverable`}
on:click={() => { class:selected
showLooping = !showLooping on:click={() => {}}
}} >
class="splitHeader" {#if loopBlock}
> <div class="blockSection">
<div class="center-items"> <div
<svg on:click={() => {
width="28px" showLooping = !showLooping
height="28px" }}
class="spectrum-Icon" class="splitHeader"
style="color:var(--spectrum-global-color-gray-700);" >
focusable="false" <div class="center-items">
> <svg
<use xlink:href="#spectrum-icon-18-Reuse" /> width="28px"
</svg> height="28px"
<div class="iconAlign"> class="spectrum-Icon"
<Detail size="S">Looping</Detail> 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>
<div class="blockTitle"> <div class="blockTitle">
<AbsTooltip type="negative" text="Remove looping"> <AbsTooltip type="negative" text="Remove looping">
<Icon on:click={removeLooping} hoverable name="DeleteOutline" /> <Icon on:click={removeLooping} hoverable name="DeleteOutline" />
</AbsTooltip> </AbsTooltip>
<div style="margin-left: 10px;" on:click={() => {}}> <div style="margin-left: 10px;" on:click={() => {}}>
<Icon hoverable name={showLooping ? "ChevronDown" : "ChevronUp"} /> <Icon
hoverable
name={showLooping ? "ChevronDown" : "ChevronUp"}
/>
</div>
</div> </div>
</div> </div>
</div> </div>
</div>
<Divider noMargin /> <Divider noMargin />
{#if !showLooping} {#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"> <div class="blockSection">
<Layout noPadding gap="S"> <Layout noPadding gap="S">
{#if isAppAction}
<div>
<Label>Role</Label>
<RoleSelect bind:value={role} />
</div>
{/if}
<AutomationBlockSetup <AutomationBlockSetup
schemaProperties={Object.entries( schemaProperties={Object.entries(
$automationStore.blockDefinitions.ACTION.LOOP.schema.inputs block?.schema?.inputs?.properties || {}
.properties
)} )}
{block}
{webhookModal} {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> </Layout>
</div> </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}
{/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} {/if}
<Modal bind:this={actionModal} width="30%">
<ActionModal modal={actionModal} {lastStep} {blockIdx} />
</Modal>
<Modal bind:this={webhookModal} width="30%"> <Modal bind:this={webhookModal} width="30%">
<CreateWebhookModal /> <CreateWebhookModal />
</Modal> </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 showTestStatus = false
export let testResult export let testResult
export let isTrigger export let isTrigger
export let idx
export let addLooping export let addLooping
export let deleteStep export let deleteStep
export let enableNaming = true export let enableNaming = true
export let itemName
let validRegex = /^[A-Za-z0-9_\s]+$/ let validRegex = /^[A-Za-z0-9_\s]+$/
let typing = false let typing = false
let editing = false let editing = false
@ -21,10 +22,11 @@
$: stepNames = $selectedAutomation?.definition.stepNames $: stepNames = $selectedAutomation?.definition.stepNames
$: allSteps = $selectedAutomation?.definition.steps || [] $: allSteps = $selectedAutomation?.definition.steps || []
$: automationName = stepNames?.[block.id] || block?.name || "" $: automationName = itemName || stepNames?.[block.id] || block?.name || ""
$: automationNameError = getAutomationNameError(automationName) $: automationNameError = getAutomationNameError(automationName)
$: status = updateStatus(testResult) $: status = updateStatus(testResult)
$: isHeaderTrigger = isTrigger || block.type === "TRIGGER" $: isHeaderTrigger = isTrigger || block.type === "TRIGGER"
$: isBranch = block.stepId === "BRANCH"
$: { $: {
if (!testResult) { if (!testResult) {
@ -33,9 +35,9 @@
)?.[0] )?.[0]
} }
} }
$: loopBlock = $selectedAutomation?.definition.steps.find(
x => x.blockToLoop === block?.id $: blockRef = $automationStore.blocks[block.id]
) $: isLooped = blockRef?.looped
async function onSelect(block) { async function onSelect(block) {
await automationStore.update(state => { await automationStore.update(state => {
@ -84,30 +86,18 @@
return null 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 = () => { const startEditing = () => {
editing = true editing = true
typing = true typing = true
} }
const stopEditing = async () => { const stopEditing = () => {
editing = false editing = false
typing = false typing = false
if (automationNameError) { if (automationNameError) {
automationName = stepNames[block.id] || block?.name automationName = stepNames[block.id] || block?.name
} else { } else {
await saveName() dispatch("update", automationName)
} }
} }
</script> </script>
@ -118,7 +108,6 @@
class:typing={typing && !automationNameError && editing} class:typing={typing && !automationNameError && editing}
class:typing-error={automationNameError && editing} class:typing-error={automationNameError && editing}
class="blockSection" class="blockSection"
on:click={() => dispatch("toggle")}
> >
<div class="splitHeader"> <div class="splitHeader">
<div class="center-items"> <div class="center-items">
@ -144,16 +133,14 @@
{#if isHeaderTrigger} {#if isHeaderTrigger}
<Body size="XS"><b>Trigger</b></Body> <Body size="XS"><b>Trigger</b></Body>
{:else} {:else}
<div style="margin-left: 2px;"> <Body size="XS"><b>{isBranch ? "Branch" : "Step"}</b></Body>
<Body size="XS"><b>Step {idx}</b></Body>
</div>
{/if} {/if}
{#if enableNaming} {#if enableNaming}
<input <input
class="input-text" class="input-text"
disabled={!enableNaming} disabled={!enableNaming}
placeholder="Enter step name" placeholder={`Enter ${isBranch ? "branch" : "step"} name`}
name="name" name="name"
autocomplete="off" autocomplete="off"
value={automationName} value={automationName}
@ -208,8 +195,9 @@
onSelect(block) onSelect(block)
}} }}
> >
<slot name="custom-actions" />
{#if !showTestStatus} {#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"> <AbsTooltip type="info" text="Add looping">
<Icon on:click={addLooping} hoverable name="RotateCW" /> <Icon on:click={addLooping} hoverable name="RotateCW" />
</AbsTooltip> </AbsTooltip>
@ -220,6 +208,9 @@
</AbsTooltip> </AbsTooltip>
{/if} {/if}
{/if} {/if}
{#if !showTestStatus && !isHeaderTrigger}
<span class="action-spacer" />
{/if}
{#if !showTestStatus} {#if !showTestStatus}
<Icon <Icon
on:click={e => { on:click={e => {
@ -245,6 +236,9 @@
</div> </div>
<style> <style>
.action-spacer {
border-left: 1px solid var(--spectrum-global-color-gray-300);
}
.status-container { .status-container {
display: flex; display: flex;
align-items: center; align-items: center;
@ -298,6 +292,8 @@
font-size: var(--spectrum-alias-font-size-default); font-size: var(--spectrum-alias-font-size-default);
font-family: var(--font-sans); font-family: var(--font-sans);
text-overflow: ellipsis; text-overflow: ellipsis;
padding-left: 0px;
border: 0px;
} }
input:focus { 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 FlowItemHeader from "./FlowChart/FlowItemHeader.svelte"
import { ActionStepID } from "constants/backend/automations" import { ActionStepID } from "constants/backend/automations"
import { JsonView } from "@zerodevx/svelte-json-view" import { JsonView } from "@zerodevx/svelte-json-view"
import { automationStore } from "stores/builder"
import { AutomationActionStepId } from "@budibase/types"
export let automation export let automation
export let testResults 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) { if (testResults.message) {
blocks = automation?.definition?.trigger blocks = automation?.definition?.trigger
? [automation.definition.trigger] ? [automation.definition.trigger]
: [] : []
} else if (automation) { } else if (automation) {
blocks = [] const terminatingStep = filteredResults.at(-1)
if (automation.definition.trigger) { const terminatingBlockRef = $automationStore.blocks[terminatingStep.id]
blocks.push(automation.definition.trigger) const pathSteps = automationStore.actions.getPathSteps(
} terminatingBlockRef.pathTo,
blocks = blocks automation
.concat(automation.definition.steps || []) )
.filter(x => x.stepId !== ActionStepID.LOOP) blocks = [...pathSteps].filter(x => x.stepId !== ActionStepID.LOOP)
} else if (filteredResults) { } else if (filteredResults) {
blocks = filteredResults || [] blocks = filteredResults || []
// make sure there is an ID for each block being displayed // make sure there is an ID for each block being displayed
@ -60,6 +68,9 @@
{#if block.stepId !== ActionStepID.LOOP} {#if block.stepId !== ActionStepID.LOOP}
<FlowItemHeader <FlowItemHeader
enableNaming={false} enableNaming={false}
itemName={block.stepId === AutomationActionStepId.BRANCH
? getBranchName(block, filteredResults?.[idx].outputs?.branchId)
: null}
open={!!openBlocks[block.id]} open={!!openBlocks[block.id]}
on:toggle={() => (openBlocks[block.id] = !openBlocks[block.id])} on:toggle={() => (openBlocks[block.id] = !openBlocks[block.id])}
isTrigger={idx === 0} isTrigger={idx === 0}

View File

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

View File

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

View File

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

View File

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