Compare commits

...

26 Commits

Author SHA1 Message Date
c6c99bb02c Update package-lock.json to add @tanstack/react-query dependency and adjust peer dependencies for various packages. Modify backend and API server URLs in config.js for production environment. 2025-11-24 03:34:37 +00:00
91e7121fd5 Add filament stock management to ControlPrinter component with modal integration for loading and unloading filament. Implement new actions for pausing, resuming, and canceling jobs, enhancing printer control functionality. 2025-11-24 03:34:28 +00:00
afe25c1e09 Enhance DocumentJob, DocumentPrinter, and Printer models by adding new fields for state, object IDs, and document size management. Update action labels for clarity and improve data handling with date formatting. Introduce new connection options for printers and enhance the user interface for job management. 2025-11-24 03:34:20 +00:00
8d63b9a84d Add NewDocumentPrinter component to manage document printer creation with a multi-step wizard interface. Integrate ObjectInfo for required and optional fields, enhancing user experience during setup. 2025-11-24 03:34:04 +00:00
808d45273d Refactor DocumentPrinterInfo component to improve state management and enhance editing functionality. Introduce useRef for action handling and object form references, and update the rendering logic for better performance and user experience. 2025-11-24 03:33:57 +00:00
64c4d25982 Add New Document Printer functionality to DocumentPrinters component, including modal integration and success message upon creation. Enhance dropdown menu with new action item for creating document printers. 2025-11-24 03:33:44 +00:00
7ac56cc69c Refactor NewDocumentJob and NewDocumentTemplate components to utilize WizardView for improved user experience, implement file download functionality with dynamic filename generation, and enhance form handling with context integration. 2025-11-24 03:33:32 +00:00
e4b8f52e6b Add downloadTemplatePDF function to ApiServerContext for PDF downloading and implement formatFileName utility for safe filename generation 2025-11-24 03:33:11 +00:00
e114a45348 Enhance WizardView component by adding support for custom action buttons, sideBarGrow functionality, and a disabled state. Refactor layout styles for better responsiveness and usability. 2025-11-24 03:32:59 +00:00
b7bb6121b7 Update StateTag component to rename 'unconsumed' state to 'new' and add 'used' state with corresponding status and text. 2025-11-24 03:32:40 +00:00
dabad4f489 Enhance StateDisplay component to include 'used' as a loading progress type and adjust progress status and color accordingly. 2025-11-24 03:32:16 +00:00
94c55c8e77 Enable treeDefaultExpandAll in ObjectSelect component for improved tree view visibility 2025-11-24 03:32:06 +00:00
8f67d7daae Add roundNumber prop to ObjectProperty component for configurable number rounding and enhance select case handling for better value display 2025-11-24 03:31:58 +00:00
e788eaba91 Refactor ObjectForm component to implement debounced validation and state updates, improving performance and reducing unnecessary re-renders. 2025-11-24 03:31:51 +00:00
6af832afe5 Add disabled prop to NewObjectButtons for enhanced control over button states 2025-11-24 03:31:39 +00:00
69c387ca1d Update DocumentPrintButton width settings for responsive design across various screen sizes. 2025-11-24 03:31:31 +00:00
ecf73c13c0 Refactor Load and Unload Filament Stock components to utilize WizardView for improved user experience, integrate API server context for real-time temperature and filament sensor updates, and streamline form validation logic. 2025-11-23 13:26:46 +00:00
657d6a5c6e Added PDF preview to ApiServerContext 2025-11-23 13:26:31 +00:00
8f34c262a0 Added PDF preview option. 2025-11-23 13:25:43 +00:00
5cb586246f Enhance StateDisplay component to conditionally show progress for specific loading types 2025-11-23 13:22:51 +00:00
b12d230a8e Fixed object type search bug 2025-11-23 13:22:21 +00:00
aeebaaddda Refactor CSS for improved readability and maintainability, including adjustments to selectors and spacing for various components. 2025-11-23 13:22:07 +00:00
8ed0287d73 Adjust ObjectTable fixed column behavior for mobile and update action column width calculation 2025-11-23 13:21:48 +00:00
030053b965 Minor bug fixes 2025-11-23 13:21:34 +00:00
2d2df403e3 Added select type 2025-11-23 13:20:22 +00:00
8c622420bb Fixed loading glitch if web server becomes unresponsive. 2025-11-21 19:15:25 +00:00
26 changed files with 2141 additions and 1001 deletions

View File

@ -43,7 +43,7 @@
vertical-align: middle !important;
}
.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;
}
@ -78,10 +81,9 @@
}
.loading-modal .ant-modal-footer {
display: none;
display: none;
}
:root {
--unit-100vh: 100vh;
}
@ -92,7 +94,7 @@ display: none;
}
.dashboard-cards-header .ant-table-tbody {
display: none;
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,7 +253,6 @@ body {
--sb-size: 8px;
}
::-webkit-scrollbar {
width: 8px;
}
@ -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

File diff suppressed because it is too large Load Diff

View File

@ -1,27 +1,14 @@
import { useState, useContext, useEffect } from 'react'
import {
Form,
Button,
Typography,
Flex,
Steps,
Divider,
Descriptions,
Alert
} from 'antd'
import { useMediaQuery } from 'react-responsive'
import { Form, Flex, Descriptions, Alert } from 'antd'
import PropTypes from 'prop-types'
import { PrintServerContext } from '../../context/PrintServerContext'
import FilamentStockSelect from '../../common/FilamentStockSelect'
import PrinterSelect from '../../common/PrinterSelect'
import FilamentStockDisplay from '../../common/FilamentStockDisplay'
import PrinterTemperaturePanel from '../../common/PrinterTemperaturePanel'
import { LoadingOutlined } from '@ant-design/icons'
import PrinterState from '../../common/StateDisplay'
const { Title } = Typography
import ObjectSelect from '../../common/ObjectSelect'
import ObjectDisplay from '../../common/ObjectDisplay'
import WizardView from '../../common/WizardView'
import { ApiServerContext } from '../../context/ApiServerContext'
const LoadFilamentStock = ({
onOk,
@ -29,7 +16,8 @@ const LoadFilamentStock = ({
printer = null,
filamentStockLoaded = false
}) => {
const isMobile = useMediaQuery({ maxWidth: 768 })
const { connected, subscribeToObjectEvent, sendObjectAction } =
useContext(ApiServerContext)
LoadFilamentStock.propTypes = {
onOk: PropTypes.func.isRequired,
@ -38,8 +26,6 @@ const LoadFilamentStock = ({
filamentStockLoaded: PropTypes.bool
}
const { printServer } = useContext(PrintServerContext)
const initialLoadFilamentStockForm = {
printer: printer,
filamentStock: null
@ -47,8 +33,7 @@ const LoadFilamentStock = ({
const [loadFilamentStockLoading, setLoadFilamentStockLoading] =
useState(false)
const [currentStep, setCurrentStep] = useState(0)
const [nextEnabled, setNextEnabled] = useState(false)
const [formValid, setFormValid] = useState(false)
const [currentTemperature, setCurrentTemperature] = useState(-1)
const [targetTemperature, setTargetTemperature] = useState(0)
const [filamentSensorDetected, setFilamentSensorDetected] =
@ -62,77 +47,69 @@ const LoadFilamentStock = ({
loadFilamentStockForm
)
// Add websocket temperature monitoring
useEffect(() => {
if (loadFilamentStockFormValues.printer) {
const params = {
printerId: loadFilamentStockFormValues.printer._id,
objects: {
extruder: null,
'filament_switch_sensor fsensor': null
if (printer?._id && connected) {
const temperatureEventUnsubscribe = subscribeToObjectEvent(
printer._id,
'printer',
'temperature',
(event) => {
if (event.data?.extruder?.current) {
setCurrentTemperature(event.data.extruder.current)
}
if (event.data?.extruder?.target) {
setTargetTemperature(event.data.extruder.target)
}
}
const notifyStatusUpdate = (statusUpdate) => {
if (statusUpdate?.extruder?.temperature !== undefined) {
setCurrentTemperature(statusUpdate.extruder.temperature)
}
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
)
const filamentStockEventUnsubscribe = subscribeToObjectEvent(
printer._id,
'printer',
'filamentSensor',
(event) => {
console.log('filamentSensor', event.data)
setFilamentSensorDetected(event.data.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,26 +253,8 @@ 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'
name='loadFilamentStock'
autoComplete='off'
form={loadFilamentStockForm}
onFinish={handleLoadFilamentStock}
@ -298,44 +266,15 @@ const LoadFilamentStock = ({
}
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'
<WizardView
title='Load Filament Stock'
steps={steps}
onSubmit={() => loadFilamentStockForm.submit()}
formValid={formValid}
loading={loadFilamentStockLoading}
onClick={() => {
loadFilamentStockForm.submit()
}}
>
Done
</Button>
)}
</Flex>
submitText='Done'
/>
</Form>
</Flex>
</Flex>
)
}

View File

@ -1,34 +1,30 @@
import { useState, useContext, useEffect } from 'react'
import { Form, Button, Typography, Flex, Steps, Divider, Alert } from 'antd'
import { useMediaQuery } from 'react-responsive'
import { Form, Flex, Alert } from 'antd'
import PropTypes from 'prop-types'
import { PrintServerContext } from '../../context/PrintServerContext'
import PrinterSelect from '../../common/PrinterSelect'
import ObjectSelect from '../../common/ObjectSelect'
import PrinterTemperaturePanel from '../../common/PrinterTemperaturePanel'
import WizardView from '../../common/WizardView'
import { ApiServerContext } from '../../context/ApiServerContext'
import { LoadingOutlined } from '@ant-design/icons'
const { Title } = Typography
const UnloadFilamentStock = ({ onOk, reset, printer = null }) => {
const { connected, subscribeToObjectEvent, sendObjectAction } =
useContext(ApiServerContext)
UnloadFilamentStock.propTypes = {
onOk: PropTypes.func.isRequired,
reset: PropTypes.bool.isRequired,
printer: PropTypes.object
}
const { printServer } = useContext(PrintServerContext)
const isMobile = useMediaQuery({ maxWidth: 768 })
const initialUnloadFilamentStockForm = {
printer: printer
}
const [unloadFilamentStockLoading, setUnloadFilamentStockLoading] =
useState(false)
const [currentStep, setCurrentStep] = useState(0)
const [nextEnabled, setNextEnabled] = useState(false)
const [formValid, setFormValid] = useState(false)
const [currentTemperature, setCurrentTemperature] = useState(-1)
const [targetTemperature, setTargetTemperature] = useState(0)
const [filamentSensorDetected, setFilamentSensorDetected] = useState(true)
@ -36,46 +32,35 @@ const UnloadFilamentStock = ({ onOk, reset, printer = null }) => {
const [unloadFilamentStockFormValues, setUnloadFilamentStockFormValues] =
useState(initialUnloadFilamentStockForm)
// Add websocket temperature monitoring
useEffect(() => {
if (unloadFilamentStockFormValues.printer) {
const params = {
printerId: unloadFilamentStockFormValues.printer._id,
objects: {
extruder: null,
'filament_switch_sensor fsensor': null
if (printer?._id && connected) {
const temperatureEventUnsubscribe = subscribeToObjectEvent(
printer._id,
'printer',
'temperature',
(event) => {
if (event.data?.extruder?.current) {
setCurrentTemperature(event.data.extruder.current)
}
if (event.data?.extruder?.target) {
setTargetTemperature(event.data.extruder.target)
}
}
const notifyStatusUpdate = (statusUpdate) => {
if (statusUpdate?.extruder?.temperature !== undefined) {
setCurrentTemperature(statusUpdate.extruder.temperature)
}
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
)
const filamentStockEventUnsubscribe = subscribeToObjectEvent(
printer._id,
'printer',
'filamentSensor',
(event) => {
setFilamentSensorDetected(event.data.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,24 +183,6 @@ 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'
@ -226,34 +196,15 @@ const UnloadFilamentStock = ({ onOk, reset, printer = null }) => {
}
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'
<WizardView
title='Unload Filament Stock'
steps={steps}
onSubmit={() => unloadFilamentStockForm.submit()}
formValid={formValid}
loading={unloadFilamentStockLoading}
disabled={!nextEnabled}
onClick={() => {
unloadFilamentStockForm.submit()
}}
>
{unloadFilamentStockLoading ? 'Unloading...' : 'Unload'}
</Button>
)}
</Flex>
submitText={unloadFilamentStockLoading ? 'Unloading...' : 'Unload'}
/>
</Form>
</Flex>
</Flex>
)
}

View File

@ -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()
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'
}
]
}
]}
/>
)
}}

View File

@ -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>
</>
)
}

View File

@ -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,52 +35,42 @@ const DocumentPrinterInfo = () => {
'DocumentPrinterInfo',
{
info: true,
stocks: true,
notes: true,
auditLogs: 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 [objectFormState, setEditFormState] = useState({
isEditing: false,
editLoading: false,
formValid: false,
locked: false,
loading: false,
objectData: {}
})
const actions = {
reload: () => {
fetchObject()
objectFormRef?.current.handleFetchObject()
return true
},
edit: () => {
startEditing()
objectFormRef?.current.startEditing()
return false
},
cancelEdit: () => {
cancelEditing()
objectFormRef?.current.cancelEditing()
return true
},
finishEdit: () => {
handleUpdate()
objectFormRef?.current.handleUpdate()
return true
}
}
return (
<ActionHandler actions={actions} loading={loading}>
{({ callAction }) => (
<>
<Flex
gap='large'
vertical='true'
@ -92,17 +85,16 @@ const DocumentPrinterInfo = () => {
<ObjectActions
type='documentPrinter'
id={documentPrinterId}
disabled={loading}
objectData={objectData}
disabled={objectFormState.loading}
objectData={objectFormState.objectData}
/>
<ViewButton
disabled={loading}
disabled={objectFormState.loading}
items={[
{
key: 'info',
label: 'DocumentPrinter Information'
},
{ key: 'stocks', label: 'DocumentPrinter Stocks' },
{ key: 'notes', label: 'Notes' },
{ key: 'auditLogs', label: 'Audit Logs' }
]}
@ -111,43 +103,57 @@ const DocumentPrinterInfo = () => {
/>
<DocumentPrintButton
type='documentPrinter'
objectData={objectData}
disabled={loading}
objectData={objectFormState.objectData}
disabled={objectFormState.loading}
/>
</Space>
<LockIndicator lock={lock} />
<LockIndicator lock={objectFormState.lock} />
</Space>
<Space>
<EditButtons
isEditing={isEditing}
isEditing={objectFormState.isEditing}
handleUpdate={() => {
callAction('finishEdit')
actionHandlerRef.current.callAction('finishEdit')
}}
cancelEditing={() => {
callAction('cancelEdit')
actionHandlerRef.current.callAction('cancelEdit')
}}
startEditing={() => {
callAction('edit')
actionHandlerRef.current.callAction('edit')
}}
editLoading={editLoading}
formValid={formValid}
disabled={lock?.locked || loading}
loading={editLoading}
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}
>
<InfoCollapse
title='Document Printer Information'
icon={<InfoCircleIcon />}
active={collapseState.info}
onToggle={(expanded) =>
updateCollapseState('info', expanded)
}
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,22 +161,21 @@ const DocumentPrinterInfo = () => {
type='documentPrinter'
objectData={objectData}
/>
)
}}
</ObjectForm>
</InfoCollapse>
</ActionHandler>
<InfoCollapse
title='Notes'
icon={<NoteIcon />}
active={collapseState.notes}
onToggle={(expanded) =>
updateCollapseState('notes', expanded)
}
onToggle={(expanded) => updateCollapseState('notes', expanded)}
collapseKey='notes'
>
<Card>
<NotesPanel
_id={documentPrinterId}
type='documentPrinter'
/>
<NotesPanel _id={documentPrinterId} type='documentPrinter' />
</Card>
</InfoCollapse>
@ -183,7 +188,7 @@ const DocumentPrinterInfo = () => {
}
collapseKey='auditLogs'
>
{loading ? (
{objectFormState.loading ? (
<InfoCollapsePlaceholder />
) : (
<ObjectTable
@ -196,11 +201,7 @@ const DocumentPrinterInfo = () => {
</Flex>
</div>
</Flex>
)}
</ActionHandler>
)
}}
</ObjectForm>
</>
)
}

View File

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

View File

@ -1,19 +1,9 @@
import PropTypes from 'prop-types'
import { 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)}
<WizardView
steps={steps}
loading={submitLoading}
formValid={formValid}
title='New Document Template'
onSubmit={() => {
handleSubmit()
onOk()
}}
formValid={formValid}
submitLoading={submitLoading}
/>
</Flex>
</Flex>
)
}}
</NewObjectForm>

View File

@ -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,6 +185,7 @@ const ControlPrinter = () => {
)
return (
<>
<Flex
gap='large'
vertical='true'
@ -272,7 +312,15 @@ const ControlPrinter = () => {
'moonraker.host': false,
tags: false,
firmware: false,
alerts: 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'
@ -333,7 +381,7 @@ const ControlPrinter = () => {
{objectFormState.objectData?.currentSubJob?._id ? (
<ObjectForm
id={objectFormState.objectData.currentSubJob._id}
type='subjob'
type='subJob'
onStateChange={() => {}}
>
{({
@ -370,10 +418,12 @@ const ControlPrinter = () => {
updateCollapseState('filamentStock', expanded)
}
>
{objectFormState.objectData?.currentFilamentStock?._id ? (
{objectFormState.objectData?.currentFilamentStock
?._id ? (
<ObjectForm
id={
objectFormState.objectData.currentFilamentStock._id
objectFormState.objectData.currentFilamentStock
._id
}
type='filamentStock'
onStateChange={() => {}}
@ -438,6 +488,35 @@ const ControlPrinter = () => {
</Flex>
</div>
</Flex>
<Modal
open={loadFilamentStockOpen}
onCancel={() => setLoadFilamentStockOpen(false)}
footer={null}
width='700px'
destroyOnHidden={true}
>
<LoadFilamentStock
printer={objectFormState.objectData}
onOk={() => setLoadFilamentStockOpen(false)}
reset={false}
filamentStockLoaded={false}
/>
</Modal>
<Modal
open={unloadFilamentStockOpen}
onCancel={() => setUnloadFilamentStockOpen(false)}
footer={null}
width='700px'
destroyOnHidden={true}
>
<UnloadFilamentStock
printer={objectFormState.objectData}
onOk={() => setUnloadFilamentStockOpen(false)}
reset={false}
filamentStockLoaded={false}
/>
</Modal>
</>
)
}

View File

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

View File

@ -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

View File

@ -138,8 +138,9 @@ const ObjectForm = forwardRef(
return computedValues
}, [])
// Validate form on change
// Validate form on change (debounced to avoid heavy work on every keystroke)
useEffect(() => {
const timeoutId = setTimeout(() => {
form
.validateFields({ validateOnly: true })
.then(() => {
@ -150,12 +151,16 @@ const ObjectForm = forwardRef(
})
})
.catch(() => {
setFormValid(false)
onStateChange({
formValid: true,
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(() => {
const timeoutId = setTimeout(() => {
onStateChange({ objectData })
}, [objectData])
}, 150)
return () => clearTimeout(timeoutId)
}, [objectData, onStateChange])
const startEditing = () => {
setIsEditing(true)
@ -366,10 +376,19 @@ 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) {
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)
const allValues = { ...values, ...computedValues }

View File

@ -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

View File

@ -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])

View File

@ -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)
}

View File

@ -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}
/>

View File

@ -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}

View File

@ -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'

View File

@ -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) {
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

View File

@ -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

View File

@ -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}

View File

@ -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'
}
}

View File

@ -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'
}
]
}

View File

@ -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'
}
]
}

View File

@ -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
}
]
}