Enhance DeployJob and NewObjectForm Components with Object Hydration and State Management
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:
Tom Butcher 2026-11-08 23:41:04 +00:00
parent b65fe208c0
commit 82d76db106
6 changed files with 635 additions and 78 deletions

View File

@ -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 PropTypes from 'prop-types'
import WizardView from '../../common/WizardView' import WizardView from '../../common/WizardView'
import { ApiServerContext } from '../../context/ApiServerContext' import { ApiServerContext } from '../../context/ApiServerContext'
@ -6,7 +6,17 @@ import { Descriptions, Flex, Typography } from 'antd'
import ObjectTable from '../../common/ObjectTable' import ObjectTable from '../../common/ObjectTable'
import { getModelByName } from '../../../../database/ObjectModels.js' import { getModelByName } from '../../../../database/ObjectModels.js'
import ObjectProperty from '../../common/ObjectProperty' import ObjectProperty from '../../common/ObjectProperty'
import { calculateModelComputedEntries } from '../../utils/Utils'
import {
applyHydratedObjects,
applyRelatedObjectUpdates,
collectObjectPropertyRefs,
getReferenceId,
getRelatedObjectSubscriptionKey
} from '../../utils/objectPropertyRefs'
const { Text } = Typography const { Text } = Typography
const DeployJob = ({ const DeployJob = ({
onOk, onOk,
objectData = undefined, objectData = undefined,
@ -14,12 +24,45 @@ const DeployJob = ({
}) => { }) => {
const [deployLoading, setDeployLoading] = useState(false) const [deployLoading, setDeployLoading] = useState(false)
const [job, setJob] = useState(objectData) const [job, setJob] = useState(objectData)
const jobRef = useRef(job)
const [deployedSubJobsCount, setDeployedSubJobsCount] = useState(0) const [deployedSubJobsCount, setDeployedSubJobsCount] = useState(0)
const [subJobsCount, setSubJobsCount] = useState(999) const [subJobsCount, setSubJobsCount] = useState(999)
const [allPrintersOnline, setAllPrintersOnline] = useState(false) 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 () => { const handleDeploy = useCallback(async () => {
if (!job?._id) return
setDeployLoading(true) setDeployLoading(true)
var hasMore = true var hasMore = true
var currentPage = 1 var currentPage = 1
@ -47,7 +90,7 @@ const DeployJob = ({
} }
) )
}) })
}, [job._id, sendObjectAction, fetchObjects]) }, [job?._id, sendObjectAction, fetchObjects])
useEffect(() => { useEffect(() => {
if (deployedSubJobsCount == subJobsCount && deployLoading == true) { if (deployedSubJobsCount == subJobsCount && deployLoading == true) {
@ -56,8 +99,110 @@ const DeployJob = ({
}, [deployedSubJobsCount, subJobsCount, deployLoading, onOk]) }, [deployedSubJobsCount, subJobsCount, deployLoading, onOk])
useEffect(() => { useEffect(() => {
if (objectData?.printers) { commitJob(objectDataRef.current)
if (objectData.printers.every((printer) => printer.online)) { }, [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) setAllPrintersOnline(true)
} else { } else {
setAllPrintersOnline(false) setAllPrintersOnline(false)
@ -65,7 +210,7 @@ const DeployJob = ({
} else { } else {
setAllPrintersOnline(false) setAllPrintersOnline(false)
} }
}, [objectData]) }, [job])
const steps = [ const steps = [
{ {
@ -98,13 +243,13 @@ const DeployJob = ({
<ObjectProperty <ObjectProperty
type={'object'} type={'object'}
name={'job'} name={'job'}
objectData={{ job: objectData }} objectData={{ job }}
value={(object) => object.job} value={(object) => object.job}
readOnly={true} readOnly={true}
isEditing={true} isEditing={true}
objectType={'job'} objectType={'job'}
onChange={(newJob) => { onChange={(newJob) => {
setJob(newJob) commitJob(newJob)
}} }}
/> />
</div> </div>
@ -131,7 +276,7 @@ const DeployJob = ({
<ObjectProperty <ObjectProperty
type={'objectList'} type={'objectList'}
name={'printers'} name={'printers'}
objectData={{ printers: objectData.printers }} objectData={{ printers: job?.printers }}
value={(object) => object.printers} value={(object) => object.printers}
readOnly={true} readOnly={true}
isEditing={true} isEditing={true}
@ -151,7 +296,9 @@ const DeployJob = ({
printer: false, printer: false,
job: false job: false
}} }}
masterFilter={{ job: getModelByName('job').prefix + ':' + job?._id }} masterFilter={{
job: getModelByName('job').prefix + ':' + job?._id
}}
size={'small'} size={'small'}
showActions={!slicerIntegration} showActions={!slicerIntegration}
/> />
@ -164,7 +311,7 @@ const DeployJob = ({
<WizardView <WizardView
steps={steps} steps={steps}
loading={deployLoading} loading={deployLoading}
formValid={objectData != undefined && allPrintersOnline} formValid={job != undefined && allPrintersOnline}
title='Deploy Job' title='Deploy Job'
showSteps={false} showSteps={false}
showSkipButton={slicerIntegration} showSkipButton={slicerIntegration}

View File

@ -50,6 +50,7 @@ const NewJob = ({
visibleProperties={{ visibleProperties={{
_id: false, _id: false,
_reference: false, _reference: false,
name: false,
createdAt: false, createdAt: false,
updatedAt: false, updatedAt: false,
startedAt: false, startedAt: false,

View File

@ -3,6 +3,7 @@ import { Form } from 'antd'
import { ApiServerContext } from '../context/ApiServerContext' import { ApiServerContext } from '../context/ApiServerContext'
import { useMessageContext } from '../context/MessageContext' import { useMessageContext } from '../context/MessageContext'
import PropTypes from 'prop-types' import PropTypes from 'prop-types'
import get from 'lodash/get'
import set from 'lodash/set' import set from 'lodash/set'
import { getModelByName } from '../../../database/ObjectModels' import { getModelByName } from '../../../database/ObjectModels'
@ -11,6 +12,14 @@ import {
stripNestedObjectProperties, stripNestedObjectProperties,
calculateModelComputedEntries calculateModelComputedEntries
} from '../utils/Utils' } from '../utils/Utils'
import {
applyComputedEntries,
applyHydratedObjects,
applyRelatedObjectUpdates,
collectObjectPropertyRefs,
getReferenceId,
getRelatedObjectSubscriptionKey
} from '../utils/objectPropertyRefs'
const EMPTY_DEFAULT_VALUES = {} const EMPTY_DEFAULT_VALUES = {}
@ -36,18 +45,17 @@ const buildObjectFromEntries = (entries = []) => {
}, {}) }, {})
} }
// Patch computed fields onto the complete object. const syncFormValues = (form, data, refs = [], computedEntries = []) => {
// This mirrors the handling used by ObjectForm. refs.forEach((ref) => {
const applyComputedEntries = (base, entries = []) => { form.setFieldValue(ref.namePath, get(data, ref.namePath))
const result = mergeFormData(base || {})
entries.forEach((entry) => {
const { namePath, value } = entry || {}
if (!Array.isArray(namePath) || value === undefined) return
set(result, namePath, value)
}) })
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({ const [objectData, setObjectData] = useState({
_isEditing: true _isEditing: true
}) })
const objectDataRef = useRef(objectData)
const defaultValuesKey = serializeDefaultValues(defaultValues) const defaultValuesKey = serializeDefaultValues(defaultValues)
const defaultValuesRef = useRef(defaultValues) const defaultValuesRef = useRef(defaultValues)
defaultValuesRef.current = 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 [submitLoading, setSubmitLoading] = useState(false)
const [formValid, setFormValid] = useState(false) const [formValid, setFormValid] = useState(false)
@ -90,7 +106,15 @@ const NewObjectForm = ({
const { showSuccess, showError: showMessageError } = useMessageContext() 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) const model = getModelByName(type)
@ -170,10 +194,126 @@ const NewObjectForm = ({
form.setFieldsValue(initialFormData) form.setFieldsValue(initialFormData)
// Keep objectData in sync with exactly the same initial data. // Keep objectData in sync with exactly the same initial data.
setObjectData(nextObjectData) commitObjectData(nextObjectData)
return validateForm() 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 // Validate form on change
useEffect(() => { useEffect(() => {
@ -186,7 +326,10 @@ const NewObjectForm = ({
const currentFormValues = form.getFieldsValue() const currentFormValues = form.getFieldsValue()
const currentFormData = mergeFormData(objectData || {}, currentFormValues) const currentFormData = mergeFormData(
objectDataRef.current || {},
currentFormValues
)
const computedEntries = calculateComputedValues(currentFormData, model) const computedEntries = calculateComputedValues(currentFormData, model)
@ -227,7 +370,10 @@ const NewObjectForm = ({
style={style} style={style}
disabled={submitLoading} disabled={submitLoading}
onValuesChange={(_changedValues, allFormValues) => { onValuesChange={(_changedValues, allFormValues) => {
const currentFormData = mergeFormData(objectData || {}, allFormValues) const currentFormData = mergeFormData(
objectDataRef.current || {},
allFormValues
)
const computedEntries = calculateComputedValues(currentFormData, model) const computedEntries = calculateComputedValues(currentFormData, model)
@ -257,7 +403,7 @@ const NewObjectForm = ({
allValues._isEditing = true allValues._isEditing = true
setObjectData((prev) => mergeFormData(prev, allValues)) commitObjectData(mergeFormData(objectDataRef.current, allValues))
}} }}
> >
{children({ {children({
@ -268,7 +414,7 @@ const NewObjectForm = ({
form, form,
formValid, formValid,
objectData, objectData,
setObjectData setObjectData: commitObjectData
})} })}
</Form> </Form>
) )

View 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 }
}

View File

@ -150,6 +150,7 @@ export const Job = {
], ],
columns: [ columns: [
'_reference', '_reference',
'name',
'quantity', 'quantity',
'state', 'state',
'gcodeFile', 'gcodeFile',
@ -159,6 +160,7 @@ export const Job = {
'updatedAt' 'updatedAt'
], ],
filters: [ filters: [
'name',
'state', 'state',
'gcodeFile', 'gcodeFile',
'quantity', 'quantity',
@ -170,6 +172,7 @@ export const Job = {
'finishedAt' 'finishedAt'
], ],
sorters: [ sorters: [
'name',
'createdAt', 'createdAt',
'state', 'state',
'quantity', 'quantity',
@ -212,6 +215,21 @@ export const Job = {
readOnly: true, readOnly: true,
columnWidth: 200 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', name: 'state',
label: 'State', label: 'State',
@ -224,21 +242,6 @@ export const Job = {
columnWidth: 260, columnWidth: 260,
readOnly: true 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', name: 'finishedAt',
label: 'Finished At', label: 'Finished At',
@ -248,13 +251,12 @@ export const Job = {
}, },
{ {
name: 'gcodeFile', name: 'quantity',
label: 'GCode File', label: 'Quantity',
type: 'object', type: 'number',
columnWidth: 200, columnFixed: 'left',
objectType: 'gcodeFile', columnWidth: 125,
required: true, required: true
showHyperlink: true
}, },
{ {
@ -285,6 +287,15 @@ export const Job = {
return parts.join(' ') return parts.join(' ')
} }
}, },
{
name: 'gcodeFile',
label: 'GCode File',
type: 'object',
columnWidth: 200,
objectType: 'gcodeFile',
required: true,
showHyperlink: true
},
{ {
name: 'printers', name: 'printers',
label: 'Printers', label: 'Printers',

View File

@ -53,6 +53,7 @@ export const SubJob = {
], ],
columns: [ columns: [
'_reference', '_reference',
'name',
'printer', 'printer',
'job', 'job',
'state', 'state',
@ -62,6 +63,7 @@ export const SubJob = {
'updatedAt' 'updatedAt'
], ],
filters: [ filters: [
'name',
'_id', '_id',
'job', 'job',
'printer', 'printer',
@ -73,6 +75,7 @@ export const SubJob = {
'finishedAt' 'finishedAt'
], ],
sorters: [ sorters: [
'name',
'createdAt', 'createdAt',
'state', 'state',
'updatedAt', 'updatedAt',
@ -116,6 +119,21 @@ export const SubJob = {
readOnly: true, readOnly: true,
columnWidth: 200 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', name: 'state',
label: 'State', label: 'State',
@ -128,9 +146,10 @@ export const SubJob = {
columnWidth: 260, columnWidth: 260,
readOnly: true readOnly: true
}, },
{ {
name: 'startedAt', name: 'finishedAt',
label: 'Started At', label: 'Finished At',
type: 'dateTime', type: 'dateTime',
readOnly: true, readOnly: true,
columnWidth: 175 columnWidth: 175
@ -144,32 +163,6 @@ export const SubJob = {
showCopy: true 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', name: 'totalTime',
label: 'Total Time', label: 'Total Time',
@ -197,6 +190,25 @@ export const SubJob = {
return parts.join(' ') 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
} }
] ]
} }