Trigger Automation from frontend actions

This commit is contained in:
Michael Shanks 2021-01-08 17:25:06 +00:00
parent 239f27ccee
commit a878d7eb40
9 changed files with 292 additions and 13 deletions

View File

@ -1,6 +1,7 @@
<script> <script>
import TableSelector from "./TableSelector.svelte" import TableSelector from "./TableSelector.svelte"
import RowSelector from "./RowSelector.svelte" import RowSelector from "./RowSelector.svelte"
import SchemaSetup from "./SchemaSetup.svelte"
import { Button, Input, Select, Label } from "@budibase/bbui" import { Button, Input, Select, Label } from "@budibase/bbui"
import { automationStore } from "builderStore" import { automationStore } from "builderStore"
import WebhookDisplay from "../Shared/WebhookDisplay.svelte" import WebhookDisplay from "../Shared/WebhookDisplay.svelte"
@ -70,6 +71,8 @@
<RowSelector bind:value={block.inputs[key]} {bindings} /> <RowSelector bind:value={block.inputs[key]} {bindings} />
{:else if value.customType === 'webhookUrl'} {:else if value.customType === 'webhookUrl'}
<WebhookDisplay value={block.inputs[key]} /> <WebhookDisplay value={block.inputs[key]} />
{:else if value.customType === 'triggerSchema'}
<SchemaSetup bind:value={block.inputs[key]} />
{:else if value.type === 'string' || value.type === 'number'} {:else if value.type === 'string' || value.type === 'number'}
<BindableInput <BindableInput
type="string" type="string"

View File

@ -0,0 +1,113 @@
<script>
import { Input } from "@budibase/bbui"
export let value = {}
$: fieldsArray = Object.entries(value).map(([name, type]) => ({
name,
type,
}))
let addNewName = ""
let addNewType = "string"
function addField() {
if (!addNewName) return
if (value[addNewName.trim()]) {
addNewName = ""
return
}
const newValue = { ...value }
newValue[addNewName.trim()] = "string"
value = newValue
addNewName = ""
}
function onInputEnter(e) {
if (e.key === "Enter") {
addField()
}
}
function removeField(name) {
const newValues = { ...value }
delete newValues[name]
value = newValues
}
const fieldNameChanged = originalName => e => {
// reconstruct using fieldsArray, so field order is preserved
let entries = [...fieldsArray]
const newName = e.target.value
if (newName) {
entries.find(f => f.name === originalName).name = newName
} else {
entries = entries.filter(f => f.name !== originalName)
}
value = entries.reduce((newVals, current) => {
newVals[current.name] = current.type
return newVals
}, {})
}
</script>
<div class="root">
{#each fieldsArray as field}
<i
class="remove-field ri-delete-bin-line"
on:click={() => removeField(field.name)} />
<input
value={field.name}
on:change={fieldNameChanged(field.name)}
class="grid-field" />
<select
value={field.type}
on:blur={e => (value[field.name] = e.target.value)}
class="grid-field">
<option>string</option>
<option>number</option>
<option>boolean</option>
<option>datetime</option>
</select>
{/each}
<div class="new-field" on:keyup={onInputEnter}>
<Input
outline
small
bind:value={addNewName}
placeholder="Enter field name" />
</div>
<!--button on:click={addField}>Add</button-->
</div>
<style>
.root {
display: grid;
grid-template-columns: auto 1fr auto;
}
.remove-field {
cursor: pointer;
color: var(--grey-6);
margin: auto 4px auto 0;
}
.remove-field:hover {
color: var(--black);
}
.new-field {
grid-column: 1 / span 3;
max-width: 100%;
}
.grid-field {
min-width: 50px;
font-family: inherit;
font-size: inherit;
padding: 0.4em;
margin: 0 0 0.5em 0;
box-sizing: border-box;
border: 1px solid var(--grey-4);
border-radius: 0px;
}
</style>

View File

@ -0,0 +1,84 @@
<script>
import { Select, Label } from "@budibase/bbui"
import { automationStore } from "builderStore"
import SaveFields from "./SaveFields.svelte"
// parameters.contextPath used in the client handler to determine which row to save
// this could be "data" or "data.parent", "data.parent.parent" etc
export let parameters
let idFields
let schemaFields
const automationSchema = automation => {
const schema = Object.entries(
automation.definition.trigger.inputs.fields
).map(([name, type]) => ({ name, type }))
return {
name: automation.name,
_id: automation._id,
schema,
}
}
$: automations = $automationStore.automations
.filter(a => a.definition.trigger && a.definition.trigger.stepId === "APP")
.map(automationSchema)
$: selectedAutomation =
parameters &&
parameters.automationId &&
automations.find(a => a._id === parameters.automationId)
const onFieldsChanged = e => {
parameters.fields = e.detail
}
</script>
<div class="root">
{#if !automations || automations.length === 0}
<div class="cannot-use">
You must have an automation that has an "App Action" trigger.
</div>
{:else}
<Label size="m" color="dark">Automation</Label>
<Select secondary bind:value={parameters.automationId}>
<option value="" />
{#each automations as automation}
<option value={automation._id}>{automation.name}</option>
{/each}
</Select>
{#if selectedAutomation}
<SaveFields
parameterFields={parameters.fields}
schemaFields={selectedAutomation.schema}
on:fieldschanged={onFieldsChanged} />
{/if}
{/if}
</div>
<style>
.root {
display: grid;
column-gap: var(--spacing-s);
row-gap: var(--spacing-s);
grid-template-columns: auto 1fr auto 1fr auto;
align-items: baseline;
}
.root :global(> div:nth-child(2)) {
grid-column-start: 2;
grid-column-end: 6;
}
.cannot-use {
color: var(--red);
font-size: var(--font-size-s);
text-align: center;
width: 70%;
margin: auto;
}
</style>

View File

@ -1,6 +1,7 @@
import NavigateTo from "./NavigateTo.svelte" import NavigateTo from "./NavigateTo.svelte"
import SaveRow from "./SaveRow.svelte" import SaveRow from "./SaveRow.svelte"
import DeleteRow from "./DeleteRow.svelte" import DeleteRow from "./DeleteRow.svelte"
import TriggerAutomation from "./TriggerAutomation.svelte"
// defines what actions are available, when adding a new one // defines what actions are available, when adding a new one
// the component is the setup panel for the action // the component is the setup panel for the action
@ -20,4 +21,8 @@ export default [
name: "Navigate To", name: "Navigate To",
component: NavigateTo, component: NavigateTo,
}, },
{
name: "Trigger Automation",
component: TriggerAutomation,
},
] ]

View File

@ -0,0 +1,10 @@
import API from "./api"
/**
* Executes an automation. Must have "App Action" trigger.
*/
export const triggerAutomation = async (automationId, fields) => {
return await API.post({
url: `/api/automations/${automationId}/trigger`,
body: { fields },
})
}

View File

@ -7,3 +7,4 @@ export * from "./views"
export * from "./relationships" export * from "./relationships"
export * from "./routes" export * from "./routes"
export * from "./app" export * from "./app"
export * from "./automations"

View File

@ -1,6 +1,6 @@
import { enrichDataBinding } from "./enrichDataBinding" import { enrichDataBinding } from "./enrichDataBinding"
import { routeStore } from "../store" import { routeStore } from "../store"
import { saveRow, deleteRow } from "../api" import { saveRow, deleteRow, triggerAutomation } from "../api"
const saveRowHandler = async (action, context) => { const saveRowHandler = async (action, context) => {
let draft = context[`${action.parameters.contextPath}_draft`] let draft = context[`${action.parameters.contextPath}_draft`]
@ -21,6 +21,17 @@ const deleteRowHandler = async (action, context) => {
}) })
} }
const triggerAutomationHandler = async (action, context) => {
const params = {}
for (let field in action.parameters.fields) {
params[field] = enrichDataBinding(
action.parameters.fields[field].value,
context
)
}
await triggerAutomation(action.parameters.automationId, params)
}
const navigationHandler = action => { const navigationHandler = action => {
routeStore.actions.navigate(action.parameters.url) routeStore.actions.navigate(action.parameters.url)
} }
@ -29,6 +40,7 @@ const handlerMap = {
["Save Row"]: saveRowHandler, ["Save Row"]: saveRowHandler,
["Delete Row"]: deleteRowHandler, ["Delete Row"]: deleteRowHandler,
["Navigate To"]: navigationHandler, ["Navigate To"]: navigationHandler,
["Trigger Automation"]: triggerAutomationHandler,
} }
/** /**

View File

@ -2,6 +2,7 @@ const CouchDB = require("../db")
const emitter = require("../events/index") const emitter = require("../events/index")
const InMemoryQueue = require("../utilities/queue/inMemoryQueue") const InMemoryQueue = require("../utilities/queue/inMemoryQueue")
const { getAutomationParams } = require("../db/utils") const { getAutomationParams } = require("../db/utils")
const { coerceValue } = require("../utilities")
let automationQueue = new InMemoryQueue("automationQueue") let automationQueue = new InMemoryQueue("automationQueue")
@ -119,6 +120,37 @@ const BUILTIN_DEFINITIONS = {
}, },
type: "TRIGGER", type: "TRIGGER",
}, },
APP: {
name: "App Action",
event: "app:trigger",
icon: "ri-window-fill",
tagline: "Automation fired from the frontend",
description: "Trigger an automation from an action inside your app",
stepId: "APP",
inputs: {},
schema: {
inputs: {
properties: {
fields: {
type: "object",
customType: "triggerSchema",
title: "Fields",
},
},
required: [],
},
outputs: {
properties: {
fields: {
type: "object",
description: "Fields submitted from the app frontend",
},
},
required: ["fields"],
},
},
type: "TRIGGER",
},
} }
async function queueRelevantRowAutomations(event, eventType) { async function queueRelevantRowAutomations(event, eventType) {
@ -200,12 +232,19 @@ async function fillRowOutput(automation, params) {
module.exports.externalTrigger = async function(automation, params) { module.exports.externalTrigger = async function(automation, params) {
// TODO: replace this with allowing user in builder to input values in future // TODO: replace this with allowing user in builder to input values in future
if ( if (automation.definition != null && automation.definition.trigger != null) {
automation.definition != null && if (automation.definition.trigger.inputs.tableId != null) {
automation.definition.trigger != null && params = await fillRowOutput(automation, params)
automation.definition.trigger.inputs.tableId != null }
) { if (automation.definition.trigger.stepId === "APP") {
params = await fillRowOutput(automation, params) // values are likely to be submitted as strings, so we shall convert to correct type
const coercedFields = {}
const fields = automation.definition.trigger.inputs.fields
for (let key in fields) {
coercedFields[key] = coerceValue(params.fields[key], fields[key])
}
params.fields = coercedFields
}
} }
automationQueue.add({ automation, event: params }) automationQueue.add({ automation, event: params })

View File

@ -144,6 +144,23 @@ exports.walkDir = (dirPath, callback) => {
} }
} }
/**
* This will coerce a value to the correct types based on the type transform map
* @param {object} row The value to coerce
* @param {object} type The type fo coerce to
* @returns {object} The coerced value
*/
exports.coerceValue = (value, type) => {
// eslint-disable-next-line no-prototype-builtins
if (TYPE_TRANSFORM_MAP[type].hasOwnProperty(value)) {
return TYPE_TRANSFORM_MAP[type][value]
} else if (TYPE_TRANSFORM_MAP[type].parse) {
return TYPE_TRANSFORM_MAP[type].parse(value)
}
return value
}
/** /**
* This will coerce the values in a row to the correct types based on the type transform map and the * This will coerce the values in a row to the correct types based on the type transform map and the
* table schema. * table schema.
@ -159,12 +176,7 @@ exports.coerceRowValues = (row, table) => {
const field = table.schema[key] const field = table.schema[key]
if (!field) continue if (!field) continue
// eslint-disable-next-line no-prototype-builtins clonedRow[key] = exports.coerceValue(value, field.type)
if (TYPE_TRANSFORM_MAP[field.type].hasOwnProperty(value)) {
clonedRow[key] = TYPE_TRANSFORM_MAP[field.type][value]
} else if (TYPE_TRANSFORM_MAP[field.type].parse) {
clonedRow[key] = TYPE_TRANSFORM_MAP[field.type].parse(value)
}
} }
return clonedRow return clonedRow
} }