Add Slicer Integration feature with lazy loading and routing support

- Introduced SlicerIntegration component for managing slicer uploads and printer control.
- Added SlicerUploadFlow component to handle the upload process and job creation.
- Updated App.jsx to include a new route for the SlicerIntegration component.
- Enhanced ProductionRoutes with new routes for PrinterProfiles and FilamentProfiles, improving navigation and organization of printer-related features.
This commit is contained in:
Tom Butcher 2026-07-20 02:09:59 +01:00
parent f5dffd5512
commit c0a6968045
4 changed files with 420 additions and 0 deletions

View File

@ -6,6 +6,7 @@ import {
Navigate
} from 'react-router-dom'
import { App, ConfigProvider } from 'antd'
import { lazy } from 'react'
import Dashboard from './components/Dashboard/Dashboard.jsx'
import PrivateRoute from './components/PrivateRoute'
@ -33,6 +34,10 @@ import AuthCallback from './components/App/AuthCallback.jsx'
import EmailNotificationTemplate from './components/Email/EmailNotificationTemplate.jsx'
import MarketplaceAuthCallback from './components/Dashboard/Sales/Marketplaces/MarketplaceAuthCallback.jsx'
import AuthLaunch from './components/App/AppLaunch.jsx'
const SlicerIntegration = lazy(
() =>
import('./components/Dashboard/Production/Printers/SlicerIntegration.jsx')
)
import {
ProductionRoutes,
@ -121,6 +126,10 @@ const AppContent = () => {
path='/email/notification'
element={<EmailNotificationTemplate />}
/>
<Route
path='/slicer'
element={<SlicerIntegration />}
/>
<Route
path='/dashboard'

View File

@ -0,0 +1,225 @@
import { useContext, useEffect, useState } from 'react'
import { Divider, Flex, Layout, Typography, Button, Badge, Modal } from 'antd'
import { LoadingOutlined } from '@ant-design/icons'
import { useLocation } from 'react-router-dom'
import ControlPrinter from './ControlPrinter.jsx'
import SlicerUploadFlow from './SlicerUploadFlow.jsx'
import PrinterIcon from '../../../Icons/PrinterIcon.jsx'
import ObjectForm from '../../common/ObjectForm.jsx'
import FileIcon from '../../../Icons/FileIcon.jsx'
import ObjectProperty from '../../common/ObjectProperty.jsx'
import { getModelProperty } from '../../../../database/ObjectModels.js'
import { ApiServerContext } from '../../context/ApiServerContext.jsx'
import { useThemeContext } from '../../context/ThemeContext.jsx'
const { Content } = Layout
const { Title } = Typography
const SlicerIntegration = () => {
const location = useLocation()
const printerId = new URLSearchParams(location.search).get('printerId')
const { updateObject } = useContext(ApiServerContext)
const [pendingSlicerUploads, setPendingSlicerUploads] = useState([])
const [currentPendingSlicerUpload, setCurrentPendingSlicerUpload] =
useState(null)
const { setPrimaryColorOverride } = useThemeContext()
useEffect(() => {
setPrimaryColorOverride('#00baa8')
}, [])
const [objectFormState, setEditFormState] = useState({
isEditing: false,
editLoading: false,
formValid: false,
locked: false,
loading: false,
objectData: {
pendingSlicerUploads: []
}
})
const [showSlicerUploadFlow, setShowSlicerUploadFlow] = useState(false)
useEffect(() => {
setPendingSlicerUploads(
objectFormState.objectData?.pendingSlicerUploads || []
)
}, [objectFormState.objectData?.pendingSlicerUploads])
useEffect(() => {
const nextUpload = pendingSlicerUploads.find(
(upload) => upload.new === true
)
if (nextUpload) {
setCurrentPendingSlicerUpload(nextUpload)
setShowSlicerUploadFlow(true)
} else {
setCurrentPendingSlicerUpload(null)
setShowSlicerUploadFlow(false)
}
}, [pendingSlicerUploads])
const showPendingSlicerUpload = () => {
const upload =
pendingSlicerUploads.find(
(pendingUpload) => pendingUpload.new === true
) || pendingSlicerUploads[0]
if (upload) {
setCurrentPendingSlicerUpload(upload)
setShowSlicerUploadFlow(true)
}
}
const handleGCodeFileCreated = async (gcodeFile) => {
const updatedUploads = pendingSlicerUploads.map((upload) =>
upload._id === currentPendingSlicerUpload?._id
? { ...upload, gcodeFile: gcodeFile }
: upload
)
setPendingSlicerUploads(updatedUploads)
setCurrentPendingSlicerUpload((previous) => ({
...previous,
gcodeFile
}))
await updateObject(printerId, 'printer', {
...objectFormState.objectData,
pendingSlicerUploads: updatedUploads
})
}
const handleJobCreated = async (job) => {
const updatedUploads = pendingSlicerUploads.map((upload) =>
upload._id === currentPendingSlicerUpload?._id
? { ...upload, job }
: upload
)
setPendingSlicerUploads(updatedUploads)
setCurrentPendingSlicerUpload((previous) => ({
...previous,
job
}))
await updateObject(printerId, 'printer', {
...objectFormState.objectData,
pendingSlicerUploads: updatedUploads
})
}
const handleSlicerUploadComplete = async () => {
const updatedUploads = pendingSlicerUploads.filter(
(upload) => upload._id !== currentPendingSlicerUpload?._id
)
setPendingSlicerUploads(updatedUploads)
await updateObject(printerId, 'printer', {
...objectFormState.objectData,
pendingSlicerUploads: updatedUploads
})
}
const handleSlicerUploadFlowClosed = async () => {
const updatedUploads = pendingSlicerUploads.map((upload) =>
upload._id === currentPendingSlicerUpload?._id
? { ...upload, new: false }
: upload
)
setPendingSlicerUploads(updatedUploads)
await updateObject(printerId, 'printer', {
...objectFormState.objectData,
pendingSlicerUploads: updatedUploads
})
}
const slicerUploadsButton = () => {
if (pendingSlicerUploads.length > 0) {
return (
<Badge
count={pendingSlicerUploads.length}
style={{ marginTop: '8px' }}
size='small'
>
<Button
type='text'
icon={<FileIcon />}
onClick={showPendingSlicerUpload}
/>
</Badge>
)
}
return null
}
return (
<Layout style={{ height: 'var(--unit-100vh)' }}>
<Layout style={{ padding: '24px' }}>
<Content>
<Flex vertical style={{ height: '100%' }} gap='20px'>
<ObjectForm
id={printerId}
type='printer'
onStateChange={(state) =>
setEditFormState((previousState) => ({
...previousState,
...state
}))
}
>
{({ loading, objectData }) => (
<Flex justify='space-between' align='center'>
<Flex align='center' gap='middle'>
{loading ? (
<LoadingOutlined spin style={{ fontSize: '26px' }} />
) : (
<PrinterIcon style={{ fontSize: '26px' }} />
)}
<Title level={3} style={{ margin: 0 }}>
{loading
? 'Loading...'
: objectData?.name || 'Slicer Integration'}
</Title>
{!loading && (
<ObjectProperty
objectData={objectData}
{...getModelProperty('printer', 'state')}
/>
)}
</Flex>
{slicerUploadsButton()}
</Flex>
)}
</ObjectForm>
<Divider style={{ margin: '0' }} />
<ControlPrinter slicerIntegration={true} />
</Flex>
</Content>
<Modal
open={showSlicerUploadFlow}
onCancel={() => setShowSlicerUploadFlow(false)}
afterClose={handleSlicerUploadFlowClosed}
footer={null}
width={740}
destroyOnHidden={true}
>
{currentPendingSlicerUpload && (
<SlicerUploadFlow
key={currentPendingSlicerUpload._id}
currentPendingSlicerUpload={currentPendingSlicerUpload}
printer={objectFormState.objectData}
onGCodeFileCreated={handleGCodeFileCreated}
onJobCreated={handleJobCreated}
onComplete={handleSlicerUploadComplete}
/>
)}
</Modal>
</Layout>
</Layout>
)
}
export default SlicerIntegration

View File

@ -0,0 +1,152 @@
import { useMemo, useState } from 'react'
import PropTypes from 'prop-types'
import NewGCodeFile from '../GCodeFiles/NewGCodeFile.jsx'
import NewJob from '../Jobs/NewJob.jsx'
import DeployJob from '../Jobs/DeployJob.jsx'
import WizardView from '../../common/WizardView.jsx'
const SlicerUploadFlow = ({
currentPendingSlicerUpload,
printer,
onGCodeFileCreated,
onJobCreated,
onComplete
}) => {
const [createdGCodeFile, setCreatedGCodeFile] = useState(
() => currentPendingSlicerUpload.gcodeFile || null
)
const [createdJob, setCreatedJob] = useState(
() => currentPendingSlicerUpload.job || null
)
const [currentSubStep, setCurrentSubStep] = useState(0)
const gcodeFileDefaults = useMemo(
() => ({
...(currentPendingSlicerUpload?.properties || {}),
file: currentPendingSlicerUpload?.file
}),
[currentPendingSlicerUpload]
)
const jobDefaults = useMemo(
() => ({
quantity: 1,
gcodeFile: createdGCodeFile,
printers: [printer]
}),
[createdGCodeFile, printer]
)
const steps = [
{
title: 'GCode File',
key: 'gcodeFile',
subSteps: [
{
title: 'Required',
key: 'required'
},
{
title: 'Summary',
key: 'summary'
}
],
content: (
<NewGCodeFile
defaultValues={gcodeFileDefaults}
slicerIntegration={true}
shouldPrint={currentPendingSlicerUpload.shouldPrint}
currentStep={currentSubStep}
onStepChange={setCurrentSubStep}
onOk={(gcodeFile) => {
if (currentPendingSlicerUpload.shouldPrint) {
setCurrentSubStep(0)
setCreatedGCodeFile(gcodeFile)
onGCodeFileCreated(gcodeFile)
} else {
onComplete({ gcodeFile })
}
}}
/>
)
}
]
if (currentPendingSlicerUpload.shouldPrint) {
steps.push({
title: 'Print Job',
key: 'job',
subSteps: [
{
title: 'Required',
key: 'required'
},
{
title: 'Summary',
key: 'summary'
}
],
content: createdGCodeFile ? (
<NewJob
slicerIntegration={true}
defaultValues={jobDefaults}
currentStep={currentSubStep}
onStepChange={setCurrentSubStep}
onOk={(job) => {
if (job == null || job == undefined) {
onComplete({ gcodeFile: createdGCodeFile })
} else {
setCurrentSubStep(0)
setCreatedJob(job)
onJobCreated(job)
}
}}
/>
) : null
})
steps.push({
title: 'Deploy',
key: 'deploy',
content: createdJob ? (
<DeployJob
objectData={createdJob}
slicerIntegration={true}
onOk={() =>
onComplete({ gcodeFile: createdGCodeFile, job: createdJob })
}
/>
) : null
})
}
return (
<WizardView
title='Slicer Upload'
steps={steps}
currentStep={createdJob ? 2 : createdGCodeFile ? 1 : 0}
currentSubStep={currentSubStep}
showButtons={false}
showTitle={false}
showSteps={currentPendingSlicerUpload.shouldPrint}
formValid={true}
onSubmit={() => {}}
/>
)
}
SlicerUploadFlow.propTypes = {
currentPendingSlicerUpload: PropTypes.shape({
file: PropTypes.oneOfType([PropTypes.string, PropTypes.object]).isRequired,
gcodeFile: PropTypes.oneOfType([PropTypes.string, PropTypes.object]),
job: PropTypes.oneOfType([PropTypes.string, PropTypes.object]),
shouldPrint: PropTypes.bool.isRequired,
properties: PropTypes.object
}).isRequired,
printer: PropTypes.object.isRequired,
onGCodeFileCreated: PropTypes.func.isRequired,
onJobCreated: PropTypes.func.isRequired,
onComplete: PropTypes.func.isRequired
}
export default SlicerUploadFlow

View File

@ -13,6 +13,20 @@ const ControlPrinter = lazy(
const PrinterInfo = lazy(
() => import('../components/Dashboard/Production/Printers/PrinterInfo.jsx')
)
const PrinterProfiles = lazy(
() => import('../components/Dashboard/Production/PrinterProfiles.jsx')
)
const PrinterProfileInfo = lazy(
() =>
import('../components/Dashboard/Production/PrinterProfiles/PrinterProfileInfo.jsx')
)
const FilamentProfiles = lazy(
() => import('../components/Dashboard/Production/FilamentProfiles.jsx')
)
const FilamentProfileInfo = lazy(
() =>
import('../components/Dashboard/Production/FilamentProfiles/FilamentProfileInfo.jsx')
)
const Jobs = lazy(() => import('../components/Dashboard/Production/Jobs.jsx'))
const JobInfo = lazy(
() => import('../components/Dashboard/Production/Jobs/JobInfo.jsx')
@ -52,6 +66,26 @@ const ProductionRoutes = [
path='production/printers/info'
element={<PrinterInfo />}
/>,
<Route
key='printerprofiles'
path='production/printerprofiles'
element={<PrinterProfiles />}
/>,
<Route
key='printerprofiles-info'
path='production/printerprofiles/info'
element={<PrinterProfileInfo />}
/>,
<Route
key='filamentprofiles'
path='production/filamentprofiles'
element={<FilamentProfiles />}
/>,
<Route
key='filamentprofiles-info'
path='production/filamentprofiles/info'
element={<FilamentProfileInfo />}
/>,
<Route key='jobs' path='production/jobs' element={<Jobs />} />,
<Route key='subjobs' path='production/subjobs' element={<SubJobs />} />,
<Route