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:
commit
421e42d8fa
|
@ -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)}
|
||||||
|
|
|
@ -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
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|
|
@ -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 => {
|
||||||
|
|
|
@ -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)
|
||||||
}
|
}
|
||||||
|
|
|
@ -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({
|
||||||
|
|
|
@ -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")
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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]],
|
||||||
|
|
|
@ -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)
|
||||||
|
|
||||||
|
|
|
@ -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",
|
||||||
|
|
|
@ -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
|
||||||
|
}
|
|
@ -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"
|
||||||
|
|
Loading…
Reference in New Issue