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 { backendUiStore, store } from "builderStore"
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
* provided by data provider components, such as lists or row detail components.
* Gets all data provider components above a component.
*/
export const getBindableContexts = (rootComponent, componentId) => {
export const getDataProviderComponents = (rootComponent, componentId) => {
if (!rootComponent || !componentId) {
return []
}
@ -28,43 +28,67 @@ export const getBindableContexts = (rootComponent, componentId) => {
const path = findComponentPath(rootComponent, componentId)
path.pop()
// Enrich components with their definitions
const enriched = path.map(component => ({
instance: component,
definition: store.actions.components.getDefinition(component._component),
}))
// Filter by only data provider components
return path.filter(component => {
const def = 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
const providers = enriched.filter(comp => comp.definition?.dataProvider)
let contexts = []
providers.forEach(({ definition, instance }) => {
// Extract datasource from component instance
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",
}
}
dataProviders.forEach(component => {
const datasource = getDatasourceForProvider(component)
if (!datasource) {
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()
// Create bindable properties for each schema field
keys.forEach(key => {
const fieldSchema = schema[key]
// Replace certain bindings with a new property to help display components
@ -77,11 +101,12 @@ export const getBindableContexts = (rootComponent, componentId) => {
contexts.push({
type: "context",
runtimeBinding: `${instance._id}.${runtimeBoundKey}`,
readableBinding: `${instance._instanceName}.${table.name}.${key}`,
runtimeBinding: `${component._id}.${runtimeBoundKey}`,
readableBinding: `${component._instanceName}.${table.name}.${key}`,
fieldSchema,
providerId: instance._id,
providerId: component._id,
tableId: datasource.tableId,
field: key,
})
})
})
@ -115,27 +140,22 @@ export const getBindableComponents = rootComponent => {
/**
* Gets a schema for a datasource object.
*/
const getSchemaForDatasource = datasource => {
const tables = get(backendUiStore).tables
const { type } = datasource
const table = tables.find(table => table._id === datasource.tableId)
let schema
if (table) {
if (type === "table") {
schema = table.schema ?? {}
} else if (type === "view") {
schema = table.views?.[datasource.name]?.schema ?? {}
} else if (type === "link") {
schema = table.schema ?? {}
export const getSchemaForDatasource = datasource => {
let schema, table
if (datasource) {
const tables = get(backendUiStore).tables
const { type } = datasource
table = tables.find(table => table._id === datasource.tableId)
if (table) {
if (type === "table") {
schema = cloneDeep(table.schema)
} else if (type === "view") {
schema = cloneDeep(table.views?.[datasource.name]?.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 }
}
@ -143,6 +163,9 @@ const getSchemaForDatasource = datasource => {
* Converts a readable data binding into a runtime data binding
*/
export function readableToRuntimeBinding(bindableProperties, textWithBindings) {
if (typeof textWithBindings !== "string") {
return textWithBindings
}
const boundValues = textWithBindings.match(CAPTURE_VAR_INSIDE_MUSTACHE) || []
let result = textWithBindings
boundValues.forEach(boundValue => {
@ -160,6 +183,9 @@ export function readableToRuntimeBinding(bindableProperties, textWithBindings) {
* Converts a runtime data binding into a readable data binding
*/
export function runtimeToReadableBinding(bindableProperties, textWithBindings) {
if (typeof textWithBindings !== "string") {
return textWithBindings
}
const boundValues = textWithBindings.match(CAPTURE_VAR_INSIDE_MUSTACHE) || []
let result = textWithBindings
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>
import { Select, Label } from "@budibase/bbui"
import { store, backendUiStore, currentAsset } from "builderStore"
import { getBindableProperties } from "builderStore/dataBinding"
import { store, currentAsset } from "builderStore"
import {
getDataProviderComponents,
getDatasourceForProvider,
getSchemaForDatasource,
} from "builderStore/dataBinding"
export let parameters
let idFields
$: bindableProperties = getBindableProperties(
$: dataProviderComponents = getDataProviderComponents(
$currentAsset.props,
$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) {
// Set rev ID
parameters.revId = parameters.rowId.replace("_id", "_rev")
// Set table ID
const idBinding = bindableProperties.find(
prop =>
prop.runtimeBinding ===
parameters.rowId
.replace("{{", "")
.replace("}}", "")
.trim()
const providerComponent = dataProviderComponents.find(
provider => provider._id === parameters.providerId
)
if (idBinding) {
const { instance } = idBinding
const component = $store.components[instance._component]
const tableInfo = instance[component.context]
if (tableInfo) {
parameters.tableId =
typeof tableInfo === "string" ? tableInfo : tableInfo.tableId
}
const datasource = getDatasourceForProvider(providerComponent)
const { table } = getSchemaForDatasource(datasource)
if (table) {
parameters.tableId = table._id
}
console.log(parameters)
}
}
</script>
<div class="root">
{#if idFields.length === 0}
{#if dataProviderComponents.length === 0}
<div class="cannot-use">
Delete row can only be used within a component that provides data, such as
a List
@ -54,9 +39,9 @@
<Label size="m" color="dark">Datasource</Label>
<Select secondary bind:value={parameters.rowId}>
<option value="" />
{#each idFields as idField}
<option value={`{{ ${idField.runtimeBinding} }}`}>
{idField.instance._instanceName}
{#each dataProviderComponents as provider}
<option value={`{{ ${provider._id}._id }}`}>
{provider._instanceName}
</option>
{/each}
</Select>

View File

@ -1,77 +1,28 @@
<script>
import { Select, Label } from "@budibase/bbui"
import { store, backendUiStore, currentAsset } from "builderStore"
import { getBindableProperties } from "builderStore/dataBinding"
import { store, currentAsset } from "builderStore"
import {
getDataProviderComponents,
getDatasourceForProvider,
getSchemaForDatasource,
} from "builderStore/dataBinding"
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
let idFields
let schemaFields
$: bindableProperties = getBindableProperties(
$: dataProviderComponents = getDataProviderComponents(
$currentAsset.props,
$store.selectedComponentId
)
$: providerComponent = dataProviderComponents.find(
provider => provider._id === parameters.providerId
)
$: schemaFields = getSchemaFields(providerComponent)
$: {
if (parameters && parameters.contextPath) {
schemaFields = schemaFromContextPath(parameters.contextPath)
} else {
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 getSchemaFields = component => {
const datasource = getDatasourceForProvider(component)
const { schema } = getSchemaForDatasource(datasource)
return Object.values(schema || {})
}
const onFieldsChanged = e => {
@ -80,19 +31,17 @@
</script>
<div class="root">
{#if idFields.length === 0}
{#if !dataProviderComponents.length}
<div class="cannot-use">
Update row can only be used within a component that provides data, such as
a List
Save Row can only be used within a component that provides data, such as a
Repeater
</div>
{:else}
<Label size="m" color="dark">Datasource</Label>
<Select secondary bind:value={parameters.contextPath}>
<Select secondary bind:value={parameters.providerId}>
<option value="" />
{#each idFields as idField}
<option value={idBindingToContextPath(idField.runtimeBinding)}>
{idField.instance._instanceName}
</option>
{#each dataProviderComponents as provider}
<option value={provider._id}>{provider._instanceName}</option>
{/each}
</Select>
{/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 { routeStore } from "../store"
import { routeStore, builderStore } from "../store"
import { saveRow, deleteRow, triggerAutomation } from "../api"
const saveRowHandler = async (action, context) => {
let draft = context[`${action.parameters.contextPath}_draft`]
if (action.parameters.fields) {
Object.entries(action.parameters.fields).forEach(([key, entry]) => {
draft[key] = enrichDataBinding(entry.value, context)
})
const { fields, providerId } = action.parameters
if (providerId) {
let draft = context[`${action.parameters.providerId}_draft`]
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 { tableId, revId, rowId } = action.parameters
await deleteRow({
tableId: enrichDataBinding(tableId, context),
rowId: enrichDataBinding(rowId, context),
revId: enrichDataBinding(revId, context),
})
if (tableId && revId && rowId) {
await deleteRow({
tableId: enrichDataBinding(tableId, context),
rowId: enrichDataBinding(rowId, context),
revId: enrichDataBinding(revId, context),
})
}
}
const triggerAutomationHandler = async (action, context) => {
const params = {}
for (let field in action.parameters.fields) {
params[field] = enrichDataBinding(
action.parameters.fields[field].value,
context
)
if (action.parameters.fields) {
for (let field in action.parameters.fields) {
params[field] = enrichDataBinding(
action.parameters.fields[field].value,
context
)
}
}
await triggerAutomation(action.parameters.automationId, params)
}
const navigationHandler = action => {
routeStore.actions.navigate(action.parameters.url)
if (action.parameters.url) {
routeStore.actions.navigate(action.parameters.url)
}
}
const handlerMap = {
@ -48,6 +58,10 @@ const handlerMap = {
* actions in the current 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"]])
return async () => {
for (let i = 0; i < handlers.length; i++) {

View File

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