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 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={() => {}}
|
||||||
>
|
>
|
||||||
|
|||||||
@ -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) {
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user