Make all blindings global and improve client component performance

This commit is contained in:
Andrew Kingston 2023-04-28 09:03:09 +01:00
parent a896a75f8f
commit d03f96ceb8
10 changed files with 127 additions and 101 deletions

View File

@ -199,15 +199,7 @@ export const getContextProviderComponents = (
return [] return []
} }
// Get the component tree leading up to this component, ignoring the component return findAllMatchingComponents(asset.props, component => {
// itself
const path = findComponentPath(asset.props, componentId)
if (!options?.includeSelf) {
path.pop()
}
// Filter by only data provider components
return path.filter(component => {
const def = store.actions.components.getDefinition(component._component) const def = store.actions.components.getDefinition(component._component)
if (!def?.context) { if (!def?.context) {
return false return false
@ -222,6 +214,30 @@ export const getContextProviderComponents = (
const contexts = Array.isArray(def.context) ? def.context : [def.context] const contexts = Array.isArray(def.context) ? def.context : [def.context]
return contexts.find(context => context.type === type) != null return contexts.find(context => context.type === type) != null
}) })
//
// // Get the component tree leading up to this component, ignoring the component
// // itself
// const path = findComponentPath(asset.props, componentId)
// if (!options?.includeSelf) {
// path.pop()
// }
//
// // Filter by only data provider components
// return path.filter(component => {
// const def = store.actions.components.getDefinition(component._component)
// if (!def?.context) {
// return false
// }
//
// // If no type specified, return anything that exposes context
// if (!type) {
// return true
// }
//
// // Otherwise only match components with the specific context type
// const contexts = Array.isArray(def.context) ? def.context : [def.context]
// return contexts.find(context => context.type === type) != null
// })
} }
/** /**

View File

@ -621,10 +621,9 @@ export const getFrontendStore = () => {
else { else {
if (setting.type === "dataProvider") { if (setting.type === "dataProvider") {
// Validate data provider exists, or else clear it // Validate data provider exists, or else clear it
const treeId = parent?._id || component._id const providers = findAllMatchingComponents(
const path = findComponentPath(screen?.props, treeId) screen?.props,
const providers = path.filter(component => component => component._component?.endsWith("/dataprovider")
component._component?.endsWith("/dataprovider")
) )
// Validate non-empty values // Validate non-empty values
const valid = providers?.some(dp => value.includes?.(dp._id)) const valid = providers?.some(dp => value.includes?.(dp._id))

View File

@ -1,15 +1,16 @@
<script> <script>
import { Select } from "@budibase/bbui" import { Select } from "@budibase/bbui"
import { makePropSafe } from "@budibase/string-templates" import { makePropSafe } from "@budibase/string-templates"
import { currentAsset, store } from "builderStore" import { currentAsset } from "builderStore"
import { findComponentPath } from "builderStore/componentUtils" import { findAllMatchingComponents } from "builderStore/componentUtils"
export let value export let value
const getValue = component => `{{ literal ${makePropSafe(component._id)} }}` const getValue = component => `{{ literal ${makePropSafe(component._id)} }}`
$: path = findComponentPath($currentAsset?.props, $store.selectedComponentId) $: providers = findAllMatchingComponents($currentAsset?.props, c =>
$: providers = path.filter(c => c._component?.endsWith("/dataprovider")) c._component?.endsWith("/dataprovider")
)
</script> </script>
<Select <Select

View File

@ -394,7 +394,6 @@
"description": "A configurable data list that attaches to your backend tables.", "description": "A configurable data list that attaches to your backend tables.",
"icon": "JourneyData", "icon": "JourneyData",
"illegalChildren": ["section"], "illegalChildren": ["section"],
"requiredAncestors": ["dataprovider"],
"hasChildren": true, "hasChildren": true,
"size": { "size": {
"width": 400, "width": 400,
@ -1385,7 +1384,6 @@
"name": "Bar Chart", "name": "Bar Chart",
"description": "Bar chart", "description": "Bar chart",
"icon": "GraphBarVertical", "icon": "GraphBarVertical",
"requiredAncestors": ["dataprovider"],
"size": { "size": {
"width": 600, "width": 600,
"height": 400 "height": 400
@ -1548,7 +1546,6 @@
"width": 600, "width": 600,
"height": 400 "height": 400
}, },
"requiredAncestors": ["dataprovider"],
"settings": [ "settings": [
{ {
"type": "text", "type": "text",
@ -1702,7 +1699,6 @@
"width": 600, "width": 600,
"height": 400 "height": 400
}, },
"requiredAncestors": ["dataprovider"],
"settings": [ "settings": [
{ {
"type": "text", "type": "text",
@ -1868,7 +1864,6 @@
"width": 600, "width": 600,
"height": 400 "height": 400
}, },
"requiredAncestors": ["dataprovider"],
"settings": [ "settings": [
{ {
"type": "text", "type": "text",
@ -1998,7 +1993,6 @@
"width": 600, "width": 600,
"height": 400 "height": 400
}, },
"requiredAncestors": ["dataprovider"],
"settings": [ "settings": [
{ {
"type": "text", "type": "text",
@ -2128,7 +2122,6 @@
"width": 600, "width": 600,
"height": 400 "height": 400
}, },
"requiredAncestors": ["dataprovider"],
"settings": [ "settings": [
{ {
"type": "text", "type": "text",
@ -3177,7 +3170,6 @@
"width": 400, "width": 400,
"height": 320 "height": 320
}, },
"requiredAncestors": ["dataprovider"],
"settings": [ "settings": [
{ {
"type": "dataProvider", "type": "dataProvider",
@ -3601,7 +3593,6 @@
"name": "Table", "name": "Table",
"icon": "Table", "icon": "Table",
"illegalChildren": ["section"], "illegalChildren": ["section"],
"requiredAncestors": ["dataprovider"],
"hasChildren": true, "hasChildren": true,
"showEmptyState": false, "showEmptyState": false,
"size": { "size": {
@ -3692,7 +3683,6 @@
"name": "Date Range", "name": "Date Range",
"icon": "Calendar", "icon": "Calendar",
"styles": ["size"], "styles": ["size"],
"requiredAncestors": ["dataprovider"],
"hasChildren": false, "hasChildren": false,
"size": { "size": {
"width": 200, "width": 200,
@ -3800,7 +3790,6 @@
"width": 100, "width": 100,
"height": 35 "height": 35
}, },
"requiredAncestors": ["dataprovider"],
"settings": [ "settings": [
{ {
"type": "dataProvider", "type": "dataProvider",

View File

@ -30,6 +30,11 @@
import ScreenPlaceholder from "components/app/ScreenPlaceholder.svelte" import ScreenPlaceholder from "components/app/ScreenPlaceholder.svelte"
import ComponentErrorState from "components/error-states/ComponentErrorState.svelte" import ComponentErrorState from "components/error-states/ComponentErrorState.svelte"
import { BudibasePrefix } from "../stores/components.js" import { BudibasePrefix } from "../stores/components.js"
import {
decodeJSBinding,
findHBSBlocks,
isJSBinding,
} from "@budibase/string-templates"
export let instance = {} export let instance = {}
export let isLayout = false export let isLayout = false
@ -98,6 +103,13 @@
// We clear these whenever a new instance is received. // We clear these whenever a new instance is received.
let ephemeralStyles let ephemeralStyles
// Single string of all HBS blocks, used to check if we use a certain binding
// or not
let bindingString = ""
// List of context keys which we use inside bindings
let knownContextKeyMap = {}
// Set up initial state for each new component instance // Set up initial state for each new component instance
$: initialise(instance) $: initialise(instance)
@ -155,7 +167,7 @@
$: emptyState = empty && showEmptyState $: emptyState = empty && showEmptyState
// Enrich component settings // Enrich component settings
$: enrichComponentSettings($context, settingsDefinitionMap) // $: enrichComponentSettings($context, settingsDefinitionMap)
// Evaluate conditional UI settings and store any component setting changes // Evaluate conditional UI settings and store any component setting changes
// which need to be made // which need to be made
@ -212,7 +224,8 @@
} }
// Ensure we're processing a new instance // Ensure we're processing a new instance
const instanceKey = Helpers.hashString(JSON.stringify(instance)) const stringifiedInstance = JSON.stringify(instance)
const instanceKey = Helpers.hashString(stringifiedInstance)
if (instanceKey === lastInstanceKey && !force) { if (instanceKey === lastInstanceKey && !force) {
return return
} else { } else {
@ -272,10 +285,22 @@
return missing return missing
}) })
// Force an initial enrichment of the new settings // When considering bindings we can ignore children, so we remove that
enrichComponentSettings(get(context), settingsDefinitionMap, { // before storing the reference stringified version
force: true, const noChildren = JSON.stringify({ ...instance, _children: null })
const bindings = findHBSBlocks(noChildren).map(binding => {
let sanitizedBinding = binding.replace(/\\"/g, '"')
if (isJSBinding(sanitizedBinding)) {
return decodeJSBinding(sanitizedBinding)
} else {
return sanitizedBinding
}
}) })
bindingString = bindings.join(" ")
knownContextKeyMap = {}
// Force an initial enrichment of the new settings
enrichComponentSettings($context, settingsDefinitionMap)
} }
const getSettingsDefinitionMap = settingsDefinition => { const getSettingsDefinitionMap = settingsDefinition => {
@ -355,17 +380,7 @@
} }
// Enriches any string component props using handlebars // Enriches any string component props using handlebars
const enrichComponentSettings = ( const enrichComponentSettings = (context, settingsDefinitionMap) => {
context,
settingsDefinitionMap,
options = { force: false }
) => {
const contextChanged = context.key !== lastContextKey
if (!contextChanged && !options?.force) {
return
}
lastContextKey = context.key
// Record the timestamp so we can reference it after enrichment // Record the timestamp so we can reference it after enrichment
latestUpdateTime = Date.now() latestUpdateTime = Date.now()
const enrichmentTime = latestUpdateTime const enrichmentTime = latestUpdateTime
@ -480,6 +495,28 @@
}) })
} }
const handleContextChange = key => {
// Check if we already know if this key is used
let used = knownContextKeyMap[key]
// If we don't know, check
if (used == null) {
// Check HBS
if (bindingString.indexOf(`[${key}]`) !== -1) {
used = true
} else {
used = false
}
// Cache result
knownContextKeyMap[key] = used
}
// Enrich settings if we use this key
if (used) {
enrichComponentSettings($context, settingsDefinitionMap)
}
}
onMount(() => { onMount(() => {
if ( if (
$appStore.isDevApp && $appStore.isDevApp &&
@ -497,6 +534,8 @@
} }
}) })
onMount(() => context.actions.observeChanges(handleContextChange))
onDestroy(() => { onDestroy(() => {
if ( if (
$appStore.isDevApp && $appStore.isDevApp &&

View File

@ -11,8 +11,6 @@
// Clone and create new data context for this component tree // Clone and create new data context for this component tree
const context = getContext("context") const context = getContext("context")
const component = getContext("component") const component = getContext("component")
const newContext = createContextStore(context)
setContext("context", newContext)
const providerKey = key || $component.id const providerKey = key || $component.id
@ -30,7 +28,7 @@
const provideData = newData => { const provideData = newData => {
const dataKey = JSON.stringify(newData) const dataKey = JSON.stringify(newData)
if (dataKey !== lastDataKey) { if (dataKey !== lastDataKey) {
newContext.actions.provideData(providerKey, newData) context.actions.provideData(providerKey, newData)
lastDataKey = dataKey lastDataKey = dataKey
} }
} }
@ -40,7 +38,7 @@
if (actionsKey !== lastActionsKey) { if (actionsKey !== lastActionsKey) {
lastActionsKey = actionsKey lastActionsKey = actionsKey
newActions?.forEach(({ type, callback, metadata }) => { newActions?.forEach(({ type, callback, metadata }) => {
newContext.actions.provideAction(providerKey, type, callback) context.actions.provideAction(providerKey, type, callback)
// Register any "refresh datasource" actions with a singleton store // Register any "refresh datasource" actions with a singleton store
// so we can easily refresh data at all levels for any datasource // so we can easily refresh data at all levels for any datasource

View File

@ -1,44 +1,21 @@
import { writable, derived } from "svelte/store" import { writable } from "svelte/store"
import { Helpers } from "@budibase/bbui"
export const createContextStore = oldContext => { export const createContextStore = () => {
const newContext = writable({}) const context = writable({})
const contexts = oldContext ? [oldContext, newContext] : [newContext] let observers = []
const totalContext = derived(contexts, $contexts => {
// The key is the serialized representation of context
let key = ""
for (let i = 0; i < $contexts.length - 1; i++) {
key += $contexts[i].key
}
key = Helpers.hashString(
key + JSON.stringify($contexts[$contexts.length - 1])
)
// Reduce global state
const reducer = (total, context) => ({ ...total, ...context })
const context = $contexts.reduce(reducer, {})
return {
...context,
key,
}
})
// Adds a data context layer to the tree // Adds a data context layer to the tree
const provideData = (providerId, data) => { const provideData = (providerId, data) => {
if (!providerId || data === undefined) { if (!providerId || data === undefined) {
return return
} }
newContext.update(state => { // console.log(`[${providerId}]`, data)
context.update(state => {
state[providerId] = data state[providerId] = data
// Keep track of the closest component ID so we can later hydrate a "data" prop.
// This is only required for legacy bindings that used "data" rather than a
// component ID.
state.closestComponentId = providerId
return state return state
}) })
broadcastChange(providerId)
} }
// Adds an action context layer to the tree // Adds an action context layer to the tree
@ -46,14 +23,30 @@ export const createContextStore = oldContext => {
if (!providerId || !actionType) { if (!providerId || !actionType) {
return return
} }
newContext.update(state => { context.update(state => {
state[`${providerId}_${actionType}`] = callback state[`${providerId}_${actionType}`] = callback
return state return state
}) })
} }
const observeChanges = callback => {
observers.push(callback)
return () => {
observers = observers.filter(cb => cb !== callback)
}
}
const broadcastChange = key => {
observers.forEach(cb => cb(key))
}
return { return {
subscribe: totalContext.subscribe, subscribe: context.subscribe,
actions: { provideData, provideAction }, actions: {
provideData,
provideAction,
observeChanges,
},
} }
} }

View File

@ -23,16 +23,6 @@ export const propsAreSame = (a, b) => {
* Data bindings are enriched, and button actions are enriched. * Data bindings are enriched, and button actions are enriched.
*/ */
export const enrichProps = (props, context, settingsDefinitionMap) => { export const enrichProps = (props, context, settingsDefinitionMap) => {
// Create context of all bindings and data contexts
// Duplicate the closest context as "data" which the builder requires
const totalContext = {
...context,
// This is only required for legacy bindings that used "data" rather than a
// component ID.
data: context[context.closestComponentId],
}
// We want to exclude any button actions from enrichment at this stage. // We want to exclude any button actions from enrichment at this stage.
// Extract top level button action settings. // Extract top level button action settings.
let normalProps = { ...props } let normalProps = { ...props }
@ -49,13 +39,13 @@ export const enrichProps = (props, context, settingsDefinitionMap) => {
let rawConditions = normalProps._conditions let rawConditions = normalProps._conditions
// Enrich all props except button actions // Enrich all props except button actions
let enrichedProps = enrichDataBindings(normalProps, totalContext) let enrichedProps = enrichDataBindings(normalProps, context)
// Enrich button actions. // Enrich button actions.
// Actions are enriched into a function at this stage, but actual data // Actions are enriched into a function at this stage, but actual data
// binding enrichment is done dynamically at runtime. // binding enrichment is done dynamically at runtime.
Object.keys(actionProps).forEach(prop => { Object.keys(actionProps).forEach(prop => {
enrichedProps[prop] = enrichButtonActions(actionProps[prop], totalContext) enrichedProps[prop] = enrichButtonActions(actionProps[prop], context)
}) })
// Conditions // Conditions
@ -66,7 +56,7 @@ export const enrichProps = (props, context, settingsDefinitionMap) => {
// action // action
condition.settingValue = enrichButtonActions( condition.settingValue = enrichButtonActions(
rawConditions[idx].settingValue, rawConditions[idx].settingValue,
totalContext context
) )
// Since we can't compare functions, we need to assume that conditions // Since we can't compare functions, we need to assume that conditions

View File

@ -24,5 +24,6 @@ export const enrichDataBinding = async (input, context) => {
* Props are deeply cloned so that no mutation is done to the source object. * Props are deeply cloned so that no mutation is done to the source object.
*/ */
export const enrichDataBindings = (props, context) => { export const enrichDataBindings = (props, context) => {
console.log("enrich")
return processObjectSync(Helpers.cloneDeep(props), context, { cache: true }) return processObjectSync(Helpers.cloneDeep(props), context, { cache: true })
} }

View File

@ -1486,15 +1486,15 @@
pouchdb-promise "^6.0.4" pouchdb-promise "^6.0.4"
through2 "^2.0.0" through2 "^2.0.0"
"@budibase/pro@2.5.6-alpha.36": "@budibase/pro@2.5.6-alpha.37":
version "2.5.6-alpha.36" version "2.5.6-alpha.37"
resolved "https://registry.yarnpkg.com/@budibase/pro/-/pro-2.5.6-alpha.36.tgz#361afe64b0881ee436a5ef294fb315c05ea94ce6" resolved "https://registry.yarnpkg.com/@budibase/pro/-/pro-2.5.6-alpha.37.tgz#3f4c7ba36bd01e2f7cbc56461c1249cc4098bc38"
integrity sha512-uX1wgOk47aVGl/yIJZiZS8x31sTS6wGDEFv0AMZ2h6rwIp6GwHDGq2/QT6a8hRMsAM4sqr8R2GkyyAG+dm0DGQ== integrity sha512-D0P4ePioE43yZ+CvLE5XdO84x6/UcF8oY3rHIhd8+bS1LW1yrzAf4kG9lyBRsNUPZoTMPmJeD9zqGRw67pdjzA==
dependencies: dependencies:
"@budibase/backend-core" "2.5.6-alpha.36" "@budibase/backend-core" "2.5.6-alpha.37"
"@budibase/shared-core" "2.4.44-alpha.1" "@budibase/shared-core" "2.4.44-alpha.1"
"@budibase/string-templates" "2.4.44-alpha.1" "@budibase/string-templates" "2.4.44-alpha.1"
"@budibase/types" "2.5.6-alpha.36" "@budibase/types" "2.5.6-alpha.37"
"@koa/router" "8.0.8" "@koa/router" "8.0.8"
bull "4.10.1" bull "4.10.1"
joi "17.6.0" joi "17.6.0"