farmcontrol-ui/src/components/Dashboard/utils/objectPropertyRefs.js
Tom Butcher 82d76db106
All checks were successful
farmcontrol/farmcontrol-ui/pipeline/head This commit looks good
Enhance DeployJob and NewObjectForm Components with Object Hydration and State Management
- Updated DeployJob to utilize useRef for job state management and improved object hydration logic.
- Introduced utility functions for managing object property references and hydration in object forms.
- Enhanced NewObjectForm to synchronize form values with fetched object data, ensuring real-time updates.
- Added new properties to Job and SubJob models for better data representation and filtering capabilities.
2026-11-08 23:41:04 +00:00

241 lines
6.3 KiB
JavaScript

import get from 'lodash/get'
import set from 'lodash/set'
import cloneDeep from 'lodash/cloneDeep'
import { mergeFormData } from './Utils'
// Patch computed fields onto the complete object.
// This mirrors the handling used by ObjectForm.
export 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
}
const idsEqual = (left, right) =>
left != null &&
right != null &&
String(left).toLowerCase() === String(right).toLowerCase()
export const getReferenceId = (value) => {
if (typeof value === 'string') {
const trimmed = value.trim()
return trimmed || null
}
if (!value || typeof value !== 'object' || Array.isArray(value)) return null
if (typeof value._id === 'string' && value._id.trim()) return value._id
if (typeof value._id?._id === 'string' && value._id._id.trim()) {
return value._id._id
}
return null
}
// A reference is hydrated when it has fields besides _id.
// API object ids often arrive as { _id } until they are fetched.
const isHydratedObject = (value) => {
if (!value || typeof value !== 'object' || Array.isArray(value)) return false
return Object.keys(value).some((key) => key !== '_id')
}
const toNamePath = (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 resolvePropertyObjectType = (property, scopeData, parentData) => {
if (typeof property?.objectType !== 'function') {
return property?.objectType || null
}
try {
return property.objectType(scopeData || {}, parentData) || null
} catch (error) {
console.warn(
`Error resolving object type for property ${property?.name}:`,
error
)
return null
}
}
export const collectObjectPropertyRefs = (data, modelDefinition) => {
const refs = []
if (!data || typeof data !== 'object') return refs
const visit = (properties, parentPath, scopeData, parentData) => {
if (!Array.isArray(properties)) return
properties.forEach((property) => {
if (!property?.name) return
const propertyPath = toNamePath(property.name, parentPath)
const value = get(data, propertyPath)
if (property.type === 'object' || property.type === 'objectList') {
const objectType = resolvePropertyObjectType(
property,
scopeData,
parentData
)
if (!objectType) return
const items =
property.type === 'objectList'
? Array.isArray(value)
? value
: []
: value == null
? []
: [value]
items.forEach((item, index) => {
const id = getReferenceId(item)
if (!id) return
refs.push({
namePath:
property.type === 'objectList'
? [...propertyPath, index]
: propertyPath,
id: String(id),
objectType,
hydrated: isHydratedObject(item)
})
})
return
}
if (
!Array.isArray(property.properties) ||
property.properties.length === 0
) {
return
}
if (property.type === 'objectChildren' && Array.isArray(value)) {
value.forEach((child, index) => {
visit(
property.properties,
[...propertyPath, index],
child,
scopeData
)
})
return
}
if (value && typeof value === 'object' && !Array.isArray(value)) {
visit(property.properties, propertyPath, value, scopeData)
}
})
}
visit(modelDefinition?.properties, [], data, null)
return refs
}
export const getRelatedObjectSubscriptionKey = (data, modelDefinition) => {
const seen = new Set()
collectObjectPropertyRefs(data, modelDefinition).forEach((ref) => {
seen.add(`${ref.objectType}:${ref.id.toLowerCase()}`)
})
return Array.from(seen).sort().join('|')
}
const mergeRelatedObjectValue = (existing, id, value) => {
const base =
existing && typeof existing === 'object' && !Array.isArray(existing)
? existing
: { _id: id }
return mergeFormData(base, value, { _id: id })
}
const withEditingFlag = (prev, next) => {
if (prev?._isEditing != null) {
next._isEditing = prev._isEditing
}
return next
}
export const applyHydratedObjects = (
prev,
refs,
fetchedByKey,
modelDefinition,
calculateComputedValues
) => {
if (!prev || refs.length === 0 || fetchedByKey.size === 0) return null
const draft = cloneDeep(prev)
const applied = []
refs.forEach((ref) => {
const fullObject = fetchedByKey.get(
`${ref.objectType}:${ref.id.toLowerCase()}`
)
if (!fullObject) return
const current = get(draft, ref.namePath)
const currentId = getReferenceId(current)
if (!idsEqual(currentId, ref.id) || isHydratedObject(current)) return
set(
draft,
ref.namePath,
mergeRelatedObjectValue(current, ref.id, fullObject)
)
applied.push(ref)
})
if (applied.length === 0) return null
const computedEntries = calculateComputedValues(draft, modelDefinition)
const next = withEditingFlag(
prev,
applyComputedEntries(draft, computedEntries)
)
return { next, refs: applied, computedEntries }
}
export const applyRelatedObjectUpdates = (
prev,
objectType,
id,
value,
modelDefinition,
calculateComputedValues
) => {
if (!prev || !objectType || id == null || value == null) return null
const normalizedId = String(id).toLowerCase()
const refs = collectObjectPropertyRefs(prev, modelDefinition).filter(
(ref) =>
ref.objectType === objectType && ref.id.toLowerCase() === normalizedId
)
if (refs.length === 0) return null
const draft = cloneDeep(prev)
refs.forEach((ref) => {
set(
draft,
ref.namePath,
mergeRelatedObjectValue(get(draft, ref.namePath), ref.id, value)
)
})
const computedEntries = calculateComputedValues(draft, modelDefinition)
const next = withEditingFlag(
prev,
applyComputedEntries(draft, computedEntries)
)
return { next, refs, computedEntries }
}