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
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:
parent
5839b6b896
commit
4a90675ef5
@ -40,6 +40,7 @@ import PrinterControlButtons from '../../common/PrinterControlButtons.jsx'
|
||||
import PrinterQueue from '../../common/PrinterQueue.jsx'
|
||||
import ListIcon from '../../../Icons/ListIcon.jsx'
|
||||
import { useObjectNavigationTabPage } from '../../hooks/useObjectNavigationTabPage'
|
||||
import { getReferenceId } from '../../utils/objectPropertyRefs'
|
||||
|
||||
const log = loglevel.getLogger('ControlPrinter')
|
||||
log.setLevel(config.logLevel)
|
||||
@ -60,7 +61,7 @@ const ControlPrinter = ({ slicerIntegration = false }) => {
|
||||
{
|
||||
printer: true,
|
||||
job: true,
|
||||
subjob: true,
|
||||
subJob: true,
|
||||
gcodeFile: true,
|
||||
queue: true,
|
||||
filamentStock: true,
|
||||
@ -137,53 +138,63 @@ const ControlPrinter = ({ slicerIntegration = false }) => {
|
||||
? { showHyperlink: false, objectPropertyProps: { showHyperlink: false } }
|
||||
: {}
|
||||
|
||||
const actions = {
|
||||
edit: () => {
|
||||
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
|
||||
},
|
||||
const currentJobId = getReferenceId(objectFormState.objectData?.currentJob)
|
||||
const currentSubJobId = getReferenceId(
|
||||
objectFormState.objectData?.currentSubJob
|
||||
)
|
||||
const currentFilamentStockId = getReferenceId(
|
||||
objectFormState.objectData?.currentFilamentStock
|
||||
)
|
||||
|
||||
resumeJob: () => {
|
||||
if (connected == true) {
|
||||
sendObjectAction(printerId, 'printer', {
|
||||
type: 'resumeJob'
|
||||
})
|
||||
const actions = useMemo(
|
||||
() => ({
|
||||
edit: () => {
|
||||
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
|
||||
},
|
||||
cancelJob: () => {
|
||||
if (connected == true) {
|
||||
sendObjectAction(printerId, 'printer', {
|
||||
type: 'cancelJob'
|
||||
})
|
||||
}
|
||||
return true
|
||||
}
|
||||
}
|
||||
}),
|
||||
[connected, printerId, sendObjectAction]
|
||||
)
|
||||
|
||||
const sideBarItems = (
|
||||
<Flex gap={'large'} vertical>
|
||||
@ -375,7 +386,9 @@ const ControlPrinter = ({ slicerIntegration = false }) => {
|
||||
<Flex vertical gap={'large'}>
|
||||
<ActionHandler
|
||||
actions={actions}
|
||||
loading={objectFormState.loading}
|
||||
loading={
|
||||
objectFormState.loading || !objectFormState.objectData?._id
|
||||
}
|
||||
ref={actionHandlerRef}
|
||||
>
|
||||
<Flex vertical>
|
||||
@ -479,9 +492,9 @@ const ControlPrinter = ({ slicerIntegration = false }) => {
|
||||
updateCollapseState('job', expanded)
|
||||
}
|
||||
>
|
||||
{objectFormState.objectData?.currentJob?._id ? (
|
||||
{currentJobId ? (
|
||||
<ObjectForm
|
||||
id={objectFormState.objectData.currentJob._id}
|
||||
id={currentJobId}
|
||||
type='job'
|
||||
onStateChange={() => {}}
|
||||
>
|
||||
@ -518,14 +531,14 @@ const ControlPrinter = ({ slicerIntegration = false }) => {
|
||||
title={'Sub Job'}
|
||||
icon={<SubJobIcon />}
|
||||
collapseKey='subJob'
|
||||
active={collapseState.subJob}
|
||||
active={collapseState.subJob ?? collapseState.subjob}
|
||||
onToggle={(expanded) =>
|
||||
updateCollapseState('subJob', expanded)
|
||||
}
|
||||
>
|
||||
{objectFormState.objectData?.currentSubJob?._id ? (
|
||||
{currentSubJobId ? (
|
||||
<ObjectForm
|
||||
id={objectFormState.objectData.currentSubJob._id}
|
||||
id={currentSubJobId}
|
||||
type='subJob'
|
||||
onStateChange={() => {}}
|
||||
>
|
||||
@ -572,9 +585,7 @@ const ControlPrinter = ({ slicerIntegration = false }) => {
|
||||
>
|
||||
<PrinterGCodeFilePanel
|
||||
id={printerId}
|
||||
subJobId={
|
||||
objectFormState.objectData?.currentSubJob?._id
|
||||
}
|
||||
subJobId={currentSubJobId}
|
||||
offline={!objectFormState.objectData?.online}
|
||||
/>
|
||||
</InfoCollapse>
|
||||
@ -587,13 +598,9 @@ const ControlPrinter = ({ slicerIntegration = false }) => {
|
||||
updateCollapseState('filamentStock', expanded)
|
||||
}
|
||||
>
|
||||
{objectFormState.objectData?.currentFilamentStock
|
||||
?._id ? (
|
||||
{currentFilamentStockId ? (
|
||||
<ObjectForm
|
||||
id={
|
||||
objectFormState.objectData.currentFilamentStock
|
||||
._id
|
||||
}
|
||||
id={currentFilamentStockId}
|
||||
type='filamentStock'
|
||||
onStateChange={() => {}}
|
||||
>
|
||||
|
||||
@ -282,6 +282,10 @@ const ObjectForm = forwardRef(
|
||||
const currentHasFilePropertiesRef = useRef(hasFileProperties)
|
||||
const currentFlushOrphanFilesRef = useRef(flushOrphanFiles)
|
||||
const fetchedObjectRef = useRef(null)
|
||||
const fetchInFlightRef = useRef(false)
|
||||
const updatesDuringFetchRef = useRef([])
|
||||
const hasCompletedFetchRef = useRef(false)
|
||||
const loadedObjectKeyRef = useRef(null)
|
||||
|
||||
// Update refs when values change
|
||||
useEffect(() => {
|
||||
@ -480,12 +484,17 @@ const ObjectForm = forwardRef(
|
||||
.validateFields({ validateOnly: true })
|
||||
.then(() => {
|
||||
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({
|
||||
formValid: true,
|
||||
objectData: mergedObjectData,
|
||||
...(isEditingRef.current ? { objectData: mergedObjectData } : {}),
|
||||
editDisabled: getEditDisabled(
|
||||
model,
|
||||
mergedObjectData,
|
||||
isEditingRef.current
|
||||
? mergedObjectData
|
||||
: serverObjectData.current || mergedObjectData,
|
||||
userProfile
|
||||
)
|
||||
})
|
||||
@ -494,10 +503,12 @@ const ObjectForm = forwardRef(
|
||||
setFormValid(false)
|
||||
onStateChangeRef.current({
|
||||
formValid: false,
|
||||
objectData: mergedObjectData,
|
||||
...(isEditingRef.current ? { objectData: mergedObjectData } : {}),
|
||||
editDisabled: getEditDisabled(
|
||||
model,
|
||||
mergedObjectData,
|
||||
isEditingRef.current
|
||||
? mergedObjectData
|
||||
: serverObjectData.current || mergedObjectData,
|
||||
userProfile
|
||||
)
|
||||
})
|
||||
@ -531,6 +542,18 @@ const ObjectForm = forwardRef(
|
||||
async (options = {}) => {
|
||||
const objectKey = `${type}:${id}`
|
||||
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 {
|
||||
setFetchLoading(true)
|
||||
@ -538,7 +561,10 @@ const ObjectForm = forwardRef(
|
||||
const data = await fetchObject(id, type)
|
||||
const initialActivities = await fetchObjectActivities(id, type)
|
||||
|
||||
if (fetchedObjectRef.current !== objectKey) {
|
||||
if (
|
||||
fetchedObjectRef.current !== objectKey ||
|
||||
updatesDuringFetchRef.current !== updatesDuringFetch
|
||||
) {
|
||||
return
|
||||
}
|
||||
|
||||
@ -551,16 +577,39 @@ const ObjectForm = forwardRef(
|
||||
releaseEditingState()
|
||||
}
|
||||
|
||||
serverObjectData.current = data
|
||||
|
||||
// Calculate and set computed values on initial load
|
||||
// Calculate and set computed values on initial load.
|
||||
// Updates that arrived while this request was in flight (a print
|
||||
// starting and assigning currentJob/currentSubJob) must win over
|
||||
// the older fetched snapshot.
|
||||
const computedEntries = calculateComputedValues(data, model, {
|
||||
skipObjectChildrenValue
|
||||
})
|
||||
const initialFormData = applyComputedEntries(data, computedEntries)
|
||||
let initialFormData = applyComputedEntries(data, computedEntries)
|
||||
if (skipObjectChildrenValue) {
|
||||
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({
|
||||
...initialFormData,
|
||||
_isEditing: isEditingRef.current
|
||||
@ -573,6 +622,15 @@ const ObjectForm = forwardRef(
|
||||
showError(
|
||||
`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
|
||||
const updateObjectEventHandler = useCallback(
|
||||
(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) => {
|
||||
const next = mergeFormData(prev, value)
|
||||
if (!isEditingRef.current) {
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user