diff --git a/packages/builder/src/builderStore/componentUtils.js b/packages/builder/src/builderStore/componentUtils.js
index 733ce0948e..a7ee3ff351 100644
--- a/packages/builder/src/builderStore/componentUtils.js
+++ b/packages/builder/src/builderStore/componentUtils.js
@@ -92,14 +92,7 @@ export const findAllMatchingComponents = (rootComponent, selector) => {
}
/**
- * 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
+ * Finds the closes 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 85d5046d8c..52368a0723 100644
--- a/packages/builder/src/builderStore/dataBinding.js
+++ b/packages/builder/src/builderStore/dataBinding.js
@@ -1,7 +1,6 @@
import { cloneDeep } from "lodash/fp"
import { get } from "svelte/store"
import {
- findAllComponents,
findAllMatchingComponents,
findComponent,
findComponentPath,
@@ -103,9 +102,6 @@ export const getAuthBindings = () => {
return bindings
}
-/**
- * Gets all bindings for environment variables
- */
export const getEnvironmentBindings = () => {
let envVars = get(environment).variables
return envVars.map(variable => {
@@ -134,22 +130,26 @@ 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 to covert a map of readable bindings to runtime
+ * Utility - coverting 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 to covert a map of runtime bindings to readable bindings
+ * Utility - coverting a map of runtime bindings to readable
*/
export const runtimeToReadableMap = (bindings, ctx) => {
if (!bindings || !ctx) {
@@ -188,23 +188,15 @@ 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
- const componentContext = {
- component,
- definition: def,
- contexts,
- }
- return generateComponentContextBindings(asset, componentContext)
+ return getProviderContextBindings(asset, 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.
+ * Gets all data provider components above a component.
*/
-export const getComponentContexts = (
+export const getContextProviderComponents = (
asset,
componentId,
type,
@@ -213,55 +205,30 @@ export const getComponentContexts = (
if (!asset || !componentId) {
return []
}
- let map = {}
- // Processes all contexts exposed by a component
- const processContexts = scope => component => {
+ // 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 => {
const def = store.actions.components.getDefinition(component._component)
if (!def?.context) {
- return
+ return false
}
- if (!map[component._id]) {
- map[component._id] = {
- component,
- definition: def,
- contexts: [],
- }
+
+ // If no type specified, return anything that exposes context
+ if (!type) {
+ return true
}
+
+ // Otherwise only match components with the specific context type
const contexts = Array.isArray(def.context) ? def.context : [def.context]
- 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)
+ return contexts.find(context => context.type === type) != null
+ })
}
/**
@@ -273,19 +240,20 @@ export const getActionProviders = (
actionType,
options = { includeSelf: false }
) => {
- if (!asset) {
+ if (!asset || !componentId) {
return []
}
- // Get all components
- const components = findAllComponents(asset.props)
+ // Get the component tree leading up to this component, ignoring the component
+ // itself
+ const path = findComponentPath(asset.props, componentId)
+ if (!options?.includeSelf) {
+ path.pop()
+ }
// Find matching contexts and generate bindings
let providers = []
- components.forEach(component => {
- if (!options?.includeSelf && component._id === componentId) {
- return
- }
+ path.forEach(component => {
const def = store.actions.components.getDefinition(component._component)
const actions = (def?.actions || []).map(action => {
return typeof action === "string" ? { type: action } : action
@@ -349,135 +317,142 @@ export const getDatasourceForProvider = (asset, component) => {
* Gets all bindable data properties from component data contexts.
*/
const getContextBindings = (asset, componentId) => {
- // Get all available contexts for this component
- const componentContexts = getComponentContexts(asset, componentId)
+ // Extract any components which provide data contexts
+ const dataProviders = getContextProviderComponents(asset, componentId)
- // Generate bindings for each context
- return componentContexts
- .map(componentContext => {
- return generateComponentContextBindings(asset, componentContext)
- })
- .flat()
+ // Generate bindings for all matching components
+ return getProviderContextBindings(asset, dataProviders)
}
/**
- * Generates a set of bindings for a given component context
+ * Gets the context bindings exposed by a set of data provider components.
*/
-const generateComponentContextBindings = (asset, componentContext) => {
- const { component, definition, contexts } = componentContext
- if (!component || !definition || !contexts?.length) {
+const getProviderContextBindings = (asset, dataProviders) => {
+ if (!asset || !dataProviders) {
return []
}
+ // Ensure providers is an array
+ if (!Array.isArray(dataProviders)) {
+ dataProviders = [dataProviders]
+ }
+
// Create bindings for each data provider
let bindings = []
- contexts.forEach(context => {
- if (!context?.type) {
- return
- }
+ dataProviders.forEach(component => {
+ const def = store.actions.components.getDefinition(component._component)
+ const contexts = Array.isArray(def.context) ? def.context : [def.context]
- 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) {
+ // Create bindings for each context block provided by this data provider
+ contexts.forEach(context => {
+ if (!context?.type) {
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
+ 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
)
- 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}`
- }
- 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,
- },
+ // 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,
+ },
+ })
})
})
})
@@ -485,38 +460,25 @@ const generateComponentContextBindings = (asset, componentContext) => {
return bindings
}
-/**
- * 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
+// Exclude a data context based on the component settings
+const filterCategoryByContext = (component, context) => {
+ const { _component } = component
if (_component.endsWith("formblock")) {
if (
- (actionType === "Create" && type === "schema") ||
- (actionType === "View" && type === "form")
+ (component.actionType === "Create" && context.type === "schema") ||
+ (component.actionType === "View" && context.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`
@@ -534,7 +496,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 = []
@@ -604,7 +566,6 @@ 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 = []
@@ -647,9 +608,6 @@ const getSelectedRowsBindings = asset => {
return bindings
}
-/**
- * Generates a state binding for a certain key name
- */
export const makeStateBinding = key => {
return {
type: "context",
@@ -704,9 +662,6 @@ 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 ce2cac9781..b05b127b1c 100644
--- a/packages/builder/src/builderStore/store/frontend.js
+++ b/packages/builder/src/builderStore/store/frontend.js
@@ -709,9 +709,10 @@ export const getFrontendStore = () => {
else {
if (setting.type === "dataProvider") {
// Validate data provider exists, or else clear it
- const providers = findAllMatchingComponents(
- screen?.props,
- component => component._component?.endsWith("/dataprovider")
+ const treeId = parent?._id || component._id
+ const path = findComponentPath(screen?.props, treeId)
+ const providers = path.filter(component =>
+ component._component?.endsWith("/dataprovider")
)
// Validate non-empty values
const valid = providers?.some(dp => value.includes?.(dp._id))
@@ -733,16 +734,6 @@ 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(),
@@ -752,7 +743,7 @@ export const getFrontendStore = () => {
hover: {},
active: {},
},
- _instanceName: name,
+ _instanceName: `New ${definition.friendlyName || definition.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 bdcd3a7838..aa076fdd3e 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,4 +1,5 @@
-import { getComponentContexts } from "builderStore/dataBinding"
+import { getContextProviderComponents } from "builderStore/dataBinding"
+import { store } from "builderStore"
import { capitalise } from "helpers"
// Generates bindings for all components that provider "datasource like"
@@ -7,49 +8,58 @@ import { capitalise } from "helpers"
// Some examples are saving rows or duplicating rows.
export const getDatasourceLikeProviders = ({ asset, componentId, nested }) => {
// Get all form context providers
- const formComponentContexts = getComponentContexts(
+ const formComponents = getContextProviderComponents(
asset,
componentId,
"form",
- {
- includeSelf: nested,
- }
+ { includeSelf: nested }
)
+
// Get all schema context providers
- const schemaComponentContexts = getComponentContexts(
+ const schemaComponents = getContextProviderComponents(
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
- schemaComponentContexts.forEach(schemaContext => {
+ schemaContexts.forEach(schemaContext => {
// Check if we have a form context for this component
const id = schemaContext.component._id
- const existing = formComponentContexts.find(x => x.component._id === id)
+ const existing = formContexts.find(x => x.component._id === id)
if (existing) {
- if (existing.contexts[0].suffix) {
- const suffix = capitalise(existing.contexts[0].suffix)
+ if (existing.context.suffix) {
+ const suffix = capitalise(existing.context.suffix)
existing.readableSuffix = ` - ${suffix}`
}
- if (schemaContext.contexts[0].suffix) {
- const suffix = capitalise(schemaContext.contexts[0].suffix)
+ if (schemaContext.context.suffix) {
+ const suffix = capitalise(schemaContext.context.suffix)
schemaContext.readableSuffix = ` - ${suffix}`
}
}
})
// Generate bindings for all contexts
- const allContexts = formComponentContexts.concat(schemaComponentContexts)
- return allContexts.map(({ component, contexts, readableSuffix }) => {
+ const allContexts = formContexts.concat(schemaContexts)
+ return allContexts.map(({ component, context, readableSuffix }) => {
let readableBinding = component._instanceName
let runtimeBinding = component._id
- if (contexts[0].suffix) {
- runtimeBinding += `-${contexts[0].suffix}`
+ if (context.suffix) {
+ runtimeBinding += `-${context.suffix}`
}
if (readableSuffix) {
readableBinding += readableSuffix
@@ -60,3 +70,13 @@ 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 9fd220e798..83255ec325 100644
--- a/packages/builder/src/components/design/settings/controls/DataProviderSelect.svelte
+++ b/packages/builder/src/components/design/settings/controls/DataProviderSelect.svelte
@@ -1,16 +1,15 @@