Enhance DeployJob and NewObjectForm Components with Object Hydration and State Management
All checks were successful
farmcontrol/farmcontrol-ui/pipeline/head This commit looks good
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.
This commit is contained in:
parent
b65fe208c0
commit
82d76db106
@ -1,4 +1,4 @@
|
||||
import { useState, useContext, useEffect, useCallback } from 'react'
|
||||
import { useState, useContext, useEffect, useCallback, useRef } from 'react'
|
||||
import PropTypes from 'prop-types'
|
||||
import WizardView from '../../common/WizardView'
|
||||
import { ApiServerContext } from '../../context/ApiServerContext'
|
||||
@ -6,7 +6,17 @@ import { Descriptions, Flex, Typography } from 'antd'
|
||||
import ObjectTable from '../../common/ObjectTable'
|
||||
import { getModelByName } from '../../../../database/ObjectModels.js'
|
||||
import ObjectProperty from '../../common/ObjectProperty'
|
||||
import { calculateModelComputedEntries } from '../../utils/Utils'
|
||||
import {
|
||||
applyHydratedObjects,
|
||||
applyRelatedObjectUpdates,
|
||||
collectObjectPropertyRefs,
|
||||
getReferenceId,
|
||||
getRelatedObjectSubscriptionKey
|
||||
} from '../../utils/objectPropertyRefs'
|
||||
|
||||
const { Text } = Typography
|
||||
|
||||
const DeployJob = ({
|
||||
onOk,
|
||||
objectData = undefined,
|
||||
@ -14,12 +24,45 @@ const DeployJob = ({
|
||||
}) => {
|
||||
const [deployLoading, setDeployLoading] = useState(false)
|
||||
const [job, setJob] = useState(objectData)
|
||||
const jobRef = useRef(job)
|
||||
const [deployedSubJobsCount, setDeployedSubJobsCount] = useState(0)
|
||||
const [subJobsCount, setSubJobsCount] = useState(999)
|
||||
const [allPrintersOnline, setAllPrintersOnline] = useState(false)
|
||||
const { sendObjectAction, fetchObjects } = useContext(ApiServerContext)
|
||||
const {
|
||||
sendObjectAction,
|
||||
fetchObjects,
|
||||
fetchObject,
|
||||
subscribeToObjectUpdates,
|
||||
connected
|
||||
} = useContext(ApiServerContext)
|
||||
const fetchObjectRef = useRef(fetchObject)
|
||||
fetchObjectRef.current = fetchObject
|
||||
|
||||
const model = getModelByName('job')
|
||||
const objectDataId = getReferenceId(objectData)
|
||||
const objectDataRef = useRef(objectData)
|
||||
objectDataRef.current = objectData
|
||||
|
||||
const commitJob = useCallback((value) => {
|
||||
const next = typeof value === 'function' ? value(jobRef.current) : value
|
||||
jobRef.current = next
|
||||
setJob(next)
|
||||
}, [])
|
||||
|
||||
const calculateComputedValues = useCallback(
|
||||
(currentData, modelDefinition, options) => {
|
||||
return calculateModelComputedEntries(
|
||||
currentData,
|
||||
modelDefinition,
|
||||
options
|
||||
)
|
||||
},
|
||||
[]
|
||||
)
|
||||
|
||||
const handleDeploy = useCallback(async () => {
|
||||
if (!job?._id) return
|
||||
|
||||
setDeployLoading(true)
|
||||
var hasMore = true
|
||||
var currentPage = 1
|
||||
@ -47,7 +90,7 @@ const DeployJob = ({
|
||||
}
|
||||
)
|
||||
})
|
||||
}, [job._id, sendObjectAction, fetchObjects])
|
||||
}, [job?._id, sendObjectAction, fetchObjects])
|
||||
|
||||
useEffect(() => {
|
||||
if (deployedSubJobsCount == subJobsCount && deployLoading == true) {
|
||||
@ -56,8 +99,110 @@ const DeployJob = ({
|
||||
}, [deployedSubJobsCount, subJobsCount, deployLoading, onOk])
|
||||
|
||||
useEffect(() => {
|
||||
if (objectData?.printers) {
|
||||
if (objectData.printers.every((printer) => printer.online)) {
|
||||
commitJob(objectDataRef.current)
|
||||
}, [objectDataId, commitJob])
|
||||
|
||||
const hydrationKey = collectObjectPropertyRefs(job, model)
|
||||
.filter((ref) => !ref.hydrated)
|
||||
.map((ref) => `${ref.objectType}:${ref.id.toLowerCase()}`)
|
||||
.sort()
|
||||
.join('|')
|
||||
|
||||
useEffect(() => {
|
||||
if (!hydrationKey) return undefined
|
||||
|
||||
const refs = collectObjectPropertyRefs(jobRef.current, model).filter(
|
||||
(ref) => !ref.hydrated
|
||||
)
|
||||
if (refs.length === 0) return undefined
|
||||
|
||||
let cancelled = false
|
||||
const seen = new Set()
|
||||
const uniqueRefs = []
|
||||
refs.forEach((ref) => {
|
||||
const key = `${ref.objectType}:${ref.id.toLowerCase()}`
|
||||
if (seen.has(key)) return
|
||||
seen.add(key)
|
||||
uniqueRefs.push(ref)
|
||||
})
|
||||
|
||||
const hydrate = async () => {
|
||||
const fetchedByKey = new Map()
|
||||
|
||||
await Promise.all(
|
||||
uniqueRefs.map(async (ref) => {
|
||||
const fullObject = await fetchObjectRef.current(
|
||||
ref.id,
|
||||
ref.objectType
|
||||
)
|
||||
if (!getReferenceId(fullObject)) return
|
||||
fetchedByKey.set(
|
||||
`${ref.objectType}:${ref.id.toLowerCase()}`,
|
||||
fullObject
|
||||
)
|
||||
})
|
||||
)
|
||||
|
||||
if (cancelled) return
|
||||
|
||||
const applied = applyHydratedObjects(
|
||||
jobRef.current,
|
||||
refs,
|
||||
fetchedByKey,
|
||||
model,
|
||||
calculateComputedValues
|
||||
)
|
||||
if (!applied) return
|
||||
|
||||
commitJob(applied.next)
|
||||
}
|
||||
|
||||
hydrate()
|
||||
|
||||
return () => {
|
||||
cancelled = true
|
||||
}
|
||||
}, [hydrationKey, model, calculateComputedValues, commitJob])
|
||||
|
||||
const relatedObjectKey = getRelatedObjectSubscriptionKey(job, model)
|
||||
|
||||
useEffect(() => {
|
||||
if (connected !== true || !relatedObjectKey) return undefined
|
||||
|
||||
const unsubscribes = relatedObjectKey.split('|').map((entry) => {
|
||||
const separator = entry.indexOf(':')
|
||||
const objectType = entry.slice(0, separator)
|
||||
const id = entry.slice(separator + 1)
|
||||
|
||||
return subscribeToObjectUpdates(id, objectType, (value) => {
|
||||
const applied = applyRelatedObjectUpdates(
|
||||
jobRef.current,
|
||||
objectType,
|
||||
id,
|
||||
value,
|
||||
model,
|
||||
calculateComputedValues
|
||||
)
|
||||
if (!applied) return
|
||||
commitJob(applied.next)
|
||||
})
|
||||
})
|
||||
|
||||
return () => {
|
||||
unsubscribes.forEach((unsubscribe) => unsubscribe?.())
|
||||
}
|
||||
}, [
|
||||
connected,
|
||||
relatedObjectKey,
|
||||
subscribeToObjectUpdates,
|
||||
model,
|
||||
calculateComputedValues,
|
||||
commitJob
|
||||
])
|
||||
|
||||
useEffect(() => {
|
||||
if (job?.printers) {
|
||||
if (job.printers.every((printer) => printer.online)) {
|
||||
setAllPrintersOnline(true)
|
||||
} else {
|
||||
setAllPrintersOnline(false)
|
||||
@ -65,7 +210,7 @@ const DeployJob = ({
|
||||
} else {
|
||||
setAllPrintersOnline(false)
|
||||
}
|
||||
}, [objectData])
|
||||
}, [job])
|
||||
|
||||
const steps = [
|
||||
{
|
||||
@ -98,13 +243,13 @@ const DeployJob = ({
|
||||
<ObjectProperty
|
||||
type={'object'}
|
||||
name={'job'}
|
||||
objectData={{ job: objectData }}
|
||||
objectData={{ job }}
|
||||
value={(object) => object.job}
|
||||
readOnly={true}
|
||||
isEditing={true}
|
||||
objectType={'job'}
|
||||
onChange={(newJob) => {
|
||||
setJob(newJob)
|
||||
commitJob(newJob)
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
@ -131,7 +276,7 @@ const DeployJob = ({
|
||||
<ObjectProperty
|
||||
type={'objectList'}
|
||||
name={'printers'}
|
||||
objectData={{ printers: objectData.printers }}
|
||||
objectData={{ printers: job?.printers }}
|
||||
value={(object) => object.printers}
|
||||
readOnly={true}
|
||||
isEditing={true}
|
||||
@ -151,7 +296,9 @@ const DeployJob = ({
|
||||
printer: false,
|
||||
job: false
|
||||
}}
|
||||
masterFilter={{ job: getModelByName('job').prefix + ':' + job?._id }}
|
||||
masterFilter={{
|
||||
job: getModelByName('job').prefix + ':' + job?._id
|
||||
}}
|
||||
size={'small'}
|
||||
showActions={!slicerIntegration}
|
||||
/>
|
||||
@ -164,7 +311,7 @@ const DeployJob = ({
|
||||
<WizardView
|
||||
steps={steps}
|
||||
loading={deployLoading}
|
||||
formValid={objectData != undefined && allPrintersOnline}
|
||||
formValid={job != undefined && allPrintersOnline}
|
||||
title='Deploy Job'
|
||||
showSteps={false}
|
||||
showSkipButton={slicerIntegration}
|
||||
|
||||
@ -50,6 +50,7 @@ const NewJob = ({
|
||||
visibleProperties={{
|
||||
_id: false,
|
||||
_reference: false,
|
||||
name: false,
|
||||
createdAt: false,
|
||||
updatedAt: false,
|
||||
startedAt: false,
|
||||
|
||||
@ -3,6 +3,7 @@ import { Form } from 'antd'
|
||||
import { ApiServerContext } from '../context/ApiServerContext'
|
||||
import { useMessageContext } from '../context/MessageContext'
|
||||
import PropTypes from 'prop-types'
|
||||
import get from 'lodash/get'
|
||||
import set from 'lodash/set'
|
||||
import { getModelByName } from '../../../database/ObjectModels'
|
||||
|
||||
@ -11,6 +12,14 @@ import {
|
||||
stripNestedObjectProperties,
|
||||
calculateModelComputedEntries
|
||||
} from '../utils/Utils'
|
||||
import {
|
||||
applyComputedEntries,
|
||||
applyHydratedObjects,
|
||||
applyRelatedObjectUpdates,
|
||||
collectObjectPropertyRefs,
|
||||
getReferenceId,
|
||||
getRelatedObjectSubscriptionKey
|
||||
} from '../utils/objectPropertyRefs'
|
||||
|
||||
const EMPTY_DEFAULT_VALUES = {}
|
||||
|
||||
@ -36,18 +45,17 @@ const buildObjectFromEntries = (entries = []) => {
|
||||
}, {})
|
||||
}
|
||||
|
||||
// Patch computed fields onto the complete object.
|
||||
// This mirrors the handling used by ObjectForm.
|
||||
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)
|
||||
const syncFormValues = (form, data, refs = [], computedEntries = []) => {
|
||||
refs.forEach((ref) => {
|
||||
form.setFieldValue(ref.namePath, get(data, ref.namePath))
|
||||
})
|
||||
|
||||
return result
|
||||
computedEntries.forEach(({ namePath, value }) => {
|
||||
if (!Array.isArray(namePath) || value === undefined) return
|
||||
if (form.getFieldValue(namePath) !== value) {
|
||||
form.setFieldValue(namePath, value)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
@ -75,10 +83,18 @@ const NewObjectForm = ({
|
||||
const [objectData, setObjectData] = useState({
|
||||
_isEditing: true
|
||||
})
|
||||
const objectDataRef = useRef(objectData)
|
||||
const defaultValuesKey = serializeDefaultValues(defaultValues)
|
||||
const defaultValuesRef = useRef(defaultValues)
|
||||
defaultValuesRef.current = defaultValues
|
||||
|
||||
const commitObjectData = useCallback((value) => {
|
||||
const next =
|
||||
typeof value === 'function' ? value(objectDataRef.current) : value
|
||||
objectDataRef.current = next
|
||||
setObjectData(next)
|
||||
}, [])
|
||||
|
||||
const [submitLoading, setSubmitLoading] = useState(false)
|
||||
const [formValid, setFormValid] = useState(false)
|
||||
|
||||
@ -90,7 +106,15 @@ const NewObjectForm = ({
|
||||
|
||||
const { showSuccess, showError: showMessageError } = useMessageContext()
|
||||
|
||||
const { createObject, showError } = useContext(ApiServerContext)
|
||||
const {
|
||||
createObject,
|
||||
showError,
|
||||
fetchObject,
|
||||
subscribeToObjectUpdates,
|
||||
connected
|
||||
} = useContext(ApiServerContext)
|
||||
const fetchObjectRef = useRef(fetchObject)
|
||||
fetchObjectRef.current = fetchObject
|
||||
|
||||
const model = getModelByName(type)
|
||||
|
||||
@ -170,10 +194,126 @@ const NewObjectForm = ({
|
||||
form.setFieldsValue(initialFormData)
|
||||
|
||||
// Keep objectData in sync with exactly the same initial data.
|
||||
setObjectData(nextObjectData)
|
||||
commitObjectData(nextObjectData)
|
||||
|
||||
return validateForm()
|
||||
}, [form, defaultValuesKey, calculateComputedValues, model, validateForm])
|
||||
}, [
|
||||
form,
|
||||
defaultValuesKey,
|
||||
calculateComputedValues,
|
||||
model,
|
||||
validateForm,
|
||||
commitObjectData
|
||||
])
|
||||
|
||||
/*
|
||||
* Default object and objectList values are often id stubs ({ _id }).
|
||||
* Fetch the full records, then keep them current from object updates
|
||||
* the same way ObjectForm does for the record it is editing.
|
||||
*/
|
||||
useEffect(() => {
|
||||
const refs = collectObjectPropertyRefs(
|
||||
defaultValuesRef.current,
|
||||
model
|
||||
).filter((ref) => !ref.hydrated)
|
||||
|
||||
if (refs.length === 0) return undefined
|
||||
|
||||
let cancelled = false
|
||||
const seen = new Set()
|
||||
const uniqueRefs = []
|
||||
refs.forEach((ref) => {
|
||||
const key = `${ref.objectType}:${ref.id.toLowerCase()}`
|
||||
if (seen.has(key)) return
|
||||
seen.add(key)
|
||||
uniqueRefs.push(ref)
|
||||
})
|
||||
|
||||
const hydrate = async () => {
|
||||
const fetchedByKey = new Map()
|
||||
|
||||
await Promise.all(
|
||||
uniqueRefs.map(async (ref) => {
|
||||
const fullObject = await fetchObjectRef.current(ref.id, ref.objectType)
|
||||
if (!getReferenceId(fullObject)) return
|
||||
fetchedByKey.set(
|
||||
`${ref.objectType}:${ref.id.toLowerCase()}`,
|
||||
fullObject
|
||||
)
|
||||
})
|
||||
)
|
||||
|
||||
if (cancelled) return
|
||||
|
||||
const applied = applyHydratedObjects(
|
||||
objectDataRef.current,
|
||||
refs,
|
||||
fetchedByKey,
|
||||
model,
|
||||
calculateComputedValues
|
||||
)
|
||||
if (!applied) return
|
||||
|
||||
syncFormValues(
|
||||
form,
|
||||
applied.next,
|
||||
applied.refs,
|
||||
applied.computedEntries
|
||||
)
|
||||
commitObjectData(applied.next)
|
||||
}
|
||||
|
||||
hydrate()
|
||||
|
||||
return () => {
|
||||
cancelled = true
|
||||
}
|
||||
}, [defaultValuesKey, model, calculateComputedValues, form, commitObjectData])
|
||||
|
||||
const relatedObjectKey = getRelatedObjectSubscriptionKey(objectData, model)
|
||||
|
||||
// Keep referenced objects current while the form is open.
|
||||
useEffect(() => {
|
||||
if (connected !== true || !relatedObjectKey) return undefined
|
||||
|
||||
const unsubscribes = relatedObjectKey.split('|').map((entry) => {
|
||||
const separator = entry.indexOf(':')
|
||||
const objectType = entry.slice(0, separator)
|
||||
const id = entry.slice(separator + 1)
|
||||
|
||||
return subscribeToObjectUpdates(id, objectType, (value) => {
|
||||
const applied = applyRelatedObjectUpdates(
|
||||
objectDataRef.current,
|
||||
objectType,
|
||||
id,
|
||||
value,
|
||||
model,
|
||||
calculateComputedValues
|
||||
)
|
||||
if (!applied) return
|
||||
|
||||
syncFormValues(
|
||||
form,
|
||||
applied.next,
|
||||
applied.refs,
|
||||
applied.computedEntries
|
||||
)
|
||||
commitObjectData(applied.next)
|
||||
})
|
||||
})
|
||||
|
||||
return () => {
|
||||
unsubscribes.forEach((unsubscribe) => unsubscribe?.())
|
||||
}
|
||||
}, [
|
||||
connected,
|
||||
relatedObjectKey,
|
||||
subscribeToObjectUpdates,
|
||||
model,
|
||||
calculateComputedValues,
|
||||
form,
|
||||
commitObjectData
|
||||
])
|
||||
|
||||
// Validate form on change
|
||||
useEffect(() => {
|
||||
@ -186,7 +326,10 @@ const NewObjectForm = ({
|
||||
|
||||
const currentFormValues = form.getFieldsValue()
|
||||
|
||||
const currentFormData = mergeFormData(objectData || {}, currentFormValues)
|
||||
const currentFormData = mergeFormData(
|
||||
objectDataRef.current || {},
|
||||
currentFormValues
|
||||
)
|
||||
|
||||
const computedEntries = calculateComputedValues(currentFormData, model)
|
||||
|
||||
@ -227,7 +370,10 @@ const NewObjectForm = ({
|
||||
style={style}
|
||||
disabled={submitLoading}
|
||||
onValuesChange={(_changedValues, allFormValues) => {
|
||||
const currentFormData = mergeFormData(objectData || {}, allFormValues)
|
||||
const currentFormData = mergeFormData(
|
||||
objectDataRef.current || {},
|
||||
allFormValues
|
||||
)
|
||||
|
||||
const computedEntries = calculateComputedValues(currentFormData, model)
|
||||
|
||||
@ -257,7 +403,7 @@ const NewObjectForm = ({
|
||||
|
||||
allValues._isEditing = true
|
||||
|
||||
setObjectData((prev) => mergeFormData(prev, allValues))
|
||||
commitObjectData(mergeFormData(objectDataRef.current, allValues))
|
||||
}}
|
||||
>
|
||||
{children({
|
||||
@ -268,7 +414,7 @@ const NewObjectForm = ({
|
||||
form,
|
||||
formValid,
|
||||
objectData,
|
||||
setObjectData
|
||||
setObjectData: commitObjectData
|
||||
})}
|
||||
</Form>
|
||||
)
|
||||
|
||||
240
src/components/Dashboard/utils/objectPropertyRefs.js
Normal file
240
src/components/Dashboard/utils/objectPropertyRefs.js
Normal file
@ -0,0 +1,240 @@
|
||||
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 }
|
||||
}
|
||||
@ -150,6 +150,7 @@ export const Job = {
|
||||
],
|
||||
columns: [
|
||||
'_reference',
|
||||
'name',
|
||||
'quantity',
|
||||
'state',
|
||||
'gcodeFile',
|
||||
@ -159,6 +160,7 @@ export const Job = {
|
||||
'updatedAt'
|
||||
],
|
||||
filters: [
|
||||
'name',
|
||||
'state',
|
||||
'gcodeFile',
|
||||
'quantity',
|
||||
@ -170,6 +172,7 @@ export const Job = {
|
||||
'finishedAt'
|
||||
],
|
||||
sorters: [
|
||||
'name',
|
||||
'createdAt',
|
||||
'state',
|
||||
'quantity',
|
||||
@ -212,6 +215,21 @@ export const Job = {
|
||||
readOnly: true,
|
||||
columnWidth: 200
|
||||
},
|
||||
{
|
||||
name: 'name',
|
||||
label: 'Name',
|
||||
type: 'text',
|
||||
readOnly: true,
|
||||
columnFixed: 'left',
|
||||
columnWidth: 240
|
||||
},
|
||||
{
|
||||
name: 'startedAt',
|
||||
label: 'Started At',
|
||||
type: 'dateTime',
|
||||
readOnly: true,
|
||||
columnWidth: 175
|
||||
},
|
||||
{
|
||||
name: 'state',
|
||||
label: 'State',
|
||||
@ -224,21 +242,6 @@ export const Job = {
|
||||
columnWidth: 260,
|
||||
readOnly: true
|
||||
},
|
||||
{
|
||||
name: 'startedAt',
|
||||
label: 'Started At',
|
||||
type: 'dateTime',
|
||||
readOnly: true,
|
||||
columnWidth: 175
|
||||
},
|
||||
{
|
||||
name: 'quantity',
|
||||
label: 'Quantity',
|
||||
type: 'number',
|
||||
columnFixed: 'left',
|
||||
columnWidth: 125,
|
||||
required: true
|
||||
},
|
||||
{
|
||||
name: 'finishedAt',
|
||||
label: 'Finished At',
|
||||
@ -248,13 +251,12 @@ export const Job = {
|
||||
},
|
||||
|
||||
{
|
||||
name: 'gcodeFile',
|
||||
label: 'GCode File',
|
||||
type: 'object',
|
||||
columnWidth: 200,
|
||||
objectType: 'gcodeFile',
|
||||
required: true,
|
||||
showHyperlink: true
|
||||
name: 'quantity',
|
||||
label: 'Quantity',
|
||||
type: 'number',
|
||||
columnFixed: 'left',
|
||||
columnWidth: 125,
|
||||
required: true
|
||||
},
|
||||
|
||||
{
|
||||
@ -285,6 +287,15 @@ export const Job = {
|
||||
return parts.join(' ')
|
||||
}
|
||||
},
|
||||
{
|
||||
name: 'gcodeFile',
|
||||
label: 'GCode File',
|
||||
type: 'object',
|
||||
columnWidth: 200,
|
||||
objectType: 'gcodeFile',
|
||||
required: true,
|
||||
showHyperlink: true
|
||||
},
|
||||
{
|
||||
name: 'printers',
|
||||
label: 'Printers',
|
||||
|
||||
@ -53,6 +53,7 @@ export const SubJob = {
|
||||
],
|
||||
columns: [
|
||||
'_reference',
|
||||
'name',
|
||||
'printer',
|
||||
'job',
|
||||
'state',
|
||||
@ -62,6 +63,7 @@ export const SubJob = {
|
||||
'updatedAt'
|
||||
],
|
||||
filters: [
|
||||
'name',
|
||||
'_id',
|
||||
'job',
|
||||
'printer',
|
||||
@ -73,6 +75,7 @@ export const SubJob = {
|
||||
'finishedAt'
|
||||
],
|
||||
sorters: [
|
||||
'name',
|
||||
'createdAt',
|
||||
'state',
|
||||
'updatedAt',
|
||||
@ -116,6 +119,21 @@ export const SubJob = {
|
||||
readOnly: true,
|
||||
columnWidth: 200
|
||||
},
|
||||
{
|
||||
name: 'name',
|
||||
label: 'Name',
|
||||
type: 'text',
|
||||
readOnly: true,
|
||||
columnFixed: 'left',
|
||||
columnWidth: 220
|
||||
},
|
||||
{
|
||||
name: 'startedAt',
|
||||
label: 'Started At',
|
||||
type: 'dateTime',
|
||||
readOnly: true,
|
||||
columnWidth: 175
|
||||
},
|
||||
{
|
||||
name: 'state',
|
||||
label: 'State',
|
||||
@ -128,9 +146,10 @@ export const SubJob = {
|
||||
columnWidth: 260,
|
||||
readOnly: true
|
||||
},
|
||||
|
||||
{
|
||||
name: 'startedAt',
|
||||
label: 'Started At',
|
||||
name: 'finishedAt',
|
||||
label: 'Finished At',
|
||||
type: 'dateTime',
|
||||
readOnly: true,
|
||||
columnWidth: 175
|
||||
@ -144,32 +163,6 @@ export const SubJob = {
|
||||
showCopy: true
|
||||
},
|
||||
|
||||
{
|
||||
name: 'finishedAt',
|
||||
label: 'Finished At',
|
||||
type: 'dateTime',
|
||||
readOnly: true,
|
||||
columnWidth: 175
|
||||
},
|
||||
|
||||
{
|
||||
name: 'job',
|
||||
label: 'Job',
|
||||
type: 'object',
|
||||
objectType: 'job',
|
||||
showHyperlink: true,
|
||||
columnWidth: 200
|
||||
},
|
||||
|
||||
{
|
||||
name: 'printer',
|
||||
label: 'Printer',
|
||||
type: 'object',
|
||||
columnFixed: 'left',
|
||||
objectType: 'printer',
|
||||
showHyperlink: true,
|
||||
columnWidth: 200
|
||||
},
|
||||
{
|
||||
name: 'totalTime',
|
||||
label: 'Total Time',
|
||||
@ -197,6 +190,25 @@ export const SubJob = {
|
||||
|
||||
return parts.join(' ')
|
||||
}
|
||||
},
|
||||
|
||||
{
|
||||
name: 'job',
|
||||
label: 'Job',
|
||||
type: 'object',
|
||||
objectType: 'job',
|
||||
showHyperlink: true,
|
||||
columnWidth: 200
|
||||
},
|
||||
|
||||
{
|
||||
name: 'printer',
|
||||
label: 'Printer',
|
||||
type: 'object',
|
||||
columnFixed: 'left',
|
||||
objectType: 'printer',
|
||||
showHyperlink: true,
|
||||
columnWidth: 200
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user