2021-01-19 18:38:24 +01:00
|
|
|
import { cloneDeep } from "lodash/fp"
|
2021-01-14 16:39:50 +01:00
|
|
|
import { get } from "svelte/store"
|
|
|
|
import { backendUiStore, store } from "builderStore"
|
2021-02-01 19:51:22 +01:00
|
|
|
import { findComponentPath } from "./storeUtils"
|
2021-01-22 18:57:38 +01:00
|
|
|
import { makePropSafe } from "@budibase/string-templates"
|
2021-01-28 15:29:35 +01:00
|
|
|
import { TableNames } from "../constants"
|
2021-01-14 16:39:50 +01:00
|
|
|
|
2021-01-21 12:31:45 +01:00
|
|
|
// Regex to match all instances of template strings
|
|
|
|
const CAPTURE_VAR_INSIDE_TEMPLATE = /{{([^}]+)}}/g
|
2021-01-19 16:39:04 +01:00
|
|
|
|
2021-01-14 16:39:50 +01:00
|
|
|
/**
|
|
|
|
* Gets all bindable data context fields and instance fields.
|
|
|
|
*/
|
|
|
|
export const getBindableProperties = (rootComponent, componentId) => {
|
2021-02-01 19:51:22 +01:00
|
|
|
return getContextBindings(rootComponent, componentId)
|
2021-01-14 16:39:50 +01:00
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
2021-01-19 18:38:24 +01:00
|
|
|
* Gets all data provider components above a component.
|
2021-01-14 16:39:50 +01:00
|
|
|
*/
|
2021-01-19 18:38:24 +01:00
|
|
|
export const getDataProviderComponents = (rootComponent, componentId) => {
|
2021-01-14 16:39:50 +01:00
|
|
|
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 18:38:24 +01:00
|
|
|
// Filter by only data provider components
|
|
|
|
return path.filter(component => {
|
|
|
|
const def = store.actions.components.getDefinition(component._component)
|
|
|
|
return def?.dataProvider
|
|
|
|
})
|
|
|
|
}
|
2021-01-14 16:39:50 +01:00
|
|
|
|
2021-02-01 19:51:22 +01:00
|
|
|
/**
|
|
|
|
* Gets all data provider components above a component.
|
|
|
|
*/
|
|
|
|
export const getActionProviderComponents = (
|
|
|
|
rootComponent,
|
|
|
|
componentId,
|
|
|
|
actionType
|
|
|
|
) => {
|
|
|
|
if (!rootComponent || !componentId) {
|
|
|
|
return []
|
|
|
|
}
|
|
|
|
|
|
|
|
// Get the component tree leading up to this component, ignoring the component
|
|
|
|
// itself
|
|
|
|
const path = findComponentPath(rootComponent, componentId)
|
|
|
|
path.pop()
|
|
|
|
|
|
|
|
// Filter by only data provider components
|
|
|
|
return path.filter(component => {
|
|
|
|
const def = store.actions.components.getDefinition(component._component)
|
|
|
|
return def?.actions?.includes(actionType)
|
|
|
|
})
|
|
|
|
}
|
|
|
|
|
2021-01-19 18:38:24 +01:00
|
|
|
/**
|
|
|
|
* Gets a datasource object for a certain data provider component
|
|
|
|
*/
|
|
|
|
export const getDatasourceForProvider = component => {
|
|
|
|
const def = store.actions.components.getDefinition(component?._component)
|
|
|
|
if (!def) {
|
|
|
|
return null
|
|
|
|
}
|
2021-01-19 16:39:04 +01:00
|
|
|
|
2021-01-19 18:38:24 +01:00
|
|
|
// Extract datasource from component instance
|
|
|
|
const datasourceSetting = def.settings.find(setting => {
|
2021-01-26 11:57:57 +01:00
|
|
|
return setting.type === "datasource" || setting.type === "table"
|
2021-01-19 18:38:24 +01:00
|
|
|
})
|
|
|
|
if (!datasourceSetting) {
|
|
|
|
return null
|
|
|
|
}
|
|
|
|
|
|
|
|
// 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
|
|
|
|
if (datasourceSetting.type === "datasource") {
|
|
|
|
return component[datasourceSetting?.key]
|
|
|
|
} else if (datasourceSetting.type === "table") {
|
|
|
|
return {
|
|
|
|
tableId: component[datasourceSetting?.key],
|
|
|
|
type: "table",
|
2021-01-19 16:39:04 +01:00
|
|
|
}
|
2021-01-19 18:38:24 +01:00
|
|
|
}
|
|
|
|
return null
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* 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.
|
|
|
|
*/
|
2021-01-20 12:14:36 +01:00
|
|
|
export const getContextBindings = (rootComponent, componentId) => {
|
2021-01-19 18:38:24 +01:00
|
|
|
// Extract any components which provide data contexts
|
2021-01-20 12:14:36 +01:00
|
|
|
const dataProviders = getDataProviderComponents(rootComponent, componentId)
|
|
|
|
let contextBindings = []
|
2021-01-19 18:38:24 +01:00
|
|
|
dataProviders.forEach(component => {
|
|
|
|
const datasource = getDatasourceForProvider(component)
|
2021-01-19 16:39:04 +01:00
|
|
|
if (!datasource) {
|
2021-01-14 16:39:50 +01:00
|
|
|
return
|
|
|
|
}
|
2021-01-19 16:39:04 +01:00
|
|
|
|
2021-01-21 11:40:45 +01:00
|
|
|
// Get schema and add _id and _rev fields for certain types
|
2021-01-19 18:38:24 +01:00
|
|
|
let { schema, table } = getSchemaForDatasource(datasource)
|
2021-01-21 11:40:45 +01:00
|
|
|
if (!schema || !table) {
|
|
|
|
return
|
|
|
|
}
|
|
|
|
if (datasource.type === "table" || datasource.type === "link") {
|
|
|
|
schema["_id"] = { type: "string" }
|
|
|
|
schema["_rev"] = { type: "string " }
|
|
|
|
}
|
2021-01-15 15:47:36 +01:00
|
|
|
const keys = Object.keys(schema).sort()
|
2021-01-19 18:38:24 +01:00
|
|
|
|
|
|
|
// Create bindable properties for each schema field
|
2021-01-15 15:47:36 +01:00
|
|
|
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`
|
|
|
|
}
|
|
|
|
|
2021-01-20 12:14:36 +01:00
|
|
|
contextBindings.push({
|
2021-01-14 16:39:50 +01:00
|
|
|
type: "context",
|
2021-01-22 18:58:01 +01:00
|
|
|
runtimeBinding: `${makePropSafe(component._id)}.${makePropSafe(
|
|
|
|
runtimeBoundKey
|
|
|
|
)}`,
|
2021-01-19 18:38:24 +01:00
|
|
|
readableBinding: `${component._instanceName}.${table.name}.${key}`,
|
2021-01-15 15:47:36 +01:00
|
|
|
fieldSchema,
|
2021-01-19 18:38:24 +01:00
|
|
|
providerId: component._id,
|
2021-01-19 16:39:04 +01:00
|
|
|
tableId: datasource.tableId,
|
2021-01-19 18:38:24 +01:00
|
|
|
field: key,
|
2021-01-14 16:39:50 +01:00
|
|
|
})
|
|
|
|
})
|
|
|
|
})
|
2021-01-28 15:29:35 +01:00
|
|
|
|
|
|
|
// Add logged in user bindings
|
|
|
|
const tables = get(backendUiStore).tables
|
|
|
|
const userTable = tables.find(table => table._id === TableNames.USERS)
|
|
|
|
const schema = {
|
|
|
|
...userTable.schema,
|
|
|
|
_id: { type: "string" },
|
|
|
|
_rev: { type: "string" },
|
|
|
|
}
|
|
|
|
const keys = Object.keys(schema).sort()
|
|
|
|
keys.forEach(key => {
|
|
|
|
const fieldSchema = schema[key]
|
|
|
|
// Replace certain bindings with a new property to help display components
|
|
|
|
let runtimeBoundKey = key
|
|
|
|
if (fieldSchema.type === "link") {
|
|
|
|
runtimeBoundKey = `${key}_count`
|
|
|
|
} else if (fieldSchema.type === "attachment") {
|
|
|
|
runtimeBoundKey = `${key}_first`
|
|
|
|
}
|
|
|
|
|
|
|
|
contextBindings.push({
|
|
|
|
type: "context",
|
|
|
|
runtimeBinding: `user.${runtimeBoundKey}`,
|
|
|
|
readableBinding: `Current User.${key}`,
|
|
|
|
fieldSchema,
|
|
|
|
providerId: "user",
|
|
|
|
tableId: TableNames.USERS,
|
|
|
|
field: key,
|
|
|
|
})
|
|
|
|
})
|
|
|
|
|
2021-01-20 12:14:36 +01:00
|
|
|
return contextBindings
|
2021-01-14 16:39:50 +01:00
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Gets a schema for a datasource object.
|
|
|
|
*/
|
2021-01-19 18:38:24 +01:00
|
|
|
export const getSchemaForDatasource = datasource => {
|
|
|
|
let schema, table
|
|
|
|
if (datasource) {
|
|
|
|
const { type } = datasource
|
2021-01-21 11:40:45 +01:00
|
|
|
if (type === "query") {
|
|
|
|
const queries = get(backendUiStore).queries
|
|
|
|
table = queries.find(query => query._id === datasource._id)
|
|
|
|
} else {
|
|
|
|
const tables = get(backendUiStore).tables
|
|
|
|
table = tables.find(table => table._id === datasource.tableId)
|
|
|
|
}
|
2021-01-19 18:38:24 +01:00
|
|
|
if (table) {
|
2021-01-21 11:40:45 +01:00
|
|
|
if (type === "view") {
|
2021-01-19 18:38:24 +01:00
|
|
|
schema = cloneDeep(table.views?.[datasource.name]?.schema)
|
2021-01-21 11:40:45 +01:00
|
|
|
} else {
|
2021-01-19 18:38:24 +01:00
|
|
|
schema = cloneDeep(table.schema)
|
|
|
|
}
|
2021-01-14 16:39:50 +01:00
|
|
|
}
|
|
|
|
}
|
|
|
|
return { schema, table }
|
|
|
|
}
|
2021-01-19 16:39:04 +01:00
|
|
|
|
|
|
|
/**
|
2021-01-29 21:03:09 +01:00
|
|
|
* utility function for the readableToRuntimeBinding and runtimeToReadableBinding.
|
2021-01-19 16:39:04 +01:00
|
|
|
*/
|
2021-01-29 21:03:09 +01:00
|
|
|
function bindingReplacement(bindableProperties, textWithBindings, convertTo) {
|
2021-01-30 03:54:52 +01:00
|
|
|
const convertFrom =
|
|
|
|
convertTo === "runtimeBinding" ? "readableBinding" : "runtimeBinding"
|
2021-01-19 18:38:24 +01:00
|
|
|
if (typeof textWithBindings !== "string") {
|
|
|
|
return textWithBindings
|
|
|
|
}
|
2021-01-29 21:03:09 +01:00
|
|
|
const convertFromProps = bindableProperties
|
|
|
|
.map(el => el[convertFrom])
|
|
|
|
.sort((a, b) => {
|
|
|
|
return b.length - a.length
|
|
|
|
})
|
2021-01-21 12:31:45 +01:00
|
|
|
const boundValues = textWithBindings.match(CAPTURE_VAR_INSIDE_TEMPLATE) || []
|
2021-01-19 16:39:04 +01:00
|
|
|
let result = textWithBindings
|
2021-01-26 16:59:28 +01:00
|
|
|
for (let boundValue of boundValues) {
|
2021-01-29 21:03:09 +01:00
|
|
|
let newBoundValue = boundValue
|
|
|
|
for (let from of convertFromProps) {
|
|
|
|
if (newBoundValue.includes(from)) {
|
|
|
|
const binding = bindableProperties.find(el => el[convertFrom] === from)
|
2021-01-30 03:54:52 +01:00
|
|
|
newBoundValue = newBoundValue.replace(from, binding[convertTo])
|
2021-01-29 21:03:09 +01:00
|
|
|
}
|
2021-01-19 16:39:04 +01:00
|
|
|
}
|
2021-01-26 16:59:58 +01:00
|
|
|
result = result.replace(boundValue, newBoundValue)
|
2021-01-26 16:59:28 +01:00
|
|
|
}
|
2021-01-19 16:39:04 +01:00
|
|
|
return result
|
|
|
|
}
|
|
|
|
|
2021-01-29 21:03:09 +01:00
|
|
|
/**
|
|
|
|
* Converts a readable data binding into a runtime data binding
|
|
|
|
*/
|
|
|
|
export function readableToRuntimeBinding(bindableProperties, textWithBindings) {
|
2021-01-30 03:54:52 +01:00
|
|
|
return bindingReplacement(
|
|
|
|
bindableProperties,
|
|
|
|
textWithBindings,
|
|
|
|
"runtimeBinding"
|
|
|
|
)
|
2021-01-29 21:03:09 +01:00
|
|
|
}
|
|
|
|
|
2021-01-19 16:39:04 +01:00
|
|
|
/**
|
|
|
|
* Converts a runtime data binding into a readable data binding
|
|
|
|
*/
|
|
|
|
export function runtimeToReadableBinding(bindableProperties, textWithBindings) {
|
2021-01-30 03:54:52 +01:00
|
|
|
return bindingReplacement(
|
|
|
|
bindableProperties,
|
|
|
|
textWithBindings,
|
|
|
|
"readableBinding"
|
|
|
|
)
|
2021-01-19 16:39:04 +01:00
|
|
|
}
|