Merge pull request #12833 from Budibase/revert-12832-revert-11830-global-bindings
Global bindings
This commit is contained in:
commit
97a5a34af2
24 changed files with 692 additions and 511 deletions
|
@ -1 +1 @@
|
||||||
Subproject commit 41006f147ea40a8424a9e4e66c1d0570b82d79e7
|
Subproject commit 64290ce8957d093bc997190402922df10d092953
|
|
@ -92,7 +92,14 @@ export const findAllMatchingComponents = (rootComponent, selector) => {
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Finds the closes parent component which matches certain criteria
|
* 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
|
||||||
*/
|
*/
|
||||||
export const findClosestMatchingComponent = (
|
export const findClosestMatchingComponent = (
|
||||||
rootComponent,
|
rootComponent,
|
||||||
|
|
|
@ -1,6 +1,7 @@
|
||||||
import { cloneDeep } from "lodash/fp"
|
import { cloneDeep } from "lodash/fp"
|
||||||
import { get } from "svelte/store"
|
import { get } from "svelte/store"
|
||||||
import {
|
import {
|
||||||
|
findAllComponents,
|
||||||
findAllMatchingComponents,
|
findAllMatchingComponents,
|
||||||
findComponent,
|
findComponent,
|
||||||
findComponentPath,
|
findComponentPath,
|
||||||
|
@ -102,6 +103,9 @@ export const getAuthBindings = () => {
|
||||||
return bindings
|
return bindings
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Gets all bindings for environment variables
|
||||||
|
*/
|
||||||
export const getEnvironmentBindings = () => {
|
export const getEnvironmentBindings = () => {
|
||||||
let envVars = get(environment).variables
|
let envVars = get(environment).variables
|
||||||
return envVars.map(variable => {
|
return envVars.map(variable => {
|
||||||
|
@ -130,26 +134,22 @@ export const toBindingsArray = (valueMap, prefix, category) => {
|
||||||
if (!binding) {
|
if (!binding) {
|
||||||
return acc
|
return acc
|
||||||
}
|
}
|
||||||
|
|
||||||
let config = {
|
let config = {
|
||||||
type: "context",
|
type: "context",
|
||||||
runtimeBinding: binding,
|
runtimeBinding: binding,
|
||||||
readableBinding: `${prefix}.${binding}`,
|
readableBinding: `${prefix}.${binding}`,
|
||||||
icon: "Brackets",
|
icon: "Brackets",
|
||||||
}
|
}
|
||||||
|
|
||||||
if (category) {
|
if (category) {
|
||||||
config.category = category
|
config.category = category
|
||||||
}
|
}
|
||||||
|
|
||||||
acc.push(config)
|
acc.push(config)
|
||||||
|
|
||||||
return acc
|
return acc
|
||||||
}, [])
|
}, [])
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Utility - coverting a map of readable bindings to runtime
|
* Utility to covert a map of readable bindings to runtime
|
||||||
*/
|
*/
|
||||||
export const readableToRuntimeMap = (bindings, ctx) => {
|
export const readableToRuntimeMap = (bindings, ctx) => {
|
||||||
if (!bindings || !ctx) {
|
if (!bindings || !ctx) {
|
||||||
|
@ -162,7 +162,7 @@ export const readableToRuntimeMap = (bindings, ctx) => {
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Utility - coverting a map of runtime bindings to readable
|
* Utility to covert a map of runtime bindings to readable bindings
|
||||||
*/
|
*/
|
||||||
export const runtimeToReadableMap = (bindings, ctx) => {
|
export const runtimeToReadableMap = (bindings, ctx) => {
|
||||||
if (!bindings || !ctx) {
|
if (!bindings || !ctx) {
|
||||||
|
@ -188,15 +188,23 @@ export const getComponentBindableProperties = (asset, componentId) => {
|
||||||
if (!def?.context) {
|
if (!def?.context) {
|
||||||
return []
|
return []
|
||||||
}
|
}
|
||||||
|
const contexts = Array.isArray(def.context) ? def.context : [def.context]
|
||||||
|
|
||||||
// Get the bindings for the component
|
// Get the bindings for the component
|
||||||
return getProviderContextBindings(asset, component)
|
const componentContext = {
|
||||||
|
component,
|
||||||
|
definition: def,
|
||||||
|
contexts,
|
||||||
|
}
|
||||||
|
return generateComponentContextBindings(asset, componentContext)
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Gets all data provider components above a 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.
|
||||||
*/
|
*/
|
||||||
export const getContextProviderComponents = (
|
export const getComponentContexts = (
|
||||||
asset,
|
asset,
|
||||||
componentId,
|
componentId,
|
||||||
type,
|
type,
|
||||||
|
@ -205,30 +213,55 @@ export const getContextProviderComponents = (
|
||||||
if (!asset || !componentId) {
|
if (!asset || !componentId) {
|
||||||
return []
|
return []
|
||||||
}
|
}
|
||||||
|
let map = {}
|
||||||
|
|
||||||
// Get the component tree leading up to this component, ignoring the component
|
// Processes all contexts exposed by a component
|
||||||
// itself
|
const processContexts = scope => component => {
|
||||||
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)
|
const def = store.actions.components.getDefinition(component._component)
|
||||||
if (!def?.context) {
|
if (!def?.context) {
|
||||||
return false
|
return
|
||||||
|
}
|
||||||
|
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]
|
const contexts = Array.isArray(def.context) ? def.context : [def.context]
|
||||||
return contexts.find(context => context.type === type) != null
|
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)
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -240,20 +273,19 @@ export const getActionProviders = (
|
||||||
actionType,
|
actionType,
|
||||||
options = { includeSelf: false }
|
options = { includeSelf: false }
|
||||||
) => {
|
) => {
|
||||||
if (!asset || !componentId) {
|
if (!asset) {
|
||||||
return []
|
return []
|
||||||
}
|
}
|
||||||
|
|
||||||
// Get the component tree leading up to this component, ignoring the component
|
// Get all components
|
||||||
// itself
|
const components = findAllComponents(asset.props)
|
||||||
const path = findComponentPath(asset.props, componentId)
|
|
||||||
if (!options?.includeSelf) {
|
|
||||||
path.pop()
|
|
||||||
}
|
|
||||||
|
|
||||||
// Find matching contexts and generate bindings
|
// Find matching contexts and generate bindings
|
||||||
let providers = []
|
let providers = []
|
||||||
path.forEach(component => {
|
components.forEach(component => {
|
||||||
|
if (!options?.includeSelf && component._id === componentId) {
|
||||||
|
return
|
||||||
|
}
|
||||||
const def = store.actions.components.getDefinition(component._component)
|
const def = store.actions.components.getDefinition(component._component)
|
||||||
const actions = (def?.actions || []).map(action => {
|
const actions = (def?.actions || []).map(action => {
|
||||||
return typeof action === "string" ? { type: action } : action
|
return typeof action === "string" ? { type: action } : action
|
||||||
|
@ -317,33 +349,29 @@ export const getDatasourceForProvider = (asset, component) => {
|
||||||
* Gets all bindable data properties from component data contexts.
|
* Gets all bindable data properties from component data contexts.
|
||||||
*/
|
*/
|
||||||
const getContextBindings = (asset, componentId) => {
|
const getContextBindings = (asset, componentId) => {
|
||||||
// Extract any components which provide data contexts
|
// Get all available contexts for this component
|
||||||
const dataProviders = getContextProviderComponents(asset, componentId)
|
const componentContexts = getComponentContexts(asset, componentId)
|
||||||
|
|
||||||
// Generate bindings for all matching components
|
// Generate bindings for each context
|
||||||
return getProviderContextBindings(asset, dataProviders)
|
return componentContexts
|
||||||
|
.map(componentContext => {
|
||||||
|
return generateComponentContextBindings(asset, componentContext)
|
||||||
|
})
|
||||||
|
.flat()
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Gets the context bindings exposed by a set of data provider components.
|
* Generates a set of bindings for a given component context
|
||||||
*/
|
*/
|
||||||
const getProviderContextBindings = (asset, dataProviders) => {
|
const generateComponentContextBindings = (asset, componentContext) => {
|
||||||
if (!asset || !dataProviders) {
|
console.log("Hello ")
|
||||||
|
const { component, definition, contexts } = componentContext
|
||||||
|
if (!component || !definition || !contexts?.length) {
|
||||||
return []
|
return []
|
||||||
}
|
}
|
||||||
|
|
||||||
// Ensure providers is an array
|
|
||||||
if (!Array.isArray(dataProviders)) {
|
|
||||||
dataProviders = [dataProviders]
|
|
||||||
}
|
|
||||||
|
|
||||||
// Create bindings for each data provider
|
// Create bindings for each data provider
|
||||||
let bindings = []
|
let bindings = []
|
||||||
dataProviders.forEach(component => {
|
|
||||||
const def = store.actions.components.getDefinition(component._component)
|
|
||||||
const contexts = Array.isArray(def.context) ? def.context : [def.context]
|
|
||||||
|
|
||||||
// Create bindings for each context block provided by this data provider
|
|
||||||
contexts.forEach(context => {
|
contexts.forEach(context => {
|
||||||
if (!context?.type) {
|
if (!context?.type) {
|
||||||
return
|
return
|
||||||
|
@ -406,11 +434,6 @@ const getProviderContextBindings = (asset, dataProviders) => {
|
||||||
if (runtimeSuffix) {
|
if (runtimeSuffix) {
|
||||||
providerId += `-${runtimeSuffix}`
|
providerId += `-${runtimeSuffix}`
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!filterCategoryByContext(component, context)) {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
const safeComponentId = makePropSafe(providerId)
|
const safeComponentId = makePropSafe(providerId)
|
||||||
|
|
||||||
// Create bindable properties for each schema field
|
// Create bindable properties for each schema field
|
||||||
|
@ -428,17 +451,17 @@ const getProviderContextBindings = (asset, dataProviders) => {
|
||||||
}
|
}
|
||||||
readableBinding += `.${fieldSchema.name || key}`
|
readableBinding += `.${fieldSchema.name || key}`
|
||||||
|
|
||||||
|
// Determine which category this binding belongs in
|
||||||
const bindingCategory = getComponentBindingCategory(
|
const bindingCategory = getComponentBindingCategory(
|
||||||
component,
|
component,
|
||||||
context,
|
context,
|
||||||
def
|
definition
|
||||||
)
|
)
|
||||||
|
|
||||||
// Create the binding object
|
// Create the binding object
|
||||||
bindings.push({
|
bindings.push({
|
||||||
type: "context",
|
type: "context",
|
||||||
runtimeBinding,
|
runtimeBinding,
|
||||||
readableBinding,
|
readableBinding: `${readableBinding}`,
|
||||||
// Field schema and provider are required to construct relationship
|
// Field schema and provider are required to construct relationship
|
||||||
// datasource options, based on bindable properties
|
// datasource options, based on bindable properties
|
||||||
fieldSchema,
|
fieldSchema,
|
||||||
|
@ -449,36 +472,48 @@ const getProviderContextBindings = (asset, dataProviders) => {
|
||||||
category: bindingCategory.category,
|
category: bindingCategory.category,
|
||||||
icon: bindingCategory.icon,
|
icon: bindingCategory.icon,
|
||||||
display: {
|
display: {
|
||||||
name: fieldSchema.name || key,
|
name: `${fieldSchema.name || key}`,
|
||||||
type: fieldSchema.type,
|
type: fieldSchema.type,
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
})
|
|
||||||
|
|
||||||
return bindings
|
return bindings
|
||||||
}
|
}
|
||||||
|
|
||||||
// Exclude a data context based on the component settings
|
/**
|
||||||
const filterCategoryByContext = (component, context) => {
|
* Checks if a certain data context is compatible with a certain instance of a
|
||||||
const { _component } = component
|
* 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
|
||||||
if (_component.endsWith("formblock")) {
|
if (_component.endsWith("formblock")) {
|
||||||
if (
|
if (
|
||||||
(component.actionType === "Create" && context.type === "schema") ||
|
(actionType === "Create" && type === "schema") ||
|
||||||
(component.actionType === "View" && context.type === "form")
|
(actionType === "View" && type === "form")
|
||||||
) {
|
) {
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Allow the context by default
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
// Enrich binding category information for certain components
|
// Enrich binding category information for certain components
|
||||||
const getComponentBindingCategory = (component, context, def) => {
|
const getComponentBindingCategory = (component, context, def) => {
|
||||||
|
// Default category to component name
|
||||||
let icon = def.icon
|
let icon = def.icon
|
||||||
let category = component._instanceName
|
let category = component._instanceName
|
||||||
|
|
||||||
|
// Form block edge case
|
||||||
if (component._component.endsWith("formblock")) {
|
if (component._component.endsWith("formblock")) {
|
||||||
if (context.type === "form") {
|
if (context.type === "form") {
|
||||||
category = `${component._instanceName} - Fields`
|
category = `${component._instanceName} - Fields`
|
||||||
|
@ -496,7 +531,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 = () => {
|
export const getUserBindings = () => {
|
||||||
let bindings = []
|
let bindings = []
|
||||||
|
@ -566,6 +601,7 @@ const getDeviceBindings = () => {
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Gets all selected rows bindings for tables in the current asset.
|
* 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 => {
|
const getSelectedRowsBindings = asset => {
|
||||||
let bindings = []
|
let bindings = []
|
||||||
|
@ -608,6 +644,9 @@ const getSelectedRowsBindings = asset => {
|
||||||
return bindings
|
return bindings
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Generates a state binding for a certain key name
|
||||||
|
*/
|
||||||
export const makeStateBinding = key => {
|
export const makeStateBinding = key => {
|
||||||
return {
|
return {
|
||||||
type: "context",
|
type: "context",
|
||||||
|
@ -662,6 +701,9 @@ const getUrlBindings = asset => {
|
||||||
return urlParamBindings.concat([queryParamsBinding])
|
return urlParamBindings.concat([queryParamsBinding])
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Generates all bindings for role IDs
|
||||||
|
*/
|
||||||
const getRoleBindings = () => {
|
const getRoleBindings = () => {
|
||||||
return (get(rolesStore) || []).map(role => {
|
return (get(rolesStore) || []).map(role => {
|
||||||
return {
|
return {
|
||||||
|
|
|
@ -706,10 +706,9 @@ export const getFrontendStore = () => {
|
||||||
else {
|
else {
|
||||||
if (setting.type === "dataProvider") {
|
if (setting.type === "dataProvider") {
|
||||||
// Validate data provider exists, or else clear it
|
// Validate data provider exists, or else clear it
|
||||||
const treeId = parent?._id || component._id
|
const providers = findAllMatchingComponents(
|
||||||
const path = findComponentPath(screen?.props, treeId)
|
screen?.props,
|
||||||
const providers = path.filter(component =>
|
component => component._component?.endsWith("/dataprovider")
|
||||||
component._component?.endsWith("/dataprovider")
|
|
||||||
)
|
)
|
||||||
// Validate non-empty values
|
// Validate non-empty values
|
||||||
const valid = providers?.some(dp => value.includes?.(dp._id))
|
const valid = providers?.some(dp => value.includes?.(dp._id))
|
||||||
|
@ -731,6 +730,16 @@ export const getFrontendStore = () => {
|
||||||
return null
|
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
|
// Generate basic component structure
|
||||||
let instance = {
|
let instance = {
|
||||||
_id: Helpers.uuid(),
|
_id: Helpers.uuid(),
|
||||||
|
@ -740,7 +749,7 @@ export const getFrontendStore = () => {
|
||||||
hover: {},
|
hover: {},
|
||||||
active: {},
|
active: {},
|
||||||
},
|
},
|
||||||
_instanceName: `New ${definition.friendlyName || definition.name}`,
|
_instanceName: name,
|
||||||
...presetProps,
|
...presetProps,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -1,5 +1,4 @@
|
||||||
import { getContextProviderComponents } from "builderStore/dataBinding"
|
import { getComponentContexts } from "builderStore/dataBinding"
|
||||||
import { store } from "builderStore"
|
|
||||||
import { capitalise } from "helpers"
|
import { capitalise } from "helpers"
|
||||||
|
|
||||||
// Generates bindings for all components that provider "datasource like"
|
// Generates bindings for all components that provider "datasource like"
|
||||||
|
@ -8,58 +7,49 @@ import { capitalise } from "helpers"
|
||||||
// Some examples are saving rows or duplicating rows.
|
// Some examples are saving rows or duplicating rows.
|
||||||
export const getDatasourceLikeProviders = ({ asset, componentId, nested }) => {
|
export const getDatasourceLikeProviders = ({ asset, componentId, nested }) => {
|
||||||
// Get all form context providers
|
// Get all form context providers
|
||||||
const formComponents = getContextProviderComponents(
|
const formComponentContexts = getComponentContexts(
|
||||||
asset,
|
asset,
|
||||||
componentId,
|
componentId,
|
||||||
"form",
|
"form",
|
||||||
{ includeSelf: nested }
|
{
|
||||||
|
includeSelf: nested,
|
||||||
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
// Get all schema context providers
|
// Get all schema context providers
|
||||||
const schemaComponents = getContextProviderComponents(
|
const schemaComponentContexts = getComponentContexts(
|
||||||
asset,
|
asset,
|
||||||
componentId,
|
componentId,
|
||||||
"schema",
|
"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
|
// Check for duplicate contexts by the same component. In this case, attempt
|
||||||
// to label contexts with their suffixes
|
// to label contexts with their suffixes
|
||||||
schemaContexts.forEach(schemaContext => {
|
schemaComponentContexts.forEach(schemaContext => {
|
||||||
// Check if we have a form context for this component
|
// Check if we have a form context for this component
|
||||||
const id = schemaContext.component._id
|
const id = schemaContext.component._id
|
||||||
const existing = formContexts.find(x => x.component._id === id)
|
const existing = formComponentContexts.find(x => x.component._id === id)
|
||||||
if (existing) {
|
if (existing) {
|
||||||
if (existing.context.suffix) {
|
if (existing.contexts[0].suffix) {
|
||||||
const suffix = capitalise(existing.context.suffix)
|
const suffix = capitalise(existing.contexts[0].suffix)
|
||||||
existing.readableSuffix = ` - ${suffix}`
|
existing.readableSuffix = ` - ${suffix}`
|
||||||
}
|
}
|
||||||
if (schemaContext.context.suffix) {
|
if (schemaContext.contexts[0].suffix) {
|
||||||
const suffix = capitalise(schemaContext.context.suffix)
|
const suffix = capitalise(schemaContext.contexts[0].suffix)
|
||||||
schemaContext.readableSuffix = ` - ${suffix}`
|
schemaContext.readableSuffix = ` - ${suffix}`
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
// Generate bindings for all contexts
|
// Generate bindings for all contexts
|
||||||
const allContexts = formContexts.concat(schemaContexts)
|
const allContexts = formComponentContexts.concat(schemaComponentContexts)
|
||||||
return allContexts.map(({ component, context, readableSuffix }) => {
|
return allContexts.map(({ component, contexts, readableSuffix }) => {
|
||||||
let readableBinding = component._instanceName
|
let readableBinding = component._instanceName
|
||||||
let runtimeBinding = component._id
|
let runtimeBinding = component._id
|
||||||
if (context.suffix) {
|
if (contexts[0].suffix) {
|
||||||
runtimeBinding += `-${context.suffix}`
|
runtimeBinding += `-${contexts[0].suffix}`
|
||||||
}
|
}
|
||||||
if (readableSuffix) {
|
if (readableSuffix) {
|
||||||
readableBinding += readableSuffix
|
readableBinding += readableSuffix
|
||||||
|
@ -70,13 +60,3 @@ 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)
|
|
||||||
}
|
|
||||||
|
|
|
@ -1,15 +1,16 @@
|
||||||
<script>
|
<script>
|
||||||
import { Select } from "@budibase/bbui"
|
import { Select } from "@budibase/bbui"
|
||||||
import { makePropSafe } from "@budibase/string-templates"
|
import { makePropSafe } from "@budibase/string-templates"
|
||||||
import { currentAsset, store } from "builderStore"
|
import { currentAsset } from "builderStore"
|
||||||
import { findComponentPath } from "builderStore/componentUtils"
|
import { findAllMatchingComponents } from "builderStore/componentUtils"
|
||||||
|
|
||||||
export let value
|
export let value
|
||||||
|
|
||||||
const getValue = component => `{{ literal ${makePropSafe(component._id)} }}`
|
const getValue = component => `{{ literal ${makePropSafe(component._id)} }}`
|
||||||
|
|
||||||
$: path = findComponentPath($currentAsset?.props, $store.selectedComponentId)
|
$: providers = findAllMatchingComponents($currentAsset?.props, c =>
|
||||||
$: providers = path.filter(c => c._component?.endsWith("/dataprovider"))
|
c._component?.endsWith("/dataprovider")
|
||||||
|
)
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<Select
|
<Select
|
||||||
|
|
|
@ -1,6 +1,5 @@
|
||||||
<script>
|
<script>
|
||||||
import {
|
import {
|
||||||
getContextProviderComponents,
|
|
||||||
readableToRuntimeBinding,
|
readableToRuntimeBinding,
|
||||||
runtimeToReadableBinding,
|
runtimeToReadableBinding,
|
||||||
} from "builderStore/dataBinding"
|
} from "builderStore/dataBinding"
|
||||||
|
@ -30,6 +29,7 @@
|
||||||
import BindingBuilder from "components/integration/QueryBindingBuilder.svelte"
|
import BindingBuilder from "components/integration/QueryBindingBuilder.svelte"
|
||||||
import IntegrationQueryEditor from "components/integration/index.svelte"
|
import IntegrationQueryEditor from "components/integration/index.svelte"
|
||||||
import { makePropSafe as safe } from "@budibase/string-templates"
|
import { makePropSafe as safe } from "@budibase/string-templates"
|
||||||
|
import { findAllComponents } from "builderStore/componentUtils"
|
||||||
import ClientBindingPanel from "components/common/bindings/ClientBindingPanel.svelte"
|
import ClientBindingPanel from "components/common/bindings/ClientBindingPanel.svelte"
|
||||||
import DataSourceCategory from "components/design/settings/controls/DataSourceSelect/DataSourceCategory.svelte"
|
import DataSourceCategory from "components/design/settings/controls/DataSourceSelect/DataSourceCategory.svelte"
|
||||||
import { API } from "api"
|
import { API } from "api"
|
||||||
|
@ -75,12 +75,13 @@
|
||||||
...query,
|
...query,
|
||||||
type: "query",
|
type: "query",
|
||||||
}))
|
}))
|
||||||
$: contextProviders = getContextProviderComponents(
|
$: dataProviders = findAllComponents($currentAsset.props)
|
||||||
$currentAsset,
|
.filter(component => {
|
||||||
$store.selectedComponentId
|
return (
|
||||||
|
component._component?.endsWith("/dataprovider") &&
|
||||||
|
component._id !== $store.selectedComponentId
|
||||||
)
|
)
|
||||||
$: dataProviders = contextProviders
|
})
|
||||||
.filter(component => component._component?.endsWith("/dataprovider"))
|
|
||||||
.map(provider => ({
|
.map(provider => ({
|
||||||
label: provider._instanceName,
|
label: provider._instanceName,
|
||||||
name: provider._instanceName,
|
name: provider._instanceName,
|
||||||
|
|
|
@ -573,7 +573,6 @@
|
||||||
"description": "A configurable data list that attaches to your backend tables.",
|
"description": "A configurable data list that attaches to your backend tables.",
|
||||||
"icon": "JourneyData",
|
"icon": "JourneyData",
|
||||||
"illegalChildren": ["section"],
|
"illegalChildren": ["section"],
|
||||||
"requiredAncestors": ["dataprovider"],
|
|
||||||
"hasChildren": true,
|
"hasChildren": true,
|
||||||
"size": {
|
"size": {
|
||||||
"width": 400,
|
"width": 400,
|
||||||
|
@ -711,10 +710,12 @@
|
||||||
],
|
],
|
||||||
"context": [
|
"context": [
|
||||||
{
|
{
|
||||||
"type": "schema"
|
"type": "schema",
|
||||||
|
"scope": "local"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"type": "static",
|
"type": "static",
|
||||||
|
"scope": "local",
|
||||||
"values": [
|
"values": [
|
||||||
{
|
{
|
||||||
"label": "Row index",
|
"label": "Row index",
|
||||||
|
@ -1564,7 +1565,6 @@
|
||||||
"name": "Bar Chart",
|
"name": "Bar Chart",
|
||||||
"description": "Bar chart",
|
"description": "Bar chart",
|
||||||
"icon": "GraphBarVertical",
|
"icon": "GraphBarVertical",
|
||||||
"requiredAncestors": ["dataprovider"],
|
|
||||||
"size": {
|
"size": {
|
||||||
"width": 600,
|
"width": 600,
|
||||||
"height": 400
|
"height": 400
|
||||||
|
@ -1727,7 +1727,6 @@
|
||||||
"width": 600,
|
"width": 600,
|
||||||
"height": 400
|
"height": 400
|
||||||
},
|
},
|
||||||
"requiredAncestors": ["dataprovider"],
|
|
||||||
"settings": [
|
"settings": [
|
||||||
{
|
{
|
||||||
"type": "text",
|
"type": "text",
|
||||||
|
@ -1881,7 +1880,6 @@
|
||||||
"width": 600,
|
"width": 600,
|
||||||
"height": 400
|
"height": 400
|
||||||
},
|
},
|
||||||
"requiredAncestors": ["dataprovider"],
|
|
||||||
"settings": [
|
"settings": [
|
||||||
{
|
{
|
||||||
"type": "text",
|
"type": "text",
|
||||||
|
@ -2047,7 +2045,6 @@
|
||||||
"width": 600,
|
"width": 600,
|
||||||
"height": 400
|
"height": 400
|
||||||
},
|
},
|
||||||
"requiredAncestors": ["dataprovider"],
|
|
||||||
"settings": [
|
"settings": [
|
||||||
{
|
{
|
||||||
"type": "text",
|
"type": "text",
|
||||||
|
@ -2177,7 +2174,6 @@
|
||||||
"width": 600,
|
"width": 600,
|
||||||
"height": 400
|
"height": 400
|
||||||
},
|
},
|
||||||
"requiredAncestors": ["dataprovider"],
|
|
||||||
"settings": [
|
"settings": [
|
||||||
{
|
{
|
||||||
"type": "text",
|
"type": "text",
|
||||||
|
@ -2307,7 +2303,6 @@
|
||||||
"width": 600,
|
"width": 600,
|
||||||
"height": 400
|
"height": 400
|
||||||
},
|
},
|
||||||
"requiredAncestors": ["dataprovider"],
|
|
||||||
"settings": [
|
"settings": [
|
||||||
{
|
{
|
||||||
"type": "text",
|
"type": "text",
|
||||||
|
@ -4087,7 +4082,6 @@
|
||||||
"width": 400,
|
"width": 400,
|
||||||
"height": 320
|
"height": 320
|
||||||
},
|
},
|
||||||
"requiredAncestors": ["dataprovider"],
|
|
||||||
"settings": [
|
"settings": [
|
||||||
{
|
{
|
||||||
"type": "dataProvider",
|
"type": "dataProvider",
|
||||||
|
@ -4643,7 +4637,6 @@
|
||||||
"name": "Table",
|
"name": "Table",
|
||||||
"icon": "Table",
|
"icon": "Table",
|
||||||
"illegalChildren": ["section"],
|
"illegalChildren": ["section"],
|
||||||
"requiredAncestors": ["dataprovider"],
|
|
||||||
"hasChildren": true,
|
"hasChildren": true,
|
||||||
"showEmptyState": false,
|
"showEmptyState": false,
|
||||||
"size": {
|
"size": {
|
||||||
|
@ -4734,7 +4727,6 @@
|
||||||
"name": "Date Range",
|
"name": "Date Range",
|
||||||
"icon": "Calendar",
|
"icon": "Calendar",
|
||||||
"styles": ["size"],
|
"styles": ["size"],
|
||||||
"requiredAncestors": ["dataprovider"],
|
|
||||||
"hasChildren": false,
|
"hasChildren": false,
|
||||||
"size": {
|
"size": {
|
||||||
"width": 200,
|
"width": 200,
|
||||||
|
@ -4842,7 +4834,6 @@
|
||||||
"width": 100,
|
"width": 100,
|
||||||
"height": 35
|
"height": 35
|
||||||
},
|
},
|
||||||
"requiredAncestors": ["dataprovider"],
|
|
||||||
"settings": [
|
"settings": [
|
||||||
{
|
{
|
||||||
"type": "dataProvider",
|
"type": "dataProvider",
|
||||||
|
@ -5617,7 +5608,38 @@
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
],
|
||||||
|
"context": {
|
||||||
|
"type": "static",
|
||||||
|
"suffix": "provider",
|
||||||
|
"values": [
|
||||||
|
{
|
||||||
|
"label": "Rows",
|
||||||
|
"key": "rows",
|
||||||
|
"type": "array"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"label": "Extra Info",
|
||||||
|
"key": "info",
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"label": "Rows Length",
|
||||||
|
"key": "rowsLength",
|
||||||
|
"type": "number"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"label": "Schema",
|
||||||
|
"key": "schema",
|
||||||
|
"type": "object"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"label": "Page Number",
|
||||||
|
"key": "pageNumber",
|
||||||
|
"type": "number"
|
||||||
|
}
|
||||||
]
|
]
|
||||||
|
}
|
||||||
},
|
},
|
||||||
"cardsblock": {
|
"cardsblock": {
|
||||||
"block": true,
|
"block": true,
|
||||||
|
@ -5796,7 +5818,8 @@
|
||||||
],
|
],
|
||||||
"context": {
|
"context": {
|
||||||
"type": "schema",
|
"type": "schema",
|
||||||
"suffix": "repeater"
|
"suffix": "repeater",
|
||||||
|
"scope": "local"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"repeaterblock": {
|
"repeaterblock": {
|
||||||
|
@ -6020,7 +6043,8 @@
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"type": "schema",
|
"type": "schema",
|
||||||
"suffix": "repeater"
|
"suffix": "repeater",
|
||||||
|
"scope": "local"
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
|
@ -6166,6 +6190,10 @@
|
||||||
"type": "form",
|
"type": "form",
|
||||||
"suffix": "form"
|
"suffix": "form"
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"type": "schema",
|
||||||
|
"suffix": "repeater"
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"type": "static",
|
"type": "static",
|
||||||
"suffix": "form",
|
"suffix": "form",
|
||||||
|
@ -6479,9 +6507,27 @@
|
||||||
],
|
],
|
||||||
"context": {
|
"context": {
|
||||||
"type": "schema",
|
"type": "schema",
|
||||||
"suffix": "repeater"
|
"suffix": "repeater",
|
||||||
|
"scope": "local"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"grid": {
|
||||||
|
"name": "Grid",
|
||||||
|
"icon": "ViewGrid",
|
||||||
|
"hasChildren": true,
|
||||||
|
"settings": [
|
||||||
|
{
|
||||||
|
"type": "number",
|
||||||
|
"key": "cols",
|
||||||
|
"label": "Columns"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "number",
|
||||||
|
"key": "rows",
|
||||||
|
"label": "Rows"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
"gridblock": {
|
"gridblock": {
|
||||||
"name": "Grid Block",
|
"name": "Grid Block",
|
||||||
"icon": "Table",
|
"icon": "Table",
|
||||||
|
@ -6625,7 +6671,8 @@
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"context": {
|
"context": {
|
||||||
"type": "schema"
|
"type": "schema",
|
||||||
|
"scope": "local"
|
||||||
},
|
},
|
||||||
"actions": ["RefreshDatasource"]
|
"actions": ["RefreshDatasource"]
|
||||||
},
|
},
|
||||||
|
|
|
@ -9,7 +9,7 @@
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
import { getContext, setContext, onMount, onDestroy } from "svelte"
|
import { getContext, setContext, onMount } from "svelte"
|
||||||
import { writable, get } from "svelte/store"
|
import { writable, get } from "svelte/store"
|
||||||
import {
|
import {
|
||||||
enrichProps,
|
enrichProps,
|
||||||
|
@ -30,6 +30,15 @@
|
||||||
import ScreenPlaceholder from "components/app/ScreenPlaceholder.svelte"
|
import ScreenPlaceholder from "components/app/ScreenPlaceholder.svelte"
|
||||||
import ComponentErrorState from "components/error-states/ComponentErrorState.svelte"
|
import ComponentErrorState from "components/error-states/ComponentErrorState.svelte"
|
||||||
import { BudibasePrefix } from "../stores/components.js"
|
import { BudibasePrefix } from "../stores/components.js"
|
||||||
|
import {
|
||||||
|
decodeJSBinding,
|
||||||
|
findHBSBlocks,
|
||||||
|
isJSBinding,
|
||||||
|
} from "@budibase/string-templates"
|
||||||
|
import {
|
||||||
|
getActionContextKey,
|
||||||
|
getActionDependentContextKeys,
|
||||||
|
} from "../utils/buttonActions.js"
|
||||||
|
|
||||||
export let instance = {}
|
export let instance = {}
|
||||||
export let isLayout = false
|
export let isLayout = false
|
||||||
|
@ -81,7 +90,6 @@
|
||||||
|
|
||||||
// Keep track of stringified representations of context and instance
|
// Keep track of stringified representations of context and instance
|
||||||
// to avoid enriching bindings as much as possible
|
// to avoid enriching bindings as much as possible
|
||||||
let lastContextKey
|
|
||||||
let lastInstanceKey
|
let lastInstanceKey
|
||||||
|
|
||||||
// Visibility flag used by conditional UI
|
// Visibility flag used by conditional UI
|
||||||
|
@ -98,6 +106,13 @@
|
||||||
// We clear these whenever a new instance is received.
|
// We clear these whenever a new instance is received.
|
||||||
let ephemeralStyles
|
let ephemeralStyles
|
||||||
|
|
||||||
|
// Single string of all HBS blocks, used to check if we use a certain binding
|
||||||
|
// or not
|
||||||
|
let bindingString = ""
|
||||||
|
|
||||||
|
// List of context keys which we use inside bindings
|
||||||
|
let knownContextKeyMap = {}
|
||||||
|
|
||||||
// Set up initial state for each new component instance
|
// Set up initial state for each new component instance
|
||||||
$: initialise(instance)
|
$: initialise(instance)
|
||||||
|
|
||||||
|
@ -155,9 +170,6 @@
|
||||||
hasMissingRequiredSettings)
|
hasMissingRequiredSettings)
|
||||||
$: emptyState = empty && showEmptyState
|
$: emptyState = empty && showEmptyState
|
||||||
|
|
||||||
// Enrich component settings
|
|
||||||
$: enrichComponentSettings($context, settingsDefinitionMap)
|
|
||||||
|
|
||||||
// Evaluate conditional UI settings and store any component setting changes
|
// Evaluate conditional UI settings and store any component setting changes
|
||||||
// which need to be made
|
// which need to be made
|
||||||
$: evaluateConditions(conditions)
|
$: evaluateConditions(conditions)
|
||||||
|
@ -206,6 +218,7 @@
|
||||||
errorState,
|
errorState,
|
||||||
parent: id,
|
parent: id,
|
||||||
ancestors: [...($component?.ancestors ?? []), instance._component],
|
ancestors: [...($component?.ancestors ?? []), instance._component],
|
||||||
|
path: [...($component?.path ?? []), id],
|
||||||
})
|
})
|
||||||
|
|
||||||
const initialise = (instance, force = false) => {
|
const initialise = (instance, force = false) => {
|
||||||
|
@ -214,7 +227,8 @@
|
||||||
}
|
}
|
||||||
|
|
||||||
// Ensure we're processing a new instance
|
// Ensure we're processing a new instance
|
||||||
const instanceKey = Helpers.hashString(JSON.stringify(instance))
|
const stringifiedInstance = JSON.stringify(instance)
|
||||||
|
const instanceKey = Helpers.hashString(stringifiedInstance)
|
||||||
if (instanceKey === lastInstanceKey && !force) {
|
if (instanceKey === lastInstanceKey && !force) {
|
||||||
return
|
return
|
||||||
} else {
|
} else {
|
||||||
|
@ -274,13 +288,54 @@
|
||||||
return missing
|
return missing
|
||||||
})
|
})
|
||||||
|
|
||||||
|
// When considering bindings we can ignore children, so we remove that
|
||||||
|
// before storing the reference stringified version
|
||||||
|
const noChildren = JSON.stringify({ ...instance, _children: null })
|
||||||
|
const bindings = findHBSBlocks(noChildren).map(binding => {
|
||||||
|
let sanitizedBinding = binding.replace(/\\"/g, '"')
|
||||||
|
if (isJSBinding(sanitizedBinding)) {
|
||||||
|
return decodeJSBinding(sanitizedBinding)
|
||||||
|
} else {
|
||||||
|
return sanitizedBinding
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
// The known context key map is built up at runtime, as changes to keys are
|
||||||
|
// encountered. We manually seed this to the required action keys as these
|
||||||
|
// are not encountered at runtime and so need computed in advance.
|
||||||
|
knownContextKeyMap = generateActionKeyMap(instance, settingsDefinition)
|
||||||
|
bindingString = bindings.join(" ")
|
||||||
|
|
||||||
// Run any migrations
|
// Run any migrations
|
||||||
runMigrations(instance, settingsDefinition)
|
runMigrations(instance, settingsDefinition)
|
||||||
|
|
||||||
// Force an initial enrichment of the new settings
|
// Force an initial enrichment of the new settings
|
||||||
enrichComponentSettings(get(context), settingsDefinitionMap, {
|
enrichComponentSettings(get(context), settingsDefinitionMap)
|
||||||
force: true,
|
}
|
||||||
|
|
||||||
|
// Extracts a map of all context keys which are required by action settings
|
||||||
|
// to provide the functions to evaluate at runtime. This needs done manually
|
||||||
|
// as the action definitions themselves do not specify bindings for action
|
||||||
|
// keys, meaning we cannot do this while doing the other normal bindings.
|
||||||
|
const generateActionKeyMap = (instance, settingsDefinition) => {
|
||||||
|
let map = {}
|
||||||
|
settingsDefinition.forEach(setting => {
|
||||||
|
if (setting.type === "event") {
|
||||||
|
instance[setting.key]?.forEach(action => {
|
||||||
|
// We depend on the actual action key
|
||||||
|
const actionKey = getActionContextKey(action)
|
||||||
|
if (actionKey) {
|
||||||
|
map[actionKey] = true
|
||||||
|
}
|
||||||
|
|
||||||
|
// We also depend on any manually declared context keys
|
||||||
|
getActionDependentContextKeys(action)?.forEach(key => {
|
||||||
|
map[key] = true
|
||||||
})
|
})
|
||||||
|
})
|
||||||
|
}
|
||||||
|
})
|
||||||
|
return map
|
||||||
}
|
}
|
||||||
|
|
||||||
const runMigrations = (instance, settingsDefinition) => {
|
const runMigrations = (instance, settingsDefinition) => {
|
||||||
|
@ -381,17 +436,7 @@
|
||||||
}
|
}
|
||||||
|
|
||||||
// Enriches any string component props using handlebars
|
// Enriches any string component props using handlebars
|
||||||
const enrichComponentSettings = (
|
const enrichComponentSettings = (context, settingsDefinitionMap) => {
|
||||||
context,
|
|
||||||
settingsDefinitionMap,
|
|
||||||
options = { force: false }
|
|
||||||
) => {
|
|
||||||
const contextChanged = context.key !== lastContextKey
|
|
||||||
if (!contextChanged && !options?.force) {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
lastContextKey = context.key
|
|
||||||
|
|
||||||
// Record the timestamp so we can reference it after enrichment
|
// Record the timestamp so we can reference it after enrichment
|
||||||
latestUpdateTime = Date.now()
|
latestUpdateTime = Date.now()
|
||||||
const enrichmentTime = latestUpdateTime
|
const enrichmentTime = latestUpdateTime
|
||||||
|
@ -506,11 +551,26 @@
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const handleContextChange = key => {
|
||||||
|
// Check if we already know if this key is used
|
||||||
|
let used = knownContextKeyMap[key]
|
||||||
|
|
||||||
|
// If we don't know, check and cache
|
||||||
|
if (used == null) {
|
||||||
|
used = bindingString.indexOf(`[${key}]`) !== -1
|
||||||
|
knownContextKeyMap[key] = used
|
||||||
|
}
|
||||||
|
|
||||||
|
// Enrich settings if we use this key
|
||||||
|
if (used) {
|
||||||
|
enrichComponentSettings($context, settingsDefinitionMap)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Register an unregister component instance
|
||||||
onMount(() => {
|
onMount(() => {
|
||||||
if (
|
if ($appStore.isDevApp) {
|
||||||
$appStore.isDevApp &&
|
if (!componentStore.actions.isComponentRegistered(id)) {
|
||||||
!componentStore.actions.isComponentRegistered(id)
|
|
||||||
) {
|
|
||||||
componentStore.actions.registerInstance(id, {
|
componentStore.actions.registerInstance(id, {
|
||||||
component: instance._component,
|
component: instance._component,
|
||||||
getSettings: () => cachedSettings,
|
getSettings: () => cachedSettings,
|
||||||
|
@ -521,16 +581,16 @@
|
||||||
state: store,
|
state: store,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
})
|
return () => {
|
||||||
|
if (componentStore.actions.isComponentRegistered(id)) {
|
||||||
onDestroy(() => {
|
|
||||||
if (
|
|
||||||
$appStore.isDevApp &&
|
|
||||||
componentStore.actions.isComponentRegistered(id)
|
|
||||||
) {
|
|
||||||
componentStore.actions.unregisterInstance(id)
|
componentStore.actions.unregisterInstance(id)
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
|
// Observe changes to context
|
||||||
|
onMount(() => context.actions.observeChanges(handleContextChange))
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
{#if constructor && initialSettings && (visible || inSelectedPath) && !builderHidden}
|
{#if constructor && initialSettings && (visible || inSelectedPath) && !builderHidden}
|
||||||
|
|
|
@ -71,7 +71,7 @@
|
||||||
datasource: dataSource || {},
|
datasource: dataSource || {},
|
||||||
schema,
|
schema,
|
||||||
rowsLength: $fetch.rows.length,
|
rowsLength: $fetch.rows.length,
|
||||||
|
pageNumber: $fetch.pageNumber + 1,
|
||||||
// Undocumented properties. These aren't supposed to be used in builder
|
// Undocumented properties. These aren't supposed to be used in builder
|
||||||
// bindings, but are used internally by other components
|
// bindings, but are used internally by other components
|
||||||
id: $component?.id,
|
id: $component?.id,
|
||||||
|
|
|
@ -1,6 +1,5 @@
|
||||||
<script>
|
<script>
|
||||||
import { getContext } from "svelte"
|
import { getContext } from "svelte"
|
||||||
import { Icon } from "@budibase/bbui"
|
|
||||||
|
|
||||||
const component = getContext("component")
|
const component = getContext("component")
|
||||||
const { builderStore, componentStore } = getContext("sdk")
|
const { builderStore, componentStore } = getContext("sdk")
|
||||||
|
@ -10,15 +9,7 @@
|
||||||
|
|
||||||
{#if $builderStore.inBuilder}
|
{#if $builderStore.inBuilder}
|
||||||
<div class="component-placeholder">
|
<div class="component-placeholder">
|
||||||
<Icon name="Help" color="var(--spectrum-global-color-blue-600)" />
|
{$component.name || definition?.name || "Component"}
|
||||||
<span
|
|
||||||
class="spectrum-Link"
|
|
||||||
on:click={() => {
|
|
||||||
builderStore.actions.requestAddComponent()
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
Add components inside your {definition?.name || $component.type}
|
|
||||||
</span>
|
|
||||||
</div>
|
</div>
|
||||||
{/if}
|
{/if}
|
||||||
|
|
||||||
|
@ -32,14 +23,4 @@
|
||||||
font-size: var(--font-size-s);
|
font-size: var(--font-size-s);
|
||||||
gap: var(--spacing-s);
|
gap: var(--spacing-s);
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Common styles for all error states to use */
|
|
||||||
.component-placeholder :global(mark) {
|
|
||||||
background-color: var(--spectrum-global-color-gray-400);
|
|
||||||
padding: 0 4px;
|
|
||||||
border-radius: 2px;
|
|
||||||
}
|
|
||||||
.component-placeholder :global(.spectrum-Link) {
|
|
||||||
cursor: pointer;
|
|
||||||
}
|
|
||||||
</style>
|
</style>
|
||||||
|
|
|
@ -19,7 +19,36 @@
|
||||||
export let onRowClick = null
|
export let onRowClick = null
|
||||||
export let buttons = null
|
export let buttons = null
|
||||||
|
|
||||||
// parses columns to fix older formats
|
const context = getContext("context")
|
||||||
|
const component = getContext("component")
|
||||||
|
const {
|
||||||
|
styleable,
|
||||||
|
API,
|
||||||
|
builderStore,
|
||||||
|
notificationStore,
|
||||||
|
enrichButtonActions,
|
||||||
|
ActionTypes,
|
||||||
|
createContextStore,
|
||||||
|
Provider,
|
||||||
|
} = getContext("sdk")
|
||||||
|
|
||||||
|
let grid
|
||||||
|
|
||||||
|
$: columnWhitelist = parsedColumns
|
||||||
|
?.filter(col => col.active)
|
||||||
|
?.map(col => col.field)
|
||||||
|
$: schemaOverrides = getSchemaOverrides(parsedColumns)
|
||||||
|
$: enrichedButtons = enrichButtons(buttons)
|
||||||
|
$: parsedColumns = getParsedColumns(columns)
|
||||||
|
$: actions = [
|
||||||
|
{
|
||||||
|
type: ActionTypes.RefreshDatasource,
|
||||||
|
callback: () => grid?.getContext()?.rows.actions.refreshData(),
|
||||||
|
metadata: { dataSource: table },
|
||||||
|
},
|
||||||
|
]
|
||||||
|
|
||||||
|
// Parses columns to fix older formats
|
||||||
const getParsedColumns = columns => {
|
const getParsedColumns = columns => {
|
||||||
// If the first element has an active key all elements should be in the new format
|
// If the first element has an active key all elements should be in the new format
|
||||||
if (columns?.length && columns[0]?.active !== undefined) {
|
if (columns?.length && columns[0]?.active !== undefined) {
|
||||||
|
@ -33,28 +62,6 @@
|
||||||
}))
|
}))
|
||||||
}
|
}
|
||||||
|
|
||||||
$: parsedColumns = getParsedColumns(columns)
|
|
||||||
|
|
||||||
const context = getContext("context")
|
|
||||||
const component = getContext("component")
|
|
||||||
const {
|
|
||||||
styleable,
|
|
||||||
API,
|
|
||||||
builderStore,
|
|
||||||
notificationStore,
|
|
||||||
enrichButtonActions,
|
|
||||||
ActionTypes,
|
|
||||||
createContextStore,
|
|
||||||
} = getContext("sdk")
|
|
||||||
|
|
||||||
let grid
|
|
||||||
|
|
||||||
$: columnWhitelist = parsedColumns
|
|
||||||
?.filter(col => col.active)
|
|
||||||
?.map(col => col.field)
|
|
||||||
$: schemaOverrides = getSchemaOverrides(parsedColumns)
|
|
||||||
$: enrichedButtons = enrichButtons(buttons)
|
|
||||||
|
|
||||||
const getSchemaOverrides = columns => {
|
const getSchemaOverrides = columns => {
|
||||||
let overrides = {}
|
let overrides = {}
|
||||||
columns?.forEach(column => {
|
columns?.forEach(column => {
|
||||||
|
@ -78,11 +85,6 @@
|
||||||
const id = get(component).id
|
const id = get(component).id
|
||||||
const gridContext = createContextStore(context)
|
const gridContext = createContextStore(context)
|
||||||
gridContext.actions.provideData(id, row)
|
gridContext.actions.provideData(id, row)
|
||||||
gridContext.actions.provideAction(
|
|
||||||
id,
|
|
||||||
ActionTypes.RefreshDatasource,
|
|
||||||
() => grid?.getContext()?.rows.actions.refreshData()
|
|
||||||
)
|
|
||||||
const fn = enrichButtonActions(settings.onClick, get(gridContext))
|
const fn = enrichButtonActions(settings.onClick, get(gridContext))
|
||||||
return await fn?.({ row })
|
return await fn?.({ row })
|
||||||
},
|
},
|
||||||
|
@ -94,6 +96,7 @@
|
||||||
use:styleable={$component.styles}
|
use:styleable={$component.styles}
|
||||||
class:in-builder={$builderStore.inBuilder}
|
class:in-builder={$builderStore.inBuilder}
|
||||||
>
|
>
|
||||||
|
<Provider {actions}>
|
||||||
<Grid
|
<Grid
|
||||||
bind:this={grid}
|
bind:this={grid}
|
||||||
datasource={table}
|
datasource={table}
|
||||||
|
@ -117,6 +120,7 @@
|
||||||
buttons={enrichedButtons}
|
buttons={enrichedButtons}
|
||||||
on:rowclick={e => onRowClick?.({ row: e.detail })}
|
on:rowclick={e => onRowClick?.({ row: e.detail })}
|
||||||
/>
|
/>
|
||||||
|
</Provider>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<style>
|
<style>
|
||||||
|
|
|
@ -2,6 +2,7 @@
|
||||||
import { getContext } from "svelte"
|
import { getContext } from "svelte"
|
||||||
import Placeholder from "./Placeholder.svelte"
|
import Placeholder from "./Placeholder.svelte"
|
||||||
import Container from "./Container.svelte"
|
import Container from "./Container.svelte"
|
||||||
|
import { ContextScopes } from "constants"
|
||||||
|
|
||||||
export let dataProvider
|
export let dataProvider
|
||||||
export let noRowsMessage
|
export let noRowsMessage
|
||||||
|
@ -9,6 +10,7 @@
|
||||||
export let hAlign
|
export let hAlign
|
||||||
export let vAlign
|
export let vAlign
|
||||||
export let gap
|
export let gap
|
||||||
|
export let scope = ContextScopes.Local
|
||||||
|
|
||||||
const { Provider } = getContext("sdk")
|
const { Provider } = getContext("sdk")
|
||||||
const component = getContext("component")
|
const component = getContext("component")
|
||||||
|
@ -22,7 +24,7 @@
|
||||||
<Placeholder />
|
<Placeholder />
|
||||||
{:else if rows.length > 0}
|
{:else if rows.length > 0}
|
||||||
{#each rows as row, index}
|
{#each rows as row, index}
|
||||||
<Provider data={{ ...row, index }}>
|
<Provider data={{ ...row, index }} {scope}>
|
||||||
<slot />
|
<slot />
|
||||||
</Provider>
|
</Provider>
|
||||||
{/each}
|
{/each}
|
||||||
|
|
|
@ -1,5 +1,6 @@
|
||||||
<script>
|
<script>
|
||||||
import BlockComponent from "components/BlockComponent.svelte"
|
import BlockComponent from "components/BlockComponent.svelte"
|
||||||
|
import { Helpers } from "@budibase/bbui"
|
||||||
import { getContext, setContext } from "svelte"
|
import { getContext, setContext } from "svelte"
|
||||||
import { builderStore } from "stores"
|
import { builderStore } from "stores"
|
||||||
import { Utils } from "@budibase/frontend-core"
|
import { Utils } from "@budibase/frontend-core"
|
||||||
|
@ -41,7 +42,7 @@
|
||||||
let schema
|
let schema
|
||||||
|
|
||||||
$: fetchSchema(dataSource)
|
$: fetchSchema(dataSource)
|
||||||
$: enrichedSteps = enrichSteps(steps, schema, $component.id)
|
$: enrichedSteps = enrichSteps(steps, schema, $component.id, $currentStep)
|
||||||
$: updateCurrentStep(enrichedSteps, $builderStore, $component)
|
$: updateCurrentStep(enrichedSteps, $builderStore, $component)
|
||||||
|
|
||||||
const updateCurrentStep = (steps, builderStore, component) => {
|
const updateCurrentStep = (steps, builderStore, component) => {
|
||||||
|
@ -115,6 +116,7 @@
|
||||||
dataSource,
|
dataSource,
|
||||||
})
|
})
|
||||||
return {
|
return {
|
||||||
|
_stepId: Helpers.uuid(),
|
||||||
fields: getDefaultFields(fields || [], schema),
|
fields: getDefaultFields(fields || [], schema),
|
||||||
title: title ?? defaultProps.title,
|
title: title ?? defaultProps.title,
|
||||||
desc,
|
desc,
|
||||||
|
@ -142,7 +144,7 @@
|
||||||
},
|
},
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{#each enrichedSteps as step, stepIdx}
|
{#each enrichedSteps as step, stepIdx (step._stepId)}
|
||||||
<BlockComponent
|
<BlockComponent
|
||||||
type="formstep"
|
type="formstep"
|
||||||
props={{ step: stepIdx + 1, _instanceName: `Step ${stepIdx + 1}` }}
|
props={{ step: stepIdx + 1, _instanceName: `Step ${stepIdx + 1}` }}
|
||||||
|
@ -186,12 +188,13 @@
|
||||||
</BlockComponent>
|
</BlockComponent>
|
||||||
</BlockComponent>
|
</BlockComponent>
|
||||||
<BlockComponent type="text" props={{ text: step.desc }} order={1} />
|
<BlockComponent type="text" props={{ text: step.desc }} order={1} />
|
||||||
|
|
||||||
<BlockComponent type="container" order={2}>
|
<BlockComponent type="container" order={2}>
|
||||||
<div
|
<div
|
||||||
class="form-block fields"
|
class="form-block fields"
|
||||||
class:mobile={$context.device.mobile}
|
class:mobile={$context.device.mobile}
|
||||||
>
|
>
|
||||||
{#each step.fields as field, fieldIdx (`${field.field || field.name}_${stepIdx}_${fieldIdx}`)}
|
{#each step.fields as field, fieldIdx (`${field.field || field.name}_${fieldIdx}`)}
|
||||||
{#if getComponentForField(field)}
|
{#if getComponentForField(field)}
|
||||||
<BlockComponent
|
<BlockComponent
|
||||||
type={getComponentForField(field)}
|
type={getComponentForField(field)}
|
||||||
|
|
|
@ -231,6 +231,7 @@
|
||||||
paginate,
|
paginate,
|
||||||
limit: rowCount,
|
limit: rowCount,
|
||||||
}}
|
}}
|
||||||
|
context="provider"
|
||||||
order={1}
|
order={1}
|
||||||
>
|
>
|
||||||
<BlockComponent
|
<BlockComponent
|
||||||
|
|
|
@ -10,6 +10,7 @@
|
||||||
export let noRowsMessage
|
export let noRowsMessage
|
||||||
|
|
||||||
const component = getContext("component")
|
const component = getContext("component")
|
||||||
|
const { ContextScopes } = getContext("sdk")
|
||||||
|
|
||||||
$: providerId = `${$component.id}-provider`
|
$: providerId = `${$component.id}-provider`
|
||||||
$: dataProvider = `{{ literal ${safe(providerId)} }}`
|
$: dataProvider = `{{ literal ${safe(providerId)} }}`
|
||||||
|
@ -55,6 +56,7 @@
|
||||||
noRowsMessage: noRowsMessage || "We couldn't find a row to display",
|
noRowsMessage: noRowsMessage || "We couldn't find a row to display",
|
||||||
direction: "column",
|
direction: "column",
|
||||||
hAlign: "center",
|
hAlign: "center",
|
||||||
|
scope: ContextScopes.Global,
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<slot />
|
<slot />
|
||||||
|
|
|
@ -21,6 +21,7 @@
|
||||||
export let editAutoColumns = false
|
export let editAutoColumns = false
|
||||||
|
|
||||||
const context = getContext("context")
|
const context = getContext("context")
|
||||||
|
const component = getContext("component")
|
||||||
const { API, fetchDatasourceSchema } = getContext("sdk")
|
const { API, fetchDatasourceSchema } = getContext("sdk")
|
||||||
|
|
||||||
const getInitialFormStep = () => {
|
const getInitialFormStep = () => {
|
||||||
|
@ -38,28 +39,47 @@
|
||||||
|
|
||||||
$: fetchSchema(dataSource)
|
$: fetchSchema(dataSource)
|
||||||
$: schemaKey = generateSchemaKey(schema)
|
$: schemaKey = generateSchemaKey(schema)
|
||||||
$: initialValues = getInitialValues(actionType, dataSource, $context)
|
$: initialValues = getInitialValues(
|
||||||
|
actionType,
|
||||||
|
dataSource,
|
||||||
|
$component.path,
|
||||||
|
$context
|
||||||
|
)
|
||||||
$: resetKey = Helpers.hashString(
|
$: resetKey = Helpers.hashString(
|
||||||
schemaKey + JSON.stringify(initialValues) + disabled + readonly
|
schemaKey + JSON.stringify(initialValues) + disabled + readonly
|
||||||
)
|
)
|
||||||
|
|
||||||
// Returns the closes data context which isn't a built in context
|
// Returns the closes data context which isn't a built in context
|
||||||
const getInitialValues = (type, dataSource, context) => {
|
const getInitialValues = (type, dataSource, path, context) => {
|
||||||
// Only inherit values for update forms
|
// Only inherit values for update forms
|
||||||
if (type !== "Update") {
|
if (type !== "Update") {
|
||||||
return {}
|
return {}
|
||||||
}
|
}
|
||||||
// Only inherit values for forms targeting internal tables
|
// Only inherit values for forms targeting internal tables
|
||||||
if (!dataSource?.tableId) {
|
const dsType = dataSource?.type
|
||||||
|
if (dsType !== "table" && dsType !== "viewV2") {
|
||||||
return {}
|
return {}
|
||||||
}
|
}
|
||||||
// Don't inherit values representing built in contexts
|
// Look up the component tree and find something that is provided by an
|
||||||
if (["user", "url"].includes(context.closestComponentId)) {
|
// ancestor that matches our datasource. This is for backwards compatibility
|
||||||
return {}
|
// as previously we could use the "closest" context.
|
||||||
|
for (let id of path.reverse().slice(1)) {
|
||||||
|
// Check for matching view datasource
|
||||||
|
if (
|
||||||
|
dataSource.type === "viewV2" &&
|
||||||
|
context[id]?._viewId === dataSource.id
|
||||||
|
) {
|
||||||
|
return context[id]
|
||||||
}
|
}
|
||||||
// Always inherit the closest datasource
|
// Check for matching table datasource
|
||||||
const closestContext = context[`${context.closestComponentId}`] || {}
|
if (
|
||||||
return closestContext || {}
|
dataSource.type === "table" &&
|
||||||
|
context[id]?.tableId === dataSource.tableId
|
||||||
|
) {
|
||||||
|
return context[id]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return {}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Fetches the form schema from this form's dataSource
|
// Fetches the form schema from this form's dataSource
|
||||||
|
|
|
@ -1,21 +1,24 @@
|
||||||
<script>
|
<script>
|
||||||
import { getContext, setContext, onDestroy } from "svelte"
|
import { getContext, setContext, onDestroy } from "svelte"
|
||||||
import { dataSourceStore, createContextStore } from "stores"
|
import { dataSourceStore, createContextStore } from "stores"
|
||||||
import { ActionTypes } from "constants"
|
import { ActionTypes, ContextScopes } from "constants"
|
||||||
import { generate } from "shortid"
|
import { generate } from "shortid"
|
||||||
|
|
||||||
export let data
|
export let data
|
||||||
export let actions
|
export let actions
|
||||||
export let key
|
export let key
|
||||||
|
export let scope = ContextScopes.Global
|
||||||
|
|
||||||
// Clone and create new data context for this component tree
|
let context = getContext("context")
|
||||||
const context = getContext("context")
|
|
||||||
const component = getContext("component")
|
const component = getContext("component")
|
||||||
const newContext = createContextStore(context)
|
|
||||||
setContext("context", newContext)
|
|
||||||
|
|
||||||
const providerKey = key || $component.id
|
const providerKey = key || $component.id
|
||||||
|
|
||||||
|
// Create a new layer of context if we are only locally scoped
|
||||||
|
if (scope === ContextScopes.Local) {
|
||||||
|
context = createContextStore(context)
|
||||||
|
setContext("context", context)
|
||||||
|
}
|
||||||
|
|
||||||
// Generate a permanent unique ID for this component and use it to register
|
// Generate a permanent unique ID for this component and use it to register
|
||||||
// any datasource actions
|
// any datasource actions
|
||||||
const instanceId = generate()
|
const instanceId = generate()
|
||||||
|
@ -30,7 +33,7 @@
|
||||||
const provideData = newData => {
|
const provideData = newData => {
|
||||||
const dataKey = JSON.stringify(newData)
|
const dataKey = JSON.stringify(newData)
|
||||||
if (dataKey !== lastDataKey) {
|
if (dataKey !== lastDataKey) {
|
||||||
newContext.actions.provideData(providerKey, newData)
|
context.actions.provideData(providerKey, newData, scope)
|
||||||
lastDataKey = dataKey
|
lastDataKey = dataKey
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -40,7 +43,7 @@
|
||||||
if (actionsKey !== lastActionsKey) {
|
if (actionsKey !== lastActionsKey) {
|
||||||
lastActionsKey = actionsKey
|
lastActionsKey = actionsKey
|
||||||
newActions?.forEach(({ type, callback, metadata }) => {
|
newActions?.forEach(({ type, callback, metadata }) => {
|
||||||
newContext.actions.provideAction(providerKey, type, callback)
|
context.actions.provideAction(providerKey, type, callback, scope)
|
||||||
|
|
||||||
// Register any "refresh datasource" actions with a singleton store
|
// Register any "refresh datasource" actions with a singleton store
|
||||||
// so we can easily refresh data at all levels for any datasource
|
// so we can easily refresh data at all levels for any datasource
|
||||||
|
|
|
@ -12,5 +12,10 @@ export const ActionTypes = {
|
||||||
ScrollTo: "ScrollTo",
|
ScrollTo: "ScrollTo",
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export const ContextScopes = {
|
||||||
|
Local: "local",
|
||||||
|
Global: "global",
|
||||||
|
}
|
||||||
|
|
||||||
export const DNDPlaceholderID = "dnd-placeholder"
|
export const DNDPlaceholderID = "dnd-placeholder"
|
||||||
export const ScreenslotType = "screenslot"
|
export const ScreenslotType = "screenslot"
|
||||||
|
|
|
@ -23,7 +23,7 @@ import { getAction } from "utils/getAction"
|
||||||
import Provider from "components/context/Provider.svelte"
|
import Provider from "components/context/Provider.svelte"
|
||||||
import Block from "components/Block.svelte"
|
import Block from "components/Block.svelte"
|
||||||
import BlockComponent from "components/BlockComponent.svelte"
|
import BlockComponent from "components/BlockComponent.svelte"
|
||||||
import { ActionTypes } from "./constants"
|
import { ActionTypes, ContextScopes } from "./constants"
|
||||||
import { fetchDatasourceSchema } from "./utils/schema.js"
|
import { fetchDatasourceSchema } from "./utils/schema.js"
|
||||||
import { getAPIKey } from "./utils/api.js"
|
import { getAPIKey } from "./utils/api.js"
|
||||||
import { enrichButtonActions } from "./utils/buttonActions.js"
|
import { enrichButtonActions } from "./utils/buttonActions.js"
|
||||||
|
@ -54,6 +54,7 @@ export default {
|
||||||
linkable,
|
linkable,
|
||||||
getAction,
|
getAction,
|
||||||
fetchDatasourceSchema,
|
fetchDatasourceSchema,
|
||||||
|
ContextScopes,
|
||||||
getAPIKey,
|
getAPIKey,
|
||||||
enrichButtonActions,
|
enrichButtonActions,
|
||||||
processStringSync,
|
processStringSync,
|
||||||
|
|
|
@ -1,59 +1,98 @@
|
||||||
import { writable, derived } from "svelte/store"
|
import { writable, derived } from "svelte/store"
|
||||||
import { Helpers } from "@budibase/bbui"
|
import { ContextScopes } from "constants"
|
||||||
|
|
||||||
export const createContextStore = oldContext => {
|
export const createContextStore = parentContext => {
|
||||||
const newContext = writable({})
|
const context = writable({})
|
||||||
const contexts = oldContext ? [oldContext, newContext] : [newContext]
|
let observers = []
|
||||||
|
|
||||||
|
// Derive the total context state at this point in the tree
|
||||||
|
const contexts = parentContext ? [parentContext, context] : [context]
|
||||||
const totalContext = derived(contexts, $contexts => {
|
const totalContext = derived(contexts, $contexts => {
|
||||||
// The key is the serialized representation of context
|
return $contexts.reduce((total, context) => ({ ...total, ...context }), {})
|
||||||
let key = ""
|
|
||||||
for (let i = 0; i < $contexts.length - 1; i++) {
|
|
||||||
key += $contexts[i].key
|
|
||||||
}
|
|
||||||
key = Helpers.hashString(
|
|
||||||
key + JSON.stringify($contexts[$contexts.length - 1])
|
|
||||||
)
|
|
||||||
|
|
||||||
// Reduce global state
|
|
||||||
const reducer = (total, context) => ({ ...total, ...context })
|
|
||||||
const context = $contexts.reduce(reducer, {})
|
|
||||||
|
|
||||||
return {
|
|
||||||
...context,
|
|
||||||
key,
|
|
||||||
}
|
|
||||||
})
|
})
|
||||||
|
|
||||||
// Adds a data context layer to the tree
|
// Subscribe to updates in the parent context, so that we can proxy on any
|
||||||
const provideData = (providerId, data) => {
|
// change messages to our own subscribers
|
||||||
|
if (parentContext) {
|
||||||
|
parentContext.actions.observeChanges(key => {
|
||||||
|
broadcastChange(key)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// Provide some data in context
|
||||||
|
const provideData = (providerId, data, scope = ContextScopes.Global) => {
|
||||||
if (!providerId || data === undefined) {
|
if (!providerId || data === undefined) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
newContext.update(state => {
|
|
||||||
state[providerId] = data
|
|
||||||
|
|
||||||
// Keep track of the closest component ID so we can later hydrate a "data" prop.
|
// Proxy message up the chain if we have a parent and are providing global
|
||||||
// This is only required for legacy bindings that used "data" rather than a
|
// context
|
||||||
// component ID.
|
if (scope === ContextScopes.Global && parentContext) {
|
||||||
state.closestComponentId = providerId
|
parentContext.actions.provideData(providerId, data, scope)
|
||||||
|
|
||||||
return state
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Adds an action context layer to the tree
|
// Otherwise this is either the context root, or we're providing a local
|
||||||
const provideAction = (providerId, actionType, callback) => {
|
// context override, so we need to update the local context instead
|
||||||
|
else {
|
||||||
|
context.update(state => {
|
||||||
|
state[providerId] = data
|
||||||
|
return state
|
||||||
|
})
|
||||||
|
broadcastChange(providerId)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Provides some action in context
|
||||||
|
const provideAction = (
|
||||||
|
providerId,
|
||||||
|
actionType,
|
||||||
|
callback,
|
||||||
|
scope = ContextScopes.Global
|
||||||
|
) => {
|
||||||
if (!providerId || !actionType) {
|
if (!providerId || !actionType) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
newContext.update(state => {
|
|
||||||
state[`${providerId}_${actionType}`] = callback
|
// Proxy message up the chain if we have a parent and are providing global
|
||||||
|
// context
|
||||||
|
if (scope === ContextScopes.Global && parentContext) {
|
||||||
|
parentContext.actions.provideAction(
|
||||||
|
providerId,
|
||||||
|
actionType,
|
||||||
|
callback,
|
||||||
|
scope
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Otherwise this is either the context root, or we're providing a local
|
||||||
|
// context override, so we need to update the local context instead
|
||||||
|
else {
|
||||||
|
const key = `${providerId}_${actionType}`
|
||||||
|
context.update(state => {
|
||||||
|
state[key] = callback
|
||||||
return state
|
return state
|
||||||
})
|
})
|
||||||
|
broadcastChange(key)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const observeChanges = callback => {
|
||||||
|
observers.push(callback)
|
||||||
|
return () => {
|
||||||
|
observers = observers.filter(cb => cb !== callback)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const broadcastChange = key => {
|
||||||
|
observers.forEach(cb => cb(key))
|
||||||
}
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
subscribe: totalContext.subscribe,
|
subscribe: totalContext.subscribe,
|
||||||
actions: { provideData, provideAction },
|
actions: {
|
||||||
|
provideData,
|
||||||
|
provideAction,
|
||||||
|
observeChanges,
|
||||||
|
},
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -17,6 +17,54 @@ import { ActionTypes } from "constants"
|
||||||
import { enrichDataBindings } from "./enrichDataBinding"
|
import { enrichDataBindings } from "./enrichDataBinding"
|
||||||
import { Helpers } from "@budibase/bbui"
|
import { Helpers } from "@budibase/bbui"
|
||||||
|
|
||||||
|
// Default action handler, which extracts an action from context that was
|
||||||
|
// provided by another component and executes it with all action parameters
|
||||||
|
const contextActionHandler = async (action, context) => {
|
||||||
|
const key = getActionContextKey(action)
|
||||||
|
const fn = context[key]
|
||||||
|
if (fn) {
|
||||||
|
return await fn(action.parameters)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Generates the context key, which is the key that this action depends on in
|
||||||
|
// context to provide the function it will run. This is broken out as a util
|
||||||
|
// because we reuse this inside the core Component.svelte file to determine
|
||||||
|
// what the required action context keys are for all action settings.
|
||||||
|
export const getActionContextKey = action => {
|
||||||
|
const type = action?.["##eventHandlerType"]
|
||||||
|
const key = (componentId, type) => `${componentId}_${type}`
|
||||||
|
switch (type) {
|
||||||
|
case "Scroll To Field":
|
||||||
|
return key(action.parameters.componentId, ActionTypes.ScrollTo)
|
||||||
|
case "Update Field Value":
|
||||||
|
return key(action.parameters.componentId, ActionTypes.UpdateFieldValue)
|
||||||
|
case "Validate Form":
|
||||||
|
return key(action.parameters.componentId, ActionTypes.ValidateForm)
|
||||||
|
case "Refresh Data Provider":
|
||||||
|
return key(action.parameters.componentId, ActionTypes.RefreshDatasource)
|
||||||
|
case "Clear Form":
|
||||||
|
return key(action.parameters.componentId, ActionTypes.ClearForm)
|
||||||
|
case "Change Form Step":
|
||||||
|
return key(action.parameters.componentId, ActionTypes.ChangeFormStep)
|
||||||
|
default:
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// If button actions depend on context, they must declare which keys they need
|
||||||
|
export const getActionDependentContextKeys = action => {
|
||||||
|
const type = action?.["##eventHandlerType"]
|
||||||
|
switch (type) {
|
||||||
|
case "Save Row":
|
||||||
|
case "Duplicate Row":
|
||||||
|
if (action.parameters?.providerId) {
|
||||||
|
return [action.parameters.providerId]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return []
|
||||||
|
}
|
||||||
|
|
||||||
const saveRowHandler = async (action, context) => {
|
const saveRowHandler = async (action, context) => {
|
||||||
const { fields, providerId, tableId, notificationOverride } =
|
const { fields, providerId, tableId, notificationOverride } =
|
||||||
action.parameters
|
action.parameters
|
||||||
|
@ -32,20 +80,21 @@ const saveRowHandler = async (action, context) => {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (tableId) {
|
if (tableId) {
|
||||||
|
if (tableId.startsWith("view")) {
|
||||||
|
payload._viewId = tableId
|
||||||
|
} else {
|
||||||
payload.tableId = tableId
|
payload.tableId = tableId
|
||||||
}
|
}
|
||||||
|
}
|
||||||
try {
|
try {
|
||||||
const row = await API.saveRow(payload)
|
const row = await API.saveRow(payload)
|
||||||
|
|
||||||
if (!notificationOverride) {
|
if (!notificationOverride) {
|
||||||
notificationStore.actions.success("Row saved")
|
notificationStore.actions.success("Row saved")
|
||||||
}
|
}
|
||||||
|
|
||||||
// Refresh related datasources
|
// Refresh related datasources
|
||||||
await dataSourceStore.actions.invalidateDataSource(tableId, {
|
await dataSourceStore.actions.invalidateDataSource(tableId, {
|
||||||
invalidateRelationships: true,
|
invalidateRelationships: true,
|
||||||
})
|
})
|
||||||
|
|
||||||
return { row }
|
return { row }
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
// Abort next actions
|
// Abort next actions
|
||||||
|
@ -64,8 +113,12 @@ const duplicateRowHandler = async (action, context) => {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (tableId) {
|
if (tableId) {
|
||||||
|
if (tableId.startsWith("view")) {
|
||||||
|
payload._viewId = tableId
|
||||||
|
} else {
|
||||||
payload.tableId = tableId
|
payload.tableId = tableId
|
||||||
}
|
}
|
||||||
|
}
|
||||||
delete payload._id
|
delete payload._id
|
||||||
delete payload._rev
|
delete payload._rev
|
||||||
try {
|
try {
|
||||||
|
@ -73,12 +126,10 @@ const duplicateRowHandler = async (action, context) => {
|
||||||
if (!notificationOverride) {
|
if (!notificationOverride) {
|
||||||
notificationStore.actions.success("Row saved")
|
notificationStore.actions.success("Row saved")
|
||||||
}
|
}
|
||||||
|
|
||||||
// Refresh related datasources
|
// Refresh related datasources
|
||||||
await dataSourceStore.actions.invalidateDataSource(tableId, {
|
await dataSourceStore.actions.invalidateDataSource(tableId, {
|
||||||
invalidateRelationships: true,
|
invalidateRelationships: true,
|
||||||
})
|
})
|
||||||
|
|
||||||
return { row }
|
return { row }
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
// Abort next actions
|
// Abort next actions
|
||||||
|
@ -190,17 +241,6 @@ const navigationHandler = action => {
|
||||||
routeStore.actions.navigate(url, peek, externalNewTab)
|
routeStore.actions.navigate(url, peek, externalNewTab)
|
||||||
}
|
}
|
||||||
|
|
||||||
const scrollHandler = async (action, context) => {
|
|
||||||
return await executeActionHandler(
|
|
||||||
context,
|
|
||||||
action.parameters.componentId,
|
|
||||||
ActionTypes.ScrollTo,
|
|
||||||
{
|
|
||||||
field: action.parameters.field,
|
|
||||||
}
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
const queryExecutionHandler = async action => {
|
const queryExecutionHandler = async action => {
|
||||||
const { datasourceId, queryId, queryParams, notificationOverride } =
|
const { datasourceId, queryId, queryParams, notificationOverride } =
|
||||||
action.parameters
|
action.parameters
|
||||||
|
@ -236,47 +276,6 @@ const queryExecutionHandler = async action => {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const executeActionHandler = async (
|
|
||||||
context,
|
|
||||||
componentId,
|
|
||||||
actionType,
|
|
||||||
params
|
|
||||||
) => {
|
|
||||||
const fn = context[`${componentId}_${actionType}`]
|
|
||||||
if (fn) {
|
|
||||||
return await fn(params)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const updateFieldValueHandler = async (action, context) => {
|
|
||||||
return await executeActionHandler(
|
|
||||||
context,
|
|
||||||
action.parameters.componentId,
|
|
||||||
ActionTypes.UpdateFieldValue,
|
|
||||||
{
|
|
||||||
type: action.parameters.type,
|
|
||||||
field: action.parameters.field,
|
|
||||||
value: action.parameters.value,
|
|
||||||
}
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
const validateFormHandler = async (action, context) => {
|
|
||||||
return await executeActionHandler(
|
|
||||||
context,
|
|
||||||
action.parameters.componentId,
|
|
||||||
ActionTypes.ValidateForm
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
const refreshDataProviderHandler = async (action, context) => {
|
|
||||||
return await executeActionHandler(
|
|
||||||
context,
|
|
||||||
action.parameters.componentId,
|
|
||||||
ActionTypes.RefreshDatasource
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
const logoutHandler = async action => {
|
const logoutHandler = async action => {
|
||||||
await authStore.actions.logOut()
|
await authStore.actions.logOut()
|
||||||
let redirectUrl = "/builder/auth/login"
|
let redirectUrl = "/builder/auth/login"
|
||||||
|
@ -293,23 +292,6 @@ const logoutHandler = async action => {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const clearFormHandler = async (action, context) => {
|
|
||||||
return await executeActionHandler(
|
|
||||||
context,
|
|
||||||
action.parameters.componentId,
|
|
||||||
ActionTypes.ClearForm
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
const changeFormStepHandler = async (action, context) => {
|
|
||||||
return await executeActionHandler(
|
|
||||||
context,
|
|
||||||
action.parameters.componentId,
|
|
||||||
ActionTypes.ChangeFormStep,
|
|
||||||
action.parameters
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
const closeScreenModalHandler = action => {
|
const closeScreenModalHandler = action => {
|
||||||
let url
|
let url
|
||||||
if (action?.parameters) {
|
if (action?.parameters) {
|
||||||
|
@ -417,16 +399,10 @@ const handlerMap = {
|
||||||
["Duplicate Row"]: duplicateRowHandler,
|
["Duplicate Row"]: duplicateRowHandler,
|
||||||
["Delete Row"]: deleteRowHandler,
|
["Delete Row"]: deleteRowHandler,
|
||||||
["Navigate To"]: navigationHandler,
|
["Navigate To"]: navigationHandler,
|
||||||
["Scroll To Field"]: scrollHandler,
|
|
||||||
["Execute Query"]: queryExecutionHandler,
|
["Execute Query"]: queryExecutionHandler,
|
||||||
["Trigger Automation"]: triggerAutomationHandler,
|
["Trigger Automation"]: triggerAutomationHandler,
|
||||||
["Validate Form"]: validateFormHandler,
|
|
||||||
["Update Field Value"]: updateFieldValueHandler,
|
|
||||||
["Refresh Data Provider"]: refreshDataProviderHandler,
|
|
||||||
["Log Out"]: logoutHandler,
|
["Log Out"]: logoutHandler,
|
||||||
["Clear Form"]: clearFormHandler,
|
|
||||||
["Close Screen Modal"]: closeScreenModalHandler,
|
["Close Screen Modal"]: closeScreenModalHandler,
|
||||||
["Change Form Step"]: changeFormStepHandler,
|
|
||||||
["Update State"]: updateStateHandler,
|
["Update State"]: updateStateHandler,
|
||||||
["Upload File to S3"]: s3UploadHandler,
|
["Upload File to S3"]: s3UploadHandler,
|
||||||
["Export Data"]: exportDataHandler,
|
["Export Data"]: exportDataHandler,
|
||||||
|
@ -461,7 +437,12 @@ export const enrichButtonActions = (actions, context) => {
|
||||||
return actions
|
return actions
|
||||||
}
|
}
|
||||||
|
|
||||||
const handlers = actions.map(def => handlerMap[def["##eventHandlerType"]])
|
// Get handlers for each action. If no bespoke handler is configured, fall
|
||||||
|
// back to simply executing this action from context.
|
||||||
|
const handlers = actions.map(def => {
|
||||||
|
return handlerMap[def["##eventHandlerType"]] || contextActionHandler
|
||||||
|
})
|
||||||
|
|
||||||
return async eventContext => {
|
return async eventContext => {
|
||||||
// Button context is built up as actions are executed.
|
// Button context is built up as actions are executed.
|
||||||
// Inherit any previous button context which may have come from actions
|
// Inherit any previous button context which may have come from actions
|
||||||
|
|
|
@ -23,16 +23,6 @@ export const propsAreSame = (a, b) => {
|
||||||
* Data bindings are enriched, and button actions are enriched.
|
* Data bindings are enriched, and button actions are enriched.
|
||||||
*/
|
*/
|
||||||
export const enrichProps = (props, context, settingsDefinitionMap) => {
|
export const enrichProps = (props, context, settingsDefinitionMap) => {
|
||||||
// Create context of all bindings and data contexts
|
|
||||||
// Duplicate the closest context as "data" which the builder requires
|
|
||||||
const totalContext = {
|
|
||||||
...context,
|
|
||||||
|
|
||||||
// This is only required for legacy bindings that used "data" rather than a
|
|
||||||
// component ID.
|
|
||||||
data: context[context.closestComponentId],
|
|
||||||
}
|
|
||||||
|
|
||||||
// We want to exclude any button actions from enrichment at this stage.
|
// We want to exclude any button actions from enrichment at this stage.
|
||||||
// Extract top level button action settings.
|
// Extract top level button action settings.
|
||||||
let normalProps = { ...props }
|
let normalProps = { ...props }
|
||||||
|
@ -49,13 +39,13 @@ export const enrichProps = (props, context, settingsDefinitionMap) => {
|
||||||
let rawConditions = normalProps._conditions
|
let rawConditions = normalProps._conditions
|
||||||
|
|
||||||
// Enrich all props except button actions
|
// Enrich all props except button actions
|
||||||
let enrichedProps = enrichDataBindings(normalProps, totalContext)
|
let enrichedProps = enrichDataBindings(normalProps, context)
|
||||||
|
|
||||||
// Enrich button actions.
|
// Enrich button actions.
|
||||||
// Actions are enriched into a function at this stage, but actual data
|
// Actions are enriched into a function at this stage, but actual data
|
||||||
// binding enrichment is done dynamically at runtime.
|
// binding enrichment is done dynamically at runtime.
|
||||||
Object.keys(actionProps).forEach(prop => {
|
Object.keys(actionProps).forEach(prop => {
|
||||||
enrichedProps[prop] = enrichButtonActions(actionProps[prop], totalContext)
|
enrichedProps[prop] = enrichButtonActions(actionProps[prop], context)
|
||||||
})
|
})
|
||||||
|
|
||||||
// Conditions
|
// Conditions
|
||||||
|
@ -66,7 +56,7 @@ export const enrichProps = (props, context, settingsDefinitionMap) => {
|
||||||
// action
|
// action
|
||||||
condition.settingValue = enrichButtonActions(
|
condition.settingValue = enrichButtonActions(
|
||||||
rawConditions[idx].settingValue,
|
rawConditions[idx].settingValue,
|
||||||
totalContext
|
context
|
||||||
)
|
)
|
||||||
|
|
||||||
// Since we can't compare functions, we need to assume that conditions
|
// Since we can't compare functions, we need to assume that conditions
|
||||||
|
|
|
@ -19,11 +19,12 @@ export const buildRowEndpoints = API => ({
|
||||||
* @param suppressErrors whether or not to suppress error notifications
|
* @param suppressErrors whether or not to suppress error notifications
|
||||||
*/
|
*/
|
||||||
saveRow: async (row, suppressErrors = false) => {
|
saveRow: async (row, suppressErrors = false) => {
|
||||||
if (!row?.tableId) {
|
const resourceId = row?._viewId || row?.tableId
|
||||||
|
if (!resourceId) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
return await API.post({
|
return await API.post({
|
||||||
url: `/api/${row._viewId || row.tableId}/rows`,
|
url: `/api/${resourceId}/rows`,
|
||||||
body: row,
|
body: row,
|
||||||
suppressErrors,
|
suppressErrors,
|
||||||
})
|
})
|
||||||
|
@ -35,11 +36,12 @@ export const buildRowEndpoints = API => ({
|
||||||
* @param suppressErrors whether or not to suppress error notifications
|
* @param suppressErrors whether or not to suppress error notifications
|
||||||
*/
|
*/
|
||||||
patchRow: async (row, suppressErrors = false) => {
|
patchRow: async (row, suppressErrors = false) => {
|
||||||
if (!row?.tableId && !row?._viewId) {
|
const resourceId = row?._viewId || row?.tableId
|
||||||
|
if (!resourceId) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
return await API.patch({
|
return await API.patch({
|
||||||
url: `/api/${row._viewId || row.tableId}/rows`,
|
url: `/api/${resourceId}/rows`,
|
||||||
body: row,
|
body: row,
|
||||||
suppressErrors,
|
suppressErrors,
|
||||||
})
|
})
|
||||||
|
|
Loading…
Reference in a new issue