Compare commits

..

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

26 changed files with 983 additions and 2123 deletions

View File

@ -43,7 +43,7 @@
vertical-align: middle !important;
}
.ant-typography-ellipsis-single-line > code {
.ant-typography-ellipsis-single-line >code {
vertical-align: top !important;
}
@ -55,9 +55,7 @@
-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;
}
@ -69,8 +67,7 @@
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;
}
@ -81,9 +78,10 @@
}
.loading-modal .ant-modal-footer {
display: none;
display: none;
}
:root {
--unit-100vh: 100vh;
}
@ -94,7 +92,7 @@
}
.dashboard-cards-header .ant-table-tbody {
display: none;
display: none;
}
.ant-menu-overflow-item-rest::after {
@ -227,21 +225,19 @@ 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;
@ -253,6 +249,7 @@ body {
--sb-size: 8px;
}
::-webkit-scrollbar {
width: 8px;
}
@ -310,7 +307,3 @@ 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,14 +1,27 @@
import { useState, useContext, useEffect } from 'react'
import { Form, Flex, Descriptions, Alert } from 'antd'
import {
Form,
Button,
Typography,
Flex,
Steps,
Divider,
Descriptions,
Alert
} from 'antd'
import { useMediaQuery } from 'react-responsive'
import PropTypes from 'prop-types'
import { 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 ObjectSelect from '../../common/ObjectSelect'
import ObjectDisplay from '../../common/ObjectDisplay'
import WizardView from '../../common/WizardView'
import { ApiServerContext } from '../../context/ApiServerContext'
import PrinterState from '../../common/StateDisplay'
const { Title } = Typography
const LoadFilamentStock = ({
onOk,
@ -16,8 +29,7 @@ const LoadFilamentStock = ({
printer = null,
filamentStockLoaded = false
}) => {
const { connected, subscribeToObjectEvent, sendObjectAction } =
useContext(ApiServerContext)
const isMobile = useMediaQuery({ maxWidth: 768 })
LoadFilamentStock.propTypes = {
onOk: PropTypes.func.isRequired,
@ -26,6 +38,8 @@ const LoadFilamentStock = ({
filamentStockLoaded: PropTypes.bool
}
const { printServer } = useContext(PrintServerContext)
const initialLoadFilamentStockForm = {
printer: printer,
filamentStock: null
@ -33,7 +47,8 @@ const LoadFilamentStock = ({
const [loadFilamentStockLoading, setLoadFilamentStockLoading] =
useState(false)
const [formValid, setFormValid] = useState(false)
const [currentStep, setCurrentStep] = useState(0)
const [nextEnabled, setNextEnabled] = useState(false)
const [currentTemperature, setCurrentTemperature] = useState(-1)
const [targetTemperature, setTargetTemperature] = useState(0)
const [filamentSensorDetected, setFilamentSensorDetected] =
@ -47,69 +62,77 @@ const LoadFilamentStock = ({
loadFilamentStockForm
)
// Add websocket temperature monitoring
useEffect(() => {
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)
if (loadFilamentStockFormValues.printer) {
const params = {
printerId: loadFilamentStockFormValues.printer._id,
objects: {
extruder: null,
'filament_switch_sensor fsensor': null
}
}
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 () => {
if (temperatureEventUnsubscribe) temperatureEventUnsubscribe()
if (filamentStockEventUnsubscribe) filamentStockEventUnsubscribe()
printServer.off('notify_status_update', notifyStatusUpdate)
printServer.emit('printer.objects.unsubscribe', params)
}
}
}, [printer?._id, connected])
}, [printServer, loadFilamentStockFormValues.printer])
useEffect(() => {
// Validate form fields
loadFilamentStockForm
.validateFields({
validateOnly: true
})
.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))
.then(() => setNextEnabled(filamentSensorDetected))
.catch(() => setNextEnabled(false))
}, [
loadFilamentStockForm,
loadFilamentStockFormUpdateValues,
loadFilamentStockFormValues,
filamentSensorDetected
])
useEffect(() => {
if (
filamentSensorDetected == true &&
currentTemperature >= targetTemperature
) {
setNextEnabled(filamentSensorDetected)
if (currentStep == 0) {
setCurrentStep(1)
}
} else if (filamentSensorDetected == false) {
setCurrentStep(0)
}
}, [
filamentSensorDetected,
targetTemperature,
currentTemperature,
targetTemperature
currentStep
])
const summaryItems = [
@ -117,9 +140,8 @@ const LoadFilamentStock = ({
key: 'filamentStock',
label: 'Stock',
children: loadFilamentStockFormValues.filamentStock ? (
<ObjectDisplay
objectType='filamentStock'
object={loadFilamentStockFormValues.filamentStock}
<FilamentStockDisplay
filamentStock={loadFilamentStockFormValues.filamentStock}
/>
) : (
'n/a'
@ -129,12 +151,9 @@ const LoadFilamentStock = ({
key: 'printer',
label: 'Printer',
children: loadFilamentStockFormValues.printer ? (
<ObjectDisplay
objectType='printer'
object={loadFilamentStockFormValues.printer}
/>
<PrinterState printer={loadFilamentStockFormValues.printer} />
) : (
'n/a'
'n/a>'
)
}
]
@ -150,16 +169,10 @@ const LoadFilamentStock = ({
try {
// Set the extruder temperature
await sendObjectAction(
loadFilamentStockFormValues.printer._id,
'printer',
{
type: 'loadFilamentStock',
data: {
filamentStock: loadFilamentStockFormValues.filamentStock
}
}
)
await printServer.emit('printer.filamentstock.load', {
printerId: loadFilamentStockFormValues.printer._id,
filamentStockId: loadFilamentStockFormValues.filamentStock._id
})
onOk()
} finally {
setLoadFilamentStockLoading(false)
@ -183,7 +196,7 @@ const LoadFilamentStock = ({
}
]}
>
<ObjectSelect type='printer' checkable={false} />
<PrinterSelect checkable={false} />
</Form.Item>
{targetTemperature == 0 ? (
<Alert
@ -213,9 +226,10 @@ const LoadFilamentStock = ({
{loadFilamentStockFormValues.printer ? (
<PrinterTemperaturePanel
showBed={false}
showHeatedBed={false}
showMoreInfo={false}
id={loadFilamentStockFormValues.printer._id}
printerId={loadFilamentStockFormValues.printer._id}
shouldUnsubscribe={false}
/>
) : null}
</Flex>
@ -236,7 +250,7 @@ const LoadFilamentStock = ({
}
]}
>
<ObjectSelect type='filamentStock' />
<FilamentStockSelect />
</Form.Item>
</>
)
@ -253,8 +267,26 @@ 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='loadFilamentStock'
name='basic'
autoComplete='off'
form={loadFilamentStockForm}
onFinish={handleLoadFilamentStock}
@ -266,15 +298,44 @@ const LoadFilamentStock = ({
}
initialValues={initialLoadFilamentStockForm}
>
<WizardView
title='Load Filament Stock'
steps={steps}
onSubmit={() => loadFilamentStockForm.submit()}
formValid={formValid}
<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}
submitText='Done'
/>
onClick={() => {
loadFilamentStockForm.submit()
}}
>
Done
</Button>
)}
</Flex>
</Form>
</Flex>
</Flex>
)
}

View File

@ -1,30 +1,34 @@
import { useState, useContext, useEffect } from 'react'
import { Form, Flex, Alert } from 'antd'
import { Form, Button, Typography, Flex, Steps, Divider, Alert } from 'antd'
import { useMediaQuery } from 'react-responsive'
import PropTypes from 'prop-types'
import { PrintServerContext } from '../../context/PrintServerContext'
import ObjectSelect from '../../common/ObjectSelect'
import PrinterSelect from '../../common/PrinterSelect'
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 [formValid, setFormValid] = useState(false)
const [currentStep, setCurrentStep] = useState(0)
const [nextEnabled, setNextEnabled] = useState(false)
const [currentTemperature, setCurrentTemperature] = useState(-1)
const [targetTemperature, setTargetTemperature] = useState(0)
const [filamentSensorDetected, setFilamentSensorDetected] = useState(true)
@ -32,35 +36,46 @@ const UnloadFilamentStock = ({ onOk, reset, printer = null }) => {
const [unloadFilamentStockFormValues, setUnloadFilamentStockFormValues] =
useState(initialUnloadFilamentStockForm)
// Add websocket temperature monitoring
useEffect(() => {
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)
if (unloadFilamentStockFormValues.printer) {
const params = {
printerId: unloadFilamentStockFormValues.printer._id,
objects: {
extruder: null,
'filament_switch_sensor fsensor': null
}
}
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 () => {
if (temperatureEventUnsubscribe) temperatureEventUnsubscribe()
if (filamentStockEventUnsubscribe) filamentStockEventUnsubscribe()
printServer.off('notify_status_update', notifyStatusUpdate)
printServer.emit('printer.objects.unsubscribe', params)
}
}
}, [printer?._id, connected])
}, [printServer, unloadFilamentStockFormValues.printer])
useEffect(() => {
if (reset) {
@ -75,14 +90,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
setFormValid(
setNextEnabled(
Boolean(unloadFilamentStockFormValues.printer) &&
!unloadFilamentStockLoading &&
currentTemperature + 1 > targetTemperature &&
targetTemperature != 0
)
})
.catch(() => setFormValid(false))
.catch(() => setNextEnabled(false))
}, [
unloadFilamentStockForm,
unloadFilamentStockFormValues,
@ -94,13 +109,10 @@ const UnloadFilamentStock = ({ onOk, reset, printer = null }) => {
const handleUnloadFilamentStock = async () => {
setUnloadFilamentStockLoading(true)
// Send G-code to retract the filament
await sendObjectAction(
unloadFilamentStockFormValues.printer._id,
'printer',
{
type: 'unloadFilamentStock'
}
)
await printServer.emit('printer.gcode.script', {
printerId: unloadFilamentStockFormValues.printer._id,
script: `_CLIENT_LINEAR_MOVE E=-200 F=1000`
})
//setUnloadFilamentStockLoading(false)
}
@ -128,7 +140,7 @@ const UnloadFilamentStock = ({ onOk, reset, printer = null }) => {
}
]}
>
<ObjectSelect type='printer' checkable={false} />
<PrinterSelect checkable={false} />
</Form.Item>
{unloadFilamentStockLoading == false ? (
@ -170,11 +182,11 @@ const UnloadFilamentStock = ({ onOk, reset, printer = null }) => {
/>
)}
{unloadFilamentStockFormValues.printer?._id ? (
{unloadFilamentStockFormValues.printer ? (
<PrinterTemperaturePanel
showBed={false}
showHeatedBed={false}
showMoreInfo={false}
id={unloadFilamentStockFormValues.printer._id}
printerId={unloadFilamentStockFormValues.printer._id}
/>
) : null}
</Flex>
@ -183,6 +195,24 @@ 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'
@ -196,15 +226,34 @@ const UnloadFilamentStock = ({ onOk, reset, printer = null }) => {
}
initialValues={initialUnloadFilamentStockForm}
>
<WizardView
title='Unload Filament Stock'
steps={steps}
onSubmit={() => unloadFilamentStockForm.submit()}
formValid={formValid}
<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}
submitText={unloadFilamentStockLoading ? 'Unloading...' : 'Unload'}
/>
disabled={!nextEnabled}
onClick={() => {
unloadFilamentStockForm.submit()
}}
>
{unloadFilamentStockLoading ? 'Unloading...' : 'Unload'}
</Button>
)}
</Flex>
</Form>
</Flex>
</Flex>
)
}

View File

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

View File

@ -1,6 +1,5 @@
import { useRef, useState } from 'react'
import { Button, Flex, Space, Dropdown, message, Modal } from 'antd'
import PlusIcon from '../../Icons/PlusIcon'
import { useRef } from 'react'
import { Button, Flex, Space, Dropdown } from 'antd'
import ObjectTable from '../common/ObjectTable'
import ReloadIcon from '../../Icons/ReloadIcon'
import useColumnVisibility from '../hooks/useColumnVisibility'
@ -8,12 +7,10 @@ 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] =
@ -21,12 +18,6 @@ const DocumentPrinters = () => {
const actionItems = {
items: [
{
label: 'New Document Printer',
key: 'newDocumentPrinter',
icon: <PlusIcon />
},
{ type: 'divider' },
{
label: 'Reload List',
key: 'reloadList',
@ -36,8 +27,6 @@ const DocumentPrinters = () => {
onClick: ({ key }) => {
if (key === 'reloadList') {
tableRef.current?.reload()
} else if (key === 'newDocumentPrinter') {
setNewDocumentPrinterOpen(true)
}
}
}
@ -45,7 +34,6 @@ const DocumentPrinters = () => {
return (
<>
<Flex vertical={'true'} gap='large'>
{contextHolder}
<Flex justify={'space-between'}>
<Space size='small'>
<Dropdown menu={actionItems}>
@ -74,22 +62,6 @@ 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,4 +1,3 @@
import { useRef, useState } from 'react'
import { useLocation } from 'react-router-dom'
import { Space, Flex, Card } from 'antd'
import { LoadingOutlined } from '@ant-design/icons'
@ -26,8 +25,6 @@ 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'
)
@ -35,42 +32,52 @@ 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: {}
})
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: () => {
objectFormRef?.current.handleFetchObject()
fetchObject()
return true
},
edit: () => {
objectFormRef?.current.startEditing()
startEditing()
return false
},
cancelEdit: () => {
objectFormRef?.current.cancelEditing()
cancelEditing()
return true
},
finishEdit: () => {
objectFormRef?.current.handleUpdate()
handleUpdate()
return true
}
}
return (
<>
<ActionHandler actions={actions} loading={loading}>
{({ callAction }) => (
<Flex
gap='large'
vertical='true'
@ -85,16 +92,17 @@ const DocumentPrinterInfo = () => {
<ObjectActions
type='documentPrinter'
id={documentPrinterId}
disabled={objectFormState.loading}
objectData={objectFormState.objectData}
disabled={loading}
objectData={objectData}
/>
<ViewButton
disabled={objectFormState.loading}
disabled={loading}
items={[
{
key: 'info',
label: 'DocumentPrinter Information'
},
{ key: 'stocks', label: 'DocumentPrinter Stocks' },
{ key: 'notes', label: 'Notes' },
{ key: 'auditLogs', label: 'Audit Logs' }
]}
@ -103,57 +111,43 @@ const DocumentPrinterInfo = () => {
/>
<DocumentPrintButton
type='documentPrinter'
objectData={objectFormState.objectData}
disabled={objectFormState.loading}
objectData={objectData}
disabled={loading}
/>
</Space>
<LockIndicator lock={objectFormState.lock} />
<LockIndicator lock={lock} />
</Space>
<Space>
<EditButtons
isEditing={objectFormState.isEditing}
isEditing={isEditing}
handleUpdate={() => {
actionHandlerRef.current.callAction('finishEdit')
callAction('finishEdit')
}}
cancelEditing={() => {
actionHandlerRef.current.callAction('cancelEdit')
callAction('cancelEdit')
}}
startEditing={() => {
actionHandlerRef.current.callAction('edit')
callAction('edit')
}}
editLoading={objectFormState.editLoading}
formValid={objectFormState.formValid}
disabled={objectFormState.lock?.locked || objectFormState.loading}
loading={objectFormState.editLoading}
editLoading={editLoading}
formValid={formValid}
disabled={lock?.locked || loading}
loading={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 />}
@ -161,21 +155,22 @@ 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>
@ -188,7 +183,7 @@ const DocumentPrinterInfo = () => {
}
collapseKey='auditLogs'
>
{objectFormState.loading ? (
{loading ? (
<InfoCollapsePlaceholder />
) : (
<ObjectTable
@ -201,7 +196,11 @@ const DocumentPrinterInfo = () => {
</Flex>
</div>
</Flex>
</>
)}
</ActionHandler>
)
}}
</ObjectForm>
)
}

View File

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

View File

@ -1,9 +1,19 @@
import PropTypes from 'prop-types'
import { 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 WizardView from '../../common/WizardView'
import NewObjectButtons from '../../common/NewObjectButtons'
const { Title } = Typography
const NewDocumentTemplate = ({ onOk }) => {
const [currentStep, setCurrentStep] = useState(0)
const isMobile = useMediaQuery({ maxWidth: 768 })
return (
<NewObjectForm
type={'documentTemplate'}
@ -59,18 +69,44 @@ const NewDocumentTemplate = ({ onOk }) => {
)
}
]
return (
<WizardView
steps={steps}
loading={submitLoading}
formValid={formValid}
title='New Document Template'
<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>
)
}}
</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, Modal } from 'antd'
import { Space, Flex, Card, Splitter, Divider } from 'antd'
import loglevel from 'loglevel'
import config from '../../../../config.js'
import useCollapseState from '../../hooks/useCollapseState.js'
@ -28,9 +28,6 @@ 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)
@ -60,9 +57,6 @@ const ControlPrinter = () => {
collapseState.movement
)
const [loadFilamentStockOpen, setLoadFilamentStockOpen] = useState(false)
const [unloadFilamentStockOpen, setUnloadFilamentStockOpen] = useState(false)
useEffect(() => {
setSideBarVisible(
collapseState.temperature ||
@ -128,39 +122,6 @@ 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
}
}
@ -185,7 +146,6 @@ const ControlPrinter = () => {
)
return (
<>
<Flex
gap='large'
vertical='true'
@ -312,15 +272,7 @@ const ControlPrinter = () => {
'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
alerts: false
}}
objectData={printerObjectData}
type='printer'
@ -381,7 +333,7 @@ const ControlPrinter = () => {
{objectFormState.objectData?.currentSubJob?._id ? (
<ObjectForm
id={objectFormState.objectData.currentSubJob._id}
type='subJob'
type='subjob'
onStateChange={() => {}}
>
{({
@ -418,12 +370,10 @@ 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={() => {}}
@ -488,35 +438,6 @@ 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,14 +108,7 @@ const DocumentPrintButton = ({
onCancel={() => setNewDocumentJobOpen(false)}
footer={null}
destroyOnHidden={true}
width={{
xs: '100%',
sm: '100%',
md: '100%',
lg: '90%',
xl: '80%',
xxl: '80%'
}}
width={900}
>
<NewDocumentJob
onOk={() => {

View File

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

View File

@ -138,9 +138,8 @@ const ObjectForm = forwardRef(
return computedValues
}, [])
// Validate form on change (debounced to avoid heavy work on every keystroke)
// Validate form on change
useEffect(() => {
const timeoutId = setTimeout(() => {
form
.validateFields({ validateOnly: true })
.then(() => {
@ -151,16 +150,12 @@ const ObjectForm = forwardRef(
})
})
.catch(() => {
setFormValid(false)
onStateChange({
formValid: false,
formValid: true,
objectData: { ...serverObjectData, ...form.getFieldsValue() }
})
})
}, 150)
return () => clearTimeout(timeoutId)
}, [form, formUpdateValues, onStateChange, serverObjectData])
}, [form, formUpdateValues])
// Cleanup on unmount
useEffect(() => {
@ -257,14 +252,9 @@ const ObjectForm = forwardRef(
updateLockEventHandler
])
// Debounce objectData updates sent to parent to limit re-renders
useEffect(() => {
const timeoutId = setTimeout(() => {
onStateChange({ objectData })
}, 150)
return () => clearTimeout(timeoutId)
}, [objectData, onStateChange])
}, [objectData])
const startEditing = () => {
setIsEditing(true)
@ -376,19 +366,10 @@ const ObjectForm = forwardRef(
model
)
// Update form with computed values if any were calculated and they changed
// Update form with computed values if any were calculated
if (Object.keys(computedValues).length > 0) {
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,8 +81,6 @@ const ObjectProperty = ({
minimal = false,
previewOpen = false,
showPreview = true,
options = [],
roundNumber = false,
showHyperlink,
...rest
}) => {
@ -168,18 +166,6 @@ 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':
@ -248,15 +234,10 @@ const ObjectProperty = ({
</Text>
)
} else {
var roundedValue = value
if (roundNumber != false) {
roundedValue = value.toFixed(roundNumber)
}
return (
<Text {...textParams}>
{prefix}
{typeof value === 'number' ? roundedValue : value}
{typeof value === 'number' ? value.toFixed(2) : value}
{suffix}
</Text>
)
@ -572,17 +553,6 @@ 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}>
@ -803,8 +773,7 @@ ObjectProperty.propTypes = {
height: PropTypes.string,
previewOpen: PropTypes.bool,
showPreview: PropTypes.bool,
showHyperlink: PropTypes.bool,
options: PropTypes.array
showHyperlink: PropTypes.bool
}
export default ObjectProperty

View File

@ -144,7 +144,6 @@ const ObjectSelect = ({
parentKeys: parentKeys.concat(key || '-'),
filterPath: newFilterPath,
selectable: false,
children: buildTreeData(
value,
pIdx + 1,
@ -279,11 +278,8 @@ const ObjectSelect = ({
handleFetchObjectsProperties()
setInitialized(true)
}
if (value == null) {
setTreeSelectValue(null)
setInitialized(true)
}
}
handleValue()
}, [
value,
@ -307,13 +303,8 @@ 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 && lazyLoading == false) {
if (hasMore) {
setPages((prev) => {
const filteredPages = prev.map((page) => ({
...page,
@ -244,13 +244,13 @@ const ObjectTable = forwardRef(
})
fetchData(nextPage)
}
}, [pages, createSkeletonData, fetchData, hasMore, lazyLoading])
}, [pages, createSkeletonData, fetchData, hasMore])
const loadPreviousPage = useCallback(() => {
const lowestPage = Math.min(...pages.map((p) => p.pageNum))
const prevPage = lowestPage - 1
if (prevPage > 0 && lazyLoading == false) {
if (prevPage > 0) {
setPages((prev) => {
const filteredPages = prev.map((page) => ({
...page,
@ -267,7 +267,7 @@ const ObjectTable = forwardRef(
})
fetchData(prevPage)
}
}, [pages, createSkeletonData, fetchData, lazyLoading])
}, [pages, createSkeletonData, fetchData])
const handleScroll = useCallback(
(e) => {
@ -600,7 +600,7 @@ const ObjectTable = forwardRef(
title: prop.label,
dataIndex: prop.name,
width: prop.columnWidth || width,
fixed: isMobile ? undefined : fixed,
fixed: fixed,
key: prop.name,
render: (text, record) => {
if (record?.isSkeleton) {
@ -651,7 +651,7 @@ const ObjectTable = forwardRef(
),
key: 'actions',
fixed: 'right',
width: 20 + rowActions.length * 30, // Adjust width based on number of actions
width: 80 + rowActions.length * 40, // Adjust width based on number of actions
render: (record) => {
return renderActions(record)
}

View File

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

View File

@ -4,13 +4,6 @@ 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
@ -23,14 +16,10 @@ const StateDisplay = ({ state, showProgress = true, showState = true }) => {
<StateTag state={currentState.type} />
</Space>
)}
{showProgress &&
loadingProgressTypes.includes(currentState.type) &&
currentState?.progress &&
currentState?.progress > 0 ? (
{showProgress && currentState?.progress && currentState?.progress > 0 ? (
<Progress
percent={Math.round(currentState.progress * 100)}
status={currentState.type === 'used' ? '' : 'active'}
strokeColor={currentState.type === 'used' ? 'orange' : ''}
status='active'
style={{ width: '150px', marginBottom: '2px' }}
/>
) : null}

View File

@ -56,9 +56,9 @@ const StateTag = ({ state, showBadge = true, style = {} }) => {
status = 'success'
text = 'Ready'
break
case 'new':
case 'unconsumed':
status = 'success'
text = 'New'
text = 'Unconsumed'
break
case 'error':
status = 'error'
@ -80,10 +80,6 @@ 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, Select } from 'antd'
import { Flex, Button, Input } from 'antd'
import PlusIcon from '../../Icons/PlusIcon.jsx'
import MinusIcon from '../../Icons/MinusIcon.jsx'
import InfoCircleIcon from '../../Icons/InfoCircleIcon.jsx'
@ -14,26 +14,22 @@ const TemplatePreview = ({
isEditing,
onTestObjectOpen,
onPreviewMessage,
showTestObject = false,
showPreviewSwitch = true
showTestObject = false
}) => {
const iframeRef = useRef(null)
const { fetchTemplatePreview, fetchTemplatePDF } =
useContext(ApiServerContext)
const [previewContentHTML, setPreviewContentHTML] = useState('')
const [pdfBlob, setPDFBlob] = useState(null)
const { fetchTemplatePreview } = useContext(ApiServerContext)
const [previewContent, setPreviewContent] = useState('')
const [reloadLoading, setReloadLoading] = useState(false)
const [previewScale, setPreviewScale] = useState(1)
const [previewType, setPreviewType] = useState('HTML')
const updatePreviewContentHTML = (html) => {
const updatePreviewContent = (html) => {
if (iframeRef.current) {
// Save current scroll position
const scrollY = iframeRef.current.contentWindow.scrollY
const scrollX = iframeRef.current.contentWindow.scrollX
// Update srcDoc
setPreviewContentHTML(html)
setPreviewContent(html)
// Restore scroll position after iframe loads new content
const handleLoad = () => {
@ -44,23 +40,6 @@ const TemplatePreview = ({
}
}
const reloadPreviewPDF = (content, testObject = {}) => {
setReloadLoading(true)
fetchTemplatePDF(documentTemplate._id, content, testObject, (result) => {
setReloadLoading(false)
if (result?.error) {
// Handle error through parent component
onPreviewMessage(result.error, true)
} else {
const pdfBlob = new Blob([result.pdf], { type: 'application/pdf' })
const pdfUrl = URL.createObjectURL(pdfBlob)
setPDFBlob(pdfUrl)
onPreviewMessage('No issues found.', false)
}
})
}
const reloadPreview = useCallback(
(content, testObject = {}, scale = 1) => {
setReloadLoading(true)
@ -75,7 +54,7 @@ const TemplatePreview = ({
// Handle error through parent component
onPreviewMessage(result.error, true)
} else {
updatePreviewContentHTML(result.html)
updatePreviewContent(result.html)
onPreviewMessage('No issues found.', false)
}
}
@ -87,13 +66,9 @@ 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, previewType])
}, [objectData, documentTemplate, previewScale, reloadPreview])
return (
<Flex vertical gap={'middle'} style={{ height: '100%' }}>
@ -123,51 +98,34 @@ 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' }}
disabled={loading || reloadLoading || previewType == 'PDF'}
loading={loading || reloadLoading}
disabled={loading || reloadLoading}
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={previewType == 'HTML' ? previewContentHTML : undefined}
src={previewType == 'PDF' ? pdfBlob : undefined}
srcDoc={previewContent}
frameBorder='0'
style={{
width: '100%',
@ -188,8 +146,7 @@ TemplatePreview.propTypes = {
style: PropTypes.object,
showTestObject: PropTypes.bool,
onTestObjectOpen: PropTypes.func.isRequired,
onPreviewMessage: PropTypes.func.isRequired,
showPreviewSwitch: PropTypes.bool
onPreviewMessage: PropTypes.func.isRequired
}
export default TemplatePreview

View File

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

View File

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

View File

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

View File

@ -2,12 +2,11 @@ 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: 'DJB',
prefix: 'DSZ',
icon: DocumentJobIcon,
actions: [
{
@ -61,9 +60,7 @@ export const DocumentJob = {
columnWidth: 200,
columnFixed: 'left',
value: (objectData) => {
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})`
}
return `${objectData?.documentTemplate?.name || 'No template'} (${objectData?.object?.name || 'No name'})`
}
},
{
@ -72,14 +69,6 @@ export const DocumentJob = {
type: 'dateTime',
readOnly: true
},
{
name: 'state',
label: 'Status',
type: 'state',
objectType: 'printer',
showName: false,
readOnly: true
},
{
name: 'objectType',
label: 'Object Type',
@ -97,15 +86,6 @@ 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',
@ -121,13 +101,6 @@ export const DocumentJob = {
}
}
},
{
name: 'documentTemplate._id',
label: 'Template ID',
type: 'id',
showHyperlink: true,
objectType: 'documentTemplate'
},
{
name: 'documentPrinter',
label: 'Printer',
@ -141,13 +114,6 @@ export const DocumentJob = {
online: true
}
}
},
{
name: 'documentPrinter._id',
label: 'Printer ID',
type: 'id',
showHyperlink: true,
objectType: 'documentPrinter'
}
]
}

View File

@ -74,88 +74,52 @@ export const DocumentPrinter = {
readOnly: true
},
{
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',
name: 'documentSize',
label: 'Document Size',
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: 'currentDocumentSize._id',
label: 'Current Document Size ID',
name: 'documentSize._id',
label: '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: 'startQueue',
label: 'Start Queue',
name: 'Start',
label: 'Start',
icon: PlayCircleIcon,
disabled: (objectData) => {
console.log(objectData?.subJobs?.length)
@ -109,60 +109,28 @@ export const Printer = {
url: (_id) =>
`/dashboard/production/printers/control?printerId=${_id}&action=startQueue`
},
{ type: 'divider' },
{
name: 'pauseJob',
label: 'Pause Job',
name: 'pause',
label: 'Pause',
icon: PauseCircleIcon,
disabled: (objectData) => {
return objectData?.state?.type != 'printing'
},
url: (_id) =>
`/dashboard/production/printers/control?printerId=${_id}&action=pauseJob`
`/dashboard/production/printers/control?printerId=${_id}&action=pauseQueue`
},
{
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',
name: 'Stop',
label: 'Stop',
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=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`
`/dashboard/production/printers/control?printerId=${_id}&action=stopQueue`
}
]
}
@ -331,14 +299,6 @@ export const Printer = {
label: 'Alerts',
type: 'alerts',
required: false
},
{
name: 'subJobs',
label: 'Queue',
type: 'objectList',
objectType: 'subJob',
required: false,
readOnly: true
}
]
}