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:
parent
f5dffd5512
commit
c0a6968045
@ -6,6 +6,7 @@ import {
|
|||||||
Navigate
|
Navigate
|
||||||
} from 'react-router-dom'
|
} from 'react-router-dom'
|
||||||
import { App, ConfigProvider } from 'antd'
|
import { App, ConfigProvider } from 'antd'
|
||||||
|
import { lazy } from 'react'
|
||||||
|
|
||||||
import Dashboard from './components/Dashboard/Dashboard.jsx'
|
import Dashboard from './components/Dashboard/Dashboard.jsx'
|
||||||
import PrivateRoute from './components/PrivateRoute'
|
import PrivateRoute from './components/PrivateRoute'
|
||||||
@ -33,6 +34,10 @@ import AuthCallback from './components/App/AuthCallback.jsx'
|
|||||||
import EmailNotificationTemplate from './components/Email/EmailNotificationTemplate.jsx'
|
import EmailNotificationTemplate from './components/Email/EmailNotificationTemplate.jsx'
|
||||||
import MarketplaceAuthCallback from './components/Dashboard/Sales/Marketplaces/MarketplaceAuthCallback.jsx'
|
import MarketplaceAuthCallback from './components/Dashboard/Sales/Marketplaces/MarketplaceAuthCallback.jsx'
|
||||||
import AuthLaunch from './components/App/AppLaunch.jsx'
|
import AuthLaunch from './components/App/AppLaunch.jsx'
|
||||||
|
const SlicerIntegration = lazy(
|
||||||
|
() =>
|
||||||
|
import('./components/Dashboard/Production/Printers/SlicerIntegration.jsx')
|
||||||
|
)
|
||||||
|
|
||||||
import {
|
import {
|
||||||
ProductionRoutes,
|
ProductionRoutes,
|
||||||
@ -121,6 +126,10 @@ const AppContent = () => {
|
|||||||
path='/email/notification'
|
path='/email/notification'
|
||||||
element={<EmailNotificationTemplate />}
|
element={<EmailNotificationTemplate />}
|
||||||
/>
|
/>
|
||||||
|
<Route
|
||||||
|
path='/slicer'
|
||||||
|
element={<SlicerIntegration />}
|
||||||
|
/>
|
||||||
|
|
||||||
<Route
|
<Route
|
||||||
path='/dashboard'
|
path='/dashboard'
|
||||||
|
|||||||
@ -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
|
||||||
@ -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
|
||||||
@ -13,6 +13,20 @@ const ControlPrinter = lazy(
|
|||||||
const PrinterInfo = lazy(
|
const PrinterInfo = lazy(
|
||||||
() => import('../components/Dashboard/Production/Printers/PrinterInfo.jsx')
|
() => 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 Jobs = lazy(() => import('../components/Dashboard/Production/Jobs.jsx'))
|
||||||
const JobInfo = lazy(
|
const JobInfo = lazy(
|
||||||
() => import('../components/Dashboard/Production/Jobs/JobInfo.jsx')
|
() => import('../components/Dashboard/Production/Jobs/JobInfo.jsx')
|
||||||
@ -52,6 +66,26 @@ const ProductionRoutes = [
|
|||||||
path='production/printers/info'
|
path='production/printers/info'
|
||||||
element={<PrinterInfo />}
|
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='jobs' path='production/jobs' element={<Jobs />} />,
|
||||||
<Route key='subjobs' path='production/subjobs' element={<SubJobs />} />,
|
<Route key='subjobs' path='production/subjobs' element={<SubJobs />} />,
|
||||||
<Route
|
<Route
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user