Refactor NewObjectForm for Improved Initialization and State Management
- Updated the form initialization logic to ensure it accurately reflects the latest default values and computed entries. - Enhanced the handling of form state synchronization with object data, ensuring consistency during edits. - Improved validation and reset logic to maintain form integrity when default values change. - Streamlined the application of computed values to enhance clarity and maintainability of the component.
This commit is contained in:
parent
6bc81cf90f
commit
a1697fe0fd
@ -5,6 +5,7 @@ import { useMessageContext } from '../context/MessageContext'
|
|||||||
import PropTypes from 'prop-types'
|
import PropTypes from 'prop-types'
|
||||||
import set from 'lodash/set'
|
import set from 'lodash/set'
|
||||||
import { getModelByName } from '../../../database/ObjectModels'
|
import { getModelByName } from '../../../database/ObjectModels'
|
||||||
|
|
||||||
import {
|
import {
|
||||||
mergeFormData,
|
mergeFormData,
|
||||||
stripNestedObjectProperties,
|
stripNestedObjectProperties,
|
||||||
@ -14,26 +15,34 @@ import {
|
|||||||
const buildObjectFromEntries = (entries = []) => {
|
const buildObjectFromEntries = (entries = []) => {
|
||||||
return entries.reduce((acc, entry) => {
|
return entries.reduce((acc, entry) => {
|
||||||
const { namePath, value } = entry || {}
|
const { namePath, value } = entry || {}
|
||||||
|
|
||||||
if (!Array.isArray(namePath) || value === undefined) {
|
if (!Array.isArray(namePath) || value === undefined) {
|
||||||
return acc
|
return acc
|
||||||
}
|
}
|
||||||
|
|
||||||
set(acc, namePath, value)
|
set(acc, namePath, value)
|
||||||
|
|
||||||
return acc
|
return acc
|
||||||
}, {})
|
}, {})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Patch computed fields onto the complete object.
|
||||||
|
// This mirrors the handling used by ObjectForm.
|
||||||
const applyComputedEntries = (base, entries = []) => {
|
const applyComputedEntries = (base, entries = []) => {
|
||||||
const result = mergeFormData(base || {})
|
const result = mergeFormData(base || {})
|
||||||
|
|
||||||
entries.forEach((entry) => {
|
entries.forEach((entry) => {
|
||||||
const { namePath, value } = entry || {}
|
const { namePath, value } = entry || {}
|
||||||
if (!Array.isArray(namePath) || value === undefined) return
|
if (!Array.isArray(namePath) || value === undefined) return
|
||||||
set(result, namePath, value)
|
set(result, namePath, value)
|
||||||
})
|
})
|
||||||
|
|
||||||
return result
|
return result
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* NewObjectForm is a reusable form component for creating new objects.
|
* NewObjectForm is a reusable form component for creating new objects.
|
||||||
|
*
|
||||||
* It handles form validation, submission, and error handling logic.
|
* It handles form validation, submission, and error handling logic.
|
||||||
*
|
*
|
||||||
* Props:
|
* Props:
|
||||||
@ -45,21 +54,33 @@ const applyComputedEntries = (base, entries = []) => {
|
|||||||
* }) => ReactNode
|
* }) => ReactNode
|
||||||
*/
|
*/
|
||||||
const NewObjectForm = ({ type, style, defaultValues = {}, children }) => {
|
const NewObjectForm = ({ type, style, defaultValues = {}, children }) => {
|
||||||
|
// Do not initialise this from defaultValues.
|
||||||
|
// The form initialisation effect below is the single source of truth,
|
||||||
|
// matching ObjectForm's fetched-object handling.
|
||||||
const [objectData, setObjectData] = useState({
|
const [objectData, setObjectData] = useState({
|
||||||
...defaultValues,
|
|
||||||
_isEditing: true
|
_isEditing: true
|
||||||
})
|
})
|
||||||
|
|
||||||
const [submitLoading, setSubmitLoading] = useState(false)
|
const [submitLoading, setSubmitLoading] = useState(false)
|
||||||
const [formValid, setFormValid] = useState(false)
|
const [formValid, setFormValid] = useState(false)
|
||||||
|
|
||||||
const [form] = Form.useForm()
|
const [form] = Form.useForm()
|
||||||
|
|
||||||
const validationRunRef = useRef(0)
|
const validationRunRef = useRef(0)
|
||||||
|
|
||||||
const formUpdateValues = Form.useWatch([], form)
|
const formUpdateValues = Form.useWatch([], form)
|
||||||
|
|
||||||
const { showSuccess, showError: showMessageError } = useMessageContext()
|
const { showSuccess, showError: showMessageError } = useMessageContext()
|
||||||
|
|
||||||
const { createObject, showError } = useContext(ApiServerContext)
|
const { createObject, showError } = useContext(ApiServerContext)
|
||||||
|
|
||||||
|
const model = getModelByName(type)
|
||||||
|
|
||||||
const validateForm = useCallback(() => {
|
const validateForm = useCallback(() => {
|
||||||
const validationRun = ++validationRunRef.current
|
const validationRun = ++validationRunRef.current
|
||||||
|
|
||||||
let cancelled = false
|
let cancelled = false
|
||||||
|
|
||||||
const timeoutId = setTimeout(() => {
|
const timeoutId = setTimeout(() => {
|
||||||
form
|
form
|
||||||
.validateFields({ validateOnly: true })
|
.validateFields({ validateOnly: true })
|
||||||
@ -81,27 +102,59 @@ const NewObjectForm = ({ type, style, defaultValues = {}, children }) => {
|
|||||||
}
|
}
|
||||||
}, [form])
|
}, [form])
|
||||||
|
|
||||||
const model = getModelByName(type)
|
|
||||||
|
|
||||||
const calculateComputedValues = useCallback(
|
const calculateComputedValues = useCallback(
|
||||||
(currentData, modelDefinition, options = {}) => {
|
(currentData, modelDefinition, options = {}) => {
|
||||||
return calculateModelComputedEntries(currentData, modelDefinition, options)
|
return calculateModelComputedEntries(
|
||||||
|
currentData,
|
||||||
|
modelDefinition,
|
||||||
|
options
|
||||||
|
)
|
||||||
},
|
},
|
||||||
[]
|
[]
|
||||||
)
|
)
|
||||||
|
|
||||||
// Set initial form values when defaultValues change
|
/*
|
||||||
|
* Initialise the form from defaultValues.
|
||||||
|
*
|
||||||
|
* This deliberately follows ObjectForm's initial fetch handling:
|
||||||
|
*
|
||||||
|
* defaultValues
|
||||||
|
* ↓
|
||||||
|
* calculate computed values
|
||||||
|
* ↓
|
||||||
|
* apply computed values to the complete object
|
||||||
|
* ↓
|
||||||
|
* setObjectData(...)
|
||||||
|
* ↓
|
||||||
|
* form.setFieldsValue(...)
|
||||||
|
*
|
||||||
|
* Keeping the exact same object structure in objectData and the Form
|
||||||
|
* is important for object-select fields.
|
||||||
|
*/
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (Object.keys(defaultValues).length > 0) {
|
const initialData = mergeFormData(defaultValues || {})
|
||||||
const computedEntries = calculateComputedValues(defaultValues, model)
|
|
||||||
const initialFormData = applyComputedEntries(
|
const computedEntries = calculateComputedValues(initialData, model)
|
||||||
defaultValues,
|
|
||||||
computedEntries
|
const initialFormData = applyComputedEntries(initialData, computedEntries)
|
||||||
)
|
|
||||||
form.setFieldsValue(initialFormData)
|
const nextObjectData = {
|
||||||
setObjectData((prev) => mergeFormData(prev, initialFormData))
|
...initialFormData,
|
||||||
return validateForm()
|
_isEditing: true
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Clear any previous values before applying the new defaults.
|
||||||
|
// This is particularly important when defaultValues changes while
|
||||||
|
// the component remains mounted.
|
||||||
|
form.resetFields()
|
||||||
|
|
||||||
|
// Use the complete object as the form value, just like ObjectForm.
|
||||||
|
form.setFieldsValue(initialFormData)
|
||||||
|
|
||||||
|
// Keep objectData in sync with exactly the same initial data.
|
||||||
|
setObjectData(nextObjectData)
|
||||||
|
|
||||||
|
return validateForm()
|
||||||
}, [form, defaultValues, calculateComputedValues, model, validateForm])
|
}, [form, defaultValues, calculateComputedValues, model, validateForm])
|
||||||
|
|
||||||
// Validate form on change
|
// Validate form on change
|
||||||
@ -112,23 +165,34 @@ const NewObjectForm = ({ type, style, defaultValues = {}, children }) => {
|
|||||||
const handleSubmit = async () => {
|
const handleSubmit = async () => {
|
||||||
try {
|
try {
|
||||||
setSubmitLoading(true)
|
setSubmitLoading(true)
|
||||||
|
|
||||||
const currentFormValues = form.getFieldsValue()
|
const currentFormValues = form.getFieldsValue()
|
||||||
|
|
||||||
const currentFormData = mergeFormData(objectData || {}, currentFormValues)
|
const currentFormData = mergeFormData(objectData || {}, currentFormValues)
|
||||||
|
|
||||||
const computedEntries = calculateComputedValues(currentFormData, model)
|
const computedEntries = calculateComputedValues(currentFormData, model)
|
||||||
|
|
||||||
const computedObjectData = applyComputedEntries(
|
const computedObjectData = applyComputedEntries(
|
||||||
currentFormData,
|
currentFormData,
|
||||||
computedEntries
|
computedEntries
|
||||||
)
|
)
|
||||||
|
|
||||||
const payload = stripNestedObjectProperties(computedObjectData, model)
|
const payload = stripNestedObjectProperties(computedObjectData, model)
|
||||||
|
|
||||||
const newObject = await createObject(type, payload)
|
const newObject = await createObject(type, payload)
|
||||||
|
|
||||||
showSuccess('Object created successfully')
|
showSuccess('Object created successfully')
|
||||||
|
|
||||||
return newObject
|
return newObject
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.error(err)
|
console.error(err)
|
||||||
|
|
||||||
if (err.errorFields) {
|
if (err.errorFields) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
showMessageError('Failed to create object')
|
showMessageError('Failed to create object')
|
||||||
|
|
||||||
showError(
|
showError(
|
||||||
`Failed to create object. Message: ${err.message}. Code: ${err.code}`,
|
`Failed to create object. Message: ${err.message}. Code: ${err.code}`,
|
||||||
() => handleSubmit()
|
() => handleSubmit()
|
||||||
@ -146,12 +210,17 @@ const NewObjectForm = ({ type, style, defaultValues = {}, children }) => {
|
|||||||
disabled={submitLoading}
|
disabled={submitLoading}
|
||||||
onValuesChange={(_changedValues, allFormValues) => {
|
onValuesChange={(_changedValues, allFormValues) => {
|
||||||
const currentFormData = mergeFormData(objectData || {}, allFormValues)
|
const currentFormData = mergeFormData(objectData || {}, allFormValues)
|
||||||
|
|
||||||
const computedEntries = calculateComputedValues(currentFormData, model)
|
const computedEntries = calculateComputedValues(currentFormData, model)
|
||||||
|
|
||||||
if (Array.isArray(computedEntries) && computedEntries.length > 0) {
|
if (Array.isArray(computedEntries) && computedEntries.length > 0) {
|
||||||
computedEntries.forEach(({ namePath, value }) => {
|
computedEntries.forEach(({ namePath, value }) => {
|
||||||
if (!Array.isArray(namePath) || value === undefined) return
|
if (!Array.isArray(namePath) || value === undefined) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
const currentValue = form.getFieldValue(namePath)
|
const currentValue = form.getFieldValue(namePath)
|
||||||
|
|
||||||
if (currentValue !== value) {
|
if (currentValue !== value) {
|
||||||
if (typeof form.setFieldValue === 'function') {
|
if (typeof form.setFieldValue === 'function') {
|
||||||
form.setFieldValue(namePath, value)
|
form.setFieldValue(namePath, value)
|
||||||
@ -159,6 +228,7 @@ const NewObjectForm = ({ type, style, defaultValues = {}, children }) => {
|
|||||||
const fallbackPayload = buildObjectFromEntries([
|
const fallbackPayload = buildObjectFromEntries([
|
||||||
{ namePath, value }
|
{ namePath, value }
|
||||||
])
|
])
|
||||||
|
|
||||||
form.setFieldsValue(fallbackPayload)
|
form.setFieldsValue(fallbackPayload)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -166,6 +236,9 @@ const NewObjectForm = ({ type, style, defaultValues = {}, children }) => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const allValues = applyComputedEntries(allFormValues, computedEntries)
|
const allValues = applyComputedEntries(allFormValues, computedEntries)
|
||||||
|
|
||||||
|
allValues._isEditing = true
|
||||||
|
|
||||||
setObjectData((prev) => mergeFormData(prev, allValues))
|
setObjectData((prev) => mergeFormData(prev, allValues))
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
@ -185,8 +258,11 @@ const NewObjectForm = ({ type, style, defaultValues = {}, children }) => {
|
|||||||
|
|
||||||
NewObjectForm.propTypes = {
|
NewObjectForm.propTypes = {
|
||||||
type: PropTypes.string.isRequired,
|
type: PropTypes.string.isRequired,
|
||||||
|
|
||||||
children: PropTypes.func.isRequired,
|
children: PropTypes.func.isRequired,
|
||||||
|
|
||||||
style: PropTypes.object,
|
style: PropTypes.object,
|
||||||
|
|
||||||
defaultValues: PropTypes.object
|
defaultValues: PropTypes.object
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user