Update button actions and remove deprecated code

This commit is contained in:
Andrew Kingston 2021-01-19 17:38:24 +00:00
parent cbe6459a9c
commit d94473bca5
7 changed files with 153 additions and 359 deletions

View File

@ -1,3 +1,4 @@
import { cloneDeep } from "lodash/fp"
import { get } from "svelte/store" import { get } from "svelte/store"
import { backendUiStore, store } from "builderStore" import { backendUiStore, store } from "builderStore"
import { findAllMatchingComponents, findComponentPath } from "./storeUtils" import { findAllMatchingComponents, findComponentPath } from "./storeUtils"
@ -15,10 +16,9 @@ export const getBindableProperties = (rootComponent, componentId) => {
} }
/** /**
* Gets all bindable data contexts. These are fields of schemas of data contexts * Gets all data provider components above a component.
* provided by data provider components, such as lists or row detail components.
*/ */
export const getBindableContexts = (rootComponent, componentId) => { export const getDataProviderComponents = (rootComponent, componentId) => {
if (!rootComponent || !componentId) { if (!rootComponent || !componentId) {
return [] return []
} }
@ -28,43 +28,67 @@ export const getBindableContexts = (rootComponent, componentId) => {
const path = findComponentPath(rootComponent, componentId) const path = findComponentPath(rootComponent, componentId)
path.pop() path.pop()
// Enrich components with their definitions // Filter by only data provider components
const enriched = path.map(component => ({ return path.filter(component => {
instance: component, const def = store.actions.components.getDefinition(component._component)
definition: store.actions.components.getDefinition(component._component), return def?.dataProvider
})) })
}
/**
* Gets a datasource object for a certain data provider component
*/
export const getDatasourceForProvider = component => {
const def = store.actions.components.getDefinition(component?._component)
if (!def) {
return null
}
// Extract datasource from component instance
const datasourceSetting = def.settings.find(setting => {
return setting.key === def.datasourceSetting
})
if (!datasourceSetting) {
return null
}
// There are different types of setting which can be a datasource, for
// example an actual datasource object, or a table ID string.
// Convert the datasource setting into a proper datasource object so that
// we can use it properly
if (datasourceSetting.type === "datasource") {
return component[datasourceSetting?.key]
} else if (datasourceSetting.type === "table") {
return {
tableId: component[datasourceSetting?.key],
type: "table",
}
}
return null
}
/**
* Gets all bindable data contexts. These are fields of schemas of data contexts
* provided by data provider components, such as lists or row detail components.
*/
export const getBindableContexts = (rootComponent, componentId) => {
const dataProviders = getDataProviderComponents(rootComponent, componentId)
// Extract any components which provide data contexts // Extract any components which provide data contexts
const providers = enriched.filter(comp => comp.definition?.dataProvider)
let contexts = [] let contexts = []
providers.forEach(({ definition, instance }) => { dataProviders.forEach(component => {
// Extract datasource from component instance const datasource = getDatasourceForProvider(component)
const datasourceSetting = definition.settings.find(setting => {
return setting.key === definition.datasourceSetting
})
if (!datasourceSetting) {
return
}
// There are different types of setting which can be a datasource, for
// example an actual datasource object, or a table ID string.
// Convert the datasource setting into a proper datasource object so that
// we can use it properly
let datasource
if (datasourceSetting.type === "datasource") {
datasource = instance[datasourceSetting?.key]
} else if (datasourceSetting.type === "table") {
datasource = {
tableId: instance[datasourceSetting?.key],
type: "table",
}
}
if (!datasource) { if (!datasource) {
return return
} }
const { schema, table } = getSchemaForDatasource(datasource) // Get schema and add _id and _rev fields
let { schema, table } = getSchemaForDatasource(datasource)
schema["_id"] = { type: "string" }
schema["_rev"] = { type: "string " }
const keys = Object.keys(schema).sort() const keys = Object.keys(schema).sort()
// Create bindable properties for each schema field
keys.forEach(key => { keys.forEach(key => {
const fieldSchema = schema[key] const fieldSchema = schema[key]
// Replace certain bindings with a new property to help display components // Replace certain bindings with a new property to help display components
@ -77,11 +101,12 @@ export const getBindableContexts = (rootComponent, componentId) => {
contexts.push({ contexts.push({
type: "context", type: "context",
runtimeBinding: `${instance._id}.${runtimeBoundKey}`, runtimeBinding: `${component._id}.${runtimeBoundKey}`,
readableBinding: `${instance._instanceName}.${table.name}.${key}`, readableBinding: `${component._instanceName}.${table.name}.${key}`,
fieldSchema, fieldSchema,
providerId: instance._id, providerId: component._id,
tableId: datasource.tableId, tableId: datasource.tableId,
field: key,
}) })
}) })
}) })
@ -115,27 +140,22 @@ export const getBindableComponents = rootComponent => {
/** /**
* Gets a schema for a datasource object. * Gets a schema for a datasource object.
*/ */
const getSchemaForDatasource = datasource => { export const getSchemaForDatasource = datasource => {
const tables = get(backendUiStore).tables let schema, table
const { type } = datasource if (datasource) {
const table = tables.find(table => table._id === datasource.tableId) const tables = get(backendUiStore).tables
let schema const { type } = datasource
if (table) { table = tables.find(table => table._id === datasource.tableId)
if (type === "table") { if (table) {
schema = table.schema ?? {} if (type === "table") {
} else if (type === "view") { schema = cloneDeep(table.schema)
schema = table.views?.[datasource.name]?.schema ?? {} } else if (type === "view") {
} else if (type === "link") { schema = cloneDeep(table.views?.[datasource.name]?.schema)
schema = table.schema ?? {} } else if (type === "link") {
schema = cloneDeep(table.schema)
}
} }
} }
if (schema) {
// Add ID and rev fields for any valid datasources
schema["_id"] = { type: "string" }
schema["_rev"] = { type: "string " }
} else {
schema = {}
}
return { schema, table } return { schema, table }
} }
@ -143,6 +163,9 @@ const getSchemaForDatasource = datasource => {
* Converts a readable data binding into a runtime data binding * Converts a readable data binding into a runtime data binding
*/ */
export function readableToRuntimeBinding(bindableProperties, textWithBindings) { export function readableToRuntimeBinding(bindableProperties, textWithBindings) {
if (typeof textWithBindings !== "string") {
return textWithBindings
}
const boundValues = textWithBindings.match(CAPTURE_VAR_INSIDE_MUSTACHE) || [] const boundValues = textWithBindings.match(CAPTURE_VAR_INSIDE_MUSTACHE) || []
let result = textWithBindings let result = textWithBindings
boundValues.forEach(boundValue => { boundValues.forEach(boundValue => {
@ -160,6 +183,9 @@ export function readableToRuntimeBinding(bindableProperties, textWithBindings) {
* Converts a runtime data binding into a readable data binding * Converts a runtime data binding into a readable data binding
*/ */
export function runtimeToReadableBinding(bindableProperties, textWithBindings) { export function runtimeToReadableBinding(bindableProperties, textWithBindings) {
if (typeof textWithBindings !== "string") {
return textWithBindings
}
const boundValues = textWithBindings.match(CAPTURE_VAR_INSIDE_MUSTACHE) || [] const boundValues = textWithBindings.match(CAPTURE_VAR_INSIDE_MUSTACHE) || []
let result = textWithBindings let result = textWithBindings
boundValues.forEach(boundValue => { boundValues.forEach(boundValue => {

View File

@ -1,53 +0,0 @@
<script>
import { Select, Label } from "@budibase/bbui"
import { backendUiStore } from "builderStore"
import SaveFields from "./SaveFields.svelte"
export let parameters
const tableFields = tableId => {
const table = $backendUiStore.tables.find(m => m._id === tableId)
return Object.keys(table.schema).map(k => ({
name: k,
type: table.schema[k].type,
}))
}
$: schemaFields =
parameters && parameters.tableId ? tableFields(parameters.tableId) : []
const onFieldsChanged = e => {
parameters.fields = e.detail
}
</script>
<div class="root">
<Label size="m" color="dark">Table</Label>
<Select secondary bind:value={parameters.tableId}>
<option value="" />
{#each $backendUiStore.tables as table}
<option value={table._id}>{table.name}</option>
{/each}
</Select>
{#if parameters.tableId}
<SaveFields
parameterFields={parameters.fields}
{schemaFields}
on:fieldschanged={onFieldsChanged} />
{/if}
</div>
<style>
.root {
display: grid;
column-gap: var(--spacing-s);
row-gap: var(--spacing-s);
grid-template-columns: auto 1fr auto 1fr auto;
align-items: baseline;
}
.root :global(> div:nth-child(2)) {
grid-column-start: 2;
grid-column-end: 6;
}
</style>

View File

@ -1,51 +1,36 @@
<script> <script>
import { Select, Label } from "@budibase/bbui" import { Select, Label } from "@budibase/bbui"
import { store, backendUiStore, currentAsset } from "builderStore" import { store, currentAsset } from "builderStore"
import { getBindableProperties } from "builderStore/dataBinding" import {
getDataProviderComponents,
getDatasourceForProvider,
getSchemaForDatasource,
} from "builderStore/dataBinding"
export let parameters export let parameters
let idFields $: dataProviderComponents = getDataProviderComponents(
$: bindableProperties = getBindableProperties(
$currentAsset.props, $currentAsset.props,
$store.selectedComponentId $store.selectedComponentId
) )
$: idFields = bindableProperties.filter(
bindable =>
bindable.type === "context" && bindable.runtimeBinding.endsWith("._id")
)
$: { $: {
// Automatically set rev and table ID based on row ID
if (parameters.rowId) { if (parameters.rowId) {
// Set rev ID
parameters.revId = parameters.rowId.replace("_id", "_rev") parameters.revId = parameters.rowId.replace("_id", "_rev")
const providerComponent = dataProviderComponents.find(
// Set table ID provider => provider._id === parameters.providerId
const idBinding = bindableProperties.find(
prop =>
prop.runtimeBinding ===
parameters.rowId
.replace("{{", "")
.replace("}}", "")
.trim()
) )
if (idBinding) { const datasource = getDatasourceForProvider(providerComponent)
const { instance } = idBinding const { table } = getSchemaForDatasource(datasource)
const component = $store.components[instance._component] if (table) {
const tableInfo = instance[component.context] parameters.tableId = table._id
if (tableInfo) {
parameters.tableId =
typeof tableInfo === "string" ? tableInfo : tableInfo.tableId
}
} }
console.log(parameters)
} }
} }
</script> </script>
<div class="root"> <div class="root">
{#if idFields.length === 0} {#if dataProviderComponents.length === 0}
<div class="cannot-use"> <div class="cannot-use">
Delete row can only be used within a component that provides data, such as Delete row can only be used within a component that provides data, such as
a List a List
@ -54,9 +39,9 @@
<Label size="m" color="dark">Datasource</Label> <Label size="m" color="dark">Datasource</Label>
<Select secondary bind:value={parameters.rowId}> <Select secondary bind:value={parameters.rowId}>
<option value="" /> <option value="" />
{#each idFields as idField} {#each dataProviderComponents as provider}
<option value={`{{ ${idField.runtimeBinding} }}`}> <option value={`{{ ${provider._id}._id }}`}>
{idField.instance._instanceName} {provider._instanceName}
</option> </option>
{/each} {/each}
</Select> </Select>

View File

@ -1,77 +1,28 @@
<script> <script>
import { Select, Label } from "@budibase/bbui" import { Select, Label } from "@budibase/bbui"
import { store, backendUiStore, currentAsset } from "builderStore" import { store, currentAsset } from "builderStore"
import { getBindableProperties } from "builderStore/dataBinding" import {
getDataProviderComponents,
getDatasourceForProvider,
getSchemaForDatasource,
} from "builderStore/dataBinding"
import SaveFields from "./SaveFields.svelte" import SaveFields from "./SaveFields.svelte"
// parameters.contextPath used in the client handler to determine which row to save
// this could be "data" or "data.parent", "data.parent.parent" etc
export let parameters export let parameters
let idFields $: dataProviderComponents = getDataProviderComponents(
let schemaFields
$: bindableProperties = getBindableProperties(
$currentAsset.props, $currentAsset.props,
$store.selectedComponentId $store.selectedComponentId
) )
$: providerComponent = dataProviderComponents.find(
provider => provider._id === parameters.providerId
)
$: schemaFields = getSchemaFields(providerComponent)
$: { const getSchemaFields = component => {
if (parameters && parameters.contextPath) { const datasource = getDatasourceForProvider(component)
schemaFields = schemaFromContextPath(parameters.contextPath) const { schema } = getSchemaForDatasource(datasource)
} else { return Object.values(schema || {})
schemaFields = []
}
}
const idBindingToContextPath = id => id.substring(0, id.length - 4)
const contextPathToId = path => `${path}._id`
$: {
idFields = bindableProperties.filter(
bindable =>
bindable.type === "context" && bindable.runtimeBinding.endsWith("._id")
)
// ensure contextPath is always defaulted - there is usually only one option
if (idFields.length > 0 && !parameters.contextPath) {
parameters.contextPath = idBindingToContextPath(
idFields[0].runtimeBinding
)
parameters = parameters
}
}
// just wraps binding in {{ ... }}
const toBindingExpression = bindingPath => `{{ ${bindingPath} }}`
// finds the selected idBinding, then reads the table/view
// from the component instance that it belongs to.
// then returns the field names for that schema
const schemaFromContextPath = contextPath => {
if (!contextPath) return []
const idBinding = bindableProperties.find(
prop => prop.runtimeBinding === contextPathToId(contextPath)
)
if (!idBinding) return []
const { instance } = idBinding
const component = $store.components[instance._component]
// component.context is the name of the prop that holds the tableId
const tableInfo = instance[component.context]
const tableId =
typeof tableInfo === "string" ? tableInfo : tableInfo.tableId
if (!tableInfo) return []
const table = $backendUiStore.tables.find(m => m._id === tableId)
parameters.tableId = tableId
return Object.keys(table.schema).map(k => ({
name: k,
type: table.schema[k].type,
}))
} }
const onFieldsChanged = e => { const onFieldsChanged = e => {
@ -80,19 +31,17 @@
</script> </script>
<div class="root"> <div class="root">
{#if idFields.length === 0} {#if !dataProviderComponents.length}
<div class="cannot-use"> <div class="cannot-use">
Update row can only be used within a component that provides data, such as Save Row can only be used within a component that provides data, such as a
a List Repeater
</div> </div>
{:else} {:else}
<Label size="m" color="dark">Datasource</Label> <Label size="m" color="dark">Datasource</Label>
<Select secondary bind:value={parameters.contextPath}> <Select secondary bind:value={parameters.providerId}>
<option value="" /> <option value="" />
{#each idFields as idField} {#each dataProviderComponents as provider}
<option value={idBindingToContextPath(idField.runtimeBinding)}> <option value={provider._id}>{provider._instanceName}</option>
{idField.instance._instanceName}
</option>
{/each} {/each}
</Select> </Select>
{/if} {/if}

View File

@ -1,127 +0,0 @@
<script>
import { Select, Label } from "@budibase/bbui"
import { store, backendUiStore, currentAsset } from "builderStore"
import { getBindableProperties } from "builderStore/dataBinding"
import SaveFields from "./SaveFields.svelte"
export let parameters
$: bindableProperties = getBindableProperties(
$currentAsset.props,
$store.selectedComponentId
)
let idFields
let rowId
$: {
idFields = bindableProperties.filter(
bindable =>
bindable.type === "context" && bindable.runtimeBinding.endsWith("._id")
)
// ensure rowId is always defaulted - there is usually only one option
if (idFields.length > 0 && !parameters._id) {
rowId = idFields[0].runtimeBinding
parameters = parameters
} else if (!rowId && parameters._id) {
rowId = parameters._id
.replace("{{", "")
.replace("}}", "")
.trim()
}
}
$: parameters._id = `{{ ${rowId} }}`
// just wraps binding in {{ ... }}
const toBindingExpression = bindingPath => `{{ ${bindingPath} }}`
// finds the selected idBinding, then reads the table/view
// from the component instance that it belongs to.
// then returns the field names for that schema
const schemaFromIdBinding = rowId => {
if (!rowId) return []
const idBinding = bindableProperties.find(
prop => prop.runtimeBinding === rowId
)
if (!idBinding) return []
const { instance } = idBinding
const component = $store.components[instance._component]
// component.context is the name of the prop that holds the tableId
const tableInfo = instance[component.context]
if (!tableInfo) return []
const table = $backendUiStore.tables.find(m => m._id === tableInfo.tableId)
parameters.tableId = tableInfo.tableId
return Object.keys(table.schema).map(k => ({
name: k,
type: table.schema[k].type,
}))
}
let schemaFields
$: {
if (parameters && rowId) {
schemaFields = schemaFromIdBinding(rowId)
} else {
schemaFields = []
}
}
const onFieldsChanged = e => {
parameters.fields = e.detail
}
</script>
<div class="root">
{#if idFields.length === 0}
<div class="cannot-use">
Update row can only be used within a component that provides data, such as
a List
</div>
{:else}
<Label size="m" color="dark">Row Id</Label>
<Select secondary bind:value={rowId}>
<option value="" />
{#each idFields as idField}
<option value={idField.runtimeBinding}>
{idField.readableBinding}
</option>
{/each}
</Select>
{/if}
{#if rowId}
<SaveFields
parameterFields={parameters.fields}
{schemaFields}
on:fieldschanged={onFieldsChanged} />
{/if}
</div>
<style>
.root {
display: grid;
column-gap: var(--spacing-s);
row-gap: var(--spacing-s);
grid-template-columns: auto 1fr auto 1fr auto;
align-items: baseline;
}
.root :global(> div:nth-child(2)) {
grid-column-start: 2;
grid-column-end: 6;
}
.cannot-use {
color: var(--red);
font-size: var(--font-size-s);
text-align: center;
width: 70%;
margin: auto;
}
</style>

View File

@ -1,39 +1,49 @@
import { get } from "svelte/store"
import { enrichDataBinding } from "./enrichDataBinding" import { enrichDataBinding } from "./enrichDataBinding"
import { routeStore } from "../store" import { routeStore, builderStore } from "../store"
import { saveRow, deleteRow, triggerAutomation } from "../api" import { saveRow, deleteRow, triggerAutomation } from "../api"
const saveRowHandler = async (action, context) => { const saveRowHandler = async (action, context) => {
let draft = context[`${action.parameters.contextPath}_draft`] const { fields, providerId } = action.parameters
if (action.parameters.fields) { if (providerId) {
Object.entries(action.parameters.fields).forEach(([key, entry]) => { let draft = context[`${action.parameters.providerId}_draft`]
draft[key] = enrichDataBinding(entry.value, context) if (fields) {
}) Object.entries(fields).forEach(([key, entry]) => {
draft[key] = enrichDataBinding(entry.value, context)
})
}
await saveRow(draft)
} }
await saveRow(draft)
} }
const deleteRowHandler = async (action, context) => { const deleteRowHandler = async (action, context) => {
const { tableId, revId, rowId } = action.parameters const { tableId, revId, rowId } = action.parameters
await deleteRow({ if (tableId && revId && rowId) {
tableId: enrichDataBinding(tableId, context), await deleteRow({
rowId: enrichDataBinding(rowId, context), tableId: enrichDataBinding(tableId, context),
revId: enrichDataBinding(revId, context), rowId: enrichDataBinding(rowId, context),
}) revId: enrichDataBinding(revId, context),
})
}
} }
const triggerAutomationHandler = async (action, context) => { const triggerAutomationHandler = async (action, context) => {
const params = {} const params = {}
for (let field in action.parameters.fields) { if (action.parameters.fields) {
params[field] = enrichDataBinding( for (let field in action.parameters.fields) {
action.parameters.fields[field].value, params[field] = enrichDataBinding(
context action.parameters.fields[field].value,
) context
)
}
} }
await triggerAutomation(action.parameters.automationId, params) await triggerAutomation(action.parameters.automationId, params)
} }
const navigationHandler = action => { const navigationHandler = action => {
routeStore.actions.navigate(action.parameters.url) if (action.parameters.url) {
routeStore.actions.navigate(action.parameters.url)
}
} }
const handlerMap = { const handlerMap = {
@ -48,6 +58,10 @@ const handlerMap = {
* actions in the current context. * actions in the current context.
*/ */
export const enrichButtonActions = (actions, context) => { export const enrichButtonActions = (actions, context) => {
// Prevent button actions in the builder preview
if (get(builderStore).inBuilder) {
return () => {}
}
const handlers = actions.map(def => handlerMap[def["##eventHandlerType"]]) const handlers = actions.map(def => handlerMap[def["##eventHandlerType"]])
return async () => { return async () => {
for (let i = 0; i < handlers.length; i++) { for (let i = 0; i < handlers.length; i++) {

View File

@ -28,8 +28,8 @@
const getFormData = async context => { const getFormData = async context => {
if (context) { if (context) {
const tableDefinition = await API.fetchTableDefinition(context.tableId) const tableDefinition = await API.fetchTableDefinition(context.tableId)
schema = tableDefinition.schema schema = tableDefinition?.schema
fields = Object.keys(schema) fields = Object.keys(schema ?? {})
// Use the draft version for editing // Use the draft version for editing
row = $dataContext[`${$dataContext.closestComponentId}_draft`] row = $dataContext[`${$dataContext.closestComponentId}_draft`]