mirror of https://github.com/Budibase/budibase.git
11 changed files with 121 additions and 295 deletions
@ -1,168 +0,0 @@ |
|||
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} tables - array of all tables |
|||
*/ |
|||
|
|||
/** |
|||
* |
|||
* @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, tables }) { |
|||
const result = walk({ |
|||
// cloning so we are free to mutate props (e.g. by adding _contexts)
|
|||
instance: cloneDeep(screen.props), |
|||
targetId: componentInstanceId, |
|||
components, |
|||
tables, |
|||
}) |
|||
|
|||
return [ |
|||
...result.bindableInstances |
|||
.filter(isInstanceInSharedContext(result)) |
|||
.map(componentInstanceToBindable), |
|||
...(result.target?._contexts.map(contextToBindables(tables)).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 = i => { |
|||
return { |
|||
type: "instance", |
|||
instance: i.instance, |
|||
// how the binding expression persists, and is used in the app at runtime
|
|||
runtimeBinding: `${i.instance._id}`, |
|||
// how the binding exressions looks to the user of the builder
|
|||
readableBinding: `${i.instance._instanceName}`, |
|||
} |
|||
} |
|||
|
|||
const contextToBindables = tables => context => { |
|||
const tableId = context.table?.tableId ?? context.table |
|||
const table = tables.find(table => table._id === tableId) |
|||
let schema = |
|||
context.table?.type === "view" |
|||
? table?.views?.[context.table.name]?.schema |
|||
: table?.schema |
|||
|
|||
// Avoid crashing whenever no data source has been selected
|
|||
if (!schema) { |
|||
return [] |
|||
} |
|||
|
|||
const newBindable = ([key, fieldSchema]) => { |
|||
// 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` |
|||
} |
|||
return { |
|||
type: "context", |
|||
fieldSchema, |
|||
instance: context.instance, |
|||
// how the binding expression persists, and is used in the app at runtime
|
|||
runtimeBinding: `${context.instance._id}.${runtimeBoundKey}`, |
|||
// how the binding expressions looks to the user of the builder
|
|||
readableBinding: `${context.instance._instanceName}.${table.name}.${key}`, |
|||
// table / view info
|
|||
table: context.table, |
|||
} |
|||
} |
|||
|
|||
const stringType = { type: "string" } |
|||
return ( |
|||
Object.entries(schema) |
|||
.map(newBindable) |
|||
// add _id and _rev fields - not part of schema, but always valid
|
|||
.concat([ |
|||
newBindable(["_id", stringType]), |
|||
newBindable(["_rev", stringType]), |
|||
]) |
|||
) |
|||
} |
|||
|
|||
const walk = ({ instance, targetId, components, tables, 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 table = instance[component.context] |
|||
result.currentContexts.push({ instance, table }) |
|||
} |
|||
|
|||
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, tables, result }) |
|||
} |
|||
|
|||
if (contextualInstance) { |
|||
// child walk done, remove from currentContexts
|
|||
result.currentContexts.pop() |
|||
} |
|||
|
|||
return result |
|||
} |
|||
@ -1,40 +0,0 @@ |
|||
export const CAPTURE_VAR_INSIDE_MUSTACHE = /{{([^}]+)}}/g |
|||
|
|||
export function readableToRuntimeBinding(bindableProperties, textWithBindings) { |
|||
// Find all instances of mustasche
|
|||
const boundValues = textWithBindings.match(CAPTURE_VAR_INSIDE_MUSTACHE) |
|||
|
|||
let result = textWithBindings |
|||
// Replace readableBindings with runtimeBindings
|
|||
boundValues && |
|||
boundValues.forEach(boundValue => { |
|||
const binding = bindableProperties.find(({ readableBinding }) => { |
|||
return boundValue === `{{ ${readableBinding} }}` |
|||
}) |
|||
if (binding) { |
|||
result = result.replace(boundValue, `{{ ${binding.runtimeBinding} }}`) |
|||
} |
|||
}) |
|||
return result |
|||
} |
|||
|
|||
export function runtimeToReadableBinding(bindableProperties, textWithBindings) { |
|||
let temp = textWithBindings |
|||
const boundValues = |
|||
(typeof textWithBindings === "string" && |
|||
textWithBindings.match(CAPTURE_VAR_INSIDE_MUSTACHE)) || |
|||
[] |
|||
|
|||
// Replace runtimeBindings with readableBindings:
|
|||
boundValues.forEach(v => { |
|||
const binding = bindableProperties.find(({ runtimeBinding }) => { |
|||
return v === `{{ ${runtimeBinding} }}` |
|||
}) |
|||
temp = temp.replace( |
|||
v, |
|||
`{{ ${binding?.readableBinding ?? "Invalid binding"} }}` |
|||
) |
|||
}) |
|||
|
|||
return temp |
|||
} |
|||
Loading…
Reference in new issue