Refactor ControlPrinter and ObjectForm Components for Improved State Management and Fetch Handling
All checks were successful
farmcontrol/farmcontrol-ui/pipeline/head This commit looks good

- Updated ControlPrinter to utilize getReferenceId for cleaner ID retrieval of current jobs and filament stocks.
- Refactored actions in ControlPrinter to use useMemo for better performance and dependency management.
- Enhanced ObjectForm to manage fetch states and updates more effectively, ensuring real-time data consistency during asynchronous operations.
- Improved handling of object data updates during fetch to prevent race conditions and ensure accurate state representation.
This commit is contained in:
Tom Butcher 2026-11-09 00:59:37 +00:00
parent 5839b6b896
commit 4a90675ef5
2 changed files with 150 additions and 70 deletions

View File

@ -40,6 +40,7 @@ import PrinterControlButtons from '../../common/PrinterControlButtons.jsx'
import PrinterQueue from '../../common/PrinterQueue.jsx' import PrinterQueue from '../../common/PrinterQueue.jsx'
import ListIcon from '../../../Icons/ListIcon.jsx' import ListIcon from '../../../Icons/ListIcon.jsx'
import { useObjectNavigationTabPage } from '../../hooks/useObjectNavigationTabPage' import { useObjectNavigationTabPage } from '../../hooks/useObjectNavigationTabPage'
import { getReferenceId } from '../../utils/objectPropertyRefs'
const log = loglevel.getLogger('ControlPrinter') const log = loglevel.getLogger('ControlPrinter')
log.setLevel(config.logLevel) log.setLevel(config.logLevel)
@ -60,7 +61,7 @@ const ControlPrinter = ({ slicerIntegration = false }) => {
{ {
printer: true, printer: true,
job: true, job: true,
subjob: true, subJob: true,
gcodeFile: true, gcodeFile: true,
queue: true, queue: true,
filamentStock: true, filamentStock: true,
@ -137,53 +138,63 @@ const ControlPrinter = ({ slicerIntegration = false }) => {
? { showHyperlink: false, objectPropertyProps: { showHyperlink: false } } ? { showHyperlink: false, objectPropertyProps: { showHyperlink: false } }
: {} : {}
const actions = { const currentJobId = getReferenceId(objectFormState.objectData?.currentJob)
edit: () => { const currentSubJobId = getReferenceId(
objectFormRef?.current.startEditing() objectFormState.objectData?.currentSubJob
return false )
}, const currentFilamentStockId = getReferenceId(
cancelEdit: () => { objectFormState.objectData?.currentFilamentStock
objectFormRef?.current.cancelEditing() )
return true
},
finishEdit: () => {
objectFormRef?.current.handleUpdate()
return true
},
startQueue: () => {
if (connected == true) {
sendObjectAction(printerId, 'printer', {
type: 'startQueue'
})
}
return true
},
pauseJob: () => {
if (connected == true) {
sendObjectAction(printerId, 'printer', {
type: 'pauseJob'
})
}
return true
},
resumeJob: () => { const actions = useMemo(
if (connected == true) { () => ({
sendObjectAction(printerId, 'printer', { edit: () => {
type: 'resumeJob' objectFormRef?.current.startEditing()
}) return false
},
cancelEdit: () => {
objectFormRef?.current.cancelEditing()
return true
},
finishEdit: () => {
objectFormRef?.current.handleUpdate()
return true
},
startQueue: () => {
if (connected == true) {
sendObjectAction(printerId, 'printer', {
type: 'startQueue'
})
}
return true
},
pauseJob: () => {
if (connected == true) {
sendObjectAction(printerId, 'printer', {
type: 'pauseJob'
})
}
return true
},
resumeJob: () => {
if (connected == true) {
sendObjectAction(printerId, 'printer', {
type: 'resumeJob'
})
}
return true
},
cancelJob: () => {
if (connected == true) {
sendObjectAction(printerId, 'printer', {
type: 'cancelJob'
})
}
return true
} }
return true }),
}, [connected, printerId, sendObjectAction]
cancelJob: () => { )
if (connected == true) {
sendObjectAction(printerId, 'printer', {
type: 'cancelJob'
})
}
return true
}
}
const sideBarItems = ( const sideBarItems = (
<Flex gap={'large'} vertical> <Flex gap={'large'} vertical>
@ -375,7 +386,9 @@ const ControlPrinter = ({ slicerIntegration = false }) => {
<Flex vertical gap={'large'}> <Flex vertical gap={'large'}>
<ActionHandler <ActionHandler
actions={actions} actions={actions}
loading={objectFormState.loading} loading={
objectFormState.loading || !objectFormState.objectData?._id
}
ref={actionHandlerRef} ref={actionHandlerRef}
> >
<Flex vertical> <Flex vertical>
@ -479,9 +492,9 @@ const ControlPrinter = ({ slicerIntegration = false }) => {
updateCollapseState('job', expanded) updateCollapseState('job', expanded)
} }
> >
{objectFormState.objectData?.currentJob?._id ? ( {currentJobId ? (
<ObjectForm <ObjectForm
id={objectFormState.objectData.currentJob._id} id={currentJobId}
type='job' type='job'
onStateChange={() => {}} onStateChange={() => {}}
> >
@ -518,14 +531,14 @@ const ControlPrinter = ({ slicerIntegration = false }) => {
title={'Sub Job'} title={'Sub Job'}
icon={<SubJobIcon />} icon={<SubJobIcon />}
collapseKey='subJob' collapseKey='subJob'
active={collapseState.subJob} active={collapseState.subJob ?? collapseState.subjob}
onToggle={(expanded) => onToggle={(expanded) =>
updateCollapseState('subJob', expanded) updateCollapseState('subJob', expanded)
} }
> >
{objectFormState.objectData?.currentSubJob?._id ? ( {currentSubJobId ? (
<ObjectForm <ObjectForm
id={objectFormState.objectData.currentSubJob._id} id={currentSubJobId}
type='subJob' type='subJob'
onStateChange={() => {}} onStateChange={() => {}}
> >
@ -572,9 +585,7 @@ const ControlPrinter = ({ slicerIntegration = false }) => {
> >
<PrinterGCodeFilePanel <PrinterGCodeFilePanel
id={printerId} id={printerId}
subJobId={ subJobId={currentSubJobId}
objectFormState.objectData?.currentSubJob?._id
}
offline={!objectFormState.objectData?.online} offline={!objectFormState.objectData?.online}
/> />
</InfoCollapse> </InfoCollapse>
@ -587,13 +598,9 @@ const ControlPrinter = ({ slicerIntegration = false }) => {
updateCollapseState('filamentStock', expanded) updateCollapseState('filamentStock', expanded)
} }
> >
{objectFormState.objectData?.currentFilamentStock {currentFilamentStockId ? (
?._id ? (
<ObjectForm <ObjectForm
id={ id={currentFilamentStockId}
objectFormState.objectData.currentFilamentStock
._id
}
type='filamentStock' type='filamentStock'
onStateChange={() => {}} onStateChange={() => {}}
> >

View File

@ -282,6 +282,10 @@ const ObjectForm = forwardRef(
const currentHasFilePropertiesRef = useRef(hasFileProperties) const currentHasFilePropertiesRef = useRef(hasFileProperties)
const currentFlushOrphanFilesRef = useRef(flushOrphanFiles) const currentFlushOrphanFilesRef = useRef(flushOrphanFiles)
const fetchedObjectRef = useRef(null) const fetchedObjectRef = useRef(null)
const fetchInFlightRef = useRef(false)
const updatesDuringFetchRef = useRef([])
const hasCompletedFetchRef = useRef(false)
const loadedObjectKeyRef = useRef(null)
// Update refs when values change // Update refs when values change
useEffect(() => { useEffect(() => {
@ -480,12 +484,17 @@ const ObjectForm = forwardRef(
.validateFields({ validateOnly: true }) .validateFields({ validateOnly: true })
.then(() => { .then(() => {
setFormValid(true) setFormValid(true)
// While viewing, objectData is published from the fetched/live
// record. Publishing the form snapshot here races a print start:
// the snapshot still has no current job and unmounts the job forms.
onStateChangeRef.current({ onStateChangeRef.current({
formValid: true, formValid: true,
objectData: mergedObjectData, ...(isEditingRef.current ? { objectData: mergedObjectData } : {}),
editDisabled: getEditDisabled( editDisabled: getEditDisabled(
model, model,
mergedObjectData, isEditingRef.current
? mergedObjectData
: serverObjectData.current || mergedObjectData,
userProfile userProfile
) )
}) })
@ -494,10 +503,12 @@ const ObjectForm = forwardRef(
setFormValid(false) setFormValid(false)
onStateChangeRef.current({ onStateChangeRef.current({
formValid: false, formValid: false,
objectData: mergedObjectData, ...(isEditingRef.current ? { objectData: mergedObjectData } : {}),
editDisabled: getEditDisabled( editDisabled: getEditDisabled(
model, model,
mergedObjectData, isEditingRef.current
? mergedObjectData
: serverObjectData.current || mergedObjectData,
userProfile userProfile
) )
}) })
@ -531,6 +542,18 @@ const ObjectForm = forwardRef(
async (options = {}) => { async (options = {}) => {
const objectKey = `${type}:${id}` const objectKey = `${type}:${id}`
const skipObjectChildrenValue = options.skipObjectChildrenValue === true const skipObjectChildrenValue = options.skipObjectChildrenValue === true
if (loadedObjectKeyRef.current !== objectKey) {
loadedObjectKeyRef.current = objectKey
hasCompletedFetchRef.current = false
}
// A print can assign currentJob before this request returns. Keep that
// live record for the first load; later refetches should take the server.
const liveBeforeFetch = hasCompletedFetchRef.current
? null
: serverObjectData.current
const updatesDuringFetch = []
updatesDuringFetchRef.current = updatesDuringFetch
fetchInFlightRef.current = true
try { try {
setFetchLoading(true) setFetchLoading(true)
@ -538,7 +561,10 @@ const ObjectForm = forwardRef(
const data = await fetchObject(id, type) const data = await fetchObject(id, type)
const initialActivities = await fetchObjectActivities(id, type) const initialActivities = await fetchObjectActivities(id, type)
if (fetchedObjectRef.current !== objectKey) { if (
fetchedObjectRef.current !== objectKey ||
updatesDuringFetchRef.current !== updatesDuringFetch
) {
return return
} }
@ -551,16 +577,39 @@ const ObjectForm = forwardRef(
releaseEditingState() releaseEditingState()
} }
serverObjectData.current = data // Calculate and set computed values on initial load.
// Updates that arrived while this request was in flight (a print
// Calculate and set computed values on initial load // starting and assigning currentJob/currentSubJob) must win over
// the older fetched snapshot.
const computedEntries = calculateComputedValues(data, model, { const computedEntries = calculateComputedValues(data, model, {
skipObjectChildrenValue skipObjectChildrenValue
}) })
const initialFormData = applyComputedEntries(data, computedEntries) let initialFormData = applyComputedEntries(data, computedEntries)
if (skipObjectChildrenValue) { if (skipObjectChildrenValue) {
copyObjectChildren(initialFormData, data, model) copyObjectChildren(initialFormData, data, model)
} }
if (liveBeforeFetch) {
const liveId = liveBeforeFetch._id
const fetchedId = initialFormData?._id
if (
!liveId ||
!fetchedId ||
String(liveId).toLowerCase() === String(fetchedId).toLowerCase()
) {
initialFormData = mergeFormData(initialFormData, liveBeforeFetch)
if (!isEditingRef.current) {
copyObjectChildren(initialFormData, liveBeforeFetch, model)
}
}
}
updatesDuringFetch.forEach((update) => {
initialFormData = mergeFormData(initialFormData, update)
if (!isEditingRef.current) {
copyObjectChildren(initialFormData, update, model)
}
})
hasCompletedFetchRef.current = true
serverObjectData.current = initialFormData
setObjectData({ setObjectData({
...initialFormData, ...initialFormData,
_isEditing: isEditingRef.current _isEditing: isEditingRef.current
@ -573,6 +622,15 @@ const ObjectForm = forwardRef(
showError( showError(
`Failed to fetch ${getModelMessageLabel(type).toLowerCase()} info.` `Failed to fetch ${getModelMessageLabel(type).toLowerCase()} info.`
) )
if (updatesDuringFetchRef.current === updatesDuringFetch) {
setFetchLoading(false)
onStateChangeRef.current({ loading: false })
}
} finally {
if (updatesDuringFetchRef.current === updatesDuringFetch) {
fetchInFlightRef.current = false
updatesDuringFetchRef.current = []
}
} }
}, },
[ [
@ -592,6 +650,21 @@ const ObjectForm = forwardRef(
// Update event handler // Update event handler
const updateObjectEventHandler = useCallback( const updateObjectEventHandler = useCallback(
(value) => { (value) => {
if (
fetchInFlightRef.current &&
Array.isArray(updatesDuringFetchRef.current)
) {
updatesDuringFetchRef.current.push(value)
} else if (!isEditingRef.current) {
serverObjectData.current = mergeFormData(
serverObjectData.current,
value
)
if (serverObjectData.current) {
copyObjectChildren(serverObjectData.current, value, model)
}
}
setObjectData((prev) => { setObjectData((prev) => {
const next = mergeFormData(prev, value) const next = mergeFormData(prev, value)
if (!isEditingRef.current) { if (!isEditingRef.current) {