Adding handlebars formulas to the system, it is now possible to set a formula at a column level which will always be applied on the way out with a relationship depth of one.
This commit is contained in:
parent
e08df4110a
commit
a14c80bf6c
|
@ -1,7 +1,7 @@
|
||||||
import { cloneDeep } from "lodash/fp"
|
import { cloneDeep } from "lodash/fp"
|
||||||
import { get } from "svelte/store"
|
import { get } from "svelte/store"
|
||||||
import { findComponent, findComponentPath } from "./storeUtils"
|
import { findComponent, findComponentPath } from "./storeUtils"
|
||||||
import { store } from "builderStore"
|
import {currentAssetId, store} from "builderStore"
|
||||||
import { tables as tablesStore, queries as queriesStores } from "stores/backend"
|
import { tables as tablesStore, queries as queriesStores } from "stores/backend"
|
||||||
import { makePropSafe } from "@budibase/string-templates"
|
import { makePropSafe } from "@budibase/string-templates"
|
||||||
import { TableNames } from "../constants"
|
import { TableNames } from "../constants"
|
||||||
|
@ -329,6 +329,21 @@ export function removeBindings(obj) {
|
||||||
return obj
|
return obj
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* When converting from readable to runtime it can sometimes add too many square brackets,
|
||||||
|
* this makes sure that doesn't happen.
|
||||||
|
*/
|
||||||
|
function shouldReplaceBinding(currentValue, from, convertTo) {
|
||||||
|
if (!currentValue.includes(from)) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
if (convertTo === "readableBinding") {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
const noSpaces = currentValue.replace(/\s+/g, "")
|
||||||
|
return !noSpaces.includes(`[${from}]`)
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* utility function for the readableToRuntimeBinding and runtimeToReadableBinding.
|
* utility function for the readableToRuntimeBinding and runtimeToReadableBinding.
|
||||||
*/
|
*/
|
||||||
|
@ -348,7 +363,7 @@ function bindingReplacement(bindableProperties, textWithBindings, convertTo) {
|
||||||
for (let boundValue of boundValues) {
|
for (let boundValue of boundValues) {
|
||||||
let newBoundValue = boundValue
|
let newBoundValue = boundValue
|
||||||
for (let from of convertFromProps) {
|
for (let from of convertFromProps) {
|
||||||
if (newBoundValue.includes(from)) {
|
if (shouldReplaceBinding(newBoundValue, from, convertTo)) {
|
||||||
const binding = bindableProperties.find(el => el[convertFrom] === from)
|
const binding = bindableProperties.find(el => el[convertFrom] === from)
|
||||||
newBoundValue = newBoundValue.replace(from, binding[convertTo])
|
newBoundValue = newBoundValue.replace(from, binding[convertTo])
|
||||||
}
|
}
|
||||||
|
|
|
@ -6,7 +6,7 @@
|
||||||
import { automationStore } from "builderStore"
|
import { automationStore } from "builderStore"
|
||||||
import WebhookDisplay from "../Shared/WebhookDisplay.svelte"
|
import WebhookDisplay from "../Shared/WebhookDisplay.svelte"
|
||||||
import DrawerBindableInput from "../../common/DrawerBindableInput.svelte"
|
import DrawerBindableInput from "../../common/DrawerBindableInput.svelte"
|
||||||
import AutomationBindingPanel from "./AutomationBindingPanel.svelte"
|
import AutomationBindingPanel from "../../common/ServerBindingPanel.svelte"
|
||||||
|
|
||||||
export let block
|
export let block
|
||||||
export let webhookModal
|
export let webhookModal
|
||||||
|
|
|
@ -2,7 +2,7 @@
|
||||||
import { tables } from "stores/backend"
|
import { tables } from "stores/backend"
|
||||||
import { Select } from "@budibase/bbui"
|
import { Select } from "@budibase/bbui"
|
||||||
import DrawerBindableInput from "../../common/DrawerBindableInput.svelte"
|
import DrawerBindableInput from "../../common/DrawerBindableInput.svelte"
|
||||||
import AutomationBindingPanel from "./AutomationBindingPanel.svelte"
|
import AutomationBindingPanel from "../../common/ServerBindingPanel.svelte"
|
||||||
|
|
||||||
export let value
|
export let value
|
||||||
export let bindings
|
export let bindings
|
||||||
|
|
|
@ -0,0 +1,75 @@
|
||||||
|
import { FIELDS } from "constants/backend"
|
||||||
|
import { tables } from "stores/backend"
|
||||||
|
import { get as svelteGet } from "svelte/store"
|
||||||
|
|
||||||
|
// currently supported level of relationship depth (server side)
|
||||||
|
const MAX_DEPTH = 1
|
||||||
|
const TYPES_TO_SKIP = [
|
||||||
|
FIELDS.FORMULA.type,
|
||||||
|
FIELDS.LONGFORM.type,
|
||||||
|
FIELDS.ATTACHMENT.type,
|
||||||
|
]
|
||||||
|
|
||||||
|
export function getBindings({
|
||||||
|
table,
|
||||||
|
path = null,
|
||||||
|
category = null,
|
||||||
|
depth = 0,
|
||||||
|
}) {
|
||||||
|
let bindings = []
|
||||||
|
if (!table) {
|
||||||
|
return bindings
|
||||||
|
}
|
||||||
|
for (let [column, schema] of Object.entries(table.schema)) {
|
||||||
|
const isRelationship = schema.type === FIELDS.LINK.type
|
||||||
|
// skip relationships after a certain depth and types which
|
||||||
|
// can't bind to
|
||||||
|
if (
|
||||||
|
TYPES_TO_SKIP.includes(schema.type) ||
|
||||||
|
(isRelationship && depth >= MAX_DEPTH)
|
||||||
|
) {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
category = category == null ? `${table.name} Fields` : category
|
||||||
|
if (isRelationship && depth < MAX_DEPTH) {
|
||||||
|
const relatedTable = svelteGet(tables).list.find(
|
||||||
|
table => table._id === schema.tableId
|
||||||
|
)
|
||||||
|
const relatedBindings = bindings.concat(
|
||||||
|
getBindings({
|
||||||
|
table: relatedTable,
|
||||||
|
path: column,
|
||||||
|
category: `${column} Relationships`,
|
||||||
|
depth: depth + 1,
|
||||||
|
})
|
||||||
|
)
|
||||||
|
// remove the ones that have already been found
|
||||||
|
bindings = bindings.concat(
|
||||||
|
relatedBindings.filter(
|
||||||
|
related => !bindings.find(binding => binding.path === related.path)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
const field = Object.values(FIELDS).find(
|
||||||
|
field => field.type === schema.type
|
||||||
|
)
|
||||||
|
const label = path == null ? column : `${path}.0.${column}`
|
||||||
|
// only supply a description for relationship paths
|
||||||
|
const description =
|
||||||
|
path == null
|
||||||
|
? undefined
|
||||||
|
: `Update the "0" with the index of relationships or use array helpers`
|
||||||
|
bindings.push({
|
||||||
|
label: label,
|
||||||
|
type: field.name === FIELDS.LINK.name ? "Array" : field.name,
|
||||||
|
category,
|
||||||
|
path: label,
|
||||||
|
description,
|
||||||
|
// don't include path, it messes things up, relationship path
|
||||||
|
// will be replaced by the main array binding
|
||||||
|
readableBinding: column,
|
||||||
|
runtimeBinding: `[${column}]`,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
return bindings
|
||||||
|
}
|
|
@ -8,23 +8,26 @@
|
||||||
Toggle,
|
Toggle,
|
||||||
Radio,
|
Radio,
|
||||||
} from "@budibase/bbui"
|
} from "@budibase/bbui"
|
||||||
import { cloneDeep } from "lodash/fp"
|
import {cloneDeep} from "lodash/fp"
|
||||||
import { tables } from "stores/backend"
|
import {tables} from "stores/backend"
|
||||||
|
|
||||||
import { TableNames, UNEDITABLE_USER_FIELDS } from "constants"
|
import {TableNames, UNEDITABLE_USER_FIELDS} from "constants"
|
||||||
import {
|
import {
|
||||||
FIELDS,
|
FIELDS,
|
||||||
AUTO_COLUMN_SUB_TYPES,
|
AUTO_COLUMN_SUB_TYPES,
|
||||||
RelationshipTypes,
|
RelationshipTypes,
|
||||||
} from "constants/backend"
|
} from "constants/backend"
|
||||||
import { getAutoColumnInformation, buildAutoColumn } from "builderStore/utils"
|
import {getAutoColumnInformation, buildAutoColumn} from "builderStore/utils"
|
||||||
import { notifier } from "builderStore/store/notifications"
|
import {notifier} from "builderStore/store/notifications"
|
||||||
import ValuesList from "components/common/ValuesList.svelte"
|
import ValuesList from "components/common/ValuesList.svelte"
|
||||||
import DatePicker from "components/common/DatePicker.svelte"
|
import DatePicker from "components/common/DatePicker.svelte"
|
||||||
import ConfirmDialog from "components/common/ConfirmDialog.svelte"
|
import ConfirmDialog from "components/common/ConfirmDialog.svelte"
|
||||||
import { truncate } from "lodash"
|
import {truncate} from "lodash"
|
||||||
|
import ModalBindableInput from "components/common/ModalBindableInput.svelte"
|
||||||
|
import {getBindings} from "components/backend/DataTable/formula"
|
||||||
|
|
||||||
const AUTO_COL = "auto"
|
const AUTO_TYPE = "auto"
|
||||||
|
const FORMULA_TYPE = FIELDS.FORMULA.type
|
||||||
const LINK_TYPE = FIELDS.LINK.type
|
const LINK_TYPE = FIELDS.LINK.type
|
||||||
let fieldDefinitions = cloneDeep(FIELDS)
|
let fieldDefinitions = cloneDeep(FIELDS)
|
||||||
|
|
||||||
|
@ -65,14 +68,17 @@
|
||||||
$: canBeSearched =
|
$: canBeSearched =
|
||||||
field.type !== LINK_TYPE &&
|
field.type !== LINK_TYPE &&
|
||||||
field.subtype !== AUTO_COLUMN_SUB_TYPES.CREATED_BY &&
|
field.subtype !== AUTO_COLUMN_SUB_TYPES.CREATED_BY &&
|
||||||
field.subtype !== AUTO_COLUMN_SUB_TYPES.UPDATED_BY
|
field.subtype !== AUTO_COLUMN_SUB_TYPES.UPDATED_BY &&
|
||||||
$: canBeDisplay = field.type !== LINK_TYPE && field.type !== AUTO_COL
|
field.type !== FORMULA_TYPE
|
||||||
|
$: canBeDisplay = field.type !== LINK_TYPE &&
|
||||||
|
field.type !== AUTO_TYPE &&
|
||||||
|
field.type !== FORMULA_TYPE
|
||||||
$: canBeRequired =
|
$: canBeRequired =
|
||||||
field.type !== LINK_TYPE && !uneditable && field.type !== AUTO_COL
|
field.type !== LINK_TYPE && !uneditable && field.type !== AUTO_TYPE
|
||||||
$: relationshipOptions = getRelationshipOptions(field)
|
$: relationshipOptions = getRelationshipOptions(field)
|
||||||
|
|
||||||
async function saveColumn() {
|
async function saveColumn() {
|
||||||
if (field.type === AUTO_COL) {
|
if (field.type === AUTO_TYPE) {
|
||||||
field = buildAutoColumn($tables.draft.name, field.name, field.subtype)
|
field = buildAutoColumn($tables.draft.name, field.name, field.subtype)
|
||||||
}
|
}
|
||||||
tables.saveField({
|
tables.saveField({
|
||||||
|
@ -115,7 +121,7 @@
|
||||||
|
|
||||||
function onChangeRequired(e) {
|
function onChangeRequired(e) {
|
||||||
const req = e.target.checked
|
const req = e.target.checked
|
||||||
field.constraints.presence = req ? { allowEmpty: false } : false
|
field.constraints.presence = req ? {allowEmpty: false} : false
|
||||||
required = req
|
required = req
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -123,7 +129,7 @@
|
||||||
const isPrimary = e.target.checked
|
const isPrimary = e.target.checked
|
||||||
// primary display is always required
|
// primary display is always required
|
||||||
if (isPrimary) {
|
if (isPrimary) {
|
||||||
field.constraints.presence = { allowEmpty: false }
|
field.constraints.presence = {allowEmpty: false}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -157,8 +163,8 @@
|
||||||
if (!linkTable) {
|
if (!linkTable) {
|
||||||
return null
|
return null
|
||||||
}
|
}
|
||||||
const thisName = truncate(table.name, { length: 14 }),
|
const thisName = truncate(table.name, {length: 14}),
|
||||||
linkName = truncate(linkTable.name, { length: 14 })
|
linkName = truncate(linkTable.name, {length: 14})
|
||||||
return [
|
return [
|
||||||
{
|
{
|
||||||
name: `Many ${thisName} rows → many ${linkName} rows`,
|
name: `Many ${thisName} rows → many ${linkName} rows`,
|
||||||
|
@ -192,7 +198,7 @@
|
||||||
{#each Object.values(fieldDefinitions) as field}
|
{#each Object.values(fieldDefinitions) as field}
|
||||||
<option value={field.type}>{field.name}</option>
|
<option value={field.type}>{field.name}</option>
|
||||||
{/each}
|
{/each}
|
||||||
<option value={AUTO_COL}>Auto Column</option>
|
<option value={AUTO_TYPE}>Auto Column</option>
|
||||||
</Select>
|
</Select>
|
||||||
|
|
||||||
{#if canBeRequired}
|
{#if canBeRequired}
|
||||||
|
@ -285,7 +291,15 @@
|
||||||
label={`Column Name in Other Table`}
|
label={`Column Name in Other Table`}
|
||||||
thin
|
thin
|
||||||
bind:value={field.fieldName} />
|
bind:value={field.fieldName} />
|
||||||
{:else if field.type === AUTO_COL}
|
{:else if field.type === FORMULA_TYPE}
|
||||||
|
<ModalBindableInput
|
||||||
|
title="Handlebars Formula"
|
||||||
|
label="Formula"
|
||||||
|
value={field.formula}
|
||||||
|
on:change={e => (field.formula = e.detail)}
|
||||||
|
bindings={getBindings({ table })}
|
||||||
|
serverSide=true />
|
||||||
|
{:else if field.type === AUTO_TYPE}
|
||||||
<Select label="Auto Column Type" thin secondary bind:value={field.subtype}>
|
<Select label="Auto Column Type" thin secondary bind:value={field.subtype}>
|
||||||
<option value="">Choose a subtype</option>
|
<option value="">Choose a subtype</option>
|
||||||
{#each Object.entries(getAutoColumnInformation()) as [subtype, info]}
|
{#each Object.entries(getAutoColumnInformation()) as [subtype, info]}
|
||||||
|
|
|
@ -5,6 +5,9 @@
|
||||||
import * as api from "../api"
|
import * as api from "../api"
|
||||||
import { ModalContent } from "@budibase/bbui"
|
import { ModalContent } from "@budibase/bbui"
|
||||||
import ErrorsBox from "components/common/ErrorsBox.svelte"
|
import ErrorsBox from "components/common/ErrorsBox.svelte"
|
||||||
|
import { FIELDS } from "constants/backend"
|
||||||
|
|
||||||
|
const FORMULA_TYPE = FIELDS.FORMULA.type
|
||||||
|
|
||||||
export let row = {}
|
export let row = {}
|
||||||
|
|
||||||
|
@ -44,7 +47,7 @@
|
||||||
onConfirm={saveRow}>
|
onConfirm={saveRow}>
|
||||||
<ErrorsBox {errors} />
|
<ErrorsBox {errors} />
|
||||||
{#each tableSchema as [key, meta]}
|
{#each tableSchema as [key, meta]}
|
||||||
{#if !meta.autocolumn}
|
{#if !meta.autocolumn && meta.type !== FORMULA_TYPE}
|
||||||
<div>
|
<div>
|
||||||
<RowFieldControl {meta} bind:value={row[key]} />
|
<RowFieldControl {meta} bind:value={row[key]} />
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -1,14 +1,16 @@
|
||||||
<script>
|
<script>
|
||||||
import { Icon, Input, Drawer, Label, Body, Button } from "@budibase/bbui"
|
import { Icon, Input, Drawer, Body, Button } from "@budibase/bbui"
|
||||||
import {
|
import {
|
||||||
readableToRuntimeBinding,
|
readableToRuntimeBinding,
|
||||||
runtimeToReadableBinding,
|
runtimeToReadableBinding,
|
||||||
} from "builderStore/dataBinding"
|
} from "builderStore/dataBinding"
|
||||||
import BindingPanel from "components/design/PropertiesPanel/BindingPanel.svelte"
|
import BindingPanel from "components/design/PropertiesPanel/BindingPanel.svelte"
|
||||||
|
import ServerBindingPanel from "components/common/ServerBindingPanel.svelte"
|
||||||
import { createEventDispatcher } from "svelte"
|
import { createEventDispatcher } from "svelte"
|
||||||
const dispatch = createEventDispatcher()
|
const dispatch = createEventDispatcher()
|
||||||
|
|
||||||
export let panel = BindingPanel
|
export let serverSide = false
|
||||||
|
export let panel = serverSide ? ServerBindingPanel : BindingPanel
|
||||||
export let value = ""
|
export let value = ""
|
||||||
export let bindings = []
|
export let bindings = []
|
||||||
export let thin = true
|
export let thin = true
|
||||||
|
|
|
@ -0,0 +1,92 @@
|
||||||
|
<script>
|
||||||
|
import { Icon, Input, Modal, Body, ModalContent } from "@budibase/bbui"
|
||||||
|
import {
|
||||||
|
readableToRuntimeBinding,
|
||||||
|
runtimeToReadableBinding,
|
||||||
|
} from "builderStore/dataBinding"
|
||||||
|
import ServerBindingPanel from "components/common/ServerBindingPanel.svelte"
|
||||||
|
import { createEventDispatcher } from "svelte"
|
||||||
|
const dispatch = createEventDispatcher()
|
||||||
|
|
||||||
|
export let panel = ServerBindingPanel
|
||||||
|
export let value = ""
|
||||||
|
export let bindings = []
|
||||||
|
export let thin = true
|
||||||
|
export let title = "Bindings"
|
||||||
|
export let placeholder
|
||||||
|
export let label
|
||||||
|
|
||||||
|
let bindingModal
|
||||||
|
let validity = true
|
||||||
|
|
||||||
|
$: readableValue = runtimeToReadableBinding(bindings, value)
|
||||||
|
$: tempValue = readableValue
|
||||||
|
$: invalid = !validity
|
||||||
|
|
||||||
|
const saveBinding = () => {
|
||||||
|
onChange(tempValue)
|
||||||
|
bindingModal.hide()
|
||||||
|
}
|
||||||
|
|
||||||
|
const onChange = input => {
|
||||||
|
dispatch("change", readableToRuntimeBinding(bindings, input))
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<div class="control">
|
||||||
|
<Input
|
||||||
|
{label}
|
||||||
|
{thin}
|
||||||
|
value={readableValue}
|
||||||
|
on:change={event => onChange(event.target.value)}
|
||||||
|
{placeholder} />
|
||||||
|
<div class="icon" on:click={bindingModal.show}>
|
||||||
|
<Icon name="lightning" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<Modal bind:this={bindingModal} width="50%">
|
||||||
|
<ModalContent
|
||||||
|
{title}
|
||||||
|
onConfirm={saveBinding}
|
||||||
|
bind:disabled={invalid}>
|
||||||
|
<Body extraSmall grey>
|
||||||
|
Add the objects on the left to enrich your text.
|
||||||
|
</Body>
|
||||||
|
<svelte:component
|
||||||
|
this={panel}
|
||||||
|
serverSide
|
||||||
|
value={readableValue}
|
||||||
|
bind:validity={validity}
|
||||||
|
on:update={event => (tempValue = event.detail)}
|
||||||
|
bindableProperties={bindings} />
|
||||||
|
</ModalContent>
|
||||||
|
</Modal>
|
||||||
|
|
||||||
|
<style>
|
||||||
|
.control {
|
||||||
|
flex: 1;
|
||||||
|
position: relative;
|
||||||
|
}
|
||||||
|
|
||||||
|
.icon {
|
||||||
|
right: 2px;
|
||||||
|
bottom: 2px;
|
||||||
|
position: absolute;
|
||||||
|
align-items: center;
|
||||||
|
display: flex;
|
||||||
|
box-sizing: border-box;
|
||||||
|
padding-left: 7px;
|
||||||
|
border-left: 1px solid var(--grey-4);
|
||||||
|
background-color: var(--grey-2);
|
||||||
|
border-top-right-radius: var(--border-radius-m);
|
||||||
|
border-bottom-right-radius: var(--border-radius-m);
|
||||||
|
color: var(--grey-7);
|
||||||
|
font-size: 14px;
|
||||||
|
height: 40px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.icon:hover {
|
||||||
|
color: var(--ink);
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
</style>
|
|
@ -4,18 +4,20 @@
|
||||||
import { createEventDispatcher } from "svelte"
|
import { createEventDispatcher } from "svelte"
|
||||||
import { isValid } from "@budibase/string-templates"
|
import { isValid } from "@budibase/string-templates"
|
||||||
import { handlebarsCompletions } from "constants/completions"
|
import { handlebarsCompletions } from "constants/completions"
|
||||||
|
import { readableToRuntimeBinding } from "../../builderStore/dataBinding"
|
||||||
|
|
||||||
const dispatch = createEventDispatcher()
|
const dispatch = createEventDispatcher()
|
||||||
|
|
||||||
export let value = ""
|
export let bindingContainer
|
||||||
export let bindingDrawer
|
|
||||||
export let bindableProperties = []
|
export let bindableProperties = []
|
||||||
|
export let validity = true
|
||||||
|
export let value = ""
|
||||||
|
|
||||||
|
let hasReadable = bindableProperties[0].readableBinding != null
|
||||||
let originalValue = value
|
let originalValue = value
|
||||||
let helpers = handlebarsCompletions()
|
let helpers = handlebarsCompletions()
|
||||||
let getCaretPosition
|
let getCaretPosition
|
||||||
let search = ""
|
let search = ""
|
||||||
let validity = true
|
|
||||||
|
|
||||||
$: categories = Object.entries(groupBy("category", bindableProperties))
|
$: categories = Object.entries(groupBy("category", bindableProperties))
|
||||||
$: value && checkValid()
|
$: value && checkValid()
|
||||||
|
@ -23,8 +25,12 @@
|
||||||
$: searchRgx = new RegExp(search, "ig")
|
$: searchRgx = new RegExp(search, "ig")
|
||||||
|
|
||||||
function checkValid() {
|
function checkValid() {
|
||||||
|
if (hasReadable) {
|
||||||
|
validity = isValid(readableToRuntimeBinding(bindableProperties, value))
|
||||||
|
} else {
|
||||||
validity = isValid(value)
|
validity = isValid(value)
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
function addToText(binding) {
|
function addToText(binding) {
|
||||||
const position = getCaretPosition()
|
const position = getCaretPosition()
|
||||||
|
@ -40,7 +46,9 @@
|
||||||
}
|
}
|
||||||
export function cancel() {
|
export function cancel() {
|
||||||
dispatch("update", originalValue)
|
dispatch("update", originalValue)
|
||||||
bindingDrawer.close()
|
if (bindingContainer && bindingContainer.close) {
|
||||||
|
bindingContainer.close()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
@ -53,14 +61,16 @@
|
||||||
{#each categories as [categoryName, bindings]}
|
{#each categories as [categoryName, bindings]}
|
||||||
<Heading extraSmall>{categoryName}</Heading>
|
<Heading extraSmall>{categoryName}</Heading>
|
||||||
<Spacer extraSmall />
|
<Spacer extraSmall />
|
||||||
{#each bindableProperties.filter(binding =>
|
{#each bindings.filter(binding =>
|
||||||
binding.label.match(searchRgx)
|
binding.label.match(searchRgx)
|
||||||
) as binding}
|
) as binding}
|
||||||
<div class="binding" on:click={() => addToText(binding)}>
|
<div class="binding" on:click={() => addToText(binding)}>
|
||||||
<span class="binding__label">{binding.label}</span>
|
<span class="binding__label">{binding.label}</span>
|
||||||
<span class="binding__type">{binding.type}</span>
|
<span class="binding__type">{binding.type}</span>
|
||||||
<br />
|
<br />
|
||||||
|
{#if binding.description}
|
||||||
<div class="binding__description">{binding.description || ''}</div>
|
<div class="binding__description">{binding.description || ''}</div>
|
||||||
|
{/if}
|
||||||
</div>
|
</div>
|
||||||
{/each}
|
{/each}
|
||||||
{/each}
|
{/each}
|
||||||
|
@ -129,8 +139,12 @@
|
||||||
|
|
||||||
.binding {
|
.binding {
|
||||||
font-size: 12px;
|
font-size: 12px;
|
||||||
padding: var(--spacing-s);
|
border: var(--border-light);
|
||||||
border-radius: var(--border-radius-m);
|
border-width: 1px 0 0 0;
|
||||||
|
padding: var(--spacing-m) 0;
|
||||||
|
margin: auto 0;
|
||||||
|
align-items: center;
|
||||||
|
cursor: pointer;
|
||||||
}
|
}
|
||||||
.binding:hover {
|
.binding:hover {
|
||||||
background-color: var(--grey-2);
|
background-color: var(--grey-2);
|
|
@ -80,6 +80,15 @@ export const FIELDS = {
|
||||||
presence: false,
|
presence: false,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
FORMULA: {
|
||||||
|
name: "Formula",
|
||||||
|
icon: "ri-braces-line",
|
||||||
|
type: "formula",
|
||||||
|
constraints: {
|
||||||
|
type: "string",
|
||||||
|
presence: false,
|
||||||
|
},
|
||||||
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
export const AUTO_COLUMN_SUB_TYPES = {
|
export const AUTO_COLUMN_SUB_TYPES = {
|
||||||
|
|
|
@ -88,6 +88,7 @@ exports.updateMetadata = async function(ctx) {
|
||||||
})
|
})
|
||||||
const metadata = {
|
const metadata = {
|
||||||
...globalUser,
|
...globalUser,
|
||||||
|
tableId: InternalTables.USER_METADATA,
|
||||||
_id: user._id || generateUserMetadataID(globalUser._id),
|
_id: user._id || generateUserMetadataID(globalUser._id),
|
||||||
_rev: user._rev,
|
_rev: user._rev,
|
||||||
}
|
}
|
||||||
|
|
|
@ -13,6 +13,7 @@ exports.FieldTypes = {
|
||||||
DATETIME: "datetime",
|
DATETIME: "datetime",
|
||||||
ATTACHMENT: "attachment",
|
ATTACHMENT: "attachment",
|
||||||
LINK: "link",
|
LINK: "link",
|
||||||
|
FORMULA: "formula",
|
||||||
AUTO: "auto",
|
AUTO: "auto",
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -10,6 +10,7 @@ const {
|
||||||
} = require("./linkUtils")
|
} = require("./linkUtils")
|
||||||
const { flatten } = require("lodash")
|
const { flatten } = require("lodash")
|
||||||
const CouchDB = require("../../db")
|
const CouchDB = require("../../db")
|
||||||
|
const { FieldTypes } = require("../../constants")
|
||||||
const { getMultiIDParams } = require("../../db/utils")
|
const { getMultiIDParams } = require("../../db/utils")
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -141,14 +142,14 @@ exports.attachLinkIDs = async (appId, rows) => {
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Given information about the table we can extract the display name from the linked rows, this
|
* Given a table and a list of rows this will retrieve all of the attached docs and enrich them into the row.
|
||||||
* is what we do for showing the display name of each linked row when in a table format.
|
* This is required for formula fields, this may only be utilised internally (for now).
|
||||||
* @param {string} appId The app in which the tables/rows/links exist.
|
* @param {string} appId The app in which the tables/rows/links exist.
|
||||||
* @param {object} table The table from which the rows originated.
|
* @param {object} table The table from which the rows originated.
|
||||||
* @param {array<object>} rows The rows which are to be enriched with the linked display names/IDs.
|
* @param {array<object>} rows The rows which are to be enriched.
|
||||||
* @returns {Promise<Array>} The enriched rows after having display names/IDs attached to the linked fields.
|
* @return {Promise<*>} returns the rows with all of the enriched relationships on it.
|
||||||
*/
|
*/
|
||||||
exports.attachLinkedPrimaryDisplay = async (appId, table, rows) => {
|
exports.attachFullLinkedDocs = async (appId, table, rows) => {
|
||||||
const linkedTableIds = getLinkedTableIDs(table)
|
const linkedTableIds = getLinkedTableIDs(table)
|
||||||
if (linkedTableIds.length === 0) {
|
if (linkedTableIds.length === 0) {
|
||||||
return rows
|
return rows
|
||||||
|
@ -161,7 +162,6 @@ exports.attachLinkedPrimaryDisplay = async (appId, table, rows) => {
|
||||||
const linked = (await db.allDocs(getMultiIDParams(linkedRowIds))).rows.map(
|
const linked = (await db.allDocs(getMultiIDParams(linkedRowIds))).rows.map(
|
||||||
row => row.doc
|
row => row.doc
|
||||||
)
|
)
|
||||||
// will populate this as we find them
|
|
||||||
const linkedTables = []
|
const linkedTables = []
|
||||||
for (let row of rows) {
|
for (let row of rows) {
|
||||||
for (let link of links.filter(link => link.thisId === row._id)) {
|
for (let link of links.filter(link => link.thisId === row._id)) {
|
||||||
|
@ -175,13 +175,44 @@ exports.attachLinkedPrimaryDisplay = async (appId, table, rows) => {
|
||||||
if (!linkedRow || !linkedTable) {
|
if (!linkedRow || !linkedTable) {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
const obj = { _id: linkedRow._id }
|
row[link.fieldName].push(linkedRow)
|
||||||
// if we know the display column, add it
|
|
||||||
if (linkedRow[linkedTable.primaryDisplay] != null) {
|
|
||||||
obj.primaryDisplay = linkedRow[linkedTable.primaryDisplay]
|
|
||||||
}
|
|
||||||
row[link.fieldName].push(obj)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return rows
|
return rows
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* This function will take the given enriched rows and squash the links to only contain the primary display field.
|
||||||
|
* @param {string} appId The app in which the tables/rows/links exist.
|
||||||
|
* @param {object} table The table from which the rows originated.
|
||||||
|
* @param {array<object>} enriched The pre-enriched rows (full docs) which are to be squashed.
|
||||||
|
* @returns {Promise<Array>} The rows after having their links squashed to only contain the ID and primary display.
|
||||||
|
*/
|
||||||
|
exports.squashLinksToPrimaryDisplay = async (appId, table, enriched) => {
|
||||||
|
const db = new CouchDB(appId)
|
||||||
|
// will populate this as we find them
|
||||||
|
const linkedTables = []
|
||||||
|
for (let [column, schema] of Object.entries(table.schema)) {
|
||||||
|
if (schema.type !== FieldTypes.LINK) {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
for (let row of enriched) {
|
||||||
|
if (!row[column] || !row[column].length) {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
const newLinks = []
|
||||||
|
for (let link of row[column]) {
|
||||||
|
const linkTblId = link.tableId || getRelatedTableForField(table, column)
|
||||||
|
// this only fetches the table if its not already in array
|
||||||
|
const linkedTable = await getLinkedTable(db, linkTblId, linkedTables)
|
||||||
|
const obj = { _id: link._id }
|
||||||
|
if (link[linkedTable.primaryDisplay]) {
|
||||||
|
obj.primaryDisplay = link[linkedTable.primaryDisplay]
|
||||||
|
}
|
||||||
|
newLinks.push(obj)
|
||||||
|
}
|
||||||
|
row[column] = newLinks
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return enriched
|
||||||
|
}
|
||||||
|
|
|
@ -3,6 +3,7 @@ const { OBJ_STORE_DIRECTORY } = require("../constants")
|
||||||
const linkRows = require("../db/linkedRows")
|
const linkRows = require("../db/linkedRows")
|
||||||
const { cloneDeep } = require("lodash/fp")
|
const { cloneDeep } = require("lodash/fp")
|
||||||
const { FieldTypes, AutoFieldSubTypes } = require("../constants")
|
const { FieldTypes, AutoFieldSubTypes } = require("../constants")
|
||||||
|
const { processStringSync } = require("@budibase/string-templates")
|
||||||
|
|
||||||
const BASE_AUTO_ID = 1
|
const BASE_AUTO_ID = 1
|
||||||
|
|
||||||
|
@ -34,6 +35,11 @@ const TYPE_TRANSFORM_MAP = {
|
||||||
[null]: "",
|
[null]: "",
|
||||||
[undefined]: undefined,
|
[undefined]: undefined,
|
||||||
},
|
},
|
||||||
|
[FieldTypes.FORMULA]: {
|
||||||
|
"": "",
|
||||||
|
[null]: "",
|
||||||
|
[undefined]: undefined,
|
||||||
|
},
|
||||||
[FieldTypes.LONGFORM]: {
|
[FieldTypes.LONGFORM]: {
|
||||||
"": "",
|
"": "",
|
||||||
[null]: "",
|
[null]: "",
|
||||||
|
@ -118,6 +124,41 @@ function processAutoColumn(user, table, row) {
|
||||||
return { table, row }
|
return { table, row }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Given a set of rows and the table they came from this function will sort by auto ID or a custom
|
||||||
|
* method if provided (not implemented yet).
|
||||||
|
*/
|
||||||
|
function sortRows(table, rows) {
|
||||||
|
// sort based on auto ID (if found)
|
||||||
|
let autoIDColumn = Object.entries(table.schema).find(
|
||||||
|
schema => schema[1].subtype === AutoFieldSubTypes.AUTO_ID
|
||||||
|
)
|
||||||
|
// get the column name, this is the first element in the array (Object.entries)
|
||||||
|
autoIDColumn = autoIDColumn && autoIDColumn.length ? autoIDColumn[0] : null
|
||||||
|
if (autoIDColumn) {
|
||||||
|
// sort in ascending order
|
||||||
|
rows.sort((a, b) => a[autoIDColumn] - b[autoIDColumn])
|
||||||
|
}
|
||||||
|
return rows
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Looks through the rows provided and finds formulas - which it then processes.
|
||||||
|
*/
|
||||||
|
function processFormulas(table, rows) {
|
||||||
|
for (let [column, schema] of Object.entries(table.schema)) {
|
||||||
|
if (schema.type !== FieldTypes.FORMULA) {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
// iterate through rows and process formula
|
||||||
|
rows = rows.map(row => ({
|
||||||
|
...row,
|
||||||
|
[column]: processStringSync(schema.formula, row),
|
||||||
|
}))
|
||||||
|
}
|
||||||
|
return rows
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* This will coerce a value to the correct types based on the type transform map
|
* This will coerce a value to the correct types based on the type transform map
|
||||||
* @param {object} row The value to coerce
|
* @param {object} row The value to coerce
|
||||||
|
@ -173,17 +214,19 @@ exports.outputProcessing = async (appId, table, rows) => {
|
||||||
rows = [rows]
|
rows = [rows]
|
||||||
wasArray = false
|
wasArray = false
|
||||||
}
|
}
|
||||||
|
// sort by auto ID
|
||||||
|
rows = sortRows(table, rows)
|
||||||
// attach any linked row information
|
// attach any linked row information
|
||||||
const outputRows = await linkRows.attachLinkedPrimaryDisplay(
|
let enriched = await linkRows.attachFullLinkedDocs(appId, table, rows)
|
||||||
appId,
|
|
||||||
table,
|
// process formulas
|
||||||
rows
|
enriched = processFormulas(table, enriched)
|
||||||
)
|
|
||||||
// update the attachments URL depending on hosting
|
// update the attachments URL depending on hosting
|
||||||
if (env.isProd() && env.SELF_HOSTED) {
|
if (env.isProd() && env.SELF_HOSTED) {
|
||||||
for (let [property, column] of Object.entries(table.schema)) {
|
for (let [property, column] of Object.entries(table.schema)) {
|
||||||
if (column.type === FieldTypes.ATTACHMENT) {
|
if (column.type === FieldTypes.ATTACHMENT) {
|
||||||
for (let row of outputRows) {
|
for (let row of enriched) {
|
||||||
if (row[property] == null || row[property].length === 0) {
|
if (row[property] == null || row[property].length === 0) {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
@ -195,5 +238,6 @@ exports.outputProcessing = async (appId, table, rows) => {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return wasArray ? outputRows : outputRows[0]
|
enriched = await linkRows.squashLinksToPrimaryDisplay(appId, table, enriched)
|
||||||
|
return wasArray ? enriched : enriched[0]
|
||||||
}
|
}
|
||||||
|
|
|
@ -122,12 +122,15 @@ exports.saveGlobalUser = async (ctx, appId, body) => {
|
||||||
if (json.status !== 200 && response.status !== 200) {
|
if (json.status !== 200 && response.status !== 200) {
|
||||||
ctx.throw(400, "Unable to save global user.")
|
ctx.throw(400, "Unable to save global user.")
|
||||||
}
|
}
|
||||||
delete body.email
|
|
||||||
delete body.password
|
delete body.password
|
||||||
delete body.roleId
|
|
||||||
delete body.status
|
|
||||||
delete body.roles
|
delete body.roles
|
||||||
delete body.builder
|
delete body.builder
|
||||||
|
// TODO: for now these have been left in as they are
|
||||||
|
// TODO: pretty important to keeping relationships working
|
||||||
|
// TODO: however if user metadata is changed this should be removed
|
||||||
|
// delete body.email
|
||||||
|
// delete body.roleId
|
||||||
|
// delete body.status
|
||||||
return {
|
return {
|
||||||
...body,
|
...body,
|
||||||
_id: json._id,
|
_id: json._id,
|
||||||
|
|
Loading…
Reference in New Issue