diff --git a/packages/builder/src/builderStore/componentUtils.js b/packages/builder/src/builderStore/componentUtils.js
index a7ee3ff351..733ce0948e 100644
--- a/packages/builder/src/builderStore/componentUtils.js
+++ b/packages/builder/src/builderStore/componentUtils.js
@@ -92,7 +92,14 @@ export const findAllMatchingComponents = (rootComponent, selector) => {
}
/**
- * Finds the closes parent component which matches certain criteria
+ * Recurses through the component tree and finds all components.
+ */
+export const findAllComponents = rootComponent => {
+ return findAllMatchingComponents(rootComponent, () => true)
+}
+
+/**
+ * Finds the closest parent component which matches certain criteria
*/
export const findClosestMatchingComponent = (
rootComponent,
diff --git a/packages/builder/src/builderStore/dataBinding.js b/packages/builder/src/builderStore/dataBinding.js
index 52368a0723..85d5046d8c 100644
--- a/packages/builder/src/builderStore/dataBinding.js
+++ b/packages/builder/src/builderStore/dataBinding.js
@@ -1,6 +1,7 @@
import { cloneDeep } from "lodash/fp"
import { get } from "svelte/store"
import {
+ findAllComponents,
findAllMatchingComponents,
findComponent,
findComponentPath,
@@ -102,6 +103,9 @@ export const getAuthBindings = () => {
return bindings
}
+/**
+ * Gets all bindings for environment variables
+ */
export const getEnvironmentBindings = () => {
let envVars = get(environment).variables
return envVars.map(variable => {
@@ -130,26 +134,22 @@ export const toBindingsArray = (valueMap, prefix, category) => {
if (!binding) {
return acc
}
-
let config = {
type: "context",
runtimeBinding: binding,
readableBinding: `${prefix}.${binding}`,
icon: "Brackets",
}
-
if (category) {
config.category = category
}
-
acc.push(config)
-
return acc
}, [])
}
/**
- * Utility - coverting a map of readable bindings to runtime
+ * Utility to covert a map of readable bindings to runtime
*/
export const readableToRuntimeMap = (bindings, ctx) => {
if (!bindings || !ctx) {
@@ -162,7 +162,7 @@ export const readableToRuntimeMap = (bindings, ctx) => {
}
/**
- * Utility - coverting a map of runtime bindings to readable
+ * Utility to covert a map of runtime bindings to readable bindings
*/
export const runtimeToReadableMap = (bindings, ctx) => {
if (!bindings || !ctx) {
@@ -188,15 +188,23 @@ export const getComponentBindableProperties = (asset, componentId) => {
if (!def?.context) {
return []
}
+ const contexts = Array.isArray(def.context) ? def.context : [def.context]
// Get the bindings for the component
- return getProviderContextBindings(asset, component)
+ const componentContext = {
+ component,
+ definition: def,
+ contexts,
+ }
+ return generateComponentContextBindings(asset, componentContext)
}
/**
- * Gets all data provider components above a component.
+ * Gets all component contexts available to a certain component. This handles
+ * both global and local bindings, taking into account a component's position
+ * in the component tree.
*/
-export const getContextProviderComponents = (
+export const getComponentContexts = (
asset,
componentId,
type,
@@ -205,30 +213,55 @@ export const getContextProviderComponents = (
if (!asset || !componentId) {
return []
}
+ let map = {}
- // Get the component tree leading up to this component, ignoring the component
- // itself
- const path = findComponentPath(asset.props, componentId)
- if (!options?.includeSelf) {
- path.pop()
- }
-
- // Filter by only data provider components
- return path.filter(component => {
+ // Processes all contexts exposed by a component
+ const processContexts = scope => component => {
const def = store.actions.components.getDefinition(component._component)
if (!def?.context) {
- return false
+ return
}
-
- // If no type specified, return anything that exposes context
- if (!type) {
- return true
+ if (!map[component._id]) {
+ map[component._id] = {
+ component,
+ definition: def,
+ contexts: [],
+ }
}
-
- // Otherwise only match components with the specific context type
const contexts = Array.isArray(def.context) ? def.context : [def.context]
- return contexts.find(context => context.type === type) != null
- })
+ contexts.forEach(context => {
+ // Ensure type matches
+ if (type && context.type !== type) {
+ return
+ }
+ // Ensure scope matches
+ let contextScope = context.scope || "global"
+ if (contextScope !== scope) {
+ return
+ }
+ // Ensure the context is compatible with the component's current settings
+ if (!isContextCompatibleWithComponent(context, component)) {
+ return
+ }
+ map[component._id].contexts.push(context)
+ })
+ }
+
+ // Process all global contexts
+ const allComponents = findAllComponents(asset.props)
+ allComponents.forEach(processContexts("global"))
+
+ // Process all local contexts
+ const localComponents = findComponentPath(asset.props, componentId)
+ localComponents.forEach(processContexts("local"))
+
+ // Exclude self if required
+ if (!options?.includeSelf) {
+ delete map[componentId]
+ }
+
+ // Only return components which provide at least 1 matching context
+ return Object.values(map).filter(x => x.contexts.length > 0)
}
/**
@@ -240,20 +273,19 @@ export const getActionProviders = (
actionType,
options = { includeSelf: false }
) => {
- if (!asset || !componentId) {
+ if (!asset) {
return []
}
- // Get the component tree leading up to this component, ignoring the component
- // itself
- const path = findComponentPath(asset.props, componentId)
- if (!options?.includeSelf) {
- path.pop()
- }
+ // Get all components
+ const components = findAllComponents(asset.props)
// Find matching contexts and generate bindings
let providers = []
- path.forEach(component => {
+ components.forEach(component => {
+ if (!options?.includeSelf && component._id === componentId) {
+ return
+ }
const def = store.actions.components.getDefinition(component._component)
const actions = (def?.actions || []).map(action => {
return typeof action === "string" ? { type: action } : action
@@ -317,142 +349,135 @@ export const getDatasourceForProvider = (asset, component) => {
* Gets all bindable data properties from component data contexts.
*/
const getContextBindings = (asset, componentId) => {
- // Extract any components which provide data contexts
- const dataProviders = getContextProviderComponents(asset, componentId)
+ // Get all available contexts for this component
+ const componentContexts = getComponentContexts(asset, componentId)
- // Generate bindings for all matching components
- return getProviderContextBindings(asset, dataProviders)
+ // Generate bindings for each context
+ return componentContexts
+ .map(componentContext => {
+ return generateComponentContextBindings(asset, componentContext)
+ })
+ .flat()
}
/**
- * Gets the context bindings exposed by a set of data provider components.
+ * Generates a set of bindings for a given component context
*/
-const getProviderContextBindings = (asset, dataProviders) => {
- if (!asset || !dataProviders) {
+const generateComponentContextBindings = (asset, componentContext) => {
+ const { component, definition, contexts } = componentContext
+ if (!component || !definition || !contexts?.length) {
return []
}
- // Ensure providers is an array
- if (!Array.isArray(dataProviders)) {
- dataProviders = [dataProviders]
- }
-
// Create bindings for each data provider
let bindings = []
- dataProviders.forEach(component => {
- const def = store.actions.components.getDefinition(component._component)
- const contexts = Array.isArray(def.context) ? def.context : [def.context]
+ contexts.forEach(context => {
+ if (!context?.type) {
+ return
+ }
- // Create bindings for each context block provided by this data provider
- contexts.forEach(context => {
- if (!context?.type) {
+ let schema
+ let table
+ let readablePrefix
+ let runtimeSuffix = context.suffix
+
+ if (context.type === "form") {
+ // Forms do not need table schemas
+ // Their schemas are built from their component field names
+ schema = buildFormSchema(component, asset)
+ readablePrefix = "Fields"
+ } else if (context.type === "static") {
+ // Static contexts are fully defined by the components
+ schema = {}
+ const values = context.values || []
+ values.forEach(value => {
+ schema[value.key] = {
+ name: value.label,
+ type: value.type || "string",
+ }
+ })
+ } else if (context.type === "schema") {
+ // Schema contexts are generated dynamically depending on their data
+ const datasource = getDatasourceForProvider(asset, component)
+ if (!datasource) {
return
}
+ const info = getSchemaForDatasource(asset, datasource)
+ schema = info.schema
+ table = info.table
- let schema
- let table
- let readablePrefix
- let runtimeSuffix = context.suffix
-
- if (context.type === "form") {
- // Forms do not need table schemas
- // Their schemas are built from their component field names
- schema = buildFormSchema(component, asset)
- readablePrefix = "Fields"
- } else if (context.type === "static") {
- // Static contexts are fully defined by the components
- schema = {}
- const values = context.values || []
- values.forEach(value => {
- schema[value.key] = {
- name: value.label,
- type: value.type || "string",
- }
- })
- } else if (context.type === "schema") {
- // Schema contexts are generated dynamically depending on their data
- const datasource = getDatasourceForProvider(asset, component)
- if (!datasource) {
- return
- }
- const info = getSchemaForDatasource(asset, datasource)
- schema = info.schema
- table = info.table
-
- // Determine what to prefix bindings with
- if (datasource.type === "jsonarray") {
- // For JSON arrays, use the array name as the readable prefix
- const split = datasource.label.split(".")
- readablePrefix = split[split.length - 1]
- } else if (datasource.type === "viewV2") {
- // For views, use the view name
- const view = Object.values(table?.views || {}).find(
- view => view.id === datasource.id
- )
- readablePrefix = view?.name
- } else {
- // Otherwise use the table name
- readablePrefix = info.table?.name
- }
- }
- if (!schema) {
- return
- }
-
- const keys = Object.keys(schema).sort()
-
- // Generate safe unique runtime prefix
- let providerId = component._id
- if (runtimeSuffix) {
- providerId += `-${runtimeSuffix}`
- }
-
- if (!filterCategoryByContext(component, context)) {
- return
- }
-
- const safeComponentId = makePropSafe(providerId)
-
- // Create bindable properties for each schema field
- keys.forEach(key => {
- const fieldSchema = schema[key]
-
- // Make safe runtime binding
- const safeKey = key.split(".").map(makePropSafe).join(".")
- const runtimeBinding = `${safeComponentId}.${safeKey}`
-
- // Optionally use a prefix with readable bindings
- let readableBinding = component._instanceName
- if (readablePrefix) {
- readableBinding += `.${readablePrefix}`
- }
- readableBinding += `.${fieldSchema.name || key}`
-
- const bindingCategory = getComponentBindingCategory(
- component,
- context,
- def
+ // Determine what to prefix bindings with
+ if (datasource.type === "jsonarray") {
+ // For JSON arrays, use the array name as the readable prefix
+ const split = datasource.label.split(".")
+ readablePrefix = split[split.length - 1]
+ } else if (datasource.type === "viewV2") {
+ // For views, use the view name
+ const view = Object.values(table?.views || {}).find(
+ view => view.id === datasource.id
)
+ readablePrefix = view?.name
+ } else {
+ // Otherwise use the table name
+ readablePrefix = info.table?.name
+ }
+ }
+ if (!schema) {
+ return
+ }
- // Create the binding object
- bindings.push({
- type: "context",
- runtimeBinding,
- readableBinding,
- // Field schema and provider are required to construct relationship
- // datasource options, based on bindable properties
- fieldSchema,
- providerId,
- // Table ID is used by JSON fields to know what table the field is in
- tableId: table?._id,
- component: component._component,
- category: bindingCategory.category,
- icon: bindingCategory.icon,
- display: {
- name: fieldSchema.name || key,
- type: fieldSchema.type,
- },
- })
+ const keys = Object.keys(schema).sort()
+
+ // Generate safe unique runtime prefix
+ let providerId = component._id
+ if (runtimeSuffix) {
+ providerId += `-${runtimeSuffix}`
+ }
+ const safeComponentId = makePropSafe(providerId)
+
+ // Create bindable properties for each schema field
+ keys.forEach(key => {
+ const fieldSchema = schema[key]
+
+ // Make safe runtime binding
+ const safeKey = key.split(".").map(makePropSafe).join(".")
+ const runtimeBinding = `${safeComponentId}.${safeKey}`
+
+ // Optionally use a prefix with readable bindings
+ let readableBinding = component._instanceName
+ if (readablePrefix) {
+ readableBinding += `.${readablePrefix}`
+ }
+ readableBinding += `.${fieldSchema.name || key}`
+
+ // Determine which category this binding belongs in
+ const bindingCategory = getComponentBindingCategory(
+ component,
+ context,
+ definition
+ )
+
+ // Temporarily append scope for debugging
+ const scope = `[${(context.scope || "global").toUpperCase()}]`
+
+ // Create the binding object
+ bindings.push({
+ type: "context",
+ runtimeBinding,
+ readableBinding: `${scope} ${readableBinding}`,
+ // Field schema and provider are required to construct relationship
+ // datasource options, based on bindable properties
+ fieldSchema,
+ providerId,
+ // Table ID is used by JSON fields to know what table the field is in
+ tableId: table?._id,
+ component: component._component,
+ category: bindingCategory.category,
+ icon: bindingCategory.icon,
+ display: {
+ name: `${scope} ${fieldSchema.name || key}`,
+ type: fieldSchema.type,
+ },
})
})
})
@@ -460,25 +485,38 @@ const getProviderContextBindings = (asset, dataProviders) => {
return bindings
}
-// Exclude a data context based on the component settings
-const filterCategoryByContext = (component, context) => {
- const { _component } = component
+/**
+ * Checks if a certain data context is compatible with a certain instance of a
+ * configured component.
+ */
+const isContextCompatibleWithComponent = (context, component) => {
+ if (!component) {
+ return false
+ }
+ const { _component, actionType } = component
+ const { type } = context
+
+ // Certain types of form blocks only allow certain contexts
if (_component.endsWith("formblock")) {
if (
- (component.actionType === "Create" && context.type === "schema") ||
- (component.actionType === "View" && context.type === "form")
+ (actionType === "Create" && type === "schema") ||
+ (actionType === "View" && type === "form")
) {
return false
}
}
+
+ // Allow the context by default
return true
}
// Enrich binding category information for certain components
const getComponentBindingCategory = (component, context, def) => {
+ // Default category to component name
let icon = def.icon
let category = component._instanceName
+ // Form block edge case
if (component._component.endsWith("formblock")) {
if (context.type === "form") {
category = `${component._instanceName} - Fields`
@@ -496,7 +534,7 @@ const getComponentBindingCategory = (component, context, def) => {
}
/**
- * Gets all bindable properties from the logged in user.
+ * Gets all bindable properties from the logged-in user.
*/
export const getUserBindings = () => {
let bindings = []
@@ -566,6 +604,7 @@ const getDeviceBindings = () => {
/**
* Gets all selected rows bindings for tables in the current asset.
+ * TODO: remove in future because we don't need a separate store for this
*/
const getSelectedRowsBindings = asset => {
let bindings = []
@@ -608,6 +647,9 @@ const getSelectedRowsBindings = asset => {
return bindings
}
+/**
+ * Generates a state binding for a certain key name
+ */
export const makeStateBinding = key => {
return {
type: "context",
@@ -662,6 +704,9 @@ const getUrlBindings = asset => {
return urlParamBindings.concat([queryParamsBinding])
}
+/**
+ * Generates all bindings for role IDs
+ */
const getRoleBindings = () => {
return (get(rolesStore) || []).map(role => {
return {
diff --git a/packages/builder/src/builderStore/store/frontend.js b/packages/builder/src/builderStore/store/frontend.js
index b05b127b1c..ce2cac9781 100644
--- a/packages/builder/src/builderStore/store/frontend.js
+++ b/packages/builder/src/builderStore/store/frontend.js
@@ -709,10 +709,9 @@ export const getFrontendStore = () => {
else {
if (setting.type === "dataProvider") {
// Validate data provider exists, or else clear it
- const treeId = parent?._id || component._id
- const path = findComponentPath(screen?.props, treeId)
- const providers = path.filter(component =>
- component._component?.endsWith("/dataprovider")
+ const providers = findAllMatchingComponents(
+ screen?.props,
+ component => component._component?.endsWith("/dataprovider")
)
// Validate non-empty values
const valid = providers?.some(dp => value.includes?.(dp._id))
@@ -734,6 +733,16 @@ export const getFrontendStore = () => {
return null
}
+ // Find all existing components of this type so that we can give this
+ // component a unique name
+ const screen = get(selectedScreen).props
+ const otherComponents = findAllMatchingComponents(
+ screen,
+ x => x._component === definition.component && x._id !== screen._id
+ )
+ let name = definition.friendlyName || definition.name
+ name = `${name} ${otherComponents.length + 1}`
+
// Generate basic component structure
let instance = {
_id: Helpers.uuid(),
@@ -743,7 +752,7 @@ export const getFrontendStore = () => {
hover: {},
active: {},
},
- _instanceName: `New ${definition.friendlyName || definition.name}`,
+ _instanceName: name,
...presetProps,
}
diff --git a/packages/builder/src/components/design/settings/controls/ButtonActionEditor/actions/utils.js b/packages/builder/src/components/design/settings/controls/ButtonActionEditor/actions/utils.js
index aa076fdd3e..bdcd3a7838 100644
--- a/packages/builder/src/components/design/settings/controls/ButtonActionEditor/actions/utils.js
+++ b/packages/builder/src/components/design/settings/controls/ButtonActionEditor/actions/utils.js
@@ -1,5 +1,4 @@
-import { getContextProviderComponents } from "builderStore/dataBinding"
-import { store } from "builderStore"
+import { getComponentContexts } from "builderStore/dataBinding"
import { capitalise } from "helpers"
// Generates bindings for all components that provider "datasource like"
@@ -8,58 +7,49 @@ import { capitalise } from "helpers"
// Some examples are saving rows or duplicating rows.
export const getDatasourceLikeProviders = ({ asset, componentId, nested }) => {
// Get all form context providers
- const formComponents = getContextProviderComponents(
+ const formComponentContexts = getComponentContexts(
asset,
componentId,
"form",
- { includeSelf: nested }
+ {
+ includeSelf: nested,
+ }
)
-
// Get all schema context providers
- const schemaComponents = getContextProviderComponents(
+ const schemaComponentContexts = getComponentContexts(
asset,
componentId,
"schema",
- { includeSelf: nested }
+ {
+ includeSelf: nested,
+ }
)
- // Generate contexts for all form providers
- const formContexts = formComponents.map(component => ({
- component,
- context: extractComponentContext(component, "form"),
- }))
-
- // Generate contexts for all schema providers
- const schemaContexts = schemaComponents.map(component => ({
- component,
- context: extractComponentContext(component, "schema"),
- }))
-
// Check for duplicate contexts by the same component. In this case, attempt
// to label contexts with their suffixes
- schemaContexts.forEach(schemaContext => {
+ schemaComponentContexts.forEach(schemaContext => {
// Check if we have a form context for this component
const id = schemaContext.component._id
- const existing = formContexts.find(x => x.component._id === id)
+ const existing = formComponentContexts.find(x => x.component._id === id)
if (existing) {
- if (existing.context.suffix) {
- const suffix = capitalise(existing.context.suffix)
+ if (existing.contexts[0].suffix) {
+ const suffix = capitalise(existing.contexts[0].suffix)
existing.readableSuffix = ` - ${suffix}`
}
- if (schemaContext.context.suffix) {
- const suffix = capitalise(schemaContext.context.suffix)
+ if (schemaContext.contexts[0].suffix) {
+ const suffix = capitalise(schemaContext.contexts[0].suffix)
schemaContext.readableSuffix = ` - ${suffix}`
}
}
})
// Generate bindings for all contexts
- const allContexts = formContexts.concat(schemaContexts)
- return allContexts.map(({ component, context, readableSuffix }) => {
+ const allContexts = formComponentContexts.concat(schemaComponentContexts)
+ return allContexts.map(({ component, contexts, readableSuffix }) => {
let readableBinding = component._instanceName
let runtimeBinding = component._id
- if (context.suffix) {
- runtimeBinding += `-${context.suffix}`
+ if (contexts[0].suffix) {
+ runtimeBinding += `-${contexts[0].suffix}`
}
if (readableSuffix) {
readableBinding += readableSuffix
@@ -70,13 +60,3 @@ export const getDatasourceLikeProviders = ({ asset, componentId, nested }) => {
}
})
}
-
-// Gets a context definition of a certain type from a component definition
-const extractComponentContext = (component, contextType) => {
- const def = store.actions.components.getDefinition(component?._component)
- if (!def) {
- return null
- }
- const contexts = Array.isArray(def.context) ? def.context : [def.context]
- return contexts.find(context => context?.type === contextType)
-}
diff --git a/packages/builder/src/components/design/settings/controls/DataProviderSelect.svelte b/packages/builder/src/components/design/settings/controls/DataProviderSelect.svelte
index 83255ec325..9fd220e798 100644
--- a/packages/builder/src/components/design/settings/controls/DataProviderSelect.svelte
+++ b/packages/builder/src/components/design/settings/controls/DataProviderSelect.svelte
@@ -1,15 +1,16 @@