All checks were successful
farmcontrol/farmcontrol-ui/pipeline/head This commit looks good
- 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.
241 lines
6.3 KiB
JavaScript
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 }
|
|
}
|