Improve copy and paste to support keeping all data bindings valid

This commit is contained in:
Andrew Kingston 2022-03-02 17:45:01 +00:00
parent 5f390383b4
commit e411354538
6 changed files with 79 additions and 22 deletions

View File

@ -1,4 +1,10 @@
import { store } from "./index"
import { Helpers } from "@budibase/bbui"
import {
decodeJSBinding,
encodeJSBinding,
findAllBindings,
} from "@budibase/string-templates"
/**
* Recursively searches for a specific component ID
@ -161,3 +167,58 @@ export const getComponentSettings = componentType => {
return settings
}
/**
* Randomises a components ID's, including all child component IDs, and also
* updates all data bindings to still be valid.
* This mutates the object in place.
* @param component the component to randomise
*/
export const makeComponentUnique = component => {
if (!component) {
return
}
// Replace component ID
const oldId = component._id
const newId = Helpers.uuid()
component._id = newId
if (component._children?.length) {
let children = JSON.stringify(component._children)
// Replace all instances of this ID in child HBS bindings
children = children.replace(new RegExp(oldId, "g"), newId)
// Replace all instances of this ID in child JS bindings
const bindings = findAllBindings(children)
bindings.forEach(binding => {
// JSON.stringify will have escaped double quotes, so we need
// to account for that
let sanitizedBinding = binding.replace(/\\"/g, '"')
// Check if this is a valid JS binding
let js = decodeJSBinding(sanitizedBinding)
if (js != null) {
// Replace ID inside JS binding
js = js.replace(new RegExp(oldId, "g"), newId)
// Create new valid JS binding
let newBinding = encodeJSBinding(js)
// Replace escaped double quotes
newBinding = newBinding.replace(/"/g, '\\"')
// Insert new JS back into binding.
// A single string replace here is better than a regex as
// the binding contains special characters, and we only need
// to replace a single instance.
children = children.replace(binding, newBinding)
}
})
// Recurse on all children
component._children = JSON.parse(children)
component._children.forEach(makeComponentUnique)
}
}

View File

@ -24,9 +24,9 @@ import {
findAllMatchingComponents,
findComponent,
getComponentSettings,
makeComponentUnique,
} from "../componentUtils"
import { Helpers } from "@budibase/bbui"
import { removeBindings } from "../dataBinding"
const INITIAL_FRONTEND_STATE = {
apps: [],
@ -490,37 +490,22 @@ export const getFrontendStore = () => {
}
}
},
paste: async (targetComponent, mode, preserveBindings = false) => {
paste: async (targetComponent, mode) => {
let promises = []
store.update(state => {
// Stop if we have nothing to paste
if (!state.componentToPaste) {
return state
}
// defines if this is a copy or a cut
const cut = state.componentToPaste.isCut
// immediately need to remove bindings, currently these aren't valid when pasted
if (!cut && !preserveBindings) {
state.componentToPaste = removeBindings(state.componentToPaste, "")
}
// Clone the component to paste
// Retain the same ID if cutting as things may be referencing this component
// Clone the component to paste and make unique if copying
delete state.componentToPaste.isCut
let componentToPaste = cloneDeep(state.componentToPaste)
if (cut) {
state.componentToPaste = null
} else {
const randomizeIds = component => {
if (!component) {
return
}
component._id = Helpers.uuid()
component._children?.forEach(randomizeIds)
}
randomizeIds(componentToPaste)
makeComponentUnique(componentToPaste)
}
if (mode === "inside") {

View File

@ -61,7 +61,7 @@
const duplicateComponent = () => {
storeComponentForCopy(false)
pasteComponent("below", true)
pasteComponent("below")
}
const deleteComponent = async () => {
@ -77,10 +77,10 @@
store.actions.components.copy(component, cut)
}
const pasteComponent = (mode, preserveBindings = false) => {
const pasteComponent = mode => {
try {
// lives in store - also used by drag drop
store.actions.components.paste(component, mode, preserveBindings)
store.actions.components.paste(component, mode)
} catch (error) {
notifications.error("Error saving component")
}

View File

@ -18,6 +18,7 @@ module.exports.processObject = templates.processObject
module.exports.doesContainStrings = templates.doesContainStrings
module.exports.doesContainString = templates.doesContainString
module.exports.disableEscaping = templates.disableEscaping
module.exports.findAllBindings = templates.findAllBindings
/**
* Use vm2 to run JS scripts in a node env

View File

@ -322,3 +322,12 @@ module.exports.doesContainStrings = (template, strings) => {
module.exports.doesContainString = (template, string) => {
return exports.doesContainStrings(template, [string])
}
/**
* Finds all regular (double bracketed) expressions inside a string.
* @param string the string to search
* @return {[]} all matching bindings
*/
module.exports.findAllBindings = string => {
return findDoubleHbsInstances(string)
}

View File

@ -18,6 +18,7 @@ export const processObject = templates.processObject
export const doesContainStrings = templates.doesContainStrings
export const doesContainString = templates.doesContainString
export const disableEscaping = templates.disableEscaping
export const findAllBindings = templates.findAllBindings
/**
* Use polyfilled vm to run JS scripts in a browser Env