Automation foreach block

This commit is contained in:
Peter Clement 2022-03-25 09:26:55 +00:00
parent b067b5ac2e
commit 554cefe997
6 changed files with 270 additions and 78 deletions

View File

@ -39,6 +39,7 @@
if (v.internal) { if (v.internal) {
acc[k] = v acc[k] = v
} }
delete acc.LOOP
return acc return acc
}, {}) }, {})

View File

@ -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 class={`block ${block.type} hoverable`} class:selected on:click={() => {}}>
{#if loopingSelected}
<div class="blockSection">
<div <div
class={`block ${block.type} hoverable`} on:click={() => {
class:selected 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={() => { on:click={() => {
onSelect(block) 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>
<div class="blockTitle">
{#if testResult && testResult[0]} {#if testResult && testResult[0]}
<span on:click={() => resultsModal.show()}> <div style="float: right;" on:click={() => resultsModal.show()}>
<StatusLight <StatusLight
positive={isTrigger || testResult[0].outputs?.success} positive={isTrigger || testResult[0].outputs?.success}
negative={!testResult[0].outputs?.success} negative={!testResult[0].outputs?.success}
><Body size="XS">View response</Body></StatusLight ><Body size="XS">View response</Body></StatusLight
> >
</span> </div>
{/if} {/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">
<ActionButton
on:click={() => {
onSelect(block)
setupToggled = !setupToggled
}}
quiet
icon={setupToggled ? "ChevronDown" : "ChevronRight"}
>
<Detail size="S">Setup</Detail>
</ActionButton>
{#if !isTrigger} {#if !isTrigger}
<div>
<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,10 +260,9 @@
<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}
@ -186,7 +273,6 @@
>Finish and test automation</Button >Finish and test automation</Button
> >
{/if} {/if}
{/if}
</Layout> </Layout>
</div> </div>
{/if} {/if}
@ -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>

View File

@ -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

View File

@ -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,

View File

@ -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 }
}

View File

@ -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,20 +82,44 @@ 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) {
stepCount++
if (step.stepId === LOOP_STEP_ID) {
loopStep = step
loopStepNumber = stepCount
continue
}
let iterations = loopStep ? loopStep.inputs.iterations : 1
for (let index = 0; index < iterations; index++) {
// execution stopped, record state for that // execution stopped, record state for that
if (stopped) { if (stopped) {
this.updateExecutionOutput(step.id, step.stepId, {}, STOPPED_STATUS) this.updateExecutionOutput(step.id, step.stepId, {}, STOPPED_STATUS)
continue continue
} }
// If it's a loop step, we need to manually add the bindings to the context
if (loopStep) {
this._context.steps[loopStepNumber] = {
currentItem: loopStep.inputs.binding.split(",")[index],
}
}
let stepFn = await this.getStepFunctionality(step.stepId) let stepFn = await this.getStepFunctionality(step.stepId)
console.log(step.inputs)
step.inputs = await processObject(step.inputs, this._context) step.inputs = await processObject(step.inputs, this._context)
step.inputs = automationUtils.cleanInputValues( step.inputs = automationUtils.cleanInputValues(
step.inputs, step.inputs,
step.schema.inputs step.schema.inputs
) )
// appId is always passed console.log(step.inputs)
try { try {
// appId is always passed
let tenantId = app.tenantId || DEFAULT_TENANT_ID let tenantId = app.tenantId || DEFAULT_TENANT_ID
const outputs = await doInTenant(tenantId, () => { const outputs = await doInTenant(tenantId, () => {
return stepFn({ return stepFn({
@ -103,7 +129,7 @@ class Orchestrator {
context: this._context, context: this._context,
}) })
}) })
this._context.steps.push(outputs) this._context.steps[stepCount] = outputs
// if filter causes us to stop execution don't break the loop, set a var // 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 // so that we can finish iterating through the steps and record that it stopped
if (step.stepId === FILTER_STEP_ID && !outputs.success) { if (step.stepId === FILTER_STEP_ID && !outputs.success) {
@ -114,17 +140,26 @@ class Orchestrator {
}) })
continue continue
} }
// THE OUTPUTS GET SET IN THE CONSTRUCTOR SO WE NEED TO RESET THEM
this.updateExecutionOutput(step.id, step.stepId, step.inputs, outputs) this.updateExecutionOutput(step.id, step.stepId, step.inputs, outputs)
console.log(this.executionOutput.input)
} catch (err) { } catch (err) {
console.error(`Automation error - ${step.stepId} - ${err}`) console.error(`Automation error - ${step.stepId} - ${err}`)
return 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
} }
} }