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