Refactor Object Form and Child Table for Improved Data Handling and Computed Values Management

- Introduced applyComputedEntries function to streamline the merging of computed entries with existing object data, enhancing clarity and maintainability.
- Updated NewObjectForm and ObjectForm components to utilize applyComputedEntries for setting initial form data and computed values, improving data handling consistency.
- Refactored ObjectChildTable to ensure proper handling of value props and fallback mechanisms, enhancing user experience during editing.
- Removed unused DeleteObjectModal from ObjectForm, simplifying the component structure.
This commit is contained in:
Tom Butcher 2026-08-22 21:01:50 +01:00
parent 978f444538
commit c99227816e
3 changed files with 48 additions and 55 deletions

View File

@ -3,6 +3,7 @@ import { Form } from 'antd'
import { ApiServerContext } from '../context/ApiServerContext'
import { useMessageContext } from '../context/MessageContext'
import PropTypes from 'prop-types'
import merge from 'lodash/merge'
import mergeWith from 'lodash/mergeWith'
import set from 'lodash/set'
import { getModelByName } from '../../../database/ObjectModels'
@ -18,6 +19,16 @@ const buildObjectFromEntries = (entries = []) => {
}, {})
}
const applyComputedEntries = (base, entries = []) => {
const result = merge({}, base || {})
entries.forEach((entry) => {
const { namePath, value } = entry || {}
if (!Array.isArray(namePath) || value === undefined) return
set(result, namePath, value)
})
return result
}
const arrayReplaceCustomizer = (objValue, srcValue, key) => {
if (Array.isArray(srcValue)) {
return srcValue
@ -172,12 +183,9 @@ const NewObjectForm = ({ type, style, defaultValues = {}, children }) => {
if (Object.keys(defaultValues).length > 0) {
// Calculate computed values for initial data
const computedEntries = calculateComputedValues(defaultValues, model)
const computedValuesObject = buildObjectFromEntries(computedEntries)
const initialFormData = mergeWith(
{},
const initialFormData = applyComputedEntries(
defaultValues,
computedValuesObject,
arrayReplaceCustomizer
computedEntries
)
form.setFieldsValue(initialFormData)
setObjectData((prev) =>
@ -196,12 +204,9 @@ const NewObjectForm = ({ type, style, defaultValues = {}, children }) => {
try {
setSubmitLoading(true)
const computedEntries = calculateComputedValues(objectData, model)
const computedValuesObject = buildObjectFromEntries(computedEntries)
const computedObjectData = mergeWith(
{},
const computedObjectData = applyComputedEntries(
objectData,
computedValuesObject,
arrayReplaceCustomizer
computedEntries
)
const newObject = await createObject(type, computedObjectData)
showSuccess('Object created successfully')
@ -253,14 +258,7 @@ const NewObjectForm = ({ type, style, defaultValues = {}, children }) => {
})
}
// Merge all values (user input + computed values)
const computedValuesObject = buildObjectFromEntries(computedEntries)
const allValues = mergeWith(
{},
allFormValues,
computedValuesObject,
arrayReplaceCustomizer
)
const allValues = applyComputedEntries(allFormValues, computedEntries)
setObjectData((prev) => {
return mergeWith({}, prev, allValues, arrayReplaceCustomizer)
})

View File

@ -64,7 +64,7 @@ const ObjectChildTable = ({
additionalColumns = [],
emptyText = 'No items',
isEditing = false,
value = [],
value,
rollups = [],
onChange,
hiddenPropertyWidth = '300px',
@ -159,10 +159,13 @@ const ObjectChildTable = ({
)
}, [columns, properties])
// When used inside antd Form.Item without Form.List, `value` will be the controlled array.
// Form.Item may not have hydrated yet; fall back to objectData the same way
// rollups already do.
const itemsSource = useMemo(() => {
return value ?? []
}, [value])
if (Array.isArray(value)) return value
if (name && Array.isArray(objectData?.[name])) return objectData[name]
return []
}, [value, name, objectData])
const handleRemoveItem = useCallback(
(record, index) => {
@ -265,12 +268,11 @@ const ObjectChildTable = ({
<ObjectProperty
{...property}
longId={false}
inTable={true}
objectData={record}
parentData={objectData}
isEditing={isEditing}
useFormItem={false}
name={undefined}
value={record[property.name]}
onChange={handleCellChange}
/>
)
@ -384,10 +386,7 @@ const ObjectChildTable = ({
const rollupDataSource = useMemo(() => {
if (!rollups || rollups.length === 0) return []
// Use value from form/props, or fall back to objectData when entering edit mode
// (form may not have populated the field yet)
const itemsForRollup =
value ?? (name && objectData ? objectData[name] : null) ?? []
const itemsForRollup = itemsSource
// Build parent object with children array for rollup functions (e.g. objectData.parts)
const updatedObjectData = { ...objectData }
@ -420,7 +419,7 @@ const ObjectChildTable = ({
})
return [summaryRow]
}, [properties, rollups, objectData, value, name])
}, [properties, rollups, objectData, itemsSource, name])
const rollupColumns = useMemo(() => {
const propertyColumns = resolvedProperties.map((property, index) => {
@ -634,7 +633,8 @@ const ObjectChildTable = ({
setMinimalModelOpen(true)
}}
>
{value?.length || 0} {value?.length == 1 ? 'item' : 'items'}
{itemsSource?.length || 0}{' '}
{itemsSource?.length == 1 ? 'item' : 'items'}
</Link>
<Modal
open={minimalModelOpen}

View File

@ -79,6 +79,19 @@ const buildObjectFromEntries = (entries = []) => {
}, {})
}
// Patch computed fields onto a full object. Building them as a sparse tree and
// merge-replacing arrays would wipe stored child-row fields (shipment, amount,
// etc.) while leaving calculated columns intact.
const applyComputedEntries = (base, entries = []) => {
const result = merge({}, base || {})
entries.forEach((entry) => {
const { namePath, value } = entry || {}
if (!Array.isArray(namePath) || value === undefined) return
set(result, namePath, value)
})
return result
}
const getEditDisabled = (model, objectData, userProfile) => {
if (model?.readOnly === true) {
return true
@ -356,12 +369,9 @@ const ObjectForm = forwardRef(
serverObjectData.current,
model
)
const computedValuesObject = buildObjectFromEntries(computedEntries)
const resetFormData = mergeWith(
{},
const resetFormData = applyComputedEntries(
serverObjectData.current,
computedValuesObject,
arrayReplaceCustomizer
computedEntries
)
setIsEditing(false)
isEditingRef.current = false
@ -498,13 +508,7 @@ const ObjectForm = forwardRef(
// Calculate and set computed values on initial load
const computedEntries = calculateComputedValues(data, model)
const computedValuesObject = buildObjectFromEntries(computedEntries)
const initialFormData = mergeWith(
{},
data,
computedValuesObject,
arrayReplaceCustomizer
)
const initialFormData = applyComputedEntries(data, computedEntries)
setObjectData({ ...initialFormData, _isEditing: isEditingRef.current })
form.setFieldsValue(initialFormData)
setFetchLoading(false)
@ -703,17 +707,11 @@ const ObjectForm = forwardRef(
setIsEditing(true)
const computedEntries = calculateComputedValues(objectData, model)
const computedValuesObject = buildObjectFromEntries(computedEntries)
setObjectData((prev) => ({
...prev,
...computedValuesObject,
_isEditing: true
}))
const nextObjectData = {
...objectData,
...computedValuesObject,
...applyComputedEntries(objectData, computedEntries),
_isEditing: true
}
setObjectData(nextObjectData)
onStateChangeRef.current({
isEditing: true,
objectData: nextObjectData,
@ -868,12 +866,9 @@ const ObjectForm = forwardRef(
})
}
const computedValuesObject = buildObjectFromEntries(computedEntries)
const mergedFormValues = mergeWith(
{},
const mergedFormValues = applyComputedEntries(
allFormValues,
computedValuesObject,
arrayReplaceCustomizer
computedEntries
)
mergedFormValues._isEditing = isEditingRef.current