Merge commit

This commit is contained in:
Dean 2024-06-13 16:56:25 +01:00
parent 3f35a41046
commit 3a10c57651
21 changed files with 852 additions and 471 deletions

View File

@ -8,10 +8,41 @@
import { automationStore, selectedAutomation } from "stores/builder" import { automationStore, selectedAutomation } from "stores/builder"
import AutomationBlockSetup from "../../SetupPanel/AutomationBlockSetup.svelte" import AutomationBlockSetup from "../../SetupPanel/AutomationBlockSetup.svelte"
import { cloneDeep } from "lodash/fp" import { cloneDeep } from "lodash/fp"
import { memo } from "@budibase/frontend-core"
import { AutomationEventType } from "@budibase/types"
let failedParse = null let failedParse = null
let trigger = {} let trigger = {}
let schemaProperties = {} let schemaProperties = {}
let baseData = {}
let rowEvents = [
AutomationEventType.ROW_DELETE,
AutomationEventType.ROW_SAVE,
AutomationEventType.ROW_UPDATE,
]
const memoTestData = memo($selectedAutomation.testData)
$: memoTestData.set($selectedAutomation.testData)
$: if (memoTestData) {
baseData = cloneDeep($selectedAutomation.testData)
// Reset the test data for row trigger data when the table is changed.
if (rowEvents.includes(trigger?.event)) {
if (
!baseData?.row?.tableId ||
baseData.row.tableId !== trigger.inputs?.tableId
) {
baseData = {
...baseData,
_tableId: trigger.inputs?.tableId,
row: { tableId: trigger.inputs?.tableId },
meta: {},
id: "",
}
}
}
}
$: { $: {
// clone the trigger so we're not mutating the reference // clone the trigger so we're not mutating the reference
@ -20,19 +51,15 @@
// get the outputs so we can define the fields // get the outputs so we can define the fields
let schema = Object.entries(trigger.schema?.outputs?.properties || {}) let schema = Object.entries(trigger.schema?.outputs?.properties || {})
if (trigger?.event === "app:trigger") { if (trigger?.event === AutomationEventType.APP_TRIGGER) {
schema = [["fields", { customType: "fields" }]] schema = [["fields", { customType: "fields" }]]
} }
schemaProperties = schema schemaProperties = schema
} }
// check to see if there is existing test data in the store
$: testData = $selectedAutomation.testData || {}
// Check the schema to see if required fields have been entered // Check the schema to see if required fields have been entered
$: isError = !trigger.schema.outputs.required.every( $: isError = !trigger.schema.outputs.required.every(
required => testData[required] || required !== "row" required => baseData?.[required] || required !== "row"
) )
function parseTestJSON(e) { function parseTestJSON(e) {
@ -47,7 +74,7 @@
const testAutomation = async () => { const testAutomation = async () => {
try { try {
await automationStore.actions.test($selectedAutomation, testData) await automationStore.actions.test($selectedAutomation, baseData)
$automationStore.showTestPanel = true $automationStore.showTestPanel = true
} catch (error) { } catch (error) {
notifications.error(error) notifications.error(error)
@ -85,7 +112,7 @@
{#if selectedValues} {#if selectedValues}
<div class="tab-content-padding"> <div class="tab-content-padding">
<AutomationBlockSetup <AutomationBlockSetup
{testData} testData={baseData}
{schemaProperties} {schemaProperties}
isTestModal isTestModal
block={trigger} block={trigger}

View File

@ -1,11 +1,9 @@
<script> <script>
import TableSelector from "./TableSelector.svelte" import TableSelector from "./TableSelector.svelte"
import RowSelector from "./RowSelector.svelte"
import FieldSelector from "./FieldSelector.svelte" import FieldSelector from "./FieldSelector.svelte"
import SchemaSetup from "./SchemaSetup.svelte" import SchemaSetup from "./SchemaSetup.svelte"
import { import {
Button, Button,
Input,
Select, Select,
Label, Label,
ActionButton, ActionButton,
@ -15,23 +13,25 @@
Checkbox, Checkbox,
DatePicker, DatePicker,
DrawerContent, DrawerContent,
Helpers,
} from "@budibase/bbui" } from "@budibase/bbui"
import CreateWebhookModal from "components/automation/Shared/CreateWebhookModal.svelte" import CreateWebhookModal from "components/automation/Shared/CreateWebhookModal.svelte"
import { automationStore, selectedAutomation, tables } from "stores/builder" import { automationStore, selectedAutomation, tables } from "stores/builder"
import { environment, licensing } from "stores/portal" import { environment, licensing } from "stores/portal"
import WebhookDisplay from "../Shared/WebhookDisplay.svelte" import WebhookDisplay from "../Shared/WebhookDisplay.svelte"
import DrawerBindableInput from "../../common/bindings/DrawerBindableInput.svelte" import {
import DrawerBindableSlot from "../../common/bindings/DrawerBindableSlot.svelte" BindingSidePanel,
import AutomationBindingPanel from "../../common/bindings/ServerBindingPanel.svelte" DrawerBindableSlot,
DrawerBindableInput,
ServerBindingPanel as AutomationBindingPanel,
ModalBindableInput,
} from "components/common/bindings"
import CodeEditorModal from "./CodeEditorModal.svelte" import CodeEditorModal from "./CodeEditorModal.svelte"
import QuerySelector from "./QuerySelector.svelte"
import QueryParamSelector from "./QueryParamSelector.svelte" import QueryParamSelector from "./QueryParamSelector.svelte"
import AutomationSelector from "./AutomationSelector.svelte" import AutomationSelector from "./AutomationSelector.svelte"
import CronBuilder from "./CronBuilder.svelte" import CronBuilder from "./CronBuilder.svelte"
import Editor from "components/integration/QueryEditor.svelte" import Editor from "components/integration/QueryEditor.svelte"
import ModalBindableInput from "components/common/bindings/ModalBindableInput.svelte"
import CodeEditor from "components/common/CodeEditor/CodeEditor.svelte" import CodeEditor from "components/common/CodeEditor/CodeEditor.svelte"
import BindingSidePanel from "components/common/bindings/BindingSidePanel.svelte"
import KeyValueBuilder from "components/integration/KeyValueBuilder.svelte" import KeyValueBuilder from "components/integration/KeyValueBuilder.svelte"
import { BindingHelpers, BindingType } from "components/common/bindings/utils" import { BindingHelpers, BindingType } from "components/common/bindings/utils"
import { import {
@ -40,7 +40,7 @@
EditorModes, EditorModes,
} from "components/common/CodeEditor" } from "components/common/CodeEditor"
import FilterBuilder from "components/design/settings/controls/FilterEditor/FilterBuilder.svelte" import FilterBuilder from "components/design/settings/controls/FilterEditor/FilterBuilder.svelte"
import { LuceneUtils, Utils } from "@budibase/frontend-core" import { LuceneUtils, Utils, memo } from "@budibase/frontend-core"
import { import {
getSchemaForDatasourcePlus, getSchemaForDatasourcePlus,
getEnvironmentBindings, getEnvironmentBindings,
@ -48,23 +48,37 @@
import { TriggerStepID, ActionStepID } from "constants/backend/automations" import { TriggerStepID, ActionStepID } from "constants/backend/automations"
import { onMount } from "svelte" import { onMount } from "svelte"
import { cloneDeep } from "lodash/fp" import { cloneDeep } from "lodash/fp"
import { AutomationEventType } from "@budibase/types"
import { FIELDS } from "constants/backend" import { FIELDS } from "constants/backend"
import PropField from "./PropField.svelte"
export let block export let block
export let testData export let testData
export let schemaProperties export let schemaProperties
export let isTestModal = false export let isTestModal = false
// Stop unnecessary rendering
const memoBlock = memo(block)
const rowTriggers = [
TriggerStepID.ROW_UPDATED,
TriggerStepID.ROW_SAVED,
TriggerStepID.ROW_DELETED,
]
const rowSteps = [ActionStepID.UPDATE_ROW, ActionStepID.CREATE_ROW]
let webhookModal let webhookModal
let drawer let drawer
let inputData let inputData
let insertAtPos, getCaretPosition let insertAtPos, getCaretPosition
let stepLayouts = {}
$: memoBlock.set(block)
$: filters = lookForFilters(schemaProperties) || [] $: filters = lookForFilters(schemaProperties) || []
$: tempFilters = filters $: tempFilters = filters
$: stepId = block.stepId $: stepId = block.stepId
$: bindings = getAvailableBindings(block, $selectedAutomation?.definition) $: bindings = getAvailableBindings(block, $selectedAutomation?.definition)
$: getInputData(testData, block.inputs) $: getInputData(testData, $memoBlock.inputs)
$: tableId = inputData ? inputData.tableId : null $: tableId = inputData ? inputData.tableId : null
$: table = tableId $: table = tableId
? $tables.list.find(table => table._id === inputData.tableId) ? $tables.list.find(table => table._id === inputData.tableId)
@ -75,14 +89,15 @@
$: schemaFields = Object.values(schema || {}) $: schemaFields = Object.values(schema || {})
$: queryLimit = tableId?.includes("datasource") ? "∞" : "1000" $: queryLimit = tableId?.includes("datasource") ? "∞" : "1000"
$: isTrigger = block?.type === "TRIGGER" $: isTrigger = block?.type === "TRIGGER"
$: isUpdateRow = stepId === ActionStepID.UPDATE_ROW
$: codeMode = $: codeMode =
stepId === "EXECUTE_BASH" ? EditorModes.Handlebars : EditorModes.JS stepId === "EXECUTE_BASH" ? EditorModes.Handlebars : EditorModes.JS
$: bindingsHelpers = new BindingHelpers(getCaretPosition, insertAtPos, { $: bindingsHelpers = new BindingHelpers(getCaretPosition, insertAtPos, {
disableWrapping: true, disableWrapping: true,
}) })
$: editingJs = codeMode === EditorModes.JS $: editingJs = codeMode === EditorModes.JS
$: requiredProperties = block.schema.inputs.required || [] $: requiredProperties =
block.schema[isTestModal ? "outputs" : "inputs"].required || []
$: stepCompletions = $: stepCompletions =
codeMode === EditorModes.Handlebars codeMode === EditorModes.Handlebars
? [hbAutocomplete([...bindingsToCompletions(bindings, codeMode)])] ? [hbAutocomplete([...bindingsToCompletions(bindings, codeMode)])]
@ -93,10 +108,12 @@
let newInputData = testData || cloneDeep(blockInputs) let newInputData = testData || cloneDeep(blockInputs)
// Ensures the app action fields are populated // Ensures the app action fields are populated
if (block.event === "app:trigger" && !newInputData?.fields) { if (
block.event === AutomationEventType.APP_TRIGGER &&
!newInputData?.fields
) {
newInputData = cloneDeep(blockInputs) newInputData = cloneDeep(blockInputs)
} }
inputData = newInputData inputData = newInputData
setDefaultEnumValues() setDefaultEnumValues()
} }
@ -108,15 +125,136 @@
} }
} }
} }
const onChange = Utils.sequential(async (e, key) => {
$: customStepLayouts($memoBlock, schemaProperties)
const customStepLayouts = block => {
if (
rowSteps.includes(block.stepId) ||
(rowTriggers.includes(block.stepId) && isTestModal)
) {
const schema = schemaProperties.reduce((acc, entry) => {
const [key, val] = entry
acc[key] = val
return acc
}, {})
// Optionally build the rev field config when its needed.
const getRevConfig = () => {
const rowRevEntry = schema["revision"]
if (!rowRevEntry) {
return []
}
const rowRevlabel = getFieldLabel("revision", rowRevEntry)
return isTestModal
? [
{
type: DrawerBindableInput,
title: rowRevlabel,
panel: AutomationBindingPanel,
value: inputData["revision"],
onChange: e => {
onChange({ ["revision"]: e.detail })
},
bindings,
updateOnChange: false,
forceModal: true,
},
]
: []
}
const getIdConfig = () => {
const rowIdentifier = isTestModal ? "id" : "rowId"
const rowIdEntry = schema[rowIdentifier]
if (!rowIdEntry) {
return []
}
const rowIdlabel = getFieldLabel(rowIdentifier, rowIdEntry)
return [
{
type: DrawerBindableInput,
title: rowIdlabel,
props: {
panel: AutomationBindingPanel,
value: inputData[rowIdentifier],
onChange: e => {
onChange({ [rowIdentifier]: e.detail })
},
bindings,
updateOnChange: false,
forceModal: true,
},
},
]
}
stepLayouts[block.stepId] = {
row: {
schema: schema["row"],
//?layout: RowLayoutStepComponent.
content: [
{
type: TableSelector,
title: "Table",
props: {
isTrigger,
value: inputData["row"]?.tableId ?? "",
onChange: e => {
onChange({
_tableId: e.detail,
meta: {},
["row"]: {
tableId: e.detail,
},
})
},
disabled: isTestModal,
},
},
...getIdConfig(),
...getRevConfig(),
{
type: Divider,
props: {
noMargin: true,
},
},
{
type: RowSelector,
props: {
row: inputData["row"],
meta: inputData["meta"] || {},
onChange: e => {
onChange(e.detail)
},
bindings,
isTestModal,
isUpdateRow: block.stepId === ActionStepID.UPDATE_ROW,
},
},
],
},
}
}
}
const onChange = Utils.sequential(async update => {
// We need to cache the schema as part of the definition because it is // We need to cache the schema as part of the definition because it is
// used in the server to detect relationships. It would be far better to // used in the server to detect relationships. It would be far better to
// instead fetch the schema in the backend at runtime. // instead fetch the schema in the backend at runtime.
const request = cloneDeep(update)
let schema let schema
if (e.detail?.tableId) { if (request?._tableId) {
schema = getSchemaForDatasourcePlus(e.detail.tableId, { schema = getSchemaForDatasourcePlus(request._tableId, {
searchableSchema: true, searchableSchema: true,
}).schema }).schema
delete request._tableId
} }
try { try {
@ -128,21 +266,22 @@
newTestData = { newTestData = {
...newTestData, ...newTestData,
body: { body: {
[key]: e.detail, ...update,
...$selectedAutomation.testData?.body, ...$selectedAutomation.testData?.body,
}, },
} }
} }
newTestData = { newTestData = {
...newTestData, ...newTestData,
[key]: e.detail, ...request,
} }
await automationStore.actions.addTestDataToAutomation(newTestData) await automationStore.actions.addTestDataToAutomation(newTestData)
} else { } else {
const data = { schema, [key]: e.detail } const data = { schema, ...request }
await automationStore.actions.updateBlockInputs(block, data) await automationStore.actions.updateBlockInputs(block, data)
} }
} catch (error) { } catch (error) {
console.error("Error saving automation", error)
notifications.error("Error saving automation") notifications.error("Error saving automation")
} }
}) })
@ -187,14 +326,17 @@
let runtimeName let runtimeName
/* Begin special cases for generating custom schemas based on triggers */ /* Begin special cases for generating custom schemas based on triggers */
if (idx === 0 && automation.trigger?.event === "app:trigger") { if (
idx === 0 &&
automation.trigger?.event === AutomationEventType.APP_TRIGGER
) {
return `trigger.fields.${name}` return `trigger.fields.${name}`
} }
if ( if (
idx === 0 && idx === 0 &&
(automation.trigger?.event === "row:update" || (automation.trigger?.event === AutomationEventType.ROW_UPDATE ||
automation.trigger?.event === "row:save") automation.trigger?.event === AutomationEventType.ROW_SAVE)
) { ) {
if (name !== "id" && name !== "revision") return `trigger.row.${name}` if (name !== "id" && name !== "revision") return `trigger.row.${name}`
} }
@ -268,7 +410,10 @@
} }
} }
if (idx === 0 && automation.trigger?.event === "app:trigger") { if (
idx === 0 &&
automation.trigger?.event === AutomationEventType.APP_TRIGGER
) {
schema = Object.fromEntries( schema = Object.fromEntries(
Object.keys(automation.trigger.inputs.fields || []).map(key => [ Object.keys(automation.trigger.inputs.fields || []).map(key => [
key, key,
@ -277,8 +422,9 @@
) )
} }
if ( if (
(idx === 0 && automation.trigger.event === "row:update") || (idx === 0 &&
(idx === 0 && automation.trigger.event === "row:save") automation.trigger.event === AutomationEventType.ROW_UPDATE) ||
(idx === 0 && automation.trigger.event === AutomationEventType.ROW_SAVE)
) { ) {
let table = $tables.list.find( let table = $tables.list.find(
table => table._id === automation.trigger.inputs.tableId table => table._id === automation.trigger.inputs.tableId
@ -344,10 +490,12 @@
function saveFilters(key) { function saveFilters(key) {
const filters = LuceneUtils.buildLuceneQuery(tempFilters) const filters = LuceneUtils.buildLuceneQuery(tempFilters)
const defKey = `${key}-def`
onChange({ detail: filters }, key) onChange({
// need to store the builder definition in the automation [key]: filters,
onChange({ detail: tempFilters }, defKey) [`${key}-def`]: tempFilters, // need to store the builder definition in the automation
})
drawer.hide() drawer.hide()
} }
@ -364,6 +512,7 @@
value.customType !== "cron" && value.customType !== "cron" &&
value.customType !== "triggerSchema" && value.customType !== "triggerSchema" &&
value.customType !== "automationFields" && value.customType !== "automationFields" &&
value.customType !== "fields" &&
value.type !== "signature_single" && value.type !== "signature_single" &&
value.type !== "attachment" && value.type !== "attachment" &&
value.type !== "attachment_single" value.type !== "attachment_single"
@ -372,7 +521,10 @@
function getFieldLabel(key, value) { function getFieldLabel(key, value) {
const requiredSuffix = requiredProperties.includes(key) ? "*" : "" const requiredSuffix = requiredProperties.includes(key) ? "*" : ""
return `${value.title || (key === "row" ? "Table" : key)} ${requiredSuffix}` const label = `${
value.title || (key === "row" ? "Table" : key)
} ${requiredSuffix}`
return Helpers.capitalise(label)
} }
function handleAttachmentParams(keyValueObj) { function handleAttachmentParams(keyValueObj) {
@ -394,293 +546,269 @@
}) })
</script> </script>
<div class="fields"> <div class="step-fields">
{#each schemaProperties as [key, value]} <!-- Custom Layouts -->
{#if canShowField(key, value)} {#if stepLayouts[block.stepId]}
{@const label = getFieldLabel(key, value)} {#each Object.keys(stepLayouts[block.stepId] || {}) as key}
<div class:block-field={shouldRenderField(value)}> {#if canShowField(key, stepLayouts[block.stepId].schema)}
{#if key !== "fields" && value.type !== "boolean" && shouldRenderField(value)} {#each stepLayouts[block.stepId][key].content as config}
<Label {#if config.title}
tooltip={value.title === "Binding / Value" <PropField label={config.title}>
? "If using the String input type, please use a comma or newline separated string" <svelte:component
: null}>{label}</Label this={config.type}
> {...config.props}
{/if} on:change={config.props.onChange}
<div class:field-width={shouldRenderField(value)}>
{#if value.type === "string" && value.enum && canShowField(key, value)}
<Select
on:change={e => onChange(e, key)}
value={inputData[key]}
placeholder={false}
options={value.enum}
getOptionLabel={(x, idx) =>
value.pretty ? value.pretty[idx] : x}
/>
{:else if value.type === "json"}
<Editor
editorHeight="250"
editorWidth="448"
mode="json"
value={inputData[key]?.value}
on:change={e => {
onChange(e, key)
}}
/>
{:else if value.type === "boolean"}
<div style="margin-top: 10px">
<Checkbox
text={value.title}
value={inputData[key]}
on:change={e => onChange(e, key)}
/> />
</div> </PropField>
{:else if value.type === "date"} {:else}
<DrawerBindableSlot <svelte:component
title={value.title ?? label} this={config.type}
panel={AutomationBindingPanel} {...config.props}
type={"date"} on:change={config.props.onChange}
value={inputData[key]} />
on:change={e => onChange(e, key)} {/if}
{bindings} {/each}
allowJS={true} {/if}
updateOnChange={false} {/each}
drawerLeft="260px" {:else}
<!-- Default Schema Property Layout -->
{#each schemaProperties as [key, value]}
{#if canShowField(key, value)}
{@const label = getFieldLabel(key, value)}
<div class:block-field={shouldRenderField(value)}>
{#if key !== "fields" && value.type !== "boolean" && shouldRenderField(value)}
<Label
tooltip={value.title === "Binding / Value"
? "If using the String input type, please use a comma or newline separated string"
: null}
> >
<DatePicker {label}
</Label>
{/if}
<div class:field-width={shouldRenderField(value)}>
{#if value.type === "string" && value.enum && canShowField(key, value)}
<Select
on:change={e => onChange({ [key]: e.detail })}
value={inputData[key]} value={inputData[key]}
on:change={e => onChange(e, key)} placeholder={false}
options={value.enum}
getOptionLabel={(x, idx) =>
value.pretty ? value.pretty[idx] : x}
/> />
</DrawerBindableSlot> {:else if value.type === "json"}
{:else if value.customType === "column"} <Editor
<Select editorHeight="250"
on:change={e => onChange(e, key)} editorWidth="448"
value={inputData[key]} mode="json"
options={Object.keys(table?.schema || {})} value={inputData[key]?.value}
/> on:change={e => onChange({ [key]: e.detail })}
{:else if value.type === "attachment" || value.type === "signature_single"} />
<div class="attachment-field-wrapper"> {:else if value.type === "boolean"}
<div class="label-wrapper"> <div style="margin-top: 10px">
<Label>{label}</Label> <Checkbox
text={value.title}
value={inputData[key]}
on:change={e => onChange({ [key]: e.detail })}
/>
</div> </div>
<div class="attachment-field-width"> {:else if value.type === "date"}
<KeyValueBuilder <DrawerBindableSlot
on:change={e => title={value.title ?? label}
onChange( panel={AutomationBindingPanel}
{ type={"date"}
detail: e.detail.map(({ name, value }) => ({ value={inputData[key]}
on:change={e => onChange({ [key]: e.detail })}
{bindings}
allowJS={true}
updateOnChange={false}
drawerLeft="260px"
>
<DatePicker
value={inputData[key]}
on:change={e => onChange({ [key]: e.detail })}
/>
</DrawerBindableSlot>
{:else if value.customType === "column"}
<Select
on:change={e => onChange({ [key]: e.detail })}
value={inputData[key]}
options={Object.keys(table?.schema || {})}
/>
{:else if value.type === "attachment" || value.type === "signature_single"}
<div class="attachment-field-wrapper">
<div class="label-wrapper">
<Label>{label}</Label>
</div>
<div class="attachment-field-width">
<KeyValueBuilder
on:change={e =>
onChange({
[key]: e.detail.map(({ name, value }) => ({
url: name, url: name,
filename: value, filename: value,
})), })),
}, })}
key object={handleAttachmentParams(inputData[key])}
)} allowJS
object={handleAttachmentParams(inputData[key])} {bindings}
allowJS keyBindings
{bindings} customButtonText={value.type === "attachment"
keyBindings ? "Add attachment"
customButtonText={"Add attachment"} : "Add signature"}
keyPlaceholder={"URL"} keyPlaceholder={"URL"}
valuePlaceholder={"Filename"} valuePlaceholder={"Filename"}
/>
</div>
</div>
{:else if value.customType === "filters"}
<ActionButton on:click={drawer.show}>Define filters</ActionButton>
<Drawer bind:this={drawer} title="Filtering">
<Button cta slot="buttons" on:click={() => saveFilters(key)}>
Save
</Button>
<DrawerContent slot="body">
<FilterBuilder
{filters}
{bindings}
{schemaFields}
datasource={{ type: "table", tableId }}
panel={AutomationBindingPanel}
on:change={e => (tempFilters = e.detail)}
/>
</DrawerContent>
</Drawer>
{:else if value.customType === "password"}
<Input
type="password"
on:change={e => onChange(e, key)}
value={inputData[key]}
/>
{:else if value.customType === "email"}
{#if isTestModal}
<ModalBindableInput
title={value.title ?? label}
value={inputData[key]}
panel={AutomationBindingPanel}
type="email"
on:change={e => onChange(e, key)}
{bindings}
updateOnChange={false}
/>
{:else}
<DrawerBindableInput
title={value.title ?? label}
panel={AutomationBindingPanel}
type="email"
value={inputData[key]}
on:change={e => onChange(e, key)}
{bindings}
allowJS={false}
updateOnChange={false}
drawerLeft="260px"
/>
{/if}
{:else if value.customType === "query"}
<QuerySelector
on:change={e => onChange(e, key)}
value={inputData[key]}
/>
{:else if value.customType === "cron"}
<CronBuilder
on:change={e => onChange(e, key)}
value={inputData[key]}
/>
{:else if value.customType === "automationFields"}
<AutomationSelector
on:change={e => onChange(e, key)}
value={inputData[key]}
{bindings}
/>
{:else if value.customType === "queryParams"}
<QueryParamSelector
on:change={e => onChange(e, key)}
value={inputData[key]}
{bindings}
/>
{:else if value.customType === "table"}
<TableSelector
{isTrigger}
value={inputData[key]}
on:change={e => onChange(e, key)}
/>
{:else if value.customType === "row"}
<RowSelector
value={inputData[key]}
meta={inputData["meta"] || {}}
on:change={e => {
if (e.detail?.key) {
onChange(e, e.detail.key)
} else {
onChange(e, key)
}
}}
{bindings}
{isTestModal}
{isUpdateRow}
/>
{:else if value.customType === "webhookUrl"}
<WebhookDisplay
on:change={e => onChange(e, key)}
value={inputData[key]}
/>
{:else if value.customType === "fields"}
<FieldSelector
{block}
value={inputData[key]}
on:change={e => onChange(e, key)}
{bindings}
{isTestModal}
/>
{:else if value.customType === "triggerSchema"}
<SchemaSetup
on:change={e => onChange(e, key)}
value={inputData[key]}
/>
{:else if value.customType === "code"}
<CodeEditorModal>
<div class:js-editor={editingJs}>
<div class:js-code={editingJs} style="width:100%;height:500px;">
<CodeEditor
value={inputData[key]}
on:change={e => {
// need to pass without the value inside
onChange({ detail: e.detail }, key)
inputData[key] = e.detail
}}
completions={stepCompletions}
mode={codeMode}
autocompleteEnabled={codeMode !== EditorModes.JS}
bind:getCaretPosition
bind:insertAtPos
placeholder={codeMode === EditorModes.Handlebars
? "Add bindings by typing {{"
: null}
/> />
</div> </div>
{#if editingJs} </div>
<div class="js-binding-picker"> {:else if value.customType === "filters"}
<BindingSidePanel <ActionButton on:click={drawer.show}>Define filters</ActionButton>
{bindings} <Drawer bind:this={drawer} title="Filtering">
allowHelpers={false} <Button cta slot="buttons" on:click={() => saveFilters(key)}>
addBinding={binding => Save
bindingsHelpers.onSelectBinding( </Button>
inputData[key], <DrawerContent slot="body">
binding, <FilterBuilder
{ {filters}
js: true, {bindings}
dontDecode: true, {schemaFields}
type: BindingType.RUNTIME, datasource={{ type: "table", tableId }}
} panel={AutomationBindingPanel}
)} on:change={e => (tempFilters = e.detail)}
mode="javascript" />
</DrawerContent>
</Drawer>
{:else if value.customType === "cron"}
<CronBuilder
on:change={e => onChange({ [key]: e.detail })}
value={inputData[key]}
/>
{:else if value.customType === "automationFields"}
<AutomationSelector
on:change={e => onChange({ [key]: e.detail })}
value={inputData[key]}
{bindings}
/>
{:else if value.customType === "queryParams"}
<QueryParamSelector
on:change={e => onChange({ [key]: e.detail })}
value={inputData[key]}
{bindings}
/>
{:else if value.customType === "table"}
<TableSelector
{isTrigger}
value={inputData[key]}
on:change={e => onChange({ [key]: e.detail })}
/>
{:else if value.customType === "webhookUrl"}
<WebhookDisplay value={inputData[key]} />
{:else if value.customType === "fields"}
<FieldSelector
{block}
value={inputData[key]}
on:change={e => onChange({ [key]: e.detail })}
{bindings}
{isTestModal}
/>
{:else if value.customType === "triggerSchema"}
<SchemaSetup
on:change={e => onChange({ [key]: e.detail })}
value={inputData[key]}
/>
{:else if value.customType === "code"}
<CodeEditorModal>
<div class:js-editor={editingJs}>
<div
class:js-code={editingJs}
style="width:100%;height:500px;"
>
<CodeEditor
value={inputData[key]}
on:change={e => {
// need to pass without the value inside
onChange({ [key]: e.detail })
inputData[key] = e.detail
}}
completions={stepCompletions}
mode={codeMode}
autocompleteEnabled={codeMode !== EditorModes.JS}
bind:getCaretPosition
bind:insertAtPos
placeholder={codeMode === EditorModes.Handlebars
? "Add bindings by typing {{"
: null}
/> />
</div> </div>
{/if} {#if editingJs}
</div> <div class="js-binding-picker">
</CodeEditorModal> <BindingSidePanel
{:else if value.customType === "loopOption"} {bindings}
<Select allowHelpers={false}
on:change={e => onChange(e, key)} addBinding={binding =>
autoWidth bindingsHelpers.onSelectBinding(
value={inputData[key]} inputData[key],
options={["Array", "String"]} binding,
defaultValue={"Array"} {
/> js: true,
{:else if value.type === "string" || value.type === "number" || value.type === "integer"} dontDecode: true,
{#if isTestModal} type: BindingType.RUNTIME,
<ModalBindableInput }
title={value.title || label} )}
mode="javascript"
/>
</div>
{/if}
</div>
</CodeEditorModal>
{:else if value.customType === "loopOption"}
<Select
on:change={e => onChange({ [key]: e.detail })}
autoWidth
value={inputData[key]} value={inputData[key]}
panel={AutomationBindingPanel} options={["Array", "String"]}
type={value.customType} defaultValue={"Array"}
on:change={e => onChange(e, key)}
{bindings}
updateOnChange={false}
/> />
{:else} {:else if value.type === "string" || value.type === "number" || value.type === "integer"}
<div class="test"> {#if isTestModal}
<DrawerBindableInput <ModalBindableInput
title={value.title ?? label} title={value.title || label}
value={inputData[key]}
panel={AutomationBindingPanel} panel={AutomationBindingPanel}
type={value.customType} type={value.customType}
value={inputData[key]} on:change={e => onChange({ [key]: e.detail })}
on:change={e => onChange(e, key)}
{bindings} {bindings}
updateOnChange={false} updateOnChange={false}
placeholder={value.customType === "queryLimit"
? queryLimit
: ""}
drawerLeft="260px"
/> />
</div> {:else}
<div>
<DrawerBindableInput
title={value.title ?? label}
panel={AutomationBindingPanel}
type={value.customType}
value={inputData[key]}
on:change={e => onChange({ [key]: e.detail })}
{bindings}
updateOnChange={false}
placeholder={value.customType === "queryLimit"
? queryLimit
: ""}
drawerLeft="260px"
/>
</div>
{/if}
{/if} {/if}
{/if} </div>
</div> </div>
</div> {/if}
{/if} {/each}
{/each} {/if}
</div> </div>
<Modal bind:this={webhookModal} width="30%"> <Modal bind:this={webhookModal} width="30%">
<CreateWebhookModal /> <CreateWebhookModal />
</Modal> </Modal>
{#if stepId === TriggerStepID.WEBHOOK} {#if stepId === TriggerStepID.WEBHOOK && !isTestModal}
<Button secondary on:click={() => webhookModal.show()}>Set Up Webhook</Button> <Button secondary on:click={() => webhookModal.show()}>Set Up Webhook</Button>
{/if} {/if}
@ -689,12 +817,12 @@
width: 320px; width: 320px;
} }
.fields { .step-fields {
display: flex; display: flex;
flex-direction: column; flex-direction: column;
justify-content: flex-start; justify-content: flex-start;
align-items: stretch; align-items: stretch;
gap: var(--spacing-s); gap: var(--spacing-l);
} }
.block-field { .block-field {
@ -714,10 +842,6 @@
margin-top: var(--spacing-s); margin-top: var(--spacing-s);
} }
.test :global(.drawer) {
width: 10000px !important;
}
.js-editor { .js-editor {
display: flex; display: flex;
flex-direction: row; flex-direction: row;

View File

@ -1,6 +1,7 @@
<script> <script>
import { createEventDispatcher } from "svelte" import { createEventDispatcher } from "svelte"
import RowSelectorTypes from "./RowSelectorTypes.svelte" import RowSelectorTypes from "./RowSelectorTypes.svelte"
import PropField from "./PropField.svelte"
const dispatch = createEventDispatcher() const dispatch = createEventDispatcher()
@ -88,27 +89,26 @@
</script> </script>
{#if schemaFields.length && isTestModal} {#if schemaFields.length && isTestModal}
<div class="schema-fields"> <div class="fields">
{#each schemaFields as [field, schema]} {#each schemaFields as [field, schema]}
<RowSelectorTypes <PropField label={field}>
{isTestModal} <RowSelectorTypes
{field} {isTestModal}
{schema} {field}
{bindings} {schema}
{value} {bindings}
{onChange} {value}
/> {onChange}
/>
</PropField>
{/each} {/each}
</div> </div>
{/if} {/if}
<style> <style>
.schema-fields { .fields {
display: grid; display: flex;
grid-gap: var(--spacing-s); flex-direction: column;
margin-top: var(--spacing-s); gap: var(--spacing-m);
}
.schema-fields :global(label) {
text-transform: capitalize;
} }
</style> </style>

View File

@ -0,0 +1,31 @@
<script>
import { Label } from "@budibase/bbui"
export let label
export let fullWidth = false
</script>
<div class="prop-field" class:fullWidth>
<div class="prop-label">
<Label>{label}</Label>
</div>
<div class="prop-control">
<slot />
</div>
</div>
<style>
.prop-field {
display: grid;
grid-template-columns: 1fr 320px;
}
.prop-field.fullWidth {
grid-template-columns: 1fr;
}
.prop-label {
display: flex;
align-items: center;
}
</style>

View File

@ -1,25 +1,34 @@
<script> <script>
import { tables } from "stores/builder" import { tables } from "stores/builder"
import { Select, Checkbox, Label } from "@budibase/bbui" import {
Label,
ActionButton,
Popover,
Icon,
TooltipPosition,
TooltipType,
} from "@budibase/bbui"
import { createEventDispatcher } from "svelte" import { createEventDispatcher } from "svelte"
import { FieldType } from "@budibase/types" import { FieldType } from "@budibase/types"
import RowSelectorTypes from "./RowSelectorTypes.svelte" import RowSelectorTypes from "./RowSelectorTypes.svelte"
import DrawerBindableSlot from "../../common/bindings/DrawerBindableSlot.svelte" import DrawerBindableSlot from "../../common/bindings/DrawerBindableSlot.svelte"
import AutomationBindingPanel from "../../common/bindings/ServerBindingPanel.svelte" import AutomationBindingPanel from "../../common/bindings/ServerBindingPanel.svelte"
import { TableNames } from "constants" import { FIELDS } from "constants/backend"
import { capitalise } from "helpers"
import { memo } from "@budibase/frontend-core"
import PropField from "./PropField.svelte"
const dispatch = createEventDispatcher() const dispatch = createEventDispatcher()
export let value export let row
export let meta export let meta
export let bindings export let bindings
export let isTestModal export let isTestModal
export let isUpdateRow
$: parsedBindings = bindings.map(binding => { const memoStore = memo({
let clone = Object.assign({}, binding) row,
clone.icon = "ShareAndroid" meta,
return clone
}) })
let table let table
@ -30,32 +39,113 @@
FieldType.SIGNATURE_SINGLE, FieldType.SIGNATURE_SINGLE,
] ]
$: { let customPopover
table = $tables.list.find(table => table._id === value?.tableId) let popoverAnchor
let editableRow = {}
let columns = new Set()
// Just sorting attachment types to the bottom here for a cleaner UX // Avoid unnecessary updates
schemaFields = Object.entries(table?.schema ?? {}).sort( $: memoStore.set({
([, schemaA], [, schemaB]) => row,
(schemaA.type === "attachment") - (schemaB.type === "attachment") meta,
) })
schemaFields.forEach(([, schema]) => { // Legacy support
if (!schema.autocolumn && !value[schema.name]) { $: fields = $memoStore?.meta?.fields
value[schema.name] = ""
$: if ($memoStore?.meta?.columns) {
columns = new Set(meta?.columns)
}
$: parsedBindings = bindings.map(binding => {
let clone = Object.assign({}, binding)
clone.icon = "ShareAndroid"
return clone
})
$: tableId = $memoStore?.row?.tableId
$: if (tableId) {
table = $tables.list.find(table => table._id === tableId)
if (table) {
editableRow["tableId"] = tableId
schemaFields = Object.entries(table?.schema ?? {})
.filter(entry => {
const [key, field] = entry
return field.type !== "formula" && !field.autocolumn
})
.sort(
([, schemaA], [, schemaB]) =>
(schemaA.type === "attachment") - (schemaB.type === "attachment")
)
// Parse out any unused data.
if ($memoStore?.meta?.columns) {
for (const column of meta?.columns) {
if (!(column in table?.schema)) {
columns.delete(column)
}
}
columns = new Set(columns)
} }
})
}
const onChangeTable = e => {
value["tableId"] = e.detail
dispatch("change", value)
}
const coerce = (value, type) => {
const re = new RegExp(/{{([^{].*?)}}/g)
if (re.test(value)) {
return value
} }
if (columns.size) {
for (const key of columns) {
const entry = schemaFields.find(entry => {
const [fieldKey] = entry
return fieldKey == key
})
if (entry) {
const [_, fieldSchema] = entry
editableRow = {
...editableRow,
[key]: coerce(
!(key in $memoStore?.row) ? "" : $memoStore?.row[key],
fieldSchema.type
),
}
}
}
} else {
schemaFields.forEach(entry => {
const [key] = entry
if ($memoStore?.row?.[key] && !editableRow?.[key]) {
editableRow = {
...editableRow,
[key]: $memoStore?.row[key],
}
columns.add(key)
}
})
columns = new Set(columns)
}
}
// Legacy - add explicitly cleared relationships to the request.
$: if (schemaFields?.length && fields) {
// Meta fields processing.
Object.keys(fields).forEach(key => {
if (fields[key]?.clearRelationships) {
columns.add(key)
}
})
columns = new Set(columns)
}
$: typeToField = Object.values(FIELDS).reduce((acc, field) => {
acc[field.type] = field
return acc
}, {})
// Row coerce
const coerce = (value, type) => {
const re = new RegExp(/{{([^{].*?)}}/g)
if (typeof value === "string" && re.test(value)) {
return value
}
if (type === "number") { if (type === "number") {
if (typeof value === "number") { if (typeof value === "number") {
return value return value
@ -66,6 +156,9 @@
return value return value
} }
if (type === "array") { if (type === "array") {
if (!value) {
return []
}
if (Array.isArray(value)) { if (Array.isArray(value)) {
return value return value
} }
@ -73,7 +166,9 @@
} }
if (type === "link") { if (type === "link") {
if (Array.isArray(value)) { if (!value) {
return []
} else if (Array.isArray(value)) {
return value return value
} }
return value.split(",").map(x => x.trim()) return value.split(",").map(x => x.trim())
@ -86,65 +181,52 @@
return value return value
} }
const onChange = (e, field, type) => { const onChange = u => {
let newValue = { const update = {
...value, _tableId: tableId,
[field]: coerce(e.detail, type), row: { ...$memoStore.row },
meta: { ...$memoStore.meta },
...u,
} }
dispatch("change", newValue) dispatch("change", update)
} }
const onChangeSetting = (e, field) => { const fieldUpdate = (e, field) => {
let fields = {} const update = {
fields[field] = { row: {
clearRelationships: e.detail, ...$memoStore?.row,
[field]: e.detail,
},
} }
dispatch("change", { onChange(update)
key: "meta",
fields,
})
} }
// Ensure any nullish tableId values get set to empty string so
// that the select works
$: if (value?.tableId == null) value = { tableId: "" }
</script> </script>
<div class="schema-fields"> {#if columns.size}
<Label>Table</Label>
<div class="field-width">
<Select
on:change={onChangeTable}
value={value.tableId}
options={$tables.list.filter(table => table._id !== TableNames.USERS)}
getOptionLabel={table => table.name}
getOptionValue={table => table._id}
/>
</div>
</div>
{#if schemaFields.length}
{#each schemaFields as [field, schema]} {#each schemaFields as [field, schema]}
{#if !schema.autocolumn} {#if !schema.autocolumn && columns.has(field)}
<div class:schema-fields={!attachmentTypes.includes(schema.type)}> <PropField
<Label>{field}</Label> label={field}
<div class:field-width={!attachmentTypes.includes(schema.type)}> fullWidth={attachmentTypes.includes(schema.type)}
>
<div class="prop-control-wrap">
{#if isTestModal} {#if isTestModal}
<RowSelectorTypes <RowSelectorTypes
{isTestModal} {isTestModal}
{field} {field}
{schema} {schema}
bindings={parsedBindings} bindings={parsedBindings}
{value} value={$memoStore?.row}
{onChange} onChange={fieldUpdate}
/> />
{:else} {:else}
<DrawerBindableSlot <DrawerBindableSlot
title={value.title || field} title={$memoStore?.row?.title || field}
panel={AutomationBindingPanel} panel={AutomationBindingPanel}
type={schema.type} type={schema.type}
{schema} {schema}
value={value[field]} value={editableRow[field]}
on:change={e => onChange(e, field)} on:change={e => fieldUpdate(e, field)}
{bindings} {bindings}
allowJS={true} allowJS={true}
updateOnChange={false} updateOnChange={false}
@ -155,52 +237,106 @@
{field} {field}
{schema} {schema}
bindings={parsedBindings} bindings={parsedBindings}
{value} value={editableRow}
{onChange} onChange={fieldUpdate}
/> />
</DrawerBindableSlot> </DrawerBindableSlot>
{/if} {/if}
<Icon
{#if isUpdateRow && schema.type === "link"} hoverable
<div class="checkbox-field"> name="Close"
<Checkbox on:click={() => {
value={meta.fields?.[field]?.clearRelationships} columns.delete(field)
text={"Clear relationships if empty?"} const update = { ...editableRow }
size={"S"} delete update[field]
on:change={e => onChangeSetting(e, field)} onChange({ row: update, meta: { columns: Array.from(columns) } })
/> }}
</div> />
{/if}
</div> </div>
</div> </PropField>
{/if} {/if}
{/each} {/each}
{/if} {/if}
{#if table && schemaFields}
<div
class="add-fields-btn"
class:empty={!columns?.size}
bind:this={popoverAnchor}
>
<ActionButton
icon="Add"
fullWidth
on:click={() => {
customPopover.show()
}}
disabled={!schemaFields}
>Add fields
</ActionButton>
</div>
{/if}
<Popover
align="center"
bind:this={customPopover}
anchor={popoverAnchor}
minWidth={popoverAnchor?.getBoundingClientRect()?.width}
maxHeight={300}
resizable={false}
offset={10}
>
<!-- svelte-ignore a11y-click-events-have-key-events -->
<!-- svelte-ignore a11y-no-noninteractive-element-interactions -->
<ul class="spectrum-Menu" role="listbox">
{#each schemaFields || [] as [field, schema]}
{#if !schema.autocolumn}
<li
class="table_field spectrum-Menu-item"
class:is-selected={columns.has(field)}
on:click={e => {
if (columns.has(field)) {
columns.delete(field)
} else {
columns.add(field)
}
onChange({ meta: { columns: Array.from(columns) } })
}}
>
<Icon
name={typeToField?.[schema.type]?.icon}
color={"var(--spectrum-global-color-gray-600)"}
tooltip={capitalise(schema.type)}
tooltipType={TooltipType.Info}
tooltipPosition={TooltipPosition.Left}
/>
<div class="field_name spectrum-Menu-itemLabel">{field}</div>
<svg
class="spectrum-Icon spectrum-UIIcon-Checkmark100 spectrum-Menu-checkmark spectrum-Menu-itemIcon"
focusable="false"
aria-hidden="true"
>
<use xlink:href="#spectrum-css-icon-Checkmark100" />
</svg>
</li>
{/if}
{/each}
</ul>
</Popover>
<style> <style>
.field-width { .table_field {
width: 320px; display: flex;
padding: var(--spacing-s) var(--spacing-l);
gap: var(--spacing-s);
} }
.schema-fields { li.is-selected .spectrum-Menu-itemLabel {
display: flex; color: var(--spectrum-global-color-gray-500);
justify-content: space-between;
align-items: center;
flex-direction: row;
align-items: center;
gap: 10px;
flex: 1;
margin-bottom: 10px;
} }
.schema-fields :global(label) {
text-transform: capitalize; .prop-control-wrap {
} display: grid;
.checkbox-field { grid-template-columns: 1fr min-content;
padding-bottom: var(--spacing-s); gap: var(--spacing-s);
padding-left: 1px;
padding-top: var(--spacing-s);
}
.checkbox-field :global(label) {
text-transform: none;
} }
</style> </style>

View File

@ -73,12 +73,12 @@
/> />
{:else if schemaHasOptions(schema) && schema.type === "array"} {:else if schemaHasOptions(schema) && schema.type === "array"}
<Multiselect <Multiselect
bind:value={value[field]} value={value[field]}
options={schema.constraints.inclusion} options={schema.constraints.inclusion}
on:change={e => onChange(e, field)} on:change={e => onChange(e, field)}
/> />
{:else if schema.type === "longform"} {:else if schema.type === "longform"}
<TextArea bind:value={value[field]} on:change={e => onChange(e, field)} /> <TextArea value={value[field]} on:change={e => onChange(e, field)} />
{:else if schema.type === "json"} {:else if schema.type === "json"}
<span> <span>
<Editor <Editor
@ -108,7 +108,7 @@
useLabel={false} useLabel={false}
/> />
{:else if attachmentTypes.includes(schema.type)} {:else if attachmentTypes.includes(schema.type)}
<div class="attachment-field-spacinng"> <div class="attachment-field-spacing">
<KeyValueBuilder <KeyValueBuilder
on:change={e => on:change={e =>
onChange( onChange(
@ -129,16 +129,18 @@
}, },
field field
)} )}
object={handleAttachmentParams(value[field])} object={handleAttachmentParams(value[field] || {})}
allowJS allowJS
{bindings} {bindings}
keyBindings keyBindings
customButtonText={"Add attachment"} customButtonText={schema.type === FieldType.SIGNATURE_SINGLE
? "Add signature"
: "Add attachment"}
keyPlaceholder={"URL"} keyPlaceholder={"URL"}
valuePlaceholder={"Filename"} valuePlaceholder={"Filename"}
actionButtonDisabled={(schema.type === FieldType.ATTACHMENT_SINGLE || actionButtonDisabled={(schema.type === FieldType.ATTACHMENT_SINGLE ||
schema.type === FieldType.SIGNATURE) && schema.type === FieldType.SIGNATURE_SINGLE) &&
Object.keys(value[field]).length >= 1} Object.keys(value[field] || {}).length >= 1}
/> />
</div> </div>
{:else if ["string", "number", "bigint", "barcodeqr", "array"].includes(schema.type)} {:else if ["string", "number", "bigint", "barcodeqr", "array"].includes(schema.type)}
@ -152,12 +154,15 @@
allowJS={true} allowJS={true}
updateOnChange={false} updateOnChange={false}
title={schema.name} title={schema.name}
autocomplete="off"
/> />
{/if} {/if}
<style> <style>
.attachment-field-spacinng { .attachment-field-spacing {
margin-top: var(--spacing-s); margin-top: var(--spacing-s);
margin-bottom: var(--spacing-l); border: 1px solid var(--spectrum-global-color-gray-400);
border-radius: 4px;
padding: var(--spacing-s);
} }
</style> </style>

View File

@ -8,6 +8,7 @@
export let value export let value
export let isTrigger export let isTrigger
export let disabled = false
$: filteredTables = $tables.list.filter(table => { $: filteredTables = $tables.list.filter(table => {
return !isTrigger || table._id !== TableNames.USERS return !isTrigger || table._id !== TableNames.USERS
@ -25,4 +26,5 @@
options={filteredTables} options={filteredTables}
getOptionLabel={table => table.name} getOptionLabel={table => table.name}
getOptionValue={table => table._id} getOptionValue={table => table._id}
{disabled}
/> />

View File

@ -23,6 +23,7 @@
export let disableBindings = false export let disableBindings = false
export let forceModal = false export let forceModal = false
export let context = null export let context = null
export let autocomplete
const dispatch = createEventDispatcher() const dispatch = createEventDispatcher()
@ -71,6 +72,7 @@
on:blur={onBlur} on:blur={onBlur}
{placeholder} {placeholder}
{updateOnChange} {updateOnChange}
{autocomplete}
/> />
{#if !disabled && !disableBindings} {#if !disabled && !disableBindings}
<div <div

View File

@ -0,0 +1,12 @@
export { default as BindableCombobox } from "./BindableCombobox.svelte"
export { default as BindingPanel } from "./BindingPanel.svelte"
export { default as BindingSidePanel } from "./BindingSidePanel.svelte"
export { default as DrawerBindableCombobox } from "./DrawerBindableCombobox.svelte"
export { default as ClientBindingPanel } from "./ClientBindingPanel.svelte"
export { default as DrawerBindableInput } from "./DrawerBindableInput.svelte"
export { default as DrawerBindableSlot } from "./DrawerBindableSlot.svelte"
export { default as EvaluationSidePanel } from "./EvaluationSidePanel.svelte"
export { default as ModalBindableInput } from "./ModalBindableInput.svelte"
export { default as ServerBindingPanel } from "./ServerBindingPanel.svelte"
export { default as SnippetDrawer } from "./SnippetDrawer.svelte"
export { default as SnippetSidePanel } from "./SnippetSidePanel.svelte"

View File

@ -11,7 +11,7 @@
notifications, notifications,
} from "@budibase/bbui" } from "@budibase/bbui"
import { AUTH_TYPE_LABELS, AUTH_TYPES } from "./authTypes" import { AUTH_TYPE_LABELS, AUTH_TYPES } from "./authTypes"
import BindableCombobox from "components/common/bindings/BindableCombobox.svelte" import { BindableCombobox } from "components/common/bindings"
import { getAuthBindings, getEnvironmentBindings } from "dataBinding" import { getAuthBindings, getEnvironmentBindings } from "dataBinding"
import { environment, licensing, auth } from "stores/portal" import { environment, licensing, auth } from "stores/portal"
import CreateEditVariableModal from "components/portal/environment/CreateEditVariableModal.svelte" import CreateEditVariableModal from "components/portal/environment/CreateEditVariableModal.svelte"

View File

@ -120,7 +120,9 @@ export async function sendAutomationAttachmentsToStorage(
} }
} }
for (const [prop, attachments] of Object.entries(attachmentRows)) { for (const [prop, attachments] of Object.entries(attachmentRows)) {
if (Array.isArray(attachments)) { if (!attachments) {
continue
} else if (Array.isArray(attachments)) {
if (attachments.length) { if (attachments.length) {
row[prop] = await Promise.all( row[prop] = await Promise.all(
attachments.map(attachment => generateAttachmentRow(attachment)) attachments.map(attachment => generateAttachmentRow(attachment))

View File

@ -82,39 +82,58 @@ export async function run({ inputs, appId, emitter }: AutomationStepInput) {
} }
const tableId = inputs.row.tableId const tableId = inputs.row.tableId
// clear any undefined, null or empty string properties so that they aren't updated // Base update
for (let propKey of Object.keys(inputs.row)) { let rowUpdate: Record<string, any> = {
const clearRelationships = tableId,
inputs.meta?.fields?.[propKey]?.clearRelationships }
if (
(inputs.row[propKey] == null || inputs.row[propKey]?.length === 0) && // Column checking - explicit clearing of empty fields
!clearRelationships if (inputs?.meta?.columns) {
) { rowUpdate = inputs?.meta?.columns.reduce(
delete inputs.row[propKey] (acc: Record<string, any>, key: string) => {
acc[key] =
!inputs.row[key] || inputs.row[key]?.length === 0
? null
: inputs.row[key]
return acc
},
{}
)
} else {
// Legacy - clear any empty string column values so that they aren't updated
rowUpdate = {
...inputs.row,
}
for (let propKey of Object.keys(rowUpdate)) {
const clearRelationships =
inputs.meta?.fields?.[propKey]?.clearRelationships
if (
(rowUpdate[propKey] == null || rowUpdate[propKey]?.length === 0) &&
!clearRelationships
) {
delete rowUpdate[propKey]
}
} }
} }
try { try {
if (tableId) { if (tableId) {
inputs.row = await automationUtils.cleanUpRow( rowUpdate = await automationUtils.cleanUpRow(tableId, rowUpdate)
inputs.row.tableId,
inputs.row
)
inputs.row = await automationUtils.sendAutomationAttachmentsToStorage( rowUpdate = await automationUtils.sendAutomationAttachmentsToStorage(
inputs.row.tableId, tableId,
inputs.row rowUpdate
) )
} }
// have to clean up the row, remove the table from it // have to clean up the row, remove the table from it
const ctx: any = buildCtx(appId, emitter, { const ctx: any = buildCtx(appId, emitter, {
body: { body: {
...inputs.row, ...rowUpdate,
_id: inputs.rowId, _id: inputs.rowId,
}, },
params: { params: {
rowId: inputs.rowId, rowId: inputs.rowId,
tableId: tableId, tableId,
}, },
}) })
await rowController.patch(ctx) await rowController.patch(ctx)

View File

@ -4,11 +4,12 @@ import {
AutomationStepType, AutomationStepType,
AutomationTriggerSchema, AutomationTriggerSchema,
AutomationTriggerStepId, AutomationTriggerStepId,
AutomationEventType,
} from "@budibase/types" } from "@budibase/types"
export const definition: AutomationTriggerSchema = { export const definition: AutomationTriggerSchema = {
name: "App Action", name: "App Action",
event: "app:trigger", event: AutomationEventType.APP_TRIGGER,
icon: "Apps", icon: "Apps",
tagline: "Automation fired from the frontend", tagline: "Automation fired from the frontend",
description: "Trigger an automation from an action inside your app", description: "Trigger an automation from an action inside your app",

View File

@ -4,11 +4,12 @@ import {
AutomationStepType, AutomationStepType,
AutomationTriggerSchema, AutomationTriggerSchema,
AutomationTriggerStepId, AutomationTriggerStepId,
AutomationEventType,
} from "@budibase/types" } from "@budibase/types"
export const definition: AutomationTriggerSchema = { export const definition: AutomationTriggerSchema = {
name: "Cron Trigger", name: "Cron Trigger",
event: "cron:trigger", event: AutomationEventType.CRON_TRIGGER,
icon: "Clock", icon: "Clock",
tagline: "Cron Trigger (<b>{{inputs.cron}}</b>)", tagline: "Cron Trigger (<b>{{inputs.cron}}</b>)",
description: "Triggers automation on a cron schedule.", description: "Triggers automation on a cron schedule.",

View File

@ -4,11 +4,12 @@ import {
AutomationStepType, AutomationStepType,
AutomationTriggerSchema, AutomationTriggerSchema,
AutomationTriggerStepId, AutomationTriggerStepId,
AutomationEventType,
} from "@budibase/types" } from "@budibase/types"
export const definition: AutomationTriggerSchema = { export const definition: AutomationTriggerSchema = {
name: "Row Deleted", name: "Row Deleted",
event: "row:delete", event: AutomationEventType.ROW_DELETE,
icon: "TableRowRemoveCenter", icon: "TableRowRemoveCenter",
tagline: "Row is deleted from {{inputs.enriched.table.name}}", tagline: "Row is deleted from {{inputs.enriched.table.name}}",
description: "Fired when a row is deleted from your database", description: "Fired when a row is deleted from your database",

View File

@ -4,11 +4,12 @@ import {
AutomationStepType, AutomationStepType,
AutomationTriggerSchema, AutomationTriggerSchema,
AutomationTriggerStepId, AutomationTriggerStepId,
AutomationEventType,
} from "@budibase/types" } from "@budibase/types"
export const definition: AutomationTriggerSchema = { export const definition: AutomationTriggerSchema = {
name: "Row Created", name: "Row Created",
event: "row:save", event: AutomationEventType.ROW_SAVE,
icon: "TableRowAddBottom", icon: "TableRowAddBottom",
tagline: "Row is added to {{inputs.enriched.table.name}}", tagline: "Row is added to {{inputs.enriched.table.name}}",
description: "Fired when a row is added to your database", description: "Fired when a row is added to your database",

View File

@ -4,11 +4,12 @@ import {
AutomationStepType, AutomationStepType,
AutomationTriggerSchema, AutomationTriggerSchema,
AutomationTriggerStepId, AutomationTriggerStepId,
AutomationEventType,
} from "@budibase/types" } from "@budibase/types"
export const definition: AutomationTriggerSchema = { export const definition: AutomationTriggerSchema = {
name: "Row Updated", name: "Row Updated",
event: "row:update", event: AutomationEventType.ROW_UPDATE,
icon: "Refresh", icon: "Refresh",
tagline: "Row is updated in {{inputs.enriched.table.name}}", tagline: "Row is updated in {{inputs.enriched.table.name}}",
description: "Fired when a row is updated in your database", description: "Fired when a row is updated in your database",

View File

@ -4,11 +4,12 @@ import {
AutomationStepType, AutomationStepType,
AutomationTriggerSchema, AutomationTriggerSchema,
AutomationTriggerStepId, AutomationTriggerStepId,
AutomationEventType,
} from "@budibase/types" } from "@budibase/types"
export const definition: AutomationTriggerSchema = { export const definition: AutomationTriggerSchema = {
name: "Webhook", name: "Webhook",
event: "web:trigger", event: AutomationEventType.WEBHOOK_TRIGGER,
icon: "Send", icon: "Send",
tagline: "Webhook endpoint is hit", tagline: "Webhook endpoint is hit",
description: "Trigger an automation when a HTTP POST webhook is hit", description: "Trigger an automation when a HTTP POST webhook is hit",

View File

@ -8,7 +8,13 @@ import { checkTestFlag } from "../utilities/redis"
import * as utils from "./utils" import * as utils from "./utils"
import env from "../environment" import env from "../environment"
import { context, db as dbCore } from "@budibase/backend-core" import { context, db as dbCore } from "@budibase/backend-core"
import { Automation, Row, AutomationData, AutomationJob } from "@budibase/types" import {
Automation,
Row,
AutomationData,
AutomationJob,
AutomationEventType,
} from "@budibase/types"
import { executeInThread } from "../threads/automation" import { executeInThread } from "../threads/automation"
export const TRIGGER_DEFINITIONS = definitions export const TRIGGER_DEFINITIONS = definitions
@ -65,28 +71,28 @@ async function queueRelevantRowAutomations(
}) })
} }
emitter.on("row:save", async function (event) { emitter.on(AutomationEventType.ROW_SAVE, async function (event) {
/* istanbul ignore next */ /* istanbul ignore next */
if (!event || !event.row || !event.row.tableId) { if (!event || !event.row || !event.row.tableId) {
return return
} }
await queueRelevantRowAutomations(event, "row:save") await queueRelevantRowAutomations(event, AutomationEventType.ROW_SAVE)
}) })
emitter.on("row:update", async function (event) { emitter.on(AutomationEventType.ROW_UPDATE, async function (event) {
/* istanbul ignore next */ /* istanbul ignore next */
if (!event || !event.row || !event.row.tableId) { if (!event || !event.row || !event.row.tableId) {
return return
} }
await queueRelevantRowAutomations(event, "row:update") await queueRelevantRowAutomations(event, AutomationEventType.ROW_UPDATE)
}) })
emitter.on("row:delete", async function (event) { emitter.on(AutomationEventType.ROW_DELETE, async function (event) {
/* istanbul ignore next */ /* istanbul ignore next */
if (!event || !event.row || !event.row.tableId) { if (!event || !event.row || !event.row.tableId) {
return return
} }
await queueRelevantRowAutomations(event, "row:delete") await queueRelevantRowAutomations(event, AutomationEventType.ROW_DELETE)
}) })
export async function externalTrigger( export async function externalTrigger(
@ -112,7 +118,6 @@ export async function externalTrigger(
} }
params.fields = coercedFields params.fields = coercedFields
} }
const data: AutomationData = { automation, event: params as any } const data: AutomationData = { automation, event: params as any }
if (getResponses) { if (getResponses) {
data.event = { data.event = {

View File

@ -24,6 +24,7 @@ import {
Query, Query,
Webhook, Webhook,
WebhookActionType, WebhookActionType,
AutomationEventType,
} from "@budibase/types" } from "@budibase/types"
import { LoopInput, LoopStepType } from "../../definitions/automations" import { LoopInput, LoopStepType } from "../../definitions/automations"
import { merge } from "lodash" import { merge } from "lodash"
@ -305,7 +306,7 @@ export function loopAutomation(
trigger: { trigger: {
id: "a", id: "a",
type: "TRIGGER", type: "TRIGGER",
event: "row:save", event: AutomationEventType.ROW_SAVE,
stepId: AutomationTriggerStepId.ROW_SAVED, stepId: AutomationTriggerStepId.ROW_SAVED,
inputs: { inputs: {
tableId, tableId,
@ -347,7 +348,7 @@ export function collectAutomation(tableId?: string): Automation {
trigger: { trigger: {
id: "a", id: "a",
type: "TRIGGER", type: "TRIGGER",
event: "row:save", event: AutomationEventType.ROW_SAVE,
stepId: AutomationTriggerStepId.ROW_SAVED, stepId: AutomationTriggerStepId.ROW_SAVED,
inputs: { inputs: {
tableId, tableId,

View File

@ -252,3 +252,12 @@ export type BucketedContent = AutomationAttachmentContent & {
bucket: string bucket: string
path: string path: string
} }
export enum AutomationEventType {
ROW_SAVE = "row:save",
ROW_UPDATE = "row:update",
ROW_DELETE = "row:delete",
APP_TRIGGER = "app:trigger",
CRON_TRIGGER = "cron:trigger",
WEBHOOK_TRIGGER = "web:trigger",
}