import { useState, useEffect, useContext, useCallback, useRef } from 'react' import { Form } from 'antd' import { ApiServerContext } from '../context/ApiServerContext' import { useMessageContext } from '../context/MessageContext' import PropTypes from 'prop-types' import set from 'lodash/set' import { getModelByName } from '../../../database/ObjectModels' import { mergeFormData, stripNestedObjectProperties, calculateModelComputedEntries } from '../utils/Utils' 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: * - type: string (required) * - formItems: array (for ObjectInfo/ObjectProperty items) * - defaultValues: object (optional) - initial values for the form * - children: function({ * loading, submitLoading, disabled, handleSubmit, form, formValid, objectData, setObjectData * }) => 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({ _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 }) .then(() => { if (!cancelled && validationRun === validationRunRef.current) { setFormValid(true) } }) .catch(() => { if (!cancelled && validationRun === validationRunRef.current) { setFormValid(false) } }) }, 0) return () => { cancelled = true clearTimeout(timeoutId) } }, [form]) const calculateComputedValues = useCallback( (currentData, modelDefinition, options = {}) => { return calculateModelComputedEntries( currentData, modelDefinition, options ) }, [] ) /* * 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(() => { 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 useEffect(() => { return validateForm() }, [validateForm, formUpdateValues]) 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() ) } finally { setSubmitLoading(false) } } return (
) } NewObjectForm.propTypes = { type: PropTypes.string.isRequired, children: PropTypes.func.isRequired, style: PropTypes.object, defaultValues: PropTypes.object } export default NewObjectForm