Automation foreach block
This commit is contained in:
parent
b067b5ac2e
commit
554cefe997
|
@ -39,6 +39,7 @@
|
||||||
if (v.internal) {
|
if (v.internal) {
|
||||||
acc[k] = v
|
acc[k] = v
|
||||||
}
|
}
|
||||||
|
delete acc.LOOP
|
||||||
return acc
|
return acc
|
||||||
}, {})
|
}, {})
|
||||||
|
|
||||||
|
|
|
@ -9,8 +9,8 @@
|
||||||
Modal,
|
Modal,
|
||||||
Button,
|
Button,
|
||||||
StatusLight,
|
StatusLight,
|
||||||
ActionButton,
|
|
||||||
Select,
|
Select,
|
||||||
|
Label,
|
||||||
notifications,
|
notifications,
|
||||||
} from "@budibase/bbui"
|
} from "@budibase/bbui"
|
||||||
import AutomationBlockSetup from "../../SetupPanel/AutomationBlockSetup.svelte"
|
import AutomationBlockSetup from "../../SetupPanel/AutomationBlockSetup.svelte"
|
||||||
|
@ -25,7 +25,6 @@
|
||||||
let webhookModal
|
let webhookModal
|
||||||
let actionModal
|
let actionModal
|
||||||
let resultsModal
|
let resultsModal
|
||||||
let setupToggled
|
|
||||||
let blockComplete
|
let blockComplete
|
||||||
|
|
||||||
$: rowControl = $automationStore.selectedAutomation.automation.rowControl
|
$: rowControl = $automationStore.selectedAutomation.automation.rowControl
|
||||||
|
@ -52,6 +51,8 @@
|
||||||
block.schema?.inputs?.properties || {}
|
block.schema?.inputs?.properties || {}
|
||||||
).every(x => block?.inputs[x])
|
).every(x => block?.inputs[x])
|
||||||
|
|
||||||
|
$: loopingSelected = !!block.llop
|
||||||
|
$: showLooping = false
|
||||||
async function deleteStep() {
|
async function deleteStep() {
|
||||||
try {
|
try {
|
||||||
automationStore.actions.deleteAutomationBlock(block)
|
automationStore.actions.deleteAutomationBlock(block)
|
||||||
|
@ -76,6 +77,37 @@
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
async function removeLooping() {
|
||||||
|
loopingSelected = false
|
||||||
|
const idx =
|
||||||
|
$automationStore.selectedAutomation.automation.definition.steps.findIndex(
|
||||||
|
x => x.id === block.id
|
||||||
|
)
|
||||||
|
|
||||||
|
delete $automationStore.selectedAutomation.automation.definition.steps[idx]
|
||||||
|
.loop
|
||||||
|
|
||||||
|
await automationStore.actions.save(
|
||||||
|
$automationStore.selectedAutomation?.automation
|
||||||
|
)
|
||||||
|
}*/
|
||||||
|
async function addLooping() {
|
||||||
|
loopingSelected = true
|
||||||
|
const loopDefinition = $automationStore.blockDefinitions.ACTION.LOOP
|
||||||
|
|
||||||
|
const loopBlock = $automationStore.selectedAutomation.constructBlock(
|
||||||
|
"ACTION",
|
||||||
|
"LOOP",
|
||||||
|
loopDefinition
|
||||||
|
)
|
||||||
|
loopBlock.blockToLoop = block.id
|
||||||
|
automationStore.actions.addBlockToAutomation(loopBlock, blockIdx - 1)
|
||||||
|
await automationStore.actions.save(
|
||||||
|
$automationStore.selectedAutomation?.automation
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
async function onSelect(block) {
|
async function onSelect(block) {
|
||||||
await automationStore.update(state => {
|
await automationStore.update(state => {
|
||||||
state.selectedBlock = block
|
state.selectedBlock = block
|
||||||
|
@ -84,13 +116,61 @@
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<div
|
<div class={`block ${block.type} hoverable`} class:selected on:click={() => {}}>
|
||||||
class={`block ${block.type} hoverable`}
|
{#if loopingSelected}
|
||||||
class:selected
|
<div class="blockSection">
|
||||||
on:click={() => {
|
<div
|
||||||
onSelect(block)
|
on:click={() => {
|
||||||
}}
|
showLooping = !showLooping
|
||||||
>
|
}}
|
||||||
|
class="splitHeader"
|
||||||
|
>
|
||||||
|
<div class="center-items">
|
||||||
|
<svg
|
||||||
|
width="28px"
|
||||||
|
height="28px"
|
||||||
|
class="spectrum-Icon"
|
||||||
|
style="color:grey;"
|
||||||
|
focusable="false"
|
||||||
|
>
|
||||||
|
<use xlink:href="#spectrum-icon-18-Reuse" />
|
||||||
|
</svg>
|
||||||
|
<div class="iconAlign">
|
||||||
|
<Detail size="S">Looping</Detail>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="blockTitle">
|
||||||
|
<div
|
||||||
|
style="margin-left: 10px;"
|
||||||
|
on:click={() => {
|
||||||
|
onSelect(block)
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Icon name={showLooping ? "ChevronDown" : "ChevronUp"} />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<Divider noMargin />
|
||||||
|
{#if !showLooping}
|
||||||
|
<div class="blockSection">
|
||||||
|
<Layout noPadding gap="S">
|
||||||
|
<AutomationBlockSetup
|
||||||
|
schemaProperties={Object.entries(
|
||||||
|
$automationStore.blockDefinitions.ACTION.LOOP.schema.inputs
|
||||||
|
.properties
|
||||||
|
)}
|
||||||
|
{block}
|
||||||
|
{webhookModal}
|
||||||
|
isLoop={true}
|
||||||
|
/>
|
||||||
|
</Layout>
|
||||||
|
</div>
|
||||||
|
<Divider noMargin />
|
||||||
|
{/if}
|
||||||
|
{/if}
|
||||||
|
|
||||||
<div class="blockSection">
|
<div class="blockSection">
|
||||||
<div
|
<div
|
||||||
on:click={() => {
|
on:click={() => {
|
||||||
|
@ -127,34 +207,42 @@
|
||||||
<Detail size="S">{block?.name?.toUpperCase() || ""}</Detail>
|
<Detail size="S">{block?.name?.toUpperCase() || ""}</Detail>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
{#if testResult && testResult[0]}
|
<div class="blockTitle">
|
||||||
<span on:click={() => resultsModal.show()}>
|
{#if testResult && testResult[0]}
|
||||||
<StatusLight
|
<div style="float: right;" on:click={() => resultsModal.show()}>
|
||||||
positive={isTrigger || testResult[0].outputs?.success}
|
<StatusLight
|
||||||
negative={!testResult[0].outputs?.success}
|
positive={isTrigger || testResult[0].outputs?.success}
|
||||||
><Body size="XS">View response</Body></StatusLight
|
negative={!testResult[0].outputs?.success}
|
||||||
>
|
><Body size="XS">View response</Body></StatusLight
|
||||||
</span>
|
>
|
||||||
{/if}
|
</div>
|
||||||
|
{/if}
|
||||||
|
<div
|
||||||
|
style="margin-left: 10px;"
|
||||||
|
on:click={() => {
|
||||||
|
onSelect(block)
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Icon name={blockComplete ? "ChevronDown" : "ChevronUp"} />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
{#if !blockComplete}
|
{#if !blockComplete}
|
||||||
<Divider noMargin />
|
<Divider noMargin />
|
||||||
<div class="blockSection">
|
<div class="blockSection">
|
||||||
<Layout noPadding gap="S">
|
<Layout noPadding gap="S">
|
||||||
<div class="splitHeader">
|
{#if !isTrigger}
|
||||||
<ActionButton
|
<div>
|
||||||
on:click={() => {
|
|
||||||
onSelect(block)
|
|
||||||
setupToggled = !setupToggled
|
|
||||||
}}
|
|
||||||
quiet
|
|
||||||
icon={setupToggled ? "ChevronDown" : "ChevronRight"}
|
|
||||||
>
|
|
||||||
<Detail size="S">Setup</Detail>
|
|
||||||
</ActionButton>
|
|
||||||
{#if !isTrigger}
|
|
||||||
<div class="block-options">
|
<div class="block-options">
|
||||||
|
{#if !loopingSelected}
|
||||||
|
<div style="display: flex;" on:click={() => addLooping()}>
|
||||||
|
<Icon name="Reuse" />
|
||||||
|
<div style="margin-left:10px">
|
||||||
|
<Label>Add looping</Label>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{/if}
|
||||||
{#if showBindingPicker}
|
{#if showBindingPicker}
|
||||||
<div>
|
<div>
|
||||||
<Select
|
<Select
|
||||||
|
@ -172,20 +260,18 @@
|
||||||
<Icon name="DeleteOutline" />
|
<Icon name="DeleteOutline" />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
{/if}
|
</div>
|
||||||
</div>
|
{/if}
|
||||||
|
|
||||||
{#if setupToggled}
|
<AutomationBlockSetup
|
||||||
<AutomationBlockSetup
|
schemaProperties={Object.entries(block.schema.inputs.properties)}
|
||||||
schemaProperties={Object.entries(block.schema.inputs.properties)}
|
{block}
|
||||||
{block}
|
{webhookModal}
|
||||||
{webhookModal}
|
/>
|
||||||
/>
|
{#if lastStep}
|
||||||
{#if lastStep}
|
<Button on:click={() => testDataModal.show()} cta
|
||||||
<Button on:click={() => testDataModal.show()} cta
|
>Finish and test automation</Button
|
||||||
>Finish and test automation</Button
|
>
|
||||||
>
|
|
||||||
{/if}
|
|
||||||
{/if}
|
{/if}
|
||||||
</Layout>
|
</Layout>
|
||||||
</div>
|
</div>
|
||||||
|
@ -220,8 +306,9 @@
|
||||||
padding-left: 30px;
|
padding-left: 30px;
|
||||||
}
|
}
|
||||||
.block-options {
|
.block-options {
|
||||||
display: flex;
|
justify-content: flex-end;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
|
display: flex;
|
||||||
}
|
}
|
||||||
.center-items {
|
.center-items {
|
||||||
display: flex;
|
display: flex;
|
||||||
|
@ -256,4 +343,9 @@
|
||||||
/* center horizontally */
|
/* center horizontally */
|
||||||
align-self: center;
|
align-self: center;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.blockTitle {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|
|
@ -35,6 +35,7 @@
|
||||||
export let testData
|
export let testData
|
||||||
export let schemaProperties
|
export let schemaProperties
|
||||||
export let isTestModal = false
|
export let isTestModal = false
|
||||||
|
export let isLoop = false
|
||||||
let webhookModal
|
let webhookModal
|
||||||
let drawer
|
let drawer
|
||||||
let tempFilters = lookForFilters(schemaProperties) || []
|
let tempFilters = lookForFilters(schemaProperties) || []
|
||||||
|
@ -73,6 +74,11 @@
|
||||||
await automationStore.actions.save(
|
await automationStore.actions.save(
|
||||||
$automationStore.selectedAutomation?.automation
|
$automationStore.selectedAutomation?.automation
|
||||||
)
|
)
|
||||||
|
} else if (isLoop) {
|
||||||
|
block.loop[key] = e.detail
|
||||||
|
await automationStore.actions.save(
|
||||||
|
$automationStore.selectedAutomation?.automation
|
||||||
|
)
|
||||||
} else {
|
} else {
|
||||||
block.inputs[key] = e.detail
|
block.inputs[key] = e.detail
|
||||||
await automationStore.actions.save(
|
await automationStore.actions.save(
|
||||||
|
@ -261,6 +267,14 @@
|
||||||
value={inputData[key]}
|
value={inputData[key]}
|
||||||
/>
|
/>
|
||||||
</CodeEditorModal>
|
</CodeEditorModal>
|
||||||
|
{:else if value.customType === "loopOption"}
|
||||||
|
<Select
|
||||||
|
on:change={e => onChange(e, key)}
|
||||||
|
autoWidth
|
||||||
|
value={inputData[key]}
|
||||||
|
options={["Array", "String"]}
|
||||||
|
defaultValue={"Array"}
|
||||||
|
/>
|
||||||
{:else if value.type === "string" || value.type === "number" || value.type === "integer"}
|
{:else if value.type === "string" || value.type === "number" || value.type === "integer"}
|
||||||
{#if isTestModal}
|
{#if isTestModal}
|
||||||
<ModalBindableInput
|
<ModalBindableInput
|
||||||
|
|
|
@ -13,6 +13,7 @@ const integromat = require("./steps/integromat")
|
||||||
let filter = require("./steps/filter")
|
let filter = require("./steps/filter")
|
||||||
let delay = require("./steps/delay")
|
let delay = require("./steps/delay")
|
||||||
let queryRow = require("./steps/queryRows")
|
let queryRow = require("./steps/queryRows")
|
||||||
|
let loop = require("./steps/loop")
|
||||||
const env = require("../environment")
|
const env = require("../environment")
|
||||||
|
|
||||||
const ACTION_IMPLS = {
|
const ACTION_IMPLS = {
|
||||||
|
@ -27,6 +28,7 @@ const ACTION_IMPLS = {
|
||||||
DELAY: delay.run,
|
DELAY: delay.run,
|
||||||
FILTER: filter.run,
|
FILTER: filter.run,
|
||||||
QUERY_ROWS: queryRow.run,
|
QUERY_ROWS: queryRow.run,
|
||||||
|
LOOP: loop.run,
|
||||||
// these used to be lowercase step IDs, maintain for backwards compat
|
// these used to be lowercase step IDs, maintain for backwards compat
|
||||||
discord: discord.run,
|
discord: discord.run,
|
||||||
slack: slack.run,
|
slack: slack.run,
|
||||||
|
@ -45,6 +47,7 @@ const ACTION_DEFINITIONS = {
|
||||||
DELAY: delay.definition,
|
DELAY: delay.definition,
|
||||||
FILTER: filter.definition,
|
FILTER: filter.definition,
|
||||||
QUERY_ROWS: queryRow.definition,
|
QUERY_ROWS: queryRow.definition,
|
||||||
|
LOOP: loop.definition,
|
||||||
// these used to be lowercase step IDs, maintain for backwards compat
|
// these used to be lowercase step IDs, maintain for backwards compat
|
||||||
discord: discord.definition,
|
discord: discord.definition,
|
||||||
slack: slack.definition,
|
slack: slack.definition,
|
||||||
|
|
|
@ -0,0 +1,47 @@
|
||||||
|
exports.definition = {
|
||||||
|
name: "Looping",
|
||||||
|
icon: "Reuse",
|
||||||
|
tagline: "Loop the block",
|
||||||
|
description: "Loop",
|
||||||
|
stepId: "LOOP",
|
||||||
|
internal: true,
|
||||||
|
inputs: {},
|
||||||
|
schema: {
|
||||||
|
inputs: {
|
||||||
|
properties: {
|
||||||
|
option: {
|
||||||
|
customType: "loopOption",
|
||||||
|
title: "Whether it's an array or a string",
|
||||||
|
},
|
||||||
|
binding: {
|
||||||
|
type: "string",
|
||||||
|
title: "Binding / Value",
|
||||||
|
},
|
||||||
|
iterations: {
|
||||||
|
type: "number",
|
||||||
|
title: "Max loop iterations",
|
||||||
|
},
|
||||||
|
failure: {
|
||||||
|
type: "string",
|
||||||
|
title: "Failure Condition",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
required: ["type", "value", "iterations", "failure"],
|
||||||
|
},
|
||||||
|
outputs: {
|
||||||
|
properties: {
|
||||||
|
currentItem: {
|
||||||
|
customType: "item",
|
||||||
|
description: "the item currently being executed",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
required: ["success"],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
type: "LOGIC",
|
||||||
|
}
|
||||||
|
|
||||||
|
exports.run = async function filter({ inputs }) {
|
||||||
|
let currentItem = inputs.binding
|
||||||
|
return { currentItem }
|
||||||
|
}
|
|
@ -12,6 +12,8 @@ const { definitions: triggerDefs } = require("../automations/triggerInfo")
|
||||||
const { doInAppContext, getAppDB } = require("@budibase/backend-core/context")
|
const { doInAppContext, getAppDB } = require("@budibase/backend-core/context")
|
||||||
|
|
||||||
const FILTER_STEP_ID = actions.ACTION_DEFINITIONS.FILTER.stepId
|
const FILTER_STEP_ID = actions.ACTION_DEFINITIONS.FILTER.stepId
|
||||||
|
const LOOP_STEP_ID = actions.ACTION_DEFINITIONS.LOOP.stepId
|
||||||
|
|
||||||
const CRON_STEP_ID = triggerDefs.CRON.stepId
|
const CRON_STEP_ID = triggerDefs.CRON.stepId
|
||||||
const STOPPED_STATUS = { success: false, status: "STOPPED" }
|
const STOPPED_STATUS = { success: false, status: "STOPPED" }
|
||||||
|
|
||||||
|
@ -80,51 +82,84 @@ class Orchestrator {
|
||||||
let automation = this._automation
|
let automation = this._automation
|
||||||
const app = await this.getApp()
|
const app = await this.getApp()
|
||||||
let stopped = false
|
let stopped = false
|
||||||
|
let loopStep
|
||||||
|
|
||||||
|
let stepCount = 0
|
||||||
|
let loopStepNumber
|
||||||
for (let step of automation.definition.steps) {
|
for (let step of automation.definition.steps) {
|
||||||
// execution stopped, record state for that
|
stepCount++
|
||||||
if (stopped) {
|
if (step.stepId === LOOP_STEP_ID) {
|
||||||
this.updateExecutionOutput(step.id, step.stepId, {}, STOPPED_STATUS)
|
loopStep = step
|
||||||
|
loopStepNumber = stepCount
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
let stepFn = await this.getStepFunctionality(step.stepId)
|
|
||||||
step.inputs = await processObject(step.inputs, this._context)
|
let iterations = loopStep ? loopStep.inputs.iterations : 1
|
||||||
step.inputs = automationUtils.cleanInputValues(
|
for (let index = 0; index < iterations; index++) {
|
||||||
step.inputs,
|
// execution stopped, record state for that
|
||||||
step.schema.inputs
|
if (stopped) {
|
||||||
)
|
this.updateExecutionOutput(step.id, step.stepId, {}, STOPPED_STATUS)
|
||||||
// appId is always passed
|
|
||||||
try {
|
|
||||||
let tenantId = app.tenantId || DEFAULT_TENANT_ID
|
|
||||||
const outputs = await doInTenant(tenantId, () => {
|
|
||||||
return stepFn({
|
|
||||||
inputs: step.inputs,
|
|
||||||
appId: this._appId,
|
|
||||||
emitter: this._emitter,
|
|
||||||
context: this._context,
|
|
||||||
})
|
|
||||||
})
|
|
||||||
this._context.steps.push(outputs)
|
|
||||||
// if filter causes us to stop execution don't break the loop, set a var
|
|
||||||
// so that we can finish iterating through the steps and record that it stopped
|
|
||||||
if (step.stepId === FILTER_STEP_ID && !outputs.success) {
|
|
||||||
stopped = true
|
|
||||||
this.updateExecutionOutput(step.id, step.stepId, step.inputs, {
|
|
||||||
...outputs,
|
|
||||||
...STOPPED_STATUS,
|
|
||||||
})
|
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
this.updateExecutionOutput(step.id, step.stepId, step.inputs, outputs)
|
|
||||||
} catch (err) {
|
// If it's a loop step, we need to manually add the bindings to the context
|
||||||
console.error(`Automation error - ${step.stepId} - ${err}`)
|
if (loopStep) {
|
||||||
return err
|
this._context.steps[loopStepNumber] = {
|
||||||
|
currentItem: loopStep.inputs.binding.split(",")[index],
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let stepFn = await this.getStepFunctionality(step.stepId)
|
||||||
|
console.log(step.inputs)
|
||||||
|
|
||||||
|
step.inputs = await processObject(step.inputs, this._context)
|
||||||
|
step.inputs = automationUtils.cleanInputValues(
|
||||||
|
step.inputs,
|
||||||
|
step.schema.inputs
|
||||||
|
)
|
||||||
|
console.log(step.inputs)
|
||||||
|
try {
|
||||||
|
// appId is always passed
|
||||||
|
let tenantId = app.tenantId || DEFAULT_TENANT_ID
|
||||||
|
const outputs = await doInTenant(tenantId, () => {
|
||||||
|
return stepFn({
|
||||||
|
inputs: step.inputs,
|
||||||
|
appId: this._appId,
|
||||||
|
emitter: this._emitter,
|
||||||
|
context: this._context,
|
||||||
|
})
|
||||||
|
})
|
||||||
|
this._context.steps[stepCount] = outputs
|
||||||
|
// if filter causes us to stop execution don't break the loop, set a var
|
||||||
|
// so that we can finish iterating through the steps and record that it stopped
|
||||||
|
if (step.stepId === FILTER_STEP_ID && !outputs.success) {
|
||||||
|
stopped = true
|
||||||
|
this.updateExecutionOutput(step.id, step.stepId, step.inputs, {
|
||||||
|
...outputs,
|
||||||
|
...STOPPED_STATUS,
|
||||||
|
})
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
// THE OUTPUTS GET SET IN THE CONSTRUCTOR SO WE NEED TO RESET THEM
|
||||||
|
|
||||||
|
this.updateExecutionOutput(step.id, step.stepId, step.inputs, outputs)
|
||||||
|
console.log(this.executionOutput.input)
|
||||||
|
} catch (err) {
|
||||||
|
console.error(`Automation error - ${step.stepId} - ${err}`)
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if (index === iterations - 1) {
|
||||||
|
loopStep = null
|
||||||
|
break
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Increment quota for automation runs
|
// Increment quota for automation runs
|
||||||
if (!env.SELF_HOSTED && !isDevAppID(this._appId)) {
|
if (!env.SELF_HOSTED && !isDevAppID(this._appId)) {
|
||||||
await usage.update(usage.Properties.AUTOMATION, 1)
|
await usage.update(usage.Properties.AUTOMATION, 1)
|
||||||
}
|
}
|
||||||
|
// make that we don't loop the next step if we have already been looping (loop block only has one step)
|
||||||
|
|
||||||
return this.executionOutput
|
return this.executionOutput
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in New Issue