Compare commits

..

No commits in common. "c6c99bb02cb6c21eb59979153745a94e1e65aafc" and "c2f55a596784d6f210526e9be0709ec8ba1a998d" have entirely different histories.

26 changed files with 983 additions and 2123 deletions

View File

@ -43,7 +43,7 @@
vertical-align: middle !important; vertical-align: middle !important;
} }
.ant-typography-ellipsis-single-line > code { .ant-typography-ellipsis-single-line >code {
vertical-align: top !important; vertical-align: top !important;
} }
@ -55,9 +55,7 @@
-webkit-app-region: drag; -webkit-app-region: drag;
} }
.electron-navigation-wrapper li, .electron-navigation-wrapper li, .electron-navigation-wrapper button, .electron-navigation-wrapper .ant-tag{
.electron-navigation-wrapper button,
.electron-navigation-wrapper .ant-tag {
-webkit-app-region: no-drag; -webkit-app-region: no-drag;
} }
@ -69,8 +67,7 @@
padding-inline: 10px; padding-inline: 10px;
} }
.electron-sidebar .ant-menu-item, .electron-sidebar .ant-menu-item, .electron-sidebar .ant-menu-submenu-title {
.electron-sidebar .ant-menu-submenu-title {
height: 32.5px !important; height: 32.5px !important;
line-height: 32.5px !important; line-height: 32.5px !important;
} }
@ -80,10 +77,11 @@
min-width: 55px !important; min-width: 55px !important;
} }
.loading-modal .ant-modal-footer { .loading-modal .ant-modal-footer {
display: none; display: none;
} }
:root { :root {
--unit-100vh: 100vh; --unit-100vh: 100vh;
} }
@ -93,8 +91,8 @@
} }
} }
.dashboard-cards-header .ant-table-tbody { .dashboard-cards-header .ant-table-tbody {
display: none; display: none;
} }
.ant-menu-overflow-item-rest::after { .ant-menu-overflow-item-rest::after {
@ -227,21 +225,19 @@ body {
} }
/* --- End of src/components/Dashboard/common/DashboardSidebar.css --- */ /* --- End of src/components/Dashboard/common/DashboardSidebar.css --- */
.objectTableDescritions > .ant-descriptions-view > table { .objectTableDescritions >.ant-descriptions-view > table {
table-layout: fixed !important; table-layout: fixed !important;
} }
.objectTableDescritions .objectTableDescritions >.ant-descriptions-view .ant-descriptions-row >.ant-descriptions-item-label {
> .ant-descriptions-view
.ant-descriptions-row
> .ant-descriptions-item-label {
width: 35%; width: 35%;
} }
.farmcontrol-splitter > .ant-splitter-bar { .farmcontrol-splitter > .ant-splitter-bar {
margin: 8px; margin: 8px
} }
.dark-mode { .dark-mode {
--sb-track-color: #1d1d1d; --sb-track-color: #1d1d1d;
--sb-thumb-color: #848484; --sb-thumb-color: #848484;
@ -253,31 +249,32 @@ body {
--sb-size: 8px; --sb-size: 8px;
} }
::-webkit-scrollbar { ::-webkit-scrollbar {
width: 8px; width: 8px;
} }
::-webkit-scrollbar:vertical { ::-webkit-scrollbar:vertical {
width: 8px; width: 8px;
} }
::-webkit-scrollbar:horizontal { ::-webkit-scrollbar:horizontal {
height: 8px; height: 8px;
} }
/* Track */ /* Track */
::-webkit-scrollbar-track { ::-webkit-scrollbar-track {
background: transparent; background: transparent;
} }
/* Handle */ /* Handle */
::-webkit-scrollbar-thumb { ::-webkit-scrollbar-thumb {
background: #8888881a; background: #8888881a;
border: 2px solid rgba(0, 0, 0, 0); border: 2px solid rgba(0, 0, 0, 0);
background-clip: padding-box; background-clip: padding-box;
transition: all 1s; transition: all 1s;
-moz-transition: all 1s; -moz-transition: all 1s;
-webkit-transition: all 1s; -webkit-transition: all 1s;
} }
::-webkit-scrollbar-corner { ::-webkit-scrollbar-corner {
@ -294,9 +291,9 @@ body {
/* Handle on hover */ /* Handle on hover */
::-webkit-scrollbar-thumb:hover { ::-webkit-scrollbar-thumb:hover {
background: #5555551f; background: #5555551f;
border: 2px solid rgba(0, 0, 0, 0); border: 2px solid rgba(0, 0, 0, 0);
background-clip: padding-box; background-clip: padding-box;
} }
::-webkit-scrollbar-thumb:vertical:hover { ::-webkit-scrollbar-thumb:vertical:hover {
@ -309,8 +306,4 @@ body {
.ant-table-body { .ant-table-body {
scrollbar-color: auto; scrollbar-color: auto;
} }
.ant-select-selection-item .ant-tag {
margin-left: 1px !important;
}

855
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

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

View File

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

View File

@ -3,26 +3,12 @@ import ObjectInfo from '../../common/ObjectInfo'
import NewObjectForm from '../../common/NewObjectForm' import NewObjectForm from '../../common/NewObjectForm'
import WizardView from '../../common/WizardView' import WizardView from '../../common/WizardView'
import TemplatePreview from '../../common/TemplatePreview' import TemplatePreview from '../../common/TemplatePreview'
import { ApiServerContext } from '../../context/ApiServerContext'
import { useContext, useRef, useState } from 'react'
import dayjs from 'dayjs'
const NewDocumentJob = ({ onOk, defaultValues = {} }) => { const NewDocumentJob = ({ onOk, defaultValues = {} }) => {
const { sendObjectAction, downloadTemplatePDF, formatFileName } =
useContext(ApiServerContext)
const [downloading, setDownloading] = useState(false)
// Capture initial default values so later prop changes don't re-initialize the form
const defaultValuesRef = useRef({
objectType: 'documentJob',
...defaultValues,
saveToFile: false
})
return ( return (
<NewObjectForm <NewObjectForm
type={'documentJob'} type={'documentJob'}
defaultValues={defaultValuesRef.current} defaultValues={{ objectType: 'documentJob', ...defaultValues }}
> >
{({ handleSubmit, submitLoading, objectData, formValid }) => { {({ handleSubmit, submitLoading, objectData, formValid }) => {
const steps = [ const steps = [
@ -42,10 +28,6 @@ const NewDocumentJob = ({ onOk, defaultValues = {} }) => {
) )
} }
] ]
const fileName =
formatFileName(
objectData?.name + ' ' + dayjs().format('YYYY-MM-DD HH:mm:ss')
) || 'document'
return ( return (
<WizardView <WizardView
steps={steps} steps={steps}
@ -53,10 +35,8 @@ const NewDocumentJob = ({ onOk, defaultValues = {} }) => {
title={'Print Document'} title={'Print Document'}
formValid={formValid} formValid={formValid}
loading={submitLoading} loading={submitLoading}
disabled={downloading}
sideBarGrow={true}
sideBar={ sideBar={
<div style={{ minHeight: '500px', flexGrow: 1 }}> <div style={{ minWidth: '400px', minHeight: '500px' }}>
<TemplatePreview <TemplatePreview
objectData={objectData?.object} objectData={objectData?.object}
documentTemplate={objectData?.documentTemplate} documentTemplate={objectData?.documentTemplate}
@ -66,52 +46,10 @@ const NewDocumentJob = ({ onOk, defaultValues = {} }) => {
/> />
</div> </div>
} }
onSubmit={async () => { onSubmit={() => {
const newDocumentJob = await handleSubmit() handleSubmit()
if (newDocumentJob.sendToFile == true) { onOk()
sendObjectAction(newDocumentJob._id, 'documentJob', {
type: 'print',
data: newDocumentJob
})
}
if (onOk) {
onOk()
}
}} }}
actions={[
{
label: 'Download',
steps: ['required'],
loading: downloading == true,
disabled: downloading == true || submitLoading == true,
children: [
{
label: 'PDF',
key: 'pdf',
onClick: () => {
setDownloading(true)
downloadTemplatePDF(
objectData.documentTemplate._id,
objectData.documentTemplate.content,
objectData.object,
fileName,
() => {
setDownloading(false)
}
)
}
},
{
label: 'PNG',
key: 'png'
},
{
label: 'JPEG',
key: 'jpeg'
}
]
}
]}
/> />
) )
}} }}

View File

@ -1,6 +1,5 @@
import { useRef, useState } from 'react' import { useRef } from 'react'
import { Button, Flex, Space, Dropdown, message, Modal } from 'antd' import { Button, Flex, Space, Dropdown } from 'antd'
import PlusIcon from '../../Icons/PlusIcon'
import ObjectTable from '../common/ObjectTable' import ObjectTable from '../common/ObjectTable'
import ReloadIcon from '../../Icons/ReloadIcon' import ReloadIcon from '../../Icons/ReloadIcon'
import useColumnVisibility from '../hooks/useColumnVisibility' import useColumnVisibility from '../hooks/useColumnVisibility'
@ -8,12 +7,10 @@ import GridIcon from '../../Icons/GridIcon'
import ListIcon from '../../Icons/ListIcon' import ListIcon from '../../Icons/ListIcon'
import useViewMode from '../hooks/useViewMode' import useViewMode from '../hooks/useViewMode'
import ColumnViewButton from '../common/ColumnViewButton' import ColumnViewButton from '../common/ColumnViewButton'
import NewDocumentPrinter from './DocumentPrinters/NewDocumentPrinter'
const DocumentPrinters = () => { const DocumentPrinters = () => {
const [messageApi, contextHolder] = message.useMessage()
const tableRef = useRef() const tableRef = useRef()
const [newDocumentPrinterOpen, setNewDocumentPrinterOpen] = useState(false)
const [viewMode, setViewMode] = useViewMode('documentPrinter') const [viewMode, setViewMode] = useViewMode('documentPrinter')
const [columnVisibility, setColumnVisibility] = const [columnVisibility, setColumnVisibility] =
@ -21,12 +18,6 @@ const DocumentPrinters = () => {
const actionItems = { const actionItems = {
items: [ items: [
{
label: 'New Document Printer',
key: 'newDocumentPrinter',
icon: <PlusIcon />
},
{ type: 'divider' },
{ {
label: 'Reload List', label: 'Reload List',
key: 'reloadList', key: 'reloadList',
@ -36,8 +27,6 @@ const DocumentPrinters = () => {
onClick: ({ key }) => { onClick: ({ key }) => {
if (key === 'reloadList') { if (key === 'reloadList') {
tableRef.current?.reload() tableRef.current?.reload()
} else if (key === 'newDocumentPrinter') {
setNewDocumentPrinterOpen(true)
} }
} }
} }
@ -45,7 +34,6 @@ const DocumentPrinters = () => {
return ( return (
<> <>
<Flex vertical={'true'} gap='large'> <Flex vertical={'true'} gap='large'>
{contextHolder}
<Flex justify={'space-between'}> <Flex justify={'space-between'}>
<Space size='small'> <Space size='small'>
<Dropdown menu={actionItems}> <Dropdown menu={actionItems}>
@ -74,22 +62,6 @@ const DocumentPrinters = () => {
cards={viewMode === 'cards'} cards={viewMode === 'cards'}
/> />
</Flex> </Flex>
<Modal
open={newDocumentPrinterOpen}
onCancel={() => setNewDocumentPrinterOpen(false)}
footer={null}
destroyOnHidden={true}
width={700}
>
<NewDocumentPrinter
onOk={() => {
setNewDocumentPrinterOpen(false)
messageApi.success('New note type created successfully.')
tableRef.current?.reload()
}}
reset={!newDocumentPrinterOpen}
/>
</Modal>
</> </>
) )
} }

View File

@ -1,4 +1,3 @@
import { useRef, useState } from 'react'
import { useLocation } from 'react-router-dom' import { useLocation } from 'react-router-dom'
import { Space, Flex, Card } from 'antd' import { Space, Flex, Card } from 'antd'
import { LoadingOutlined } from '@ant-design/icons' import { LoadingOutlined } from '@ant-design/icons'
@ -26,8 +25,6 @@ log.setLevel(config.logLevel)
const DocumentPrinterInfo = () => { const DocumentPrinterInfo = () => {
const location = useLocation() const location = useLocation()
const objectFormRef = useRef(null)
const actionHandlerRef = useRef(null)
const documentPrinterId = new URLSearchParams(location.search).get( const documentPrinterId = new URLSearchParams(location.search).get(
'documentPrinterId' 'documentPrinterId'
) )
@ -35,125 +32,122 @@ const DocumentPrinterInfo = () => {
'DocumentPrinterInfo', 'DocumentPrinterInfo',
{ {
info: true, info: true,
stocks: true,
notes: true, notes: true,
auditLogs: true auditLogs: true
} }
) )
const [objectFormState, setEditFormState] = useState({
isEditing: false,
editLoading: false,
formValid: false,
locked: false,
loading: false,
objectData: {}
})
const actions = {
reload: () => {
objectFormRef?.current.handleFetchObject()
return true
},
edit: () => {
objectFormRef?.current.startEditing()
return false
},
cancelEdit: () => {
objectFormRef?.current.cancelEditing()
return true
},
finishEdit: () => {
objectFormRef?.current.handleUpdate()
return true
}
}
return ( return (
<> <ObjectForm
<Flex id={documentPrinterId}
gap='large' type='documentPrinter'
vertical='true' style={{ height: '100%' }}
style={{ >
maxHeight: '100%', {({
minHeight: 0 loading,
}} isEditing,
> startEditing,
<Flex justify={'space-between'}> cancelEditing,
<Space size='middle'> handleUpdate,
<Space size='small'> formValid,
<ObjectActions objectData,
type='documentPrinter' editLoading,
id={documentPrinterId} lock,
disabled={objectFormState.loading} fetchObject
objectData={objectFormState.objectData} }) => {
/> // Define actions for ActionHandler
<ViewButton const actions = {
disabled={objectFormState.loading} reload: () => {
items={[ fetchObject()
{ return true
key: 'info', },
label: 'DocumentPrinter Information' edit: () => {
}, startEditing()
{ key: 'notes', label: 'Notes' }, return false
{ key: 'auditLogs', label: 'Audit Logs' } },
]} cancelEdit: () => {
visibleState={collapseState} cancelEditing()
updateVisibleState={updateCollapseState} return true
/> },
<DocumentPrintButton finishEdit: () => {
type='documentPrinter' handleUpdate()
objectData={objectFormState.objectData} return true
disabled={objectFormState.loading} }
/> }
</Space>
<LockIndicator lock={objectFormState.lock} />
</Space>
<Space>
<EditButtons
isEditing={objectFormState.isEditing}
handleUpdate={() => {
actionHandlerRef.current.callAction('finishEdit')
}}
cancelEditing={() => {
actionHandlerRef.current.callAction('cancelEdit')
}}
startEditing={() => {
actionHandlerRef.current.callAction('edit')
}}
editLoading={objectFormState.editLoading}
formValid={objectFormState.formValid}
disabled={objectFormState.lock?.locked || objectFormState.loading}
loading={objectFormState.editLoading}
/>
</Space>
</Flex>
<div style={{ height: '100%', overflowY: 'scroll' }}> return (
<Flex vertical gap={'large'}> <ActionHandler actions={actions} loading={loading}>
<ActionHandler {({ callAction }) => (
actions={actions} <Flex
loading={objectFormState.loading} gap='large'
ref={actionHandlerRef} vertical='true'
> style={{
<InfoCollapse maxHeight: '100%',
title='Document Printer Information' minHeight: 0
icon={<InfoCircleIcon />} }}
active={collapseState.info}
onToggle={(expanded) => updateCollapseState('info', expanded)}
collapseKey='info'
> >
<ObjectForm <Flex justify={'space-between'}>
id={documentPrinterId} <Space size='middle'>
type='documentPrinter' <Space size='small'>
style={{ height: '100%' }} <ObjectActions
ref={objectFormRef} type='documentPrinter'
onStateChange={(state) => { id={documentPrinterId}
setEditFormState((prev) => ({ ...prev, ...state })) disabled={loading}
}} objectData={objectData}
> />
{({ loading, isEditing, objectData }) => { <ViewButton
return ( disabled={loading}
items={[
{
key: 'info',
label: 'DocumentPrinter Information'
},
{ key: 'stocks', label: 'DocumentPrinter Stocks' },
{ key: 'notes', label: 'Notes' },
{ key: 'auditLogs', label: 'Audit Logs' }
]}
visibleState={collapseState}
updateVisibleState={updateCollapseState}
/>
<DocumentPrintButton
type='documentPrinter'
objectData={objectData}
disabled={loading}
/>
</Space>
<LockIndicator lock={lock} />
</Space>
<Space>
<EditButtons
isEditing={isEditing}
handleUpdate={() => {
callAction('finishEdit')
}}
cancelEditing={() => {
callAction('cancelEdit')
}}
startEditing={() => {
callAction('edit')
}}
editLoading={editLoading}
formValid={formValid}
disabled={lock?.locked || loading}
loading={editLoading}
/>
</Space>
</Flex>
<div style={{ height: '100%', overflowY: 'scroll' }}>
<Flex vertical gap={'large'}>
<InfoCollapse
title='Document Printer Information'
icon={<InfoCircleIcon />}
active={collapseState.info}
onToggle={(expanded) =>
updateCollapseState('info', expanded)
}
collapseKey='info'
>
<ObjectInfo <ObjectInfo
loading={loading} loading={loading}
indicator={<LoadingOutlined />} indicator={<LoadingOutlined />}
@ -161,47 +155,52 @@ const DocumentPrinterInfo = () => {
type='documentPrinter' type='documentPrinter'
objectData={objectData} objectData={objectData}
/> />
) </InfoCollapse>
}}
</ObjectForm>
</InfoCollapse>
</ActionHandler>
<InfoCollapse <InfoCollapse
title='Notes' title='Notes'
icon={<NoteIcon />} icon={<NoteIcon />}
active={collapseState.notes} active={collapseState.notes}
onToggle={(expanded) => updateCollapseState('notes', expanded)} onToggle={(expanded) =>
collapseKey='notes' updateCollapseState('notes', expanded)
> }
<Card> collapseKey='notes'
<NotesPanel _id={documentPrinterId} type='documentPrinter' /> >
</Card> <Card>
</InfoCollapse> <NotesPanel
_id={documentPrinterId}
type='documentPrinter'
/>
</Card>
</InfoCollapse>
<InfoCollapse <InfoCollapse
title='Audit Logs' title='Audit Logs'
icon={<AuditLogIcon />} icon={<AuditLogIcon />}
active={collapseState.auditLogs} active={collapseState.auditLogs}
onToggle={(expanded) => onToggle={(expanded) =>
updateCollapseState('auditLogs', expanded) updateCollapseState('auditLogs', expanded)
} }
collapseKey='auditLogs' collapseKey='auditLogs'
> >
{objectFormState.loading ? ( {loading ? (
<InfoCollapsePlaceholder /> <InfoCollapsePlaceholder />
) : ( ) : (
<ObjectTable <ObjectTable
type='auditLog' type='auditLog'
masterFilter={{ 'parent._id': documentPrinterId }} masterFilter={{ 'parent._id': documentPrinterId }}
visibleColumns={{ _id: false, 'parent._id': false }} visibleColumns={{ _id: false, 'parent._id': false }}
/> />
)} )}
</InfoCollapse> </InfoCollapse>
</Flex> </Flex>
</div> </div>
</Flex> </Flex>
</> )}
</ActionHandler>
)
}}
</ObjectForm>
) )
} }

View File

@ -1,85 +0,0 @@
import PropTypes from 'prop-types'
import ObjectInfo from '../../common/ObjectInfo'
import NewObjectForm from '../../common/NewObjectForm'
import WizardView from '../../common/WizardView'
const NewDocumentPrinter = ({ onOk }) => {
return (
<NewObjectForm
type={'documentPrinter'}
defaultValues={{ active: true, global: false }}
>
{({ handleSubmit, submitLoading, objectData, formValid }) => {
const steps = [
{
title: 'Required',
key: 'required',
content: (
<ObjectInfo
type='documentPrinter'
column={1}
bordered={false}
isEditing={true}
required={true}
objectData={objectData}
/>
)
},
{
title: 'Optional',
key: 'optional',
content: (
<ObjectInfo
type='documentPrinter'
column={1}
bordered={false}
isEditing={true}
required={false}
visibleProperties={{ content: false, testObject: false }}
objectData={objectData}
/>
)
},
{
title: 'Summary',
key: 'summary',
content: (
<ObjectInfo
type='documentPrinter'
column={1}
bordered={false}
visibleProperties={{
_id: false,
createdAt: false,
updatedAt: false
}}
isEditing={false}
objectData={objectData}
/>
)
}
]
return (
<WizardView
steps={steps}
loading={submitLoading}
formValid={formValid}
title='New Document Printer'
onSubmit={() => {
handleSubmit()
onOk()
}}
/>
)
}}
</NewObjectForm>
)
}
NewDocumentPrinter.propTypes = {
onOk: PropTypes.func.isRequired,
reset: PropTypes.bool
}
export default NewDocumentPrinter

View File

@ -1,9 +1,19 @@
import PropTypes from 'prop-types' import PropTypes from 'prop-types'
import { useState } from 'react'
import { useMediaQuery } from 'react-responsive'
import { Typography, Flex, Steps, Divider } from 'antd'
import ObjectInfo from '../../common/ObjectInfo' import ObjectInfo from '../../common/ObjectInfo'
import NewObjectForm from '../../common/NewObjectForm' import NewObjectForm from '../../common/NewObjectForm'
import WizardView from '../../common/WizardView' import NewObjectButtons from '../../common/NewObjectButtons'
const { Title } = Typography
const NewDocumentTemplate = ({ onOk }) => { const NewDocumentTemplate = ({ onOk }) => {
const [currentStep, setCurrentStep] = useState(0)
const isMobile = useMediaQuery({ maxWidth: 768 })
return ( return (
<NewObjectForm <NewObjectForm
type={'documentTemplate'} type={'documentTemplate'}
@ -59,18 +69,44 @@ const NewDocumentTemplate = ({ onOk }) => {
) )
} }
] ]
return ( return (
<WizardView <Flex gap='middle'>
steps={steps} {!isMobile && (
loading={submitLoading} <div style={{ minWidth: '160px' }}>
formValid={formValid} <Steps
title='New Document Template' current={currentStep}
onSubmit={() => { items={steps}
handleSubmit() direction='vertical'
onOk() style={{ width: 'fit-content' }}
}} />
/> </div>
)}
{!isMobile && (
<Divider type='vertical' style={{ height: 'unset' }} />
)}
<Flex vertical gap='middle' style={{ flexGrow: 1 }}>
<Title level={2} style={{ margin: 0 }}>
New Document Template
</Title>
<div style={{ minHeight: '260px', marginBottom: 8 }}>
{steps[currentStep].content}
</div>
<NewObjectButtons
currentStep={currentStep}
totalSteps={steps.length}
onPrevious={() => setCurrentStep((prev) => prev - 1)}
onNext={() => setCurrentStep((prev) => prev + 1)}
onSubmit={() => {
handleSubmit()
onOk()
}}
formValid={formValid}
submitLoading={submitLoading}
/>
</Flex>
</Flex>
) )
}} }}
</NewObjectForm> </NewObjectForm>

View File

@ -1,6 +1,6 @@
import { useState, useRef, useEffect, useContext } from 'react' import { useState, useRef, useEffect, useContext } from 'react'
import { useLocation } from 'react-router-dom' import { useLocation } from 'react-router-dom'
import { Space, Flex, Card, Splitter, Divider, Modal } from 'antd' import { Space, Flex, Card, Splitter, Divider } from 'antd'
import loglevel from 'loglevel' import loglevel from 'loglevel'
import config from '../../../../config.js' import config from '../../../../config.js'
import useCollapseState from '../../hooks/useCollapseState.js' import useCollapseState from '../../hooks/useCollapseState.js'
@ -28,9 +28,6 @@ import { useMediaQuery } from 'react-responsive'
import AlertsDisplay from '../../common/AlertsDisplay.jsx' import AlertsDisplay from '../../common/AlertsDisplay.jsx'
import { ApiServerContext } from '../../context/ApiServerContext.jsx' import { ApiServerContext } from '../../context/ApiServerContext.jsx'
import LoadFilamentStock from '../../Inventory/FilamentStocks/LoadFilamentStock.jsx'
import UnloadFilamentStock from '../../Inventory/FilamentStocks/UnloadFilamentStock.jsx'
const log = loglevel.getLogger('ControlPrinter') const log = loglevel.getLogger('ControlPrinter')
log.setLevel(config.logLevel) log.setLevel(config.logLevel)
@ -60,9 +57,6 @@ const ControlPrinter = () => {
collapseState.movement collapseState.movement
) )
const [loadFilamentStockOpen, setLoadFilamentStockOpen] = useState(false)
const [unloadFilamentStockOpen, setUnloadFilamentStockOpen] = useState(false)
useEffect(() => { useEffect(() => {
setSideBarVisible( setSideBarVisible(
collapseState.temperature || collapseState.temperature ||
@ -128,39 +122,6 @@ const ControlPrinter = () => {
}) })
} }
return true 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
},
loadFilamentStock: () => {
setLoadFilamentStockOpen(true)
return true
},
unloadFilamentStock: () => {
setUnloadFilamentStockOpen(true)
return true
} }
} }
@ -185,338 +146,298 @@ const ControlPrinter = () => {
) )
return ( return (
<> <Flex
<Flex gap='large'
gap='large' vertical='true'
vertical='true' style={{
style={{ maxHeight: '100%',
maxHeight: '100%', minHeight: 0
minHeight: 0 }}
}} >
> <Flex justify={'space-between'}>
<Flex justify={'space-between'}> <Space size='middle'>
<Space size='middle'> <Space size='small'>
<Space size='small'> <ObjectActions
<ObjectActions type='printer'
type='printer' id={printerId}
id={printerId} disabled={objectFormState.loading}
disabled={objectFormState.loading} visibleActions={{ edit: false }}
visibleActions={{ edit: false }} objectData={objectFormState.objectData}
objectData={objectFormState.objectData}
/>
<ViewButton
disabled={objectFormState.loading}
items={[
{
key: 'printer',
label: 'Printer'
},
{
key: 'job',
label: 'Job'
},
{
key: 'subJob',
label: 'Sub Job'
},
{
key: 'filamentStock',
label: 'Filament Stock'
},
{
key: 'temperature',
label: 'Temperature'
},
{
key: 'position',
label: 'Position'
},
{
key: 'movement',
label: 'Movement'
},
{ key: 'notes', label: 'Notes' }
]}
visibleState={collapseState}
updateVisibleState={updateCollapseState}
/>
<AlertsDisplay alerts={objectFormState.objectData?.alerts} />
</Space>
</Space>
<Space>
<EditButtons
isEditing={objectFormState.isEditing}
handleUpdate={() => {
actionHandlerRef.current.callAction('finishEdit')
}}
cancelEditing={() => {
actionHandlerRef.current.callAction('cancelEdit')
}}
startEditing={() => {
actionHandlerRef.current.callAction('edit')
}}
editLoading={objectFormState.editLoading}
formValid={objectFormState.formValid}
disabled={objectFormState.lock?.locked || objectFormState.loading}
loading={objectFormState.editLoading}
/> />
<ViewButton
disabled={objectFormState.loading}
items={[
{
key: 'printer',
label: 'Printer'
},
{
key: 'job',
label: 'Job'
},
{
key: 'subJob',
label: 'Sub Job'
},
{
key: 'filamentStock',
label: 'Filament Stock'
},
{
key: 'temperature',
label: 'Temperature'
},
{
key: 'position',
label: 'Position'
},
{
key: 'movement',
label: 'Movement'
},
{ key: 'notes', label: 'Notes' }
]}
visibleState={collapseState}
updateVisibleState={updateCollapseState}
/>
<AlertsDisplay alerts={objectFormState.objectData?.alerts} />
</Space> </Space>
</Flex> </Space>
<Space>
<EditButtons
isEditing={objectFormState.isEditing}
handleUpdate={() => {
actionHandlerRef.current.callAction('finishEdit')
}}
cancelEditing={() => {
actionHandlerRef.current.callAction('cancelEdit')
}}
startEditing={() => {
actionHandlerRef.current.callAction('edit')
}}
editLoading={objectFormState.editLoading}
formValid={objectFormState.formValid}
disabled={objectFormState.lock?.locked || objectFormState.loading}
loading={objectFormState.editLoading}
/>
</Space>
</Flex>
<div style={{ height: '100%', overflowY: 'scroll' }}> <div style={{ height: '100%', overflowY: 'scroll' }}>
<Flex vertical gap={'large'}> <Flex vertical gap={'large'}>
<ActionHandler <ActionHandler
actions={actions} actions={actions}
loading={objectFormState.loading} loading={objectFormState.loading}
ref={actionHandlerRef} ref={actionHandlerRef}
> >
<Flex vertical> <Flex vertical>
<Splitter className={'farmcontrol-splitter'}> <Splitter className={'farmcontrol-splitter'}>
<Splitter.Panel> <Splitter.Panel>
<Flex vertical gap={'large'}> <Flex vertical gap={'large'}>
<InfoCollapse <InfoCollapse
title={'Printer'} title={'Printer'}
icon={<PrinterIcon />} icon={<PrinterIcon />}
collapseKey='printer' collapseKey='printer'
active={collapseState.printer} active={collapseState.printer}
onToggle={(expanded) => onToggle={(expanded) =>
updateCollapseState('printer', expanded) updateCollapseState('printer', expanded)
} }
>
<ObjectForm
id={printerId}
type='printer'
ref={objectFormRef}
onStateChange={(state) => {
console.log('Got edit form state change', state)
setEditFormState((prev) => ({ ...prev, ...state }))
}}
> >
{({
loading: printerObjectLoading,
objectData: printerObjectData
}) => {
return (
<ObjectInfo
loading={printerObjectLoading}
column={sideBarVisible ? 1 : undefined}
visibleProperties={{
connectedAt: false,
vendor: false,
'vendor._id': false,
host: false,
'host._id': false,
'moonraker.port': false,
'moonraker.apiKey': false,
'moonraker.protocol': false,
'moonraker.host': false,
tags: false,
firmware: false,
alerts: false
}}
objectData={printerObjectData}
type='printer'
/>
)
}}
</ObjectForm>
</InfoCollapse>
<InfoCollapse
title={'Job'}
icon={<JobIcon />}
collapseKey='job'
active={collapseState.job}
onToggle={(expanded) =>
updateCollapseState('job', expanded)
}
>
{objectFormState.objectData?.currentJob?._id ? (
<ObjectForm <ObjectForm
id={printerId} id={objectFormState.objectData.currentJob._id}
type='printer' type='job'
ref={objectFormRef} onStateChange={() => {}}
onStateChange={(state) => {
console.log('Got edit form state change', state)
setEditFormState((prev) => ({ ...prev, ...state }))
}}
> >
{({ {({
loading: printerObjectLoading, loading: jobObjectLoading,
objectData: printerObjectData objectData: jobObjectData
}) => { }) => {
return ( return (
<ObjectInfo <ObjectInfo
loading={printerObjectLoading} loading={jobObjectLoading}
column={sideBarVisible ? 1 : undefined} column={sideBarVisible ? 1 : undefined}
visibleProperties={{ visibleProperties={{
connectedAt: false, printers: false,
vendor: false, createdAt: false
'vendor._id': false,
host: false,
'host._id': false,
'moonraker.port': false,
'moonraker.apiKey': false,
'moonraker.protocol': false,
'moonraker.host': false,
tags: false,
firmware: false,
alerts: false,
online: false,
active: false,
currentFilamentStock: false,
'currentFilamentStock._id': false,
currentJob: false,
'currentJob._id': false,
currentSubJob: false,
'currentSubJob._id': false
}} }}
objectData={printerObjectData} objectData={jobObjectData}
type='printer' type='job'
/> />
) )
}} }}
</ObjectForm> </ObjectForm>
</InfoCollapse> ) : (
<InfoCollapse <MissingPlaceholder
title={'Job'} message={'No job.'}
icon={<JobIcon />} hasBackground={false}
collapseKey='job' />
active={collapseState.job} )}
onToggle={(expanded) => </InfoCollapse>
updateCollapseState('job', expanded) <InfoCollapse
} title={'Sub Job'}
> icon={<SubJobIcon />}
{objectFormState.objectData?.currentJob?._id ? ( collapseKey='subJob'
<ObjectForm active={collapseState.subJob}
id={objectFormState.objectData.currentJob._id} onToggle={(expanded) =>
type='job' updateCollapseState('subJob', expanded)
onStateChange={() => {}} }
>
{({
loading: jobObjectLoading,
objectData: jobObjectData
}) => {
return (
<ObjectInfo
loading={jobObjectLoading}
column={sideBarVisible ? 1 : undefined}
visibleProperties={{
printers: false,
createdAt: false
}}
objectData={jobObjectData}
type='job'
/>
)
}}
</ObjectForm>
) : (
<MissingPlaceholder
message={'No job.'}
hasBackground={false}
/>
)}
</InfoCollapse>
<InfoCollapse
title={'Sub Job'}
icon={<SubJobIcon />}
collapseKey='subJob'
active={collapseState.subJob}
onToggle={(expanded) =>
updateCollapseState('subJob', expanded)
}
>
{objectFormState.objectData?.currentSubJob?._id ? (
<ObjectForm
id={objectFormState.objectData.currentSubJob._id}
type='subJob'
onStateChange={() => {}}
>
{({
loading: subJobObjectLoading,
objectData: subJobObjectData
}) => {
return (
<ObjectInfo
loading={subJobObjectLoading}
column={sideBarVisible ? 1 : undefined}
visibleProperties={{
printers: false,
createdAt: false
}}
objectData={subJobObjectData}
type='subJob'
/>
)
}}
</ObjectForm>
) : (
<MissingPlaceholder
message={'No sub job.'}
hasBackground={false}
/>
)}
</InfoCollapse>
<InfoCollapse
title={'Filament Stock'}
icon={<FilamentStockIcon />}
collapseKey='filamentStock'
active={collapseState.filamentStock}
onToggle={(expanded) =>
updateCollapseState('filamentStock', expanded)
}
>
{objectFormState.objectData?.currentFilamentStock
?._id ? (
<ObjectForm
id={
objectFormState.objectData.currentFilamentStock
._id
}
type='filamentStock'
onStateChange={() => {}}
>
{({
loading: filamentStockObjectLoading,
objectData: filamentStockObjectData
}) => {
return (
<ObjectInfo
loading={filamentStockObjectLoading}
column={sideBarVisible ? 1 : undefined}
showHyperlink={true}
visibleProperties={{
updatedAt: false,
createdAt: false
}}
objectData={filamentStockObjectData}
type='filamentStock'
/>
)
}}
</ObjectForm>
) : (
<MissingPlaceholder
message={'No filament stock.'}
hasBackground={false}
/>
)}
</InfoCollapse>
</Flex>
</Splitter.Panel>
{sideBarVisible && !isMobile ? (
<Splitter.Panel
style={{ minWidth: '325px' }}
defaultSize='20%'
max='35%'
> >
{sideBarItems} {objectFormState.objectData?.currentSubJob?._id ? (
</Splitter.Panel> <ObjectForm
) : null} id={objectFormState.objectData.currentSubJob._id}
</Splitter> type='subjob'
{isMobile ? ( onStateChange={() => {}}
<> >
<Divider /> {({
loading: subJobObjectLoading,
objectData: subJobObjectData
}) => {
return (
<ObjectInfo
loading={subJobObjectLoading}
column={sideBarVisible ? 1 : undefined}
visibleProperties={{
printers: false,
createdAt: false
}}
objectData={subJobObjectData}
type='subJob'
/>
)
}}
</ObjectForm>
) : (
<MissingPlaceholder
message={'No sub job.'}
hasBackground={false}
/>
)}
</InfoCollapse>
<InfoCollapse
title={'Filament Stock'}
icon={<FilamentStockIcon />}
collapseKey='filamentStock'
active={collapseState.filamentStock}
onToggle={(expanded) =>
updateCollapseState('filamentStock', expanded)
}
>
{objectFormState.objectData?.currentFilamentStock?._id ? (
<ObjectForm
id={
objectFormState.objectData.currentFilamentStock._id
}
type='filamentStock'
onStateChange={() => {}}
>
{({
loading: filamentStockObjectLoading,
objectData: filamentStockObjectData
}) => {
return (
<ObjectInfo
loading={filamentStockObjectLoading}
column={sideBarVisible ? 1 : undefined}
showHyperlink={true}
visibleProperties={{
updatedAt: false,
createdAt: false
}}
objectData={filamentStockObjectData}
type='filamentStock'
/>
)
}}
</ObjectForm>
) : (
<MissingPlaceholder
message={'No filament stock.'}
hasBackground={false}
/>
)}
</InfoCollapse>
</Flex>
</Splitter.Panel>
{sideBarVisible && !isMobile ? (
<Splitter.Panel
style={{ minWidth: '325px' }}
defaultSize='20%'
max='35%'
>
{sideBarItems} {sideBarItems}
</> </Splitter.Panel>
) : null} ) : null}
</Flex> </Splitter>
</ActionHandler> {isMobile ? (
<InfoCollapse <>
title='Notes' <Divider />
icon={<NoteIcon />} {sideBarItems}
active={collapseState.notes} </>
onToggle={(expanded) => updateCollapseState('notes', expanded)} ) : null}
collapseKey='notes' </Flex>
> </ActionHandler>
<Card> <InfoCollapse
<NotesPanel _id={printerId} type='printer' /> title='Notes'
</Card> icon={<NoteIcon />}
</InfoCollapse> active={collapseState.notes}
</Flex> onToggle={(expanded) => updateCollapseState('notes', expanded)}
</div> collapseKey='notes'
</Flex> >
<Modal <Card>
open={loadFilamentStockOpen} <NotesPanel _id={printerId} type='printer' />
onCancel={() => setLoadFilamentStockOpen(false)} </Card>
footer={null} </InfoCollapse>
width='700px' </Flex>
destroyOnHidden={true} </div>
> </Flex>
<LoadFilamentStock
printer={objectFormState.objectData}
onOk={() => setLoadFilamentStockOpen(false)}
reset={false}
filamentStockLoaded={false}
/>
</Modal>
<Modal
open={unloadFilamentStockOpen}
onCancel={() => setUnloadFilamentStockOpen(false)}
footer={null}
width='700px'
destroyOnHidden={true}
>
<UnloadFilamentStock
printer={objectFormState.objectData}
onOk={() => setUnloadFilamentStockOpen(false)}
reset={false}
filamentStockLoaded={false}
/>
</Modal>
</>
) )
} }

View File

@ -108,14 +108,7 @@ const DocumentPrintButton = ({
onCancel={() => setNewDocumentJobOpen(false)} onCancel={() => setNewDocumentJobOpen(false)}
footer={null} footer={null}
destroyOnHidden={true} destroyOnHidden={true}
width={{ width={900}
xs: '100%',
sm: '100%',
md: '100%',
lg: '90%',
xl: '80%',
xxl: '80%'
}}
> >
<NewDocumentJob <NewDocumentJob
onOk={() => { onOk={() => {

View File

@ -9,8 +9,7 @@ const NewObjectButtons = ({
onSubmit, onSubmit,
formValid, formValid,
submitLoading, submitLoading,
submitText = 'Done', submitText = 'Done'
disabled = false
}) => { }) => {
return ( return (
<Flex justify='end'> <Flex justify='end'>
@ -25,18 +24,14 @@ const NewObjectButtons = ({
) : null} ) : null}
{currentStep < totalSteps - 1 ? ( {currentStep < totalSteps - 1 ? (
<Button <Button type='primary' disabled={!formValid} onClick={onNext}>
type='primary'
disabled={!formValid || disabled}
onClick={onNext}
>
Next Next
</Button> </Button>
) : ( ) : (
<Button <Button
type='primary' type='primary'
loading={submitLoading} loading={submitLoading}
disabled={!formValid || disabled} disabled={!formValid}
onClick={onSubmit} onClick={onSubmit}
> >
{submitText} {submitText}
@ -54,8 +49,7 @@ NewObjectButtons.propTypes = {
onSubmit: PropTypes.func.isRequired, onSubmit: PropTypes.func.isRequired,
formValid: PropTypes.bool.isRequired, formValid: PropTypes.bool.isRequired,
submitLoading: PropTypes.bool, submitLoading: PropTypes.bool,
submitText: PropTypes.string, submitText: PropTypes.string
disabled: PropTypes.bool
} }
export default NewObjectButtons export default NewObjectButtons

View File

@ -138,29 +138,24 @@ const ObjectForm = forwardRef(
return computedValues return computedValues
}, []) }, [])
// Validate form on change (debounced to avoid heavy work on every keystroke) // Validate form on change
useEffect(() => { useEffect(() => {
const timeoutId = setTimeout(() => { form
form .validateFields({ validateOnly: true })
.validateFields({ validateOnly: true }) .then(() => {
.then(() => { setFormValid(true)
setFormValid(true) onStateChange({
onStateChange({ formValid: true,
formValid: true, objectData: { ...serverObjectData, ...form.getFieldsValue() }
objectData: { ...serverObjectData, ...form.getFieldsValue() }
})
}) })
.catch(() => { })
setFormValid(false) .catch(() => {
onStateChange({ onStateChange({
formValid: false, formValid: true,
objectData: { ...serverObjectData, ...form.getFieldsValue() } objectData: { ...serverObjectData, ...form.getFieldsValue() }
})
}) })
}, 150) })
}, [form, formUpdateValues])
return () => clearTimeout(timeoutId)
}, [form, formUpdateValues, onStateChange, serverObjectData])
// Cleanup on unmount // Cleanup on unmount
useEffect(() => { useEffect(() => {
@ -257,14 +252,9 @@ const ObjectForm = forwardRef(
updateLockEventHandler updateLockEventHandler
]) ])
// Debounce objectData updates sent to parent to limit re-renders
useEffect(() => { useEffect(() => {
const timeoutId = setTimeout(() => { onStateChange({ objectData })
onStateChange({ objectData }) }, [objectData])
}, 150)
return () => clearTimeout(timeoutId)
}, [objectData, onStateChange])
const startEditing = () => { const startEditing = () => {
setIsEditing(true) setIsEditing(true)
@ -376,18 +366,9 @@ const ObjectForm = forwardRef(
model model
) )
// Update form with computed values if any were calculated and they changed // Update form with computed values if any were calculated
if (Object.keys(computedValues).length > 0) { if (Object.keys(computedValues).length > 0) {
const currentComputedValues = form.getFieldsValue( form.setFieldsValue(computedValues)
Object.keys(computedValues)
)
const hasDiff = Object.keys(computedValues).some(
(key) => currentComputedValues[key] !== computedValues[key]
)
if (hasDiff) {
form.setFieldsValue(computedValues)
}
} }
// Merge all values (user input + computed values) // Merge all values (user input + computed values)

View File

@ -81,8 +81,6 @@ const ObjectProperty = ({
minimal = false, minimal = false,
previewOpen = false, previewOpen = false,
showPreview = true, showPreview = true,
options = [],
roundNumber = false,
showHyperlink, showHyperlink,
...rest ...rest
}) => { }) => {
@ -168,18 +166,6 @@ const ObjectProperty = ({
</Text> </Text>
) )
} }
case 'select': {
const selectValue = options.find((option) => option.value === value)
if (selectValue) {
return <Text {...textParams}>{selectValue.label}</Text>
} else {
return (
<Text type='secondary' {...textParams}>
n/a
</Text>
)
}
}
case 'priceMode': case 'priceMode':
switch (value) { switch (value) {
case 'margin': case 'margin':
@ -248,15 +234,10 @@ const ObjectProperty = ({
</Text> </Text>
) )
} else { } else {
var roundedValue = value
if (roundNumber != false) {
roundedValue = value.toFixed(roundNumber)
}
return ( return (
<Text {...textParams}> <Text {...textParams}>
{prefix} {prefix}
{typeof value === 'number' ? roundedValue : value} {typeof value === 'number' ? value.toFixed(2) : value}
{suffix} {suffix}
</Text> </Text>
) )
@ -572,17 +553,6 @@ const ObjectProperty = ({
/> />
</Form.Item> </Form.Item>
) )
case 'select':
return (
<Form.Item name={formItemName} {...mergedFormItemProps}>
<Select
defaultValue={value}
placeholder={'Select a ' + label.toLowerCase() + '...'}
disabled={disabled}
options={options}
/>
</Form.Item>
)
case 'priceMode': case 'priceMode':
return ( return (
<Form.Item name={formItemName} {...mergedFormItemProps}> <Form.Item name={formItemName} {...mergedFormItemProps}>
@ -803,8 +773,7 @@ ObjectProperty.propTypes = {
height: PropTypes.string, height: PropTypes.string,
previewOpen: PropTypes.bool, previewOpen: PropTypes.bool,
showPreview: PropTypes.bool, showPreview: PropTypes.bool,
showHyperlink: PropTypes.bool, showHyperlink: PropTypes.bool
options: PropTypes.array
} }
export default ObjectProperty export default ObjectProperty

View File

@ -144,7 +144,6 @@ const ObjectSelect = ({
parentKeys: parentKeys.concat(key || '-'), parentKeys: parentKeys.concat(key || '-'),
filterPath: newFilterPath, filterPath: newFilterPath,
selectable: false, selectable: false,
children: buildTreeData( children: buildTreeData(
value, value,
pIdx + 1, pIdx + 1,
@ -279,11 +278,8 @@ const ObjectSelect = ({
handleFetchObjectsProperties() handleFetchObjectsProperties()
setInitialized(true) setInitialized(true)
} }
if (value == null) {
setTreeSelectValue(null)
setInitialized(true)
}
} }
handleValue() handleValue()
}, [ }, [
value, value,
@ -307,13 +303,8 @@ const ObjectSelect = ({
if (hasChanged) { if (hasChanged) {
setObjectPropertiesTree({}) setObjectPropertiesTree({})
setObjectList([])
setTreeData([]) setTreeData([])
setInitialized(false) setInitialized(false)
onTreeSelectChange(null)
setTreeSelectValue(null)
setInitialLoading(true)
setError(false)
prevValuesRef.current = { type, masterFilter } prevValuesRef.current = { type, masterFilter }
} }
}, [type, masterFilter]) }, [type, masterFilter])

View File

@ -227,7 +227,7 @@ const ObjectTable = forwardRef(
const loadNextPage = useCallback(() => { const loadNextPage = useCallback(() => {
const highestPage = Math.max(...pages.map((p) => p.pageNum)) const highestPage = Math.max(...pages.map((p) => p.pageNum))
const nextPage = highestPage + 1 const nextPage = highestPage + 1
if (hasMore && lazyLoading == false) { if (hasMore) {
setPages((prev) => { setPages((prev) => {
const filteredPages = prev.map((page) => ({ const filteredPages = prev.map((page) => ({
...page, ...page,
@ -244,13 +244,13 @@ const ObjectTable = forwardRef(
}) })
fetchData(nextPage) fetchData(nextPage)
} }
}, [pages, createSkeletonData, fetchData, hasMore, lazyLoading]) }, [pages, createSkeletonData, fetchData, hasMore])
const loadPreviousPage = useCallback(() => { const loadPreviousPage = useCallback(() => {
const lowestPage = Math.min(...pages.map((p) => p.pageNum)) const lowestPage = Math.min(...pages.map((p) => p.pageNum))
const prevPage = lowestPage - 1 const prevPage = lowestPage - 1
if (prevPage > 0 && lazyLoading == false) { if (prevPage > 0) {
setPages((prev) => { setPages((prev) => {
const filteredPages = prev.map((page) => ({ const filteredPages = prev.map((page) => ({
...page, ...page,
@ -267,7 +267,7 @@ const ObjectTable = forwardRef(
}) })
fetchData(prevPage) fetchData(prevPage)
} }
}, [pages, createSkeletonData, fetchData, lazyLoading]) }, [pages, createSkeletonData, fetchData])
const handleScroll = useCallback( const handleScroll = useCallback(
(e) => { (e) => {
@ -600,7 +600,7 @@ const ObjectTable = forwardRef(
title: prop.label, title: prop.label,
dataIndex: prop.name, dataIndex: prop.name,
width: prop.columnWidth || width, width: prop.columnWidth || width,
fixed: isMobile ? undefined : fixed, fixed: fixed,
key: prop.name, key: prop.name,
render: (text, record) => { render: (text, record) => {
if (record?.isSkeleton) { if (record?.isSkeleton) {
@ -651,7 +651,7 @@ const ObjectTable = forwardRef(
), ),
key: 'actions', key: 'actions',
fixed: 'right', fixed: 'right',
width: 20 + rowActions.length * 30, // Adjust width based on number of actions width: 80 + rowActions.length * 40, // Adjust width based on number of actions
render: (record) => { render: (record) => {
return renderActions(record) return renderActions(record)
} }

View File

@ -17,8 +17,7 @@ const ObjectTypeSelect = ({
.sort((a, b) => a.label.localeCompare(b.label)) .sort((a, b) => a.label.localeCompare(b.label))
.map((model) => ({ .map((model) => ({
value: model.name, value: model.name,
label: <ObjectTypeDisplay objectType={model.name} />, label: <ObjectTypeDisplay objectType={model.name} />
searchText: model.label?.toLowerCase() || ''
})) }))
return ( return (
@ -32,7 +31,9 @@ const ObjectTypeSelect = ({
allowClear={allowClear} allowClear={allowClear}
disabled={disabled} disabled={disabled}
filterOption={(input, option) => filterOption={(input, option) =>
option.searchText?.includes(input.toLowerCase()) ?? false option.label.props.children[1].props.children
.toLowerCase()
.indexOf(input.toLowerCase()) >= 0
} }
options={options} options={options}
/> />

View File

@ -4,13 +4,6 @@ import { Progress, Flex, Space } from 'antd'
import StateTag from './StateTag' import StateTag from './StateTag'
const StateDisplay = ({ state, showProgress = true, showState = true }) => { const StateDisplay = ({ state, showProgress = true, showState = true }) => {
const loadingProgressTypes = [
'loading',
'processing',
'queued',
'printing',
'used'
]
const currentState = state || { const currentState = state || {
type: 'unknown', type: 'unknown',
progress: 0 progress: 0
@ -23,14 +16,10 @@ const StateDisplay = ({ state, showProgress = true, showState = true }) => {
<StateTag state={currentState.type} /> <StateTag state={currentState.type} />
</Space> </Space>
)} )}
{showProgress && {showProgress && currentState?.progress && currentState?.progress > 0 ? (
loadingProgressTypes.includes(currentState.type) &&
currentState?.progress &&
currentState?.progress > 0 ? (
<Progress <Progress
percent={Math.round(currentState.progress * 100)} percent={Math.round(currentState.progress * 100)}
status={currentState.type === 'used' ? '' : 'active'} status='active'
strokeColor={currentState.type === 'used' ? 'orange' : ''}
style={{ width: '150px', marginBottom: '2px' }} style={{ width: '150px', marginBottom: '2px' }}
/> />
) : null} ) : null}

View File

@ -56,9 +56,9 @@ const StateTag = ({ state, showBadge = true, style = {} }) => {
status = 'success' status = 'success'
text = 'Ready' text = 'Ready'
break break
case 'new': case 'unconsumed':
status = 'success' status = 'success'
text = 'New' text = 'Unconsumed'
break break
case 'error': case 'error':
status = 'error' status = 'error'
@ -80,10 +80,6 @@ const StateTag = ({ state, showBadge = true, style = {} }) => {
status = 'warning' status = 'warning'
text = 'Queued' text = 'Queued'
break break
case 'used':
status = 'warning'
text = 'Used'
break
default: default:
status = 'default' status = 'default'
text = state || 'Unknown' text = state || 'Unknown'

View File

@ -1,6 +1,6 @@
import { useState, useContext, useEffect, useRef, useCallback } from 'react' import { useState, useContext, useEffect, useRef, useCallback } from 'react'
import PropTypes from 'prop-types' import PropTypes from 'prop-types'
import { Flex, Button, Input, Select } from 'antd' import { Flex, Button, Input } from 'antd'
import PlusIcon from '../../Icons/PlusIcon.jsx' import PlusIcon from '../../Icons/PlusIcon.jsx'
import MinusIcon from '../../Icons/MinusIcon.jsx' import MinusIcon from '../../Icons/MinusIcon.jsx'
import InfoCircleIcon from '../../Icons/InfoCircleIcon.jsx' import InfoCircleIcon from '../../Icons/InfoCircleIcon.jsx'
@ -14,26 +14,22 @@ const TemplatePreview = ({
isEditing, isEditing,
onTestObjectOpen, onTestObjectOpen,
onPreviewMessage, onPreviewMessage,
showTestObject = false, showTestObject = false
showPreviewSwitch = true
}) => { }) => {
const iframeRef = useRef(null) const iframeRef = useRef(null)
const { fetchTemplatePreview, fetchTemplatePDF } = const { fetchTemplatePreview } = useContext(ApiServerContext)
useContext(ApiServerContext) const [previewContent, setPreviewContent] = useState('')
const [previewContentHTML, setPreviewContentHTML] = useState('')
const [pdfBlob, setPDFBlob] = useState(null)
const [reloadLoading, setReloadLoading] = useState(false) const [reloadLoading, setReloadLoading] = useState(false)
const [previewScale, setPreviewScale] = useState(1) const [previewScale, setPreviewScale] = useState(1)
const [previewType, setPreviewType] = useState('HTML')
const updatePreviewContentHTML = (html) => { const updatePreviewContent = (html) => {
if (iframeRef.current) { if (iframeRef.current) {
// Save current scroll position // Save current scroll position
const scrollY = iframeRef.current.contentWindow.scrollY const scrollY = iframeRef.current.contentWindow.scrollY
const scrollX = iframeRef.current.contentWindow.scrollX const scrollX = iframeRef.current.contentWindow.scrollX
// Update srcDoc // Update srcDoc
setPreviewContentHTML(html) setPreviewContent(html)
// Restore scroll position after iframe loads new content // Restore scroll position after iframe loads new content
const handleLoad = () => { const handleLoad = () => {
@ -44,23 +40,6 @@ const TemplatePreview = ({
} }
} }
const reloadPreviewPDF = (content, testObject = {}) => {
setReloadLoading(true)
fetchTemplatePDF(documentTemplate._id, content, testObject, (result) => {
setReloadLoading(false)
if (result?.error) {
// Handle error through parent component
onPreviewMessage(result.error, true)
} else {
const pdfBlob = new Blob([result.pdf], { type: 'application/pdf' })
const pdfUrl = URL.createObjectURL(pdfBlob)
setPDFBlob(pdfUrl)
onPreviewMessage('No issues found.', false)
}
})
}
const reloadPreview = useCallback( const reloadPreview = useCallback(
(content, testObject = {}, scale = 1) => { (content, testObject = {}, scale = 1) => {
setReloadLoading(true) setReloadLoading(true)
@ -75,7 +54,7 @@ const TemplatePreview = ({
// Handle error through parent component // Handle error through parent component
onPreviewMessage(result.error, true) onPreviewMessage(result.error, true)
} else { } else {
updatePreviewContentHTML(result.html) updatePreviewContent(result.html)
onPreviewMessage('No issues found.', false) onPreviewMessage('No issues found.', false)
} }
} }
@ -87,13 +66,9 @@ const TemplatePreview = ({
// Move useEffect to component level and use state to track objectData changes // Move useEffect to component level and use state to track objectData changes
useEffect(() => { useEffect(() => {
if (documentTemplate?.content) { if (documentTemplate?.content) {
if (previewType == 'HTML') { reloadPreview(documentTemplate.content, objectData, previewScale)
reloadPreview(documentTemplate.content, objectData, previewScale)
} else {
reloadPreviewPDF(documentTemplate.content, objectData)
}
} }
}, [objectData, documentTemplate, previewScale, previewType]) }, [objectData, documentTemplate, previewScale, reloadPreview])
return ( return (
<Flex vertical gap={'middle'} style={{ height: '100%' }}> <Flex vertical gap={'middle'} style={{ height: '100%' }}>
@ -123,51 +98,34 @@ const TemplatePreview = ({
/> />
</> </>
) : null} ) : null}
<Button <Button
icon={<PlusIcon />} icon={<PlusIcon />}
onClick={() => { onClick={() => {
setPreviewScale((prev) => prev + 0.05) setPreviewScale((prev) => prev + 0.05)
}} }}
disabled={loading || reloadLoading || previewType == 'PDF'}
/> />
<Button <Button
icon={<MinusIcon />} icon={<MinusIcon />}
onClick={() => { onClick={() => {
setPreviewScale((prev) => prev - 0.05) setPreviewScale((prev) => prev - 0.05)
}} }}
disabled={loading || reloadLoading || previewType == 'PDF'}
/> />
<Button <Button
readOnly={true} readOnly={true}
style={{ width: '65px' }} style={{ width: '65px' }}
disabled={loading || reloadLoading || previewType == 'PDF'} loading={loading || reloadLoading}
disabled={loading || reloadLoading}
onClick={() => { onClick={() => {
setPreviewScale(1) setPreviewScale(1)
}} }}
> >
{previewScale.toFixed(2)}x {previewScale.toFixed(2)}x
</Button> </Button>
{showPreviewSwitch == true ? (
<Select
options={[
{ value: 'HTML', label: 'HTML' },
{ value: 'PDF', label: 'PDF' }
]}
loading={loading || reloadLoading}
disabled={loading || reloadLoading}
value={previewType}
onChange={(value) => {
setPreviewType(value)
}}
/>
) : null}
</Flex> </Flex>
<iframe <iframe
ref={iframeRef} ref={iframeRef}
srcDoc={previewType == 'HTML' ? previewContentHTML : undefined} srcDoc={previewContent}
src={previewType == 'PDF' ? pdfBlob : undefined}
frameBorder='0' frameBorder='0'
style={{ style={{
width: '100%', width: '100%',
@ -188,8 +146,7 @@ TemplatePreview.propTypes = {
style: PropTypes.object, style: PropTypes.object,
showTestObject: PropTypes.bool, showTestObject: PropTypes.bool,
onTestObjectOpen: PropTypes.func.isRequired, onTestObjectOpen: PropTypes.func.isRequired,
onPreviewMessage: PropTypes.func.isRequired, onPreviewMessage: PropTypes.func.isRequired
showPreviewSwitch: PropTypes.bool
} }
export default TemplatePreview export default TemplatePreview

View File

@ -1,15 +1,7 @@
import PropTypes from 'prop-types' import PropTypes from 'prop-types'
import { useState } from 'react' import { useState } from 'react'
import { useMediaQuery } from 'react-responsive' import { useMediaQuery } from 'react-responsive'
import { import { Typography, Flex, Steps, Divider, Progress } from 'antd'
Typography,
Flex,
Steps,
Divider,
Progress,
Button,
Dropdown
} from 'antd'
import NewObjectButtons from './NewObjectButtons' import NewObjectButtons from './NewObjectButtons'
const { Title } = Typography const { Title } = Typography
@ -23,10 +15,7 @@ const WizardView = ({
loading, loading,
sideBar = null, sideBar = null,
submitText = 'Done', submitText = 'Done',
progress = 0, progress = 0
actions = [],
sideBarGrow = false,
disabled = false
}) => { }) => {
const [currentStep, setCurrentStep] = useState(0) const [currentStep, setCurrentStep] = useState(0)
const isMobile = useMediaQuery({ maxWidth: 768 }) const isMobile = useMediaQuery({ maxWidth: 768 })
@ -37,7 +26,7 @@ const WizardView = ({
sideBar != null ? ( sideBar != null ? (
sideBar sideBar
) : ( ) : (
<div style={{ minWidth: sideBarGrow == true ? '100%' : '160px' }}> <div style={{ minWidth: '160px' }}>
<Steps <Steps
current={currentStep} current={currentStep}
items={steps} items={steps}
@ -56,13 +45,7 @@ const WizardView = ({
vertical vertical
justify='space-between' justify='space-between'
gap={'middle'} gap={'middle'}
style={ style={{ width: '100%' }}
sideBarGrow == false
? { width: '100%' }
: isMobile
? { width: '100%' }
: { width: '400px' }
}
> >
<Flex vertical gap='middle' style={{ flexGrow: 1, width: '100%' }}> <Flex vertical gap='middle' style={{ flexGrow: 1, width: '100%' }}>
<Title level={2} style={{ margin: 0 }}> <Title level={2} style={{ margin: 0 }}>
@ -80,37 +63,8 @@ const WizardView = ({
percent={progress} percent={progress}
/> />
) : null} ) : null}
{(actions || []).map((action) => {
if (action.steps.includes(steps[currentStep].key)) {
if (action.children) {
return (
<Dropdown menu={{ items: action.children }} key={action.key}>
<Button
onClick={action?.onClick}
disabled={action?.disabled || disabled}
loading={action?.loading || false}
>
{action.label}
</Button>
</Dropdown>
)
}
return (
<Button
key={action.key}
onClick={action?.onClick}
disabled={action?.disabled || disabled}
loading={action?.loading || false}
>
{action.label}
</Button>
)
}
return null
})}
<NewObjectButtons <NewObjectButtons
disabled={disabled}
currentStep={currentStep} currentStep={currentStep}
totalSteps={steps.length} totalSteps={steps.length}
onPrevious={() => setCurrentStep((prev) => prev - 1)} onPrevious={() => setCurrentStep((prev) => prev - 1)}
@ -133,12 +87,9 @@ WizardView.propTypes = {
showSteps: PropTypes.bool, showSteps: PropTypes.bool,
title: PropTypes.string, title: PropTypes.string,
loading: PropTypes.bool, loading: PropTypes.bool,
disabled: PropTypes.bool,
sideBar: PropTypes.node, sideBar: PropTypes.node,
submitText: PropTypes.string, submitText: PropTypes.string,
progress: PropTypes.number, progress: PropTypes.number
actions: PropTypes.array,
sideBarGrow: PropTypes.bool
} }
export default WizardView export default WizardView

View File

@ -308,14 +308,9 @@ const ApiServerProvider = ({ children }) => {
.get(callbacksRefKey) .get(callbacksRefKey)
.filter((cb) => cb !== callback) .filter((cb) => cb !== callback)
if (callbacks.length === 0) { if (callbacks.length === 0) {
logger.debug(
'No callbacks found for object:',
callbacksRefKey,
'unsubscribing from object update...'
)
subscribedCallbacksRef.current.delete(callbacksRefKey) subscribedCallbacksRef.current.delete(callbacksRefKey)
socketRef.current.emit('unsubscribeObjectUpdate', { socketRef.current.emit('unsubscribeObjectUpdate', {
_id: id, id: id,
objectType: objectType objectType: objectType
}) })
} else { } else {
@ -534,7 +529,7 @@ const ApiServerProvider = ({ children }) => {
`Added lock callback for object ${id}, total lock callbacks: ${subscribedLockCallbacksRef.current.get(id).length}` `Added lock callback for object ${id}, total lock callbacks: ${subscribedLockCallbacksRef.current.get(id).length}`
) )
socketRef.current.emit('subscribe_lock', { _id: id, objectType: type }) socketRef.current.emit('subscribe_lock', { id: id, type: type })
logger.debug('Registered lock event listener for object:', id) logger.debug('Registered lock event listener for object:', id)
// Return cleanup function // Return cleanup function
@ -858,53 +853,6 @@ const ApiServerProvider = ({ children }) => {
} }
} }
const fetchTemplatePDF = async (id, content, testObject, callback) => {
logger.debug('Fetching pdf template...')
if (socketRef.current && socketRef.current.connected) {
return socketRef.current.emit(
'renderTemplatePDF',
{
_id: id,
content: content,
object: testObject
},
callback
)
}
}
const downloadTemplatePDF = async (
id,
content,
object,
filename,
callback
) => {
logger.debug('Downloading template PDF...')
fetchTemplatePDF(id, content, object, (result) => {
logger.debug('Downloading template PDF result:', result)
if (result?.error) {
console.error(result.error)
if (callback) {
callback(result.error)
}
} else {
const pdfBlob = new Blob([result.pdf], { type: 'application/pdf' })
const pdfUrl = URL.createObjectURL(pdfBlob)
const fileLink = document.createElement('a')
fileLink.href = pdfUrl
fileLink.setAttribute('download', `${filename}.pdf`)
document.body.appendChild(fileLink)
fileLink.click()
fileLink.parentNode.removeChild(fileLink)
if (callback) {
callback()
}
}
})
}
const fetchHostOTP = async (id, callback) => { const fetchHostOTP = async (id, callback) => {
logger.debug('Fetching host OTP...') logger.debug('Fetching host OTP...')
if (socketRef.current && socketRef.current.connected) { if (socketRef.current && socketRef.current.connected) {
@ -994,22 +942,6 @@ const ApiServerProvider = ({ children }) => {
} }
} }
// Sanitize a string so it is safe to use as a filename on most file systems
const formatFileName = (name) => {
if (!name || typeof name !== 'string') {
return ''
}
// Remove characters that are problematic on most common file systems
const cleaned = name.replace(/[^a-zA-Z0-9.\-_\s]/g, '')
// Normalize whitespace to single underscores
const normalized = cleaned.trim().replace(/\s+/g, '_')
// Most file systems limit filenames to 255 characters
return normalized.slice(0, 255)
}
return ( return (
<ApiServerContext.Provider <ApiServerContext.Provider
value={{ value={{
@ -1035,14 +967,11 @@ const ApiServerProvider = ({ children }) => {
showError, showError,
fetchFileContent, fetchFileContent,
fetchTemplatePreview, fetchTemplatePreview,
fetchTemplatePDF,
fetchNotes, fetchNotes,
downloadTemplatePDF,
fetchHostOTP, fetchHostOTP,
sendObjectAction, sendObjectAction,
uploadFile, uploadFile,
flushFile, flushFile
formatFileName
}} }}
> >
{contextHolder} {contextHolder}

View File

@ -6,9 +6,9 @@ const config = {
logLevel: 'trace' logLevel: 'trace'
}, },
production: { production: {
backendUrl: 'https://dev.tombutcher.work/api', backendUrl: 'http://192.168.68.53:8080',
printServerUrl: 'ws://192.168.68.53:8081', printServerUrl: 'ws://192.168.68.53:8081',
apiServerUrl: 'https://dev-wss.tombutcher.work', apiServerUrl: 'ws://192.168.68.53:9090',
logLevel: 'error' logLevel: 'error'
} }
} }

View File

@ -2,12 +2,11 @@ import InfoCircleIcon from '../../components/Icons/InfoCircleIcon'
import ReloadIcon from '../../components/Icons/ReloadIcon' import ReloadIcon from '../../components/Icons/ReloadIcon'
import EditIcon from '../../components/Icons/EditIcon' import EditIcon from '../../components/Icons/EditIcon'
import DocumentJobIcon from '../../components/Icons/DocumentJobIcon' import DocumentJobIcon from '../../components/Icons/DocumentJobIcon'
import dayjs from 'dayjs'
export const DocumentJob = { export const DocumentJob = {
name: 'documentJob', name: 'documentJob',
label: 'Document Job', label: 'Document Job',
prefix: 'DJB', prefix: 'DSZ',
icon: DocumentJobIcon, icon: DocumentJobIcon,
actions: [ actions: [
{ {
@ -61,9 +60,7 @@ export const DocumentJob = {
columnWidth: 200, columnWidth: 200,
columnFixed: 'left', columnFixed: 'left',
value: (objectData) => { value: (objectData) => {
if (objectData?.createdAt == undefined) { return `${objectData?.documentTemplate?.name || 'No template'} (${objectData?.object?.name || 'No name'})`
return `${objectData?.documentTemplate?.name || 'No template'} ${dayjs(new Date()).format('YYYY-MM-DD HH:mm:ss')} (${objectData?.object?.name || objectData?.object?._id})`
}
} }
}, },
{ {
@ -72,14 +69,6 @@ export const DocumentJob = {
type: 'dateTime', type: 'dateTime',
readOnly: true readOnly: true
}, },
{
name: 'state',
label: 'Status',
type: 'state',
objectType: 'printer',
showName: false,
readOnly: true
},
{ {
name: 'objectType', name: 'objectType',
label: 'Object Type', label: 'Object Type',
@ -97,15 +86,6 @@ export const DocumentJob = {
return objectData?.objectType return objectData?.objectType
} }
}, },
{
name: 'object._id',
label: 'Object ID',
type: 'id',
showHyperlink: true,
objectType: (objectData) => {
return objectData?.objectType
}
},
{ {
name: 'documentTemplate', name: 'documentTemplate',
label: 'Template', label: 'Template',
@ -121,13 +101,6 @@ export const DocumentJob = {
} }
} }
}, },
{
name: 'documentTemplate._id',
label: 'Template ID',
type: 'id',
showHyperlink: true,
objectType: 'documentTemplate'
},
{ {
name: 'documentPrinter', name: 'documentPrinter',
label: 'Printer', label: 'Printer',
@ -141,13 +114,6 @@ export const DocumentJob = {
online: true online: true
} }
} }
},
{
name: 'documentPrinter._id',
label: 'Printer ID',
type: 'id',
showHyperlink: true,
objectType: 'documentPrinter'
} }
] ]
} }

View File

@ -74,88 +74,52 @@ export const DocumentPrinter = {
readOnly: true readOnly: true
}, },
{ {
name: 'state', name: 'documentSize',
label: 'Status', label: 'Document Size',
type: 'state',
objectType: 'printer',
showName: false,
readOnly: true
},
{
name: 'active',
label: 'Active',
type: 'bool',
required: true
},
{
name: 'online',
label: 'Online',
type: 'bool',
readOnly: true
},
{
name: 'host',
label: 'Host',
required: true, required: true,
type: 'object', type: 'object',
objectType: 'host',
showHyperlink: true
},
{
name: 'host._id',
label: 'Host ID',
type: 'id',
objectType: 'host',
showCopy: true,
showHyperlink: true
},
{
name: 'connection.mode',
label: 'Mode',
type: 'select',
options: [
{ label: 'Network', value: 'network' },
{ label: 'Serial', value: 'serial' }
],
required: true
},
{
name: 'connection.interface',
label: 'Interface',
type: 'select',
options: [
{ label: 'CUPS', value: 'cups' },
{ label: 'Epson Receipt', value: 'epsonReceipt' },
{ label: 'Star Receipt', value: 'starReceipt' }
],
required: true
},
{
name: 'connection.host',
label: 'Connection String',
type: 'text',
required: true
},
{
name: 'currentDocumentSize',
label: 'Current Document Size',
required: false,
type: 'object',
objectType: 'documentSize' objectType: 'documentSize'
}, },
{ {
name: 'currentDocumentSize._id', name: 'documentSize._id',
label: 'Current Document Size ID', label: 'Document Size ID',
type: 'id', type: 'id',
objectType: 'documentSize', objectType: 'documentSize',
showCopy: true, showCopy: true,
showHyperlink: true showHyperlink: true
}, },
{
name: 'active',
label: 'Active',
required: true,
type: 'bool'
},
{ {
name: 'tags', name: 'tags',
label: 'Tags', label: 'Tags',
required: false, required: false,
type: 'tags' type: 'tags'
},
{ name: 'global', label: 'Global', required: false, type: 'bool' },
{
name: 'parent',
label: 'Parent',
required: false,
type: 'object',
objectType: 'documentPrinter',
disabled: (documentPrinter) => {
if (documentPrinter.global == true) {
documentPrinter.parent = null
}
return documentPrinter.global
}
},
{
name: 'parent._id',
label: 'Parent ID',
required: false,
type: 'id',
objectType: 'documentPrinter'
} }
] ]
} }

View File

@ -5,7 +5,7 @@ import EditIcon from '../../components/Icons/EditIcon'
import PlayCircleIcon from '../../components/Icons/PlayCircleIcon' import PlayCircleIcon from '../../components/Icons/PlayCircleIcon'
import PauseCircleIcon from '../../components/Icons/PauseCircleIcon' import PauseCircleIcon from '../../components/Icons/PauseCircleIcon'
import StopCircleIcon from '../../components/Icons/StopCircleIcon' import StopCircleIcon from '../../components/Icons/StopCircleIcon'
import FilamentStockIcon from '../../components/Icons/FilamentStockIcon'
export const Printer = { export const Printer = {
name: 'printer', name: 'printer',
label: 'Printer', label: 'Printer',
@ -94,8 +94,8 @@ export const Printer = {
}, },
children: [ children: [
{ {
name: 'startQueue', name: 'Start',
label: 'Start Queue', label: 'Start',
icon: PlayCircleIcon, icon: PlayCircleIcon,
disabled: (objectData) => { disabled: (objectData) => {
console.log(objectData?.subJobs?.length) console.log(objectData?.subJobs?.length)
@ -109,60 +109,28 @@ export const Printer = {
url: (_id) => url: (_id) =>
`/dashboard/production/printers/control?printerId=${_id}&action=startQueue` `/dashboard/production/printers/control?printerId=${_id}&action=startQueue`
}, },
{ type: 'divider' },
{ {
name: 'pauseJob', name: 'pause',
label: 'Pause Job', label: 'Pause',
icon: PauseCircleIcon, icon: PauseCircleIcon,
disabled: (objectData) => { disabled: (objectData) => {
return objectData?.state?.type != 'printing' return objectData?.state?.type != 'printing'
}, },
url: (_id) => url: (_id) =>
`/dashboard/production/printers/control?printerId=${_id}&action=pauseJob` `/dashboard/production/printers/control?printerId=${_id}&action=pauseQueue`
}, },
{ {
name: 'resumeJob', name: 'Stop',
label: 'Resume Job', label: 'Stop',
icon: PlayCircleIcon,
disabled: (objectData) => {
return objectData?.state?.type != 'printing'
},
url: (_id) =>
`/dashboard/production/printers/control?printerId=${_id}&action=resumeJob`
},
{
name: 'cancelJob',
label: 'Cancel Job',
icon: StopCircleIcon, icon: StopCircleIcon,
disabled: (objectData) => { disabled: (objectData) => {
return ( return (
objectData?.state?.type != 'printing' && objectData?.state?.type != 'printing' ||
objectData?.state?.type != 'error' objectData?.state?.type != 'error'
) )
}, },
url: (_id) => url: (_id) =>
`/dashboard/production/printers/control?printerId=${_id}&action=cancelJob` `/dashboard/production/printers/control?printerId=${_id}&action=stopQueue`
}
]
},
{
name: 'filamentStock',
label: 'Filament Stock',
icon: FilamentStockIcon,
children: [
{
name: 'loadFilamentStock',
label: 'Load Filament Stock',
icon: FilamentStockIcon,
url: (_id) =>
`/dashboard/production/printers/control?printerId=${_id}&action=loadFilamentStock`
},
{
name: 'unloadFilamentStock',
label: 'Unload Filament Stock',
icon: FilamentStockIcon,
url: (_id) =>
`/dashboard/production/printers/control?printerId=${_id}&action=unloadFilamentStock`
} }
] ]
} }
@ -331,14 +299,6 @@ export const Printer = {
label: 'Alerts', label: 'Alerts',
type: 'alerts', type: 'alerts',
required: false required: false
},
{
name: 'subJobs',
label: 'Queue',
type: 'objectList',
objectType: 'subJob',
required: false,
readOnly: true
} }
] ]
} }