import { useState, useEffect, useContext, useCallback } from 'react' import { Form, message } from 'antd' import { ApiServerContext } from '../context/ApiServerContext' import PropTypes from 'prop-types' import merge from 'lodash/merge' import set from 'lodash/set' import { getModelByName } from '../../../database/ObjectModels' 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 }, {}) } /** * 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, isSubmitting, handleSubmit, form, formValid, objectData, setObjectData * }) => ReactNode */ const NewObjectForm = ({ type, style, defaultValues = {}, children }) => { const [objectData, setObjectData] = useState(defaultValues) const [submitLoading, setSubmitLoading] = useState(false) const [formValid, setFormValid] = useState(false) const [form] = Form.useForm() const formUpdateValues = Form.useWatch([], form) const [messageApi, contextHolder] = message.useMessage() const { createObject, showError } = useContext(ApiServerContext) // Get the model definition for this object type const model = getModelByName(type) // Function to calculate computed values from model properties const calculateComputedValues = useCallback( (currentData, modelDefinition) => { if (!modelDefinition || !Array.isArray(modelDefinition.properties)) { return [] } const normalizedPath = (name, parentPath = []) => { if (Array.isArray(name)) { return [...parentPath, ...name] } if (typeof name === 'number') { return [...parentPath, name] } if (typeof name === 'string' && name.length > 0) { return [...parentPath, ...name.split('.')] } return parentPath } const getValueAtPath = (dataSource, path) => { if (!Array.isArray(path) || path.length === 0) { return dataSource } return path.reduce((acc, key) => { if (acc == null) return acc return acc[key] }, dataSource) } const computedEntries = [] const processProperty = (property, scopeData, parentPath = []) => { if (!property?.name) return const propertyPath = normalizedPath(property.name, parentPath) if (property.value && typeof property.value === 'function') { try { const computedValue = property.value(scopeData || {}) if (computedValue !== undefined) { computedEntries.push({ namePath: propertyPath, value: computedValue }) } } catch (error) { console.warn( `Error calculating value for property ${property.name}:`, error ) } } if ( Array.isArray(property.properties) && property.properties.length > 0 ) { if (property.type === 'objectChildren') { const childValues = getValueAtPath(currentData, propertyPath) if (Array.isArray(childValues)) { childValues.forEach((childData = {}, index) => { property.properties.forEach((childProperty) => { processProperty(childProperty, childData || {}, [ ...propertyPath, index ]) }) }) } } else { const nestedScope = getValueAtPath(currentData, propertyPath) || {} property.properties.forEach((childProperty) => { processProperty(childProperty, nestedScope || {}, propertyPath) }) } } } modelDefinition.properties.forEach((property) => { processProperty(property, currentData) }) return computedEntries }, [] ) // Set initial form values when defaultValues change useEffect(() => { if (Object.keys(defaultValues).length > 0) { // Calculate computed values for initial data const computedEntries = calculateComputedValues(defaultValues, model) const computedValuesObject = buildObjectFromEntries(computedEntries) const initialFormData = merge({}, defaultValues, computedValuesObject) form.setFieldsValue(initialFormData) setObjectData((prev) => merge({}, prev, initialFormData)) } }, [form, defaultValues, calculateComputedValues, model]) // Validate form on change useEffect(() => { form .validateFields({ validateOnly: true }) .then(() => setFormValid(true)) .catch(() => setFormValid(false)) }, [form, formUpdateValues]) const handleSubmit = async () => { try { setSubmitLoading(true) const newObject = await createObject(type, objectData) messageApi.success('Object created successfully') return newObject } catch (err) { console.error(err) if (err.errorFields) { return } messageApi.error('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