Tom Butcher 2feba8b3cc Enhance NewObjectButtons, NewObjectForm, and related components with improved loading and disabled state handling
- Updated NewObjectButtons to disable controls based on loading state and current step.
- Refactored NewObjectForm to integrate computed entries calculation and manage loading state more effectively.
- Enhanced ObjectProperty and WizardView components to respect the disabled state during form interactions.
- Introduced new utility functions for calculating model computed entries, improving data handling and clarity.
2026-09-01 17:11:07 +01:00

194 lines
6.0 KiB
JavaScript

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
}, {})
}
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 }) => {
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 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 model = getModelByName(type)
const calculateComputedValues = useCallback(
(currentData, modelDefinition, options = {}) => {
return calculateModelComputedEntries(currentData, modelDefinition, options)
},
[]
)
// Set initial form values when defaultValues change
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()
}
}, [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 (
<Form
form={form}
layout='vertical'
style={style}
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
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)
}
}
})
}
const allValues = applyComputedEntries(allFormValues, computedEntries)
setObjectData((prev) => mergeFormData(prev, allValues))
}}
>
{children({
loading: submitLoading,
submitLoading,
disabled: 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