onChange(e, field)}
+ value={fieldData}
+ on:change={e =>
+ onChange({
+ row: {
+ [field]: e.detail,
+ },
+ })}
type="string"
bindings={parsedBindings}
allowJS={true}
updateOnChange={false}
title={schema.name}
+ autocomplete="off"
/>
{/if}
diff --git a/packages/builder/src/components/automation/SetupPanel/TableSelector.svelte b/packages/builder/src/components/automation/SetupPanel/TableSelector.svelte
index c278c7d0e3..74bfe27b29 100644
--- a/packages/builder/src/components/automation/SetupPanel/TableSelector.svelte
+++ b/packages/builder/src/components/automation/SetupPanel/TableSelector.svelte
@@ -8,6 +8,7 @@
export let value
export let isTrigger
+ export let disabled = false
$: filteredTables = $tables.list.filter(table => {
return !isTrigger || table._id !== TableNames.USERS
@@ -25,4 +26,5 @@
options={filteredTables}
getOptionLabel={table => table.name}
getOptionValue={table => table._id}
+ {disabled}
/>
diff --git a/packages/builder/src/components/common/bindings/DrawerBindableInput.svelte b/packages/builder/src/components/common/bindings/DrawerBindableInput.svelte
index 0cb10d1aa5..9c482dbc0a 100644
--- a/packages/builder/src/components/common/bindings/DrawerBindableInput.svelte
+++ b/packages/builder/src/components/common/bindings/DrawerBindableInput.svelte
@@ -23,6 +23,7 @@
export let disableBindings = false
export let forceModal = false
export let context = null
+ export let autocomplete
const dispatch = createEventDispatcher()
@@ -71,6 +72,7 @@
on:blur={onBlur}
{placeholder}
{updateOnChange}
+ {autocomplete}
/>
{#if !disabled && !disableBindings}
({
)
}
},
- updateBlockInputs: async (block, data) => {
+
+ processBlockInputs: async (block, data) => {
// Create new modified block
let newBlock = {
...block,
@@ -184,6 +185,14 @@ const automationActions = store => ({
// Don't save if no changes were made
if (JSON.stringify(newAutomation) === JSON.stringify(automation)) {
+ return false
+ }
+
+ return newAutomation
+ },
+ updateBlockInputs: async (block, data) => {
+ const newAutomation = await store.actions.processBlockInputs(block, data)
+ if (newAutomation === false) {
return
}
await store.actions.save(newAutomation)
diff --git a/packages/server/src/automations/automationUtils.ts b/packages/server/src/automations/automationUtils.ts
index 5467e0757c..bb63be8bce 100644
--- a/packages/server/src/automations/automationUtils.ts
+++ b/packages/server/src/automations/automationUtils.ts
@@ -100,7 +100,10 @@ export function getError(err: any) {
}
export function guardAttachment(attachmentObject: any) {
- if (!("url" in attachmentObject) || !("filename" in attachmentObject)) {
+ if (
+ attachmentObject &&
+ (!("url" in attachmentObject) || !("filename" in attachmentObject))
+ ) {
const providedKeys = Object.keys(attachmentObject).join(", ")
throw new Error(
`Attachments must have both "url" and "filename" keys. You have provided: ${providedKeys}`
@@ -135,7 +138,9 @@ export async function sendAutomationAttachmentsToStorage(
}
for (const [prop, attachments] of Object.entries(attachmentRows)) {
- if (Array.isArray(attachments)) {
+ if (!attachments) {
+ continue
+ } else if (Array.isArray(attachments)) {
if (attachments.length) {
row[prop] = await Promise.all(
attachments.map(attachment => generateAttachmentRow(attachment))
diff --git a/packages/server/src/automations/steps/updateRow.ts b/packages/server/src/automations/steps/updateRow.ts
index 32c8addd7a..c1e7e286ce 100644
--- a/packages/server/src/automations/steps/updateRow.ts
+++ b/packages/server/src/automations/steps/updateRow.ts
@@ -82,39 +82,73 @@ export async function run({ inputs, appId, emitter }: AutomationStepInput) {
}
const tableId = inputs.row.tableId
- // clear any undefined, null or empty string properties so that they aren't updated
- for (let propKey of Object.keys(inputs.row)) {
- const clearRelationships =
- inputs.meta?.fields?.[propKey]?.clearRelationships
- if (
- (inputs.row[propKey] == null || inputs.row[propKey]?.length === 0) &&
- !clearRelationships
- ) {
- delete inputs.row[propKey]
- }
+ // Base update
+ let rowUpdate: Record
+
+ // Legacy
+ // Find previously set values and add them to the update. Ensure empty relationships
+ // are added to the update if clearRelationships is true
+ const legacyUpdated = Object.keys(inputs.row || {}).reduce(
+ (acc: Record, key: string) => {
+ const isEmpty = inputs.row[key] == null || inputs.row[key]?.length === 0
+ const fieldConfig = inputs.meta?.fields || {}
+
+ if (isEmpty) {
+ if (
+ Object.hasOwn(fieldConfig, key) &&
+ fieldConfig[key].clearRelationships === true
+ ) {
+ // Explicitly clear the field on update
+ acc[key] = []
+ }
+ } else {
+ // Keep non-empty values
+ acc[key] = inputs.row[key]
+ }
+ return acc
+ },
+ {}
+ )
+
+ // The source of truth for inclusion in the update is: inputs.meta?.fields
+ const parsedUpdate = Object.keys(inputs.meta?.fields || {}).reduce(
+ (acc: Record, key: string) => {
+ const fieldConfig = inputs.meta?.fields?.[key] || {}
+ // Ignore legacy config.
+ if (Object.hasOwn(fieldConfig, "clearRelationships")) {
+ return acc
+ }
+ acc[key] =
+ !inputs.row[key] || inputs.row[key]?.length === 0 ? "" : inputs.row[key]
+ return acc
+ },
+ {}
+ )
+
+ rowUpdate = {
+ tableId,
+ ...parsedUpdate,
+ ...legacyUpdated,
}
try {
if (tableId) {
- inputs.row = await automationUtils.cleanUpRow(
- inputs.row.tableId,
- inputs.row
- )
+ rowUpdate = await automationUtils.cleanUpRow(tableId, rowUpdate)
- inputs.row = await automationUtils.sendAutomationAttachmentsToStorage(
- inputs.row.tableId,
- inputs.row
+ rowUpdate = await automationUtils.sendAutomationAttachmentsToStorage(
+ tableId,
+ rowUpdate
)
}
// have to clean up the row, remove the table from it
const ctx: any = buildCtx(appId, emitter, {
body: {
- ...inputs.row,
+ ...rowUpdate,
_id: inputs.rowId,
},
params: {
rowId: inputs.rowId,
- tableId: tableId,
+ tableId,
},
})
await rowController.patch(ctx)
diff --git a/packages/server/src/automations/triggerInfo/app.ts b/packages/server/src/automations/triggerInfo/app.ts
index abc1463f1a..bfd284cc53 100644
--- a/packages/server/src/automations/triggerInfo/app.ts
+++ b/packages/server/src/automations/triggerInfo/app.ts
@@ -4,11 +4,12 @@ import {
AutomationStepType,
AutomationTriggerSchema,
AutomationTriggerStepId,
+ AutomationEventType,
} from "@budibase/types"
export const definition: AutomationTriggerSchema = {
name: "App Action",
- event: "app:trigger",
+ event: AutomationEventType.APP_TRIGGER,
icon: "Apps",
tagline: "Automation fired from the frontend",
description: "Trigger an automation from an action inside your app",
diff --git a/packages/server/src/automations/triggerInfo/cron.ts b/packages/server/src/automations/triggerInfo/cron.ts
index 1c47aeaeec..be4b60cb27 100644
--- a/packages/server/src/automations/triggerInfo/cron.ts
+++ b/packages/server/src/automations/triggerInfo/cron.ts
@@ -4,11 +4,12 @@ import {
AutomationStepType,
AutomationTriggerSchema,
AutomationTriggerStepId,
+ AutomationEventType,
} from "@budibase/types"
export const definition: AutomationTriggerSchema = {
name: "Cron Trigger",
- event: "cron:trigger",
+ event: AutomationEventType.CRON_TRIGGER,
icon: "Clock",
tagline: "Cron Trigger ({{inputs.cron}})",
description: "Triggers automation on a cron schedule.",
diff --git a/packages/server/src/automations/triggerInfo/rowDeleted.ts b/packages/server/src/automations/triggerInfo/rowDeleted.ts
index e1014f3dbc..06e53ce63f 100644
--- a/packages/server/src/automations/triggerInfo/rowDeleted.ts
+++ b/packages/server/src/automations/triggerInfo/rowDeleted.ts
@@ -4,11 +4,12 @@ import {
AutomationStepType,
AutomationTriggerSchema,
AutomationTriggerStepId,
+ AutomationEventType,
} from "@budibase/types"
export const definition: AutomationTriggerSchema = {
name: "Row Deleted",
- event: "row:delete",
+ event: AutomationEventType.ROW_DELETE,
icon: "TableRowRemoveCenter",
tagline: "Row is deleted from {{inputs.enriched.table.name}}",
description: "Fired when a row is deleted from your database",
diff --git a/packages/server/src/automations/triggerInfo/rowSaved.ts b/packages/server/src/automations/triggerInfo/rowSaved.ts
index faa32ef96e..81cb098b69 100644
--- a/packages/server/src/automations/triggerInfo/rowSaved.ts
+++ b/packages/server/src/automations/triggerInfo/rowSaved.ts
@@ -4,11 +4,12 @@ import {
AutomationStepType,
AutomationTriggerSchema,
AutomationTriggerStepId,
+ AutomationEventType,
} from "@budibase/types"
export const definition: AutomationTriggerSchema = {
name: "Row Created",
- event: "row:save",
+ event: AutomationEventType.ROW_SAVE,
icon: "TableRowAddBottom",
tagline: "Row is added to {{inputs.enriched.table.name}}",
description: "Fired when a row is added to your database",
diff --git a/packages/server/src/automations/triggerInfo/rowUpdated.ts b/packages/server/src/automations/triggerInfo/rowUpdated.ts
index eab7c40a09..3e65525f51 100644
--- a/packages/server/src/automations/triggerInfo/rowUpdated.ts
+++ b/packages/server/src/automations/triggerInfo/rowUpdated.ts
@@ -4,11 +4,12 @@ import {
AutomationStepType,
AutomationTriggerSchema,
AutomationTriggerStepId,
+ AutomationEventType,
} from "@budibase/types"
export const definition: AutomationTriggerSchema = {
name: "Row Updated",
- event: "row:update",
+ event: AutomationEventType.ROW_UPDATE,
icon: "Refresh",
tagline: "Row is updated in {{inputs.enriched.table.name}}",
description: "Fired when a row is updated in your database",
diff --git a/packages/server/src/automations/triggerInfo/webhook.ts b/packages/server/src/automations/triggerInfo/webhook.ts
index 310dd66b41..b97c76ec61 100644
--- a/packages/server/src/automations/triggerInfo/webhook.ts
+++ b/packages/server/src/automations/triggerInfo/webhook.ts
@@ -4,11 +4,12 @@ import {
AutomationStepType,
AutomationTriggerSchema,
AutomationTriggerStepId,
+ AutomationEventType,
} from "@budibase/types"
export const definition: AutomationTriggerSchema = {
name: "Webhook",
- event: "web:trigger",
+ event: AutomationEventType.WEBHOOK_TRIGGER,
icon: "Send",
tagline: "Webhook endpoint is hit",
description: "Trigger an automation when a HTTP POST webhook is hit",
diff --git a/packages/server/src/automations/triggers.ts b/packages/server/src/automations/triggers.ts
index 9aa80035bd..399e1a5dd6 100644
--- a/packages/server/src/automations/triggers.ts
+++ b/packages/server/src/automations/triggers.ts
@@ -13,6 +13,7 @@ import {
Row,
AutomationData,
AutomationJob,
+ AutomationEventType,
UpdatedRowEventEmitter,
} from "@budibase/types"
import { executeInThread } from "../threads/automation"
@@ -71,28 +72,31 @@ async function queueRelevantRowAutomations(
})
}
-emitter.on("row:save", async function (event: UpdatedRowEventEmitter) {
+emitter.on(
+ AutomationEventType.ROW_SAVE,
+ async function (event: UpdatedRowEventEmitter) {
+ /* istanbul ignore next */
+ if (!event || !event.row || !event.row.tableId) {
+ return
+ }
+ await queueRelevantRowAutomations(event, AutomationEventType.ROW_SAVE)
+ }
+)
+
+emitter.on(AutomationEventType.ROW_UPDATE, async function (event) {
/* istanbul ignore next */
if (!event || !event.row || !event.row.tableId) {
return
}
- await queueRelevantRowAutomations(event, "row:save")
+ await queueRelevantRowAutomations(event, AutomationEventType.ROW_UPDATE)
})
-emitter.on("row:update", async function (event) {
+emitter.on(AutomationEventType.ROW_DELETE, async function (event) {
/* istanbul ignore next */
if (!event || !event.row || !event.row.tableId) {
return
}
- await queueRelevantRowAutomations(event, "row:update")
-})
-
-emitter.on("row:delete", async function (event) {
- /* istanbul ignore next */
- if (!event || !event.row || !event.row.tableId) {
- return
- }
- await queueRelevantRowAutomations(event, "row:delete")
+ await queueRelevantRowAutomations(event, AutomationEventType.ROW_DELETE)
})
export async function externalTrigger(
@@ -118,7 +122,6 @@ export async function externalTrigger(
}
params.fields = coercedFields
}
-
const data: AutomationData = { automation, event: params as any }
if (getResponses) {
data.event = {
diff --git a/packages/server/src/tests/utilities/structures.ts b/packages/server/src/tests/utilities/structures.ts
index a59719ab2c..e65ce12873 100644
--- a/packages/server/src/tests/utilities/structures.ts
+++ b/packages/server/src/tests/utilities/structures.ts
@@ -24,6 +24,7 @@ import {
Query,
Webhook,
WebhookActionType,
+ AutomationEventType,
} from "@budibase/types"
import { LoopInput, LoopStepType } from "../../definitions/automations"
import { merge } from "lodash"
@@ -305,7 +306,7 @@ export function loopAutomation(
trigger: {
id: "a",
type: "TRIGGER",
- event: "row:save",
+ event: AutomationEventType.ROW_SAVE,
stepId: AutomationTriggerStepId.ROW_SAVED,
inputs: {
tableId,
@@ -347,7 +348,7 @@ export function collectAutomation(tableId?: string): Automation {
trigger: {
id: "a",
type: "TRIGGER",
- event: "row:save",
+ event: AutomationEventType.ROW_SAVE,
stepId: AutomationTriggerStepId.ROW_SAVED,
inputs: {
tableId,
diff --git a/packages/server/src/utilities/rowProcessor/map.ts b/packages/server/src/utilities/rowProcessor/map.ts
index 2e0ac9efe1..ccaf07ee96 100644
--- a/packages/server/src/utilities/rowProcessor/map.ts
+++ b/packages/server/src/utilities/rowProcessor/map.ts
@@ -50,6 +50,13 @@ export const TYPE_TRANSFORM_MAP: any = {
[undefined]: undefined,
parse: parseArrayString,
},
+ [FieldType.BB_REFERENCE]: {
+ //@ts-ignore
+ [null]: [],
+ //@ts-ignore
+ [undefined]: undefined,
+ parse: parseArrayString,
+ },
[FieldType.STRING]: {
"": null,
//@ts-ignore
@@ -113,6 +120,9 @@ export const TYPE_TRANSFORM_MAP: any = {
[undefined]: undefined,
parse: parseArrayString,
},
+ [FieldType.ATTACHMENT_SINGLE]: {
+ "": null,
+ },
[FieldType.BOOLEAN]: {
"": null,
//@ts-ignore
diff --git a/packages/server/src/utilities/rowProcessor/tests/inputProcessing.spec.ts b/packages/server/src/utilities/rowProcessor/tests/inputProcessing.spec.ts
index 81094583e2..244ea3794c 100644
--- a/packages/server/src/utilities/rowProcessor/tests/inputProcessing.spec.ts
+++ b/packages/server/src/utilities/rowProcessor/tests/inputProcessing.spec.ts
@@ -209,10 +209,22 @@ describe("rowProcessor - inputProcessing", () => {
const { row } = await inputProcessing(userId, table, newRow)
+ if (userValue === undefined) {
+ // The 'user' field is omitted
+ expect(row).toEqual({
+ name: "Jack",
+ })
+ } else {
+ // The update is processed if null or "". 'user' is changed to an empty array.
+ expect(row).toEqual({
+ name: "Jack",
+ user: [],
+ })
+ }
+
expect(
bbReferenceProcessor.processInputBBReferences
).not.toHaveBeenCalled()
- expect(row).toEqual(newRow)
}
)
diff --git a/packages/types/src/documents/app/automation.ts b/packages/types/src/documents/app/automation.ts
index 6ea62ffffb..5207bd4df0 100644
--- a/packages/types/src/documents/app/automation.ts
+++ b/packages/types/src/documents/app/automation.ts
@@ -255,6 +255,15 @@ export type BucketedContent = AutomationAttachmentContent & {
path: string
}
+export enum AutomationEventType {
+ ROW_SAVE = "row:save",
+ ROW_UPDATE = "row:update",
+ ROW_DELETE = "row:delete",
+ APP_TRIGGER = "app:trigger",
+ CRON_TRIGGER = "cron:trigger",
+ WEBHOOK_TRIGGER = "web:trigger",
+}
+
export type UpdatedRowEventEmitter = {
row: Row
oldRow: Row