2021-01-14 16:39:50 +01:00
|
|
|
import { get } from "svelte/store"
|
|
|
|
import { backendUiStore, store } from "builderStore"
|
|
|
|
import { findAllMatchingComponents, findComponentPath } from "./storeUtils"
|
|
|
|
|
2021-01-19 16:39:04 +01:00
|
|
|
// Regex to match mustache variables, for replacing bindings
|
|
|
|
const CAPTURE_VAR_INSIDE_MUSTACHE = /{{([^}]+)}}/g
|
|
|
|
|
2021-01-14 16:39:50 +01:00
|
|
|
/**
|
|
|
|
* Gets all bindable data context fields and instance fields.
|
|
|
|
*/
|
|
|
|
export const getBindableProperties = (rootComponent, componentId) => {
|
|
|
|
const bindableContexts = getBindableContexts(rootComponent, componentId)
|
|
|
|
const bindableComponents = getBindableComponents(rootComponent)
|
|
|
|
return [...bindableContexts, ...bindableComponents]
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Gets all bindable data contexts. These are fields of schemas of data contexts
|
|
|
|
* provided by data provider components, such as lists or row detail components.
|
|
|
|
*/
|
|
|
|
export const getBindableContexts = (rootComponent, componentId) => {
|
|
|
|
if (!rootComponent || !componentId) {
|
|
|
|
return []
|
|
|
|
}
|
|
|
|
|
2021-01-19 16:39:04 +01:00
|
|
|
// Get the component tree leading up to this component, ignoring the component
|
|
|
|
// itself
|
2021-01-14 16:39:50 +01:00
|
|
|
const path = findComponentPath(rootComponent, componentId)
|
|
|
|
path.pop()
|
|
|
|
|
2021-01-19 16:39:04 +01:00
|
|
|
// Enrich components with their definitions
|
|
|
|
const enriched = path.map(component => ({
|
|
|
|
instance: component,
|
|
|
|
definition: store.actions.components.getDefinition(component._component),
|
|
|
|
}))
|
2021-01-14 16:39:50 +01:00
|
|
|
|
2021-01-19 16:39:04 +01:00
|
|
|
// Extract any components which provide data contexts
|
|
|
|
const providers = enriched.filter(comp => comp.definition?.dataProvider)
|
2021-01-14 16:39:50 +01:00
|
|
|
let contexts = []
|
2021-01-19 16:39:04 +01:00
|
|
|
providers.forEach(({ definition, instance }) => {
|
|
|
|
// Extract datasource from component instance
|
|
|
|
const datasourceSetting = definition.settings.find(setting => {
|
|
|
|
return setting.key === definition.datasourceSetting
|
|
|
|
})
|
|
|
|
if (!datasourceSetting) {
|
|
|
|
return
|
|
|
|
}
|
|
|
|
|
|
|
|
// There are different types of setting which can be a datasource, for
|
|
|
|
// example an actual datasource object, or a table ID string.
|
|
|
|
// Convert the datasource setting into a proper datasource object so that
|
|
|
|
// we can use it properly
|
|
|
|
let datasource
|
|
|
|
if (datasourceSetting.type === "datasource") {
|
|
|
|
datasource = instance[datasourceSetting?.key]
|
|
|
|
} else if (datasourceSetting.type === "table") {
|
|
|
|
datasource = {
|
|
|
|
tableId: instance[datasourceSetting?.key],
|
|
|
|
type: "table",
|
|
|
|
}
|
|
|
|
}
|
|
|
|
if (!datasource) {
|
2021-01-14 16:39:50 +01:00
|
|
|
return
|
|
|
|
}
|
2021-01-19 16:39:04 +01:00
|
|
|
|
|
|
|
const { schema, table } = getSchemaForDatasource(datasource)
|
2021-01-15 15:47:36 +01:00
|
|
|
const keys = Object.keys(schema).sort()
|
|
|
|
keys.forEach(key => {
|
|
|
|
const fieldSchema = schema[key]
|
2021-01-14 16:39:50 +01:00
|
|
|
// Replace certain bindings with a new property to help display components
|
|
|
|
let runtimeBoundKey = key
|
2021-01-15 15:47:36 +01:00
|
|
|
if (fieldSchema.type === "link") {
|
2021-01-14 16:39:50 +01:00
|
|
|
runtimeBoundKey = `${key}_count`
|
2021-01-15 15:47:36 +01:00
|
|
|
} else if (fieldSchema.type === "attachment") {
|
2021-01-14 16:39:50 +01:00
|
|
|
runtimeBoundKey = `${key}_first`
|
|
|
|
}
|
|
|
|
|
|
|
|
contexts.push({
|
|
|
|
type: "context",
|
2021-01-19 16:39:04 +01:00
|
|
|
runtimeBinding: `${instance._id}.${runtimeBoundKey}`,
|
|
|
|
readableBinding: `${instance._instanceName}.${table.name}.${key}`,
|
2021-01-15 15:47:36 +01:00
|
|
|
fieldSchema,
|
2021-01-19 16:39:04 +01:00
|
|
|
providerId: instance._id,
|
|
|
|
tableId: datasource.tableId,
|
2021-01-14 16:39:50 +01:00
|
|
|
})
|
|
|
|
})
|
|
|
|
})
|
|
|
|
return contexts
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Gets all bindable components. These are form components which allow their
|
|
|
|
* values to be bound to.
|
|
|
|
*/
|
|
|
|
export const getBindableComponents = rootComponent => {
|
|
|
|
if (!rootComponent) {
|
|
|
|
return []
|
|
|
|
}
|
|
|
|
const componentSelector = component => {
|
|
|
|
const type = component._component
|
|
|
|
const definition = store.actions.components.getDefinition(type)
|
2021-01-15 12:02:00 +01:00
|
|
|
return definition?.bindable
|
2021-01-14 16:39:50 +01:00
|
|
|
}
|
|
|
|
const components = findAllMatchingComponents(rootComponent, componentSelector)
|
2021-01-15 11:59:22 +01:00
|
|
|
return components.map(component => {
|
|
|
|
return {
|
|
|
|
type: "instance",
|
|
|
|
providerId: component._id,
|
|
|
|
runtimeBinding: `${component._id}`,
|
|
|
|
readableBinding: `${component._instanceName}`,
|
|
|
|
}
|
|
|
|
})
|
2021-01-14 16:39:50 +01:00
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Gets a schema for a datasource object.
|
|
|
|
*/
|
|
|
|
const getSchemaForDatasource = datasource => {
|
|
|
|
const tables = get(backendUiStore).tables
|
|
|
|
const { type } = datasource
|
|
|
|
const table = tables.find(table => table._id === datasource.tableId)
|
2021-01-15 15:47:36 +01:00
|
|
|
let schema
|
2021-01-14 16:39:50 +01:00
|
|
|
if (table) {
|
|
|
|
if (type === "table") {
|
|
|
|
schema = table.schema ?? {}
|
|
|
|
} else if (type === "view") {
|
|
|
|
schema = table.views?.[datasource.name]?.schema ?? {}
|
|
|
|
} else if (type === "link") {
|
|
|
|
schema = table.schema ?? {}
|
|
|
|
}
|
|
|
|
}
|
2021-01-15 15:47:36 +01:00
|
|
|
if (schema) {
|
|
|
|
// Add ID and rev fields for any valid datasources
|
|
|
|
schema["_id"] = { type: "string" }
|
|
|
|
schema["_rev"] = { type: "string " }
|
|
|
|
} else {
|
|
|
|
schema = {}
|
|
|
|
}
|
2021-01-14 16:39:50 +01:00
|
|
|
return { schema, table }
|
|
|
|
}
|
2021-01-19 16:39:04 +01:00
|
|
|
|
|
|
|
/**
|
|
|
|
* Converts a readable data binding into a runtime data binding
|
|
|
|
*/
|
|
|
|
export function readableToRuntimeBinding(bindableProperties, textWithBindings) {
|
|
|
|
const boundValues = textWithBindings.match(CAPTURE_VAR_INSIDE_MUSTACHE) || []
|
|
|
|
let result = textWithBindings
|
|
|
|
boundValues.forEach(boundValue => {
|
|
|
|
const binding = bindableProperties.find(({ readableBinding }) => {
|
|
|
|
return boundValue === `{{ ${readableBinding} }}`
|
|
|
|
})
|
|
|
|
if (binding) {
|
|
|
|
result = result.replace(boundValue, `{{ ${binding.runtimeBinding} }}`)
|
|
|
|
}
|
|
|
|
})
|
|
|
|
return result
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Converts a runtime data binding into a readable data binding
|
|
|
|
*/
|
|
|
|
export function runtimeToReadableBinding(bindableProperties, textWithBindings) {
|
|
|
|
const boundValues = textWithBindings.match(CAPTURE_VAR_INSIDE_MUSTACHE) || []
|
|
|
|
let result = textWithBindings
|
|
|
|
boundValues.forEach(boundValue => {
|
|
|
|
const binding = bindableProperties.find(({ runtimeBinding }) => {
|
|
|
|
return boundValue === `{{ ${runtimeBinding} }}`
|
|
|
|
})
|
|
|
|
// Show invalid bindings as invalid rather than a long ID
|
|
|
|
result = result.replace(
|
|
|
|
boundValue,
|
|
|
|
`{{ ${binding?.readableBinding ?? "Invalid binding"} }}`
|
|
|
|
)
|
|
|
|
})
|
|
|
|
return result
|
|
|
|
}
|