From b2daff62853c6b938e0d99d656244385f5d361a2 Mon Sep 17 00:00:00 2001 From: Michael Shanks Date: Fri, 8 Jan 2021 17:25:06 +0000 Subject: [PATCH 1/5] Trigger Automation from frontend actions --- .../SetupPanel/AutomationBlockSetup.svelte | 3 + .../automation/SetupPanel/SchemaSetup.svelte | 113 ++++++++++++++++++ .../actions/TriggerAutomation.svelte | 84 +++++++++++++ .../EventsEditor/actions/index.js | 5 + packages/client/src/api/automations.js | 10 ++ packages/client/src/api/index.js | 1 + packages/client/src/utils/buttonActions.js | 14 ++- packages/server/src/automations/triggers.js | 51 +++++++- packages/server/src/utilities/index.js | 24 +++- 9 files changed, 292 insertions(+), 13 deletions(-) create mode 100644 packages/builder/src/components/automation/SetupPanel/SchemaSetup.svelte create mode 100644 packages/builder/src/components/userInterface/EventsEditor/actions/TriggerAutomation.svelte create mode 100644 packages/client/src/api/automations.js diff --git a/packages/builder/src/components/automation/SetupPanel/AutomationBlockSetup.svelte b/packages/builder/src/components/automation/SetupPanel/AutomationBlockSetup.svelte index 7e26b2155e..5d3371ee7d 100644 --- a/packages/builder/src/components/automation/SetupPanel/AutomationBlockSetup.svelte +++ b/packages/builder/src/components/automation/SetupPanel/AutomationBlockSetup.svelte @@ -1,6 +1,7 @@ + +
+ {#each fieldsArray as field} + removeField(field.name)} /> + + + {/each} +
+ +
+ +
+ + diff --git a/packages/builder/src/components/userInterface/EventsEditor/actions/TriggerAutomation.svelte b/packages/builder/src/components/userInterface/EventsEditor/actions/TriggerAutomation.svelte new file mode 100644 index 0000000000..4c58175401 --- /dev/null +++ b/packages/builder/src/components/userInterface/EventsEditor/actions/TriggerAutomation.svelte @@ -0,0 +1,84 @@ + + +
+ + {#if !automations || automations.length === 0} +
+ You must have an automation that has an "App Action" trigger. +
+ {:else} + + + + {#if selectedAutomation} + + {/if} + {/if} +
+ + diff --git a/packages/builder/src/components/userInterface/EventsEditor/actions/index.js b/packages/builder/src/components/userInterface/EventsEditor/actions/index.js index 1c07356ee9..1bb0ab4d00 100644 --- a/packages/builder/src/components/userInterface/EventsEditor/actions/index.js +++ b/packages/builder/src/components/userInterface/EventsEditor/actions/index.js @@ -1,6 +1,7 @@ import NavigateTo from "./NavigateTo.svelte" import SaveRow from "./SaveRow.svelte" import DeleteRow from "./DeleteRow.svelte" +import TriggerAutomation from "./TriggerAutomation.svelte" // defines what actions are available, when adding a new one // the component is the setup panel for the action @@ -20,4 +21,8 @@ export default [ name: "Navigate To", component: NavigateTo, }, + { + name: "Trigger Automation", + component: TriggerAutomation, + }, ] diff --git a/packages/client/src/api/automations.js b/packages/client/src/api/automations.js new file mode 100644 index 0000000000..c163ffee82 --- /dev/null +++ b/packages/client/src/api/automations.js @@ -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 }, + }) +} diff --git a/packages/client/src/api/index.js b/packages/client/src/api/index.js index 878964107b..a8de677367 100644 --- a/packages/client/src/api/index.js +++ b/packages/client/src/api/index.js @@ -7,3 +7,4 @@ export * from "./views" export * from "./relationships" export * from "./routes" export * from "./app" +export * from "./automations" diff --git a/packages/client/src/utils/buttonActions.js b/packages/client/src/utils/buttonActions.js index 2968525188..b2caf83d1c 100644 --- a/packages/client/src/utils/buttonActions.js +++ b/packages/client/src/utils/buttonActions.js @@ -1,6 +1,6 @@ import { enrichDataBinding } from "./enrichDataBinding" import { routeStore } from "../store" -import { saveRow, deleteRow } from "../api" +import { saveRow, deleteRow, triggerAutomation } from "../api" const saveRowHandler = async (action, context) => { 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 => { routeStore.actions.navigate(action.parameters.url) } @@ -29,6 +40,7 @@ const handlerMap = { ["Save Row"]: saveRowHandler, ["Delete Row"]: deleteRowHandler, ["Navigate To"]: navigationHandler, + ["Trigger Automation"]: triggerAutomationHandler, } /** diff --git a/packages/server/src/automations/triggers.js b/packages/server/src/automations/triggers.js index 6fbd8e4c25..6634016e3f 100644 --- a/packages/server/src/automations/triggers.js +++ b/packages/server/src/automations/triggers.js @@ -2,6 +2,7 @@ const CouchDB = require("../db") const emitter = require("../events/index") const InMemoryQueue = require("../utilities/queue/inMemoryQueue") const { getAutomationParams } = require("../db/utils") +const { coerceValue } = require("../utilities") let automationQueue = new InMemoryQueue("automationQueue") @@ -119,6 +120,37 @@ const BUILTIN_DEFINITIONS = { }, 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) { @@ -200,12 +232,19 @@ async function fillRowOutput(automation, params) { module.exports.externalTrigger = async function(automation, params) { // TODO: replace this with allowing user in builder to input values in future - if ( - automation.definition != null && - automation.definition.trigger != null && - automation.definition.trigger.inputs.tableId != null - ) { - params = await fillRowOutput(automation, params) + if (automation.definition != null && automation.definition.trigger != null) { + if (automation.definition.trigger.inputs.tableId != null) { + params = await fillRowOutput(automation, params) + } + if (automation.definition.trigger.stepId === "APP") { + // 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 }) diff --git a/packages/server/src/utilities/index.js b/packages/server/src/utilities/index.js index 8a43e9d27a..190a66838e 100644 --- a/packages/server/src/utilities/index.js +++ b/packages/server/src/utilities/index.js @@ -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 * table schema. @@ -159,12 +176,7 @@ exports.coerceRowValues = (row, table) => { const field = table.schema[key] if (!field) continue - // eslint-disable-next-line no-prototype-builtins - 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) - } + clonedRow[key] = exports.coerceValue(value, field.type) } return clonedRow } From 0927ab06912c8b870ed7186cb2bf226da1d00f1e Mon Sep 17 00:00:00 2001 From: Michael Shanks Date: Mon, 11 Jan 2021 09:56:39 +0000 Subject: [PATCH 2/5] tidy up --- .../EventsEditor/actions/TriggerAutomation.svelte | 5 ----- 1 file changed, 5 deletions(-) diff --git a/packages/builder/src/components/userInterface/EventsEditor/actions/TriggerAutomation.svelte b/packages/builder/src/components/userInterface/EventsEditor/actions/TriggerAutomation.svelte index 4c58175401..01f54598e0 100644 --- a/packages/builder/src/components/userInterface/EventsEditor/actions/TriggerAutomation.svelte +++ b/packages/builder/src/components/userInterface/EventsEditor/actions/TriggerAutomation.svelte @@ -3,13 +3,8 @@ 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 From 5d2612f54726311dc0a54ddbc9c7cc128c3c5b3f Mon Sep 17 00:00:00 2001 From: Michael Shanks Date: Mon, 11 Jan 2021 10:11:31 +0000 Subject: [PATCH 3/5] changed input style for automation schema setup --- .../src/components/automation/SetupPanel/SchemaSetup.svelte | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/builder/src/components/automation/SetupPanel/SchemaSetup.svelte b/packages/builder/src/components/automation/SetupPanel/SchemaSetup.svelte index f9c12fac23..281ec9f0d4 100644 --- a/packages/builder/src/components/automation/SetupPanel/SchemaSetup.svelte +++ b/packages/builder/src/components/automation/SetupPanel/SchemaSetup.svelte @@ -71,7 +71,7 @@ {/each}
From dd1e5cd7cd1f8fea553420bb74b17a9e818044b9 Mon Sep 17 00:00:00 2001 From: Michael Shanks Date: Tue, 12 Jan 2021 15:21:27 +0000 Subject: [PATCH 4/5] allow new automation from button action setup --- .../store/automation/Automation.js | 9 ++ .../builderStore/store/automation/index.js | 4 + .../AutomationBuilder/BlockList.svelte | 11 +- .../automation/SetupPanel/SchemaSetup.svelte | 6 +- .../EventsEditor/EventEditorModal.svelte | 46 +++++-- .../EventsEditor/actions/SaveFields.svelte | 25 ++-- .../actions/TriggerAutomation.svelte | 128 ++++++++++++------ 7 files changed, 162 insertions(+), 67 deletions(-) diff --git a/packages/builder/src/builderStore/store/automation/Automation.js b/packages/builder/src/builderStore/store/automation/Automation.js index cbdcabeccc..a9dce88258 100644 --- a/packages/builder/src/builderStore/store/automation/Automation.js +++ b/packages/builder/src/builderStore/store/automation/Automation.js @@ -56,4 +56,13 @@ export default class Automation { steps.splice(stepIdx, 1) this.automation.definition.steps = steps } + + constructBlock(type, stepId, blockDefinition) { + return { + ...blockDefinition, + inputs: blockDefinition.inputs || {}, + stepId, + type, + } + } } diff --git a/packages/builder/src/builderStore/store/automation/index.js b/packages/builder/src/builderStore/store/automation/index.js index 3f0743aa1e..7a01bccfab 100644 --- a/packages/builder/src/builderStore/store/automation/index.js +++ b/packages/builder/src/builderStore/store/automation/index.js @@ -2,6 +2,7 @@ import { writable } from "svelte/store" import api from "../../api" import Automation from "./Automation" import { cloneDeep } from "lodash/fp" +import analytics from "analytics" const automationActions = store => ({ fetch: async () => { @@ -93,6 +94,9 @@ const automationActions = store => ({ state.selectedBlock = newBlock return state }) + analytics.captureEvent("Added Automation Block", { + name: block.name, + }) }, deleteAutomationBlock: block => { store.update(state => { diff --git a/packages/builder/src/components/automation/AutomationBuilder/BlockList.svelte b/packages/builder/src/components/automation/AutomationBuilder/BlockList.svelte index de3e9660ad..5ea1d64e51 100644 --- a/packages/builder/src/components/automation/AutomationBuilder/BlockList.svelte +++ b/packages/builder/src/components/automation/AutomationBuilder/BlockList.svelte @@ -49,16 +49,9 @@ } function addBlockToAutomation(stepId, blockDefinition) { - const newBlock = { - ...blockDefinition, - inputs: blockDefinition.inputs || {}, - stepId, - type: selectedTab, - } + const newBlock = $automationStore.selectedAutomation.constructBlock( + selectedTab, stepId, blockDefinition) automationStore.actions.addBlockToAutomation(newBlock) - analytics.captureEvent("Added Automation Block", { - name: blockDefinition.name, - }) closePopover() if (stepId === "WEBHOOK") { webhookModal.show() diff --git a/packages/builder/src/components/automation/SetupPanel/SchemaSetup.svelte b/packages/builder/src/components/automation/SetupPanel/SchemaSetup.svelte index 281ec9f0d4..6a68f08980 100644 --- a/packages/builder/src/components/automation/SetupPanel/SchemaSetup.svelte +++ b/packages/builder/src/components/automation/SetupPanel/SchemaSetup.svelte @@ -8,7 +8,6 @@ })) let addNewName = "" - let addNewType = "string" function addField() { if (!addNewName) return @@ -76,7 +75,6 @@ bind:value={addNewName} placeholder="Enter field name" />
- diff --git a/packages/builder/src/components/userInterface/EventsEditor/EventEditorModal.svelte b/packages/builder/src/components/userInterface/EventsEditor/EventEditorModal.svelte index 27e91cd29e..d92ebed33e 100644 --- a/packages/builder/src/components/userInterface/EventsEditor/EventEditorModal.svelte +++ b/packages/builder/src/components/userInterface/EventsEditor/EventEditorModal.svelte @@ -3,6 +3,7 @@ import { AddIcon, ArrowDownIcon } from "components/common/Icons/" import actionTypes from "./actions" import { createEventDispatcher } from "svelte" + import { automationStore } from "builderStore" const dispatch = createEventDispatcher() const eventTypeKey = "##eventHandlerType" @@ -13,17 +14,11 @@ let addActionDropdown let selectedAction - let draftEventHandler = { parameters: [] } - $: actions = event || [] $: selectedActionComponent = selectedAction && actionTypes.find(t => t.name === selectedAction[eventTypeKey]).component - const updateEventHandler = (updatedHandler, index) => { - actions[index] = updatedHandler - } - const deleteAction = index => { actions.splice(index, 1) actions = actions @@ -44,8 +39,43 @@ selectedAction = action } - const saveEventData = () => { - dispatch("change", actions) + const saveEventData = async () => { + // e.g. The Trigger Automation action exposes beforeSave, so it can + // create any automations it needs to + for (let action of actions) { + if (action[eventTypeKey] === "Trigger Automation") { + await createAutomation(action.parameters) + } + } + dispatch("change", actions) + } + + // called by the parent modal when actions are saved + const createAutomation = async parameters => { + if (parameters.automationId || !parameters.newAutomationName) return + + await automationStore.actions.create({name: parameters.newAutomationName}) + + const appActionDefinition = $automationStore.blockDefinitions.TRIGGER.APP + + const newBlock = $automationStore.selectedAutomation.constructBlock( + "TRIGGER", "APP", appActionDefinition) + + + newBlock.inputs = { + fields: Object.entries(parameters.fields).reduce((fields, [key, value]) => { + fields[key] = value.type + return fields + }, {}) + } + + automationStore.actions.addBlockToAutomation(newBlock) + + await automationStore.actions.save( + $automationStore.selectedAutomation) + + parameters.automationId = $automationStore.selectedAutomation.automation._id + delete parameters.newAutomationName } diff --git a/packages/builder/src/components/userInterface/EventsEditor/actions/SaveFields.svelte b/packages/builder/src/components/userInterface/EventsEditor/actions/SaveFields.svelte index 3fe85cbfda..71d0ddf17f 100644 --- a/packages/builder/src/components/userInterface/EventsEditor/actions/SaveFields.svelte +++ b/packages/builder/src/components/userInterface/EventsEditor/actions/SaveFields.svelte @@ -1,6 +1,6 @@
- {#if !automations || automations.length === 0} -
- You must have an automation that has an "App Action" trigger. -
- {:else} - - + + + +
+ +
+ + + + + +
+ + + + {#if newOrExisting=== "existing"} + - - {#if selectedAutomation} - - {/if} + {:else} + {/if} + + + From 2a3ab3ebe2fbcc70ed9fae048e24df6d51975229 Mon Sep 17 00:00:00 2001 From: Michael Shanks Date: Wed, 13 Jan 2021 10:20:07 +0000 Subject: [PATCH 5/5] Schema setup - styling changes --- .../automation/SetupPanel/SchemaSetup.svelte | 132 ++++++++++-------- 1 file changed, 75 insertions(+), 57 deletions(-) diff --git a/packages/builder/src/components/automation/SetupPanel/SchemaSetup.svelte b/packages/builder/src/components/automation/SetupPanel/SchemaSetup.svelte index 6a68f08980..51c105e9f4 100644 --- a/packages/builder/src/components/automation/SetupPanel/SchemaSetup.svelte +++ b/packages/builder/src/components/automation/SetupPanel/SchemaSetup.svelte @@ -1,5 +1,5 @@
- {#each fieldsArray as field} - removeField(field.name)} /> - - - {/each} -
- +
+
+
+ {#each fieldsArray as field} +
+ + + + removeField(field.name)} /> + + +
+ {/each} +