Merge pull request #12350 from Budibase/settings-enhancements

Settings enhancements
This commit is contained in:
Andrew Kingston 2023-11-22 12:30:36 +00:00 committed by GitHub
commit 5603f18aac
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
15 changed files with 248 additions and 163 deletions

View File

@ -228,7 +228,12 @@ export const getContextProviderComponents = (
/** /**
* Gets all data provider components above a component. * 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) { if (!asset || !componentId) {
return [] return []
} }
@ -236,13 +241,30 @@ export const getActionProviderComponents = (asset, componentId, actionType) => {
// Get the component tree leading up to this component, ignoring the component // Get the component tree leading up to this component, ignoring the component
// itself // itself
const path = findComponentPath(asset.props, componentId) const path = findComponentPath(asset.props, componentId)
if (!options?.includeSelf) {
path.pop() path.pop()
}
// Filter by only data provider components // Find matching contexts and generate bindings
return path.filter(component => { let providers = []
path.forEach(component => {
const def = store.actions.components.getDefinition(component._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
} }
/** /**

View File

@ -1,17 +1,19 @@
<script> <script>
import { Select, Label } from "@budibase/bbui" import { Select, Label } from "@budibase/bbui"
import { currentAsset, store } from "builderStore" import { currentAsset, store } from "builderStore"
import { getActionProviderComponents } from "builderStore/dataBinding" import { getActionProviders } from "builderStore/dataBinding"
import { onMount } from "svelte" import { onMount } from "svelte"
import DrawerBindableInput from "components/common/bindings/DrawerBindableInput.svelte" import DrawerBindableInput from "components/common/bindings/DrawerBindableInput.svelte"
export let parameters export let parameters
export let bindings = [] export let bindings = []
export let nested
$: actionProviders = getActionProviderComponents( $: actionProviders = getActionProviders(
$currentAsset, $currentAsset,
$store.selectedComponentId, $store.selectedComponentId,
"ChangeFormStep" "ChangeFormStep",
{ includeSelf: nested }
) )
const typeOptions = [ const typeOptions = [
@ -46,8 +48,8 @@
placeholder={null} placeholder={null}
bind:value={parameters.componentId} bind:value={parameters.componentId}
options={actionProviders} options={actionProviders}
getOptionLabel={x => x._instanceName} getOptionLabel={x => x.readableBinding}
getOptionValue={x => x._id} getOptionValue={x => x.runtimeBinding}
/> />
<Label small>Step</Label> <Label small>Step</Label>
<Select bind:value={parameters.type} options={typeOptions} /> <Select bind:value={parameters.type} options={typeOptions} />

View File

@ -1,14 +1,16 @@
<script> <script>
import { Select, Label } from "@budibase/bbui" import { Select, Label } from "@budibase/bbui"
import { currentAsset, store } from "builderStore" import { currentAsset, store } from "builderStore"
import { getActionProviderComponents } from "builderStore/dataBinding" import { getActionProviders } from "builderStore/dataBinding"
export let parameters export let parameters
export let nested
$: actionProviders = getActionProviderComponents( $: actionProviders = getActionProviders(
$currentAsset, $currentAsset,
$store.selectedComponentId, $store.selectedComponentId,
"ClearForm" "ClearForm",
{ includeSelf: nested }
) )
</script> </script>
@ -17,8 +19,8 @@
<Select <Select
bind:value={parameters.componentId} bind:value={parameters.componentId}
options={actionProviders} options={actionProviders}
getOptionLabel={x => x._instanceName} getOptionLabel={x => x.readableBinding}
getOptionValue={x => x._id} getOptionValue={x => x.runtimeBinding}
/> />
</div> </div>

View File

@ -2,27 +2,20 @@
import { Select, Label, Body, Checkbox, Input } from "@budibase/bbui" import { Select, Label, Body, Checkbox, Input } from "@budibase/bbui"
import { store, currentAsset } from "builderStore" import { store, currentAsset } from "builderStore"
import { tables, viewsV2 } from "stores/backend" import { tables, viewsV2 } from "stores/backend"
import { import { getSchemaForDatasourcePlus } from "builderStore/dataBinding"
getContextProviderComponents,
getSchemaForDatasourcePlus,
} from "builderStore/dataBinding"
import SaveFields from "./SaveFields.svelte" import SaveFields from "./SaveFields.svelte"
import { getDatasourceLikeProviders } from "components/design/settings/controls/ButtonActionEditor/actions/utils"
export let parameters export let parameters
export let bindings = [] export let bindings = []
export let nested
$: formComponents = getContextProviderComponents( $: providerOptions = getDatasourceLikeProviders({
$currentAsset, asset: $currentAsset,
$store.selectedComponentId, componentId: $store.selectedComponentId,
"form" nested,
) })
$: schemaComponents = getContextProviderComponents( $: schemaFields = getSchemaFields(parameters?.tableId)
$currentAsset,
$store.selectedComponentId,
"schema"
)
$: providerOptions = getProviderOptions(formComponents, schemaComponents)
$: schemaFields = getSchemaFields($currentAsset, parameters?.tableId)
$: tableOptions = $tables.list.map(table => ({ $: tableOptions = $tables.list.map(table => ({
label: table.name, label: table.name,
resourceId: table._id, resourceId: table._id,
@ -33,44 +26,8 @@
})) }))
$: options = [...(tableOptions || []), ...(viewOptions || [])] $: options = [...(tableOptions || []), ...(viewOptions || [])]
// Gets a context definition of a certain type from a component definition const getSchemaFields = resourceId => {
const extractComponentContext = (component, contextType) => { const { schema } = getSchemaForDatasourcePlus(resourceId)
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 = (asset, tableId) => {
const { schema } = getSchemaForDatasourcePlus(tableId)
delete schema._id
delete schema._rev
return Object.values(schema || {}) return Object.values(schema || {})
} }

View File

@ -1,14 +1,16 @@
<script> <script>
import { Select, Label } from "@budibase/bbui" import { Select, Label } from "@budibase/bbui"
import { currentAsset, store } from "builderStore" import { currentAsset, store } from "builderStore"
import { getActionProviderComponents } from "builderStore/dataBinding" import { getActionProviders } from "builderStore/dataBinding"
export let parameters export let parameters
export let nested
$: actionProviders = getActionProviderComponents( $: actionProviders = getActionProviders(
$currentAsset, $currentAsset,
$store.selectedComponentId, $store.selectedComponentId,
"RefreshDatasource" "RefreshDatasource",
{ includeSelf: nested }
) )
</script> </script>
@ -17,8 +19,8 @@
<Select <Select
bind:value={parameters.componentId} bind:value={parameters.componentId}
options={actionProviders} options={actionProviders}
getOptionLabel={x => x._instanceName} getOptionLabel={x => x.readableBinding}
getOptionValue={x => x._id} getOptionValue={x => x.runtimeBinding}
/> />
</div> </div>

View File

@ -2,29 +2,19 @@
import { Select, Label, Body, Checkbox, Input } from "@budibase/bbui" import { Select, Label, Body, Checkbox, Input } from "@budibase/bbui"
import { store, currentAsset } from "builderStore" import { store, currentAsset } from "builderStore"
import { tables, viewsV2 } from "stores/backend" import { tables, viewsV2 } from "stores/backend"
import { import { getSchemaForDatasourcePlus } from "builderStore/dataBinding"
getContextProviderComponents,
getSchemaForDatasourcePlus,
} from "builderStore/dataBinding"
import SaveFields from "./SaveFields.svelte" import SaveFields from "./SaveFields.svelte"
import { getDatasourceLikeProviders } from "components/design/settings/controls/ButtonActionEditor/actions/utils"
export let parameters export let parameters
export let bindings = [] export let bindings = []
export let nested export let nested
$: formComponents = getContextProviderComponents( $: providerOptions = getDatasourceLikeProviders({
$currentAsset, asset: $currentAsset,
$store.selectedComponentId, componentId: $store.selectedComponentId,
"form", nested,
{ includeSelf: nested } })
)
$: schemaComponents = getContextProviderComponents(
$currentAsset,
$store.selectedComponentId,
"schema",
{ includeSelf: nested }
)
$: providerOptions = getProviderOptions(formComponents, schemaComponents)
$: schemaFields = getSchemaFields(parameters?.tableId) $: schemaFields = getSchemaFields(parameters?.tableId)
$: tableOptions = $tables.list.map(table => ({ $: tableOptions = $tables.list.map(table => ({
label: table.name, label: table.name,
@ -36,40 +26,6 @@
})) }))
$: options = [...(tableOptions || []), ...(viewOptions || [])] $: 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 getSchemaFields = resourceId => {
const { schema } = getSchemaForDatasourcePlus(resourceId) const { schema } = getSchemaForDatasourcePlus(resourceId)
return Object.values(schema || {}) return Object.values(schema || {})

View File

@ -1,22 +1,36 @@
<script> <script>
import { currentAsset, store } from "builderStore" import { currentAsset, store } from "builderStore"
import { Label, Combobox, Select } from "@budibase/bbui" import { Label, Combobox, Select } from "@budibase/bbui"
import { import { getActionProviders, buildFormSchema } from "builderStore/dataBinding"
getActionProviderComponents,
buildFormSchema,
} from "builderStore/dataBinding"
import { findComponent } from "builderStore/componentUtils" import { findComponent } from "builderStore/componentUtils"
export let parameters export let parameters
export let nested
$: formComponent = findComponent($currentAsset.props, parameters.componentId) $: formComponent = getFormComponent(
$currentAsset.props,
parameters.componentId
)
$: formSchema = buildFormSchema(formComponent) $: formSchema = buildFormSchema(formComponent)
$: fieldOptions = Object.keys(formSchema || {}) $: fieldOptions = Object.keys(formSchema || {})
$: actionProviders = getActionProviderComponents( $: actionProviders = getActionProviders(
$currentAsset, $currentAsset,
$store.selectedComponentId, $store.selectedComponentId,
"ScrollTo" "ScrollTo",
{ includeSelf: nested }
) )
const getFormComponent = (asset, id) => {
let component = findComponent(asset, id)
if (component) {
return component
}
// Check for block component IDs, and use the block itself instead
if (id?.includes("-")) {
return findComponent(asset, id.split("-")[0])
}
return null
}
</script> </script>
<div class="root"> <div class="root">
@ -24,8 +38,8 @@
<Select <Select
bind:value={parameters.componentId} bind:value={parameters.componentId}
options={actionProviders} options={actionProviders}
getOptionLabel={x => x._instanceName} getOptionLabel={x => x.readableBinding}
getOptionValue={x => x._id} getOptionValue={x => x.runtimeBinding}
/> />
<Label small>Field</Label> <Label small>Field</Label>
<Combobox bind:value={parameters.field} options={fieldOptions} /> <Combobox bind:value={parameters.field} options={fieldOptions} />

View File

@ -3,14 +3,12 @@
import { onMount } from "svelte" import { onMount } from "svelte"
import DrawerBindableInput from "components/common/bindings/DrawerBindableInput.svelte" import DrawerBindableInput from "components/common/bindings/DrawerBindableInput.svelte"
import { currentAsset, store } from "builderStore" import { currentAsset, store } from "builderStore"
import { import { getActionProviders, buildFormSchema } from "builderStore/dataBinding"
getActionProviderComponents,
buildFormSchema,
} from "builderStore/dataBinding"
import { findComponent } from "builderStore/componentUtils" import { findComponent } from "builderStore/componentUtils"
export let parameters export let parameters
export let bindings = [] export let bindings = []
export let nested
const typeOptions = [ const typeOptions = [
{ {
@ -23,15 +21,31 @@
}, },
] ]
$: formComponent = findComponent($currentAsset.props, parameters.componentId) $: formComponent = getFormComponent(
$currentAsset.props,
parameters.componentId
)
$: formSchema = buildFormSchema(formComponent) $: formSchema = buildFormSchema(formComponent)
$: fieldOptions = Object.keys(formSchema || {}) $: fieldOptions = Object.keys(formSchema || {})
$: actionProviders = getActionProviderComponents( $: actionProviders = getActionProviders(
$currentAsset, $currentAsset,
$store.selectedComponentId, $store.selectedComponentId,
"ValidateForm" "ValidateForm",
{ includeSelf: nested }
) )
const getFormComponent = (asset, id) => {
let component = findComponent(asset, id)
if (component) {
return component
}
// Check for block component IDs, and use the block itself instead
if (id?.includes("-")) {
return findComponent(asset, id.split("-")[0])
}
return null
}
onMount(() => { onMount(() => {
if (!parameters.type) { if (!parameters.type) {
parameters.type = "set" parameters.type = "set"
@ -44,8 +58,8 @@
<Select <Select
bind:value={parameters.componentId} bind:value={parameters.componentId}
options={actionProviders} options={actionProviders}
getOptionLabel={x => x._instanceName} getOptionLabel={x => x.readableBinding}
getOptionValue={x => x._id} getOptionValue={x => x.runtimeBinding}
/> />
<Label small>Type</Label> <Label small>Type</Label>
<Select <Select

View File

@ -1,14 +1,16 @@
<script> <script>
import { Select, Label } from "@budibase/bbui" import { Select, Label } from "@budibase/bbui"
import { currentAsset, store } from "builderStore" import { currentAsset, store } from "builderStore"
import { getActionProviderComponents } from "builderStore/dataBinding" import { getActionProviders } from "builderStore/dataBinding"
export let parameters export let parameters
export let nested
$: actionProviders = getActionProviderComponents( $: actionProviders = getActionProviders(
$currentAsset, $currentAsset,
$store.selectedComponentId, $store.selectedComponentId,
"ValidateForm" "ValidateForm",
{ includeSelf: nested }
) )
</script> </script>
@ -17,8 +19,8 @@
<Select <Select
bind:value={parameters.componentId} bind:value={parameters.componentId}
options={actionProviders} options={actionProviders}
getOptionLabel={x => x._instanceName} getOptionLabel={x => x.readableBinding}
getOptionValue={x => x._id} getOptionValue={x => x.runtimeBinding}
/> />
<div /> <div />
</div> </div>

View File

@ -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)
}

View File

@ -6165,6 +6165,24 @@
"defaultValue": "spectrum--medium" "defaultValue": "spectrum--medium"
} }
], ],
"actions": [
{
"type": "ValidateForm",
"suffix": "form"
},
{
"type": "ClearForm",
"suffix": "form"
},
{
"type": "UpdateFieldValue",
"suffix": "form"
},
{
"type": "ScrollTo",
"suffix": "form"
}
],
"context": [ "context": [
{ {
"type": "form", "type": "form",
@ -6420,7 +6438,8 @@
], ],
"context": { "context": {
"type": "schema" "type": "schema"
} },
"actions": ["RefreshDatasource"]
}, },
"bbreferencefield": { "bbreferencefield": {
"devComment": "As bb reference is only used for user subtype for now, we are using user for icon and labels", "devComment": "As bb reference is only used for user subtype for now, we are using user for icon and labels",

View File

@ -3,9 +3,9 @@
import Block from "../Block.svelte" import Block from "../Block.svelte"
export let buttons = [] export let buttons = []
export let direction export let direction = "row"
export let hAlign export let hAlign = "left"
export let vAlign export let vAlign = "top"
export let gap = "S" export let gap = "S"
</script> </script>

View File

@ -27,8 +27,12 @@
builderStore, builderStore,
notificationStore, notificationStore,
enrichButtonActions, enrichButtonActions,
ActionTypes,
createContextStore,
} = getContext("sdk") } = getContext("sdk")
let grid
$: columnWhitelist = columns?.map(col => col.name) $: columnWhitelist = columns?.map(col => col.name)
$: schemaOverrides = getSchemaOverrides(columns) $: schemaOverrides = getSchemaOverrides(columns)
$: enrichedButtons = enrichButtons(buttons) $: enrichedButtons = enrichButtons(buttons)
@ -53,11 +57,16 @@
text: settings.text, text: settings.text,
type: settings.type, type: settings.type,
onClick: async row => { onClick: async row => {
// We add a fake context binding in here, which allows us to pretend // Create a fake, ephemeral context to run the buttons actions with
// that the grid provides a "schema" binding - that lets us use the const id = get(component).id
// clicked row in things like save row actions const gridContext = createContextStore(context)
const enrichedContext = { ...get(context), [get(component).id]: row } gridContext.actions.provideData(id, row)
const fn = enrichButtonActions(settings.onClick, enrichedContext) gridContext.actions.provideAction(
id,
ActionTypes.RefreshDatasource,
() => grid?.getContext()?.rows.actions.refreshData()
)
const fn = enrichButtonActions(settings.onClick, get(gridContext))
return await fn?.({ row }) return await fn?.({ row })
}, },
})) }))
@ -69,6 +78,7 @@
class:in-builder={$builderStore.inBuilder} class:in-builder={$builderStore.inBuilder}
> >
<Grid <Grid
bind:this={grid}
datasource={table} datasource={table}
{API} {API}
{stripeRows} {stripeRows}

View File

@ -45,13 +45,14 @@
// 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
if (type === ActionTypes.RefreshDatasource) { if (type === ActionTypes.RefreshDatasource) {
const { dataSource } = metadata || {} if (metadata?.dataSource) {
dataSourceStore.actions.registerDataSource( dataSourceStore.actions.registerDataSource(
dataSource, metadata.dataSource,
instanceId, instanceId,
callback callback
) )
} }
}
}) })
} }
} }

View File

@ -15,6 +15,7 @@ import {
confirmationStore, confirmationStore,
roleStore, roleStore,
stateStore, stateStore,
createContextStore,
} from "stores" } from "stores"
import { styleable } from "utils/styleable" import { styleable } from "utils/styleable"
import { linkable } from "utils/linkable" import { linkable } from "utils/linkable"
@ -57,6 +58,7 @@ export default {
enrichButtonActions, enrichButtonActions,
processStringSync, processStringSync,
makePropSafe, makePropSafe,
createContextStore,
// Components // Components
Provider, Provider,