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.
This commit is contained in:
Tom Butcher 2026-09-01 17:11:07 +01:00
parent a41f168adc
commit 2feba8b3cc
5 changed files with 156 additions and 95 deletions

View File

@ -12,13 +12,15 @@ const NewObjectButtons = ({
submitText = 'Done', submitText = 'Done',
disabled = false disabled = false
}) => { }) => {
const controlsDisabled = disabled || submitLoading
return ( return (
<Flex justify='end'> <Flex justify='end'>
{totalSteps > 1 ? ( {totalSteps > 1 ? (
<Button <Button
style={{ margin: '0 8px' }} style={{ margin: '0 8px' }}
onClick={onPrevious} onClick={onPrevious}
disabled={currentStep === 0} disabled={currentStep === 0 || controlsDisabled}
> >
Previous Previous
</Button> </Button>
@ -27,7 +29,7 @@ const NewObjectButtons = ({
{currentStep < totalSteps - 1 ? ( {currentStep < totalSteps - 1 ? (
<Button <Button
type='primary' type='primary'
disabled={!formValid || disabled} disabled={!formValid || controlsDisabled}
onClick={onNext} onClick={onNext}
> >
Next Next
@ -36,7 +38,7 @@ const NewObjectButtons = ({
<Button <Button
type='primary' type='primary'
loading={submitLoading} loading={submitLoading}
disabled={!formValid || disabled} disabled={!formValid || controlsDisabled}
onClick={onSubmit} onClick={onSubmit}
> >
{submitText} {submitText}

View File

@ -5,7 +5,11 @@ import { useMessageContext } from '../context/MessageContext'
import PropTypes from 'prop-types' import PropTypes from 'prop-types'
import set from 'lodash/set' import set from 'lodash/set'
import { getModelByName } from '../../../database/ObjectModels' import { getModelByName } from '../../../database/ObjectModels'
import { mergeFormData, stripNestedObjectProperties } from '../utils/Utils' import {
mergeFormData,
stripNestedObjectProperties,
calculateModelComputedEntries
} from '../utils/Utils'
const buildObjectFromEntries = (entries = []) => { const buildObjectFromEntries = (entries = []) => {
return entries.reduce((acc, entry) => { return entries.reduce((acc, entry) => {
@ -37,7 +41,7 @@ const applyComputedEntries = (base, entries = []) => {
* - formItems: array (for ObjectInfo/ObjectProperty items) * - formItems: array (for ObjectInfo/ObjectProperty items)
* - defaultValues: object (optional) - initial values for the form * - defaultValues: object (optional) - initial values for the form
* - children: function({ * - children: function({
* loading, isSubmitting, handleSubmit, form, formValid, objectData, setObjectData * loading, submitLoading, disabled, handleSubmit, form, formValid, objectData, setObjectData
* }) => ReactNode * }) => ReactNode
*/ */
const NewObjectForm = ({ type, style, defaultValues = {}, children }) => { const NewObjectForm = ({ type, style, defaultValues = {}, children }) => {
@ -77,93 +81,11 @@ const NewObjectForm = ({ type, style, defaultValues = {}, children }) => {
} }
}, [form]) }, [form])
// Get the model definition for this object type
const model = getModelByName(type) const model = getModelByName(type)
// Function to calculate computed values from model properties
const calculateComputedValues = useCallback( const calculateComputedValues = useCallback(
(currentData, modelDefinition) => { (currentData, modelDefinition, options = {}) => {
if (!modelDefinition || !Array.isArray(modelDefinition.properties)) { return calculateModelComputedEntries(currentData, modelDefinition, options)
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
}, },
[] []
) )
@ -171,7 +93,6 @@ const NewObjectForm = ({ type, style, defaultValues = {}, children }) => {
// Set initial form values when defaultValues change // Set initial form values when defaultValues change
useEffect(() => { useEffect(() => {
if (Object.keys(defaultValues).length > 0) { if (Object.keys(defaultValues).length > 0) {
// Calculate computed values for initial data
const computedEntries = calculateComputedValues(defaultValues, model) const computedEntries = calculateComputedValues(defaultValues, model)
const initialFormData = applyComputedEntries( const initialFormData = applyComputedEntries(
defaultValues, defaultValues,
@ -191,9 +112,11 @@ const NewObjectForm = ({ type, style, defaultValues = {}, children }) => {
const handleSubmit = async () => { const handleSubmit = async () => {
try { try {
setSubmitLoading(true) setSubmitLoading(true)
const computedEntries = calculateComputedValues(objectData, model) const currentFormValues = form.getFieldsValue()
const currentFormData = mergeFormData(objectData || {}, currentFormValues)
const computedEntries = calculateComputedValues(currentFormData, model)
const computedObjectData = applyComputedEntries( const computedObjectData = applyComputedEntries(
objectData, currentFormData,
computedEntries computedEntries
) )
const payload = stripNestedObjectProperties(computedObjectData, model) const payload = stripNestedObjectProperties(computedObjectData, model)
@ -220,8 +143,8 @@ const NewObjectForm = ({ type, style, defaultValues = {}, children }) => {
form={form} form={form}
layout='vertical' layout='vertical'
style={style} style={style}
disabled={submitLoading}
onValuesChange={(_changedValues, allFormValues) => { onValuesChange={(_changedValues, allFormValues) => {
// Calculate computed values based on current form data
const currentFormData = mergeFormData(objectData || {}, allFormValues) const currentFormData = mergeFormData(objectData || {}, allFormValues)
const computedEntries = calculateComputedValues(currentFormData, model) const computedEntries = calculateComputedValues(currentFormData, model)
@ -247,7 +170,9 @@ const NewObjectForm = ({ type, style, defaultValues = {}, children }) => {
}} }}
> >
{children({ {children({
loading: submitLoading,
submitLoading, submitLoading,
disabled: submitLoading,
handleSubmit, handleSubmit,
form, form,
formValid, formValid,

View File

@ -814,7 +814,7 @@ const ObjectProperty = ({
} }
const inputProps = useFormItem const inputProps = useFormItem
? {} ? { disabled }
: { : {
value, value,
onChange, onChange,

View File

@ -195,7 +195,7 @@ const WizardView = ({
{showButtons && ( {showButtons && (
<NewObjectButtons <NewObjectButtons
disabled={disabled} disabled={disabled || loading}
currentStep={currentStep} currentStep={currentStep}
totalSteps={steps.length} totalSteps={steps.length}
onPrevious={() => setCurrentStep(currentStep - 1)} onPrevious={() => setCurrentStep(currentStep - 1)}

View File

@ -144,3 +144,137 @@ const stripProperties = (data, properties) => {
export function stripNestedObjectProperties(data, modelDefinition) { export function stripNestedObjectProperties(data, modelDefinition) {
return stripProperties(data, modelDefinition?.properties) return stripProperties(data, modelDefinition?.properties)
} }
const collectModelPropertyPaths = (modelDefinition) => {
const paths = []
const visit = (properties) => {
;(properties || []).forEach((property) => {
if (property?.name) {
paths.push(property.name)
}
if (Array.isArray(property?.properties) && property.properties.length > 0) {
visit(property.properties)
}
})
}
visit(modelDefinition?.properties)
return paths
}
const pathToString = (path) =>
Array.isArray(path) ? path.join('.') : String(path)
// Computed display fields (e.g. deviceInfo.cpu) must not replace parent objects
// when sibling nested properties exist (e.g. deviceInfo.cpu.model).
const hasNestedPropertyPaths = (propertyPath, allPropertyPaths) => {
const pathStr = pathToString(propertyPath)
const prefix = `${pathStr}.`
return allPropertyPaths.some(
(name) => name !== pathStr && name.startsWith(prefix)
)
}
export function calculateModelComputedEntries(
currentData,
modelDefinition,
options = {}
) {
if (!modelDefinition || !Array.isArray(modelDefinition.properties)) {
return []
}
const skipObjectChildrenValue = options.skipObjectChildrenValue === true
const workingData = mergeFormData(currentData)
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 allPropertyPaths = collectModelPropertyPaths(modelDefinition)
const processProperty = (property, parentPath = []) => {
if (!property?.name) return
const propertyPath = normalizedPath(property.name, parentPath)
const scopeData =
parentPath.length === 0
? workingData
: getValueAtPath(workingData, parentPath)
if (property.value && typeof property.value === 'function') {
const skipValue =
skipObjectChildrenValue && property.type === 'objectChildren'
if (!skipValue) {
try {
const computedValue = property.value(scopeData || {})
if (computedValue !== undefined) {
const preserveNestedObject = hasNestedPropertyPaths(
propertyPath,
allPropertyPaths
)
if (!preserveNestedObject) {
computedEntries.push({
namePath: propertyPath,
value: computedValue
})
set(workingData, propertyPath, 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(workingData, propertyPath)
if (Array.isArray(childValues)) {
childValues.forEach((_, index) => {
property.properties.forEach((childProperty) => {
processProperty(childProperty, [...propertyPath, index])
})
})
}
} else {
property.properties.forEach((childProperty) => {
processProperty(childProperty, propertyPath)
})
}
}
}
modelDefinition.properties.forEach((property) => {
processProperty(property, [])
})
return computedEntries
}