Updates and refactoring of Automation flows

This commit is contained in:
Dean 2024-06-25 11:52:11 +01:00
parent 27917c78d1
commit e9985adcd4
9 changed files with 380 additions and 239 deletions

View File

@ -14,36 +14,10 @@
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) const memoTestData = memo($selectedAutomation.testData)
$: memoTestData.set($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
trigger = cloneDeep($selectedAutomation.definition.trigger) trigger = cloneDeep($selectedAutomation.definition.trigger)
@ -59,7 +33,7 @@
// 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 => baseData?.[required] || required !== "row" required => $memoTestData?.[required] || required !== "row"
) )
function parseTestJSON(e) { function parseTestJSON(e) {
@ -74,7 +48,7 @@
const testAutomation = async () => { const testAutomation = async () => {
try { try {
await automationStore.actions.test($selectedAutomation, baseData) await automationStore.actions.test($selectedAutomation, $memoTestData)
$automationStore.showTestPanel = true $automationStore.showTestPanel = true
} catch (error) { } catch (error) {
notifications.error(error) notifications.error(error)
@ -112,7 +86,7 @@
{#if selectedValues} {#if selectedValues}
<div class="tab-content-padding"> <div class="tab-content-padding">
<AutomationBlockSetup <AutomationBlockSetup
testData={baseData} testData={$memoTestData}
{schemaProperties} {schemaProperties}
isTestModal isTestModal
block={trigger} block={trigger}

View File

@ -52,7 +52,11 @@
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 {
AutomationEventType,
AutomationStepType,
AutomationActionStepId,
} from "@budibase/types"
import { FIELDS } from "constants/backend" import { FIELDS } from "constants/backend"
import PropField from "./PropField.svelte" import PropField from "./PropField.svelte"
@ -69,6 +73,13 @@
TriggerStepID.ROW_SAVED, TriggerStepID.ROW_SAVED,
TriggerStepID.ROW_DELETED, TriggerStepID.ROW_DELETED,
] ]
let rowEvents = [
AutomationEventType.ROW_DELETE,
AutomationEventType.ROW_SAVE,
AutomationEventType.ROW_UPDATE,
]
const rowSteps = [ActionStepID.UPDATE_ROW, ActionStepID.CREATE_ROW] const rowSteps = [ActionStepID.UPDATE_ROW, ActionStepID.CREATE_ROW]
let webhookModal let webhookModal
@ -92,9 +103,11 @@
}).schema }).schema
$: 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 === AutomationStepType.TRIGGER
$: codeMode = $: codeMode =
stepId === "EXECUTE_BASH" ? EditorModes.Handlebars : EditorModes.JS stepId === AutomationActionStepId.EXECUTE_BASH
? EditorModes.Handlebars
: EditorModes.JS
$: bindingsHelpers = new BindingHelpers(getCaretPosition, insertAtPos, { $: bindingsHelpers = new BindingHelpers(getCaretPosition, insertAtPos, {
disableWrapping: true, disableWrapping: true,
}) })
@ -133,7 +146,6 @@
$: customStepLayouts($memoBlock, schemaProperties) $: customStepLayouts($memoBlock, schemaProperties)
const customStepLayouts = block => { const customStepLayouts = block => {
console.log("BUILDING", inputData["row"])
if ( if (
rowSteps.includes(block.stepId) || rowSteps.includes(block.stepId) ||
(rowTriggers.includes(block.stepId) && isTestModal) (rowTriggers.includes(block.stepId) && isTestModal)
@ -157,14 +169,16 @@
{ {
type: DrawerBindableInput, type: DrawerBindableInput,
title: rowRevlabel, title: rowRevlabel,
panel: AutomationBindingPanel, props: {
value: inputData["revision"], panel: AutomationBindingPanel,
onChange: e => { value: inputData["revision"],
onChange({ ["revision"]: e.detail }) onChange: e => {
onChange({ ["revision"]: e.detail })
},
bindings,
updateOnChange: false,
forceModal: true,
}, },
bindings,
updateOnChange: false,
forceModal: true,
}, },
] ]
: [] : []
@ -248,12 +262,73 @@
} }
} }
/**
* Handler for row trigger automation updates.
@param {object} update - An automation block.inputs update object
@example
onRowTriggerUpdate({
"tableId" : "ta_bb_employee"
})
*/
const onRowTriggerUpdate = async update => {
if (
update.hasOwnProperty("tableId") &&
$selectedAutomation.testData.row.tableId !== update.tableId
) {
try {
const reqSchema = getSchemaForDatasourcePlus(update.tableId, {
searchableSchema: true,
}).schema
// Parse the block inputs as usual
const updatedAutomation =
await automationStore.actions.processBlockInputs(block, {
schema: reqSchema,
...update,
})
// Save the entire automation and reset the testData
await automationStore.actions.save({
...updatedAutomation,
testData: {
// Reset Core fields
row: { tableId: update.tableId },
meta: {},
id: "",
revision: "",
},
})
return
} catch (e) {
console.error("Error saving automation", error)
notifications.error("Error saving automation")
}
}
}
/**
* Handler for automation block input updates.
@param {object} update - An automation inputs update object
@example
onChange({
meta: { fields : { "Photo": { useAttachmentBinding: false }} }
row: { "Active": true, "Order Id" : 14, ... }
})
*/
const onChange = Utils.sequential(async update => { const onChange = Utils.sequential(async update => {
const request = cloneDeep(update)
// Process row trigger updates
if (isTrigger && !isTestModal) {
await onRowTriggerUpdate(request)
return
}
// 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) // If _tableId is explicitly included in the update request, the schema will be requested
let schema let schema
if (request?._tableId) { if (request?._tableId) {
schema = getSchemaForDatasourcePlus(request._tableId, { schema = getSchemaForDatasourcePlus(request._tableId, {
@ -643,18 +718,16 @@
<div class="label-wrapper"> <div class="label-wrapper">
<Label>{label}</Label> <Label>{label}</Label>
</div> </div>
{JSON.stringify(inputData)}
<div class="toggle-container"> <div class="toggle-container">
<Toggle <Toggle
value={inputData?.meta?.useAttachmentBinding} value={inputData?.meta?.useAttachmentBinding}
text={"Use bindings"} text={"Use bindings"}
size={"XS"} size={"XS"}
on:change={e => { on:change={e => {
// DEAN - review this
onChange({ onChange({
row: { [key]: "" }, //null [key]: null,
meta: { meta: {
[key]: e.detail, useAttachmentBinding: e.detail,
}, },
}) })
}} }}
@ -662,24 +735,50 @@
</div> </div>
<div class="attachment-field-width"> <div class="attachment-field-width">
<KeyValueBuilder {#if !inputData?.meta?.useAttachmentBinding}
on:change={e => <KeyValueBuilder
onChange({ on:change={e =>
[key]: e.detail.map(({ name, value }) => ({ onChange({
url: name, [key]: e.detail.map(({ name, value }) => ({
filename: value, url: name,
})), filename: value,
})} })),
object={handleAttachmentParams(inputData[key])} })}
allowJS object={handleAttachmentParams(inputData[key])}
{bindings} allowJS
keyBindings {bindings}
customButtonText={value.type === "attachment" keyBindings
? "Add attachment" customButtonText={value.type === "attachment"
: "Add signature"} ? "Add attachment"
keyPlaceholder={"URL"} : "Add signature"}
valuePlaceholder={"Filename"} keyPlaceholder={"URL"}
/> valuePlaceholder={"Filename"}
/>
{:else if isTestModal}
<ModalBindableInput
title={value.title || label}
value={inputData[key]}
panel={AutomationBindingPanel}
type={value.customType}
on:change={e => onChange({ [key]: e.detail })}
{bindings}
updateOnChange={false}
/>
{:else}
<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"
/>
{/if}
</div> </div>
</div> </div>
{:else if value.customType === "filters"} {:else if value.customType === "filters"}

View File

@ -1,20 +1,38 @@
<script> <script>
import { createEventDispatcher } from "svelte" import { createEventDispatcher } from "svelte"
import RowSelectorTypes from "./RowSelectorTypes.svelte"
import PropField from "./PropField.svelte" import PropField from "./PropField.svelte"
import DrawerBindableInput from "../../common/bindings/DrawerBindableInput.svelte"
import ModalBindableInput from "../../common/bindings/ModalBindableInput.svelte"
import AutomationBindingPanel from "../../common/bindings/ServerBindingPanel.svelte"
import { DatePicker, Select } from "@budibase/bbui"
const dispatch = createEventDispatcher() const dispatch = createEventDispatcher()
export let value export let value = {}
export let bindings export let bindings
export let block export let block
export let isTestModal export let isTestModal
let schemaFields let schemaFields
let editableValue
$: processValue(value)
const processValue = value => {
editableValue = { ...value }
// DEAN - review this
// const fieldKeys = Object.keys(block?.inputs?.fields)
// // Purge orphaned keys
// Object.keys(editableValue || {}).forEach(key => {
// if (!fieldKeys.includes(key)) {
// delete editableValue[key]
// }
// })
}
$: { $: {
let fields = {} let fields = {}
// DEAN - review this
for (const [key, type] of Object.entries(block?.inputs?.fields ?? {})) { for (const [key, type] of Object.entries(block?.inputs?.fields ?? {})) {
fields = { fields = {
...fields, ...fields,
@ -26,8 +44,8 @@
}, },
} }
if (value[key] === type) { if (editableValue[key] === type) {
value[key] = INITIAL_VALUES[type.toUpperCase()] editableValue[key] = INITIAL_VALUES[type.toUpperCase()]
} }
} }
@ -39,52 +57,14 @@
NUMBER: null, NUMBER: null,
DATETIME: null, DATETIME: null,
STRING: "", STRING: "",
OPTIONS: [], ARRAY: "",
ARRAY: [],
}
const coerce = (value, type) => {
const re = new RegExp(/{{([^{].*?)}}/g)
if (re.test(value)) {
return value
}
if (type === "boolean") {
if (typeof value === "boolean") {
return value
}
return value === "true"
}
if (type === "number") {
if (typeof value === "number") {
return value
}
return Number(value)
}
if (type === "options") {
return [value]
}
if (type === "array") {
if (Array.isArray(value)) {
return value
}
return value.split(",").map(x => x.trim())
}
if (type === "link") {
if (Array.isArray(value)) {
return value
}
return [value]
}
return value
} }
const onChange = (e, field, type) => { const onChange = (e, field, type) => {
value[field] = coerce(e.detail, type) if (e.detail !== editableValue[field]) {
dispatch("change", value) editableValue[field] = e.detail
dispatch("change", editableValue)
}
} }
</script> </script>
@ -92,14 +72,34 @@
<div class="fields"> <div class="fields">
{#each schemaFields as [field, schema]} {#each schemaFields as [field, schema]}
<PropField label={field}> <PropField label={field}>
<RowSelectorTypes {#if ["string", "number", "array"].includes(schema.type)}
{isTestModal} <svelte:component
{field} this={isTestModal ? ModalBindableInput : DrawerBindableInput}
{schema} panel={AutomationBindingPanel}
{bindings} value={editableValue[field]}
{value} on:change={e => onChange(e, field)}
{onChange} type="string"
/> {bindings}
allowJS={true}
updateOnChange={false}
title={schema.name}
autocomplete="off"
/>
{:else if schema.type === "boolean"}
<Select
on:change={e => onChange(e, field)}
value={editableValue[field]}
options={[
{ label: "True", value: "true" },
{ label: "False", value: "false" },
]}
/>
{:else if schema.type === "datetime"}
<DatePicker
value={editableValue[field]}
on:change={e => onChange(e, field)}
/>
{/if}
</PropField> </PropField>
{/each} {/each}
</div> </div>

View File

@ -2,12 +2,18 @@
import { Label } from "@budibase/bbui" import { Label } from "@budibase/bbui"
export let label export let label
export let labelTooltip
export let fullWidth = false export let fullWidth = false
export let componentWidth = 320
</script> </script>
<div class="prop-field" class:fullWidth> <div
<div class="prop-label"> class="prop-field"
<Label>{label}</Label> class:fullWidth
style={`--comp-width: ${componentWidth}px;`}
>
<div class="prop-label" title={label}>
<Label tooltip={labelTooltip}>{label}</Label>
</div> </div>
<div class="prop-control"> <div class="prop-control">
<slot /> <slot />
@ -17,15 +23,30 @@
<style> <style>
.prop-field { .prop-field {
display: grid; display: grid;
grid-template-columns: 1fr 320px; grid-template-columns: 1fr var(--comp-width);
} }
.prop-field.fullWidth { .prop-field.fullWidth {
grid-template-columns: 1fr; grid-template-columns: 1fr;
} }
.prop-field.fullWidth .prop-label {
margin-bottom: var(--spacing-s);
}
.prop-label { .prop-label {
display: flex; display: flex;
align-items: center; align-items: center;
overflow: hidden;
}
.prop-label :global(> div) {
width: 100%;
}
.prop-label :global(> div > label) {
text-overflow: ellipsis;
white-space: nowrap;
overflow: hidden;
} }
</style> </style>

View File

@ -43,13 +43,9 @@
let customPopover let customPopover
let popoverAnchor let popoverAnchor
let editableRow = {} let editableRow = {}
//??
let editableMeta = {}
let editableFields = {} let editableFields = {}
// let columns = new Set()
// Avoid unnecessary updates - DEAN double check after refactor // Avoid unnecessary updates
$: memoStore.set({ $: memoStore.set({
row, row,
meta, meta,
@ -61,11 +57,6 @@
editableFields = cloneDeep($memoStore?.meta?.fields) editableFields = cloneDeep($memoStore?.meta?.fields)
} }
// Needs to go now... entirely
// $: if ($memoStore?.meta?.columns) {
// columns = new Set(meta?.columns)
// }
$: parsedBindings = bindings.map(binding => { $: parsedBindings = bindings.map(binding => {
let clone = Object.assign({}, binding) let clone = Object.assign({}, binding)
clone.icon = "ShareAndroid" clone.icon = "ShareAndroid"
@ -74,6 +65,9 @@
$: tableId = $memoStore?.row?.tableId $: tableId = $memoStore?.row?.tableId
$: if (tableId) { $: if (tableId) {
// Refresh all the row data
editableRow = cloneDeep($memoStore?.row)
table = $tables.list.find(table => table._id === tableId) table = $tables.list.find(table => table._id === tableId)
if (table) { if (table) {
@ -97,12 +91,11 @@
} }
editableFields = editableFields editableFields = editableFields
} }
// Go through the table schema and build out the editable content // Go through the table schema and build out the editable content
// schemaFields.forEach(entry => {
for (const entry of schemaFields) { for (const entry of schemaFields) {
const [key, fieldSchema] = entry const [key, fieldSchema] = entry
if ($memoStore?.row?.[key] && !editableRow?.[key]) { if ($memoStore?.row?.[key]) {
// DEAN - review this
editableRow = { editableRow = {
...editableRow, ...editableRow,
[key]: $memoStore?.row[key], [key]: $memoStore?.row[key],
@ -110,7 +103,23 @@
} }
// Legacy // Legacy
if (editableFields[key]?.clearRelationships) { const emptyField =
!$memoStore?.row[key] || $memoStore?.row[key]?.length === 0
// Legacy
// Put non-empty elements into the update and add their key to the fields list.
if (!emptyField && !editableFields.hasOwnProperty(key)) {
//DEAN - review this - IF THEY ADDED A NEW ONE IT WOULD BE MISSING FROM editableFields + editableFields
console.log("EMPTY STATE DETECTED")
editableFields = {
...editableFields,
[key]: key,
}
}
// Legacy - clearRelationships
// Init the field and add it to the update.
if (emptyField && editableFields[key]?.clearRelationships === true) {
const emptyField = coerce( const emptyField = coerce(
!$memoStore?.row.hasOwnProperty(key) ? "" : $memoStore?.row[key], !$memoStore?.row.hasOwnProperty(key) ? "" : $memoStore?.row[key],
fieldSchema.type fieldSchema.type
@ -124,8 +133,6 @@
...editableRow, ...editableRow,
[key]: emptyField, [key]: emptyField,
} }
console.log("DEAN EMPTY - clearRelationships", emptyField)
} }
} }
@ -193,6 +200,14 @@
return value return value
} }
const isFullWidth = type => {
return (
attachmentTypes.includes(type) ||
type === FieldType.JSON ||
type === FieldType.LONGFORM
)
}
const onChange = update => { const onChange = update => {
const customizer = (objValue, srcValue, key) => { const customizer = (objValue, srcValue, key) => {
if (isPlainObject(objValue) && isPlainObject(srcValue)) { if (isPlainObject(objValue) && isPlainObject(srcValue)) {
@ -201,7 +216,6 @@
if (result[key] !== null) { if (result[key] !== null) {
acc[key] = result[key] acc[key] = result[key]
} else { } else {
console.log(key + " is null", objValue)
} }
return acc return acc
}, {}) }, {})
@ -228,7 +242,7 @@
{#each schemaFields || [] as [field, schema]} {#each schemaFields || [] as [field, schema]}
{#if !schema.autocolumn && editableFields.hasOwnProperty(field)} {#if !schema.autocolumn && editableFields.hasOwnProperty(field)}
<PropField label={field} fullWidth={attachmentTypes.includes(schema.type)}> <PropField label={field} fullWidth={isFullWidth(schema.type)}>
<div class="prop-control-wrap"> <div class="prop-control-wrap">
{#if isTestModal} {#if isTestModal}
<RowSelectorTypes <RowSelectorTypes
@ -249,13 +263,12 @@
type={schema.type} type={schema.type}
{schema} {schema}
value={editableRow[field]} value={editableRow[field]}
on:change={e => { on:change={e =>
onChange({ onChange({
row: { row: {
[field]: e.detail.row[field], [field]: e.detail,
}, },
}) })}
}}
{bindings} {bindings}
allowJS={true} allowJS={true}
updateOnChange={false} updateOnChange={false}
@ -280,24 +293,11 @@
<Icon <Icon
hoverable hoverable
name="Close" name="Close"
on:click={() => { on:click={() =>
// Clear row data
const update = { ...editableRow }
update[field] = null
// delete update[field]
// Clear any related metadata
// delete editableFields[field]
// editableFields[field] = null
console.log("REMOVE STATE", {
row: update,
meta: { fields: { ...editableFields, [field]: null } },
})
onChange({ onChange({
row: update, row: { [field]: null },
meta: { fields: { ...editableFields, [field]: null } }, meta: { fields: { [field]: null } },
}) })}
}}
/> />
</div> </div>
</PropField> </PropField>
@ -327,6 +327,7 @@
bind:this={customPopover} bind:this={customPopover}
anchor={popoverAnchor} anchor={popoverAnchor}
minWidth={popoverAnchor?.getBoundingClientRect()?.width} minWidth={popoverAnchor?.getBoundingClientRect()?.width}
maxWidth={popoverAnchor?.getBoundingClientRect()?.width}
maxHeight={300} maxHeight={300}
resizable={false} resizable={false}
offset={10} offset={10}
@ -385,4 +386,9 @@
grid-template-columns: 1fr min-content; grid-template-columns: 1fr min-content;
gap: var(--spacing-s); gap: var(--spacing-s);
} }
/* Override for general json field override */
.prop-control-wrap :global(.icon.json-slot-icon) {
right: 1px !important;
}
</style> </style>

View File

@ -11,7 +11,7 @@
import DrawerBindableInput from "../../common/bindings/DrawerBindableInput.svelte" import DrawerBindableInput from "../../common/bindings/DrawerBindableInput.svelte"
import ModalBindableInput from "../../common/bindings/ModalBindableInput.svelte" import ModalBindableInput from "../../common/bindings/ModalBindableInput.svelte"
import AutomationBindingPanel from "../../common/bindings/ServerBindingPanel.svelte" import AutomationBindingPanel from "../../common/bindings/ServerBindingPanel.svelte"
import Editor from "components/integration/QueryEditor.svelte" import CodeEditor from "components/common/CodeEditor/CodeEditor.svelte"
import KeyValueBuilder from "components/integration/KeyValueBuilder.svelte" import KeyValueBuilder from "components/integration/KeyValueBuilder.svelte"
export let onChange export let onChange
@ -22,7 +22,7 @@
export let bindings export let bindings
export let isTestModal export let isTestModal
$: console.log(field + "VALUE???", value[field]) $: fieldData = value[field]
$: parsedBindings = bindings.map(binding => { $: parsedBindings = bindings.map(binding => {
let clone = Object.assign({}, binding) let clone = Object.assign({}, binding)
@ -56,7 +56,6 @@
params[param.url || ""] = param.filename || "" params[param.url || ""] = param.filename || ""
} }
} }
console.log("handleAttachmentParams ", params)
return params return params
} }
</script> </script>
@ -69,12 +68,12 @@
[field]: e.detail, [field]: e.detail,
}, },
})} })}
value={value[field]} value={fieldData}
options={schema.constraints.inclusion} options={schema.constraints.inclusion}
/> />
{:else if schema.type === "datetime"} {:else if schema.type === "datetime"}
<DatePicker <DatePicker
value={value[field]} value={fieldData}
on:change={e => on:change={e =>
onChange({ onChange({
row: { row: {
@ -90,7 +89,7 @@
[field]: e.detail, [field]: e.detail,
}, },
})} })}
value={value[field]} value={fieldData}
options={[ options={[
{ label: "True", value: "true" }, { label: "True", value: "true" },
{ label: "False", value: "false" }, { label: "False", value: "false" },
@ -98,7 +97,7 @@
/> />
{:else if schemaHasOptions(schema) && schema.type === "array"} {:else if schemaHasOptions(schema) && schema.type === "array"}
<Multiselect <Multiselect
value={value[field]} value={fieldData}
options={schema.constraints.inclusion} options={schema.constraints.inclusion}
on:change={e => on:change={e =>
onChange({ onChange({
@ -109,7 +108,7 @@
/> />
{:else if schema.type === "longform"} {:else if schema.type === "longform"}
<TextArea <TextArea
value={value[field]} value={fieldData}
on:change={e => on:change={e =>
onChange({ onChange({
row: { row: {
@ -119,24 +118,25 @@
/> />
{:else if schema.type === "json"} {:else if schema.type === "json"}
<span> <span>
<Editor <div class="field-wrap">
editorHeight="150" <CodeEditor
mode="json" value={fieldData}
on:change={e => { on:change={e => {
if (e.detail?.value !== value[field]) { console.log("JSON change", e.detail?.value, fieldData)
onChange({ if (e.detail?.value !== fieldData) {
row: { onChange({
[field]: e.detail, row: {
}, [field]: e.detail,
}) },
} })
}} }
value={value[field]} }}
/> />
</div>
</span> </span>
{:else if schema.type === "link"} {:else if schema.type === "link"}
<LinkedRowSelector <LinkedRowSelector
linkedRows={value[field]} linkedRows={fieldData}
{schema} {schema}
on:change={e => on:change={e =>
onChange({ onChange({
@ -148,7 +148,7 @@
/> />
{:else if schema.type === "bb_reference" || schema.type === "bb_reference_single"} {:else if schema.type === "bb_reference" || schema.type === "bb_reference_single"}
<LinkedRowSelector <LinkedRowSelector
linkedRows={value[field]} linkedRows={fieldData}
{schema} {schema}
linkedTableId={"ta_users"} linkedTableId={"ta_users"}
on:change={e => on:change={e =>
@ -167,16 +167,10 @@
text={"Use bindings"} text={"Use bindings"}
size={"XS"} size={"XS"}
on:change={e => { on:change={e => {
const fromFalse =
!meta?.fields?.[field]?.useAttachmentBinding && e.detail === true
onChange({ onChange({
...(fromFalse row: {
? { [field]: null,
row: { },
[field]: "", //clear the value if switching
},
}
: {}),
meta: { meta: {
fields: { fields: {
[field]: { [field]: {
@ -192,7 +186,7 @@
{#if !meta?.fields?.[field]?.useAttachmentBinding} {#if !meta?.fields?.[field]?.useAttachmentBinding}
<div class="attachment-field-spacing"> <div class="attachment-field-spacing">
<KeyValueBuilder <KeyValueBuilder
on:change={e => on:change={e => {
onChange({ onChange({
row: { row: {
[field]: [field]:
@ -203,14 +197,15 @@
url: e.detail[0].name, url: e.detail[0].name,
filename: e.detail[0].value, filename: e.detail[0].value,
} }
: {} : null
: e.detail.map(({ name, value }) => ({ : e.detail.map(({ name, value }) => ({
url: name, url: name,
filename: value, filename: value,
})), })),
}, },
})} })
object={handleAttachmentParams(value[field], false)} }}
object={handleAttachmentParams(fieldData)}
allowJS allowJS
{bindings} {bindings}
keyBindings keyBindings
@ -221,16 +216,15 @@
valuePlaceholder={"Filename"} valuePlaceholder={"Filename"}
actionButtonDisabled={(schema.type === FieldType.ATTACHMENT_SINGLE || actionButtonDisabled={(schema.type === FieldType.ATTACHMENT_SINGLE ||
schema.type === FieldType.SIGNATURE_SINGLE) && schema.type === FieldType.SIGNATURE_SINGLE) &&
Object.keys(value[field] || {}).length >= 1} Object.keys(fieldData || {}).length >= 1}
/> />
</div> </div>
{:else} {:else}
<div class="json-input-spacing"> <div class="json-input-spacing">
{JSON.stringify(value[field])}
<svelte:component <svelte:component
this={isTestModal ? ModalBindableInput : DrawerBindableInput} this={isTestModal ? ModalBindableInput : DrawerBindableInput}
panel={AutomationBindingPanel} panel={AutomationBindingPanel}
value={value[field]} value={fieldData}
on:change={e => on:change={e =>
onChange({ onChange({
row: { row: {
@ -247,11 +241,10 @@
{/if} {/if}
</div> </div>
{:else if ["string", "number", "bigint", "barcodeqr", "array"].includes(schema.type)} {:else if ["string", "number", "bigint", "barcodeqr", "array"].includes(schema.type)}
{JSON.stringify(value[field])}
<svelte:component <svelte:component
this={isTestModal ? ModalBindableInput : DrawerBindableInput} this={isTestModal ? ModalBindableInput : DrawerBindableInput}
panel={AutomationBindingPanel} panel={AutomationBindingPanel}
value={value[field]} value={fieldData}
on:change={e => on:change={e =>
onChange({ onChange({
row: { row: {
@ -268,11 +261,20 @@
{/if} {/if}
<style> <style>
.attachment-field-spacing, .attachment-field-spacing {
.json-input-spacing {
margin-top: var(--spacing-s);
border: 1px solid var(--spectrum-global-color-gray-400); border: 1px solid var(--spectrum-global-color-gray-400);
border-radius: 4px; border-radius: 4px;
padding: var(--spacing-s); padding: var(--spacing-s);
} }
.field-wrap {
box-sizing: border-box;
border: 1px solid var(--spectrum-global-color-gray-400);
border-radius: 4px;
}
.field-wrap :global(.cm-editor),
.field-wrap :global(.cm-scroller) {
border-radius: 4px;
}
</style> </style>

View File

@ -157,7 +157,8 @@ const automationActions = store => ({
) )
} }
}, },
updateBlockInputs: async (block, data) => {
processBlockInputs: async (block, data) => {
// Create new modified block // Create new modified block
let newBlock = { let newBlock = {
...block, ...block,
@ -184,6 +185,14 @@ const automationActions = store => ({
// Don't save if no changes were made // Don't save if no changes were made
if (JSON.stringify(newAutomation) === JSON.stringify(automation)) { if (JSON.stringify(newAutomation) === JSON.stringify(automation)) {
return false
}
return newAutomation
},
updateBlockInputs: async (block, data) => {
const newAutomation = await store.actions.processBlockInputs(block, data)
if (newAutomation === false) {
return return
} }
await store.actions.save(newAutomation) await store.actions.save(newAutomation)

View File

@ -100,7 +100,10 @@ export function getError(err: any) {
} }
export function guardAttachment(attachmentObject: any) { export function guardAttachment(attachmentObject: any) {
if (!("url" in attachmentObject) || !("filename" in attachmentObject)) { if (
attachmentObject &&
(!("url" in attachmentObject) || !("filename" in attachmentObject))
) {
const providedKeys = Object.keys(attachmentObject).join(", ") const providedKeys = Object.keys(attachmentObject).join(", ")
throw new Error( throw new Error(
`Attachments must have both "url" and "filename" keys. You have provided: ${providedKeys}` `Attachments must have both "url" and "filename" keys. You have provided: ${providedKeys}`

View File

@ -29,6 +29,17 @@ export const definition: AutomationStepSchema = {
meta: { meta: {
type: AutomationIOType.OBJECT, type: AutomationIOType.OBJECT,
title: "Field settings", title: "Field settings",
// DEAN - REVIEW THIS - add in some record of these types
// properties: {
// fields: {
// properties: {
// useAttachmentBinding: {
// type: AutomationIOType.BOOLEAN,
// },
// },
// },
// },
}, },
row: { row: {
type: AutomationIOType.OBJECT, type: AutomationIOType.OBJECT,
@ -83,37 +94,53 @@ export async function run({ inputs, appId, emitter }: AutomationStepInput) {
const tableId = inputs.row.tableId const tableId = inputs.row.tableId
// Base update // Base update
let rowUpdate: Record<string, any> = { let rowUpdate: Record<string, any>
tableId,
}
// Column checking - explicit clearing of empty fields // Legacy - find any empty values in the row that need to be cleared
if (inputs?.meta?.columns) { const legacyUpdated = Object.keys(inputs.row || {}).reduce(
rowUpdate = inputs?.meta?.columns.reduce( (acc: Record<string, any>, key: string) => {
(acc: Record<string, any>, key: string) => { const isEmpty = inputs.row[key] == null || inputs.row[key]?.length === 0
acc[key] = const fieldConfig = inputs.meta?.fields?.[key]
!inputs.row[key] || inputs.row[key]?.length === 0
? null if (isEmpty) {
: inputs.row[key] if (
return acc inputs.meta?.fields.hasOwnProperty(key) &&
}, fieldConfig?.clearRelationships === true
{} ) {
) // Explicitly clear the field on update
} else { acc[key] = []
// Legacy - clear any empty string column values so that they aren't updated }
rowUpdate = { } else {
...inputs.row, // Keep non-empty values
} acc[key] = inputs.row[key]
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]
} }
} return acc
},
{}
)
// The source of truth for inclusion in the update is: inputs.meta?.fields
const parsedUpdate = Object.keys(inputs.meta?.fields || {}).reduce(
(acc: Record<string, any>, key: string) => {
const fieldConfig = inputs.meta?.fields?.[key]
// Ignore legacy config.
if (fieldConfig.hasOwnProperty("clearRelationships")) {
return acc
}
acc[key] =
inputs.row.hasOwnProperty(key) &&
(inputs.row[key] == null || inputs.row[key]?.length === 0)
? undefined
: inputs.row[key]
return acc
},
{}
)
rowUpdate = {
tableId,
...parsedUpdate,
...legacyUpdated,
} }
try { try {