diff --git a/packages/builder/src/builderStore/dataBinding.js b/packages/builder/src/builderStore/dataBinding.js index 8445bf9e6d..246590a22e 100644 --- a/packages/builder/src/builderStore/dataBinding.js +++ b/packages/builder/src/builderStore/dataBinding.js @@ -228,7 +228,12 @@ export const getContextProviderComponents = ( /** * Gets all data provider components above a component. */ -export const getActionProviderComponents = (asset, componentId, actionType) => { +export const getActionProviders = ( + asset, + componentId, + actionType, + options = { includeSelf: false } +) => { if (!asset || !componentId) { return [] } @@ -236,13 +241,30 @@ export const getActionProviderComponents = (asset, componentId, actionType) => { // Get the component tree leading up to this component, ignoring the component // itself const path = findComponentPath(asset.props, componentId) - path.pop() + if (!options?.includeSelf) { + path.pop() + } - // Filter by only data provider components - return path.filter(component => { + // Find matching contexts and generate bindings + let providers = [] + path.forEach(component => { const def = store.actions.components.getDefinition(component._component) - return def?.actions?.includes(actionType) + const actions = (def?.actions || []).map(action => { + return typeof action === "string" ? { type: action } : action + }) + const action = actions.find(x => x.type === actionType) + if (action) { + let runtimeBinding = component._id + if (action.suffix) { + runtimeBinding += `-${action.suffix}` + } + providers.push({ + readableBinding: component._instanceName, + runtimeBinding, + }) + } }) + return providers } /** diff --git a/packages/builder/src/components/design/settings/controls/ButtonActionEditor/actions/ChangeFormStep.svelte b/packages/builder/src/components/design/settings/controls/ButtonActionEditor/actions/ChangeFormStep.svelte index 81a2119474..5905d240dc 100644 --- a/packages/builder/src/components/design/settings/controls/ButtonActionEditor/actions/ChangeFormStep.svelte +++ b/packages/builder/src/components/design/settings/controls/ButtonActionEditor/actions/ChangeFormStep.svelte @@ -1,17 +1,19 @@ @@ -17,8 +19,8 @@ x._instanceName} - getOptionValue={x => x._id} + getOptionLabel={x => x.readableBinding} + getOptionValue={x => x.runtimeBinding} /> diff --git a/packages/builder/src/components/design/settings/controls/ButtonActionEditor/actions/SaveRow.svelte b/packages/builder/src/components/design/settings/controls/ButtonActionEditor/actions/SaveRow.svelte index 27b6463ffa..9f70272d78 100644 --- a/packages/builder/src/components/design/settings/controls/ButtonActionEditor/actions/SaveRow.svelte +++ b/packages/builder/src/components/design/settings/controls/ButtonActionEditor/actions/SaveRow.svelte @@ -2,29 +2,19 @@ import { Select, Label, Body, Checkbox, Input } from "@budibase/bbui" import { store, currentAsset } from "builderStore" import { tables, viewsV2 } from "stores/backend" - import { - getContextProviderComponents, - getSchemaForDatasourcePlus, - } from "builderStore/dataBinding" + import { getSchemaForDatasourcePlus } from "builderStore/dataBinding" import SaveFields from "./SaveFields.svelte" + import { getDatasourceLikeProviders } from "components/design/settings/controls/ButtonActionEditor/actions/utils" export let parameters export let bindings = [] export let nested - $: formComponents = getContextProviderComponents( - $currentAsset, - $store.selectedComponentId, - "form", - { includeSelf: nested } - ) - $: schemaComponents = getContextProviderComponents( - $currentAsset, - $store.selectedComponentId, - "schema", - { includeSelf: nested } - ) - $: providerOptions = getProviderOptions(formComponents, schemaComponents) + $: providerOptions = getDatasourceLikeProviders({ + asset: $currentAsset, + componentId: $store.selectedComponentId, + nested, + }) $: schemaFields = getSchemaFields(parameters?.tableId) $: tableOptions = $tables.list.map(table => ({ label: table.name, @@ -36,40 +26,6 @@ })) $: options = [...(tableOptions || []), ...(viewOptions || [])] - // 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) - } - - // Gets options for valid context keys which provide valid data to submit - const getProviderOptions = (formComponents, schemaComponents) => { - const formContexts = formComponents.map(component => ({ - component, - context: extractComponentContext(component, "form"), - })) - const schemaContexts = schemaComponents.map(component => ({ - component, - context: extractComponentContext(component, "schema"), - })) - const allContexts = formContexts.concat(schemaContexts) - - return allContexts.map(({ component, context }) => { - let runtimeBinding = component._id - if (context.suffix) { - runtimeBinding += `-${context.suffix}` - } - return { - label: component._instanceName, - value: runtimeBinding, - } - }) - } - const getSchemaFields = resourceId => { const { schema } = getSchemaForDatasourcePlus(resourceId) return Object.values(schema || {}) diff --git a/packages/builder/src/components/design/settings/controls/ButtonActionEditor/actions/ScrollTo.svelte b/packages/builder/src/components/design/settings/controls/ButtonActionEditor/actions/ScrollTo.svelte index 49a93d71dd..e73884495d 100644 --- a/packages/builder/src/components/design/settings/controls/ButtonActionEditor/actions/ScrollTo.svelte +++ b/packages/builder/src/components/design/settings/controls/ButtonActionEditor/actions/ScrollTo.svelte @@ -1,22 +1,36 @@
@@ -24,8 +38,8 @@ x._instanceName} - getOptionValue={x => x._id} + getOptionLabel={x => x.readableBinding} + getOptionValue={x => x.runtimeBinding} /> x._instanceName} - getOptionValue={x => x._id} + getOptionLabel={x => x.readableBinding} + getOptionValue={x => x.runtimeBinding} />
diff --git a/packages/builder/src/components/design/settings/controls/ButtonActionEditor/actions/utils.js b/packages/builder/src/components/design/settings/controls/ButtonActionEditor/actions/utils.js new file mode 100644 index 0000000000..aa076fdd3e --- /dev/null +++ b/packages/builder/src/components/design/settings/controls/ButtonActionEditor/actions/utils.js @@ -0,0 +1,82 @@ +import { getContextProviderComponents } from "builderStore/dataBinding" +import { store } from "builderStore" +import { capitalise } from "helpers" + +// Generates bindings for all components that provider "datasource like" +// contexts. This includes "form" contexts and "schema" contexts. This is used +// by various button actions as candidates for whole "row" objects. +// Some examples are saving rows or duplicating rows. +export const getDatasourceLikeProviders = ({ asset, componentId, nested }) => { + // Get all form context providers + const formComponents = getContextProviderComponents( + asset, + componentId, + "form", + { includeSelf: nested } + ) + + // Get all schema context providers + const schemaComponents = getContextProviderComponents( + asset, + componentId, + "schema", + { includeSelf: nested } + ) + + // Generate contexts for all form providers + const formContexts = formComponents.map(component => ({ + component, + context: extractComponentContext(component, "form"), + })) + + // Generate contexts for all schema providers + const schemaContexts = schemaComponents.map(component => ({ + component, + context: extractComponentContext(component, "schema"), + })) + + // Check for duplicate contexts by the same component. In this case, attempt + // to label contexts with their suffixes + schemaContexts.forEach(schemaContext => { + // Check if we have a form context for this component + const id = schemaContext.component._id + const existing = formContexts.find(x => x.component._id === id) + if (existing) { + if (existing.context.suffix) { + const suffix = capitalise(existing.context.suffix) + existing.readableSuffix = ` - ${suffix}` + } + if (schemaContext.context.suffix) { + const suffix = capitalise(schemaContext.context.suffix) + schemaContext.readableSuffix = ` - ${suffix}` + } + } + }) + + // Generate bindings for all contexts + const allContexts = formContexts.concat(schemaContexts) + return allContexts.map(({ component, context, readableSuffix }) => { + let readableBinding = component._instanceName + let runtimeBinding = component._id + if (context.suffix) { + runtimeBinding += `-${context.suffix}` + } + if (readableSuffix) { + readableBinding += readableSuffix + } + return { + label: readableBinding, + value: runtimeBinding, + } + }) +} + +// Gets a context definition of a certain type from a component definition +const extractComponentContext = (component, contextType) => { + const def = store.actions.components.getDefinition(component?._component) + if (!def) { + return null + } + const contexts = Array.isArray(def.context) ? def.context : [def.context] + return contexts.find(context => context?.type === contextType) +} diff --git a/packages/client/manifest.json b/packages/client/manifest.json index 202dc8bd2f..07645d874a 100644 --- a/packages/client/manifest.json +++ b/packages/client/manifest.json @@ -6165,6 +6165,24 @@ "defaultValue": "spectrum--medium" } ], + "actions": [ + { + "type": "ValidateForm", + "suffix": "form" + }, + { + "type": "ClearForm", + "suffix": "form" + }, + { + "type": "UpdateFieldValue", + "suffix": "form" + }, + { + "type": "ScrollTo", + "suffix": "form" + } + ], "context": [ { "type": "form", @@ -6420,7 +6438,8 @@ ], "context": { "type": "schema" - } + }, + "actions": ["RefreshDatasource"] }, "bbreferencefield": { "devComment": "As bb reference is only used for user subtype for now, we are using user for icon and labels", diff --git a/packages/client/src/components/app/ButtonGroup.svelte b/packages/client/src/components/app/ButtonGroup.svelte index 87b0990701..4954704b1b 100644 --- a/packages/client/src/components/app/ButtonGroup.svelte +++ b/packages/client/src/components/app/ButtonGroup.svelte @@ -3,9 +3,9 @@ import Block from "../Block.svelte" export let buttons = [] - export let direction - export let hAlign - export let vAlign + export let direction = "row" + export let hAlign = "left" + export let vAlign = "top" export let gap = "S" diff --git a/packages/client/src/components/app/GridBlock.svelte b/packages/client/src/components/app/GridBlock.svelte index 801e1a4d0a..0b1c12524a 100644 --- a/packages/client/src/components/app/GridBlock.svelte +++ b/packages/client/src/components/app/GridBlock.svelte @@ -27,8 +27,12 @@ builderStore, notificationStore, enrichButtonActions, + ActionTypes, + createContextStore, } = getContext("sdk") + let grid + $: columnWhitelist = columns?.map(col => col.name) $: schemaOverrides = getSchemaOverrides(columns) $: enrichedButtons = enrichButtons(buttons) @@ -53,11 +57,16 @@ text: settings.text, type: settings.type, onClick: async row => { - // We add a fake context binding in here, which allows us to pretend - // that the grid provides a "schema" binding - that lets us use the - // clicked row in things like save row actions - const enrichedContext = { ...get(context), [get(component).id]: row } - const fn = enrichButtonActions(settings.onClick, enrichedContext) + // Create a fake, ephemeral context to run the buttons actions with + const id = get(component).id + const gridContext = createContextStore(context) + gridContext.actions.provideData(id, row) + gridContext.actions.provideAction( + id, + ActionTypes.RefreshDatasource, + () => grid?.getContext()?.rows.actions.refreshData() + ) + const fn = enrichButtonActions(settings.onClick, get(gridContext)) return await fn?.({ row }) }, })) @@ -69,6 +78,7 @@ class:in-builder={$builderStore.inBuilder} >