diff --git a/src/components/Dashboard/common/NewObjectForm.jsx b/src/components/Dashboard/common/NewObjectForm.jsx index 92484060..66564f32 100644 --- a/src/components/Dashboard/common/NewObjectForm.jsx +++ b/src/components/Dashboard/common/NewObjectForm.jsx @@ -5,6 +5,7 @@ import { useMessageContext } from '../context/MessageContext' import PropTypes from 'prop-types' import set from 'lodash/set' import { getModelByName } from '../../../database/ObjectModels' + import { mergeFormData, stripNestedObjectProperties, @@ -14,26 +15,34 @@ import { const buildObjectFromEntries = (entries = []) => { return entries.reduce((acc, entry) => { const { namePath, value } = entry || {} + if (!Array.isArray(namePath) || value === undefined) { return acc } + set(acc, namePath, value) + return acc }, {}) } +// Patch computed fields onto the complete object. +// This mirrors the handling used by ObjectForm. const applyComputedEntries = (base, entries = []) => { const result = mergeFormData(base || {}) + entries.forEach((entry) => { const { namePath, value } = entry || {} if (!Array.isArray(namePath) || value === undefined) return set(result, namePath, value) }) + return result } /** * NewObjectForm is a reusable form component for creating new objects. + * * It handles form validation, submission, and error handling logic. * * Props: @@ -45,21 +54,33 @@ const applyComputedEntries = (base, entries = []) => { * }) => ReactNode */ 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({ - ...defaultValues, _isEditing: true }) + const [submitLoading, setSubmitLoading] = useState(false) const [formValid, setFormValid] = useState(false) + const [form] = Form.useForm() + const validationRunRef = useRef(0) + const formUpdateValues = Form.useWatch([], form) + const { showSuccess, showError: showMessageError } = useMessageContext() + const { createObject, showError } = useContext(ApiServerContext) + const model = getModelByName(type) + const validateForm = useCallback(() => { const validationRun = ++validationRunRef.current + let cancelled = false + const timeoutId = setTimeout(() => { form .validateFields({ validateOnly: true }) @@ -81,27 +102,59 @@ const NewObjectForm = ({ type, style, defaultValues = {}, children }) => { } }, [form]) - const model = getModelByName(type) - const calculateComputedValues = useCallback( (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(() => { - if (Object.keys(defaultValues).length > 0) { - const computedEntries = calculateComputedValues(defaultValues, model) - const initialFormData = applyComputedEntries( - defaultValues, - computedEntries - ) - form.setFieldsValue(initialFormData) - setObjectData((prev) => mergeFormData(prev, initialFormData)) - return validateForm() + const initialData = mergeFormData(defaultValues || {}) + + const computedEntries = calculateComputedValues(initialData, model) + + const initialFormData = applyComputedEntries(initialData, computedEntries) + + const nextObjectData = { + ...initialFormData, + _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]) // Validate form on change @@ -112,23 +165,34 @@ const NewObjectForm = ({ type, style, defaultValues = {}, children }) => { const handleSubmit = async () => { try { setSubmitLoading(true) + const currentFormValues = form.getFieldsValue() + const currentFormData = mergeFormData(objectData || {}, currentFormValues) + const computedEntries = calculateComputedValues(currentFormData, model) + const computedObjectData = applyComputedEntries( currentFormData, computedEntries ) + const payload = stripNestedObjectProperties(computedObjectData, model) + const newObject = await createObject(type, payload) + showSuccess('Object created successfully') + return newObject } catch (err) { console.error(err) + if (err.errorFields) { return } + showMessageError('Failed to create object') + showError( `Failed to create object. Message: ${err.message}. Code: ${err.code}`, () => handleSubmit() @@ -146,12 +210,17 @@ const NewObjectForm = ({ type, style, defaultValues = {}, children }) => { disabled={submitLoading} onValuesChange={(_changedValues, allFormValues) => { const currentFormData = mergeFormData(objectData || {}, allFormValues) + const computedEntries = calculateComputedValues(currentFormData, model) if (Array.isArray(computedEntries) && computedEntries.length > 0) { computedEntries.forEach(({ namePath, value }) => { - if (!Array.isArray(namePath) || value === undefined) return + if (!Array.isArray(namePath) || value === undefined) { + return + } + const currentValue = form.getFieldValue(namePath) + if (currentValue !== value) { if (typeof form.setFieldValue === 'function') { form.setFieldValue(namePath, value) @@ -159,6 +228,7 @@ const NewObjectForm = ({ type, style, defaultValues = {}, children }) => { const fallbackPayload = buildObjectFromEntries([ { namePath, value } ]) + form.setFieldsValue(fallbackPayload) } } @@ -166,6 +236,9 @@ const NewObjectForm = ({ type, style, defaultValues = {}, children }) => { } const allValues = applyComputedEntries(allFormValues, computedEntries) + + allValues._isEditing = true + setObjectData((prev) => mergeFormData(prev, allValues)) }} > @@ -185,8 +258,11 @@ const NewObjectForm = ({ type, style, defaultValues = {}, children }) => { NewObjectForm.propTypes = { type: PropTypes.string.isRequired, + children: PropTypes.func.isRequired, + style: PropTypes.object, + defaultValues: PropTypes.object }