diff --git a/packages/builder/src/components/automation/AutomationPanel/AutomationNavItem.svelte b/packages/builder/src/components/automation/AutomationPanel/AutomationNavItem.svelte index 31f86f5d78..df5ac3bd98 100644 --- a/packages/builder/src/components/automation/AutomationPanel/AutomationNavItem.svelte +++ b/packages/builder/src/components/automation/AutomationPanel/AutomationNavItem.svelte @@ -6,6 +6,7 @@ contextMenuStore, } from "stores/builder" import { notifications, Icon } from "@budibase/bbui" + import { sdk } from "@budibase/shared-core" import ConfirmDialog from "components/common/ConfirmDialog.svelte" import UpdateAutomationModal from "components/automation/AutomationPanel/UpdateAutomationModal.svelte" import NavItem from "components/common/NavItem.svelte" @@ -35,45 +36,53 @@ } const getContextMenuItems = () => { - return [ - { - icon: "Delete", - name: "Delete", - keyBind: null, - visible: true, - disabled: false, - callback: confirmDeleteDialog.show, + const isRowAction = sdk.automations.isRowAction(automation) + const result = [] + if (!isRowAction) { + result.push( + ...[ + { + icon: "Delete", + name: "Delete", + keyBind: null, + visible: true, + disabled: false, + callback: confirmDeleteDialog.show, + }, + { + icon: "Edit", + name: "Edit", + keyBind: null, + visible: true, + disabled: false, + callback: updateAutomationDialog.show, + }, + { + icon: "Duplicate", + name: "Duplicate", + keyBind: null, + visible: true, + disabled: automation.definition.trigger.name === "Webhook", + callback: duplicateAutomation, + }, + ] + ) + } + + result.push({ + icon: automation.disabled ? "CheckmarkCircle" : "Cancel", + name: automation.disabled ? "Activate" : "Pause", + keyBind: null, + visible: true, + disabled: false, + callback: () => { + automationStore.actions.toggleDisabled( + automation._id, + automation.disabled + ) }, - { - icon: "Edit", - name: "Edit", - keyBind: null, - visible: true, - disabled: false, - callback: updateAutomationDialog.show, - }, - { - icon: "Duplicate", - name: "Duplicate", - keyBind: null, - visible: true, - disabled: automation.definition.trigger.name === "Webhook", - callback: duplicateAutomation, - }, - { - icon: automation.disabled ? "CheckmarkCircle" : "Cancel", - name: automation.disabled ? "Activate" : "Pause", - keyBind: null, - visible: true, - disabled: false, - callback: () => { - automationStore.actions.toggleDisabled( - automation._id, - automation.disabled - ) - }, - }, - ] + }) + return result } const openContextMenu = e => { @@ -89,7 +98,7 @@ on:contextmenu={openContextMenu} {icon} iconColor={"var(--spectrum-global-color-gray-900)"} - text={automation.name} + text={automation.displayName} selected={automation._id === $selectedAutomation?._id} hovering={automation._id === $contextMenuStore.id} on:click={() => automationStore.actions.select(automation._id)} diff --git a/packages/builder/src/components/automation/AutomationPanel/AutomationPanel.svelte b/packages/builder/src/components/automation/AutomationPanel/AutomationPanel.svelte index 718a0dbcb5..e017e6a26a 100644 --- a/packages/builder/src/components/automation/AutomationPanel/AutomationPanel.svelte +++ b/packages/builder/src/components/automation/AutomationPanel/AutomationPanel.svelte @@ -19,13 +19,13 @@ }) .map(automation => ({ ...automation, - name: + displayName: $automationStore.automationDisplayData[automation._id].displayName || automation.name, })) .sort((a, b) => { - const lowerA = a.name.toLowerCase() - const lowerB = b.name.toLowerCase() + const lowerA = a.displayName.toLowerCase() + const lowerB = b.displayName.toLowerCase() return lowerA > lowerB ? 1 : -1 }) diff --git a/packages/builder/src/stores/builder/automations.js b/packages/builder/src/stores/builder/automations.js index c1cb963ac4..57c823da9b 100644 --- a/packages/builder/src/stores/builder/automations.js +++ b/packages/builder/src/stores/builder/automations.js @@ -104,19 +104,8 @@ const automationActions = store => ({ }, save: async automation => { const response = await API.updateAutomation(automation) - store.update(state => { - const updatedAutomation = response.automation - const existingIdx = state.automations.findIndex( - existing => existing._id === automation._id - ) - if (existingIdx !== -1) { - state.automations.splice(existingIdx, 1, updatedAutomation) - return state - } else { - state.automations = [...state.automations, updatedAutomation] - } - return state - }) + + await store.actions.fetch() return response.automation }, delete: async automation => { diff --git a/packages/server/src/api/controllers/automation.ts b/packages/server/src/api/controllers/automation.ts index 6d1f3125fd..6177868303 100644 --- a/packages/server/src/api/controllers/automation.ts +++ b/packages/server/src/api/controllers/automation.ts @@ -1,4 +1,5 @@ import * as triggers from "../../automations/triggers" +import { sdk as coreSdk } from "@budibase/shared-core" import { DocumentType } from "../../db/utils" import { updateTestHistory, removeDeprecated } from "../../automations/utils" import { setTestFlag, clearTestFlag } from "../../utilities/redis" @@ -94,6 +95,11 @@ export async function find(ctx: UserCtx) { export async function destroy(ctx: UserCtx) { const automationId = ctx.params.id + const automation = await sdk.automations.get(ctx.params.id) + if (coreSdk.automations.isRowAction(automation)) { + ctx.throw("Row actions automations cannot be deleted", 422) + } + ctx.body = await sdk.automations.remove(automationId, ctx.params.rev) builderSocket?.emitAutomationDeletion(ctx, automationId) } diff --git a/packages/server/src/api/routes/tests/automation.spec.ts b/packages/server/src/api/routes/tests/automation.spec.ts index f0c3d6e1c5..990828dcde 100644 --- a/packages/server/src/api/routes/tests/automation.spec.ts +++ b/packages/server/src/api/routes/tests/automation.spec.ts @@ -425,6 +425,22 @@ describe("/automations", () => { expect(events.automation.deleted).toHaveBeenCalledTimes(1) }) + it("cannot delete a row action automation", async () => { + const automation = await config.createAutomation( + setup.structures.rowActionAutomation() + ) + await request + .delete(`/api/automations/${automation._id}/${automation._rev}`) + .set(config.defaultHeaders()) + .expect("Content-Type", /json/) + .expect(422, { + message: "Row actions automations cannot be deleted", + status: 422, + }) + + expect(events.automation.deleted).not.toHaveBeenCalled() + }) + it("should apply authorization to endpoint", async () => { const automation = await config.createAutomation() await checkBuilderEndpoint({ diff --git a/packages/server/src/sdk/app/automations/crud.ts b/packages/server/src/sdk/app/automations/crud.ts index f3dd3e7a3c..ee7e3a3506 100644 --- a/packages/server/src/sdk/app/automations/crud.ts +++ b/packages/server/src/sdk/app/automations/crud.ts @@ -1,4 +1,5 @@ import { Automation, Webhook, WebhookActionType } from "@budibase/types" +import { sdk } from "@budibase/shared-core" import { generateAutomationID, getAutomationParams } from "../../../db/utils" import { deleteEntityMetadata } from "../../../utilities" import { MetadataTypes } from "../../../constants" @@ -117,7 +118,6 @@ export async function create(automation: Automation) { export async function update(automation: Automation) { automation = { ...automation } - if (!automation._id || !automation._rev) { throw new HTTPError("_id or _rev fields missing", 400) } @@ -281,4 +281,11 @@ function guardInvalidUpdatesAndThrow( } }) } + + if ( + sdk.automations.isRowAction(automation) && + automation.name !== oldAutomation.name + ) { + throw new Error("Row actions cannot be renamed") + } } diff --git a/packages/server/src/sdk/app/automations/tests/index.spec.ts b/packages/server/src/sdk/app/automations/tests/index.spec.ts index 124a5d9276..868dfb30ac 100644 --- a/packages/server/src/sdk/app/automations/tests/index.spec.ts +++ b/packages/server/src/sdk/app/automations/tests/index.spec.ts @@ -1,5 +1,6 @@ import { sample } from "lodash/fp" -import { Automation } from "@budibase/types" +import { Automation, AutomationTriggerStepId } from "@budibase/types" +import { generator } from "@budibase/backend-core/tests" import automationSdk from "../" import { structures } from "../../../../api/routes/tests/utilities" import TestConfiguration from "../../../../tests/utilities/TestConfiguration" @@ -12,6 +13,38 @@ describe("automation sdk", () => { }) describe("update", () => { + it("can rename existing automations", async () => { + await config.doInContext(config.getAppId(), async () => { + const automation = structures.newAutomation() + + const response = await automationSdk.create(automation) + + const newName = generator.guid() + const update = { ...response, name: newName } + const result = await automationSdk.update(update) + expect(result.name).toEqual(newName) + }) + }) + + it("cannot rename row action automations", async () => { + await config.doInContext(config.getAppId(), async () => { + const automation = structures.newAutomation({ + trigger: { + ...structures.automationTrigger(), + stepId: AutomationTriggerStepId.ROW_ACTION, + }, + }) + + const response = await automationSdk.create(automation) + + const newName = generator.guid() + const update = { ...response, name: newName } + await expect(automationSdk.update(update)).rejects.toThrow( + "Row actions cannot be renamed" + ) + }) + }) + it.each([ ["trigger", (a: Automation) => a.definition.trigger], ["step", (a: Automation) => a.definition.steps[0]], diff --git a/packages/server/src/sdk/app/automations/utils.ts b/packages/server/src/sdk/app/automations/utils.ts index 52ed935f3b..ad06e55418 100644 --- a/packages/server/src/sdk/app/automations/utils.ts +++ b/packages/server/src/sdk/app/automations/utils.ts @@ -2,9 +2,9 @@ import { Automation, AutomationActionStepId, AutomationBuilderData, - AutomationTriggerStepId, TableRowActions, } from "@budibase/types" +import { sdk as coreSdk } from "@budibase/shared-core" export function checkForCollectStep(automation: Automation) { return automation.definition.steps.some( @@ -39,14 +39,13 @@ export async function getBuilderData( const result: Record = {} for (const automation of automations) { - const { trigger } = automation.definition - const isRowAction = trigger.stepId === AutomationTriggerStepId.ROW_ACTION + const isRowAction = coreSdk.automations.isRowAction(automation) if (!isRowAction) { result[automation._id!] = { displayName: automation.name } continue } - const { tableId, rowActionId } = trigger.inputs + const { tableId, rowActionId } = automation.definition.trigger.inputs const tableName = await getTableName(tableId) diff --git a/packages/server/src/tests/utilities/structures.ts b/packages/server/src/tests/utilities/structures.ts index 970df2e2c9..16ab049eb4 100644 --- a/packages/server/src/tests/utilities/structures.ts +++ b/packages/server/src/tests/utilities/structures.ts @@ -158,7 +158,10 @@ export function automationTrigger( } } -export function newAutomation({ steps, trigger }: any = {}) { +export function newAutomation({ + steps, + trigger, +}: { steps?: AutomationStep[]; trigger?: AutomationTrigger } = {}) { const automation = basicAutomation() if (trigger) { @@ -176,6 +179,16 @@ export function newAutomation({ steps, trigger }: any = {}) { return automation } +export function rowActionAutomation() { + const automation = newAutomation({ + trigger: { + ...automationTrigger(), + stepId: AutomationTriggerStepId.ROW_ACTION, + }, + }) + return automation +} + export function basicAutomation(appId?: string): Automation { return { name: "My Automation", diff --git a/packages/shared-core/src/sdk/documents/automations.ts b/packages/shared-core/src/sdk/documents/automations.ts new file mode 100644 index 0000000000..93e08c86e0 --- /dev/null +++ b/packages/shared-core/src/sdk/documents/automations.ts @@ -0,0 +1,7 @@ +import { Automation, AutomationTriggerStepId } from "@budibase/types" + +export function isRowAction(automation: Automation) { + const result = + automation.definition.trigger.stepId === AutomationTriggerStepId.ROW_ACTION + return result +} diff --git a/packages/shared-core/src/sdk/documents/index.ts b/packages/shared-core/src/sdk/documents/index.ts index d20631eef4..4b17c1ea08 100644 --- a/packages/shared-core/src/sdk/documents/index.ts +++ b/packages/shared-core/src/sdk/documents/index.ts @@ -1,2 +1,3 @@ export * as applications from "./applications" +export * as automations from "./automations" export * as users from "./users"