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
}