From 18d7176ab72a20017dc6bba04a7b02187a4e389d Mon Sep 17 00:00:00 2001 From: Andrew Kingston Date: Wed, 2 Mar 2022 17:45:01 +0000 Subject: [PATCH] Improve copy and paste to support keeping all data bindings valid --- .../src/builderStore/componentUtils.js | 61 +++++++++++++++++++ .../src/builderStore/store/frontend.js | 23 ++----- .../ComponentDropdownMenu.svelte | 6 +- packages/string-templates/src/index.cjs | 1 + packages/string-templates/src/index.js | 9 +++ packages/string-templates/src/index.mjs | 1 + 6 files changed, 79 insertions(+), 22 deletions(-) diff --git a/packages/builder/src/builderStore/componentUtils.js b/packages/builder/src/builderStore/componentUtils.js index 04a87998fe..557d83a00c 100644 --- a/packages/builder/src/builderStore/componentUtils.js +++ b/packages/builder/src/builderStore/componentUtils.js @@ -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) + } +} diff --git a/packages/builder/src/builderStore/store/frontend.js b/packages/builder/src/builderStore/store/frontend.js index d8118c9c60..c8e4c85b06 100644 --- a/packages/builder/src/builderStore/store/frontend.js +++ b/packages/builder/src/builderStore/store/frontend.js @@ -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") { diff --git a/packages/builder/src/components/design/NavigationPanel/ComponentDropdownMenu.svelte b/packages/builder/src/components/design/NavigationPanel/ComponentDropdownMenu.svelte index 0fcd43b58e..c8796fab25 100644 --- a/packages/builder/src/components/design/NavigationPanel/ComponentDropdownMenu.svelte +++ b/packages/builder/src/components/design/NavigationPanel/ComponentDropdownMenu.svelte @@ -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") } diff --git a/packages/string-templates/src/index.cjs b/packages/string-templates/src/index.cjs index 5a84c45d78..9584374880 100644 --- a/packages/string-templates/src/index.cjs +++ b/packages/string-templates/src/index.cjs @@ -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 diff --git a/packages/string-templates/src/index.js b/packages/string-templates/src/index.js index eedd9423ee..76ad979670 100644 --- a/packages/string-templates/src/index.js +++ b/packages/string-templates/src/index.js @@ -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) +} diff --git a/packages/string-templates/src/index.mjs b/packages/string-templates/src/index.mjs index f2cffdf378..a9745b3a34 100644 --- a/packages/string-templates/src/index.mjs +++ b/packages/string-templates/src/index.mjs @@ -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