From b2c790650be6c45f9eb9e03e6cf5ca36c5d91b1b Mon Sep 17 00:00:00 2001 From: deanhannigan Date: Mon, 22 Jan 2024 11:03:05 +0000 Subject: [PATCH] Revert "Global bindings" --- .../src/builderStore/componentUtils.js | 9 +- .../builder/src/builderStore/dataBinding.js | 367 ++++++++---------- .../src/builderStore/store/frontend.js | 19 +- .../ButtonActionEditor/actions/utils.js | 58 ++- .../controls/DataProviderSelect.svelte | 9 +- .../DataSourceSelect/DataSourceSelect.svelte | 15 +- packages/client/manifest.json | 72 +--- .../client/src/components/Component.svelte | 140 ++----- .../src/components/app/DataProvider.svelte | 2 +- .../components/app/EmptyPlaceholder.svelte | 21 +- .../src/components/app/GridBlock.svelte | 106 +++-- .../client/src/components/app/Repeater.svelte | 4 +- .../app/blocks/MultiStepFormblock.svelte | 9 +- .../components/app/blocks/TableBlock.svelte | 1 - .../app/blocks/form/FormBlockWrapper.svelte | 2 - .../src/components/app/forms/Form.svelte | 38 +- .../src/components/context/Provider.svelte | 19 +- packages/client/src/constants.js | 5 - packages/client/src/sdk.js | 3 +- packages/client/src/stores/context.js | 115 ++---- packages/client/src/utils/buttonActions.js | 147 ++++--- packages/client/src/utils/componentProps.js | 16 +- packages/frontend-core/src/api/rows.js | 10 +- 23 files changed, 503 insertions(+), 684 deletions(-) diff --git a/packages/builder/src/builderStore/componentUtils.js b/packages/builder/src/builderStore/componentUtils.js index 733ce0948e..a7ee3ff351 100644 --- a/packages/builder/src/builderStore/componentUtils.js +++ b/packages/builder/src/builderStore/componentUtils.js @@ -92,14 +92,7 @@ export const findAllMatchingComponents = (rootComponent, selector) => { } /** - * Recurses through the component tree and finds all components. - */ -export const findAllComponents = rootComponent => { - return findAllMatchingComponents(rootComponent, () => true) -} - -/** - * Finds the closest parent component which matches certain criteria + * Finds the closes parent component which matches certain criteria */ export const findClosestMatchingComponent = ( rootComponent, diff --git a/packages/builder/src/builderStore/dataBinding.js b/packages/builder/src/builderStore/dataBinding.js index 85d5046d8c..52368a0723 100644 --- a/packages/builder/src/builderStore/dataBinding.js +++ b/packages/builder/src/builderStore/dataBinding.js @@ -1,7 +1,6 @@ import { cloneDeep } from "lodash/fp" import { get } from "svelte/store" import { - findAllComponents, findAllMatchingComponents, findComponent, findComponentPath, @@ -103,9 +102,6 @@ export const getAuthBindings = () => { return bindings } -/** - * Gets all bindings for environment variables - */ export const getEnvironmentBindings = () => { let envVars = get(environment).variables return envVars.map(variable => { @@ -134,22 +130,26 @@ export const toBindingsArray = (valueMap, prefix, category) => { if (!binding) { return acc } + let config = { type: "context", runtimeBinding: binding, readableBinding: `${prefix}.${binding}`, icon: "Brackets", } + if (category) { config.category = category } + acc.push(config) + return acc }, []) } /** - * Utility to covert a map of readable bindings to runtime + * Utility - coverting a map of readable bindings to runtime */ export const readableToRuntimeMap = (bindings, ctx) => { if (!bindings || !ctx) { @@ -162,7 +162,7 @@ export const readableToRuntimeMap = (bindings, ctx) => { } /** - * Utility to covert a map of runtime bindings to readable bindings + * Utility - coverting a map of runtime bindings to readable */ export const runtimeToReadableMap = (bindings, ctx) => { if (!bindings || !ctx) { @@ -188,23 +188,15 @@ export const getComponentBindableProperties = (asset, componentId) => { if (!def?.context) { return [] } - const contexts = Array.isArray(def.context) ? def.context : [def.context] // Get the bindings for the component - const componentContext = { - component, - definition: def, - contexts, - } - return generateComponentContextBindings(asset, componentContext) + return getProviderContextBindings(asset, component) } /** - * Gets all component contexts available to a certain component. This handles - * both global and local bindings, taking into account a component's position - * in the component tree. + * Gets all data provider components above a component. */ -export const getComponentContexts = ( +export const getContextProviderComponents = ( asset, componentId, type, @@ -213,55 +205,30 @@ export const getComponentContexts = ( if (!asset || !componentId) { return [] } - let map = {} - // Processes all contexts exposed by a component - const processContexts = scope => component => { + // Get the component tree leading up to this component, ignoring the component + // itself + const path = findComponentPath(asset.props, componentId) + if (!options?.includeSelf) { + path.pop() + } + + // Filter by only data provider components + return path.filter(component => { const def = store.actions.components.getDefinition(component._component) if (!def?.context) { - return + return false } - if (!map[component._id]) { - map[component._id] = { - component, - definition: def, - contexts: [], - } + + // If no type specified, return anything that exposes context + if (!type) { + return true } + + // Otherwise only match components with the specific context type const contexts = Array.isArray(def.context) ? def.context : [def.context] - contexts.forEach(context => { - // Ensure type matches - if (type && context.type !== type) { - return - } - // Ensure scope matches - let contextScope = context.scope || "global" - if (contextScope !== scope) { - return - } - // Ensure the context is compatible with the component's current settings - if (!isContextCompatibleWithComponent(context, component)) { - return - } - map[component._id].contexts.push(context) - }) - } - - // Process all global contexts - const allComponents = findAllComponents(asset.props) - allComponents.forEach(processContexts("global")) - - // Process all local contexts - const localComponents = findComponentPath(asset.props, componentId) - localComponents.forEach(processContexts("local")) - - // Exclude self if required - if (!options?.includeSelf) { - delete map[componentId] - } - - // Only return components which provide at least 1 matching context - return Object.values(map).filter(x => x.contexts.length > 0) + return contexts.find(context => context.type === type) != null + }) } /** @@ -273,19 +240,20 @@ export const getActionProviders = ( actionType, options = { includeSelf: false } ) => { - if (!asset) { + if (!asset || !componentId) { return [] } - // Get all components - const components = findAllComponents(asset.props) + // Get the component tree leading up to this component, ignoring the component + // itself + const path = findComponentPath(asset.props, componentId) + if (!options?.includeSelf) { + path.pop() + } // Find matching contexts and generate bindings let providers = [] - components.forEach(component => { - if (!options?.includeSelf && component._id === componentId) { - return - } + path.forEach(component => { const def = store.actions.components.getDefinition(component._component) const actions = (def?.actions || []).map(action => { return typeof action === "string" ? { type: action } : action @@ -349,135 +317,142 @@ export const getDatasourceForProvider = (asset, component) => { * Gets all bindable data properties from component data contexts. */ const getContextBindings = (asset, componentId) => { - // Get all available contexts for this component - const componentContexts = getComponentContexts(asset, componentId) + // Extract any components which provide data contexts + const dataProviders = getContextProviderComponents(asset, componentId) - // Generate bindings for each context - return componentContexts - .map(componentContext => { - return generateComponentContextBindings(asset, componentContext) - }) - .flat() + // Generate bindings for all matching components + return getProviderContextBindings(asset, dataProviders) } /** - * Generates a set of bindings for a given component context + * Gets the context bindings exposed by a set of data provider components. */ -const generateComponentContextBindings = (asset, componentContext) => { - const { component, definition, contexts } = componentContext - if (!component || !definition || !contexts?.length) { +const getProviderContextBindings = (asset, dataProviders) => { + if (!asset || !dataProviders) { return [] } + // Ensure providers is an array + if (!Array.isArray(dataProviders)) { + dataProviders = [dataProviders] + } + // Create bindings for each data provider let bindings = [] - contexts.forEach(context => { - if (!context?.type) { - return - } + dataProviders.forEach(component => { + const def = store.actions.components.getDefinition(component._component) + const contexts = Array.isArray(def.context) ? def.context : [def.context] - let schema - let table - let readablePrefix - let runtimeSuffix = context.suffix - - if (context.type === "form") { - // Forms do not need table schemas - // Their schemas are built from their component field names - schema = buildFormSchema(component, asset) - readablePrefix = "Fields" - } else if (context.type === "static") { - // Static contexts are fully defined by the components - schema = {} - const values = context.values || [] - values.forEach(value => { - schema[value.key] = { - name: value.label, - type: value.type || "string", - } - }) - } else if (context.type === "schema") { - // Schema contexts are generated dynamically depending on their data - const datasource = getDatasourceForProvider(asset, component) - if (!datasource) { + // Create bindings for each context block provided by this data provider + contexts.forEach(context => { + if (!context?.type) { return } - const info = getSchemaForDatasource(asset, datasource) - schema = info.schema - table = info.table - // Determine what to prefix bindings with - if (datasource.type === "jsonarray") { - // For JSON arrays, use the array name as the readable prefix - const split = datasource.label.split(".") - readablePrefix = split[split.length - 1] - } else if (datasource.type === "viewV2") { - // For views, use the view name - const view = Object.values(table?.views || {}).find( - view => view.id === datasource.id + let schema + let table + let readablePrefix + let runtimeSuffix = context.suffix + + if (context.type === "form") { + // Forms do not need table schemas + // Their schemas are built from their component field names + schema = buildFormSchema(component, asset) + readablePrefix = "Fields" + } else if (context.type === "static") { + // Static contexts are fully defined by the components + schema = {} + const values = context.values || [] + values.forEach(value => { + schema[value.key] = { + name: value.label, + type: value.type || "string", + } + }) + } else if (context.type === "schema") { + // Schema contexts are generated dynamically depending on their data + const datasource = getDatasourceForProvider(asset, component) + if (!datasource) { + return + } + const info = getSchemaForDatasource(asset, datasource) + schema = info.schema + table = info.table + + // Determine what to prefix bindings with + if (datasource.type === "jsonarray") { + // For JSON arrays, use the array name as the readable prefix + const split = datasource.label.split(".") + readablePrefix = split[split.length - 1] + } else if (datasource.type === "viewV2") { + // For views, use the view name + const view = Object.values(table?.views || {}).find( + view => view.id === datasource.id + ) + readablePrefix = view?.name + } else { + // Otherwise use the table name + readablePrefix = info.table?.name + } + } + if (!schema) { + return + } + + const keys = Object.keys(schema).sort() + + // Generate safe unique runtime prefix + let providerId = component._id + if (runtimeSuffix) { + providerId += `-${runtimeSuffix}` + } + + if (!filterCategoryByContext(component, context)) { + return + } + + const safeComponentId = makePropSafe(providerId) + + // Create bindable properties for each schema field + keys.forEach(key => { + const fieldSchema = schema[key] + + // Make safe runtime binding + const safeKey = key.split(".").map(makePropSafe).join(".") + const runtimeBinding = `${safeComponentId}.${safeKey}` + + // Optionally use a prefix with readable bindings + let readableBinding = component._instanceName + if (readablePrefix) { + readableBinding += `.${readablePrefix}` + } + readableBinding += `.${fieldSchema.name || key}` + + const bindingCategory = getComponentBindingCategory( + component, + context, + def ) - readablePrefix = view?.name - } else { - // Otherwise use the table name - readablePrefix = info.table?.name - } - } - if (!schema) { - return - } - const keys = Object.keys(schema).sort() - - // Generate safe unique runtime prefix - let providerId = component._id - if (runtimeSuffix) { - providerId += `-${runtimeSuffix}` - } - const safeComponentId = makePropSafe(providerId) - - // Create bindable properties for each schema field - keys.forEach(key => { - const fieldSchema = schema[key] - - // Make safe runtime binding - const safeKey = key.split(".").map(makePropSafe).join(".") - const runtimeBinding = `${safeComponentId}.${safeKey}` - - // Optionally use a prefix with readable bindings - let readableBinding = component._instanceName - if (readablePrefix) { - readableBinding += `.${readablePrefix}` - } - readableBinding += `.${fieldSchema.name || key}` - - // Determine which category this binding belongs in - const bindingCategory = getComponentBindingCategory( - component, - context, - definition - ) - - // Temporarily append scope for debugging - const scope = `[${(context.scope || "global").toUpperCase()}]` - - // Create the binding object - bindings.push({ - type: "context", - runtimeBinding, - readableBinding: `${scope} ${readableBinding}`, - // Field schema and provider are required to construct relationship - // datasource options, based on bindable properties - fieldSchema, - providerId, - // Table ID is used by JSON fields to know what table the field is in - tableId: table?._id, - component: component._component, - category: bindingCategory.category, - icon: bindingCategory.icon, - display: { - name: `${scope} ${fieldSchema.name || key}`, - type: fieldSchema.type, - }, + // Create the binding object + bindings.push({ + type: "context", + runtimeBinding, + readableBinding, + // Field schema and provider are required to construct relationship + // datasource options, based on bindable properties + fieldSchema, + providerId, + // Table ID is used by JSON fields to know what table the field is in + tableId: table?._id, + component: component._component, + category: bindingCategory.category, + icon: bindingCategory.icon, + display: { + name: fieldSchema.name || key, + type: fieldSchema.type, + }, + }) }) }) }) @@ -485,38 +460,25 @@ const generateComponentContextBindings = (asset, componentContext) => { return bindings } -/** - * Checks if a certain data context is compatible with a certain instance of a - * configured component. - */ -const isContextCompatibleWithComponent = (context, component) => { - if (!component) { - return false - } - const { _component, actionType } = component - const { type } = context - - // Certain types of form blocks only allow certain contexts +// Exclude a data context based on the component settings +const filterCategoryByContext = (component, context) => { + const { _component } = component if (_component.endsWith("formblock")) { if ( - (actionType === "Create" && type === "schema") || - (actionType === "View" && type === "form") + (component.actionType === "Create" && context.type === "schema") || + (component.actionType === "View" && context.type === "form") ) { return false } } - - // Allow the context by default return true } // Enrich binding category information for certain components const getComponentBindingCategory = (component, context, def) => { - // Default category to component name let icon = def.icon let category = component._instanceName - // Form block edge case if (component._component.endsWith("formblock")) { if (context.type === "form") { category = `${component._instanceName} - Fields` @@ -534,7 +496,7 @@ const getComponentBindingCategory = (component, context, def) => { } /** - * Gets all bindable properties from the logged-in user. + * Gets all bindable properties from the logged in user. */ export const getUserBindings = () => { let bindings = [] @@ -604,7 +566,6 @@ const getDeviceBindings = () => { /** * Gets all selected rows bindings for tables in the current asset. - * TODO: remove in future because we don't need a separate store for this */ const getSelectedRowsBindings = asset => { let bindings = [] @@ -647,9 +608,6 @@ const getSelectedRowsBindings = asset => { return bindings } -/** - * Generates a state binding for a certain key name - */ export const makeStateBinding = key => { return { type: "context", @@ -704,9 +662,6 @@ const getUrlBindings = asset => { return urlParamBindings.concat([queryParamsBinding]) } -/** - * Generates all bindings for role IDs - */ const getRoleBindings = () => { return (get(rolesStore) || []).map(role => { return { diff --git a/packages/builder/src/builderStore/store/frontend.js b/packages/builder/src/builderStore/store/frontend.js index ce2cac9781..b05b127b1c 100644 --- a/packages/builder/src/builderStore/store/frontend.js +++ b/packages/builder/src/builderStore/store/frontend.js @@ -709,9 +709,10 @@ export const getFrontendStore = () => { else { if (setting.type === "dataProvider") { // Validate data provider exists, or else clear it - const providers = findAllMatchingComponents( - screen?.props, - component => component._component?.endsWith("/dataprovider") + const treeId = parent?._id || component._id + const path = findComponentPath(screen?.props, treeId) + const providers = path.filter(component => + component._component?.endsWith("/dataprovider") ) // Validate non-empty values const valid = providers?.some(dp => value.includes?.(dp._id)) @@ -733,16 +734,6 @@ export const getFrontendStore = () => { return null } - // Find all existing components of this type so that we can give this - // component a unique name - const screen = get(selectedScreen).props - const otherComponents = findAllMatchingComponents( - screen, - x => x._component === definition.component && x._id !== screen._id - ) - let name = definition.friendlyName || definition.name - name = `${name} ${otherComponents.length + 1}` - // Generate basic component structure let instance = { _id: Helpers.uuid(), @@ -752,7 +743,7 @@ export const getFrontendStore = () => { hover: {}, active: {}, }, - _instanceName: name, + _instanceName: `New ${definition.friendlyName || definition.name}`, ...presetProps, } diff --git a/packages/builder/src/components/design/settings/controls/ButtonActionEditor/actions/utils.js b/packages/builder/src/components/design/settings/controls/ButtonActionEditor/actions/utils.js index bdcd3a7838..aa076fdd3e 100644 --- a/packages/builder/src/components/design/settings/controls/ButtonActionEditor/actions/utils.js +++ b/packages/builder/src/components/design/settings/controls/ButtonActionEditor/actions/utils.js @@ -1,4 +1,5 @@ -import { getComponentContexts } from "builderStore/dataBinding" +import { getContextProviderComponents } from "builderStore/dataBinding" +import { store } from "builderStore" import { capitalise } from "helpers" // Generates bindings for all components that provider "datasource like" @@ -7,49 +8,58 @@ import { capitalise } from "helpers" // Some examples are saving rows or duplicating rows. export const getDatasourceLikeProviders = ({ asset, componentId, nested }) => { // Get all form context providers - const formComponentContexts = getComponentContexts( + const formComponents = getContextProviderComponents( asset, componentId, "form", - { - includeSelf: nested, - } + { includeSelf: nested } ) + // Get all schema context providers - const schemaComponentContexts = getComponentContexts( + const schemaComponents = getContextProviderComponents( asset, componentId, "schema", - { - includeSelf: nested, - } + { includeSelf: nested } ) + // Generate contexts for all form providers + const formContexts = formComponents.map(component => ({ + component, + context: extractComponentContext(component, "form"), + })) + + // Generate contexts for all schema providers + const schemaContexts = schemaComponents.map(component => ({ + component, + context: extractComponentContext(component, "schema"), + })) + // Check for duplicate contexts by the same component. In this case, attempt // to label contexts with their suffixes - schemaComponentContexts.forEach(schemaContext => { + schemaContexts.forEach(schemaContext => { // Check if we have a form context for this component const id = schemaContext.component._id - const existing = formComponentContexts.find(x => x.component._id === id) + const existing = formContexts.find(x => x.component._id === id) if (existing) { - if (existing.contexts[0].suffix) { - const suffix = capitalise(existing.contexts[0].suffix) + if (existing.context.suffix) { + const suffix = capitalise(existing.context.suffix) existing.readableSuffix = ` - ${suffix}` } - if (schemaContext.contexts[0].suffix) { - const suffix = capitalise(schemaContext.contexts[0].suffix) + if (schemaContext.context.suffix) { + const suffix = capitalise(schemaContext.context.suffix) schemaContext.readableSuffix = ` - ${suffix}` } } }) // Generate bindings for all contexts - const allContexts = formComponentContexts.concat(schemaComponentContexts) - return allContexts.map(({ component, contexts, readableSuffix }) => { + const allContexts = formContexts.concat(schemaContexts) + return allContexts.map(({ component, context, readableSuffix }) => { let readableBinding = component._instanceName let runtimeBinding = component._id - if (contexts[0].suffix) { - runtimeBinding += `-${contexts[0].suffix}` + if (context.suffix) { + runtimeBinding += `-${context.suffix}` } if (readableSuffix) { readableBinding += readableSuffix @@ -60,3 +70,13 @@ export const getDatasourceLikeProviders = ({ asset, componentId, nested }) => { } }) } + +// Gets a context definition of a certain type from a component definition +const extractComponentContext = (component, contextType) => { + const def = store.actions.components.getDefinition(component?._component) + if (!def) { + return null + } + const contexts = Array.isArray(def.context) ? def.context : [def.context] + return contexts.find(context => context?.type === contextType) +} diff --git a/packages/builder/src/components/design/settings/controls/DataProviderSelect.svelte b/packages/builder/src/components/design/settings/controls/DataProviderSelect.svelte index 9fd220e798..83255ec325 100644 --- a/packages/builder/src/components/design/settings/controls/DataProviderSelect.svelte +++ b/packages/builder/src/components/design/settings/controls/DataProviderSelect.svelte @@ -1,16 +1,15 @@