1
0
Fork 0
mirror of synced 2024-09-20 19:33:10 +12:00

Merge pull request #14218 from Budibase/BUDI-8430/prevent-edits-and-deletions

Prevent edits and deletions for row action automations
This commit is contained in:
Adria Navarro 2024-07-23 12:34:45 +02:00 committed by GitHub
commit 421e42d8fa
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
11 changed files with 142 additions and 62 deletions

View file

@ -6,6 +6,7 @@
contextMenuStore, contextMenuStore,
} from "stores/builder" } from "stores/builder"
import { notifications, Icon } from "@budibase/bbui" import { notifications, Icon } from "@budibase/bbui"
import { sdk } from "@budibase/shared-core"
import ConfirmDialog from "components/common/ConfirmDialog.svelte" import ConfirmDialog from "components/common/ConfirmDialog.svelte"
import UpdateAutomationModal from "components/automation/AutomationPanel/UpdateAutomationModal.svelte" import UpdateAutomationModal from "components/automation/AutomationPanel/UpdateAutomationModal.svelte"
import NavItem from "components/common/NavItem.svelte" import NavItem from "components/common/NavItem.svelte"
@ -35,45 +36,53 @@
} }
const getContextMenuItems = () => { const getContextMenuItems = () => {
return [ const isRowAction = sdk.automations.isRowAction(automation)
{ const result = []
icon: "Delete", if (!isRowAction) {
name: "Delete", result.push(
keyBind: null, ...[
visible: true, {
disabled: false, icon: "Delete",
callback: confirmDeleteDialog.show, 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", return result
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
)
},
},
]
} }
const openContextMenu = e => { const openContextMenu = e => {
@ -89,7 +98,7 @@
on:contextmenu={openContextMenu} on:contextmenu={openContextMenu}
{icon} {icon}
iconColor={"var(--spectrum-global-color-gray-900)"} iconColor={"var(--spectrum-global-color-gray-900)"}
text={automation.name} text={automation.displayName}
selected={automation._id === $selectedAutomation?._id} selected={automation._id === $selectedAutomation?._id}
hovering={automation._id === $contextMenuStore.id} hovering={automation._id === $contextMenuStore.id}
on:click={() => automationStore.actions.select(automation._id)} on:click={() => automationStore.actions.select(automation._id)}

View file

@ -19,13 +19,13 @@
}) })
.map(automation => ({ .map(automation => ({
...automation, ...automation,
name: displayName:
$automationStore.automationDisplayData[automation._id].displayName || $automationStore.automationDisplayData[automation._id].displayName ||
automation.name, automation.name,
})) }))
.sort((a, b) => { .sort((a, b) => {
const lowerA = a.name.toLowerCase() const lowerA = a.displayName.toLowerCase()
const lowerB = b.name.toLowerCase() const lowerB = b.displayName.toLowerCase()
return lowerA > lowerB ? 1 : -1 return lowerA > lowerB ? 1 : -1
}) })

View file

@ -104,19 +104,8 @@ const automationActions = store => ({
}, },
save: async automation => { save: async automation => {
const response = await API.updateAutomation(automation) const response = await API.updateAutomation(automation)
store.update(state => {
const updatedAutomation = response.automation await store.actions.fetch()
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
})
return response.automation return response.automation
}, },
delete: async automation => { delete: async automation => {

View file

@ -1,4 +1,5 @@
import * as triggers from "../../automations/triggers" import * as triggers from "../../automations/triggers"
import { sdk as coreSdk } from "@budibase/shared-core"
import { DocumentType } from "../../db/utils" import { DocumentType } from "../../db/utils"
import { updateTestHistory, removeDeprecated } from "../../automations/utils" import { updateTestHistory, removeDeprecated } from "../../automations/utils"
import { setTestFlag, clearTestFlag } from "../../utilities/redis" import { setTestFlag, clearTestFlag } from "../../utilities/redis"
@ -94,6 +95,11 @@ export async function find(ctx: UserCtx) {
export async function destroy(ctx: UserCtx<void, DeleteAutomationResponse>) { export async function destroy(ctx: UserCtx<void, DeleteAutomationResponse>) {
const automationId = ctx.params.id 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) ctx.body = await sdk.automations.remove(automationId, ctx.params.rev)
builderSocket?.emitAutomationDeletion(ctx, automationId) builderSocket?.emitAutomationDeletion(ctx, automationId)
} }

View file

@ -425,6 +425,22 @@ describe("/automations", () => {
expect(events.automation.deleted).toHaveBeenCalledTimes(1) 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 () => { it("should apply authorization to endpoint", async () => {
const automation = await config.createAutomation() const automation = await config.createAutomation()
await checkBuilderEndpoint({ await checkBuilderEndpoint({

View file

@ -1,4 +1,5 @@
import { Automation, Webhook, WebhookActionType } from "@budibase/types" import { Automation, Webhook, WebhookActionType } from "@budibase/types"
import { sdk } from "@budibase/shared-core"
import { generateAutomationID, getAutomationParams } from "../../../db/utils" import { generateAutomationID, getAutomationParams } from "../../../db/utils"
import { deleteEntityMetadata } from "../../../utilities" import { deleteEntityMetadata } from "../../../utilities"
import { MetadataTypes } from "../../../constants" import { MetadataTypes } from "../../../constants"
@ -117,7 +118,6 @@ export async function create(automation: Automation) {
export async function update(automation: Automation) { export async function update(automation: Automation) {
automation = { ...automation } automation = { ...automation }
if (!automation._id || !automation._rev) { if (!automation._id || !automation._rev) {
throw new HTTPError("_id or _rev fields missing", 400) 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")
}
} }

View file

@ -1,5 +1,6 @@
import { sample } from "lodash/fp" 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 automationSdk from "../"
import { structures } from "../../../../api/routes/tests/utilities" import { structures } from "../../../../api/routes/tests/utilities"
import TestConfiguration from "../../../../tests/utilities/TestConfiguration" import TestConfiguration from "../../../../tests/utilities/TestConfiguration"
@ -12,6 +13,38 @@ describe("automation sdk", () => {
}) })
describe("update", () => { 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([ it.each([
["trigger", (a: Automation) => a.definition.trigger], ["trigger", (a: Automation) => a.definition.trigger],
["step", (a: Automation) => a.definition.steps[0]], ["step", (a: Automation) => a.definition.steps[0]],

View file

@ -2,9 +2,9 @@ import {
Automation, Automation,
AutomationActionStepId, AutomationActionStepId,
AutomationBuilderData, AutomationBuilderData,
AutomationTriggerStepId,
TableRowActions, TableRowActions,
} from "@budibase/types" } from "@budibase/types"
import { sdk as coreSdk } from "@budibase/shared-core"
export function checkForCollectStep(automation: Automation) { export function checkForCollectStep(automation: Automation) {
return automation.definition.steps.some( return automation.definition.steps.some(
@ -39,14 +39,13 @@ export async function getBuilderData(
const result: Record<string, AutomationBuilderData> = {} const result: Record<string, AutomationBuilderData> = {}
for (const automation of automations) { for (const automation of automations) {
const { trigger } = automation.definition const isRowAction = coreSdk.automations.isRowAction(automation)
const isRowAction = trigger.stepId === AutomationTriggerStepId.ROW_ACTION
if (!isRowAction) { if (!isRowAction) {
result[automation._id!] = { displayName: automation.name } result[automation._id!] = { displayName: automation.name }
continue continue
} }
const { tableId, rowActionId } = trigger.inputs const { tableId, rowActionId } = automation.definition.trigger.inputs
const tableName = await getTableName(tableId) const tableName = await getTableName(tableId)

View file

@ -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() const automation = basicAutomation()
if (trigger) { if (trigger) {
@ -176,6 +179,16 @@ export function newAutomation({ steps, trigger }: any = {}) {
return automation return automation
} }
export function rowActionAutomation() {
const automation = newAutomation({
trigger: {
...automationTrigger(),
stepId: AutomationTriggerStepId.ROW_ACTION,
},
})
return automation
}
export function basicAutomation(appId?: string): Automation { export function basicAutomation(appId?: string): Automation {
return { return {
name: "My Automation", name: "My Automation",

View file

@ -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
}

View file

@ -1,2 +1,3 @@
export * as applications from "./applications" export * as applications from "./applications"
export * as automations from "./automations"
export * as users from "./users" export * as users from "./users"