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

View File

@ -64,7 +64,7 @@ const ObjectChildTable = ({
additionalColumns = [], additionalColumns = [],
emptyText = 'No items', emptyText = 'No items',
isEditing = false, isEditing = false,
value = [], value,
rollups = [], rollups = [],
onChange, onChange,
hiddenPropertyWidth = '300px', hiddenPropertyWidth = '300px',
@ -159,10 +159,13 @@ const ObjectChildTable = ({
) )
}, [columns, properties]) }, [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(() => { const itemsSource = useMemo(() => {
return value ?? [] if (Array.isArray(value)) return value
}, [value]) if (name && Array.isArray(objectData?.[name])) return objectData[name]
return []
}, [value, name, objectData])
const handleRemoveItem = useCallback( const handleRemoveItem = useCallback(
(record, index) => { (record, index) => {
@ -265,12 +268,11 @@ const ObjectChildTable = ({
<ObjectProperty <ObjectProperty
{...property} {...property}
longId={false} longId={false}
inTable={true}
objectData={record} objectData={record}
parentData={objectData} parentData={objectData}
isEditing={isEditing} isEditing={isEditing}
useFormItem={false} useFormItem={false}
name={undefined}
value={record[property.name]}
onChange={handleCellChange} onChange={handleCellChange}
/> />
) )
@ -384,10 +386,7 @@ const ObjectChildTable = ({
const rollupDataSource = useMemo(() => { const rollupDataSource = useMemo(() => {
if (!rollups || rollups.length === 0) return [] if (!rollups || rollups.length === 0) return []
// Use value from form/props, or fall back to objectData when entering edit mode const itemsForRollup = itemsSource
// (form may not have populated the field yet)
const itemsForRollup =
value ?? (name && objectData ? objectData[name] : null) ?? []
// Build parent object with children array for rollup functions (e.g. objectData.parts) // Build parent object with children array for rollup functions (e.g. objectData.parts)
const updatedObjectData = { ...objectData } const updatedObjectData = { ...objectData }
@ -420,7 +419,7 @@ const ObjectChildTable = ({
}) })
return [summaryRow] return [summaryRow]
}, [properties, rollups, objectData, value, name]) }, [properties, rollups, objectData, itemsSource, name])
const rollupColumns = useMemo(() => { const rollupColumns = useMemo(() => {
const propertyColumns = resolvedProperties.map((property, index) => { const propertyColumns = resolvedProperties.map((property, index) => {
@ -634,7 +633,8 @@ const ObjectChildTable = ({
setMinimalModelOpen(true) setMinimalModelOpen(true)
}} }}
> >
{value?.length || 0} {value?.length == 1 ? 'item' : 'items'} {itemsSource?.length || 0}{' '}
{itemsSource?.length == 1 ? 'item' : 'items'}
</Link> </Link>
<Modal <Modal
open={minimalModelOpen} 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) => { const getEditDisabled = (model, objectData, userProfile) => {
if (model?.readOnly === true) { if (model?.readOnly === true) {
return true return true
@ -356,12 +369,9 @@ const ObjectForm = forwardRef(
serverObjectData.current, serverObjectData.current,
model model
) )
const computedValuesObject = buildObjectFromEntries(computedEntries) const resetFormData = applyComputedEntries(
const resetFormData = mergeWith(
{},
serverObjectData.current, serverObjectData.current,
computedValuesObject, computedEntries
arrayReplaceCustomizer
) )
setIsEditing(false) setIsEditing(false)
isEditingRef.current = false isEditingRef.current = false
@ -498,13 +508,7 @@ const ObjectForm = forwardRef(
// Calculate and set computed values on initial load // Calculate and set computed values on initial load
const computedEntries = calculateComputedValues(data, model) const computedEntries = calculateComputedValues(data, model)
const computedValuesObject = buildObjectFromEntries(computedEntries) const initialFormData = applyComputedEntries(data, computedEntries)
const initialFormData = mergeWith(
{},
data,
computedValuesObject,
arrayReplaceCustomizer
)
setObjectData({ ...initialFormData, _isEditing: isEditingRef.current }) setObjectData({ ...initialFormData, _isEditing: isEditingRef.current })
form.setFieldsValue(initialFormData) form.setFieldsValue(initialFormData)
setFetchLoading(false) setFetchLoading(false)
@ -703,17 +707,11 @@ const ObjectForm = forwardRef(
setIsEditing(true) setIsEditing(true)
const computedEntries = calculateComputedValues(objectData, model) const computedEntries = calculateComputedValues(objectData, model)
const computedValuesObject = buildObjectFromEntries(computedEntries)
setObjectData((prev) => ({
...prev,
...computedValuesObject,
_isEditing: true
}))
const nextObjectData = { const nextObjectData = {
...objectData, ...applyComputedEntries(objectData, computedEntries),
...computedValuesObject,
_isEditing: true _isEditing: true
} }
setObjectData(nextObjectData)
onStateChangeRef.current({ onStateChangeRef.current({
isEditing: true, isEditing: true,
objectData: nextObjectData, objectData: nextObjectData,
@ -868,12 +866,9 @@ const ObjectForm = forwardRef(
}) })
} }
const computedValuesObject = buildObjectFromEntries(computedEntries) const mergedFormValues = applyComputedEntries(
const mergedFormValues = mergeWith(
{},
allFormValues, allFormValues,
computedValuesObject, computedEntries
arrayReplaceCustomizer
) )
mergedFormValues._isEditing = isEditingRef.current mergedFormValues._isEditing = isEditingRef.current