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 => {
|
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
|
||||||
}
|
}
|
||||||
|
|
|
@ -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)
|
|
||||||
}
|
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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}
|
||||||
/>
|
/>
|
||||||
|
|
|
@ -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) => {
|
||||||
|
|
|
@ -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,
|
||||||
|
|
Loading…
Reference in New Issue