From cf43cf765c2dbf6da98e20790db8cf218d01e073 Mon Sep 17 00:00:00 2001 From: Andrew Kingston Date: Mon, 1 Feb 2021 18:51:22 +0000 Subject: [PATCH] Add custom component actions. Simplify client context. Add form validation action --- .../builder/src/builderStore/dataBinding.js | 54 ++++++------ .../EventsEditor/EventEditor.svelte | 11 +-- .../EventsEditor/actions/ValidateForm.svelte | 39 ++++++++ .../EventsEditor/actions/index.js | 5 ++ .../client/src/components/ClientApp.svelte | 9 +- .../client/src/components/Component.svelte | 10 +-- .../client/src/components/DataProvider.svelte | 15 ---- .../client/src/components/Provider.svelte | 29 ++++++ packages/client/src/constants.js | 4 + packages/client/src/sdk.js | 8 +- packages/client/src/store/binding.js | 21 ----- packages/client/src/store/context.js | 34 +++++++ packages/client/src/store/data.js | 26 ------ packages/client/src/store/index.js | 5 +- packages/client/src/utils/buttonActions.js | 23 ++++- packages/client/src/utils/componentProps.js | 18 ++-- packages/standard-components/manifest.json | 17 ++-- packages/standard-components/src/Form.svelte | 6 +- packages/standard-components/src/Input.svelte | 14 --- packages/standard-components/src/List.svelte | 6 +- .../standard-components/src/NewRow.svelte | 6 +- .../standard-components/src/RowDetail.svelte | 6 +- .../src/charts/BarChart.svelte | 1 - .../src/charts/CandleStickChart.svelte | 1 - .../src/charts/LineChart.svelte | 1 - .../src/charts/PieChart.svelte | 1 - .../standard-components/src/forms/Form.svelte | 88 ++++++++++++------- .../src/grid/Component.svelte | 1 - 28 files changed, 264 insertions(+), 195 deletions(-) create mode 100644 packages/builder/src/components/design/PropertiesPanel/PropertyControls/EventsEditor/actions/ValidateForm.svelte delete mode 100644 packages/client/src/components/DataProvider.svelte create mode 100644 packages/client/src/components/Provider.svelte delete mode 100644 packages/client/src/store/binding.js create mode 100644 packages/client/src/store/context.js delete mode 100644 packages/client/src/store/data.js delete mode 100644 packages/standard-components/src/Input.svelte 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 @@ + +
+ + + + {/each} + {/if} + +
+ + 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}
-
+