diff --git a/packages/builder/src/builderStore/dataBinding.js b/packages/builder/src/builderStore/dataBinding.js
index 679a77ca2b..65241b7875 100644
--- a/packages/builder/src/builderStore/dataBinding.js
+++ b/packages/builder/src/builderStore/dataBinding.js
@@ -1,7 +1,7 @@
import { cloneDeep } from "lodash/fp"
import { get } from "svelte/store"
import { backendUiStore, store } from "builderStore"
-import { findAllMatchingComponents, findComponentPath } from "./storeUtils"
+import { findComponentPath } from "./storeUtils"
import { TableNames } from "../constants"
// Regex to match all instances of template strings
@@ -11,9 +11,7 @@ const CAPTURE_VAR_INSIDE_TEMPLATE = /{{([^}]+)}}/g
* Gets all bindable data context fields and instance fields.
*/
export const getBindableProperties = (rootComponent, componentId) => {
- const contextBindings = getContextBindings(rootComponent, componentId)
- const componentBindings = getComponentBindings(rootComponent)
- return [...contextBindings, ...componentBindings]
+ return getContextBindings(rootComponent, componentId)
}
/**
@@ -36,6 +34,30 @@ export const getDataProviderComponents = (rootComponent, componentId) => {
})
}
+/**
+ * Gets all data provider components above a component.
+ */
+export const getActionProviderComponents = (
+ rootComponent,
+ componentId,
+ actionType
+) => {
+ if (!rootComponent || !componentId) {
+ return []
+ }
+
+ // Get the component tree leading up to this component, ignoring the component
+ // itself
+ const path = findComponentPath(rootComponent, componentId)
+ path.pop()
+
+ // Filter by only data provider components
+ return path.filter(component => {
+ const def = store.actions.components.getDefinition(component._component)
+ return def?.actions?.includes(actionType)
+ })
+}
+
/**
* Gets a datasource object for a certain data provider component
*/
@@ -149,30 +171,6 @@ export const getContextBindings = (rootComponent, componentId) => {
return contextBindings
}
-/**
- * Gets all bindable components. These are form components which allow their
- * values to be bound to.
- */
-export const getComponentBindings = rootComponent => {
- if (!rootComponent) {
- return []
- }
- const componentSelector = component => {
- const type = component._component
- const definition = store.actions.components.getDefinition(type)
- return definition?.bindable
- }
- const components = findAllMatchingComponents(rootComponent, componentSelector)
- return components.map(component => {
- return {
- type: "instance",
- providerId: component._id,
- runtimeBinding: `${component._id}`,
- readableBinding: `${component._instanceName}`,
- }
- })
-}
-
/**
* Gets a schema for a datasource object.
*/
diff --git a/packages/builder/src/components/design/PropertiesPanel/PropertyControls/EventsEditor/EventEditor.svelte b/packages/builder/src/components/design/PropertiesPanel/PropertyControls/EventsEditor/EventEditor.svelte
index 0e52aa8f76..7dd7fc5044 100644
--- a/packages/builder/src/components/design/PropertiesPanel/PropertyControls/EventsEditor/EventEditor.svelte
+++ b/packages/builder/src/components/design/PropertiesPanel/PropertyControls/EventsEditor/EventEditor.svelte
@@ -1,15 +1,6 @@
+
+
+
+
+
+
+
diff --git a/packages/builder/src/components/design/PropertiesPanel/PropertyControls/EventsEditor/actions/index.js b/packages/builder/src/components/design/PropertiesPanel/PropertyControls/EventsEditor/actions/index.js
index 677c646728..e851bdb4be 100644
--- a/packages/builder/src/components/design/PropertiesPanel/PropertyControls/EventsEditor/actions/index.js
+++ b/packages/builder/src/components/design/PropertiesPanel/PropertyControls/EventsEditor/actions/index.js
@@ -3,6 +3,7 @@ import SaveRow from "./SaveRow.svelte"
import DeleteRow from "./DeleteRow.svelte"
import ExecuteQuery from "./ExecuteQuery.svelte"
import TriggerAutomation from "./TriggerAutomation.svelte"
+import ValidateForm from "./ValidateForm.svelte"
// defines what actions are available, when adding a new one
// the component is the setup panel for the action
@@ -30,4 +31,8 @@ export default [
name: "Trigger Automation",
component: TriggerAutomation,
},
+ {
+ name: "Validate Form",
+ component: ValidateForm,
+ },
]
diff --git a/packages/client/src/components/ClientApp.svelte b/packages/client/src/components/ClientApp.svelte
index 92a050a91e..cad74a3e63 100644
--- a/packages/client/src/components/ClientApp.svelte
+++ b/packages/client/src/components/ClientApp.svelte
@@ -4,12 +4,17 @@
import Component from "./Component.svelte"
import NotificationDisplay from "./NotificationDisplay.svelte"
import SDK from "../sdk"
- import { createDataStore, initialise, screenStore, authStore } from "../store"
+ import {
+ createContextStore,
+ initialise,
+ screenStore,
+ authStore,
+ } from "../store"
// Provide contexts
setContext("sdk", SDK)
setContext("component", writable({}))
- setContext("data", createDataStore())
+ setContext("context", createContextStore())
let loaded = false
diff --git a/packages/client/src/components/Component.svelte b/packages/client/src/components/Component.svelte
index c57a85b7e3..a9d80a3c5f 100644
--- a/packages/client/src/components/Component.svelte
+++ b/packages/client/src/components/Component.svelte
@@ -4,7 +4,7 @@
import * as ComponentLibrary from "@budibase/standard-components"
import Router from "./Router.svelte"
import { enrichProps, propsAreSame } from "../utils/componentProps"
- import { authStore, bindingStore, builderStore } from "../store"
+ import { authStore, builderStore } from "../store"
import { hashString } from "../utils/hash"
export let definition = {}
@@ -22,7 +22,7 @@
let latestUpdateTime
// Get contexts
- const dataContext = getContext("data")
+ const context = getContext("context")
// Create component context
const componentStore = writable({})
@@ -32,7 +32,7 @@
$: constructor = getComponentConstructor(definition._component)
$: children = definition._children || []
$: id = definition._id
- $: updateComponentProps(definition, $dataContext, $bindingStore, $authStore)
+ $: updateComponentProps(definition, $context, $authStore)
$: styles = definition._styles
// Update component context
@@ -53,13 +53,13 @@
}
// Enriches any string component props using handlebars
- const updateComponentProps = async (definition, context, bindings, user) => {
+ const updateComponentProps = async (definition, context, user) => {
// Record the timestamp so we can reference it after enrichment
latestUpdateTime = Date.now()
const enrichmentTime = latestUpdateTime
// Enrich props with context
- const enrichedProps = await enrichProps(definition, context, bindings, user)
+ const enrichedProps = await enrichProps(definition, context, user)
// Abandon this update if a newer update has started
if (enrichmentTime !== latestUpdateTime) {
diff --git a/packages/client/src/components/DataProvider.svelte b/packages/client/src/components/DataProvider.svelte
deleted file mode 100644
index 0e926b4973..0000000000
--- a/packages/client/src/components/DataProvider.svelte
+++ /dev/null
@@ -1,15 +0,0 @@
-
-
-
diff --git a/packages/client/src/components/Provider.svelte b/packages/client/src/components/Provider.svelte
new file mode 100644
index 0000000000..f1349796eb
--- /dev/null
+++ b/packages/client/src/components/Provider.svelte
@@ -0,0 +1,29 @@
+
+
+
diff --git a/packages/client/src/constants.js b/packages/client/src/constants.js
index f5bdb4bb10..effb8f2449 100644
--- a/packages/client/src/constants.js
+++ b/packages/client/src/constants.js
@@ -1,3 +1,7 @@
export const TableNames = {
USERS: "ta_users",
}
+
+export const ActionTypes = {
+ ValidateForm: "ValidateForm",
+}
diff --git a/packages/client/src/sdk.js b/packages/client/src/sdk.js
index b5efe1257c..1a3a4177a8 100644
--- a/packages/client/src/sdk.js
+++ b/packages/client/src/sdk.js
@@ -4,12 +4,12 @@ import {
notificationStore,
routeStore,
screenStore,
- bindingStore,
builderStore,
} from "./store"
import { styleable } from "./utils/styleable"
import { linkable } from "./utils/linkable"
-import DataProvider from "./components/DataProvider.svelte"
+import Provider from "./components/Provider.svelte"
+import { ActionTypes } from "./constants"
export default {
API,
@@ -20,6 +20,6 @@ export default {
builderStore,
styleable,
linkable,
- DataProvider,
- setBindableValue: bindingStore.actions.setBindableValue,
+ Provider,
+ ActionTypes,
}
diff --git a/packages/client/src/store/binding.js b/packages/client/src/store/binding.js
deleted file mode 100644
index e9ab28831d..0000000000
--- a/packages/client/src/store/binding.js
+++ /dev/null
@@ -1,21 +0,0 @@
-import { writable } from "svelte/store"
-
-const createBindingStore = () => {
- const store = writable({})
-
- const setBindableValue = (componentId, value) => {
- store.update(state => {
- if (componentId) {
- state[componentId] = value
- }
- return state
- })
- }
-
- return {
- subscribe: store.subscribe,
- actions: { setBindableValue },
- }
-}
-
-export const bindingStore = createBindingStore()
diff --git a/packages/client/src/store/context.js b/packages/client/src/store/context.js
new file mode 100644
index 0000000000..a040c5bd20
--- /dev/null
+++ b/packages/client/src/store/context.js
@@ -0,0 +1,34 @@
+import { writable } from "svelte/store"
+import { cloneDeep } from "lodash/fp"
+
+export const createContextStore = existingContext => {
+ const store = writable({ ...existingContext })
+
+ // Adds a data context layer to the tree
+ const provideData = (componentId, data) => {
+ store.update(state => {
+ if (componentId) {
+ state[componentId] = data
+ state[`${componentId}_draft`] = cloneDeep(data)
+ state.closestComponentId = componentId
+ }
+ return state
+ })
+ }
+
+ // Adds an action context layer to the tree
+ const provideAction = (componentId, actionType, callback) => {
+ store.update(state => {
+ if (actionType && componentId) {
+ state[`${componentId}_${actionType}`] = callback
+ }
+ return state
+ })
+ }
+
+ return {
+ subscribe: store.subscribe,
+ update: store.update,
+ actions: { provideData, provideAction },
+ }
+}
diff --git a/packages/client/src/store/data.js b/packages/client/src/store/data.js
deleted file mode 100644
index 5ff2b9b631..0000000000
--- a/packages/client/src/store/data.js
+++ /dev/null
@@ -1,26 +0,0 @@
-import { writable } from "svelte/store"
-import { cloneDeep } from "lodash/fp"
-
-export const createDataStore = existingContext => {
- const store = writable({ ...existingContext })
-
- // Adds a context layer to the data context tree
- const addContext = (row, componentId) => {
- store.update(state => {
- if (componentId) {
- state[componentId] = row
- state[`${componentId}_draft`] = cloneDeep(row)
- state.closestComponentId = componentId
- }
- return state
- })
- }
-
- return {
- subscribe: store.subscribe,
- update: store.update,
- actions: { addContext },
- }
-}
-
-export const dataStore = createDataStore()
diff --git a/packages/client/src/store/index.js b/packages/client/src/store/index.js
index ae1a477c8f..575c5d98f2 100644
--- a/packages/client/src/store/index.js
+++ b/packages/client/src/store/index.js
@@ -3,10 +3,9 @@ export { notificationStore } from "./notification"
export { routeStore } from "./routes"
export { screenStore } from "./screens"
export { builderStore } from "./builder"
-export { bindingStore } from "./binding"
-// Data stores are layered and duplicated, so it is not a singleton
-export { createDataStore, dataStore } from "./data"
+// Context stores are layered and duplicated, so it is not a singleton
+export { createContextStore } from "./context"
// Initialises an app by loading screens and routes
export { initialise } from "./initialise"
diff --git a/packages/client/src/utils/buttonActions.js b/packages/client/src/utils/buttonActions.js
index 178abd328d..e293223c5d 100644
--- a/packages/client/src/utils/buttonActions.js
+++ b/packages/client/src/utils/buttonActions.js
@@ -2,6 +2,7 @@ import { get } from "svelte/store"
import { enrichDataBinding, enrichDataBindings } from "./enrichDataBinding"
import { routeStore, builderStore } from "../store"
import { saveRow, deleteRow, executeQuery, triggerAutomation } from "../api"
+import { ActionTypes } from "../constants"
const saveRowHandler = async (action, context) => {
const { fields, providerId } = action.parameters
@@ -59,12 +60,21 @@ const queryExecutionHandler = async (action, context) => {
})
}
+const validateFormHandler = async (action, context) => {
+ const { componentId } = action.parameters
+ const fn = context[`${componentId}_${ActionTypes.ValidateForm}`]
+ if (fn) {
+ return await fn()
+ }
+}
+
const handlerMap = {
["Save Row"]: saveRowHandler,
["Delete Row"]: deleteRowHandler,
["Navigate To"]: navigationHandler,
["Execute Query"]: queryExecutionHandler,
["Trigger Automation"]: triggerAutomationHandler,
+ ["Validate Form"]: validateFormHandler,
}
/**
@@ -79,7 +89,18 @@ export const enrichButtonActions = (actions, context) => {
const handlers = actions.map(def => handlerMap[def["##eventHandlerType"]])
return async () => {
for (let i = 0; i < handlers.length; i++) {
- await handlers[i](actions[i], context)
+ try {
+ const result = await handlers[i](actions[i], context)
+ // A handler returning `false` is a flag to stop execution of handlers
+ if (result === false) {
+ return
+ }
+ } catch (error) {
+ console.error("Error while executing button handler")
+ console.error(error)
+ // Stop executing on an error
+ return
+ }
}
}
}
diff --git a/packages/client/src/utils/componentProps.js b/packages/client/src/utils/componentProps.js
index fb421ca9fb..c4d2d668b1 100644
--- a/packages/client/src/utils/componentProps.js
+++ b/packages/client/src/utils/componentProps.js
@@ -21,7 +21,7 @@ export const propsAreSame = (a, b) => {
* Enriches component props.
* Data bindings are enriched, and button actions are enriched.
*/
-export const enrichProps = async (props, dataContexts, dataBindings, user) => {
+export const enrichProps = async (props, context, user) => {
// Exclude all private props that start with an underscore
let validProps = {}
Object.entries(props)
@@ -32,20 +32,22 @@ export const enrichProps = async (props, dataContexts, dataBindings, user) => {
// Create context of all bindings and data contexts
// Duplicate the closest context as "data" which the builder requires
- const context = {
- ...dataContexts,
- ...dataBindings,
+ const totalContext = {
+ ...context,
user,
- data: dataContexts[dataContexts.closestComponentId],
- data_draft: dataContexts[`${dataContexts.closestComponentId}_draft`],
+ data: context[context.closestComponentId],
+ data_draft: context[`${context.closestComponentId}_draft`],
}
// Enrich all data bindings in top level props
- let enrichedProps = await enrichDataBindings(validProps, context)
+ let enrichedProps = await enrichDataBindings(validProps, totalContext)
// Enrich button actions if they exist
if (props._component.endsWith("/button") && enrichedProps.onClick) {
- enrichedProps.onClick = enrichButtonActions(enrichedProps.onClick, context)
+ enrichedProps.onClick = enrichButtonActions(
+ enrichedProps.onClick,
+ totalContext
+ )
}
return enrichedProps
diff --git a/packages/standard-components/manifest.json b/packages/standard-components/manifest.json
index 9733007bb9..0d5a61ad4c 100644
--- a/packages/standard-components/manifest.json
+++ b/packages/standard-components/manifest.json
@@ -1051,11 +1051,12 @@
},
"form": {
"name": "Form",
- "icon": "ri-file-edit-line",
+ "icon": "ri-file-text-line",
"styleable": true,
"hasChildren": true,
"dataProvider": true,
"datasourceSetting": "datasource",
+ "actions": ["ValidateForm"],
"settings": [
{
"type": "datasource",
@@ -1102,7 +1103,7 @@
},
"fieldgroup": {
"name": "Field Group",
- "icon": "ri-edit-box-line",
+ "icon": "ri-layout-row-line",
"styleable": true,
"hasChildren": true,
"settings": [
@@ -1130,7 +1131,7 @@
},
"stringfield": {
"name": "Text Field",
- "icon": "ri-edit-box-line",
+ "icon": "ri-t-box-line",
"styleable": true,
"settings": [
{
@@ -1174,7 +1175,7 @@
},
"optionsfield": {
"name": "Options Picker",
- "icon": "ri-edit-box-line",
+ "icon": "ri-file-list-line",
"styleable": true,
"settings": [
{
@@ -1197,7 +1198,7 @@
},
"booleanfield": {
"name": "Checkbox",
- "icon": "ri-edit-box-line",
+ "icon": "ri-checkbox-line",
"styleable": true,
"settings": [
{
@@ -1219,7 +1220,7 @@
},
"longformfield": {
"name": "Rich Text",
- "icon": "ri-edit-box-line",
+ "icon": "ri-file-edit-line",
"styleable": true,
"settings": [
{
@@ -1270,7 +1271,7 @@
},
"attachmentfield": {
"name": "Attachment",
- "icon": "ri-calendar-line",
+ "icon": "ri-image-edit-line",
"styleable": true,
"settings": [
{
@@ -1287,7 +1288,7 @@
},
"relationshipfield": {
"name": "Relationship Picker",
- "icon": "ri-edit-box-line",
+ "icon": "ri-links-line",
"styleable": true,
"settings": [
{
diff --git a/packages/standard-components/src/Form.svelte b/packages/standard-components/src/Form.svelte
index 0da894b31e..bec99de553 100644
--- a/packages/standard-components/src/Form.svelte
+++ b/packages/standard-components/src/Form.svelte
@@ -14,7 +14,7 @@
const { styleable, API } = getContext("sdk")
const component = getContext("component")
- const dataContext = getContext("data")
+ const context = getContext("context")
export let wide = false
@@ -23,7 +23,7 @@
let fields = []
// Fetch info about the closest data context
- $: getFormData($dataContext[$dataContext.closestComponentId])
+ $: getFormData($context[$context.closestComponentId])
const getFormData = async context => {
if (context) {
@@ -32,7 +32,7 @@
fields = Object.keys(schema ?? {})
// Use the draft version for editing
- row = $dataContext[`${$dataContext.closestComponentId}_draft`]
+ row = $context[`${$context.closestComponentId}_draft`]
}
}
diff --git a/packages/standard-components/src/Input.svelte b/packages/standard-components/src/Input.svelte
deleted file mode 100644
index 03dd6ea023..0000000000
--- a/packages/standard-components/src/Input.svelte
+++ /dev/null
@@ -1,14 +0,0 @@
-
-
-
diff --git a/packages/standard-components/src/List.svelte b/packages/standard-components/src/List.svelte
index 9ee59a79b7..ca484d75d3 100644
--- a/packages/standard-components/src/List.svelte
+++ b/packages/standard-components/src/List.svelte
@@ -2,7 +2,7 @@
import { getContext } from "svelte"
import { isEmpty } from "lodash/fp"
- const { API, styleable, DataProvider, builderStore } = getContext("sdk")
+ const { API, styleable, Provider, builderStore } = getContext("sdk")
const component = getContext("component")
export let datasource = []
@@ -26,9 +26,9 @@
Add some components too
{:else}
{#each rows as row}
-
+
-
+
{/each}
{/if}
{:else if loaded && $builderStore.inBuilder}
diff --git a/packages/standard-components/src/NewRow.svelte b/packages/standard-components/src/NewRow.svelte
index 68dcac0b11..9830c87015 100644
--- a/packages/standard-components/src/NewRow.svelte
+++ b/packages/standard-components/src/NewRow.svelte
@@ -1,14 +1,14 @@
diff --git a/packages/standard-components/src/RowDetail.svelte b/packages/standard-components/src/RowDetail.svelte
index 7f01341665..ed46fd6927 100644
--- a/packages/standard-components/src/RowDetail.svelte
+++ b/packages/standard-components/src/RowDetail.svelte
@@ -1,7 +1,7 @@
-
+
{/if}
-
+