1
0
Fork 0
mirror of synced 2024-09-20 19:33:10 +12:00

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

View file

@ -52,7 +52,11 @@
import { TriggerStepID, ActionStepID } from "constants/backend/automations"
import { onMount } from "svelte"
import { cloneDeep } from "lodash/fp"
import { AutomationEventType } from "@budibase/types"
import {
AutomationEventType,
AutomationStepType,
AutomationActionStepId,
} from "@budibase/types"
import { FIELDS } from "constants/backend"
import PropField from "./PropField.svelte"
@ -69,6 +73,13 @@
TriggerStepID.ROW_SAVED,
TriggerStepID.ROW_DELETED,
]
let rowEvents = [
AutomationEventType.ROW_DELETE,
AutomationEventType.ROW_SAVE,
AutomationEventType.ROW_UPDATE,
]
const rowSteps = [ActionStepID.UPDATE_ROW, ActionStepID.CREATE_ROW]
let webhookModal
@ -92,9 +103,11 @@
}).schema
$: schemaFields = Object.values(schema || {})
$: queryLimit = tableId?.includes("datasource") ? "∞" : "1000"
$: isTrigger = block?.type === "TRIGGER"
$: isTrigger = block?.type === AutomationStepType.TRIGGER
$: codeMode =
stepId === "EXECUTE_BASH" ? EditorModes.Handlebars : EditorModes.JS
stepId === AutomationActionStepId.EXECUTE_BASH
? EditorModes.Handlebars
: EditorModes.JS
$: bindingsHelpers = new BindingHelpers(getCaretPosition, insertAtPos, {
disableWrapping: true,
})
@ -133,7 +146,6 @@
$: customStepLayouts($memoBlock, schemaProperties)
const customStepLayouts = block => {
console.log("BUILDING", inputData["row"])
if (
rowSteps.includes(block.stepId) ||
(rowTriggers.includes(block.stepId) && isTestModal)
@ -157,6 +169,7 @@
{
type: DrawerBindableInput,
title: rowRevlabel,
props: {
panel: AutomationBindingPanel,
value: inputData["revision"],
onChange: e => {
@ -166,6 +179,7 @@
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 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
// used in the server to detect relationships. It would be far better to
// 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
if (request?._tableId) {
schema = getSchemaForDatasourcePlus(request._tableId, {
@ -643,18 +718,16 @@
<div class="label-wrapper">
<Label>{label}</Label>
</div>
{JSON.stringify(inputData)}
<div class="toggle-container">
<Toggle
value={inputData?.meta?.useAttachmentBinding}
text={"Use bindings"}
size={"XS"}
on:change={e => {
// DEAN - review this
onChange({
row: { [key]: "" }, //null
[key]: null,
meta: {
[key]: e.detail,
useAttachmentBinding: e.detail,
},
})
}}
@ -662,6 +735,7 @@
</div>
<div class="attachment-field-width">
{#if !inputData?.meta?.useAttachmentBinding}
<KeyValueBuilder
on:change={e =>
onChange({
@ -680,6 +754,31 @@
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>
{:else if value.customType === "filters"}

View file

@ -1,20 +1,38 @@
<script>
import { createEventDispatcher } from "svelte"
import RowSelectorTypes from "./RowSelectorTypes.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()
export let value
export let value = {}
export let bindings
export let block
export let isTestModal
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 = {}
// DEAN - review this
for (const [key, type] of Object.entries(block?.inputs?.fields ?? {})) {
fields = {
...fields,
@ -26,8 +44,8 @@
},
}
if (value[key] === type) {
value[key] = INITIAL_VALUES[type.toUpperCase()]
if (editableValue[key] === type) {
editableValue[key] = INITIAL_VALUES[type.toUpperCase()]
}
}
@ -39,52 +57,14 @@
NUMBER: null,
DATETIME: null,
STRING: "",
OPTIONS: [],
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
ARRAY: "",
}
const onChange = (e, field, type) => {
value[field] = coerce(e.detail, type)
dispatch("change", value)
if (e.detail !== editableValue[field]) {
editableValue[field] = e.detail
dispatch("change", editableValue)
}
}
</script>
@ -92,14 +72,34 @@
<div class="fields">
{#each schemaFields as [field, schema]}
<PropField label={field}>
<RowSelectorTypes
{isTestModal}
{field}
{schema}
{#if ["string", "number", "array"].includes(schema.type)}
<svelte:component
this={isTestModal ? ModalBindableInput : DrawerBindableInput}
panel={AutomationBindingPanel}
value={editableValue[field]}
on:change={e => onChange(e, field)}
type="string"
{bindings}
{value}
{onChange}
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>
{/each}
</div>

View file

@ -2,12 +2,18 @@
import { Label } from "@budibase/bbui"
export let label
export let labelTooltip
export let fullWidth = false
export let componentWidth = 320
</script>
<div class="prop-field" class:fullWidth>
<div class="prop-label">
<Label>{label}</Label>
<div
class="prop-field"
class:fullWidth
style={`--comp-width: ${componentWidth}px;`}
>
<div class="prop-label" title={label}>
<Label tooltip={labelTooltip}>{label}</Label>
</div>
<div class="prop-control">
<slot />
@ -17,15 +23,30 @@
<style>
.prop-field {
display: grid;
grid-template-columns: 1fr 320px;
grid-template-columns: 1fr var(--comp-width);
}
.prop-field.fullWidth {
grid-template-columns: 1fr;
}
.prop-field.fullWidth .prop-label {
margin-bottom: var(--spacing-s);
}
.prop-label {
display: flex;
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>

View file

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

View file

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

View file

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

View file

@ -100,7 +100,10 @@ export function getError(err: 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(", ")
throw new Error(
`Attachments must have both "url" and "filename" keys. You have provided: ${providedKeys}`

View file

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