Remove deprecated code around data binding
This commit is contained in:
parent
6030c4849e
commit
cbe6459a9c
|
@ -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
|
||||
}
|
||||
|
|
|
@ -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
|
||||
}
|
|
@ -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>
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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) {
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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"
|
||||
|
||||
|
|
|
@ -16,7 +16,6 @@
|
|||
|
||||
$: tables = $backendUiStore.tables.map(m => ({
|
||||
label: m.name,
|
||||
name: `all_${m._id}`,
|
||||
tableId: m._id,
|
||||
type: "table",
|
||||
}))
|
||||
|
|
|
@ -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",
|
||||
|
|
Loading…
Reference in New Issue