Remove deprecated code around data binding

This commit is contained in:
Andrew Kingston 2021-01-19 15:39:04 +00:00
parent 6030c4849e
commit cbe6459a9c
11 changed files with 121 additions and 295 deletions

View File

@ -2,6 +2,9 @@ import { get } from "svelte/store"
import { backendUiStore, store } from "builderStore"
import { findAllMatchingComponents, findComponentPath } from "./storeUtils"
// Regex to match mustache variables, for replacing bindings
const CAPTURE_VAR_INSIDE_MUSTACHE = /{{([^}]+)}}/g
/**
* Gets all bindable data context fields and instance fields.
*/
@ -20,22 +23,47 @@ export const getBindableContexts = (rootComponent, componentId) => {
return []
}
// Get the component tree leading up to this component
// Get the component tree leading up to this component, ignoring the component
// itself
const path = findComponentPath(rootComponent, componentId)
path.pop()
// Extract any components which provide data contexts
const dataProviders = path.filter(component => {
const def = store.actions.components.getDefinition(component._component)
return def?.dataProvider
})
// Enrich components with their definitions
const enriched = path.map(component => ({
instance: component,
definition: store.actions.components.getDefinition(component._component),
}))
// Extract any components which provide data contexts
const providers = enriched.filter(comp => comp.definition?.dataProvider)
let contexts = []
dataProviders.forEach(provider => {
if (!provider.datasource) {
providers.forEach(({ definition, instance }) => {
// Extract datasource from component instance
const datasourceSetting = definition.settings.find(setting => {
return setting.key === definition.datasourceSetting
})
if (!datasourceSetting) {
return
}
const { schema, table } = getSchemaForDatasource(provider.datasource)
// 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) {
return
}
const { schema, table } = getSchemaForDatasource(datasource)
const keys = Object.keys(schema).sort()
keys.forEach(key => {
const fieldSchema = schema[key]
@ -49,11 +77,11 @@ export const getBindableContexts = (rootComponent, componentId) => {
contexts.push({
type: "context",
runtimeBinding: `${provider._id}.${runtimeBoundKey}`,
readableBinding: `${provider._instanceName}.${table.name}.${key}`,
runtimeBinding: `${instance._id}.${runtimeBoundKey}`,
readableBinding: `${instance._instanceName}.${table.name}.${key}`,
fieldSchema,
providerId: provider._id,
tableId: provider.datasource.tableId,
providerId: instance._id,
tableId: datasource.tableId,
})
})
})
@ -110,3 +138,39 @@ const getSchemaForDatasource = datasource => {
}
return { schema, table }
}
/**
* 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
}

View File

@ -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
}

View File

@ -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
}

View File

@ -1,24 +1,12 @@
<script>
import { Select, Label } from "@budibase/bbui"
import { store, backendUiStore, currentAsset } from "builderStore"
import fetchBindableProperties from "builderStore/fetchBindableProperties"
import { backendUiStore } from "builderStore"
import SaveFields from "./SaveFields.svelte"
export let parameters
$: bindableProperties = fetchBindableProperties({
componentInstanceId: $store.selectedComponentId,
components: $store.components,
screen: $currentAsset,
tables: $backendUiStore.tables,
})
// just wraps binding in {{ ... }}
const toBindingExpression = bindingPath => `{{ ${bindingPath} }}`
const tableFields = tableId => {
const table = $backendUiStore.tables.find(m => m._id === tableId)
return Object.keys(table.schema).map(k => ({
name: k,
type: table.schema[k].type,
@ -58,17 +46,8 @@
grid-template-columns: auto 1fr auto 1fr auto;
align-items: baseline;
}
.root :global(> div:nth-child(2)) {
grid-column-start: 2;
grid-column-end: 6;
}
.cannot-use {
color: var(--red);
font-size: var(--font-size-s);
text-align: center;
width: 70%;
margin: auto;
}
</style>

View File

@ -1,24 +1,20 @@
<script>
import { Select, Label } from "@budibase/bbui"
import { store, backendUiStore, currentAsset } from "builderStore"
import fetchBindableProperties from "builderStore/fetchBindableProperties"
import { getBindableProperties } from "builderStore/dataBinding"
export let parameters
let idFields
$: bindableProperties = fetchBindableProperties({
componentInstanceId: $store.selectedComponentId,
components: $store.components,
screen: $currentAsset,
tables: $backendUiStore.tables,
})
$: bindableProperties = getBindableProperties(
$currentAsset.props,
$store.selectedComponentId
)
$: idFields = bindableProperties.filter(
bindable =>
bindable.type === "context" && bindable.runtimeBinding.endsWith("._id")
)
$: {
if (parameters.rowId) {
// Set rev ID

View File

@ -1,23 +1,34 @@
<script>
// accepts an array of field names, and outputs an object of { FieldName: value }
import { DataList, Label, TextButton, Spacer, Select, Input } from "@budibase/bbui"
import { store, backendUiStore, currentAsset } from "builderStore"
import fetchBindableProperties from "builderStore/fetchBindableProperties"
import { CloseCircleIcon, AddIcon } from "components/common/Icons"
import {
DataList,
Label,
TextButton,
Spacer,
Select,
Input,
} from "@budibase/bbui"
import { store, currentAsset } from "builderStore"
import {
getBindableProperties,
readableToRuntimeBinding,
runtimeToReadableBinding,
} from "builderStore/replaceBindings"
} from "builderStore/dataBinding"
import { CloseCircleIcon, AddIcon } from "components/common/Icons"
import { createEventDispatcher } from "svelte"
const dispatch = createEventDispatcher()
export let parameterFields
export let schemaFields
export let fieldLabel="Column"
export let fieldLabel = "Column"
const emptyField = () => ({ name: "", value: "" })
$: bindableProperties = getBindableProperties(
$currentAsset.props,
$store.selectedComponentId
)
// this statement initialises fields from parameters.fields
$: fields =
fields ||
@ -32,13 +43,6 @@
"",
}))
$: bindableProperties = fetchBindableProperties({
componentInstanceId: $store.selectedComponentId,
components: $store.components,
screen: $currentAsset,
tables: $backendUiStore.tables,
})
const addField = () => {
const newFields = fields.filter(f => f.name)
newFields.push(emptyField())
@ -60,7 +64,9 @@
// value and type is needed by the client, so it can parse
// a string into a correct type
newParameterFields[field.name] = {
type: schemaFields ? schemaFields.find(f => f.name === field.name).type : "string",
type: schemaFields
? schemaFields.find(f => f.name === field.name).type
: "string",
value: readableToRuntimeBinding(bindableProperties, field.value),
}
}
@ -83,7 +89,7 @@
{/each}
</Select>
{:else}
<Input secondary bind:value={field.name} on:blur={rebuildParameters}/>
<Input secondary bind:value={field.name} on:blur={rebuildParameters} />
{/if}
<Label size="m" color="dark">Value</Label>
<DataList secondary bind:value={field.value} on:blur={rebuildParameters}>
@ -105,7 +111,8 @@
<Spacer small />
<TextButton text small blue on:click={addField}>
Add {fieldLabel}
Add
{fieldLabel}
<div style="height: 20px; width: 20px;">
<AddIcon />
</div>

View File

@ -1,12 +1,8 @@
<script>
import { Select, Label } from "@budibase/bbui"
import { store, backendUiStore, currentAsset } from "builderStore"
import fetchBindableProperties from "builderStore/fetchBindableProperties"
import { getBindableProperties } from "builderStore/dataBinding"
import SaveFields from "./SaveFields.svelte"
import {
readableToRuntimeBinding,
runtimeToReadableBinding,
} from "builderStore/replaceBindings"
// parameters.contextPath used in the client handler to determine which row to save
// this could be "data" or "data.parent", "data.parent.parent" etc
@ -15,12 +11,10 @@
let idFields
let schemaFields
$: bindableProperties = fetchBindableProperties({
componentInstanceId: $store.selectedComponentId,
components: $store.components,
screen: $currentAsset,
tables: $backendUiStore.tables,
})
$: bindableProperties = getBindableProperties(
$currentAsset.props,
$store.selectedComponentId
)
$: {
if (parameters && parameters.contextPath) {

View File

@ -1,21 +1,15 @@
<script>
import { Select, Label } from "@budibase/bbui"
import { store, backendUiStore, currentAsset } from "builderStore"
import fetchBindableProperties from "builderStore/fetchBindableProperties"
import { getBindableProperties } from "builderStore/dataBinding"
import SaveFields from "./SaveFields.svelte"
import {
readableToRuntimeBinding,
runtimeToReadableBinding,
} from "builderStore/replaceBindings"
export let parameters
$: bindableProperties = fetchBindableProperties({
componentInstanceId: $store.selectedComponentId,
components: $store.components,
screen: $currentAsset,
tables: $backendUiStore.tables,
})
$: bindableProperties = getBindableProperties(
$currentAsset.props,
$store.selectedComponentId
)
let idFields
let rowId

View File

@ -1,12 +1,11 @@
<script>
import { Icon } from "@budibase/bbui"
import Input from "./Input.svelte"
import { store, currentAsset } from "builderStore"
import { getBindableProperties } from "builderStore/dataBinding"
import {
getBindableProperties,
readableToRuntimeBinding,
runtimeToReadableBinding,
} from "builderStore/replaceBindings"
} from "builderStore/dataBinding"
import { DropdownMenu } from "@budibase/bbui"
import BindingDropdown from "./BindingDropdown.svelte"

View File

@ -16,7 +16,6 @@
$: tables = $backendUiStore.tables.map(m => ({
label: m.name,
name: `all_${m._id}`,
tableId: m._id,
type: "table",
}))

View File

@ -106,6 +106,7 @@
"styleable": true,
"hasChildren": true,
"dataProvider": true,
"datasourceSetting": "datasource",
"settings": [
{
"type": "datasource",
@ -410,6 +411,7 @@
"styleable": true,
"hasChildren": true,
"dataProvider": true,
"datasourceSetting": "table",
"settings": [
{
"type": "table",