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

This commit is contained in:
Andrew Kingston 2021-12-08 15:31:06 +00:00
parent 7beaa80759
commit 2da952af24
6 changed files with 160 additions and 72 deletions

View File

@ -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 => { export const getComponentSettings = componentType => {
const def = store.actions.components.getDefinition(componentType) // Ensure whole component name is used
if (!def) { if (!componentType.startsWith("@budibase")) {
return [] componentType = `@budibase/standard-components/${componentType}`
} }
let settings = def.settings?.filter(setting => !setting.section) ?? []
def.settings // Check if we have cached this type already
?.filter(setting => setting.section) if (componentSettingCache[componentType]) {
.forEach(section => { return componentSettingCache[componentType]
settings = settings.concat(section.settings || []) }
})
// 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 return settings
} }

View File

@ -19,12 +19,16 @@ import {
convertJSONSchemaToTableSchema, convertJSONSchemaToTableSchema,
getJSONArrayDatasourceSchema, getJSONArrayDatasourceSchema,
} from "./jsonUtils" } from "./jsonUtils"
import { getAvailableActions } from "components/design/PropertiesPanel/PropertyControls/EventsEditor/actions"
// Regex to match all instances of template strings // Regex to match all instances of template strings
const CAPTURE_VAR_INSIDE_TEMPLATE = /{{([^}]+)}}/g const CAPTURE_VAR_INSIDE_TEMPLATE = /{{([^}]+)}}/g
const CAPTURE_VAR_INSIDE_JS = /\$\("([^")]+)"\)/g const CAPTURE_VAR_INSIDE_JS = /\$\("([^")]+)"\)/g
const CAPTURE_HBS_TEMPLATE = /{{[\S\s]*?}}/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. * 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. * Gets a schema for a datasource object.
*/ */
@ -504,15 +553,58 @@ const buildFormSchema = component => {
return schema 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. * 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)) { for (let [key, value] of Object.entries(obj)) {
if (value && typeof value === "object") { if (value && typeof value === "object") {
obj[key] = removeBindings(value) obj[key] = removeBindings(value, replacement)
} else if (typeof value === "string") { } else if (typeof value === "string") {
obj[key] = value.replace(CAPTURE_HBS_TEMPLATE, "Invalid binding") obj[key] = value.replace(CAPTURE_HBS_TEMPLATE, replacement)
} }
} }
return obj return obj
@ -522,8 +614,8 @@ export function removeBindings(obj) {
* When converting from readable to runtime it can sometimes add too many square brackets, * When converting from readable to runtime it can sometimes add too many square brackets,
* this makes sure that doesn't happen. * this makes sure that doesn't happen.
*/ */
function shouldReplaceBinding(currentValue, from, convertTo) { const shouldReplaceBinding = (currentValue, convertFrom, convertTo) => {
if (!currentValue?.includes(from)) { if (!currentValue?.includes(convertFrom)) {
return false return false
} }
if (convertTo === "readableBinding") { 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 // remove all the spaces, if the input is surrounded by spaces e.g. [ Auto ID ] then
// this makes sure it is detected // this makes sure it is detected
const noSpaces = currentValue.replace(/\s+/g, "") const noSpaces = currentValue.replace(/\s+/g, "")
const fromNoSpaces = from.replace(/\s+/g, "") const fromNoSpaces = convertFrom.replace(/\s+/g, "")
const invalids = [ const invalids = [
`[${fromNoSpaces}]`, `[${fromNoSpaces}]`,
`"${fromNoSpaces}"`, `"${fromNoSpaces}"`,
@ -541,14 +633,21 @@ function shouldReplaceBinding(currentValue, from, convertTo) {
return !invalids.find(invalid => noSpaces?.includes(invalid)) 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) 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 // Decide from base64 if using JS
const isJS = isJSBinding(textWithBindings) const isJS = isJSBinding(textWithBindings)
if (isJS) { if (isJS) {
@ -613,14 +712,17 @@ function bindingReplacement(bindableProperties, textWithBindings, convertTo) {
* Extracts a component ID from a handlebars expression setting of * Extracts a component ID from a handlebars expression setting of
* {{ literal [componentId] }} * {{ literal [componentId] }}
*/ */
function extractLiteralHandlebarsID(value) { const extractLiteralHandlebarsID = value => {
return value?.match(/{{\s*literal\s*\[+([^\]]+)].*}}/)?.[1] return value?.match(/{{\s*literal\s*\[+([^\]]+)].*}}/)?.[1]
} }
/** /**
* Converts a readable data binding into a runtime data binding * Converts a readable data binding into a runtime data binding
*/ */
export function readableToRuntimeBinding(bindableProperties, textWithBindings) { export const readableToRuntimeBinding = (
bindableProperties,
textWithBindings
) => {
return bindingReplacement( return bindingReplacement(
bindableProperties, bindableProperties,
textWithBindings, textWithBindings,
@ -631,56 +733,13 @@ export function readableToRuntimeBinding(bindableProperties, textWithBindings) {
/** /**
* Converts a runtime data binding into a readable data binding * Converts a runtime data binding into a readable data binding
*/ */
export function runtimeToReadableBinding(bindableProperties, textWithBindings) { export const runtimeToReadableBinding = (
bindableProperties,
textWithBindings
) => {
return bindingReplacement( return bindingReplacement(
bindableProperties, bindableProperties,
textWithBindings, textWithBindings,
"readableBinding" "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)
}

View File

@ -537,7 +537,7 @@ export const getFrontendStore = () => {
// immediately need to remove bindings, currently these aren't valid when pasted // immediately need to remove bindings, currently these aren't valid when pasted
if (!cut && !preserveBindings) { if (!cut && !preserveBindings) {
state.componentToPaste = removeBindings(state.componentToPaste) state.componentToPaste = removeBindings(state.componentToPaste, "")
} }
// Clone the component to paste // Clone the component to paste

View File

@ -121,6 +121,7 @@
<div class="selected-action-container"> <div class="selected-action-container">
<svelte:component <svelte:component
this={selectedActionComponent} this={selectedActionComponent}
selectedActionId={selectedAction.id}
parameters={selectedAction.parameters} parameters={selectedAction.parameters}
{bindings} {bindings}
/> />

View File

@ -1,13 +1,15 @@
<script> <script>
import { Select, Label, Body, Checkbox, Input } from "@budibase/bbui" import { Select, Label, Body, Checkbox, Input } from "@budibase/bbui"
import { store, currentAsset } from "builderStore" import { store, currentAsset, selectedComponent } from "builderStore"
import { tables } from "stores/backend" import { tables } from "stores/backend"
import { import {
getButtonContextBindings,
getContextProviderComponents, getContextProviderComponents,
getSchemaForDatasource, getSchemaForDatasource,
} from "builderStore/dataBinding" } from "builderStore/dataBinding"
import SaveFields from "./SaveFields.svelte" import SaveFields from "./SaveFields.svelte"
export let selectedActionId
export let parameters export let parameters
export let bindings = [] export let bindings = []
@ -24,6 +26,11 @@
$: providerOptions = getProviderOptions(formComponents, schemaComponents) $: providerOptions = getProviderOptions(formComponents, schemaComponents)
$: schemaFields = getSchemaFields($currentAsset, parameters?.tableId) $: schemaFields = getSchemaFields($currentAsset, parameters?.tableId)
$: tableOptions = $tables.list || [] $: tableOptions = $tables.list || []
$: buttonContextBindings = getButtonContextBindings(
$selectedComponent,
selectedActionId
)
$: console.log(buttonContextBindings)
// Gets a context definition of a certain type from a component definition // Gets a context definition of a certain type from a component definition
const extractComponentContext = (component, contextType) => { const extractComponentContext = (component, contextType) => {

View File

@ -22,11 +22,17 @@ import DuplicateRow from "./DuplicateRow.svelte"
// be considered as camel case too. // be considered as camel case too.
// There is technical debt here to sanitize all these and standardise them // There is technical debt here to sanitize all these and standardise them
// across the packages but it's a breaking change to existing apps. // across the packages but it's a breaking change to existing apps.
export const getAvailableActions = () => { export const getAvailableActions = (getAllActions = false) => {
let actions = [ let actions = [
{ {
name: "Save Row", name: "Save Row",
component: SaveRow, component: SaveRow,
context: [
{
label: "Saved row",
value: "row",
},
],
}, },
{ {
name: "Duplicate Row", name: "Duplicate Row",
@ -74,7 +80,7 @@ export const getAvailableActions = () => {
}, },
] ]
if (get(store).clientFeatures?.state) { if (getAllActions || get(store).clientFeatures?.state) {
actions.push({ actions.push({
name: "Update State", name: "Update State",
component: UpdateStateStep, component: UpdateStateStep,