2020-08-12 17:30:20 +02:00
|
|
|
import { cloneDeep, difference } from "lodash/fp"
|
2020-08-04 17:11:46 +02:00
|
|
|
|
2020-08-04 17:31:31 +02:00
|
|
|
/**
|
|
|
|
* parameter for fetchBindableProperties function
|
|
|
|
* @typedef {Object} fetchBindablePropertiesParameter
|
2020-08-27 11:00:36 +02:00
|
|
|
* @property {string} componentInstanceId - an _id of a component that has been added to a screen, which you want to fetch bindable props for
|
2020-08-04 17:31:31 +02:00
|
|
|
* @propperty {Object} screen - current screen - where componentInstanceId lives
|
|
|
|
* @property {Object} components - dictionary of component definitions
|
2020-10-09 19:49:23 +02:00
|
|
|
* @property {Array} tables - array of all tables
|
2020-08-04 17:31:31 +02:00
|
|
|
*/
|
2020-08-04 12:10:02 +02:00
|
|
|
|
2020-08-04 17:31:31 +02:00
|
|
|
/**
|
|
|
|
*
|
|
|
|
* @typedef {Object} BindableProperty
|
|
|
|
* @property {string} type - either "instance" (binding to a component instance) or "context" (binding to data in context e.g. List Item)
|
|
|
|
* @property {Object} instance - relevant component instance. If "context" type, this instance is the component that provides the context... e.g. the List
|
|
|
|
* @property {string} runtimeBinding - a binding string that is a) saved against the string, and b) used at runtime to read/write the value
|
|
|
|
* @property {string} readableBinding - a binding string that is displayed to the user, in the builder
|
|
|
|
*/
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Generates all allowed bindings from within any particular component instance
|
|
|
|
* @param {fetchBindablePropertiesParameter} param
|
|
|
|
* @returns {Array.<BindableProperty>}
|
|
|
|
*/
|
2020-10-09 19:49:23 +02:00
|
|
|
export default function({ componentInstanceId, screen, components, tables }) {
|
2020-11-20 11:49:39 +01:00
|
|
|
const result = walk({
|
2020-08-04 17:11:46 +02:00
|
|
|
// cloning so we are free to mutate props (e.g. by adding _contexts)
|
|
|
|
instance: cloneDeep(screen.props),
|
|
|
|
targetId: componentInstanceId,
|
|
|
|
components,
|
2020-10-09 19:49:23 +02:00
|
|
|
tables,
|
2020-08-04 17:11:46 +02:00
|
|
|
})
|
2020-08-03 16:06:51 +02:00
|
|
|
|
|
|
|
return [
|
2020-11-20 11:49:39 +01:00
|
|
|
...result.bindableInstances
|
|
|
|
.filter(isInstanceInSharedContext(result))
|
|
|
|
.map(componentInstanceToBindable),
|
|
|
|
...(result.target?._contexts.map(contextToBindables(tables)).flat() ?? []),
|
2020-08-03 16:06:51 +02:00
|
|
|
]
|
|
|
|
}
|
|
|
|
|
2020-08-04 17:11:46 +02:00
|
|
|
const isInstanceInSharedContext = walkResult => i =>
|
|
|
|
// should cover
|
|
|
|
// - neither are in any context
|
|
|
|
// - both in same context
|
|
|
|
// - instance is in ancestor context of target
|
|
|
|
i.instance._contexts.length <= walkResult.target._contexts.length &&
|
|
|
|
difference(i.instance._contexts, walkResult.target._contexts).length === 0
|
2020-08-03 16:06:51 +02:00
|
|
|
|
|
|
|
// turns a component instance prop into binding expressions
|
|
|
|
// used by the UI
|
2020-11-20 11:49:39 +01:00
|
|
|
const componentInstanceToBindable = i => {
|
2020-08-04 17:11:46 +02:00
|
|
|
return {
|
|
|
|
type: "instance",
|
|
|
|
instance: i.instance,
|
|
|
|
// how the binding expression persists, and is used in the app at runtime
|
2020-11-20 11:49:39 +01:00
|
|
|
runtimeBinding: `${i.instance._id}`,
|
2020-08-04 17:11:46 +02:00
|
|
|
// how the binding exressions looks to the user of the builder
|
|
|
|
readableBinding: `${i.instance._instanceName}`,
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2020-11-20 11:49:39 +01:00
|
|
|
const contextToBindables = tables => context => {
|
2020-10-13 10:58:08 +02:00
|
|
|
const tableId = context.table?.tableId ?? context.table
|
2020-10-09 19:49:23 +02:00
|
|
|
const table = tables.find(table => table._id === tableId)
|
2020-10-12 22:35:51 +02:00
|
|
|
let schema =
|
2020-10-13 10:58:08 +02:00
|
|
|
context.table?.type === "view"
|
|
|
|
? table?.views?.[context.table.name]?.schema
|
|
|
|
: table?.schema
|
2020-08-04 17:11:46 +02:00
|
|
|
|
2020-10-08 10:48:03 +02:00
|
|
|
// Avoid crashing whenever no data source has been selected
|
2020-10-09 13:24:18 +02:00
|
|
|
if (!schema) {
|
2020-10-08 10:48:03 +02:00
|
|
|
return []
|
|
|
|
}
|
|
|
|
|
2020-10-12 15:14:50 +02:00
|
|
|
const newBindable = ([key, fieldSchema]) => {
|
2020-10-20 13:23:52 +02:00
|
|
|
// Replace certain bindings with a new property to help display components
|
2020-10-12 15:14:50 +02:00
|
|
|
let runtimeBoundKey = key
|
|
|
|
if (fieldSchema.type === "link") {
|
|
|
|
runtimeBoundKey = `${key}_count`
|
2020-10-20 13:23:52 +02:00
|
|
|
} else if (fieldSchema.type === "attachment") {
|
|
|
|
runtimeBoundKey = `${key}_first`
|
2020-10-12 15:14:50 +02:00
|
|
|
}
|
|
|
|
return {
|
|
|
|
type: "context",
|
|
|
|
fieldSchema,
|
|
|
|
instance: context.instance,
|
|
|
|
// how the binding expression persists, and is used in the app at runtime
|
2020-11-20 11:49:39 +01:00
|
|
|
runtimeBinding: `${context.instance._id}.${runtimeBoundKey}`,
|
2020-10-12 15:14:50 +02:00
|
|
|
// how the binding expressions looks to the user of the builder
|
2020-10-13 10:58:08 +02:00
|
|
|
readableBinding: `${context.instance._instanceName}.${table.name}.${key}`,
|
|
|
|
// table / view info
|
|
|
|
table: context.table,
|
2020-10-12 15:14:50 +02:00
|
|
|
}
|
|
|
|
}
|
2020-09-10 22:55:04 +02:00
|
|
|
|
2020-10-28 20:37:53 +01:00
|
|
|
const stringType = { type: "string" }
|
2020-09-09 16:56:31 +02:00
|
|
|
return (
|
2020-10-09 13:24:18 +02:00
|
|
|
Object.entries(schema)
|
2020-09-09 16:56:31 +02:00
|
|
|
.map(newBindable)
|
|
|
|
// add _id and _rev fields - not part of schema, but always valid
|
2020-10-28 20:37:53 +01:00
|
|
|
.concat([
|
|
|
|
newBindable(["_id", stringType]),
|
|
|
|
newBindable(["_rev", stringType]),
|
|
|
|
])
|
2020-09-09 16:56:31 +02:00
|
|
|
)
|
2020-08-03 16:06:51 +02:00
|
|
|
}
|
|
|
|
|
2020-10-09 19:49:23 +02:00
|
|
|
const walk = ({ instance, targetId, components, tables, result }) => {
|
2020-08-03 16:06:51 +02:00
|
|
|
if (!result) {
|
|
|
|
result = {
|
|
|
|
target: null,
|
|
|
|
bindableInstances: [],
|
2020-08-04 17:11:46 +02:00
|
|
|
allContexts: [],
|
|
|
|
currentContexts: [],
|
2020-08-03 16:06:51 +02:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2020-08-04 17:11:46 +02:00
|
|
|
if (!instance._contexts) instance._contexts = []
|
|
|
|
|
2020-08-03 16:06:51 +02:00
|
|
|
// "component" is the component definition (object in component.json)
|
|
|
|
const component = components[instance._component]
|
|
|
|
|
|
|
|
if (instance._id === targetId) {
|
|
|
|
// found it
|
|
|
|
result.target = instance
|
|
|
|
} else {
|
2020-08-25 11:25:56 +02:00
|
|
|
if (component && component.bindable) {
|
2020-08-03 16:06:51 +02:00
|
|
|
// pushing all components in here initially
|
|
|
|
// but this will not be correct, as some of
|
|
|
|
// these components will be in another context
|
|
|
|
// but we dont know this until the end of the walk
|
2020-08-27 11:00:36 +02:00
|
|
|
// so we will filter in another method
|
2020-08-03 16:06:51 +02:00
|
|
|
result.bindableInstances.push({
|
|
|
|
instance,
|
2020-08-04 12:10:02 +02:00
|
|
|
prop: component.bindable,
|
2020-08-03 16:06:51 +02:00
|
|
|
})
|
|
|
|
}
|
|
|
|
}
|
2020-08-04 17:11:46 +02:00
|
|
|
|
2020-08-03 16:06:51 +02:00
|
|
|
// a component that provides context to it's children
|
2020-08-27 11:00:36 +02:00
|
|
|
const contextualInstance =
|
|
|
|
component && component.context && instance[component.context]
|
2020-08-03 16:06:51 +02:00
|
|
|
|
|
|
|
if (contextualInstance) {
|
|
|
|
// add to currentContexts (ancestory of context)
|
|
|
|
// before walking children
|
2020-10-09 19:49:23 +02:00
|
|
|
const table = instance[component.context]
|
|
|
|
result.currentContexts.push({ instance, table })
|
2020-08-03 16:06:51 +02:00
|
|
|
}
|
|
|
|
|
2020-08-04 17:11:46 +02:00
|
|
|
const currentContexts = [...result.currentContexts]
|
2020-08-03 16:06:51 +02:00
|
|
|
for (let child of instance._children || []) {
|
2020-08-04 17:11:46 +02:00
|
|
|
// attaching _contexts of components, for eas comparison later
|
|
|
|
// these have been deep cloned above, so shouln't modify the
|
|
|
|
// original component instances
|
|
|
|
child._contexts = currentContexts
|
2020-10-09 19:49:23 +02:00
|
|
|
walk({ instance: child, targetId, components, tables, result })
|
2020-08-03 16:06:51 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
if (contextualInstance) {
|
|
|
|
// child walk done, remove from currentContexts
|
|
|
|
result.currentContexts.pop()
|
|
|
|
}
|
|
|
|
|
|
|
|
return result
|
|
|
|
}
|