2024-01-30 04:21:52 +13:00
|
|
|
import { componentStore } from "stores/builder"
|
2023-11-22 03:51:01 +13:00
|
|
|
import { get } from "svelte/store"
|
2022-03-03 06:45:01 +13:00
|
|
|
import { Helpers } from "@budibase/bbui"
|
|
|
|
import {
|
|
|
|
decodeJSBinding,
|
|
|
|
encodeJSBinding,
|
2022-03-07 22:32:31 +13:00
|
|
|
findHBSBlocks,
|
2022-03-03 06:45:01 +13:00
|
|
|
} from "@budibase/string-templates"
|
2023-10-20 21:44:42 +13:00
|
|
|
import { capitalise } from "helpers"
|
2024-02-03 03:59:45 +13:00
|
|
|
import { Constants } from "@budibase/frontend-core"
|
|
|
|
|
|
|
|
const { ContextScopes } = Constants
|
2021-11-05 00:30:43 +13:00
|
|
|
|
2020-12-08 04:27:46 +13:00
|
|
|
/**
|
2021-01-13 09:00:35 +13:00
|
|
|
* Recursively searches for a specific component ID
|
2020-12-08 04:27:46 +13:00
|
|
|
*/
|
2021-01-13 09:00:35 +13:00
|
|
|
export const findComponent = (rootComponent, id) => {
|
2021-05-04 22:32:22 +12:00
|
|
|
return searchComponentTree(rootComponent, comp => comp._id === id)
|
2020-06-01 23:12:25 +12:00
|
|
|
}
|
|
|
|
|
2021-01-13 09:00:35 +13:00
|
|
|
/**
|
|
|
|
* Recursively searches for a specific component type
|
|
|
|
*/
|
|
|
|
export const findComponentType = (rootComponent, type) => {
|
2021-05-04 22:32:22 +12:00
|
|
|
return searchComponentTree(rootComponent, comp => comp._component === type)
|
2021-01-13 09:00:35 +13:00
|
|
|
}
|
2020-06-01 23:12:25 +12:00
|
|
|
|
2021-01-13 09:00:35 +13:00
|
|
|
/**
|
|
|
|
* Recursively searches for the parent component of a specific component ID
|
|
|
|
*/
|
|
|
|
export const findComponentParent = (rootComponent, id, parentComponent) => {
|
|
|
|
if (!rootComponent || !id) {
|
|
|
|
return null
|
|
|
|
}
|
|
|
|
if (rootComponent._id === id) {
|
|
|
|
return parentComponent
|
|
|
|
}
|
|
|
|
if (!rootComponent._children) {
|
|
|
|
return null
|
|
|
|
}
|
|
|
|
for (const child of rootComponent._children) {
|
|
|
|
const childResult = findComponentParent(child, id, rootComponent)
|
|
|
|
if (childResult) {
|
|
|
|
return childResult
|
2020-06-01 23:12:25 +12:00
|
|
|
}
|
|
|
|
}
|
2021-01-13 09:00:35 +13:00
|
|
|
return null
|
2020-06-01 23:15:44 +12:00
|
|
|
}
|
2020-06-02 22:11:53 +12:00
|
|
|
|
2021-01-13 09:00:35 +13:00
|
|
|
/**
|
|
|
|
* Recursively searches for a specific component ID and records the component
|
|
|
|
* path to this component
|
|
|
|
*/
|
|
|
|
export const findComponentPath = (rootComponent, id, path = []) => {
|
|
|
|
if (!rootComponent || !id) {
|
2021-01-15 04:39:50 +13:00
|
|
|
return []
|
2020-10-18 06:20:06 +13:00
|
|
|
}
|
2021-01-13 09:00:35 +13:00
|
|
|
if (rootComponent._id === id) {
|
2021-01-15 04:39:50 +13:00
|
|
|
return [...path, rootComponent]
|
2020-10-18 06:20:06 +13:00
|
|
|
}
|
2021-01-13 09:00:35 +13:00
|
|
|
if (!rootComponent._children) {
|
2021-01-15 04:39:50 +13:00
|
|
|
return []
|
2020-10-18 06:20:06 +13:00
|
|
|
}
|
2021-01-13 09:00:35 +13:00
|
|
|
for (const child of rootComponent._children) {
|
2021-01-15 04:39:50 +13:00
|
|
|
const newPath = [...path, rootComponent]
|
2021-01-13 09:00:35 +13:00
|
|
|
const childResult = findComponentPath(child, id, newPath)
|
2021-01-15 04:39:50 +13:00
|
|
|
if (childResult?.length) {
|
2021-01-13 09:00:35 +13:00
|
|
|
return childResult
|
|
|
|
}
|
|
|
|
}
|
2021-01-15 04:39:50 +13:00
|
|
|
return []
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
2021-01-27 03:40:44 +13:00
|
|
|
* Recurses through the component tree and finds all components which match
|
|
|
|
* a certain selector
|
2021-01-15 04:39:50 +13:00
|
|
|
*/
|
|
|
|
export const findAllMatchingComponents = (rootComponent, selector) => {
|
|
|
|
if (!rootComponent || !selector) {
|
|
|
|
return []
|
|
|
|
}
|
|
|
|
let components = []
|
|
|
|
if (rootComponent._children) {
|
2021-05-04 22:32:22 +12:00
|
|
|
rootComponent._children.forEach(child => {
|
2021-01-15 04:39:50 +13:00
|
|
|
components = [
|
|
|
|
...components,
|
|
|
|
...findAllMatchingComponents(child, selector),
|
|
|
|
]
|
|
|
|
})
|
|
|
|
}
|
|
|
|
if (selector(rootComponent)) {
|
|
|
|
components.push(rootComponent)
|
|
|
|
}
|
|
|
|
return components.reverse()
|
2021-01-13 09:00:35 +13:00
|
|
|
}
|
2020-10-18 06:20:06 +13:00
|
|
|
|
2021-01-27 03:40:44 +13:00
|
|
|
/**
|
2024-01-23 00:10:03 +13:00
|
|
|
* Recurses through the component tree and finds all components.
|
|
|
|
*/
|
|
|
|
export const findAllComponents = rootComponent => {
|
|
|
|
return findAllMatchingComponents(rootComponent, () => true)
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Finds the closest parent component which matches certain criteria
|
2021-01-27 03:40:44 +13:00
|
|
|
*/
|
|
|
|
export const findClosestMatchingComponent = (
|
|
|
|
rootComponent,
|
|
|
|
componentId,
|
|
|
|
selector
|
|
|
|
) => {
|
|
|
|
if (!selector) {
|
|
|
|
return null
|
|
|
|
}
|
|
|
|
const componentPath = findComponentPath(rootComponent, componentId).reverse()
|
|
|
|
for (let component of componentPath) {
|
|
|
|
if (selector(component)) {
|
|
|
|
return component
|
|
|
|
}
|
|
|
|
}
|
|
|
|
return null
|
|
|
|
}
|
|
|
|
|
2021-01-13 09:00:35 +13:00
|
|
|
/**
|
|
|
|
* Recurses through a component tree evaluating a matching function against
|
|
|
|
* components until a match is found
|
|
|
|
*/
|
|
|
|
const searchComponentTree = (rootComponent, matchComponent) => {
|
|
|
|
if (!rootComponent || !matchComponent) {
|
|
|
|
return null
|
|
|
|
}
|
|
|
|
if (matchComponent(rootComponent)) {
|
|
|
|
return rootComponent
|
|
|
|
}
|
|
|
|
if (!rootComponent._children) {
|
|
|
|
return null
|
|
|
|
}
|
|
|
|
for (const child of rootComponent._children) {
|
|
|
|
const childResult = searchComponentTree(child, matchComponent)
|
2020-10-18 06:20:06 +13:00
|
|
|
if (childResult) {
|
|
|
|
return childResult
|
|
|
|
}
|
|
|
|
}
|
|
|
|
return null
|
|
|
|
}
|
2021-11-05 00:30:43 +13:00
|
|
|
|
2022-03-03 06:45:01 +13:00
|
|
|
/**
|
|
|
|
* 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
|
|
|
|
}
|
|
|
|
|
2022-11-18 03:49:13 +13:00
|
|
|
// Generate a full set of component ID replacements in this tree
|
|
|
|
const idReplacements = []
|
|
|
|
const generateIdReplacements = (component, replacements) => {
|
|
|
|
const oldId = component._id
|
|
|
|
const newId = Helpers.uuid()
|
|
|
|
replacements.push([oldId, newId])
|
|
|
|
component._children?.forEach(x => generateIdReplacements(x, replacements))
|
|
|
|
}
|
|
|
|
generateIdReplacements(component, idReplacements)
|
2022-09-06 20:21:16 +12:00
|
|
|
|
|
|
|
// Replace all instances of this ID in HBS bindings
|
2022-11-18 03:49:13 +13:00
|
|
|
let definition = JSON.stringify(component)
|
|
|
|
idReplacements.forEach(([oldId, newId]) => {
|
|
|
|
definition = definition.replace(new RegExp(oldId, "g"), newId)
|
|
|
|
})
|
2022-09-06 20:21:16 +12:00
|
|
|
|
|
|
|
// Replace all instances of this ID in JS bindings
|
|
|
|
const bindings = findHBSBlocks(definition)
|
|
|
|
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
|
2022-11-18 03:49:13 +13:00
|
|
|
idReplacements.forEach(([oldId, newId]) => {
|
|
|
|
js = js.replace(new RegExp(oldId, "g"), newId)
|
|
|
|
})
|
2022-09-06 20:21:16 +12:00
|
|
|
|
|
|
|
// 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.
|
|
|
|
definition = definition.replace(binding, newBinding)
|
|
|
|
}
|
|
|
|
})
|
2022-03-03 06:45:01 +13:00
|
|
|
|
2022-09-06 20:21:16 +12:00
|
|
|
// Recurse on all children
|
2022-11-18 03:49:13 +13:00
|
|
|
return JSON.parse(definition)
|
2022-03-03 06:45:01 +13:00
|
|
|
}
|
2023-10-20 21:44:42 +13:00
|
|
|
|
|
|
|
export const getComponentText = component => {
|
2023-11-22 03:51:01 +13:00
|
|
|
if (component == null) {
|
|
|
|
return ""
|
|
|
|
}
|
|
|
|
|
2023-10-20 21:44:42 +13:00
|
|
|
if (component?._instanceName) {
|
|
|
|
return component._instanceName
|
|
|
|
}
|
|
|
|
const type =
|
|
|
|
component._component.replace("@budibase/standard-components/", "") ||
|
|
|
|
"component"
|
|
|
|
return capitalise(type)
|
|
|
|
}
|
2023-11-22 03:51:01 +13:00
|
|
|
|
|
|
|
export const getComponentName = component => {
|
|
|
|
if (component == null) {
|
|
|
|
return ""
|
|
|
|
}
|
|
|
|
|
2024-01-18 06:17:01 +13:00
|
|
|
const components = get(componentStore)?.components || {}
|
2023-11-24 06:07:00 +13:00
|
|
|
const componentDefinition = components[component._component] || {}
|
2024-02-03 03:59:45 +13:00
|
|
|
return componentDefinition.friendlyName || componentDefinition.name || ""
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Recurses through the component tree and builds a tree of contexts provided
|
|
|
|
* by components.
|
|
|
|
*/
|
|
|
|
export const buildContextTree = (
|
|
|
|
rootComponent,
|
|
|
|
tree = { root: [] },
|
|
|
|
currentBranch = "root"
|
|
|
|
) => {
|
|
|
|
// Sanity check
|
|
|
|
if (!rootComponent) {
|
|
|
|
return tree
|
|
|
|
}
|
|
|
|
|
|
|
|
// Process this component's contexts
|
2024-02-08 03:53:16 +13:00
|
|
|
const def = componentStore.getDefinition(rootComponent._component)
|
2024-02-03 03:59:45 +13:00
|
|
|
if (def?.context) {
|
|
|
|
tree[currentBranch].push(rootComponent._id)
|
|
|
|
const contexts = Array.isArray(def.context) ? def.context : [def.context]
|
|
|
|
|
|
|
|
// If we provide local context, start a new branch for our children
|
|
|
|
if (contexts.some(context => context.scope === ContextScopes.Local)) {
|
|
|
|
currentBranch = rootComponent._id
|
|
|
|
tree[rootComponent._id] = []
|
|
|
|
}
|
|
|
|
}
|
2023-11-22 03:51:01 +13:00
|
|
|
|
2024-02-03 03:59:45 +13:00
|
|
|
// Process children
|
|
|
|
if (rootComponent._children) {
|
|
|
|
rootComponent._children.forEach(child => {
|
|
|
|
buildContextTree(child, tree, currentBranch)
|
|
|
|
})
|
|
|
|
}
|
2023-11-22 03:51:01 +13:00
|
|
|
|
2024-02-03 03:59:45 +13:00
|
|
|
return tree
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
2024-02-06 01:21:29 +13:00
|
|
|
* Generates a lookup map of which context branch all components in a component
|
2024-02-03 03:59:45 +13:00
|
|
|
* tree are inside.
|
|
|
|
*/
|
|
|
|
export const buildContextTreeLookupMap = rootComponent => {
|
|
|
|
const tree = buildContextTree(rootComponent)
|
|
|
|
let map = {}
|
|
|
|
Object.entries(tree).forEach(([branch, ids]) => {
|
|
|
|
ids.forEach(id => {
|
|
|
|
map[id] = branch
|
|
|
|
})
|
|
|
|
})
|
|
|
|
return map
|
2023-11-22 03:51:01 +13:00
|
|
|
}
|
2024-03-15 04:21:55 +13:00
|
|
|
|
|
|
|
// Get a flat list of ids for all descendants of a component
|
|
|
|
export const getChildIdsForComponent = component => {
|
|
|
|
return [
|
|
|
|
component._id,
|
2024-03-15 04:51:48 +13:00
|
|
|
...(component?._children ?? []).map(getChildIdsForComponent).flat(1),
|
2024-03-15 04:21:55 +13:00
|
|
|
]
|
|
|
|
}
|