From 2da952af2479e81c10d6f66df96f3cd20cbe1da8 Mon Sep 17 00:00:00 2001 From: Andrew Kingston Date: Wed, 8 Dec 2021 15:31:06 +0000 Subject: [PATCH] Fix bug where state variables were not always extracted, improve performance when determining bindable state values and add initial work on generating button context bindings --- .../src/builderStore/componentUtils.js | 35 +++- .../builder/src/builderStore/dataBinding.js | 175 ++++++++++++------ .../src/builderStore/store/frontend.js | 2 +- .../EventsEditor/EventEditor.svelte | 1 + .../EventsEditor/actions/SaveRow.svelte | 9 +- .../EventsEditor/actions/index.js | 10 +- 6 files changed, 160 insertions(+), 72 deletions(-) diff --git a/packages/builder/src/builderStore/componentUtils.js b/packages/builder/src/builderStore/componentUtils.js index e25949000f..15af369abf 100644 --- a/packages/builder/src/builderStore/componentUtils.js +++ b/packages/builder/src/builderStore/componentUtils.js @@ -127,18 +127,33 @@ const searchComponentTree = (rootComponent, matchComponent) => { } /** - * Searches a component's definition for a setting matching a certin predicate. + * Searches a component's definition for a setting matching a certain predicate. + * These settings are cached because they cannot change at run time. */ +let componentSettingCache = {} export const getComponentSettings = componentType => { - const def = store.actions.components.getDefinition(componentType) - if (!def) { - return [] + // Ensure whole component name is used + if (!componentType.startsWith("@budibase")) { + componentType = `@budibase/standard-components/${componentType}` } - let settings = def.settings?.filter(setting => !setting.section) ?? [] - def.settings - ?.filter(setting => setting.section) - .forEach(section => { - settings = settings.concat(section.settings || []) - }) + + // Check if we have cached this type already + if (componentSettingCache[componentType]) { + return componentSettingCache[componentType] + } + + // Otherwise get the settings and cache them + const def = store.actions.components.getDefinition(componentType) + let settings = [] + if (def) { + settings = def.settings?.filter(setting => !setting.section) ?? [] + def.settings + ?.filter(setting => setting.section) + .forEach(section => { + settings = settings.concat(section.settings || []) + }) + } + componentSettingCache[componentType] = settings + return settings } diff --git a/packages/builder/src/builderStore/dataBinding.js b/packages/builder/src/builderStore/dataBinding.js index 61859df2b7..279e6a034f 100644 --- a/packages/builder/src/builderStore/dataBinding.js +++ b/packages/builder/src/builderStore/dataBinding.js @@ -19,12 +19,16 @@ import { convertJSONSchemaToTableSchema, getJSONArrayDatasourceSchema, } from "./jsonUtils" +import { getAvailableActions } from "components/design/PropertiesPanel/PropertyControls/EventsEditor/actions" // Regex to match all instances of template strings const CAPTURE_VAR_INSIDE_TEMPLATE = /{{([^}]+)}}/g const CAPTURE_VAR_INSIDE_JS = /\$\("([^")]+)"\)/g const CAPTURE_HBS_TEMPLATE = /{{[\S\s]*?}}/g +// List of all available button actions +const AllButtonActions = getAvailableActions(true) + /** * Gets all bindable data context fields and instance fields. */ @@ -373,6 +377,51 @@ const getUrlBindings = asset => { })) } +/** + * Gets all bindable properties exposed in a button actions flow up until + * the specified action ID. + */ +export const getButtonContextBindings = (component, actionId) => { + // Find the setting we are working on + let settingValue = [] + const settings = getComponentSettings(component._component) + const eventSettings = settings.filter(setting => setting.type === "event") + for (let i = 0; i < eventSettings.length; i++) { + const setting = component[eventSettings[i].key] + if ( + Array.isArray(setting) && + setting.find(action => action.id === actionId) + ) { + settingValue = setting + break + } + } + if (!settingValue?.length) { + return [] + } + + // Get the steps leading up to this value + const index = settingValue.findIndex(action => action.id === actionId) + const prevActions = settingValue.slice(0, index) + + // Generate bindings for any steps which provide context + let bindings = [] + prevActions.forEach((action, idx) => { + const def = AllButtonActions.find( + x => x.name === action["##eventHandlerType"] + ) + if (def.context) { + def.context.forEach(contextValue => { + bindings.push({ + readableBinding: `Action ${idx + 1}.${contextValue.label}`, + runtimeBinding: `actions.${idx}.${contextValue.value}`, + }) + }) + } + }) + return bindings +} + /** * Gets a schema for a datasource object. */ @@ -504,15 +553,58 @@ const buildFormSchema = component => { return schema } +/** + * Returns an array of the keys of any state variables which are set anywhere + * in the app. + */ +export const getAllStateVariables = () => { + // Get all component containing assets + let allAssets = [] + allAssets = allAssets.concat(get(store).layouts || []) + allAssets = allAssets.concat(get(store).screens || []) + + // Find all button action settings in all components + let eventSettings = [] + allAssets.forEach(asset => { + findAllMatchingComponents(asset.props, component => { + const settings = getComponentSettings(component._component) + settings + .filter(setting => setting.type === "event") + .forEach(setting => { + eventSettings.push(component[setting.key]) + }) + }) + }) + + // Extract all state keys from any "update state" actions in each setting + let bindingSet = new Set() + eventSettings.forEach(setting => { + if (!Array.isArray(setting)) { + return + } + setting.forEach(action => { + if ( + action["##eventHandlerType"] === "Update State" && + action.parameters?.type === "set" && + action.parameters?.key && + action.parameters?.value + ) { + bindingSet.add(action.parameters.key) + } + }) + }) + return Array.from(bindingSet) +} + /** * Recurses the input object to remove any instances of bindings. */ -export function removeBindings(obj) { +export const removeBindings = (obj, replacement = "Invalid binding") => { for (let [key, value] of Object.entries(obj)) { if (value && typeof value === "object") { - obj[key] = removeBindings(value) + obj[key] = removeBindings(value, replacement) } else if (typeof value === "string") { - obj[key] = value.replace(CAPTURE_HBS_TEMPLATE, "Invalid binding") + obj[key] = value.replace(CAPTURE_HBS_TEMPLATE, replacement) } } return obj @@ -522,8 +614,8 @@ export function removeBindings(obj) { * When converting from readable to runtime it can sometimes add too many square brackets, * this makes sure that doesn't happen. */ -function shouldReplaceBinding(currentValue, from, convertTo) { - if (!currentValue?.includes(from)) { +const shouldReplaceBinding = (currentValue, convertFrom, convertTo) => { + if (!currentValue?.includes(convertFrom)) { return false } if (convertTo === "readableBinding") { @@ -532,7 +624,7 @@ function shouldReplaceBinding(currentValue, from, convertTo) { // remove all the spaces, if the input is surrounded by spaces e.g. [ Auto ID ] then // this makes sure it is detected const noSpaces = currentValue.replace(/\s+/g, "") - const fromNoSpaces = from.replace(/\s+/g, "") + const fromNoSpaces = convertFrom.replace(/\s+/g, "") const invalids = [ `[${fromNoSpaces}]`, `"${fromNoSpaces}"`, @@ -541,14 +633,21 @@ function shouldReplaceBinding(currentValue, from, convertTo) { return !invalids.find(invalid => noSpaces?.includes(invalid)) } -function replaceBetween(string, start, end, replacement) { +/** + * Utility function which replaces a string between given indices. + */ +const replaceBetween = (string, start, end, replacement) => { return string.substring(0, start) + replacement + string.substring(end) } /** - * utility function for the readableToRuntimeBinding and runtimeToReadableBinding. + * Utility function for the readableToRuntimeBinding and runtimeToReadableBinding. */ -function bindingReplacement(bindableProperties, textWithBindings, convertTo) { +const bindingReplacement = ( + bindableProperties, + textWithBindings, + convertTo +) => { // Decide from base64 if using JS const isJS = isJSBinding(textWithBindings) if (isJS) { @@ -613,14 +712,17 @@ function bindingReplacement(bindableProperties, textWithBindings, convertTo) { * Extracts a component ID from a handlebars expression setting of * {{ literal [componentId] }} */ -function extractLiteralHandlebarsID(value) { +const extractLiteralHandlebarsID = value => { return value?.match(/{{\s*literal\s*\[+([^\]]+)].*}}/)?.[1] } /** * Converts a readable data binding into a runtime data binding */ -export function readableToRuntimeBinding(bindableProperties, textWithBindings) { +export const readableToRuntimeBinding = ( + bindableProperties, + textWithBindings +) => { return bindingReplacement( bindableProperties, textWithBindings, @@ -631,56 +733,13 @@ export function readableToRuntimeBinding(bindableProperties, textWithBindings) { /** * Converts a runtime data binding into a readable data binding */ -export function runtimeToReadableBinding(bindableProperties, textWithBindings) { +export const runtimeToReadableBinding = ( + bindableProperties, + textWithBindings +) => { return bindingReplacement( bindableProperties, textWithBindings, "readableBinding" ) } - -/** - * Returns an array of the keys of any state variables which are set anywhere - * in the app. - */ -export const getAllStateVariables = () => { - let allComponents = [] - - // Find all onClick settings in all layouts - get(store).layouts.forEach(layout => { - const components = findAllMatchingComponents( - layout.props, - c => c.onClick != null - ) - allComponents = allComponents.concat(components || []) - }) - - // Find all onClick settings in all screens - get(store).screens.forEach(screen => { - const components = findAllMatchingComponents( - screen.props, - c => c.onClick != null - ) - allComponents = allComponents.concat(components || []) - }) - - // Add state bindings for all state actions - let bindingSet = new Set() - allComponents.forEach(component => { - if (!Array.isArray(component.onClick)) { - return - } - component.onClick.forEach(action => { - if ( - action["##eventHandlerType"] === "Update State" && - action.parameters?.type === "set" && - action.parameters?.key && - action.parameters?.value - ) { - bindingSet.add(action.parameters.key) - } - }) - }) - - return Array.from(bindingSet) -} diff --git a/packages/builder/src/builderStore/store/frontend.js b/packages/builder/src/builderStore/store/frontend.js index e0ec84591c..36d707e1a6 100644 --- a/packages/builder/src/builderStore/store/frontend.js +++ b/packages/builder/src/builderStore/store/frontend.js @@ -537,7 +537,7 @@ export const getFrontendStore = () => { // immediately need to remove bindings, currently these aren't valid when pasted if (!cut && !preserveBindings) { - state.componentToPaste = removeBindings(state.componentToPaste) + state.componentToPaste = removeBindings(state.componentToPaste, "") } // Clone the component to paste diff --git a/packages/builder/src/components/design/PropertiesPanel/PropertyControls/EventsEditor/EventEditor.svelte b/packages/builder/src/components/design/PropertiesPanel/PropertyControls/EventsEditor/EventEditor.svelte index d0c561546f..49d4b2b25d 100644 --- a/packages/builder/src/components/design/PropertiesPanel/PropertyControls/EventsEditor/EventEditor.svelte +++ b/packages/builder/src/components/design/PropertiesPanel/PropertyControls/EventsEditor/EventEditor.svelte @@ -121,6 +121,7 @@
diff --git a/packages/builder/src/components/design/PropertiesPanel/PropertyControls/EventsEditor/actions/SaveRow.svelte b/packages/builder/src/components/design/PropertiesPanel/PropertyControls/EventsEditor/actions/SaveRow.svelte index 55aac87cfd..0d9e0c4278 100644 --- a/packages/builder/src/components/design/PropertiesPanel/PropertyControls/EventsEditor/actions/SaveRow.svelte +++ b/packages/builder/src/components/design/PropertiesPanel/PropertyControls/EventsEditor/actions/SaveRow.svelte @@ -1,13 +1,15 @@