Compare commits

...

26 Commits

Author SHA1 Message Date
c6c99bb02c Update package-lock.json to add @tanstack/react-query dependency and adjust peer dependencies for various packages. Modify backend and API server URLs in config.js for production environment. 2025-11-24 03:34:37 +00:00
91e7121fd5 Add filament stock management to ControlPrinter component with modal integration for loading and unloading filament. Implement new actions for pausing, resuming, and canceling jobs, enhancing printer control functionality. 2025-11-24 03:34:28 +00:00
afe25c1e09 Enhance DocumentJob, DocumentPrinter, and Printer models by adding new fields for state, object IDs, and document size management. Update action labels for clarity and improve data handling with date formatting. Introduce new connection options for printers and enhance the user interface for job management. 2025-11-24 03:34:20 +00:00
8d63b9a84d Add NewDocumentPrinter component to manage document printer creation with a multi-step wizard interface. Integrate ObjectInfo for required and optional fields, enhancing user experience during setup. 2025-11-24 03:34:04 +00:00
808d45273d Refactor DocumentPrinterInfo component to improve state management and enhance editing functionality. Introduce useRef for action handling and object form references, and update the rendering logic for better performance and user experience. 2025-11-24 03:33:57 +00:00
64c4d25982 Add New Document Printer functionality to DocumentPrinters component, including modal integration and success message upon creation. Enhance dropdown menu with new action item for creating document printers. 2025-11-24 03:33:44 +00:00
7ac56cc69c Refactor NewDocumentJob and NewDocumentTemplate components to utilize WizardView for improved user experience, implement file download functionality with dynamic filename generation, and enhance form handling with context integration. 2025-11-24 03:33:32 +00:00
e4b8f52e6b Add downloadTemplatePDF function to ApiServerContext for PDF downloading and implement formatFileName utility for safe filename generation 2025-11-24 03:33:11 +00:00
e114a45348 Enhance WizardView component by adding support for custom action buttons, sideBarGrow functionality, and a disabled state. Refactor layout styles for better responsiveness and usability. 2025-11-24 03:32:59 +00:00
b7bb6121b7 Update StateTag component to rename 'unconsumed' state to 'new' and add 'used' state with corresponding status and text. 2025-11-24 03:32:40 +00:00
dabad4f489 Enhance StateDisplay component to include 'used' as a loading progress type and adjust progress status and color accordingly. 2025-11-24 03:32:16 +00:00
94c55c8e77 Enable treeDefaultExpandAll in ObjectSelect component for improved tree view visibility 2025-11-24 03:32:06 +00:00
8f67d7daae Add roundNumber prop to ObjectProperty component for configurable number rounding and enhance select case handling for better value display 2025-11-24 03:31:58 +00:00
e788eaba91 Refactor ObjectForm component to implement debounced validation and state updates, improving performance and reducing unnecessary re-renders. 2025-11-24 03:31:51 +00:00
6af832afe5 Add disabled prop to NewObjectButtons for enhanced control over button states 2025-11-24 03:31:39 +00:00
69c387ca1d Update DocumentPrintButton width settings for responsive design across various screen sizes. 2025-11-24 03:31:31 +00:00
ecf73c13c0 Refactor Load and Unload Filament Stock components to utilize WizardView for improved user experience, integrate API server context for real-time temperature and filament sensor updates, and streamline form validation logic. 2025-11-23 13:26:46 +00:00
657d6a5c6e Added PDF preview to ApiServerContext 2025-11-23 13:26:31 +00:00
8f34c262a0 Added PDF preview option. 2025-11-23 13:25:43 +00:00
5cb586246f Enhance StateDisplay component to conditionally show progress for specific loading types 2025-11-23 13:22:51 +00:00
b12d230a8e Fixed object type search bug 2025-11-23 13:22:21 +00:00
aeebaaddda Refactor CSS for improved readability and maintainability, including adjustments to selectors and spacing for various components. 2025-11-23 13:22:07 +00:00
8ed0287d73 Adjust ObjectTable fixed column behavior for mobile and update action column width calculation 2025-11-23 13:21:48 +00:00
030053b965 Minor bug fixes 2025-11-23 13:21:34 +00:00
2d2df403e3 Added select type 2025-11-23 13:20:22 +00:00
8c622420bb Fixed loading glitch if web server becomes unresponsive. 2025-11-21 19:15:25 +00:00
26 changed files with 2141 additions and 1001 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,7 +55,9 @@
-webkit-app-region: drag; -webkit-app-region: drag;
} }
.electron-navigation-wrapper li, .electron-navigation-wrapper button, .electron-navigation-wrapper .ant-tag{ .electron-navigation-wrapper li,
.electron-navigation-wrapper button,
.electron-navigation-wrapper .ant-tag {
-webkit-app-region: no-drag; -webkit-app-region: no-drag;
} }
@ -67,7 +69,8 @@
padding-inline: 10px; padding-inline: 10px;
} }
.electron-sidebar .ant-menu-item, .electron-sidebar .ant-menu-submenu-title { .electron-sidebar .ant-menu-item,
.electron-sidebar .ant-menu-submenu-title {
height: 32.5px !important; height: 32.5px !important;
line-height: 32.5px !important; line-height: 32.5px !important;
} }
@ -77,11 +80,10 @@
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;
} }
@ -91,8 +93,8 @@ display: none;
} }
} }
.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 {
@ -225,19 +227,21 @@ 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 >.ant-descriptions-view .ant-descriptions-row >.ant-descriptions-item-label { .objectTableDescritions
> .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;
@ -249,32 +253,31 @@ 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 {
@ -291,9 +294,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 {
@ -307,3 +310,7 @@ 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,27 +1,14 @@
import { useState, useContext, useEffect } from 'react' import { useState, useContext, useEffect } from 'react'
import { import { Form, Flex, Descriptions, Alert } from 'antd'
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 PrinterState from '../../common/StateDisplay' import ObjectSelect from '../../common/ObjectSelect'
import ObjectDisplay from '../../common/ObjectDisplay'
const { Title } = Typography import WizardView from '../../common/WizardView'
import { ApiServerContext } from '../../context/ApiServerContext'
const LoadFilamentStock = ({ const LoadFilamentStock = ({
onOk, onOk,
@ -29,7 +16,8 @@ const LoadFilamentStock = ({
printer = null, printer = null,
filamentStockLoaded = false filamentStockLoaded = false
}) => { }) => {
const isMobile = useMediaQuery({ maxWidth: 768 }) const { connected, subscribeToObjectEvent, sendObjectAction } =
useContext(ApiServerContext)
LoadFilamentStock.propTypes = { LoadFilamentStock.propTypes = {
onOk: PropTypes.func.isRequired, onOk: PropTypes.func.isRequired,
@ -38,8 +26,6 @@ const LoadFilamentStock = ({
filamentStockLoaded: PropTypes.bool filamentStockLoaded: PropTypes.bool
} }
const { printServer } = useContext(PrintServerContext)
const initialLoadFilamentStockForm = { const initialLoadFilamentStockForm = {
printer: printer, printer: printer,
filamentStock: null filamentStock: null
@ -47,8 +33,7 @@ const LoadFilamentStock = ({
const [loadFilamentStockLoading, setLoadFilamentStockLoading] = const [loadFilamentStockLoading, setLoadFilamentStockLoading] =
useState(false) useState(false)
const [currentStep, setCurrentStep] = useState(0) const [formValid, setFormValid] = useState(false)
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] =
@ -62,77 +47,69 @@ const LoadFilamentStock = ({
loadFilamentStockForm loadFilamentStockForm
) )
// Add websocket temperature monitoring
useEffect(() => { useEffect(() => {
if (loadFilamentStockFormValues.printer) { if (printer?._id && connected) {
const params = { const temperatureEventUnsubscribe = subscribeToObjectEvent(
printerId: loadFilamentStockFormValues.printer._id, printer._id,
objects: { 'printer',
extruder: null, 'temperature',
'filament_switch_sensor fsensor': null (event) => {
if (event.data?.extruder?.current) {
setCurrentTemperature(event.data.extruder.current)
}
if (event.data?.extruder?.target) {
setTargetTemperature(event.data.extruder.target)
}
} }
} )
const filamentStockEventUnsubscribe = subscribeToObjectEvent(
const notifyStatusUpdate = (statusUpdate) => { printer._id,
if (statusUpdate?.extruder?.temperature !== undefined) { 'printer',
setCurrentTemperature(statusUpdate.extruder.temperature) 'filamentSensor',
(event) => {
console.log('filamentSensor', event.data)
setFilamentSensorDetected(event.data.detected)
} }
if (statusUpdate?.extruder?.target !== undefined) { )
setTargetTemperature(statusUpdate.extruder.target)
}
if (
statusUpdate?.['filament_switch_sensor fsensor']
?.filament_detected !== undefined
) {
setFilamentSensorDetected(
Boolean(
statusUpdate['filament_switch_sensor fsensor'].filament_detected
)
)
}
}
printServer.emit('printer.objects.subscribe', params)
printServer.emit('printer.objects.query', params)
printServer.on('notify_status_update', notifyStatusUpdate)
return () => { return () => {
printServer.off('notify_status_update', notifyStatusUpdate) if (temperatureEventUnsubscribe) temperatureEventUnsubscribe()
printServer.emit('printer.objects.unsubscribe', params) if (filamentStockEventUnsubscribe) filamentStockEventUnsubscribe()
} }
} }
}, [printServer, loadFilamentStockFormValues.printer]) }, [printer?._id, connected])
useEffect(() => { useEffect(() => {
// Validate form fields
loadFilamentStockForm loadFilamentStockForm
.validateFields({ .validateFields({
validateOnly: true validateOnly: true
}) })
.then(() => setNextEnabled(filamentSensorDetected)) .then(() => {
.catch(() => setNextEnabled(false)) const hasPrinter = Boolean(loadFilamentStockFormValues.printer)
const hasFilamentStock = Boolean(
loadFilamentStockFormValues.filamentStock
)
// Step 0 (Preheat): needs printer + filament detected + (temp reached or no temp set)
const preheatReady =
hasPrinter &&
filamentSensorDetected &&
(targetTemperature === 0 || currentTemperature >= targetTemperature)
// Step 1+ (Required/Summary): needs filamentStock selected
// Form is valid if preheat is ready (for step 0) OR filamentStock is selected (for step 1+)
// This allows progression: step 0 can proceed when preheat is ready,
// and step 1+ can proceed when filamentStock is selected
setFormValid(preheatReady || (hasPrinter && hasFilamentStock))
})
.catch(() => setFormValid(false))
}, [ }, [
loadFilamentStockForm, loadFilamentStockForm,
loadFilamentStockFormUpdateValues, loadFilamentStockFormUpdateValues,
filamentSensorDetected loadFilamentStockFormValues,
])
useEffect(() => {
if (
filamentSensorDetected == true &&
currentTemperature >= targetTemperature
) {
setNextEnabled(filamentSensorDetected)
if (currentStep == 0) {
setCurrentStep(1)
}
} else if (filamentSensorDetected == false) {
setCurrentStep(0)
}
}, [
filamentSensorDetected, filamentSensorDetected,
targetTemperature,
currentTemperature, currentTemperature,
currentStep targetTemperature
]) ])
const summaryItems = [ const summaryItems = [
@ -140,8 +117,9 @@ const LoadFilamentStock = ({
key: 'filamentStock', key: 'filamentStock',
label: 'Stock', label: 'Stock',
children: loadFilamentStockFormValues.filamentStock ? ( children: loadFilamentStockFormValues.filamentStock ? (
<FilamentStockDisplay <ObjectDisplay
filamentStock={loadFilamentStockFormValues.filamentStock} objectType='filamentStock'
object={loadFilamentStockFormValues.filamentStock}
/> />
) : ( ) : (
'n/a' 'n/a'
@ -151,9 +129,12 @@ const LoadFilamentStock = ({
key: 'printer', key: 'printer',
label: 'Printer', label: 'Printer',
children: loadFilamentStockFormValues.printer ? ( children: loadFilamentStockFormValues.printer ? (
<PrinterState printer={loadFilamentStockFormValues.printer} /> <ObjectDisplay
objectType='printer'
object={loadFilamentStockFormValues.printer}
/>
) : ( ) : (
'n/a>' 'n/a'
) )
} }
] ]
@ -169,10 +150,16 @@ const LoadFilamentStock = ({
try { try {
// Set the extruder temperature // Set the extruder temperature
await printServer.emit('printer.filamentstock.load', { await sendObjectAction(
printerId: loadFilamentStockFormValues.printer._id, loadFilamentStockFormValues.printer._id,
filamentStockId: loadFilamentStockFormValues.filamentStock._id 'printer',
}) {
type: 'loadFilamentStock',
data: {
filamentStock: loadFilamentStockFormValues.filamentStock
}
}
)
onOk() onOk()
} finally { } finally {
setLoadFilamentStockLoading(false) setLoadFilamentStockLoading(false)
@ -196,7 +183,7 @@ const LoadFilamentStock = ({
} }
]} ]}
> >
<PrinterSelect checkable={false} /> <ObjectSelect type='printer' checkable={false} />
</Form.Item> </Form.Item>
{targetTemperature == 0 ? ( {targetTemperature == 0 ? (
<Alert <Alert
@ -226,10 +213,9 @@ const LoadFilamentStock = ({
{loadFilamentStockFormValues.printer ? ( {loadFilamentStockFormValues.printer ? (
<PrinterTemperaturePanel <PrinterTemperaturePanel
showHeatedBed={false} showBed={false}
showMoreInfo={false} showMoreInfo={false}
printerId={loadFilamentStockFormValues.printer._id} id={loadFilamentStockFormValues.printer._id}
shouldUnsubscribe={false}
/> />
) : null} ) : null}
</Flex> </Flex>
@ -250,7 +236,7 @@ const LoadFilamentStock = ({
} }
]} ]}
> >
<FilamentStockSelect /> <ObjectSelect type='filamentStock' />
</Form.Item> </Form.Item>
</> </>
) )
@ -267,75 +253,28 @@ const LoadFilamentStock = ({
] ]
return ( return (
<Flex gap={'middle'}> <Form
{!isMobile && ( name='loadFilamentStock'
<div style={{ minWidth: '160px' }}> autoComplete='off'
<Steps form={loadFilamentStockForm}
current={currentStep} onFinish={handleLoadFilamentStock}
items={steps} onValuesChange={(changedValues) =>
direction='vertical' setLoadFilamentStockFormValues((prevValues) => ({
style={{ width: 'fit-content' }} ...prevValues,
/> ...changedValues
</div> }))
)} }
initialValues={initialLoadFilamentStockForm}
{!isMobile && <Divider type={'vertical'} style={{ height: 'unset' }} />} >
<WizardView
<Flex vertical={'true'} style={{ flexGrow: 1 }} gap='middle'> title='Load Filament Stock'
<Title level={2} style={{ marginTop: 0, marginBottom: 4 }}> steps={steps}
Load Filament Stock onSubmit={() => loadFilamentStockForm.submit()}
</Title> formValid={formValid}
<Form loading={loadFilamentStockLoading}
name='basic' submitText='Done'
autoComplete='off' />
form={loadFilamentStockForm} </Form>
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,34 +1,30 @@
import { useState, useContext, useEffect } from 'react' import { useState, useContext, useEffect } from 'react'
import { Form, Button, Typography, Flex, Steps, Divider, Alert } from 'antd' import { Form, Flex, Alert } from 'antd'
import { useMediaQuery } from 'react-responsive'
import PropTypes from 'prop-types' import PropTypes from 'prop-types'
import { PrintServerContext } from '../../context/PrintServerContext'
import PrinterSelect from '../../common/PrinterSelect' import ObjectSelect from '../../common/ObjectSelect'
import PrinterTemperaturePanel from '../../common/PrinterTemperaturePanel' import 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 [currentStep, setCurrentStep] = useState(0) const [formValid, setFormValid] = useState(false)
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)
@ -36,46 +32,35 @@ const UnloadFilamentStock = ({ onOk, reset, printer = null }) => {
const [unloadFilamentStockFormValues, setUnloadFilamentStockFormValues] = const [unloadFilamentStockFormValues, setUnloadFilamentStockFormValues] =
useState(initialUnloadFilamentStockForm) useState(initialUnloadFilamentStockForm)
// Add websocket temperature monitoring
useEffect(() => { useEffect(() => {
if (unloadFilamentStockFormValues.printer) { if (printer?._id && connected) {
const params = { const temperatureEventUnsubscribe = subscribeToObjectEvent(
printerId: unloadFilamentStockFormValues.printer._id, printer._id,
objects: { 'printer',
extruder: null, 'temperature',
'filament_switch_sensor fsensor': null (event) => {
if (event.data?.extruder?.current) {
setCurrentTemperature(event.data.extruder.current)
}
if (event.data?.extruder?.target) {
setTargetTemperature(event.data.extruder.target)
}
} }
} )
const filamentStockEventUnsubscribe = subscribeToObjectEvent(
const notifyStatusUpdate = (statusUpdate) => { printer._id,
if (statusUpdate?.extruder?.temperature !== undefined) { 'printer',
setCurrentTemperature(statusUpdate.extruder.temperature) 'filamentSensor',
(event) => {
setFilamentSensorDetected(event.data.detected)
} }
if (statusUpdate?.extruder?.target !== undefined) { )
setTargetTemperature(statusUpdate.extruder.target)
}
if (
statusUpdate?.['filament_switch_sensor fsensor']
?.filament_detected !== undefined
) {
setFilamentSensorDetected(
Boolean(
statusUpdate['filament_switch_sensor fsensor'].filament_detected
)
)
}
}
printServer.emit('printer.objects.subscribe', params)
printServer.emit('printer.objects.query', params)
printServer.on('notify_status_update', notifyStatusUpdate)
return () => { return () => {
printServer.off('notify_status_update', notifyStatusUpdate) if (temperatureEventUnsubscribe) temperatureEventUnsubscribe()
printServer.emit('printer.objects.unsubscribe', params) if (filamentStockEventUnsubscribe) filamentStockEventUnsubscribe()
} }
} }
}, [printServer, unloadFilamentStockFormValues.printer]) }, [printer?._id, connected])
useEffect(() => { useEffect(() => {
if (reset) { if (reset) {
@ -90,14 +75,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
setNextEnabled( setFormValid(
Boolean(unloadFilamentStockFormValues.printer) && Boolean(unloadFilamentStockFormValues.printer) &&
!unloadFilamentStockLoading && !unloadFilamentStockLoading &&
currentTemperature + 1 > targetTemperature && currentTemperature + 1 > targetTemperature &&
targetTemperature != 0 targetTemperature != 0
) )
}) })
.catch(() => setNextEnabled(false)) .catch(() => setFormValid(false))
}, [ }, [
unloadFilamentStockForm, unloadFilamentStockForm,
unloadFilamentStockFormValues, unloadFilamentStockFormValues,
@ -109,10 +94,13 @@ 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 printServer.emit('printer.gcode.script', { await sendObjectAction(
printerId: unloadFilamentStockFormValues.printer._id, unloadFilamentStockFormValues.printer._id,
script: `_CLIENT_LINEAR_MOVE E=-200 F=1000` 'printer',
}) {
type: 'unloadFilamentStock'
}
)
//setUnloadFilamentStockLoading(false) //setUnloadFilamentStockLoading(false)
} }
@ -140,7 +128,7 @@ const UnloadFilamentStock = ({ onOk, reset, printer = null }) => {
} }
]} ]}
> >
<PrinterSelect checkable={false} /> <ObjectSelect type='printer' checkable={false} />
</Form.Item> </Form.Item>
{unloadFilamentStockLoading == false ? ( {unloadFilamentStockLoading == false ? (
@ -182,11 +170,11 @@ const UnloadFilamentStock = ({ onOk, reset, printer = null }) => {
/> />
)} )}
{unloadFilamentStockFormValues.printer ? ( {unloadFilamentStockFormValues.printer?._id ? (
<PrinterTemperaturePanel <PrinterTemperaturePanel
showHeatedBed={false} showBed={false}
showMoreInfo={false} showMoreInfo={false}
printerId={unloadFilamentStockFormValues.printer._id} id={unloadFilamentStockFormValues.printer._id}
/> />
) : null} ) : null}
</Flex> </Flex>
@ -195,65 +183,28 @@ const UnloadFilamentStock = ({ onOk, reset, printer = null }) => {
] ]
return ( return (
<Flex gap={'middle'}> <Form
{!isMobile && ( name='unloadFilamentStock'
<div style={{ minWidth: '160px' }}> autoComplete='off'
<Steps form={unloadFilamentStockForm}
current={currentStep} onFinish={handleUnloadFilamentStock}
items={steps} onValuesChange={(changedValues) =>
direction='vertical' setUnloadFilamentStockFormValues((prevValues) => ({
style={{ width: 'fit-content' }} ...prevValues,
/> ...changedValues
</div> }))
)} }
initialValues={initialUnloadFilamentStockForm}
{!isMobile && <Divider type={'vertical'} style={{ height: 'unset' }} />} >
<WizardView
<Flex vertical={'true'} style={{ flexGrow: 1 }} gap='middle'> title='Unload Filament Stock'
<Title level={2} style={{ marginTop: 0, marginBottom: 4 }}> steps={steps}
Unload Filament Stock onSubmit={() => unloadFilamentStockForm.submit()}
</Title> formValid={formValid}
<Form loading={unloadFilamentStockLoading}
name='unloadFilamentStock' submitText={unloadFilamentStockLoading ? 'Unloading...' : 'Unload'}
autoComplete='off' />
form={unloadFilamentStockForm} </Form>
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,12 +3,26 @@ 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={{ objectType: 'documentJob', ...defaultValues }} defaultValues={defaultValuesRef.current}
> >
{({ handleSubmit, submitLoading, objectData, formValid }) => { {({ handleSubmit, submitLoading, objectData, formValid }) => {
const steps = [ const steps = [
@ -28,6 +42,10 @@ 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}
@ -35,8 +53,10 @@ 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={{ minWidth: '400px', minHeight: '500px' }}> <div style={{ minHeight: '500px', flexGrow: 1 }}>
<TemplatePreview <TemplatePreview
objectData={objectData?.object} objectData={objectData?.object}
documentTemplate={objectData?.documentTemplate} documentTemplate={objectData?.documentTemplate}
@ -46,10 +66,52 @@ const NewDocumentJob = ({ onOk, defaultValues = {} }) => {
/> />
</div> </div>
} }
onSubmit={() => { onSubmit={async () => {
handleSubmit() const newDocumentJob = await handleSubmit()
onOk() if (newDocumentJob.sendToFile == true) {
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,5 +1,6 @@
import { useRef } from 'react' import { useRef, useState } from 'react'
import { Button, Flex, Space, Dropdown } from 'antd' import { Button, Flex, Space, Dropdown, message, Modal } 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'
@ -7,10 +8,12 @@ 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] =
@ -18,6 +21,12 @@ 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',
@ -27,6 +36,8 @@ const DocumentPrinters = () => {
onClick: ({ key }) => { onClick: ({ key }) => {
if (key === 'reloadList') { if (key === 'reloadList') {
tableRef.current?.reload() tableRef.current?.reload()
} else if (key === 'newDocumentPrinter') {
setNewDocumentPrinterOpen(true)
} }
} }
} }
@ -34,6 +45,7 @@ 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}>
@ -62,6 +74,22 @@ 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,3 +1,4 @@
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'
@ -25,6 +26,8 @@ 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'
) )
@ -32,122 +35,125 @@ 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 <>
id={documentPrinterId} <Flex
type='documentPrinter' gap='large'
style={{ height: '100%' }} vertical='true'
> style={{
{({ maxHeight: '100%',
loading, minHeight: 0
isEditing, }}
startEditing, >
cancelEditing, <Flex justify={'space-between'}>
handleUpdate, <Space size='middle'>
formValid, <Space size='small'>
objectData, <ObjectActions
editLoading, type='documentPrinter'
lock, id={documentPrinterId}
fetchObject disabled={objectFormState.loading}
}) => { objectData={objectFormState.objectData}
// Define actions for ActionHandler />
const actions = { <ViewButton
reload: () => { disabled={objectFormState.loading}
fetchObject() items={[
return true {
}, key: 'info',
edit: () => { label: 'DocumentPrinter Information'
startEditing() },
return false { key: 'notes', label: 'Notes' },
}, { key: 'auditLogs', label: 'Audit Logs' }
cancelEdit: () => { ]}
cancelEditing() visibleState={collapseState}
return true updateVisibleState={updateCollapseState}
}, />
finishEdit: () => { <DocumentPrintButton
handleUpdate() type='documentPrinter'
return true objectData={objectFormState.objectData}
} 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>
return ( <div style={{ height: '100%', overflowY: 'scroll' }}>
<ActionHandler actions={actions} loading={loading}> <Flex vertical gap={'large'}>
{({ callAction }) => ( <ActionHandler
<Flex actions={actions}
gap='large' loading={objectFormState.loading}
vertical='true' ref={actionHandlerRef}
style={{ >
maxHeight: '100%', <InfoCollapse
minHeight: 0 title='Document Printer Information'
}} icon={<InfoCircleIcon />}
active={collapseState.info}
onToggle={(expanded) => updateCollapseState('info', expanded)}
collapseKey='info'
> >
<Flex justify={'space-between'}> <ObjectForm
<Space size='middle'> id={documentPrinterId}
<Space size='small'> type='documentPrinter'
<ObjectActions style={{ height: '100%' }}
type='documentPrinter' ref={objectFormRef}
id={documentPrinterId} onStateChange={(state) => {
disabled={loading} setEditFormState((prev) => ({ ...prev, ...state }))
objectData={objectData} }}
/> >
<ViewButton {({ loading, isEditing, objectData }) => {
disabled={loading} return (
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 />}
@ -155,52 +161,47 @@ 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) => onToggle={(expanded) => updateCollapseState('notes', expanded)}
updateCollapseState('notes', expanded) collapseKey='notes'
} >
collapseKey='notes' <Card>
> <NotesPanel _id={documentPrinterId} type='documentPrinter' />
<Card> </Card>
<NotesPanel </InfoCollapse>
_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'
> >
{loading ? ( {objectFormState.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

@ -0,0 +1,85 @@
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,19 +1,9 @@
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 NewObjectButtons from '../../common/NewObjectButtons' import WizardView from '../../common/WizardView'
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'}
@ -69,44 +59,18 @@ const NewDocumentTemplate = ({ onOk }) => {
) )
} }
] ]
return ( return (
<Flex gap='middle'> <WizardView
{!isMobile && ( steps={steps}
<div style={{ minWidth: '160px' }}> loading={submitLoading}
<Steps formValid={formValid}
current={currentStep} title='New Document Template'
items={steps} onSubmit={() => {
direction='vertical' handleSubmit()
style={{ width: 'fit-content' }} onOk()
/> }}
</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 } from 'antd' import { Space, Flex, Card, Splitter, Divider, Modal } 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,6 +28,9 @@ 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)
@ -57,6 +60,9 @@ const ControlPrinter = () => {
collapseState.movement collapseState.movement
) )
const [loadFilamentStockOpen, setLoadFilamentStockOpen] = useState(false)
const [unloadFilamentStockOpen, setUnloadFilamentStockOpen] = useState(false)
useEffect(() => { useEffect(() => {
setSideBarVisible( setSideBarVisible(
collapseState.temperature || collapseState.temperature ||
@ -122,6 +128,39 @@ 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
} }
} }
@ -146,298 +185,338 @@ const ControlPrinter = () => {
) )
return ( return (
<Flex <>
gap='large' <Flex
vertical='true' gap='large'
style={{ vertical='true'
maxHeight: '100%', style={{
minHeight: 0 maxHeight: '100%',
}} minHeight: 0
> }}
<Flex justify={'space-between'}> >
<Space size='middle'> <Flex justify={'space-between'}>
<Space size='small'> <Space size='middle'>
<ObjectActions <Space size='small'>
type='printer' <ObjectActions
id={printerId} type='printer'
disabled={objectFormState.loading} id={printerId}
visibleActions={{ edit: false }} disabled={objectFormState.loading}
objectData={objectFormState.objectData} visibleActions={{ edit: false }}
/> objectData={objectFormState.objectData}
<ViewButton />
disabled={objectFormState.loading} <ViewButton
items={[ disabled={objectFormState.loading}
{ items={[
key: 'printer', {
label: 'Printer' key: 'printer',
}, label: 'Printer'
{ },
key: 'job', {
label: 'Job' key: 'job',
}, label: 'Job'
{ },
key: 'subJob', {
label: 'Sub Job' key: 'subJob',
}, label: 'Sub Job'
{ },
key: 'filamentStock', {
label: 'Filament Stock' key: 'filamentStock',
}, label: 'Filament Stock'
{ },
key: 'temperature', {
label: 'Temperature' key: 'temperature',
}, label: 'Temperature'
{ },
key: 'position', {
label: 'Position' key: 'position',
}, label: 'Position'
{ },
key: 'movement', {
label: 'Movement' key: 'movement',
}, label: 'Movement'
{ key: 'notes', label: 'Notes' } },
]} { key: 'notes', label: 'Notes' }
visibleState={collapseState} ]}
updateVisibleState={updateCollapseState} visibleState={collapseState}
/> updateVisibleState={updateCollapseState}
<AlertsDisplay alerts={objectFormState.objectData?.alerts} /> />
<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}
/>
</Space> </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}
/>
</Space>
</Flex>
<div style={{ height: '100%', overflowY: 'scroll' }}>
<Flex vertical gap={'large'}>
<ActionHandler
actions={actions}
loading={objectFormState.loading}
ref={actionHandlerRef}
>
<Flex vertical>
<Splitter className={'farmcontrol-splitter'}>
<Splitter.Panel>
<Flex vertical gap={'large'}>
<InfoCollapse
title={'Printer'}
icon={<PrinterIcon />}
collapseKey='printer'
active={collapseState.printer}
onToggle={(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
id={objectFormState.objectData.currentJob._id}
type='job'
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}
</Splitter.Panel>
) : null}
</Splitter>
{isMobile ? (
<>
<Divider />
{sideBarItems}
</>
) : null}
</Flex>
</ActionHandler>
<InfoCollapse
title='Notes'
icon={<NoteIcon />}
active={collapseState.notes}
onToggle={(expanded) => updateCollapseState('notes', expanded)}
collapseKey='notes'
>
<Card>
<NotesPanel _id={printerId} type='printer' />
</Card>
</InfoCollapse>
</Flex> </Flex>
</div>
</Flex> <div style={{ height: '100%', overflowY: 'scroll' }}>
<Flex vertical gap={'large'}>
<ActionHandler
actions={actions}
loading={objectFormState.loading}
ref={actionHandlerRef}
>
<Flex vertical>
<Splitter className={'farmcontrol-splitter'}>
<Splitter.Panel>
<Flex vertical gap={'large'}>
<InfoCollapse
title={'Printer'}
icon={<PrinterIcon />}
collapseKey='printer'
active={collapseState.printer}
onToggle={(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,
online: false,
active: false,
currentFilamentStock: false,
'currentFilamentStock._id': false,
currentJob: false,
'currentJob._id': false,
currentSubJob: false,
'currentSubJob._id': 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
id={objectFormState.objectData.currentJob._id}
type='job'
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}
</Splitter.Panel>
) : null}
</Splitter>
{isMobile ? (
<>
<Divider />
{sideBarItems}
</>
) : null}
</Flex>
</ActionHandler>
<InfoCollapse
title='Notes'
icon={<NoteIcon />}
active={collapseState.notes}
onToggle={(expanded) => updateCollapseState('notes', expanded)}
collapseKey='notes'
>
<Card>
<NotesPanel _id={printerId} type='printer' />
</Card>
</InfoCollapse>
</Flex>
</div>
</Flex>
<Modal
open={loadFilamentStockOpen}
onCancel={() => setLoadFilamentStockOpen(false)}
footer={null}
width='700px'
destroyOnHidden={true}
>
<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,7 +108,14 @@ const DocumentPrintButton = ({
onCancel={() => setNewDocumentJobOpen(false)} onCancel={() => setNewDocumentJobOpen(false)}
footer={null} footer={null}
destroyOnHidden={true} destroyOnHidden={true}
width={900} width={{
xs: '100%',
sm: '100%',
md: '100%',
lg: '90%',
xl: '80%',
xxl: '80%'
}}
> >
<NewDocumentJob <NewDocumentJob
onOk={() => { onOk={() => {

View File

@ -9,7 +9,8 @@ const NewObjectButtons = ({
onSubmit, onSubmit,
formValid, formValid,
submitLoading, submitLoading,
submitText = 'Done' submitText = 'Done',
disabled = false
}) => { }) => {
return ( return (
<Flex justify='end'> <Flex justify='end'>
@ -24,14 +25,18 @@ const NewObjectButtons = ({
) : null} ) : null}
{currentStep < totalSteps - 1 ? ( {currentStep < totalSteps - 1 ? (
<Button type='primary' disabled={!formValid} onClick={onNext}> <Button
type='primary'
disabled={!formValid || disabled}
onClick={onNext}
>
Next Next
</Button> </Button>
) : ( ) : (
<Button <Button
type='primary' type='primary'
loading={submitLoading} loading={submitLoading}
disabled={!formValid} disabled={!formValid || disabled}
onClick={onSubmit} onClick={onSubmit}
> >
{submitText} {submitText}
@ -49,7 +54,8 @@ 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,24 +138,29 @@ const ObjectForm = forwardRef(
return computedValues return computedValues
}, []) }, [])
// Validate form on change // Validate form on change (debounced to avoid heavy work on every keystroke)
useEffect(() => { useEffect(() => {
form const timeoutId = setTimeout(() => {
.validateFields({ validateOnly: true }) form
.then(() => { .validateFields({ validateOnly: true })
setFormValid(true) .then(() => {
onStateChange({ setFormValid(true)
formValid: true, onStateChange({
objectData: { ...serverObjectData, ...form.getFieldsValue() } formValid: true,
objectData: { ...serverObjectData, ...form.getFieldsValue() }
})
}) })
}) .catch(() => {
.catch(() => { setFormValid(false)
onStateChange({ onStateChange({
formValid: true, formValid: false,
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(() => {
@ -252,9 +257,14 @@ const ObjectForm = forwardRef(
updateLockEventHandler updateLockEventHandler
]) ])
// Debounce objectData updates sent to parent to limit re-renders
useEffect(() => { useEffect(() => {
onStateChange({ objectData }) const timeoutId = setTimeout(() => {
}, [objectData]) onStateChange({ objectData })
}, 150)
return () => clearTimeout(timeoutId)
}, [objectData, onStateChange])
const startEditing = () => { const startEditing = () => {
setIsEditing(true) setIsEditing(true)
@ -366,9 +376,18 @@ const ObjectForm = forwardRef(
model model
) )
// Update form with computed values if any were calculated // Update form with computed values if any were calculated and they changed
if (Object.keys(computedValues).length > 0) { if (Object.keys(computedValues).length > 0) {
form.setFieldsValue(computedValues) const currentComputedValues = form.getFieldsValue(
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,6 +81,8 @@ const ObjectProperty = ({
minimal = false, minimal = false,
previewOpen = false, previewOpen = false,
showPreview = true, showPreview = true,
options = [],
roundNumber = false,
showHyperlink, showHyperlink,
...rest ...rest
}) => { }) => {
@ -166,6 +168,18 @@ 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':
@ -234,10 +248,15 @@ 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' ? value.toFixed(2) : value} {typeof value === 'number' ? roundedValue : value}
{suffix} {suffix}
</Text> </Text>
) )
@ -553,6 +572,17 @@ 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}>
@ -773,7 +803,8 @@ 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,6 +144,7 @@ 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,
@ -278,8 +279,11 @@ const ObjectSelect = ({
handleFetchObjectsProperties() handleFetchObjectsProperties()
setInitialized(true) setInitialized(true)
} }
if (value == null) {
setTreeSelectValue(null)
setInitialized(true)
}
} }
handleValue() handleValue()
}, [ }, [
value, value,
@ -303,8 +307,13 @@ 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) { if (hasMore && lazyLoading == false) {
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]) }, [pages, createSkeletonData, fetchData, hasMore, lazyLoading])
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) { if (prevPage > 0 && lazyLoading == false) {
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]) }, [pages, createSkeletonData, fetchData, lazyLoading])
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: fixed, fixed: isMobile ? undefined : 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: 80 + rowActions.length * 40, // Adjust width based on number of actions width: 20 + rowActions.length * 30, // Adjust width based on number of actions
render: (record) => { render: (record) => {
return renderActions(record) return renderActions(record)
} }

View File

@ -17,7 +17,8 @@ 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 (
@ -31,9 +32,7 @@ const ObjectTypeSelect = ({
allowClear={allowClear} allowClear={allowClear}
disabled={disabled} disabled={disabled}
filterOption={(input, option) => filterOption={(input, option) =>
option.label.props.children[1].props.children option.searchText?.includes(input.toLowerCase()) ?? false
.toLowerCase()
.indexOf(input.toLowerCase()) >= 0
} }
options={options} options={options}
/> />

View File

@ -4,6 +4,13 @@ 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
@ -16,10 +23,14 @@ const StateDisplay = ({ state, showProgress = true, showState = true }) => {
<StateTag state={currentState.type} /> <StateTag state={currentState.type} />
</Space> </Space>
)} )}
{showProgress && currentState?.progress && currentState?.progress > 0 ? ( {showProgress &&
loadingProgressTypes.includes(currentState.type) &&
currentState?.progress &&
currentState?.progress > 0 ? (
<Progress <Progress
percent={Math.round(currentState.progress * 100)} percent={Math.round(currentState.progress * 100)}
status='active' status={currentState.type === 'used' ? '' : '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 'unconsumed': case 'new':
status = 'success' status = 'success'
text = 'Unconsumed' text = 'New'
break break
case 'error': case 'error':
status = 'error' status = 'error'
@ -80,6 +80,10 @@ 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 } from 'antd' import { Flex, Button, Input, Select } 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,22 +14,26 @@ const TemplatePreview = ({
isEditing, isEditing,
onTestObjectOpen, onTestObjectOpen,
onPreviewMessage, onPreviewMessage,
showTestObject = false showTestObject = false,
showPreviewSwitch = true
}) => { }) => {
const iframeRef = useRef(null) const iframeRef = useRef(null)
const { fetchTemplatePreview } = useContext(ApiServerContext) const { fetchTemplatePreview, fetchTemplatePDF } =
const [previewContent, setPreviewContent] = useState('') useContext(ApiServerContext)
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 updatePreviewContent = (html) => { const updatePreviewContentHTML = (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
setPreviewContent(html) setPreviewContentHTML(html)
// Restore scroll position after iframe loads new content // Restore scroll position after iframe loads new content
const handleLoad = () => { const handleLoad = () => {
@ -40,6 +44,23 @@ 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)
@ -54,7 +75,7 @@ const TemplatePreview = ({
// Handle error through parent component // Handle error through parent component
onPreviewMessage(result.error, true) onPreviewMessage(result.error, true)
} else { } else {
updatePreviewContent(result.html) updatePreviewContentHTML(result.html)
onPreviewMessage('No issues found.', false) onPreviewMessage('No issues found.', false)
} }
} }
@ -66,9 +87,13 @@ 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) {
reloadPreview(documentTemplate.content, objectData, previewScale) if (previewType == 'HTML') {
reloadPreview(documentTemplate.content, objectData, previewScale)
} else {
reloadPreviewPDF(documentTemplate.content, objectData)
}
} }
}, [objectData, documentTemplate, previewScale, reloadPreview]) }, [objectData, documentTemplate, previewScale, previewType])
return ( return (
<Flex vertical gap={'middle'} style={{ height: '100%' }}> <Flex vertical gap={'middle'} style={{ height: '100%' }}>
@ -98,34 +123,51 @@ 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' }}
loading={loading || reloadLoading} disabled={loading || reloadLoading || previewType == 'PDF'}
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={previewContent} srcDoc={previewType == 'HTML' ? previewContentHTML : undefined}
src={previewType == 'PDF' ? pdfBlob : undefined}
frameBorder='0' frameBorder='0'
style={{ style={{
width: '100%', width: '100%',
@ -146,7 +188,8 @@ 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,7 +1,15 @@
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 { Typography, Flex, Steps, Divider, Progress } from 'antd' import {
Typography,
Flex,
Steps,
Divider,
Progress,
Button,
Dropdown
} from 'antd'
import NewObjectButtons from './NewObjectButtons' import NewObjectButtons from './NewObjectButtons'
const { Title } = Typography const { Title } = Typography
@ -15,7 +23,10 @@ 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 })
@ -26,7 +37,7 @@ const WizardView = ({
sideBar != null ? ( sideBar != null ? (
sideBar sideBar
) : ( ) : (
<div style={{ minWidth: '160px' }}> <div style={{ minWidth: sideBarGrow == true ? '100%' : '160px' }}>
<Steps <Steps
current={currentStep} current={currentStep}
items={steps} items={steps}
@ -45,7 +56,13 @@ const WizardView = ({
vertical vertical
justify='space-between' justify='space-between'
gap={'middle'} gap={'middle'}
style={{ width: '100%' }} style={
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 }}>
@ -63,8 +80,37 @@ 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)}
@ -87,9 +133,12 @@ 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,9 +308,14 @@ 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 {
@ -529,7 +534,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, type: type }) socketRef.current.emit('subscribe_lock', { _id: id, objectType: 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
@ -853,6 +858,53 @@ 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) {
@ -942,6 +994,22 @@ 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={{
@ -967,11 +1035,14 @@ 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: 'http://192.168.68.53:8080', backendUrl: 'https://dev.tombutcher.work/api',
printServerUrl: 'ws://192.168.68.53:8081', printServerUrl: 'ws://192.168.68.53:8081',
apiServerUrl: 'ws://192.168.68.53:9090', apiServerUrl: 'https://dev-wss.tombutcher.work',
logLevel: 'error' logLevel: 'error'
} }
} }

View File

@ -2,11 +2,12 @@ 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: 'DSZ', prefix: 'DJB',
icon: DocumentJobIcon, icon: DocumentJobIcon,
actions: [ actions: [
{ {
@ -60,7 +61,9 @@ export const DocumentJob = {
columnWidth: 200, columnWidth: 200,
columnFixed: 'left', columnFixed: 'left',
value: (objectData) => { value: (objectData) => {
return `${objectData?.documentTemplate?.name || 'No template'} (${objectData?.object?.name || 'No name'})` if (objectData?.createdAt == undefined) {
return `${objectData?.documentTemplate?.name || 'No template'} ${dayjs(new Date()).format('YYYY-MM-DD HH:mm:ss')} (${objectData?.object?.name || objectData?.object?._id})`
}
} }
}, },
{ {
@ -69,6 +72,14 @@ 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',
@ -86,6 +97,15 @@ 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',
@ -101,6 +121,13 @@ export const DocumentJob = {
} }
} }
}, },
{
name: 'documentTemplate._id',
label: 'Template ID',
type: 'id',
showHyperlink: true,
objectType: 'documentTemplate'
},
{ {
name: 'documentPrinter', name: 'documentPrinter',
label: 'Printer', label: 'Printer',
@ -114,6 +141,13 @@ export const DocumentJob = {
online: true online: true
} }
} }
},
{
name: 'documentPrinter._id',
label: 'Printer ID',
type: 'id',
showHyperlink: true,
objectType: 'documentPrinter'
} }
] ]
} }

View File

@ -74,52 +74,88 @@ export const DocumentPrinter = {
readOnly: true readOnly: true
}, },
{ {
name: 'documentSize', name: 'state',
label: 'Document Size', label: 'Status',
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: 'documentSize._id', name: 'currentDocumentSize._id',
label: 'Document Size ID', label: 'Current 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: 'Start', name: 'startQueue',
label: 'Start', label: 'Start Queue',
icon: PlayCircleIcon, icon: PlayCircleIcon,
disabled: (objectData) => { disabled: (objectData) => {
console.log(objectData?.subJobs?.length) console.log(objectData?.subJobs?.length)
@ -109,28 +109,60 @@ 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: 'pause', name: 'pauseJob',
label: 'Pause', label: 'Pause Job',
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=pauseQueue` `/dashboard/production/printers/control?printerId=${_id}&action=pauseJob`
}, },
{ {
name: 'Stop', name: 'resumeJob',
label: 'Stop', label: 'Resume Job',
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=stopQueue` `/dashboard/production/printers/control?printerId=${_id}&action=cancelJob`
}
]
},
{
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`
} }
] ]
} }
@ -299,6 +331,14 @@ 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
} }
] ]
} }