229 lines
7.3 KiB
JavaScript

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 (
<Form
form={form}
layout='vertical'
style={style}
onValuesChange={(_changedValues, allFormValues) => {
// Calculate computed values based on current form data
const currentFormData = merge({}, 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
const currentValue = form.getFieldValue(namePath)
if (currentValue !== value) {
if (typeof form.setFieldValue === 'function') {
form.setFieldValue(namePath, value)
} else {
const fallbackPayload = buildObjectFromEntries([
{ namePath, value }
])
form.setFieldsValue(fallbackPayload)
}
}
})
}
// Merge all values (user input + computed values)
const computedValuesObject = buildObjectFromEntries(computedEntries)
const allValues = merge({}, allFormValues, computedValuesObject)
setObjectData((prev) => {
return merge({}, prev, allValues)
})
}}
>
{contextHolder}
{children({
submitLoading: submitLoading,
handleSubmit,
form,
formValid,
objectData,
setObjectData
})}
</Form>
)
}
NewObjectForm.propTypes = {
type: PropTypes.string.isRequired,
children: PropTypes.func.isRequired,
style: PropTypes.object,
defaultValues: PropTypes.object
}
export default NewObjectForm