Refactor NewPrinter component to utilize NewObjectForm and WizardView for improved structure and user experience; streamline printer setup process and enhance form validation.

This commit is contained in:
Tom Butcher 2025-09-05 23:17:35 +01:00
parent 5300874b80
commit 6c0933b797

View File

@ -1,564 +1,109 @@
import { useState, useContext, useEffect, useCallback } from 'react'
import axios from 'axios'
import {
Form,
Button,
message,
Typography,
Flex,
Steps,
Divider,
Input,
Select,
Space,
Descriptions,
List,
InputNumber,
notification,
Progress,
Modal,
Radio
} from 'antd'
import { SearchOutlined, SettingOutlined } from '@ant-design/icons'
import PropTypes from 'prop-types'
import { PrintServerContext } from '../../context/PrintServerContext'
import EditIcon from '../../../Icons/EditIcon.jsx'
import ObjectInfo from '../../common/ObjectInfo'
import NewObjectForm from '../../common/NewObjectForm'
import WizardView from '../../common/WizardView'
import config from '../../../../config.js'
const { Title } = Typography
const initialNewPrinterForm = {
const NewPrinter = ({ onOk }) => {
return (
<NewObjectForm
type={'printer'}
defaultValues={{
moonraker: {
protocol: 'ws',
host: '',
port: '',
apiKey: ''
}
}
const NewPrinter = ({ onOk, reset }) => {
NewPrinter.propTypes = {
onOk: PropTypes.func.isRequired,
reset: PropTypes.bool.isRequired
}
const { printServer } = useContext(PrintServerContext)
const [messageApi, contextHolder] = message.useMessage()
const [notificationApi, notificationContextHolder] =
notification.useNotification()
const [newPrinterLoading, setNewPrinterLoading] = useState(false)
const [currentStep, setCurrentStep] = useState(0)
const [nextEnabled, setNextEnabled] = useState(false)
const [newPrinterForm] = Form.useForm()
const [newPrinterFormValues, setNewPrinterFormValues] = useState(
initialNewPrinterForm
)
const [discoveredPrinters, setDiscoveredPrinters] = useState([])
const [discovering, setDiscovering] = useState(false)
const [showManualSetup, setShowManualSetup] = useState(false)
const [scanPort, setScanPort] = useState(7125)
const [scanProtocol, setScanProtocol] = useState('ws')
const [editingHostname, setEditingHostname] = useState(null)
const [hostnameInput, setHostnameInput] = useState('')
const [initialized, setInitialized] = useState(false)
const newPrinterFormUpdateValues = Form.useWatch([], newPrinterForm)
useEffect(() => {
newPrinterForm
.validateFields({
validateOnly: true
})
.then(() => {
if (currentStep === 0) {
const moonraker = newPrinterForm.getFieldValue('moonraker')
setNextEnabled(
!!(moonraker?.protocol && moonraker?.host && moonraker?.port)
)
} else if (currentStep === 1) {
const name = newPrinterForm.getFieldValue('name')
setNextEnabled(!!name)
} else {
setNextEnabled(true)
}
})
.catch(() => setNextEnabled(false))
}, [newPrinterForm, newPrinterFormUpdateValues, currentStep])
const summaryItems = [
{
key: 'name',
label: 'Name',
children: newPrinterFormValues.name
port: 7125,
protocol: 'ws'
},
{
key: 'protocol',
label: 'Protocol',
children: newPrinterFormValues.moonraker?.protocol
},
{
key: 'host',
label: 'Host',
children: newPrinterFormValues.moonraker?.host
},
{
key: 'port',
label: 'Port',
children: newPrinterFormValues.moonraker?.port
}
]
useEffect(() => {
if (reset) {
newPrinterForm.resetFields()
}
}, [reset, newPrinterForm])
const handlePrinterSelect = (printer) => {
newPrinterForm.setFieldsValue({
moonraker: {
protocol: printer.protocol,
host: printer.host,
port: printer.port
}
})
setNewPrinterFormValues({
...newPrinterFormValues,
moonraker: {
protocol: printer.protocol,
host: printer.host,
port: printer.port
}
})
}
const handleHostnameEdit = (printer, newHostname) => {
if (newHostname && newHostname.trim() !== '') {
const updatedPrinter = {
...printer,
host: newHostname.trim()
}
setDiscoveredPrinters((prev) =>
prev.map((p) => (p.host === printer.host ? updatedPrinter : p))
)
setEditingHostname(null)
setHostnameInput('')
}
}
const showEditHostnameDialog = (printer) => {
setEditingHostname(printer.host)
setHostnameInput(printer.host)
}
const handleNewPrinter = async () => {
setNewPrinterLoading(true)
try {
await axios.post(
`${config.backendUrl}/printers`,
{
...newPrinterFormValues
},
{
headers: {
Accept: 'application/json'
},
withCredentials: true
}
)
onOk()
} catch (error) {
messageApi.error('Error adding new printer: ' + error.message)
} finally {
setNewPrinterLoading(false)
}
}
const notifyScanNetworkFound = useCallback(
(data) => {
const newPrinter = {
protocol: scanProtocol,
host: data.hostname || data.ip,
port: scanPort
}
notificationApi.info({
message: 'Printer Found',
description: `Printer found: ${data.hostname || data.ip}!`
})
setDiscoveredPrinters((prev) => [...prev, newPrinter])
},
[scanProtocol, scanPort, notificationApi]
)
const notifyScanNetworkComplete = useCallback(
(data) => {
setDiscovering(false)
notificationApi.destroy('network-scan')
if (data == false) {
messageApi.error('Error discovering printers!')
} else {
messageApi.success('Finished discovering printers!')
}
},
[messageApi, notificationApi]
)
const notifyScanNetworkProgress = useCallback(
(data) => {
notificationApi.info({
message: 'Scanning Network',
description: (
<div>
<div style={{ marginBottom: 12 }}>
Scanning IP: {data.currentIP}
</div>
<Progress
percent={data.progress}
size='small'
status='active'
showInfo={false}
/>
</div>
),
duration: 0,
key: 'network-scan',
icon: null,
placement: 'bottomRight',
style: {
width: 360
},
className: 'network-scan-notification',
closeIcon: null,
onClose: () => {},
btn: null
})
},
[notificationApi]
)
const discoverPrinters = useCallback(() => {
if (!discovering) {
setDiscovering(true)
setDiscoveredPrinters([])
messageApi.info('Discovering printers...')
printServer.off('notify_scan_network_found')
printServer.off('notify_scan_network_progress')
printServer.off('notify_scan_network_complete')
printServer.on('notify_scan_network_found', notifyScanNetworkFound)
printServer.on('notify_scan_network_progress', notifyScanNetworkProgress)
printServer.on('notify_scan_network_complete', notifyScanNetworkComplete)
printServer.emit('bridge.scan_network.start', {
port: scanPort,
protocol: scanProtocol
})
}
}, [
discovering,
printServer,
scanPort,
scanProtocol,
messageApi,
notifyScanNetworkFound,
notifyScanNetworkProgress,
notifyScanNetworkComplete
])
useEffect(() => {
setInitialized(true)
if (!initialized) {
discoverPrinters()
}
}, [initialized, discoverPrinters])
const stopDiscovery = () => {
if (discovering) {
setDiscovering(false)
notificationApi.destroy('network-scan')
messageApi.info('Stopping discovery...')
printServer.off('notify_scan_network_found')
printServer.off('notify_scan_network_progress')
printServer.off('notify_scan_network_complete')
printServer.emit('bridge.scan_network.stop', (response) => {
if (response == false) {
messageApi.error('Error stopping discovery!')
}
})
}
}
const handlePortChange = (value) => {
stopDiscovery()
setScanPort(value)
}
const handleProtocolChange = (value) => {
stopDiscovery()
setScanProtocol(value)
}
active: true
}}
>
{({ handleSubmit, submitLoading, objectData, formValid }) => {
const steps = [
{
title: 'Discovery',
key: 'discovery',
content: (
<>
<Flex vertical style={{ width: '100%' }} gap='large'>
{!showManualSetup ? (
<>
<Flex
style={{ width: '100%' }}
justify='space-between'
align='center'
gap='middle'
>
<Space.Compact>
<InputNumber
min={1}
max={65535}
value={scanPort}
onChange={handlePortChange}
style={{ width: '80px' }}
placeholder='Port'
/>
<Select
value={scanProtocol}
onChange={handleProtocolChange}
options={[
{ value: 'ws', label: 'ws' },
{ value: 'wss', label: 'wss' }
]}
/>
<Button
icon={<SearchOutlined />}
onClick={discoverPrinters}
loading={discovering}
>
{discovering ? 'Discovering...' : 'Discover'}
</Button>
</Space.Compact>
<Button
icon={<SettingOutlined />}
onClick={() => setShowManualSetup(true)}
>
Manual Setup
</Button>
</Flex>
<List
dataSource={discoveredPrinters}
renderItem={(printer) => (
<List.Item
key={`${printer.host}:${printer.port}`}
actions={[
<Radio
key='select'
defaultChecked={
newPrinterFormValues.moonraker?.host ===
printer.host
}
onChange={() => handlePrinterSelect(printer)}
/>
]}
>
<List.Item.Meta
title={
<Space>
{printer.host}
{!printer.hostname && (
<Button
type='text'
icon={<EditIcon />}
onClick={() => showEditHostnameDialog(printer)}
/>
)}
</Space>
}
description={`Protocol: ${printer.protocol}, Port: ${printer.port}`}
/>
</List.Item>
)}
/>
<Modal
title='Edit Host'
open={editingHostname !== null}
onOk={() => {
const printer = discoveredPrinters.find(
(p) => p.host === editingHostname
)
if (printer) {
handleHostnameEdit(printer, hostnameInput)
}
}}
onCancel={() => {
setEditingHostname(null)
setHostnameInput('')
}}
>
<Form.Item label='Host' required>
<Input
value={hostnameInput}
onChange={(e) => setHostnameInput(e.target.value)}
placeholder='Enter host'
autoFocus
/>
</Form.Item>
</Modal>
</>
) : (
<>
<Flex style={{ width: '100%' }} justify='end'>
<Button
icon={<SearchOutlined />}
onClick={() => setShowManualSetup(false)}
>
Back to Discovery
</Button>
</Flex>
<Flex vertical>
<Form.Item
label='Protocol'
name={['moonraker', 'protocol']}
rules={[
{ required: true, message: 'Protocol is required' }
]}
>
<Select
defaultValue='ws'
options={[
{ value: 'ws', label: 'Websocket' },
{ value: 'wss', label: 'Websocket Secure' }
]}
/>
</Form.Item>
<Form.Item
label='Host'
name={['moonraker', 'host']}
rules={[{ required: true, message: 'Host is required' }]}
>
<Input />
</Form.Item>
<Form.Item
label='Port'
name={['moonraker', 'port']}
rules={[{ required: true, message: 'Port is required' }]}
>
<InputNumber
min={1}
max={65535}
style={{ width: '100%' }}
/>
</Form.Item>
<Form.Item
label='API Key'
name={['moonraker', 'apiKey']}
rules={[{ required: false }]}
>
<Input.Password
placeholder='Optional API key'
style={{ width: '100%' }}
/>
</Form.Item>
</Flex>
</>
)}
</Flex>
</>
)
},
{
title: 'Required',
key: 'required',
content: (
<>
<Form.Item
label='Name'
name='name'
rules={[{ required: true, message: 'Name is required' }]}
>
<Input />
</Form.Item>
</>
<ObjectInfo
type='printer'
column={1}
bordered={false}
isEditing={true}
required={true}
objectData={objectData}
/>
)
},
{
title: 'Optional',
key: 'optional',
content: (
<ObjectInfo
type='printer'
column={1}
bordered={false}
isEditing={true}
required={false}
objectData={objectData}
visibleProperties={{
firmware: false,
currentFilamentStock: false,
'currentFilamentStock._id': false,
currentJob: false,
'currentJob._id': false,
currentSubJob: false,
'currentSubJob._id': false,
alerts: false
}}
/>
)
},
{
title: 'Summary',
key: 'summary',
content: (
<Form.Item>
<Descriptions column={1} items={summaryItems} size={'small'} />
</Form.Item>
<ObjectInfo
type='printer'
column={1}
bordered={false}
visibleProperties={{
_id: false,
createdAt: false,
updatedAt: false,
connectedAt: false,
state: false,
firmware: false,
currentFilamentStock: false,
'currentFilamentStock._id': false,
currentJob: false,
'currentJob._id': false,
currentSubJob: false,
'currentSubJob._id': false,
alerts: false
}}
isEditing={false}
objectData={objectData}
/>
)
}
]
return (
<Flex gap={'middle'}>
{contextHolder}
{notificationContextHolder}
<div style={{ minWidth: '160px' }}>
<Steps current={currentStep} items={steps} direction='vertical' />
</div>
<Divider type={'vertical'} style={{ height: 'unset' }} />
<Flex vertical={'true'} style={{ flexGrow: 1 }}>
<Title level={2} style={{ marginTop: 0 }}>
New Printer
</Title>
<Form
name='basic'
autoComplete='off'
form={newPrinterForm}
onFinish={handleNewPrinter}
onValuesChange={(changedValues) =>
setNewPrinterFormValues((prevValues) => ({
...prevValues,
...changedValues
}))
}
initialValues={initialNewPrinterForm}
>
<div style={{ minHeight: '260px' }}>{steps[currentStep].content}</div>
<Flex justify={'end'}>
<Button
style={{
margin: '0 8px'
<WizardView
steps={steps}
loading={submitLoading}
formValid={formValid}
title='New Printer'
onSubmit={() => {
handleSubmit()
onOk()
}}
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'
htmlType='submit'
loading={newPrinterLoading}
>
Done
</Button>
)}
</Flex>
</Form>
</Flex>
</Flex>
</NewObjectForm>
)
}
NewPrinter.propTypes = {
onOk: PropTypes.func.isRequired,
reset: PropTypes.bool
}
export default NewPrinter