diff --git a/packages/client/src/components/Component.svelte b/packages/client/src/components/Component.svelte index 8c7d65bd1c..3ac48146b3 100644 --- a/packages/client/src/components/Component.svelte +++ b/packages/client/src/components/Component.svelte @@ -35,6 +35,10 @@ findHBSBlocks, isJSBinding, } from "@budibase/string-templates" + import { + getActionContextKey, + getActionDependentContextKeys, + } from "../utils/buttonActions.js" export let instance = {} export let isLayout = false @@ -165,9 +169,6 @@ hasMissingRequiredSettings) $: emptyState = empty && showEmptyState - // Enrich component settings - // $: enrichComponentSettings($context, settingsDefinitionMap) - // Evaluate conditional UI settings and store any component setting changes // which need to be made $: evaluateConditions(conditions) @@ -296,18 +297,42 @@ } }) + // The known context key map is built up at runtime, as changes to keys are + // encountered. We manually seed this to the required action keys as these + // are not encountered at runtime and so need computed in advance. + knownContextKeyMap = generateActionKeyMap(instance, settingsDefinition) + bindingString = bindings.join(" ") + // Run any migrations runMigrations(instance, settingsDefinition) // Force an initial enrichment of the new settings - enrichComponentSettings(get(context), settingsDefinitionMap, { - force: true, - }) - bindingString = bindings.join(" ") - knownContextKeyMap = {} + enrichComponentSettings(get(context), settingsDefinitionMap) + } - // Force an initial enrichment of the new settings - enrichComponentSettings($context, settingsDefinitionMap) + // Extracts a map of all context keys which are required by action settings + // to provide the functions to evaluate at runtime. This needs done manually + // as the action definitions themselves do not specify bindings for action + // keys, meaning we cannot do this while doing the other normal bindings. + const generateActionKeyMap = (instance, settingsDefinition) => { + let map = {} + settingsDefinition.forEach(setting => { + if (setting.type === "event") { + instance[setting.key]?.forEach(action => { + // We depend on the actual action key + const actionKey = getActionContextKey(action) + if (actionKey) { + map[actionKey] = true + } + + // We also depend on any manually declared context keys + getActionDependentContextKeys(action)?.forEach(key => { + map[key] = true + }) + }) + } + }) + return map } const runMigrations = (instance, settingsDefinition) => { diff --git a/packages/client/src/utils/buttonActions.js b/packages/client/src/utils/buttonActions.js index 18d6b3de3c..cec47dad35 100644 --- a/packages/client/src/utils/buttonActions.js +++ b/packages/client/src/utils/buttonActions.js @@ -17,6 +17,54 @@ import { ActionTypes } from "constants" import { enrichDataBindings } from "./enrichDataBinding" import { Helpers } from "@budibase/bbui" +// Default action handler, which extracts an action from context that was +// provided by another component and executes it with all action parameters +const contextActionHandler = async (action, context) => { + const key = getActionContextKey(action) + const fn = context[key] + if (fn) { + return await fn(action.parameters) + } +} + +// Generates the context key, which is the key that this action depends on in +// context to provide the function it will run. This is broken out as a util +// because we reuse this inside the core Component.svelte file to determine +// what the required action context keys are for all action settings. +export const getActionContextKey = action => { + const type = action?.["##eventHandlerType"] + const key = (componentId, type) => `${componentId}_${type}` + switch (type) { + case "Scroll To Field": + return key(action.parameters.componentId, ActionTypes.ScrollTo) + case "Update Field Value": + return key(action.parameters.componentId, ActionTypes.UpdateFieldValue) + case "Validate Form": + return key(action.parameters.componentId, ActionTypes.ValidateForm) + case "Refresh Data Provider": + return key(action.parameters.componentId, ActionTypes.RefreshDatasource) + case "Clear Form": + return key(action.parameters.componentId, ActionTypes.ClearForm) + case "Change Form Step": + return key(action.parameters.componentId, ActionTypes.ChangeFormStep) + default: + return null + } +} + +// If button actions depend on context, they must declare which keys they need +export const getActionDependentContextKeys = action => { + const type = action?.["##eventHandlerType"] + switch (type) { + case "Save Row": + case "Duplicate Row": + if (action.parameters?.providerId) { + return [action.parameters.providerId] + } + } + return [] +} + const saveRowHandler = async (action, context) => { const { fields, providerId, tableId, notificationOverride } = action.parameters @@ -189,17 +237,6 @@ const navigationHandler = action => { routeStore.actions.navigate(url, peek, externalNewTab) } -const scrollHandler = async (action, context) => { - return await executeActionHandler( - context, - action.parameters.componentId, - ActionTypes.ScrollTo, - { - field: action.parameters.field, - } - ) -} - const queryExecutionHandler = async action => { const { datasourceId, queryId, queryParams, notificationOverride } = action.parameters @@ -235,47 +272,6 @@ const queryExecutionHandler = async action => { } } -const executeActionHandler = async ( - context, - componentId, - actionType, - params -) => { - const fn = context[`${componentId}_${actionType}`] - if (fn) { - return await fn(params) - } -} - -const updateFieldValueHandler = async (action, context) => { - return await executeActionHandler( - context, - action.parameters.componentId, - ActionTypes.UpdateFieldValue, - { - type: action.parameters.type, - field: action.parameters.field, - value: action.parameters.value, - } - ) -} - -const validateFormHandler = async (action, context) => { - return await executeActionHandler( - context, - action.parameters.componentId, - ActionTypes.ValidateForm - ) -} - -const refreshDataProviderHandler = async (action, context) => { - return await executeActionHandler( - context, - action.parameters.componentId, - ActionTypes.RefreshDatasource - ) -} - const logoutHandler = async action => { await authStore.actions.logOut() let redirectUrl = "/builder/auth/login" @@ -292,23 +288,6 @@ const logoutHandler = async action => { } } -const clearFormHandler = async (action, context) => { - return await executeActionHandler( - context, - action.parameters.componentId, - ActionTypes.ClearForm - ) -} - -const changeFormStepHandler = async (action, context) => { - return await executeActionHandler( - context, - action.parameters.componentId, - ActionTypes.ChangeFormStep, - action.parameters - ) -} - const closeScreenModalHandler = action => { let url if (action?.parameters) { @@ -416,16 +395,10 @@ const handlerMap = { ["Duplicate Row"]: duplicateRowHandler, ["Delete Row"]: deleteRowHandler, ["Navigate To"]: navigationHandler, - ["Scroll To Field"]: scrollHandler, ["Execute Query"]: queryExecutionHandler, ["Trigger Automation"]: triggerAutomationHandler, - ["Validate Form"]: validateFormHandler, - ["Update Field Value"]: updateFieldValueHandler, - ["Refresh Data Provider"]: refreshDataProviderHandler, ["Log Out"]: logoutHandler, - ["Clear Form"]: clearFormHandler, ["Close Screen Modal"]: closeScreenModalHandler, - ["Change Form Step"]: changeFormStepHandler, ["Update State"]: updateStateHandler, ["Upload File to S3"]: s3UploadHandler, ["Export Data"]: exportDataHandler, @@ -460,7 +433,12 @@ export const enrichButtonActions = (actions, context) => { return actions } - const handlers = actions.map(def => handlerMap[def["##eventHandlerType"]]) + // Get handlers for each action. If no bespoke handler is configured, fall + // back to simply executing this action from context. + const handlers = actions.map(def => { + return handlerMap[def["##eventHandlerType"]] || contextActionHandler + }) + return async eventContext => { // Button context is built up as actions are executed. // Inherit any previous button context which may have come from actions