Compare commits
26 Commits
c2f55a5967
...
c6c99bb02c
| Author | SHA1 | Date | |
|---|---|---|---|
| c6c99bb02c | |||
| 91e7121fd5 | |||
| afe25c1e09 | |||
| 8d63b9a84d | |||
| 808d45273d | |||
| 64c4d25982 | |||
| 7ac56cc69c | |||
| e4b8f52e6b | |||
| e114a45348 | |||
| b7bb6121b7 | |||
| dabad4f489 | |||
| 94c55c8e77 | |||
| 8f67d7daae | |||
| e788eaba91 | |||
| 6af832afe5 | |||
| 69c387ca1d | |||
| ecf73c13c0 | |||
| 657d6a5c6e | |||
| 8f34c262a0 | |||
| 5cb586246f | |||
| b12d230a8e | |||
| aeebaaddda | |||
| 8ed0287d73 | |||
| 030053b965 | |||
| 2d2df403e3 | |||
| 8c622420bb |
@ -43,7 +43,7 @@
|
||||
vertical-align: middle !important;
|
||||
}
|
||||
|
||||
.ant-typography-ellipsis-single-line >code {
|
||||
.ant-typography-ellipsis-single-line > code {
|
||||
vertical-align: top !important;
|
||||
}
|
||||
|
||||
@ -55,7 +55,9 @@
|
||||
-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;
|
||||
}
|
||||
|
||||
@ -67,7 +69,8 @@
|
||||
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;
|
||||
line-height: 32.5px !important;
|
||||
}
|
||||
@ -77,11 +80,10 @@
|
||||
min-width: 55px !important;
|
||||
}
|
||||
|
||||
.loading-modal .ant-modal-footer {
|
||||
display: none;
|
||||
.loading-modal .ant-modal-footer {
|
||||
display: none;
|
||||
}
|
||||
|
||||
|
||||
:root {
|
||||
--unit-100vh: 100vh;
|
||||
}
|
||||
@ -91,8 +93,8 @@ display: none;
|
||||
}
|
||||
}
|
||||
|
||||
.dashboard-cards-header .ant-table-tbody {
|
||||
display: none;
|
||||
.dashboard-cards-header .ant-table-tbody {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.ant-menu-overflow-item-rest::after {
|
||||
@ -225,19 +227,21 @@ body {
|
||||
}
|
||||
/* --- End of src/components/Dashboard/common/DashboardSidebar.css --- */
|
||||
|
||||
.objectTableDescritions >.ant-descriptions-view > table {
|
||||
.objectTableDescritions > .ant-descriptions-view > table {
|
||||
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%;
|
||||
}
|
||||
|
||||
.farmcontrol-splitter > .ant-splitter-bar {
|
||||
margin: 8px
|
||||
margin: 8px;
|
||||
}
|
||||
|
||||
|
||||
.dark-mode {
|
||||
--sb-track-color: #1d1d1d;
|
||||
--sb-thumb-color: #848484;
|
||||
@ -249,32 +253,31 @@ body {
|
||||
--sb-size: 8px;
|
||||
}
|
||||
|
||||
|
||||
::-webkit-scrollbar {
|
||||
width: 8px;
|
||||
width: 8px;
|
||||
}
|
||||
|
||||
::-webkit-scrollbar:vertical {
|
||||
width: 8px;
|
||||
width: 8px;
|
||||
}
|
||||
|
||||
::-webkit-scrollbar:horizontal {
|
||||
height: 8px;
|
||||
height: 8px;
|
||||
}
|
||||
|
||||
/* Track */
|
||||
::-webkit-scrollbar-track {
|
||||
background: transparent;
|
||||
background: transparent;
|
||||
}
|
||||
|
||||
/* Handle */
|
||||
::-webkit-scrollbar-thumb {
|
||||
background: #8888881a;
|
||||
border: 2px solid rgba(0, 0, 0, 0);
|
||||
background-clip: padding-box;
|
||||
transition: all 1s;
|
||||
-moz-transition: all 1s;
|
||||
-webkit-transition: all 1s;
|
||||
background: #8888881a;
|
||||
border: 2px solid rgba(0, 0, 0, 0);
|
||||
background-clip: padding-box;
|
||||
transition: all 1s;
|
||||
-moz-transition: all 1s;
|
||||
-webkit-transition: all 1s;
|
||||
}
|
||||
|
||||
::-webkit-scrollbar-corner {
|
||||
@ -291,9 +294,9 @@ body {
|
||||
|
||||
/* Handle on hover */
|
||||
::-webkit-scrollbar-thumb:hover {
|
||||
background: #5555551f;
|
||||
border: 2px solid rgba(0, 0, 0, 0);
|
||||
background-clip: padding-box;
|
||||
background: #5555551f;
|
||||
border: 2px solid rgba(0, 0, 0, 0);
|
||||
background-clip: padding-box;
|
||||
}
|
||||
|
||||
::-webkit-scrollbar-thumb:vertical:hover {
|
||||
@ -307,3 +310,7 @@ body {
|
||||
.ant-table-body {
|
||||
scrollbar-color: auto;
|
||||
}
|
||||
|
||||
.ant-select-selection-item .ant-tag {
|
||||
margin-left: 1px !important;
|
||||
}
|
||||
|
||||
855
package-lock.json
generated
855
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@ -1,27 +1,14 @@
|
||||
import { useState, useContext, useEffect } from 'react'
|
||||
import {
|
||||
Form,
|
||||
Button,
|
||||
Typography,
|
||||
Flex,
|
||||
Steps,
|
||||
Divider,
|
||||
Descriptions,
|
||||
Alert
|
||||
} from 'antd'
|
||||
import { useMediaQuery } from 'react-responsive'
|
||||
import { Form, Flex, Descriptions, Alert } from 'antd'
|
||||
import PropTypes from 'prop-types'
|
||||
import { PrintServerContext } from '../../context/PrintServerContext'
|
||||
|
||||
import FilamentStockSelect from '../../common/FilamentStockSelect'
|
||||
import PrinterSelect from '../../common/PrinterSelect'
|
||||
import FilamentStockDisplay from '../../common/FilamentStockDisplay'
|
||||
import PrinterTemperaturePanel from '../../common/PrinterTemperaturePanel'
|
||||
|
||||
import { LoadingOutlined } from '@ant-design/icons'
|
||||
import PrinterState from '../../common/StateDisplay'
|
||||
|
||||
const { Title } = Typography
|
||||
import ObjectSelect from '../../common/ObjectSelect'
|
||||
import ObjectDisplay from '../../common/ObjectDisplay'
|
||||
import WizardView from '../../common/WizardView'
|
||||
import { ApiServerContext } from '../../context/ApiServerContext'
|
||||
|
||||
const LoadFilamentStock = ({
|
||||
onOk,
|
||||
@ -29,7 +16,8 @@ const LoadFilamentStock = ({
|
||||
printer = null,
|
||||
filamentStockLoaded = false
|
||||
}) => {
|
||||
const isMobile = useMediaQuery({ maxWidth: 768 })
|
||||
const { connected, subscribeToObjectEvent, sendObjectAction } =
|
||||
useContext(ApiServerContext)
|
||||
|
||||
LoadFilamentStock.propTypes = {
|
||||
onOk: PropTypes.func.isRequired,
|
||||
@ -38,8 +26,6 @@ const LoadFilamentStock = ({
|
||||
filamentStockLoaded: PropTypes.bool
|
||||
}
|
||||
|
||||
const { printServer } = useContext(PrintServerContext)
|
||||
|
||||
const initialLoadFilamentStockForm = {
|
||||
printer: printer,
|
||||
filamentStock: null
|
||||
@ -47,8 +33,7 @@ const LoadFilamentStock = ({
|
||||
|
||||
const [loadFilamentStockLoading, setLoadFilamentStockLoading] =
|
||||
useState(false)
|
||||
const [currentStep, setCurrentStep] = useState(0)
|
||||
const [nextEnabled, setNextEnabled] = useState(false)
|
||||
const [formValid, setFormValid] = useState(false)
|
||||
const [currentTemperature, setCurrentTemperature] = useState(-1)
|
||||
const [targetTemperature, setTargetTemperature] = useState(0)
|
||||
const [filamentSensorDetected, setFilamentSensorDetected] =
|
||||
@ -62,77 +47,69 @@ const LoadFilamentStock = ({
|
||||
loadFilamentStockForm
|
||||
)
|
||||
|
||||
// Add websocket temperature monitoring
|
||||
useEffect(() => {
|
||||
if (loadFilamentStockFormValues.printer) {
|
||||
const params = {
|
||||
printerId: loadFilamentStockFormValues.printer._id,
|
||||
objects: {
|
||||
extruder: null,
|
||||
'filament_switch_sensor fsensor': null
|
||||
if (printer?._id && connected) {
|
||||
const temperatureEventUnsubscribe = subscribeToObjectEvent(
|
||||
printer._id,
|
||||
'printer',
|
||||
'temperature',
|
||||
(event) => {
|
||||
if (event.data?.extruder?.current) {
|
||||
setCurrentTemperature(event.data.extruder.current)
|
||||
}
|
||||
if (event.data?.extruder?.target) {
|
||||
setTargetTemperature(event.data.extruder.target)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const notifyStatusUpdate = (statusUpdate) => {
|
||||
if (statusUpdate?.extruder?.temperature !== undefined) {
|
||||
setCurrentTemperature(statusUpdate.extruder.temperature)
|
||||
)
|
||||
const filamentStockEventUnsubscribe = subscribeToObjectEvent(
|
||||
printer._id,
|
||||
'printer',
|
||||
'filamentSensor',
|
||||
(event) => {
|
||||
console.log('filamentSensor', event.data)
|
||||
setFilamentSensorDetected(event.data.detected)
|
||||
}
|
||||
if (statusUpdate?.extruder?.target !== undefined) {
|
||||
setTargetTemperature(statusUpdate.extruder.target)
|
||||
}
|
||||
if (
|
||||
statusUpdate?.['filament_switch_sensor fsensor']
|
||||
?.filament_detected !== undefined
|
||||
) {
|
||||
setFilamentSensorDetected(
|
||||
Boolean(
|
||||
statusUpdate['filament_switch_sensor fsensor'].filament_detected
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
printServer.emit('printer.objects.subscribe', params)
|
||||
printServer.emit('printer.objects.query', params)
|
||||
printServer.on('notify_status_update', notifyStatusUpdate)
|
||||
|
||||
)
|
||||
return () => {
|
||||
printServer.off('notify_status_update', notifyStatusUpdate)
|
||||
printServer.emit('printer.objects.unsubscribe', params)
|
||||
if (temperatureEventUnsubscribe) temperatureEventUnsubscribe()
|
||||
if (filamentStockEventUnsubscribe) filamentStockEventUnsubscribe()
|
||||
}
|
||||
}
|
||||
}, [printServer, loadFilamentStockFormValues.printer])
|
||||
}, [printer?._id, connected])
|
||||
|
||||
useEffect(() => {
|
||||
// Validate form fields
|
||||
loadFilamentStockForm
|
||||
.validateFields({
|
||||
validateOnly: true
|
||||
})
|
||||
.then(() => setNextEnabled(filamentSensorDetected))
|
||||
.catch(() => setNextEnabled(false))
|
||||
.then(() => {
|
||||
const hasPrinter = Boolean(loadFilamentStockFormValues.printer)
|
||||
const hasFilamentStock = Boolean(
|
||||
loadFilamentStockFormValues.filamentStock
|
||||
)
|
||||
|
||||
// Step 0 (Preheat): needs printer + filament detected + (temp reached or no temp set)
|
||||
const preheatReady =
|
||||
hasPrinter &&
|
||||
filamentSensorDetected &&
|
||||
(targetTemperature === 0 || currentTemperature >= targetTemperature)
|
||||
|
||||
// Step 1+ (Required/Summary): needs filamentStock selected
|
||||
// Form is valid if preheat is ready (for step 0) OR filamentStock is selected (for step 1+)
|
||||
// This allows progression: step 0 can proceed when preheat is ready,
|
||||
// and step 1+ can proceed when filamentStock is selected
|
||||
setFormValid(preheatReady || (hasPrinter && hasFilamentStock))
|
||||
})
|
||||
.catch(() => setFormValid(false))
|
||||
}, [
|
||||
loadFilamentStockForm,
|
||||
loadFilamentStockFormUpdateValues,
|
||||
filamentSensorDetected
|
||||
])
|
||||
|
||||
useEffect(() => {
|
||||
if (
|
||||
filamentSensorDetected == true &&
|
||||
currentTemperature >= targetTemperature
|
||||
) {
|
||||
setNextEnabled(filamentSensorDetected)
|
||||
if (currentStep == 0) {
|
||||
setCurrentStep(1)
|
||||
}
|
||||
} else if (filamentSensorDetected == false) {
|
||||
setCurrentStep(0)
|
||||
}
|
||||
}, [
|
||||
loadFilamentStockFormValues,
|
||||
filamentSensorDetected,
|
||||
targetTemperature,
|
||||
currentTemperature,
|
||||
currentStep
|
||||
targetTemperature
|
||||
])
|
||||
|
||||
const summaryItems = [
|
||||
@ -140,8 +117,9 @@ const LoadFilamentStock = ({
|
||||
key: 'filamentStock',
|
||||
label: 'Stock',
|
||||
children: loadFilamentStockFormValues.filamentStock ? (
|
||||
<FilamentStockDisplay
|
||||
filamentStock={loadFilamentStockFormValues.filamentStock}
|
||||
<ObjectDisplay
|
||||
objectType='filamentStock'
|
||||
object={loadFilamentStockFormValues.filamentStock}
|
||||
/>
|
||||
) : (
|
||||
'n/a'
|
||||
@ -151,9 +129,12 @@ const LoadFilamentStock = ({
|
||||
key: 'printer',
|
||||
label: 'Printer',
|
||||
children: loadFilamentStockFormValues.printer ? (
|
||||
<PrinterState printer={loadFilamentStockFormValues.printer} />
|
||||
<ObjectDisplay
|
||||
objectType='printer'
|
||||
object={loadFilamentStockFormValues.printer}
|
||||
/>
|
||||
) : (
|
||||
'n/a>'
|
||||
'n/a'
|
||||
)
|
||||
}
|
||||
]
|
||||
@ -169,10 +150,16 @@ const LoadFilamentStock = ({
|
||||
|
||||
try {
|
||||
// Set the extruder temperature
|
||||
await printServer.emit('printer.filamentstock.load', {
|
||||
printerId: loadFilamentStockFormValues.printer._id,
|
||||
filamentStockId: loadFilamentStockFormValues.filamentStock._id
|
||||
})
|
||||
await sendObjectAction(
|
||||
loadFilamentStockFormValues.printer._id,
|
||||
'printer',
|
||||
{
|
||||
type: 'loadFilamentStock',
|
||||
data: {
|
||||
filamentStock: loadFilamentStockFormValues.filamentStock
|
||||
}
|
||||
}
|
||||
)
|
||||
onOk()
|
||||
} finally {
|
||||
setLoadFilamentStockLoading(false)
|
||||
@ -196,7 +183,7 @@ const LoadFilamentStock = ({
|
||||
}
|
||||
]}
|
||||
>
|
||||
<PrinterSelect checkable={false} />
|
||||
<ObjectSelect type='printer' checkable={false} />
|
||||
</Form.Item>
|
||||
{targetTemperature == 0 ? (
|
||||
<Alert
|
||||
@ -226,10 +213,9 @@ const LoadFilamentStock = ({
|
||||
|
||||
{loadFilamentStockFormValues.printer ? (
|
||||
<PrinterTemperaturePanel
|
||||
showHeatedBed={false}
|
||||
showBed={false}
|
||||
showMoreInfo={false}
|
||||
printerId={loadFilamentStockFormValues.printer._id}
|
||||
shouldUnsubscribe={false}
|
||||
id={loadFilamentStockFormValues.printer._id}
|
||||
/>
|
||||
) : null}
|
||||
</Flex>
|
||||
@ -250,7 +236,7 @@ const LoadFilamentStock = ({
|
||||
}
|
||||
]}
|
||||
>
|
||||
<FilamentStockSelect />
|
||||
<ObjectSelect type='filamentStock' />
|
||||
</Form.Item>
|
||||
</>
|
||||
)
|
||||
@ -267,75 +253,28 @@ const LoadFilamentStock = ({
|
||||
]
|
||||
|
||||
return (
|
||||
<Flex gap={'middle'}>
|
||||
{!isMobile && (
|
||||
<div style={{ minWidth: '160px' }}>
|
||||
<Steps
|
||||
current={currentStep}
|
||||
items={steps}
|
||||
direction='vertical'
|
||||
style={{ width: 'fit-content' }}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{!isMobile && <Divider type={'vertical'} style={{ height: 'unset' }} />}
|
||||
|
||||
<Flex vertical={'true'} style={{ flexGrow: 1 }} gap='middle'>
|
||||
<Title level={2} style={{ marginTop: 0, marginBottom: 4 }}>
|
||||
Load Filament Stock
|
||||
</Title>
|
||||
<Form
|
||||
name='basic'
|
||||
autoComplete='off'
|
||||
form={loadFilamentStockForm}
|
||||
onFinish={handleLoadFilamentStock}
|
||||
onValuesChange={(changedValues) =>
|
||||
setLoadFilamentStockFormValues((prevValues) => ({
|
||||
...prevValues,
|
||||
...changedValues
|
||||
}))
|
||||
}
|
||||
initialValues={initialLoadFilamentStockForm}
|
||||
>
|
||||
<div style={{ minHeight: '260px' }}>{steps[currentStep].content}</div>
|
||||
|
||||
<Flex justify={'end'}>
|
||||
<Button
|
||||
style={{
|
||||
margin: '0 8px'
|
||||
}}
|
||||
onClick={() => setCurrentStep(currentStep - 1)}
|
||||
disabled={!(currentStep > 0)}
|
||||
>
|
||||
Previous
|
||||
</Button>
|
||||
{currentStep < steps.length - 1 && (
|
||||
<Button
|
||||
type='primary'
|
||||
disabled={!nextEnabled}
|
||||
onClick={() => {
|
||||
setCurrentStep(currentStep + 1)
|
||||
}}
|
||||
>
|
||||
Next
|
||||
</Button>
|
||||
)}
|
||||
{currentStep === steps.length - 1 && (
|
||||
<Button
|
||||
type='primary'
|
||||
loading={loadFilamentStockLoading}
|
||||
onClick={() => {
|
||||
loadFilamentStockForm.submit()
|
||||
}}
|
||||
>
|
||||
Done
|
||||
</Button>
|
||||
)}
|
||||
</Flex>
|
||||
</Form>
|
||||
</Flex>
|
||||
</Flex>
|
||||
<Form
|
||||
name='loadFilamentStock'
|
||||
autoComplete='off'
|
||||
form={loadFilamentStockForm}
|
||||
onFinish={handleLoadFilamentStock}
|
||||
onValuesChange={(changedValues) =>
|
||||
setLoadFilamentStockFormValues((prevValues) => ({
|
||||
...prevValues,
|
||||
...changedValues
|
||||
}))
|
||||
}
|
||||
initialValues={initialLoadFilamentStockForm}
|
||||
>
|
||||
<WizardView
|
||||
title='Load Filament Stock'
|
||||
steps={steps}
|
||||
onSubmit={() => loadFilamentStockForm.submit()}
|
||||
formValid={formValid}
|
||||
loading={loadFilamentStockLoading}
|
||||
submitText='Done'
|
||||
/>
|
||||
</Form>
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
@ -1,34 +1,30 @@
|
||||
import { useState, useContext, useEffect } from 'react'
|
||||
import { Form, Button, Typography, Flex, Steps, Divider, Alert } from 'antd'
|
||||
import { useMediaQuery } from 'react-responsive'
|
||||
import { Form, Flex, Alert } from 'antd'
|
||||
import PropTypes from 'prop-types'
|
||||
import { PrintServerContext } from '../../context/PrintServerContext'
|
||||
|
||||
import PrinterSelect from '../../common/PrinterSelect'
|
||||
import ObjectSelect from '../../common/ObjectSelect'
|
||||
import PrinterTemperaturePanel from '../../common/PrinterTemperaturePanel'
|
||||
import WizardView from '../../common/WizardView'
|
||||
import { ApiServerContext } from '../../context/ApiServerContext'
|
||||
|
||||
import { LoadingOutlined } from '@ant-design/icons'
|
||||
|
||||
const { Title } = Typography
|
||||
|
||||
const UnloadFilamentStock = ({ onOk, reset, printer = null }) => {
|
||||
const { connected, subscribeToObjectEvent, sendObjectAction } =
|
||||
useContext(ApiServerContext)
|
||||
UnloadFilamentStock.propTypes = {
|
||||
onOk: PropTypes.func.isRequired,
|
||||
reset: PropTypes.bool.isRequired,
|
||||
printer: PropTypes.object
|
||||
}
|
||||
|
||||
const { printServer } = useContext(PrintServerContext)
|
||||
const isMobile = useMediaQuery({ maxWidth: 768 })
|
||||
|
||||
const initialUnloadFilamentStockForm = {
|
||||
printer: printer
|
||||
}
|
||||
|
||||
const [unloadFilamentStockLoading, setUnloadFilamentStockLoading] =
|
||||
useState(false)
|
||||
const [currentStep, setCurrentStep] = useState(0)
|
||||
const [nextEnabled, setNextEnabled] = useState(false)
|
||||
const [formValid, setFormValid] = useState(false)
|
||||
const [currentTemperature, setCurrentTemperature] = useState(-1)
|
||||
const [targetTemperature, setTargetTemperature] = useState(0)
|
||||
const [filamentSensorDetected, setFilamentSensorDetected] = useState(true)
|
||||
@ -36,46 +32,35 @@ const UnloadFilamentStock = ({ onOk, reset, printer = null }) => {
|
||||
const [unloadFilamentStockFormValues, setUnloadFilamentStockFormValues] =
|
||||
useState(initialUnloadFilamentStockForm)
|
||||
|
||||
// Add websocket temperature monitoring
|
||||
useEffect(() => {
|
||||
if (unloadFilamentStockFormValues.printer) {
|
||||
const params = {
|
||||
printerId: unloadFilamentStockFormValues.printer._id,
|
||||
objects: {
|
||||
extruder: null,
|
||||
'filament_switch_sensor fsensor': null
|
||||
if (printer?._id && connected) {
|
||||
const temperatureEventUnsubscribe = subscribeToObjectEvent(
|
||||
printer._id,
|
||||
'printer',
|
||||
'temperature',
|
||||
(event) => {
|
||||
if (event.data?.extruder?.current) {
|
||||
setCurrentTemperature(event.data.extruder.current)
|
||||
}
|
||||
if (event.data?.extruder?.target) {
|
||||
setTargetTemperature(event.data.extruder.target)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const notifyStatusUpdate = (statusUpdate) => {
|
||||
if (statusUpdate?.extruder?.temperature !== undefined) {
|
||||
setCurrentTemperature(statusUpdate.extruder.temperature)
|
||||
)
|
||||
const filamentStockEventUnsubscribe = subscribeToObjectEvent(
|
||||
printer._id,
|
||||
'printer',
|
||||
'filamentSensor',
|
||||
(event) => {
|
||||
setFilamentSensorDetected(event.data.detected)
|
||||
}
|
||||
if (statusUpdate?.extruder?.target !== undefined) {
|
||||
setTargetTemperature(statusUpdate.extruder.target)
|
||||
}
|
||||
if (
|
||||
statusUpdate?.['filament_switch_sensor fsensor']
|
||||
?.filament_detected !== undefined
|
||||
) {
|
||||
setFilamentSensorDetected(
|
||||
Boolean(
|
||||
statusUpdate['filament_switch_sensor fsensor'].filament_detected
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
printServer.emit('printer.objects.subscribe', params)
|
||||
printServer.emit('printer.objects.query', params)
|
||||
printServer.on('notify_status_update', notifyStatusUpdate)
|
||||
|
||||
)
|
||||
return () => {
|
||||
printServer.off('notify_status_update', notifyStatusUpdate)
|
||||
printServer.emit('printer.objects.unsubscribe', params)
|
||||
if (temperatureEventUnsubscribe) temperatureEventUnsubscribe()
|
||||
if (filamentStockEventUnsubscribe) filamentStockEventUnsubscribe()
|
||||
}
|
||||
}
|
||||
}, [printServer, unloadFilamentStockFormValues.printer])
|
||||
}, [printer?._id, connected])
|
||||
|
||||
useEffect(() => {
|
||||
if (reset) {
|
||||
@ -90,14 +75,14 @@ const UnloadFilamentStock = ({ onOk, reset, printer = null }) => {
|
||||
})
|
||||
.then(() => {
|
||||
// Only enable next if we have a printer selected, we're not loading, and we've reached target temperature
|
||||
setNextEnabled(
|
||||
setFormValid(
|
||||
Boolean(unloadFilamentStockFormValues.printer) &&
|
||||
!unloadFilamentStockLoading &&
|
||||
currentTemperature + 1 > targetTemperature &&
|
||||
targetTemperature != 0
|
||||
)
|
||||
})
|
||||
.catch(() => setNextEnabled(false))
|
||||
.catch(() => setFormValid(false))
|
||||
}, [
|
||||
unloadFilamentStockForm,
|
||||
unloadFilamentStockFormValues,
|
||||
@ -109,10 +94,13 @@ const UnloadFilamentStock = ({ onOk, reset, printer = null }) => {
|
||||
const handleUnloadFilamentStock = async () => {
|
||||
setUnloadFilamentStockLoading(true)
|
||||
// Send G-code to retract the filament
|
||||
await printServer.emit('printer.gcode.script', {
|
||||
printerId: unloadFilamentStockFormValues.printer._id,
|
||||
script: `_CLIENT_LINEAR_MOVE E=-200 F=1000`
|
||||
})
|
||||
await sendObjectAction(
|
||||
unloadFilamentStockFormValues.printer._id,
|
||||
'printer',
|
||||
{
|
||||
type: 'unloadFilamentStock'
|
||||
}
|
||||
)
|
||||
//setUnloadFilamentStockLoading(false)
|
||||
}
|
||||
|
||||
@ -140,7 +128,7 @@ const UnloadFilamentStock = ({ onOk, reset, printer = null }) => {
|
||||
}
|
||||
]}
|
||||
>
|
||||
<PrinterSelect checkable={false} />
|
||||
<ObjectSelect type='printer' checkable={false} />
|
||||
</Form.Item>
|
||||
|
||||
{unloadFilamentStockLoading == false ? (
|
||||
@ -182,11 +170,11 @@ const UnloadFilamentStock = ({ onOk, reset, printer = null }) => {
|
||||
/>
|
||||
)}
|
||||
|
||||
{unloadFilamentStockFormValues.printer ? (
|
||||
{unloadFilamentStockFormValues.printer?._id ? (
|
||||
<PrinterTemperaturePanel
|
||||
showHeatedBed={false}
|
||||
showBed={false}
|
||||
showMoreInfo={false}
|
||||
printerId={unloadFilamentStockFormValues.printer._id}
|
||||
id={unloadFilamentStockFormValues.printer._id}
|
||||
/>
|
||||
) : null}
|
||||
</Flex>
|
||||
@ -195,65 +183,28 @@ const UnloadFilamentStock = ({ onOk, reset, printer = null }) => {
|
||||
]
|
||||
|
||||
return (
|
||||
<Flex gap={'middle'}>
|
||||
{!isMobile && (
|
||||
<div style={{ minWidth: '160px' }}>
|
||||
<Steps
|
||||
current={currentStep}
|
||||
items={steps}
|
||||
direction='vertical'
|
||||
style={{ width: 'fit-content' }}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{!isMobile && <Divider type={'vertical'} style={{ height: 'unset' }} />}
|
||||
|
||||
<Flex vertical={'true'} style={{ flexGrow: 1 }} gap='middle'>
|
||||
<Title level={2} style={{ marginTop: 0, marginBottom: 4 }}>
|
||||
Unload Filament Stock
|
||||
</Title>
|
||||
<Form
|
||||
name='unloadFilamentStock'
|
||||
autoComplete='off'
|
||||
form={unloadFilamentStockForm}
|
||||
onFinish={handleUnloadFilamentStock}
|
||||
onValuesChange={(changedValues) =>
|
||||
setUnloadFilamentStockFormValues((prevValues) => ({
|
||||
...prevValues,
|
||||
...changedValues
|
||||
}))
|
||||
}
|
||||
initialValues={initialUnloadFilamentStockForm}
|
||||
>
|
||||
<div style={{ minHeight: '260px' }}>{steps[currentStep].content}</div>
|
||||
|
||||
<Flex justify={'end'}>
|
||||
<Button
|
||||
style={{
|
||||
margin: '0 8px'
|
||||
}}
|
||||
onClick={() => setCurrentStep(currentStep - 1)}
|
||||
disabled={!(currentStep > 0)}
|
||||
>
|
||||
Previous
|
||||
</Button>
|
||||
{currentStep === steps.length - 1 && (
|
||||
<Button
|
||||
type='primary'
|
||||
loading={unloadFilamentStockLoading}
|
||||
disabled={!nextEnabled}
|
||||
onClick={() => {
|
||||
unloadFilamentStockForm.submit()
|
||||
}}
|
||||
>
|
||||
{unloadFilamentStockLoading ? 'Unloading...' : 'Unload'}
|
||||
</Button>
|
||||
)}
|
||||
</Flex>
|
||||
</Form>
|
||||
</Flex>
|
||||
</Flex>
|
||||
<Form
|
||||
name='unloadFilamentStock'
|
||||
autoComplete='off'
|
||||
form={unloadFilamentStockForm}
|
||||
onFinish={handleUnloadFilamentStock}
|
||||
onValuesChange={(changedValues) =>
|
||||
setUnloadFilamentStockFormValues((prevValues) => ({
|
||||
...prevValues,
|
||||
...changedValues
|
||||
}))
|
||||
}
|
||||
initialValues={initialUnloadFilamentStockForm}
|
||||
>
|
||||
<WizardView
|
||||
title='Unload Filament Stock'
|
||||
steps={steps}
|
||||
onSubmit={() => unloadFilamentStockForm.submit()}
|
||||
formValid={formValid}
|
||||
loading={unloadFilamentStockLoading}
|
||||
submitText={unloadFilamentStockLoading ? 'Unloading...' : 'Unload'}
|
||||
/>
|
||||
</Form>
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
@ -3,12 +3,26 @@ import ObjectInfo from '../../common/ObjectInfo'
|
||||
import NewObjectForm from '../../common/NewObjectForm'
|
||||
import WizardView from '../../common/WizardView'
|
||||
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 { 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 (
|
||||
<NewObjectForm
|
||||
type={'documentJob'}
|
||||
defaultValues={{ objectType: 'documentJob', ...defaultValues }}
|
||||
defaultValues={defaultValuesRef.current}
|
||||
>
|
||||
{({ handleSubmit, submitLoading, objectData, formValid }) => {
|
||||
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 (
|
||||
<WizardView
|
||||
steps={steps}
|
||||
@ -35,8 +53,10 @@ const NewDocumentJob = ({ onOk, defaultValues = {} }) => {
|
||||
title={'Print Document'}
|
||||
formValid={formValid}
|
||||
loading={submitLoading}
|
||||
disabled={downloading}
|
||||
sideBarGrow={true}
|
||||
sideBar={
|
||||
<div style={{ minWidth: '400px', minHeight: '500px' }}>
|
||||
<div style={{ minHeight: '500px', flexGrow: 1 }}>
|
||||
<TemplatePreview
|
||||
objectData={objectData?.object}
|
||||
documentTemplate={objectData?.documentTemplate}
|
||||
@ -46,10 +66,52 @@ const NewDocumentJob = ({ onOk, defaultValues = {} }) => {
|
||||
/>
|
||||
</div>
|
||||
}
|
||||
onSubmit={() => {
|
||||
handleSubmit()
|
||||
onOk()
|
||||
onSubmit={async () => {
|
||||
const newDocumentJob = await handleSubmit()
|
||||
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'
|
||||
}
|
||||
]
|
||||
}
|
||||
]}
|
||||
/>
|
||||
)
|
||||
}}
|
||||
|
||||
@ -1,5 +1,6 @@
|
||||
import { useRef } from 'react'
|
||||
import { Button, Flex, Space, Dropdown } from 'antd'
|
||||
import { useRef, useState } from 'react'
|
||||
import { Button, Flex, Space, Dropdown, message, Modal } from 'antd'
|
||||
import PlusIcon from '../../Icons/PlusIcon'
|
||||
import ObjectTable from '../common/ObjectTable'
|
||||
import ReloadIcon from '../../Icons/ReloadIcon'
|
||||
import useColumnVisibility from '../hooks/useColumnVisibility'
|
||||
@ -7,10 +8,12 @@ import GridIcon from '../../Icons/GridIcon'
|
||||
import ListIcon from '../../Icons/ListIcon'
|
||||
import useViewMode from '../hooks/useViewMode'
|
||||
import ColumnViewButton from '../common/ColumnViewButton'
|
||||
import NewDocumentPrinter from './DocumentPrinters/NewDocumentPrinter'
|
||||
|
||||
const DocumentPrinters = () => {
|
||||
const [messageApi, contextHolder] = message.useMessage()
|
||||
const tableRef = useRef()
|
||||
|
||||
const [newDocumentPrinterOpen, setNewDocumentPrinterOpen] = useState(false)
|
||||
const [viewMode, setViewMode] = useViewMode('documentPrinter')
|
||||
|
||||
const [columnVisibility, setColumnVisibility] =
|
||||
@ -18,6 +21,12 @@ const DocumentPrinters = () => {
|
||||
|
||||
const actionItems = {
|
||||
items: [
|
||||
{
|
||||
label: 'New Document Printer',
|
||||
key: 'newDocumentPrinter',
|
||||
icon: <PlusIcon />
|
||||
},
|
||||
{ type: 'divider' },
|
||||
{
|
||||
label: 'Reload List',
|
||||
key: 'reloadList',
|
||||
@ -27,6 +36,8 @@ const DocumentPrinters = () => {
|
||||
onClick: ({ key }) => {
|
||||
if (key === 'reloadList') {
|
||||
tableRef.current?.reload()
|
||||
} else if (key === 'newDocumentPrinter') {
|
||||
setNewDocumentPrinterOpen(true)
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -34,6 +45,7 @@ const DocumentPrinters = () => {
|
||||
return (
|
||||
<>
|
||||
<Flex vertical={'true'} gap='large'>
|
||||
{contextHolder}
|
||||
<Flex justify={'space-between'}>
|
||||
<Space size='small'>
|
||||
<Dropdown menu={actionItems}>
|
||||
@ -62,6 +74,22 @@ const DocumentPrinters = () => {
|
||||
cards={viewMode === 'cards'}
|
||||
/>
|
||||
</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>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
@ -1,3 +1,4 @@
|
||||
import { useRef, useState } from 'react'
|
||||
import { useLocation } from 'react-router-dom'
|
||||
import { Space, Flex, Card } from 'antd'
|
||||
import { LoadingOutlined } from '@ant-design/icons'
|
||||
@ -25,6 +26,8 @@ log.setLevel(config.logLevel)
|
||||
|
||||
const DocumentPrinterInfo = () => {
|
||||
const location = useLocation()
|
||||
const objectFormRef = useRef(null)
|
||||
const actionHandlerRef = useRef(null)
|
||||
const documentPrinterId = new URLSearchParams(location.search).get(
|
||||
'documentPrinterId'
|
||||
)
|
||||
@ -32,122 +35,125 @@ const DocumentPrinterInfo = () => {
|
||||
'DocumentPrinterInfo',
|
||||
{
|
||||
info: true,
|
||||
stocks: true,
|
||||
notes: 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 (
|
||||
<ObjectForm
|
||||
id={documentPrinterId}
|
||||
type='documentPrinter'
|
||||
style={{ height: '100%' }}
|
||||
>
|
||||
{({
|
||||
loading,
|
||||
isEditing,
|
||||
startEditing,
|
||||
cancelEditing,
|
||||
handleUpdate,
|
||||
formValid,
|
||||
objectData,
|
||||
editLoading,
|
||||
lock,
|
||||
fetchObject
|
||||
}) => {
|
||||
// Define actions for ActionHandler
|
||||
const actions = {
|
||||
reload: () => {
|
||||
fetchObject()
|
||||
return true
|
||||
},
|
||||
edit: () => {
|
||||
startEditing()
|
||||
return false
|
||||
},
|
||||
cancelEdit: () => {
|
||||
cancelEditing()
|
||||
return true
|
||||
},
|
||||
finishEdit: () => {
|
||||
handleUpdate()
|
||||
return true
|
||||
}
|
||||
}
|
||||
<>
|
||||
<Flex
|
||||
gap='large'
|
||||
vertical='true'
|
||||
style={{
|
||||
maxHeight: '100%',
|
||||
minHeight: 0
|
||||
}}
|
||||
>
|
||||
<Flex justify={'space-between'}>
|
||||
<Space size='middle'>
|
||||
<Space size='small'>
|
||||
<ObjectActions
|
||||
type='documentPrinter'
|
||||
id={documentPrinterId}
|
||||
disabled={objectFormState.loading}
|
||||
objectData={objectFormState.objectData}
|
||||
/>
|
||||
<ViewButton
|
||||
disabled={objectFormState.loading}
|
||||
items={[
|
||||
{
|
||||
key: 'info',
|
||||
label: 'DocumentPrinter Information'
|
||||
},
|
||||
{ key: 'notes', label: 'Notes' },
|
||||
{ key: 'auditLogs', label: 'Audit Logs' }
|
||||
]}
|
||||
visibleState={collapseState}
|
||||
updateVisibleState={updateCollapseState}
|
||||
/>
|
||||
<DocumentPrintButton
|
||||
type='documentPrinter'
|
||||
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 (
|
||||
<ActionHandler actions={actions} loading={loading}>
|
||||
{({ callAction }) => (
|
||||
<Flex
|
||||
gap='large'
|
||||
vertical='true'
|
||||
style={{
|
||||
maxHeight: '100%',
|
||||
minHeight: 0
|
||||
}}
|
||||
<div style={{ height: '100%', overflowY: 'scroll' }}>
|
||||
<Flex vertical gap={'large'}>
|
||||
<ActionHandler
|
||||
actions={actions}
|
||||
loading={objectFormState.loading}
|
||||
ref={actionHandlerRef}
|
||||
>
|
||||
<InfoCollapse
|
||||
title='Document Printer Information'
|
||||
icon={<InfoCircleIcon />}
|
||||
active={collapseState.info}
|
||||
onToggle={(expanded) => updateCollapseState('info', expanded)}
|
||||
collapseKey='info'
|
||||
>
|
||||
<Flex justify={'space-between'}>
|
||||
<Space size='middle'>
|
||||
<Space size='small'>
|
||||
<ObjectActions
|
||||
type='documentPrinter'
|
||||
id={documentPrinterId}
|
||||
disabled={loading}
|
||||
objectData={objectData}
|
||||
/>
|
||||
<ViewButton
|
||||
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'
|
||||
>
|
||||
<ObjectForm
|
||||
id={documentPrinterId}
|
||||
type='documentPrinter'
|
||||
style={{ height: '100%' }}
|
||||
ref={objectFormRef}
|
||||
onStateChange={(state) => {
|
||||
setEditFormState((prev) => ({ ...prev, ...state }))
|
||||
}}
|
||||
>
|
||||
{({ loading, isEditing, objectData }) => {
|
||||
return (
|
||||
<ObjectInfo
|
||||
loading={loading}
|
||||
indicator={<LoadingOutlined />}
|
||||
@ -155,52 +161,47 @@ const DocumentPrinterInfo = () => {
|
||||
type='documentPrinter'
|
||||
objectData={objectData}
|
||||
/>
|
||||
</InfoCollapse>
|
||||
)
|
||||
}}
|
||||
</ObjectForm>
|
||||
</InfoCollapse>
|
||||
</ActionHandler>
|
||||
|
||||
<InfoCollapse
|
||||
title='Notes'
|
||||
icon={<NoteIcon />}
|
||||
active={collapseState.notes}
|
||||
onToggle={(expanded) =>
|
||||
updateCollapseState('notes', expanded)
|
||||
}
|
||||
collapseKey='notes'
|
||||
>
|
||||
<Card>
|
||||
<NotesPanel
|
||||
_id={documentPrinterId}
|
||||
type='documentPrinter'
|
||||
/>
|
||||
</Card>
|
||||
</InfoCollapse>
|
||||
<InfoCollapse
|
||||
title='Notes'
|
||||
icon={<NoteIcon />}
|
||||
active={collapseState.notes}
|
||||
onToggle={(expanded) => updateCollapseState('notes', expanded)}
|
||||
collapseKey='notes'
|
||||
>
|
||||
<Card>
|
||||
<NotesPanel _id={documentPrinterId} type='documentPrinter' />
|
||||
</Card>
|
||||
</InfoCollapse>
|
||||
|
||||
<InfoCollapse
|
||||
title='Audit Logs'
|
||||
icon={<AuditLogIcon />}
|
||||
active={collapseState.auditLogs}
|
||||
onToggle={(expanded) =>
|
||||
updateCollapseState('auditLogs', expanded)
|
||||
}
|
||||
collapseKey='auditLogs'
|
||||
>
|
||||
{loading ? (
|
||||
<InfoCollapsePlaceholder />
|
||||
) : (
|
||||
<ObjectTable
|
||||
type='auditLog'
|
||||
masterFilter={{ 'parent._id': documentPrinterId }}
|
||||
visibleColumns={{ _id: false, 'parent._id': false }}
|
||||
/>
|
||||
)}
|
||||
</InfoCollapse>
|
||||
</Flex>
|
||||
</div>
|
||||
</Flex>
|
||||
)}
|
||||
</ActionHandler>
|
||||
)
|
||||
}}
|
||||
</ObjectForm>
|
||||
<InfoCollapse
|
||||
title='Audit Logs'
|
||||
icon={<AuditLogIcon />}
|
||||
active={collapseState.auditLogs}
|
||||
onToggle={(expanded) =>
|
||||
updateCollapseState('auditLogs', expanded)
|
||||
}
|
||||
collapseKey='auditLogs'
|
||||
>
|
||||
{objectFormState.loading ? (
|
||||
<InfoCollapsePlaceholder />
|
||||
) : (
|
||||
<ObjectTable
|
||||
type='auditLog'
|
||||
masterFilter={{ 'parent._id': documentPrinterId }}
|
||||
visibleColumns={{ _id: false, 'parent._id': false }}
|
||||
/>
|
||||
)}
|
||||
</InfoCollapse>
|
||||
</Flex>
|
||||
</div>
|
||||
</Flex>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
@ -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
|
||||
@ -1,19 +1,9 @@
|
||||
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 NewObjectForm from '../../common/NewObjectForm'
|
||||
import NewObjectButtons from '../../common/NewObjectButtons'
|
||||
|
||||
const { Title } = Typography
|
||||
import WizardView from '../../common/WizardView'
|
||||
|
||||
const NewDocumentTemplate = ({ onOk }) => {
|
||||
const [currentStep, setCurrentStep] = useState(0)
|
||||
|
||||
const isMobile = useMediaQuery({ maxWidth: 768 })
|
||||
|
||||
return (
|
||||
<NewObjectForm
|
||||
type={'documentTemplate'}
|
||||
@ -69,44 +59,18 @@ const NewDocumentTemplate = ({ onOk }) => {
|
||||
)
|
||||
}
|
||||
]
|
||||
|
||||
return (
|
||||
<Flex gap='middle'>
|
||||
{!isMobile && (
|
||||
<div style={{ minWidth: '160px' }}>
|
||||
<Steps
|
||||
current={currentStep}
|
||||
items={steps}
|
||||
direction='vertical'
|
||||
style={{ width: 'fit-content' }}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{!isMobile && (
|
||||
<Divider type='vertical' style={{ height: 'unset' }} />
|
||||
)}
|
||||
|
||||
<Flex vertical 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>
|
||||
<WizardView
|
||||
steps={steps}
|
||||
loading={submitLoading}
|
||||
formValid={formValid}
|
||||
title='New Document Template'
|
||||
onSubmit={() => {
|
||||
handleSubmit()
|
||||
onOk()
|
||||
}}
|
||||
/>
|
||||
)
|
||||
}}
|
||||
</NewObjectForm>
|
||||
|
||||
@ -1,6 +1,6 @@
|
||||
import { useState, useRef, useEffect, useContext } from 'react'
|
||||
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 config from '../../../../config.js'
|
||||
import useCollapseState from '../../hooks/useCollapseState.js'
|
||||
@ -28,6 +28,9 @@ import { useMediaQuery } from 'react-responsive'
|
||||
import AlertsDisplay from '../../common/AlertsDisplay.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')
|
||||
log.setLevel(config.logLevel)
|
||||
|
||||
@ -57,6 +60,9 @@ const ControlPrinter = () => {
|
||||
collapseState.movement
|
||||
)
|
||||
|
||||
const [loadFilamentStockOpen, setLoadFilamentStockOpen] = useState(false)
|
||||
const [unloadFilamentStockOpen, setUnloadFilamentStockOpen] = useState(false)
|
||||
|
||||
useEffect(() => {
|
||||
setSideBarVisible(
|
||||
collapseState.temperature ||
|
||||
@ -122,6 +128,39 @@ const ControlPrinter = () => {
|
||||
})
|
||||
}
|
||||
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 (
|
||||
<Flex
|
||||
gap='large'
|
||||
vertical='true'
|
||||
style={{
|
||||
maxHeight: '100%',
|
||||
minHeight: 0
|
||||
}}
|
||||
>
|
||||
<Flex justify={'space-between'}>
|
||||
<Space size='middle'>
|
||||
<Space size='small'>
|
||||
<ObjectActions
|
||||
type='printer'
|
||||
id={printerId}
|
||||
disabled={objectFormState.loading}
|
||||
visibleActions={{ edit: false }}
|
||||
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} />
|
||||
<>
|
||||
<Flex
|
||||
gap='large'
|
||||
vertical='true'
|
||||
style={{
|
||||
maxHeight: '100%',
|
||||
minHeight: 0
|
||||
}}
|
||||
>
|
||||
<Flex justify={'space-between'}>
|
||||
<Space size='middle'>
|
||||
<Space size='small'>
|
||||
<ObjectActions
|
||||
type='printer'
|
||||
id={printerId}
|
||||
disabled={objectFormState.loading}
|
||||
visibleActions={{ edit: false }}
|
||||
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}
|
||||
/>
|
||||
</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>
|
||||
</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>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
@ -108,7 +108,14 @@ const DocumentPrintButton = ({
|
||||
onCancel={() => setNewDocumentJobOpen(false)}
|
||||
footer={null}
|
||||
destroyOnHidden={true}
|
||||
width={900}
|
||||
width={{
|
||||
xs: '100%',
|
||||
sm: '100%',
|
||||
md: '100%',
|
||||
lg: '90%',
|
||||
xl: '80%',
|
||||
xxl: '80%'
|
||||
}}
|
||||
>
|
||||
<NewDocumentJob
|
||||
onOk={() => {
|
||||
|
||||
@ -9,7 +9,8 @@ const NewObjectButtons = ({
|
||||
onSubmit,
|
||||
formValid,
|
||||
submitLoading,
|
||||
submitText = 'Done'
|
||||
submitText = 'Done',
|
||||
disabled = false
|
||||
}) => {
|
||||
return (
|
||||
<Flex justify='end'>
|
||||
@ -24,14 +25,18 @@ const NewObjectButtons = ({
|
||||
) : null}
|
||||
|
||||
{currentStep < totalSteps - 1 ? (
|
||||
<Button type='primary' disabled={!formValid} onClick={onNext}>
|
||||
<Button
|
||||
type='primary'
|
||||
disabled={!formValid || disabled}
|
||||
onClick={onNext}
|
||||
>
|
||||
Next
|
||||
</Button>
|
||||
) : (
|
||||
<Button
|
||||
type='primary'
|
||||
loading={submitLoading}
|
||||
disabled={!formValid}
|
||||
disabled={!formValid || disabled}
|
||||
onClick={onSubmit}
|
||||
>
|
||||
{submitText}
|
||||
@ -49,7 +54,8 @@ NewObjectButtons.propTypes = {
|
||||
onSubmit: PropTypes.func.isRequired,
|
||||
formValid: PropTypes.bool.isRequired,
|
||||
submitLoading: PropTypes.bool,
|
||||
submitText: PropTypes.string
|
||||
submitText: PropTypes.string,
|
||||
disabled: PropTypes.bool
|
||||
}
|
||||
|
||||
export default NewObjectButtons
|
||||
|
||||
@ -138,24 +138,29 @@ const ObjectForm = forwardRef(
|
||||
return computedValues
|
||||
}, [])
|
||||
|
||||
// Validate form on change
|
||||
// Validate form on change (debounced to avoid heavy work on every keystroke)
|
||||
useEffect(() => {
|
||||
form
|
||||
.validateFields({ validateOnly: true })
|
||||
.then(() => {
|
||||
setFormValid(true)
|
||||
onStateChange({
|
||||
formValid: true,
|
||||
objectData: { ...serverObjectData, ...form.getFieldsValue() }
|
||||
const timeoutId = setTimeout(() => {
|
||||
form
|
||||
.validateFields({ validateOnly: true })
|
||||
.then(() => {
|
||||
setFormValid(true)
|
||||
onStateChange({
|
||||
formValid: true,
|
||||
objectData: { ...serverObjectData, ...form.getFieldsValue() }
|
||||
})
|
||||
})
|
||||
})
|
||||
.catch(() => {
|
||||
onStateChange({
|
||||
formValid: true,
|
||||
objectData: { ...serverObjectData, ...form.getFieldsValue() }
|
||||
.catch(() => {
|
||||
setFormValid(false)
|
||||
onStateChange({
|
||||
formValid: false,
|
||||
objectData: { ...serverObjectData, ...form.getFieldsValue() }
|
||||
})
|
||||
})
|
||||
})
|
||||
}, [form, formUpdateValues])
|
||||
}, 150)
|
||||
|
||||
return () => clearTimeout(timeoutId)
|
||||
}, [form, formUpdateValues, onStateChange, serverObjectData])
|
||||
|
||||
// Cleanup on unmount
|
||||
useEffect(() => {
|
||||
@ -252,9 +257,14 @@ const ObjectForm = forwardRef(
|
||||
updateLockEventHandler
|
||||
])
|
||||
|
||||
// Debounce objectData updates sent to parent to limit re-renders
|
||||
useEffect(() => {
|
||||
onStateChange({ objectData })
|
||||
}, [objectData])
|
||||
const timeoutId = setTimeout(() => {
|
||||
onStateChange({ objectData })
|
||||
}, 150)
|
||||
|
||||
return () => clearTimeout(timeoutId)
|
||||
}, [objectData, onStateChange])
|
||||
|
||||
const startEditing = () => {
|
||||
setIsEditing(true)
|
||||
@ -366,9 +376,18 @@ const ObjectForm = forwardRef(
|
||||
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) {
|
||||
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)
|
||||
|
||||
@ -81,6 +81,8 @@ const ObjectProperty = ({
|
||||
minimal = false,
|
||||
previewOpen = false,
|
||||
showPreview = true,
|
||||
options = [],
|
||||
roundNumber = false,
|
||||
showHyperlink,
|
||||
...rest
|
||||
}) => {
|
||||
@ -166,6 +168,18 @@ const ObjectProperty = ({
|
||||
</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':
|
||||
switch (value) {
|
||||
case 'margin':
|
||||
@ -234,10 +248,15 @@ const ObjectProperty = ({
|
||||
</Text>
|
||||
)
|
||||
} else {
|
||||
var roundedValue = value
|
||||
if (roundNumber != false) {
|
||||
roundedValue = value.toFixed(roundNumber)
|
||||
}
|
||||
|
||||
return (
|
||||
<Text {...textParams}>
|
||||
{prefix}
|
||||
{typeof value === 'number' ? value.toFixed(2) : value}
|
||||
{typeof value === 'number' ? roundedValue : value}
|
||||
{suffix}
|
||||
</Text>
|
||||
)
|
||||
@ -553,6 +572,17 @@ const ObjectProperty = ({
|
||||
/>
|
||||
</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':
|
||||
return (
|
||||
<Form.Item name={formItemName} {...mergedFormItemProps}>
|
||||
@ -773,7 +803,8 @@ ObjectProperty.propTypes = {
|
||||
height: PropTypes.string,
|
||||
previewOpen: PropTypes.bool,
|
||||
showPreview: PropTypes.bool,
|
||||
showHyperlink: PropTypes.bool
|
||||
showHyperlink: PropTypes.bool,
|
||||
options: PropTypes.array
|
||||
}
|
||||
|
||||
export default ObjectProperty
|
||||
|
||||
@ -144,6 +144,7 @@ const ObjectSelect = ({
|
||||
parentKeys: parentKeys.concat(key || '-'),
|
||||
filterPath: newFilterPath,
|
||||
selectable: false,
|
||||
|
||||
children: buildTreeData(
|
||||
value,
|
||||
pIdx + 1,
|
||||
@ -278,8 +279,11 @@ const ObjectSelect = ({
|
||||
handleFetchObjectsProperties()
|
||||
setInitialized(true)
|
||||
}
|
||||
if (value == null) {
|
||||
setTreeSelectValue(null)
|
||||
setInitialized(true)
|
||||
}
|
||||
}
|
||||
|
||||
handleValue()
|
||||
}, [
|
||||
value,
|
||||
@ -303,8 +307,13 @@ const ObjectSelect = ({
|
||||
|
||||
if (hasChanged) {
|
||||
setObjectPropertiesTree({})
|
||||
setObjectList([])
|
||||
setTreeData([])
|
||||
setInitialized(false)
|
||||
onTreeSelectChange(null)
|
||||
setTreeSelectValue(null)
|
||||
setInitialLoading(true)
|
||||
setError(false)
|
||||
prevValuesRef.current = { type, masterFilter }
|
||||
}
|
||||
}, [type, masterFilter])
|
||||
|
||||
@ -227,7 +227,7 @@ const ObjectTable = forwardRef(
|
||||
const loadNextPage = useCallback(() => {
|
||||
const highestPage = Math.max(...pages.map((p) => p.pageNum))
|
||||
const nextPage = highestPage + 1
|
||||
if (hasMore) {
|
||||
if (hasMore && lazyLoading == false) {
|
||||
setPages((prev) => {
|
||||
const filteredPages = prev.map((page) => ({
|
||||
...page,
|
||||
@ -244,13 +244,13 @@ const ObjectTable = forwardRef(
|
||||
})
|
||||
fetchData(nextPage)
|
||||
}
|
||||
}, [pages, createSkeletonData, fetchData, hasMore])
|
||||
}, [pages, createSkeletonData, fetchData, hasMore, lazyLoading])
|
||||
|
||||
const loadPreviousPage = useCallback(() => {
|
||||
const lowestPage = Math.min(...pages.map((p) => p.pageNum))
|
||||
const prevPage = lowestPage - 1
|
||||
|
||||
if (prevPage > 0) {
|
||||
if (prevPage > 0 && lazyLoading == false) {
|
||||
setPages((prev) => {
|
||||
const filteredPages = prev.map((page) => ({
|
||||
...page,
|
||||
@ -267,7 +267,7 @@ const ObjectTable = forwardRef(
|
||||
})
|
||||
fetchData(prevPage)
|
||||
}
|
||||
}, [pages, createSkeletonData, fetchData])
|
||||
}, [pages, createSkeletonData, fetchData, lazyLoading])
|
||||
|
||||
const handleScroll = useCallback(
|
||||
(e) => {
|
||||
@ -600,7 +600,7 @@ const ObjectTable = forwardRef(
|
||||
title: prop.label,
|
||||
dataIndex: prop.name,
|
||||
width: prop.columnWidth || width,
|
||||
fixed: fixed,
|
||||
fixed: isMobile ? undefined : fixed,
|
||||
key: prop.name,
|
||||
render: (text, record) => {
|
||||
if (record?.isSkeleton) {
|
||||
@ -651,7 +651,7 @@ const ObjectTable = forwardRef(
|
||||
),
|
||||
key: 'actions',
|
||||
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) => {
|
||||
return renderActions(record)
|
||||
}
|
||||
|
||||
@ -17,7 +17,8 @@ const ObjectTypeSelect = ({
|
||||
.sort((a, b) => a.label.localeCompare(b.label))
|
||||
.map((model) => ({
|
||||
value: model.name,
|
||||
label: <ObjectTypeDisplay objectType={model.name} />
|
||||
label: <ObjectTypeDisplay objectType={model.name} />,
|
||||
searchText: model.label?.toLowerCase() || ''
|
||||
}))
|
||||
|
||||
return (
|
||||
@ -31,9 +32,7 @@ const ObjectTypeSelect = ({
|
||||
allowClear={allowClear}
|
||||
disabled={disabled}
|
||||
filterOption={(input, option) =>
|
||||
option.label.props.children[1].props.children
|
||||
.toLowerCase()
|
||||
.indexOf(input.toLowerCase()) >= 0
|
||||
option.searchText?.includes(input.toLowerCase()) ?? false
|
||||
}
|
||||
options={options}
|
||||
/>
|
||||
|
||||
@ -4,6 +4,13 @@ import { Progress, Flex, Space } from 'antd'
|
||||
import StateTag from './StateTag'
|
||||
|
||||
const StateDisplay = ({ state, showProgress = true, showState = true }) => {
|
||||
const loadingProgressTypes = [
|
||||
'loading',
|
||||
'processing',
|
||||
'queued',
|
||||
'printing',
|
||||
'used'
|
||||
]
|
||||
const currentState = state || {
|
||||
type: 'unknown',
|
||||
progress: 0
|
||||
@ -16,10 +23,14 @@ const StateDisplay = ({ state, showProgress = true, showState = true }) => {
|
||||
<StateTag state={currentState.type} />
|
||||
</Space>
|
||||
)}
|
||||
{showProgress && currentState?.progress && currentState?.progress > 0 ? (
|
||||
{showProgress &&
|
||||
loadingProgressTypes.includes(currentState.type) &&
|
||||
currentState?.progress &&
|
||||
currentState?.progress > 0 ? (
|
||||
<Progress
|
||||
percent={Math.round(currentState.progress * 100)}
|
||||
status='active'
|
||||
status={currentState.type === 'used' ? '' : 'active'}
|
||||
strokeColor={currentState.type === 'used' ? 'orange' : ''}
|
||||
style={{ width: '150px', marginBottom: '2px' }}
|
||||
/>
|
||||
) : null}
|
||||
|
||||
@ -56,9 +56,9 @@ const StateTag = ({ state, showBadge = true, style = {} }) => {
|
||||
status = 'success'
|
||||
text = 'Ready'
|
||||
break
|
||||
case 'unconsumed':
|
||||
case 'new':
|
||||
status = 'success'
|
||||
text = 'Unconsumed'
|
||||
text = 'New'
|
||||
break
|
||||
case 'error':
|
||||
status = 'error'
|
||||
@ -80,6 +80,10 @@ const StateTag = ({ state, showBadge = true, style = {} }) => {
|
||||
status = 'warning'
|
||||
text = 'Queued'
|
||||
break
|
||||
case 'used':
|
||||
status = 'warning'
|
||||
text = 'Used'
|
||||
break
|
||||
default:
|
||||
status = 'default'
|
||||
text = state || 'Unknown'
|
||||
|
||||
@ -1,6 +1,6 @@
|
||||
import { useState, useContext, useEffect, useRef, useCallback } from 'react'
|
||||
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 MinusIcon from '../../Icons/MinusIcon.jsx'
|
||||
import InfoCircleIcon from '../../Icons/InfoCircleIcon.jsx'
|
||||
@ -14,22 +14,26 @@ const TemplatePreview = ({
|
||||
isEditing,
|
||||
onTestObjectOpen,
|
||||
onPreviewMessage,
|
||||
showTestObject = false
|
||||
showTestObject = false,
|
||||
showPreviewSwitch = true
|
||||
}) => {
|
||||
const iframeRef = useRef(null)
|
||||
const { fetchTemplatePreview } = useContext(ApiServerContext)
|
||||
const [previewContent, setPreviewContent] = useState('')
|
||||
const { fetchTemplatePreview, fetchTemplatePDF } =
|
||||
useContext(ApiServerContext)
|
||||
const [previewContentHTML, setPreviewContentHTML] = useState('')
|
||||
const [pdfBlob, setPDFBlob] = useState(null)
|
||||
const [reloadLoading, setReloadLoading] = useState(false)
|
||||
const [previewScale, setPreviewScale] = useState(1)
|
||||
const [previewType, setPreviewType] = useState('HTML')
|
||||
|
||||
const updatePreviewContent = (html) => {
|
||||
const updatePreviewContentHTML = (html) => {
|
||||
if (iframeRef.current) {
|
||||
// Save current scroll position
|
||||
const scrollY = iframeRef.current.contentWindow.scrollY
|
||||
const scrollX = iframeRef.current.contentWindow.scrollX
|
||||
|
||||
// Update srcDoc
|
||||
setPreviewContent(html)
|
||||
setPreviewContentHTML(html)
|
||||
|
||||
// Restore scroll position after iframe loads new content
|
||||
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(
|
||||
(content, testObject = {}, scale = 1) => {
|
||||
setReloadLoading(true)
|
||||
@ -54,7 +75,7 @@ const TemplatePreview = ({
|
||||
// Handle error through parent component
|
||||
onPreviewMessage(result.error, true)
|
||||
} else {
|
||||
updatePreviewContent(result.html)
|
||||
updatePreviewContentHTML(result.html)
|
||||
onPreviewMessage('No issues found.', false)
|
||||
}
|
||||
}
|
||||
@ -66,9 +87,13 @@ const TemplatePreview = ({
|
||||
// Move useEffect to component level and use state to track objectData changes
|
||||
useEffect(() => {
|
||||
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 (
|
||||
<Flex vertical gap={'middle'} style={{ height: '100%' }}>
|
||||
@ -98,34 +123,51 @@ const TemplatePreview = ({
|
||||
/>
|
||||
</>
|
||||
) : null}
|
||||
|
||||
<Button
|
||||
icon={<PlusIcon />}
|
||||
onClick={() => {
|
||||
setPreviewScale((prev) => prev + 0.05)
|
||||
}}
|
||||
disabled={loading || reloadLoading || previewType == 'PDF'}
|
||||
/>
|
||||
<Button
|
||||
icon={<MinusIcon />}
|
||||
onClick={() => {
|
||||
setPreviewScale((prev) => prev - 0.05)
|
||||
}}
|
||||
disabled={loading || reloadLoading || previewType == 'PDF'}
|
||||
/>
|
||||
<Button
|
||||
readOnly={true}
|
||||
style={{ width: '65px' }}
|
||||
loading={loading || reloadLoading}
|
||||
disabled={loading || reloadLoading}
|
||||
disabled={loading || reloadLoading || previewType == 'PDF'}
|
||||
onClick={() => {
|
||||
setPreviewScale(1)
|
||||
}}
|
||||
>
|
||||
{previewScale.toFixed(2)}x
|
||||
</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>
|
||||
|
||||
<iframe
|
||||
ref={iframeRef}
|
||||
srcDoc={previewContent}
|
||||
srcDoc={previewType == 'HTML' ? previewContentHTML : undefined}
|
||||
src={previewType == 'PDF' ? pdfBlob : undefined}
|
||||
frameBorder='0'
|
||||
style={{
|
||||
width: '100%',
|
||||
@ -146,7 +188,8 @@ TemplatePreview.propTypes = {
|
||||
style: PropTypes.object,
|
||||
showTestObject: PropTypes.bool,
|
||||
onTestObjectOpen: PropTypes.func.isRequired,
|
||||
onPreviewMessage: PropTypes.func.isRequired
|
||||
onPreviewMessage: PropTypes.func.isRequired,
|
||||
showPreviewSwitch: PropTypes.bool
|
||||
}
|
||||
|
||||
export default TemplatePreview
|
||||
|
||||
@ -1,7 +1,15 @@
|
||||
import PropTypes from 'prop-types'
|
||||
import { useState } from 'react'
|
||||
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'
|
||||
|
||||
const { Title } = Typography
|
||||
@ -15,7 +23,10 @@ const WizardView = ({
|
||||
loading,
|
||||
sideBar = null,
|
||||
submitText = 'Done',
|
||||
progress = 0
|
||||
progress = 0,
|
||||
actions = [],
|
||||
sideBarGrow = false,
|
||||
disabled = false
|
||||
}) => {
|
||||
const [currentStep, setCurrentStep] = useState(0)
|
||||
const isMobile = useMediaQuery({ maxWidth: 768 })
|
||||
@ -26,7 +37,7 @@ const WizardView = ({
|
||||
sideBar != null ? (
|
||||
sideBar
|
||||
) : (
|
||||
<div style={{ minWidth: '160px' }}>
|
||||
<div style={{ minWidth: sideBarGrow == true ? '100%' : '160px' }}>
|
||||
<Steps
|
||||
current={currentStep}
|
||||
items={steps}
|
||||
@ -45,7 +56,13 @@ const WizardView = ({
|
||||
vertical
|
||||
justify='space-between'
|
||||
gap={'middle'}
|
||||
style={{ width: '100%' }}
|
||||
style={
|
||||
sideBarGrow == false
|
||||
? { width: '100%' }
|
||||
: isMobile
|
||||
? { width: '100%' }
|
||||
: { width: '400px' }
|
||||
}
|
||||
>
|
||||
<Flex vertical gap='middle' style={{ flexGrow: 1, width: '100%' }}>
|
||||
<Title level={2} style={{ margin: 0 }}>
|
||||
@ -63,8 +80,37 @@ const WizardView = ({
|
||||
percent={progress}
|
||||
/>
|
||||
) : 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
|
||||
disabled={disabled}
|
||||
currentStep={currentStep}
|
||||
totalSteps={steps.length}
|
||||
onPrevious={() => setCurrentStep((prev) => prev - 1)}
|
||||
@ -87,9 +133,12 @@ WizardView.propTypes = {
|
||||
showSteps: PropTypes.bool,
|
||||
title: PropTypes.string,
|
||||
loading: PropTypes.bool,
|
||||
disabled: PropTypes.bool,
|
||||
sideBar: PropTypes.node,
|
||||
submitText: PropTypes.string,
|
||||
progress: PropTypes.number
|
||||
progress: PropTypes.number,
|
||||
actions: PropTypes.array,
|
||||
sideBarGrow: PropTypes.bool
|
||||
}
|
||||
|
||||
export default WizardView
|
||||
|
||||
@ -308,9 +308,14 @@ const ApiServerProvider = ({ children }) => {
|
||||
.get(callbacksRefKey)
|
||||
.filter((cb) => cb !== callback)
|
||||
if (callbacks.length === 0) {
|
||||
logger.debug(
|
||||
'No callbacks found for object:',
|
||||
callbacksRefKey,
|
||||
'unsubscribing from object update...'
|
||||
)
|
||||
subscribedCallbacksRef.current.delete(callbacksRefKey)
|
||||
socketRef.current.emit('unsubscribeObjectUpdate', {
|
||||
id: id,
|
||||
_id: id,
|
||||
objectType: objectType
|
||||
})
|
||||
} else {
|
||||
@ -529,7 +534,7 @@ const ApiServerProvider = ({ children }) => {
|
||||
`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)
|
||||
|
||||
// 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) => {
|
||||
logger.debug('Fetching host OTP...')
|
||||
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 (
|
||||
<ApiServerContext.Provider
|
||||
value={{
|
||||
@ -967,11 +1035,14 @@ const ApiServerProvider = ({ children }) => {
|
||||
showError,
|
||||
fetchFileContent,
|
||||
fetchTemplatePreview,
|
||||
fetchTemplatePDF,
|
||||
fetchNotes,
|
||||
downloadTemplatePDF,
|
||||
fetchHostOTP,
|
||||
sendObjectAction,
|
||||
uploadFile,
|
||||
flushFile
|
||||
flushFile,
|
||||
formatFileName
|
||||
}}
|
||||
>
|
||||
{contextHolder}
|
||||
|
||||
@ -6,9 +6,9 @@ const config = {
|
||||
logLevel: 'trace'
|
||||
},
|
||||
production: {
|
||||
backendUrl: 'http://192.168.68.53:8080',
|
||||
backendUrl: 'https://dev.tombutcher.work/api',
|
||||
printServerUrl: 'ws://192.168.68.53:8081',
|
||||
apiServerUrl: 'ws://192.168.68.53:9090',
|
||||
apiServerUrl: 'https://dev-wss.tombutcher.work',
|
||||
logLevel: 'error'
|
||||
}
|
||||
}
|
||||
|
||||
@ -2,11 +2,12 @@ import InfoCircleIcon from '../../components/Icons/InfoCircleIcon'
|
||||
import ReloadIcon from '../../components/Icons/ReloadIcon'
|
||||
import EditIcon from '../../components/Icons/EditIcon'
|
||||
import DocumentJobIcon from '../../components/Icons/DocumentJobIcon'
|
||||
import dayjs from 'dayjs'
|
||||
|
||||
export const DocumentJob = {
|
||||
name: 'documentJob',
|
||||
label: 'Document Job',
|
||||
prefix: 'DSZ',
|
||||
prefix: 'DJB',
|
||||
icon: DocumentJobIcon,
|
||||
actions: [
|
||||
{
|
||||
@ -60,7 +61,9 @@ export const DocumentJob = {
|
||||
columnWidth: 200,
|
||||
columnFixed: 'left',
|
||||
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',
|
||||
readOnly: true
|
||||
},
|
||||
{
|
||||
name: 'state',
|
||||
label: 'Status',
|
||||
type: 'state',
|
||||
objectType: 'printer',
|
||||
showName: false,
|
||||
readOnly: true
|
||||
},
|
||||
{
|
||||
name: 'objectType',
|
||||
label: 'Object Type',
|
||||
@ -86,6 +97,15 @@ export const DocumentJob = {
|
||||
return objectData?.objectType
|
||||
}
|
||||
},
|
||||
{
|
||||
name: 'object._id',
|
||||
label: 'Object ID',
|
||||
type: 'id',
|
||||
showHyperlink: true,
|
||||
objectType: (objectData) => {
|
||||
return objectData?.objectType
|
||||
}
|
||||
},
|
||||
{
|
||||
name: 'documentTemplate',
|
||||
label: 'Template',
|
||||
@ -101,6 +121,13 @@ export const DocumentJob = {
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
name: 'documentTemplate._id',
|
||||
label: 'Template ID',
|
||||
type: 'id',
|
||||
showHyperlink: true,
|
||||
objectType: 'documentTemplate'
|
||||
},
|
||||
{
|
||||
name: 'documentPrinter',
|
||||
label: 'Printer',
|
||||
@ -114,6 +141,13 @@ export const DocumentJob = {
|
||||
online: true
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
name: 'documentPrinter._id',
|
||||
label: 'Printer ID',
|
||||
type: 'id',
|
||||
showHyperlink: true,
|
||||
objectType: 'documentPrinter'
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
@ -74,52 +74,88 @@ export const DocumentPrinter = {
|
||||
readOnly: true
|
||||
},
|
||||
{
|
||||
name: 'documentSize',
|
||||
label: 'Document Size',
|
||||
name: 'state',
|
||||
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,
|
||||
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'
|
||||
},
|
||||
{
|
||||
name: 'documentSize._id',
|
||||
label: 'Document Size ID',
|
||||
name: 'currentDocumentSize._id',
|
||||
label: 'Current Document Size ID',
|
||||
type: 'id',
|
||||
objectType: 'documentSize',
|
||||
showCopy: true,
|
||||
showHyperlink: true
|
||||
},
|
||||
{
|
||||
name: 'active',
|
||||
label: 'Active',
|
||||
required: true,
|
||||
type: 'bool'
|
||||
},
|
||||
{
|
||||
name: 'tags',
|
||||
label: 'Tags',
|
||||
required: false,
|
||||
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'
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
@ -5,7 +5,7 @@ import EditIcon from '../../components/Icons/EditIcon'
|
||||
import PlayCircleIcon from '../../components/Icons/PlayCircleIcon'
|
||||
import PauseCircleIcon from '../../components/Icons/PauseCircleIcon'
|
||||
import StopCircleIcon from '../../components/Icons/StopCircleIcon'
|
||||
|
||||
import FilamentStockIcon from '../../components/Icons/FilamentStockIcon'
|
||||
export const Printer = {
|
||||
name: 'printer',
|
||||
label: 'Printer',
|
||||
@ -94,8 +94,8 @@ export const Printer = {
|
||||
},
|
||||
children: [
|
||||
{
|
||||
name: 'Start',
|
||||
label: 'Start',
|
||||
name: 'startQueue',
|
||||
label: 'Start Queue',
|
||||
icon: PlayCircleIcon,
|
||||
disabled: (objectData) => {
|
||||
console.log(objectData?.subJobs?.length)
|
||||
@ -109,28 +109,60 @@ export const Printer = {
|
||||
url: (_id) =>
|
||||
`/dashboard/production/printers/control?printerId=${_id}&action=startQueue`
|
||||
},
|
||||
{ type: 'divider' },
|
||||
{
|
||||
name: 'pause',
|
||||
label: 'Pause',
|
||||
name: 'pauseJob',
|
||||
label: 'Pause Job',
|
||||
icon: PauseCircleIcon,
|
||||
disabled: (objectData) => {
|
||||
return objectData?.state?.type != 'printing'
|
||||
},
|
||||
url: (_id) =>
|
||||
`/dashboard/production/printers/control?printerId=${_id}&action=pauseQueue`
|
||||
`/dashboard/production/printers/control?printerId=${_id}&action=pauseJob`
|
||||
},
|
||||
{
|
||||
name: 'Stop',
|
||||
label: 'Stop',
|
||||
name: 'resumeJob',
|
||||
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,
|
||||
disabled: (objectData) => {
|
||||
return (
|
||||
objectData?.state?.type != 'printing' ||
|
||||
objectData?.state?.type != 'printing' &&
|
||||
objectData?.state?.type != 'error'
|
||||
)
|
||||
},
|
||||
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',
|
||||
type: 'alerts',
|
||||
required: false
|
||||
},
|
||||
{
|
||||
name: 'subJobs',
|
||||
label: 'Queue',
|
||||
type: 'objectList',
|
||||
objectType: 'subJob',
|
||||
required: false,
|
||||
readOnly: true
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user