From cbe6459a9c63eb5db89d96561b3aecabbf5323b6 Mon Sep 17 00:00:00 2001 From: Andrew Kingston Date: Tue, 19 Jan 2021 15:39:04 +0000 Subject: [PATCH] Remove deprecated code around data binding --- .../builder/src/builderStore/dataBinding.js | 90 ++++++++-- .../builderStore/fetchBindableProperties.js | 168 ------------------ .../src/builderStore/replaceBindings.js | 40 ----- .../EventsEditor/actions/CreateRow.svelte | 23 +-- .../EventsEditor/actions/DeleteRow.svelte | 14 +- .../EventsEditor/actions/SaveFields.svelte | 41 +++-- .../EventsEditor/actions/SaveRow.svelte | 16 +- .../EventsEditor/actions/UpdateRow.svelte | 16 +- .../PropertyControls/PropertyControl.svelte | 5 +- .../PropertyControls/TableViewSelect.svelte | 1 - packages/standard-components/manifest.json | 2 + 11 files changed, 121 insertions(+), 295 deletions(-) delete mode 100644 packages/builder/src/builderStore/fetchBindableProperties.js delete mode 100644 packages/builder/src/builderStore/replaceBindings.js diff --git a/packages/builder/src/builderStore/dataBinding.js b/packages/builder/src/builderStore/dataBinding.js index 5b3e24211c..fb2bc62935 100644 --- a/packages/builder/src/builderStore/dataBinding.js +++ b/packages/builder/src/builderStore/dataBinding.js @@ -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 +} diff --git a/packages/builder/src/builderStore/fetchBindableProperties.js b/packages/builder/src/builderStore/fetchBindableProperties.js deleted file mode 100644 index 962fb80ebf..0000000000 --- a/packages/builder/src/builderStore/fetchBindableProperties.js +++ /dev/null @@ -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.} - */ -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 -} diff --git a/packages/builder/src/builderStore/replaceBindings.js b/packages/builder/src/builderStore/replaceBindings.js deleted file mode 100644 index a82e85d165..0000000000 --- a/packages/builder/src/builderStore/replaceBindings.js +++ /dev/null @@ -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 -} diff --git a/packages/builder/src/components/design/PropertiesPanel/PropertyControls/EventsEditor/actions/CreateRow.svelte b/packages/builder/src/components/design/PropertiesPanel/PropertyControls/EventsEditor/actions/CreateRow.svelte index 5ea1378d02..a23abfc9ca 100644 --- a/packages/builder/src/components/design/PropertiesPanel/PropertyControls/EventsEditor/actions/CreateRow.svelte +++ b/packages/builder/src/components/design/PropertiesPanel/PropertyControls/EventsEditor/actions/CreateRow.svelte @@ -1,24 +1,12 @@