budibase/packages/builder/src/builderStore/fetchBindableProperties.js

174 lines
5.9 KiB
JavaScript

import { cloneDeep, difference } from "lodash/fp"
/**
* parameter for fetchBindableProperties function
* @typedef {Object} fetchBindablePropertiesParameter
* @property {string} componentInstanceId - an _id of a component that has been added to a screen, which you want to fetch bindable props for
* @propperty {Object} screen - current screen - where componentInstanceId lives
* @property {Object} components - dictionary of component definitions
* @property {Array} models - array of all models
*/
/**
*
* @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>}
*/
export default function({ componentInstanceId, screen, components, models }) {
const walkResult = walk({
// cloning so we are free to mutate props (e.g. by adding _contexts)
instance: cloneDeep(screen.props),
targetId: componentInstanceId,
components,
models,
})
return [
...walkResult.bindableInstances
.filter(isInstanceInSharedContext(walkResult))
.map(componentInstanceToBindable(walkResult)),
...walkResult.target._contexts
.map(contextToBindables(models, walkResult))
.flat(),
]
}
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
// turns a component instance prop into binding expressions
// used by the UI
const componentInstanceToBindable = walkResult => i => {
const lastContext =
i.instance._contexts.length &&
i.instance._contexts[i.instance._contexts.length - 1]
const contextParentPath = lastContext
? getParentPath(walkResult, lastContext)
: ""
return {
type: "instance",
instance: i.instance,
// how the binding expression persists, and is used in the app at runtime
runtimeBinding: `${contextParentPath}${i.instance._id}.${i.prop}`,
// how the binding exressions looks to the user of the builder
readableBinding: `${i.instance._instanceName}`,
}
}
const contextToBindables = (models, walkResult) => context => {
const contextParentPath = getParentPath(walkResult, context)
const newBindable = key => ({
type: "context",
instance: context.instance,
// how the binding expression persists, and is used in the app at runtime
runtimeBinding: `${contextParentPath}data.${key}`,
// how the binding exressions looks to the user of the builder
readableBinding: `${context.instance._instanceName}.${context.model.label}.${key}`,
})
// see ModelViewSelect.svelte for the format of context.model
// ... this allows us to bind to Model scheams, or View schemas
const model = models.find(m => m._id === context.model.modelId)
const schema = context.model.isModel
? model.schema
: model.views[context.model.name].schema
return (
Object.keys(schema)
.map(newBindable)
// add _id and _rev fields - not part of schema, but always valid
.concat([newBindable("_id"), newBindable("_rev")])
)
}
const getParentPath = (walkResult, context) => {
// describes the number of "parent" in the path
// clone array first so original array is not mtated
const contextParentNumber = [...walkResult.target._contexts]
.reverse()
.indexOf(context)
return (
new Array(contextParentNumber).fill("parent").join(".") +
// trailing . if has parents
(contextParentNumber ? "." : "")
)
}
const walk = ({ instance, targetId, components, models, result }) => {
if (!result) {
result = {
target: null,
bindableInstances: [],
allContexts: [],
currentContexts: [],
}
}
if (!instance._contexts) instance._contexts = []
// "component" is the component definition (object in component.json)
const component = components[instance._component]
if (instance._id === targetId) {
// found it
result.target = instance
} else {
if (component && component.bindable) {
// 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
// so we will filter in another method
result.bindableInstances.push({
instance,
prop: component.bindable,
})
}
}
// a component that provides context to it's children
const contextualInstance =
component && component.context && instance[component.context]
if (contextualInstance) {
// add to currentContexts (ancestory of context)
// before walking children
const model = instance[component.context]
result.currentContexts.push({ instance, model })
}
const currentContexts = [...result.currentContexts]
for (let child of instance._children || []) {
// 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
walk({ instance: child, targetId, components, models, result })
}
if (contextualInstance) {
// child walk done, remove from currentContexts
result.currentContexts.pop()
}
return result
}