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 => {
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) ?? []
// 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
}

View File

@ -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)
}

View File

@ -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

View File

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

View File

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

View File

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