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:
parent
185fd557dd
commit
9382ca4c0b
|
@ -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")
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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>
|
|
@ -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;
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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>
|
|
@ -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 {
|
||||
|
|
|
@ -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>
|
|
@ -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}
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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
|
||||
/>
|
||||
|
|
|
@ -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
|
@ -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>
|
|
@ -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>
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
}
|
||||
}
|
||||
|
|
|
@ -116,6 +116,7 @@ export type BranchStepInputs = {
|
|||
}
|
||||
|
||||
export type Branch = {
|
||||
id: any
|
||||
name: string
|
||||
condition: SearchFilters
|
||||
}
|
||||
|
|
Loading…
Reference in New Issue