FIX broken references in a list of actions (#12459)

* Refactor

* Update action bindings on delete

* Update action bindings on move

* Fix with additional tests

* Ensure visible binding is updated on drag release

* fix

* Refresh visible binding when action is deleted

* Refactor

* Refactor
This commit is contained in:
melohagan 2023-11-29 14:48:50 +00:00 committed by GitHub
parent 37dc8ba6e4
commit c2a82bb021
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
4 changed files with 581 additions and 27 deletions

View File

@ -29,6 +29,12 @@ const CAPTURE_VAR_INSIDE_TEMPLATE = /{{([^}]+)}}/g
const CAPTURE_VAR_INSIDE_JS = /\$\("([^")]+)"\)/g const CAPTURE_VAR_INSIDE_JS = /\$\("([^")]+)"\)/g
const CAPTURE_HBS_TEMPLATE = /{{[\S\s]*?}}/g const CAPTURE_HBS_TEMPLATE = /{{[\S\s]*?}}/g
const UpdateReferenceAction = {
ADD: "add",
DELETE: "delete",
MOVE: "move",
}
/** /**
* Gets all bindable data context fields and instance fields. * Gets all bindable data context fields and instance fields.
*/ */
@ -1226,3 +1232,81 @@ export const runtimeToReadableBinding = (
"readableBinding" "readableBinding"
) )
} }
/**
* Used to update binding references for automation or action steps
*
* @param obj - The object to be updated
* @param originalIndex - The original index of the step being moved. Not applicable to add/delete.
* @param modifiedIndex - The new index of the step being modified
* @param action - Used to determine if a step is being added, deleted or moved
* @param label - The binding text that describes the steps
*/
export const updateReferencesInObject = ({
obj,
modifiedIndex,
action,
label,
originalIndex,
}) => {
const stepIndexRegex = new RegExp(`{{\\s*${label}\\.(\\d+)\\.`, "g")
const updateActionStep = (str, index, replaceWith) =>
str.replace(`{{ ${label}.${index}.`, `{{ ${label}.${replaceWith}.`)
for (const key in obj) {
if (typeof obj[key] === "string") {
let matches
while ((matches = stepIndexRegex.exec(obj[key])) !== null) {
const referencedStep = parseInt(matches[1])
if (
action === UpdateReferenceAction.ADD &&
referencedStep >= modifiedIndex
) {
obj[key] = updateActionStep(
obj[key],
referencedStep,
referencedStep + 1
)
} else if (
action === UpdateReferenceAction.DELETE &&
referencedStep > modifiedIndex
) {
obj[key] = updateActionStep(
obj[key],
referencedStep,
referencedStep - 1
)
} else if (action === UpdateReferenceAction.MOVE) {
if (referencedStep === originalIndex) {
obj[key] = updateActionStep(obj[key], referencedStep, modifiedIndex)
} else if (
modifiedIndex <= referencedStep &&
modifiedIndex < originalIndex
) {
obj[key] = updateActionStep(
obj[key],
referencedStep,
referencedStep + 1
)
} else if (
modifiedIndex >= referencedStep &&
modifiedIndex > originalIndex
) {
obj[key] = updateActionStep(
obj[key],
referencedStep,
referencedStep - 1
)
}
}
}
} else if (typeof obj[key] === "object" && obj[key] !== null) {
updateReferencesInObject({
obj: obj[key],
modifiedIndex,
action,
label,
originalIndex,
})
}
}
}

View File

@ -4,6 +4,7 @@ import { cloneDeep } from "lodash/fp"
import { generate } from "shortid" import { generate } from "shortid"
import { selectedAutomation } from "builderStore" import { selectedAutomation } from "builderStore"
import { notifications } from "@budibase/bbui" import { notifications } from "@budibase/bbui"
import { updateReferencesInObject } from "builderStore/dataBinding"
const initialAutomationState = { const initialAutomationState = {
automations: [], automations: [],
@ -22,34 +23,14 @@ export const getAutomationStore = () => {
return store return store
} }
const updateReferencesInObject = (obj, modifiedIndex, action) => {
const regex = /{{\s*steps\.(\d+)\./g
for (const key in obj) {
if (typeof obj[key] === "string") {
let matches
while ((matches = regex.exec(obj[key])) !== null) {
const referencedStep = parseInt(matches[1])
if (action === "add" && referencedStep >= modifiedIndex) {
obj[key] = obj[key].replace(
`{{ steps.${referencedStep}.`,
`{{ steps.${referencedStep + 1}.`
)
} else if (action === "delete" && referencedStep > modifiedIndex) {
obj[key] = obj[key].replace(
`{{ steps.${referencedStep}.`,
`{{ steps.${referencedStep - 1}.`
)
}
}
} else if (typeof obj[key] === "object" && obj[key] !== null) {
updateReferencesInObject(obj[key], modifiedIndex, action)
}
}
}
const updateStepReferences = (steps, modifiedIndex, action) => { const updateStepReferences = (steps, modifiedIndex, action) => {
steps.forEach(step => { steps.forEach(step => {
updateReferencesInObject(step.inputs, modifiedIndex, action) updateReferencesInObject({
obj: step.inputs,
modifiedIndex,
action,
label: "steps",
})
}) })
} }

View File

@ -2,6 +2,7 @@ import { expect, describe, it, vi } from "vitest"
import { import {
runtimeToReadableBinding, runtimeToReadableBinding,
readableToRuntimeBinding, readableToRuntimeBinding,
updateReferencesInObject,
} from "../dataBinding" } from "../dataBinding"
vi.mock("@budibase/frontend-core") vi.mock("@budibase/frontend-core")
@ -84,3 +85,461 @@ describe("readableToRuntimeBinding", () => {
).toEqual(`Hello {{ [user].[firstName] }}! The count is {{ count }}.`) ).toEqual(`Hello {{ [user].[firstName] }}! The count is {{ count }}.`)
}) })
}) })
describe("updateReferencesInObject", () => {
it("should increment steps in sequence on 'add'", () => {
let obj = [
{
id: "a0",
parameters: {
text: "Alpha",
},
},
{
id: "a1",
parameters: {
text: "Apple",
},
},
{
id: "b2",
parameters: {
text: "Banana {{ actions.1.row }}",
},
},
{
id: "c3",
parameters: {
text: "Carrot {{ actions.1.row }}",
},
},
{
id: "d4",
parameters: {
text: "Dog {{ actions.3.row }}",
},
},
{
id: "e5",
parameters: {
text: "Eagle {{ actions.4.row }}",
},
},
]
updateReferencesInObject({
obj,
modifiedIndex: 0,
action: "add",
label: "actions",
})
expect(obj).toEqual([
{
id: "a0",
parameters: {
text: "Alpha",
},
},
{
id: "a1",
parameters: {
text: "Apple",
},
},
{
id: "b2",
parameters: {
text: "Banana {{ actions.2.row }}",
},
},
{
id: "c3",
parameters: {
text: "Carrot {{ actions.2.row }}",
},
},
{
id: "d4",
parameters: {
text: "Dog {{ actions.4.row }}",
},
},
{
id: "e5",
parameters: {
text: "Eagle {{ actions.5.row }}",
},
},
])
})
it("should decrement steps in sequence on 'delete'", () => {
let obj = [
{
id: "a1",
parameters: {
text: "Apple",
},
},
{
id: "b2",
parameters: {
text: "Banana {{ actions.1.row }}",
},
},
{
id: "d4",
parameters: {
text: "Dog {{ actions.3.row }}",
},
},
{
id: "e5",
parameters: {
text: "Eagle {{ actions.4.row }}",
},
},
]
updateReferencesInObject({
obj,
modifiedIndex: 2,
action: "delete",
label: "actions",
})
expect(obj).toEqual([
{
id: "a1",
parameters: {
text: "Apple",
},
},
{
id: "b2",
parameters: {
text: "Banana {{ actions.1.row }}",
},
},
{
id: "d4",
parameters: {
text: "Dog {{ actions.2.row }}",
},
},
{
id: "e5",
parameters: {
text: "Eagle {{ actions.3.row }}",
},
},
])
})
it("should handle on 'move' to a lower index", () => {
let obj = [
{
id: "a1",
parameters: {
text: "Apple",
},
},
{
id: "b2",
parameters: {
text: "Banana {{ actions.0.row }}",
},
},
{
id: "e5",
parameters: {
text: "Eagle {{ actions.3.row }}",
},
},
{
id: "c3",
parameters: {
text: "Carrot {{ actions.0.row }}",
},
},
{
id: "d4",
parameters: {
text: "Dog {{ actions.2.row }}",
},
},
]
updateReferencesInObject({
obj,
modifiedIndex: 2,
action: "move",
label: "actions",
originalIndex: 4,
})
expect(obj).toEqual([
{
id: "a1",
parameters: {
text: "Apple",
},
},
{
id: "b2",
parameters: {
text: "Banana {{ actions.0.row }}",
},
},
{
id: "e5",
parameters: {
text: "Eagle {{ actions.4.row }}",
},
},
{
id: "c3",
parameters: {
text: "Carrot {{ actions.0.row }}",
},
},
{
id: "d4",
parameters: {
text: "Dog {{ actions.3.row }}",
},
},
])
})
it("should handle on 'move' to a higher index", () => {
let obj = [
{
id: "b2",
parameters: {
text: "Banana {{ actions.0.row }}",
},
},
{
id: "c3",
parameters: {
text: "Carrot {{ actions.0.row }}",
},
},
{
id: "a1",
parameters: {
text: "Apple",
},
},
{
id: "d4",
parameters: {
text: "Dog {{ actions.2.row }}",
},
},
{
id: "e5",
parameters: {
text: "Eagle {{ actions.3.row }}",
},
},
]
updateReferencesInObject({
obj,
modifiedIndex: 2,
action: "move",
label: "actions",
originalIndex: 0,
})
expect(obj).toEqual([
{
id: "b2",
parameters: {
text: "Banana {{ actions.2.row }}",
},
},
{
id: "c3",
parameters: {
text: "Carrot {{ actions.2.row }}",
},
},
{
id: "a1",
parameters: {
text: "Apple",
},
},
{
id: "d4",
parameters: {
text: "Dog {{ actions.1.row }}",
},
},
{
id: "e5",
parameters: {
text: "Eagle {{ actions.3.row }}",
},
},
])
})
it("should handle on 'move' of action being referenced, dragged to a higher index", () => {
let obj = [
{
"##eventHandlerType": "Validate Form",
id: "cCD0Dwcnq",
},
{
"##eventHandlerType": "Close Screen Modal",
id: "3fbbIOfN0H",
},
{
"##eventHandlerType": "Save Row",
parameters: {
tableId: "ta_bb_employee",
},
id: "aehg5cTmhR",
},
{
"##eventHandlerType": "Close Side Panel",
id: "mzkpf86cxo",
},
{
"##eventHandlerType": "Navigate To",
id: "h0uDFeJa8A",
},
{
parameters: {
autoDismiss: true,
type: "success",
message: "{{ actions.1.row }}",
},
"##eventHandlerType": "Show Notification",
id: "JEI5lAyJZ",
},
]
updateReferencesInObject({
obj,
modifiedIndex: 2,
action: "move",
label: "actions",
originalIndex: 1,
})
expect(obj).toEqual([
{
"##eventHandlerType": "Validate Form",
id: "cCD0Dwcnq",
},
{
"##eventHandlerType": "Close Screen Modal",
id: "3fbbIOfN0H",
},
{
"##eventHandlerType": "Save Row",
parameters: {
tableId: "ta_bb_employee",
},
id: "aehg5cTmhR",
},
{
"##eventHandlerType": "Close Side Panel",
id: "mzkpf86cxo",
},
{
"##eventHandlerType": "Navigate To",
id: "h0uDFeJa8A",
},
{
parameters: {
autoDismiss: true,
type: "success",
message: "{{ actions.2.row }}",
},
"##eventHandlerType": "Show Notification",
id: "JEI5lAyJZ",
},
])
})
it("should handle on 'move' of action being referenced, dragged to a lower index", () => {
let obj = [
{
"##eventHandlerType": "Save Row",
parameters: {
tableId: "ta_bb_employee",
},
id: "aehg5cTmhR",
},
{
"##eventHandlerType": "Validate Form",
id: "cCD0Dwcnq",
},
{
"##eventHandlerType": "Close Screen Modal",
id: "3fbbIOfN0H",
},
{
"##eventHandlerType": "Close Side Panel",
id: "mzkpf86cxo",
},
{
"##eventHandlerType": "Navigate To",
id: "h0uDFeJa8A",
},
{
parameters: {
autoDismiss: true,
type: "success",
message: "{{ actions.4.row }}",
},
"##eventHandlerType": "Show Notification",
id: "JEI5lAyJZ",
},
]
updateReferencesInObject({
obj,
modifiedIndex: 0,
action: "move",
label: "actions",
originalIndex: 4,
})
expect(obj).toEqual([
{
"##eventHandlerType": "Save Row",
parameters: {
tableId: "ta_bb_employee",
},
id: "aehg5cTmhR",
},
{
"##eventHandlerType": "Validate Form",
id: "cCD0Dwcnq",
},
{
"##eventHandlerType": "Close Screen Modal",
id: "3fbbIOfN0H",
},
{
"##eventHandlerType": "Close Side Panel",
id: "mzkpf86cxo",
},
{
"##eventHandlerType": "Navigate To",
id: "h0uDFeJa8A",
},
{
parameters: {
autoDismiss: true,
type: "success",
message: "{{ actions.0.row }}",
},
"##eventHandlerType": "Show Notification",
id: "JEI5lAyJZ",
},
])
})
})

View File

@ -15,6 +15,7 @@
getEventContextBindings, getEventContextBindings,
getActionBindings, getActionBindings,
makeStateBinding, makeStateBinding,
updateReferencesInObject,
} from "builderStore/dataBinding" } from "builderStore/dataBinding"
import { cloneDeep } from "lodash/fp" import { cloneDeep } from "lodash/fp"
@ -30,6 +31,7 @@
let actionQuery let actionQuery
let selectedAction = actions?.length ? actions[0] : null let selectedAction = actions?.length ? actions[0] : null
let originalActionIndex
const setUpdateActions = actions => { const setUpdateActions = actions => {
return actions return actions
@ -115,6 +117,14 @@
if (isSelected) { if (isSelected) {
selectedAction = actions?.length ? actions[0] : null selectedAction = actions?.length ? actions[0] : null
} }
// Update action binding references
updateReferencesInObject({
obj: actions,
modifiedIndex: index,
action: "delete",
label: "actions",
})
} }
const toggleActionList = () => { const toggleActionList = () => {
@ -146,9 +156,29 @@
function handleDndConsider(e) { function handleDndConsider(e) {
actions = e.detail.items actions = e.detail.items
// set the initial index of the action being dragged
if (e.detail.info.trigger === "draggedEntered") {
originalActionIndex = actions.findIndex(
action => action.id === e.detail.info.id
)
}
} }
function handleDndFinalize(e) { function handleDndFinalize(e) {
actions = e.detail.items actions = e.detail.items
// Update action binding references
updateReferencesInObject({
obj: actions,
modifiedIndex: actions.findIndex(
action => action.id === e.detail.info.id
),
action: "move",
label: "actions",
originalIndex: originalActionIndex,
})
originalActionIndex = -1
} }
const getAllBindings = (actionBindings, eventContextBindings, actions) => { const getAllBindings = (actionBindings, eventContextBindings, actions) => {
@ -289,7 +319,7 @@
</Layout> </Layout>
<Layout noPadding> <Layout noPadding>
{#if selectedActionComponent && !showAvailableActions} {#if selectedActionComponent && !showAvailableActions}
{#key selectedAction.id} {#key (selectedAction.id, originalActionIndex)}
<div class="selected-action-container"> <div class="selected-action-container">
<svelte:component <svelte:component
this={selectedActionComponent} this={selectedActionComponent}