Refactor Load and Unload Filament Stock components to utilize WizardView for improved user experience, integrate API server context for real-time temperature and filament sensor updates, and streamline form validation logic.

This commit is contained in:
Tom Butcher 2025-11-23 13:26:46 +00:00
parent 657d6a5c6e
commit ecf73c13c0
2 changed files with 163 additions and 273 deletions

View File

@ -1,27 +1,14 @@
import { useState, useContext, useEffect } from 'react'
import {
Form,
Button,
Typography,
Flex,
Steps,
Divider,
Descriptions,
Alert
} from 'antd'
import { useMediaQuery } from 'react-responsive'
import { Form, Flex, Descriptions, Alert } from 'antd'
import PropTypes from 'prop-types'
import { PrintServerContext } from '../../context/PrintServerContext'
import FilamentStockSelect from '../../common/FilamentStockSelect'
import PrinterSelect from '../../common/PrinterSelect'
import FilamentStockDisplay from '../../common/FilamentStockDisplay'
import PrinterTemperaturePanel from '../../common/PrinterTemperaturePanel'
import { LoadingOutlined } from '@ant-design/icons'
import PrinterState from '../../common/StateDisplay'
const { Title } = Typography
import ObjectSelect from '../../common/ObjectSelect'
import ObjectDisplay from '../../common/ObjectDisplay'
import WizardView from '../../common/WizardView'
import { ApiServerContext } from '../../context/ApiServerContext'
const LoadFilamentStock = ({
onOk,
@ -29,7 +16,8 @@ const LoadFilamentStock = ({
printer = null,
filamentStockLoaded = false
}) => {
const isMobile = useMediaQuery({ maxWidth: 768 })
const { connected, subscribeToObjectEvent, sendObjectAction } =
useContext(ApiServerContext)
LoadFilamentStock.propTypes = {
onOk: PropTypes.func.isRequired,
@ -38,8 +26,6 @@ const LoadFilamentStock = ({
filamentStockLoaded: PropTypes.bool
}
const { printServer } = useContext(PrintServerContext)
const initialLoadFilamentStockForm = {
printer: printer,
filamentStock: null
@ -47,8 +33,7 @@ const LoadFilamentStock = ({
const [loadFilamentStockLoading, setLoadFilamentStockLoading] =
useState(false)
const [currentStep, setCurrentStep] = useState(0)
const [nextEnabled, setNextEnabled] = useState(false)
const [formValid, setFormValid] = useState(false)
const [currentTemperature, setCurrentTemperature] = useState(-1)
const [targetTemperature, setTargetTemperature] = useState(0)
const [filamentSensorDetected, setFilamentSensorDetected] =
@ -62,77 +47,69 @@ const LoadFilamentStock = ({
loadFilamentStockForm
)
// Add websocket temperature monitoring
useEffect(() => {
if (loadFilamentStockFormValues.printer) {
const params = {
printerId: loadFilamentStockFormValues.printer._id,
objects: {
extruder: null,
'filament_switch_sensor fsensor': null
if (printer?._id && connected) {
const temperatureEventUnsubscribe = subscribeToObjectEvent(
printer._id,
'printer',
'temperature',
(event) => {
if (event.data?.extruder?.current) {
setCurrentTemperature(event.data.extruder.current)
}
if (event.data?.extruder?.target) {
setTargetTemperature(event.data.extruder.target)
}
}
}
const notifyStatusUpdate = (statusUpdate) => {
if (statusUpdate?.extruder?.temperature !== undefined) {
setCurrentTemperature(statusUpdate.extruder.temperature)
)
const filamentStockEventUnsubscribe = subscribeToObjectEvent(
printer._id,
'printer',
'filamentSensor',
(event) => {
console.log('filamentSensor', event.data)
setFilamentSensorDetected(event.data.detected)
}
if (statusUpdate?.extruder?.target !== undefined) {
setTargetTemperature(statusUpdate.extruder.target)
}
if (
statusUpdate?.['filament_switch_sensor fsensor']
?.filament_detected !== undefined
) {
setFilamentSensorDetected(
Boolean(
statusUpdate['filament_switch_sensor fsensor'].filament_detected
)
)
}
}
printServer.emit('printer.objects.subscribe', params)
printServer.emit('printer.objects.query', params)
printServer.on('notify_status_update', notifyStatusUpdate)
)
return () => {
printServer.off('notify_status_update', notifyStatusUpdate)
printServer.emit('printer.objects.unsubscribe', params)
if (temperatureEventUnsubscribe) temperatureEventUnsubscribe()
if (filamentStockEventUnsubscribe) filamentStockEventUnsubscribe()
}
}
}, [printServer, loadFilamentStockFormValues.printer])
}, [printer?._id, connected])
useEffect(() => {
// Validate form fields
loadFilamentStockForm
.validateFields({
validateOnly: true
})
.then(() => setNextEnabled(filamentSensorDetected))
.catch(() => setNextEnabled(false))
.then(() => {
const hasPrinter = Boolean(loadFilamentStockFormValues.printer)
const hasFilamentStock = Boolean(
loadFilamentStockFormValues.filamentStock
)
// Step 0 (Preheat): needs printer + filament detected + (temp reached or no temp set)
const preheatReady =
hasPrinter &&
filamentSensorDetected &&
(targetTemperature === 0 || currentTemperature >= targetTemperature)
// Step 1+ (Required/Summary): needs filamentStock selected
// Form is valid if preheat is ready (for step 0) OR filamentStock is selected (for step 1+)
// This allows progression: step 0 can proceed when preheat is ready,
// and step 1+ can proceed when filamentStock is selected
setFormValid(preheatReady || (hasPrinter && hasFilamentStock))
})
.catch(() => setFormValid(false))
}, [
loadFilamentStockForm,
loadFilamentStockFormUpdateValues,
filamentSensorDetected
])
useEffect(() => {
if (
filamentSensorDetected == true &&
currentTemperature >= targetTemperature
) {
setNextEnabled(filamentSensorDetected)
if (currentStep == 0) {
setCurrentStep(1)
}
} else if (filamentSensorDetected == false) {
setCurrentStep(0)
}
}, [
loadFilamentStockFormValues,
filamentSensorDetected,
targetTemperature,
currentTemperature,
currentStep
targetTemperature
])
const summaryItems = [
@ -140,8 +117,9 @@ const LoadFilamentStock = ({
key: 'filamentStock',
label: 'Stock',
children: loadFilamentStockFormValues.filamentStock ? (
<FilamentStockDisplay
filamentStock={loadFilamentStockFormValues.filamentStock}
<ObjectDisplay
objectType='filamentStock'
object={loadFilamentStockFormValues.filamentStock}
/>
) : (
'n/a'
@ -151,9 +129,12 @@ const LoadFilamentStock = ({
key: 'printer',
label: 'Printer',
children: loadFilamentStockFormValues.printer ? (
<PrinterState printer={loadFilamentStockFormValues.printer} />
<ObjectDisplay
objectType='printer'
object={loadFilamentStockFormValues.printer}
/>
) : (
'n/a>'
'n/a'
)
}
]
@ -169,10 +150,16 @@ const LoadFilamentStock = ({
try {
// Set the extruder temperature
await printServer.emit('printer.filamentstock.load', {
printerId: loadFilamentStockFormValues.printer._id,
filamentStockId: loadFilamentStockFormValues.filamentStock._id
})
await sendObjectAction(
loadFilamentStockFormValues.printer._id,
'printer',
{
type: 'loadFilamentStock',
data: {
filamentStock: loadFilamentStockFormValues.filamentStock
}
}
)
onOk()
} finally {
setLoadFilamentStockLoading(false)
@ -196,7 +183,7 @@ const LoadFilamentStock = ({
}
]}
>
<PrinterSelect checkable={false} />
<ObjectSelect type='printer' checkable={false} />
</Form.Item>
{targetTemperature == 0 ? (
<Alert
@ -226,10 +213,9 @@ const LoadFilamentStock = ({
{loadFilamentStockFormValues.printer ? (
<PrinterTemperaturePanel
showHeatedBed={false}
showBed={false}
showMoreInfo={false}
printerId={loadFilamentStockFormValues.printer._id}
shouldUnsubscribe={false}
id={loadFilamentStockFormValues.printer._id}
/>
) : null}
</Flex>
@ -250,7 +236,7 @@ const LoadFilamentStock = ({
}
]}
>
<FilamentStockSelect />
<ObjectSelect type='filamentStock' />
</Form.Item>
</>
)
@ -267,75 +253,28 @@ const LoadFilamentStock = ({
]
return (
<Flex gap={'middle'}>
{!isMobile && (
<div style={{ minWidth: '160px' }}>
<Steps
current={currentStep}
items={steps}
direction='vertical'
style={{ width: 'fit-content' }}
/>
</div>
)}
{!isMobile && <Divider type={'vertical'} style={{ height: 'unset' }} />}
<Flex vertical={'true'} style={{ flexGrow: 1 }} gap='middle'>
<Title level={2} style={{ marginTop: 0, marginBottom: 4 }}>
Load Filament Stock
</Title>
<Form
name='basic'
autoComplete='off'
form={loadFilamentStockForm}
onFinish={handleLoadFilamentStock}
onValuesChange={(changedValues) =>
setLoadFilamentStockFormValues((prevValues) => ({
...prevValues,
...changedValues
}))
}
initialValues={initialLoadFilamentStockForm}
>
<div style={{ minHeight: '260px' }}>{steps[currentStep].content}</div>
<Flex justify={'end'}>
<Button
style={{
margin: '0 8px'
}}
onClick={() => setCurrentStep(currentStep - 1)}
disabled={!(currentStep > 0)}
>
Previous
</Button>
{currentStep < steps.length - 1 && (
<Button
type='primary'
disabled={!nextEnabled}
onClick={() => {
setCurrentStep(currentStep + 1)
}}
>
Next
</Button>
)}
{currentStep === steps.length - 1 && (
<Button
type='primary'
loading={loadFilamentStockLoading}
onClick={() => {
loadFilamentStockForm.submit()
}}
>
Done
</Button>
)}
</Flex>
</Form>
</Flex>
</Flex>
<Form
name='loadFilamentStock'
autoComplete='off'
form={loadFilamentStockForm}
onFinish={handleLoadFilamentStock}
onValuesChange={(changedValues) =>
setLoadFilamentStockFormValues((prevValues) => ({
...prevValues,
...changedValues
}))
}
initialValues={initialLoadFilamentStockForm}
>
<WizardView
title='Load Filament Stock'
steps={steps}
onSubmit={() => loadFilamentStockForm.submit()}
formValid={formValid}
loading={loadFilamentStockLoading}
submitText='Done'
/>
</Form>
)
}

View File

@ -1,34 +1,30 @@
import { useState, useContext, useEffect } from 'react'
import { Form, Button, Typography, Flex, Steps, Divider, Alert } from 'antd'
import { useMediaQuery } from 'react-responsive'
import { Form, Flex, Alert } from 'antd'
import PropTypes from 'prop-types'
import { PrintServerContext } from '../../context/PrintServerContext'
import PrinterSelect from '../../common/PrinterSelect'
import ObjectSelect from '../../common/ObjectSelect'
import PrinterTemperaturePanel from '../../common/PrinterTemperaturePanel'
import WizardView from '../../common/WizardView'
import { ApiServerContext } from '../../context/ApiServerContext'
import { LoadingOutlined } from '@ant-design/icons'
const { Title } = Typography
const UnloadFilamentStock = ({ onOk, reset, printer = null }) => {
const { connected, subscribeToObjectEvent, sendObjectAction } =
useContext(ApiServerContext)
UnloadFilamentStock.propTypes = {
onOk: PropTypes.func.isRequired,
reset: PropTypes.bool.isRequired,
printer: PropTypes.object
}
const { printServer } = useContext(PrintServerContext)
const isMobile = useMediaQuery({ maxWidth: 768 })
const initialUnloadFilamentStockForm = {
printer: printer
}
const [unloadFilamentStockLoading, setUnloadFilamentStockLoading] =
useState(false)
const [currentStep, setCurrentStep] = useState(0)
const [nextEnabled, setNextEnabled] = useState(false)
const [formValid, setFormValid] = useState(false)
const [currentTemperature, setCurrentTemperature] = useState(-1)
const [targetTemperature, setTargetTemperature] = useState(0)
const [filamentSensorDetected, setFilamentSensorDetected] = useState(true)
@ -36,46 +32,35 @@ const UnloadFilamentStock = ({ onOk, reset, printer = null }) => {
const [unloadFilamentStockFormValues, setUnloadFilamentStockFormValues] =
useState(initialUnloadFilamentStockForm)
// Add websocket temperature monitoring
useEffect(() => {
if (unloadFilamentStockFormValues.printer) {
const params = {
printerId: unloadFilamentStockFormValues.printer._id,
objects: {
extruder: null,
'filament_switch_sensor fsensor': null
if (printer?._id && connected) {
const temperatureEventUnsubscribe = subscribeToObjectEvent(
printer._id,
'printer',
'temperature',
(event) => {
if (event.data?.extruder?.current) {
setCurrentTemperature(event.data.extruder.current)
}
if (event.data?.extruder?.target) {
setTargetTemperature(event.data.extruder.target)
}
}
}
const notifyStatusUpdate = (statusUpdate) => {
if (statusUpdate?.extruder?.temperature !== undefined) {
setCurrentTemperature(statusUpdate.extruder.temperature)
)
const filamentStockEventUnsubscribe = subscribeToObjectEvent(
printer._id,
'printer',
'filamentSensor',
(event) => {
setFilamentSensorDetected(event.data.detected)
}
if (statusUpdate?.extruder?.target !== undefined) {
setTargetTemperature(statusUpdate.extruder.target)
}
if (
statusUpdate?.['filament_switch_sensor fsensor']
?.filament_detected !== undefined
) {
setFilamentSensorDetected(
Boolean(
statusUpdate['filament_switch_sensor fsensor'].filament_detected
)
)
}
}
printServer.emit('printer.objects.subscribe', params)
printServer.emit('printer.objects.query', params)
printServer.on('notify_status_update', notifyStatusUpdate)
)
return () => {
printServer.off('notify_status_update', notifyStatusUpdate)
printServer.emit('printer.objects.unsubscribe', params)
if (temperatureEventUnsubscribe) temperatureEventUnsubscribe()
if (filamentStockEventUnsubscribe) filamentStockEventUnsubscribe()
}
}
}, [printServer, unloadFilamentStockFormValues.printer])
}, [printer?._id, connected])
useEffect(() => {
if (reset) {
@ -90,14 +75,14 @@ const UnloadFilamentStock = ({ onOk, reset, printer = null }) => {
})
.then(() => {
// Only enable next if we have a printer selected, we're not loading, and we've reached target temperature
setNextEnabled(
setFormValid(
Boolean(unloadFilamentStockFormValues.printer) &&
!unloadFilamentStockLoading &&
currentTemperature + 1 > targetTemperature &&
targetTemperature != 0
)
})
.catch(() => setNextEnabled(false))
.catch(() => setFormValid(false))
}, [
unloadFilamentStockForm,
unloadFilamentStockFormValues,
@ -109,10 +94,13 @@ const UnloadFilamentStock = ({ onOk, reset, printer = null }) => {
const handleUnloadFilamentStock = async () => {
setUnloadFilamentStockLoading(true)
// Send G-code to retract the filament
await printServer.emit('printer.gcode.script', {
printerId: unloadFilamentStockFormValues.printer._id,
script: `_CLIENT_LINEAR_MOVE E=-200 F=1000`
})
await sendObjectAction(
unloadFilamentStockFormValues.printer._id,
'printer',
{
type: 'unloadFilamentStock'
}
)
//setUnloadFilamentStockLoading(false)
}
@ -140,7 +128,7 @@ const UnloadFilamentStock = ({ onOk, reset, printer = null }) => {
}
]}
>
<PrinterSelect checkable={false} />
<ObjectSelect type='printer' checkable={false} />
</Form.Item>
{unloadFilamentStockLoading == false ? (
@ -182,11 +170,11 @@ const UnloadFilamentStock = ({ onOk, reset, printer = null }) => {
/>
)}
{unloadFilamentStockFormValues.printer ? (
{unloadFilamentStockFormValues.printer?._id ? (
<PrinterTemperaturePanel
showHeatedBed={false}
showBed={false}
showMoreInfo={false}
printerId={unloadFilamentStockFormValues.printer._id}
id={unloadFilamentStockFormValues.printer._id}
/>
) : null}
</Flex>
@ -195,65 +183,28 @@ const UnloadFilamentStock = ({ onOk, reset, printer = null }) => {
]
return (
<Flex gap={'middle'}>
{!isMobile && (
<div style={{ minWidth: '160px' }}>
<Steps
current={currentStep}
items={steps}
direction='vertical'
style={{ width: 'fit-content' }}
/>
</div>
)}
{!isMobile && <Divider type={'vertical'} style={{ height: 'unset' }} />}
<Flex vertical={'true'} style={{ flexGrow: 1 }} gap='middle'>
<Title level={2} style={{ marginTop: 0, marginBottom: 4 }}>
Unload Filament Stock
</Title>
<Form
name='unloadFilamentStock'
autoComplete='off'
form={unloadFilamentStockForm}
onFinish={handleUnloadFilamentStock}
onValuesChange={(changedValues) =>
setUnloadFilamentStockFormValues((prevValues) => ({
...prevValues,
...changedValues
}))
}
initialValues={initialUnloadFilamentStockForm}
>
<div style={{ minHeight: '260px' }}>{steps[currentStep].content}</div>
<Flex justify={'end'}>
<Button
style={{
margin: '0 8px'
}}
onClick={() => setCurrentStep(currentStep - 1)}
disabled={!(currentStep > 0)}
>
Previous
</Button>
{currentStep === steps.length - 1 && (
<Button
type='primary'
loading={unloadFilamentStockLoading}
disabled={!nextEnabled}
onClick={() => {
unloadFilamentStockForm.submit()
}}
>
{unloadFilamentStockLoading ? 'Unloading...' : 'Unload'}
</Button>
)}
</Flex>
</Form>
</Flex>
</Flex>
<Form
name='unloadFilamentStock'
autoComplete='off'
form={unloadFilamentStockForm}
onFinish={handleUnloadFilamentStock}
onValuesChange={(changedValues) =>
setUnloadFilamentStockFormValues((prevValues) => ({
...prevValues,
...changedValues
}))
}
initialValues={initialUnloadFilamentStockForm}
>
<WizardView
title='Unload Filament Stock'
steps={steps}
onSubmit={() => unloadFilamentStockForm.submit()}
formValid={formValid}
loading={unloadFilamentStockLoading}
submitText={unloadFilamentStockLoading ? 'Unloading...' : 'Unload'}
/>
</Form>
)
}