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/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/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 newOrExisting=== "existing"} + + {:else} + + {/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 a8d8400f23..b9b512a80f 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 }