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:
parent
7beaa80759
commit
2da952af24
|
@ -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
|
||||
}
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -121,6 +121,7 @@
|
|||
<div class="selected-action-container">
|
||||
<svelte:component
|
||||
this={selectedActionComponent}
|
||||
selectedActionId={selectedAction.id}
|
||||
parameters={selectedAction.parameters}
|
||||
{bindings}
|
||||
/>
|
||||
|
|
|
@ -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) => {
|
||||
|
|
|
@ -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,
|
||||
|
|
Loading…
Reference in New Issue