Update data binding generation to support both local and global binding scopes
This commit is contained in:
parent
1995a2860f
commit
071a80864d
|
@ -89,6 +89,13 @@ export const findAllMatchingComponents = (rootComponent, selector) => {
|
|||
return components.reverse()
|
||||
}
|
||||
|
||||
/**
|
||||
* Recurses through the component tree and finds all components.
|
||||
*/
|
||||
export const findAllComponents = rootComponent => {
|
||||
return findAllMatchingComponents(rootComponent, () => true)
|
||||
}
|
||||
|
||||
/**
|
||||
* Finds the closes parent component which matches certain criteria
|
||||
*/
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
import { cloneDeep } from "lodash/fp"
|
||||
import { get } from "svelte/store"
|
||||
import {
|
||||
findAllComponents,
|
||||
findAllMatchingComponents,
|
||||
findComponent,
|
||||
findComponentPath,
|
||||
|
@ -156,7 +157,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) {
|
||||
|
@ -182,9 +183,69 @@ 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 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.
|
||||
*/
|
||||
const getComponentContexts = (asset, componentId, type) => {
|
||||
if (!asset || !componentId) {
|
||||
return []
|
||||
}
|
||||
let map = {}
|
||||
|
||||
// Processes all contexts exposed by a component
|
||||
const processContexts = scope => component => {
|
||||
const def = store.actions.components.getDefinition(component._component)
|
||||
if (!def?.context) {
|
||||
return
|
||||
}
|
||||
if (!map[component._id]) {
|
||||
map[component._id] = {
|
||||
component,
|
||||
definition: def,
|
||||
contexts: [],
|
||||
}
|
||||
}
|
||||
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"))
|
||||
|
||||
return Object.values(map)
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -196,49 +257,17 @@ export const getContextProviderComponents = (
|
|||
type,
|
||||
options = { includeSelf: false }
|
||||
) => {
|
||||
if (!asset || !componentId) {
|
||||
return []
|
||||
let componentContexts = getComponentContexts(asset, componentId, type)
|
||||
|
||||
// Exclude self if required
|
||||
if (!options?.includeSelf) {
|
||||
componentContexts = componentContexts.filter(
|
||||
entry => entry.component._id !== componentId
|
||||
)
|
||||
}
|
||||
|
||||
return findAllMatchingComponents(asset.props, component => {
|
||||
const def = store.actions.components.getDefinition(component._component)
|
||||
if (!def?.context) {
|
||||
return false
|
||||
}
|
||||
|
||||
// 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]
|
||||
return contexts.find(context => context.type === type) != null
|
||||
})
|
||||
//
|
||||
// // 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 false
|
||||
// }
|
||||
//
|
||||
// // 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]
|
||||
// return contexts.find(context => context.type === type) != null
|
||||
// })
|
||||
// Ignore contexts and just return the component instances
|
||||
return componentContexts.map(entry => entry.component)
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -305,142 +334,132 @@ 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.
|
||||
*/
|
||||
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,
|
||||
},
|
||||
})
|
||||
})
|
||||
})
|
||||
|
@ -448,24 +467,40 @@ 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
|
||||
}
|
||||
|
||||
/**
|
||||
* Determines the correct category for a given binding.
|
||||
*/
|
||||
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")) {
|
||||
let contextCategorySuffix = {
|
||||
form: "Fields",
|
||||
|
@ -476,6 +511,7 @@ const getComponentBindingCategory = (component, context, def) => {
|
|||
}`
|
||||
icon = context.type === "form" ? "Form" : "Data"
|
||||
}
|
||||
|
||||
return {
|
||||
icon,
|
||||
category,
|
||||
|
@ -483,7 +519,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 = []
|
||||
|
|
|
@ -531,10 +531,12 @@
|
|||
],
|
||||
"context": [
|
||||
{
|
||||
"type": "schema"
|
||||
"type": "schema",
|
||||
"scope": "local"
|
||||
},
|
||||
{
|
||||
"type": "static",
|
||||
"scope": "local",
|
||||
"values": [
|
||||
{
|
||||
"label": "Row index",
|
||||
|
|
|
@ -1 +1 @@
|
|||
Subproject commit cf3bef2aad9c739111b306fd0712397adc363f81
|
||||
Subproject commit 4638ae916e55ce89166095578cbd01745d0ee9ee
|
37
yarn.lock
37
yarn.lock
|
@ -3610,13 +3610,6 @@
|
|||
strip-ansi "^6.0.0"
|
||||
v8-to-istanbul "^9.0.1"
|
||||
|
||||
"@jest/schemas@^28.1.3":
|
||||
version "28.1.3"
|
||||
resolved "https://registry.yarnpkg.com/@jest/schemas/-/schemas-28.1.3.tgz#ad8b86a66f11f33619e3d7e1dcddd7f2d40ff905"
|
||||
integrity sha512-/l/VWsdt/aBXgjshLWOFyFt3IVdYypu5y2Wn2rOO1un6nkqIn8SLXzgIMYXFyYsRWDyF5EthmKJMIdJvk08grg==
|
||||
dependencies:
|
||||
"@sinclair/typebox" "^0.24.1"
|
||||
|
||||
"@jest/schemas@^29.4.3":
|
||||
version "29.4.3"
|
||||
resolved "https://registry.yarnpkg.com/@jest/schemas/-/schemas-29.4.3.tgz#39cf1b8469afc40b6f5a2baaa146e332c4151788"
|
||||
|
@ -3703,18 +3696,6 @@
|
|||
"@types/yargs" "^16.0.0"
|
||||
chalk "^4.0.0"
|
||||
|
||||
"@jest/types@^28.1.3":
|
||||
version "28.1.3"
|
||||
resolved "https://registry.yarnpkg.com/@jest/types/-/types-28.1.3.tgz#b05de80996ff12512bc5ceb1d208285a7d11748b"
|
||||
integrity sha512-RyjiyMUZrKz/c+zlMFO1pm70DcIlST8AeWTkoUdZevew44wcNZQHsEVOiCVtgVnlFFD82FPaXycys58cf2muVQ==
|
||||
dependencies:
|
||||
"@jest/schemas" "^28.1.3"
|
||||
"@types/istanbul-lib-coverage" "^2.0.0"
|
||||
"@types/istanbul-reports" "^3.0.0"
|
||||
"@types/node" "*"
|
||||
"@types/yargs" "^17.0.8"
|
||||
chalk "^4.0.0"
|
||||
|
||||
"@jest/types@^29.4.3":
|
||||
version "29.4.3"
|
||||
resolved "https://registry.yarnpkg.com/@jest/types/-/types-29.4.3.tgz#9069145f4ef09adf10cec1b2901b2d390031431f"
|
||||
|
@ -5139,11 +5120,6 @@
|
|||
make-fetch-happen "^11.0.1"
|
||||
tuf-js "^1.1.3"
|
||||
|
||||
"@sinclair/typebox@^0.24.1":
|
||||
version "0.24.51"
|
||||
resolved "https://registry.yarnpkg.com/@sinclair/typebox/-/typebox-0.24.51.tgz#645f33fe4e02defe26f2f5c0410e1c094eac7f5f"
|
||||
integrity sha512-1P1OROm/rdubP5aFDSZQILU0vrLCJ4fvHt6EoqHEM+2D/G5MK3bIaymUKLit8Js9gbns5UyJnkP/TZROLw4tUA==
|
||||
|
||||
"@sinclair/typebox@^0.25.16":
|
||||
version "0.25.24"
|
||||
resolved "https://registry.yarnpkg.com/@sinclair/typebox/-/typebox-0.25.24.tgz#8c7688559979f7079aacaf31aa881c3aa410b718"
|
||||
|
@ -6293,14 +6269,6 @@
|
|||
"@types/tedious" "*"
|
||||
tarn "^3.0.1"
|
||||
|
||||
"@types/node-fetch@2.6.1":
|
||||
version "2.6.1"
|
||||
resolved "https://registry.yarnpkg.com/@types/node-fetch/-/node-fetch-2.6.1.tgz#8f127c50481db65886800ef496f20bbf15518975"
|
||||
integrity sha512-oMqjURCaxoSIsHSr1E47QHzbmzNR5rK8McHuNb11BOM9cHcIK3Avy0s/b2JlXHoQGTYS3NsvWzV1M0iK7l0wbA==
|
||||
dependencies:
|
||||
"@types/node" "*"
|
||||
form-data "^3.0.0"
|
||||
|
||||
"@types/node-fetch@2.6.4":
|
||||
version "2.6.4"
|
||||
resolved "https://registry.yarnpkg.com/@types/node-fetch/-/node-fetch-2.6.4.tgz#1bc3a26de814f6bf466b25aeb1473fa1afe6a660"
|
||||
|
@ -6322,11 +6290,6 @@
|
|||
resolved "https://registry.yarnpkg.com/@types/node/-/node-18.11.18.tgz#8dfb97f0da23c2293e554c5a50d61ef134d7697f"
|
||||
integrity sha512-DHQpWGjyQKSHj3ebjFI/wRKcqQcdR+MoFBygntYOZytCqNfkd2ZC4ARDJ2DQqhjH5p85Nnd3jhUJIXrszFX/JA==
|
||||
|
||||
"@types/node@14.18.20":
|
||||
version "14.18.20"
|
||||
resolved "https://registry.yarnpkg.com/@types/node/-/node-14.18.20.tgz#268f028b36eaf51181c3300252f605488c4f0650"
|
||||
integrity sha512-Q8KKwm9YqEmUBRsqJ2GWJDtXltBDxTdC4m5vTdXBolu2PeQh8LX+f6BTwU+OuXPu37fLxoN6gidqBmnky36FXA==
|
||||
|
||||
"@types/node@16.9.1":
|
||||
version "16.9.1"
|
||||
resolved "https://registry.yarnpkg.com/@types/node/-/node-16.9.1.tgz#0611b37db4246c937feef529ddcc018cf8e35708"
|
||||
|
|
Loading…
Reference in New Issue