diff --git a/assets/stylesheets/App.css b/assets/stylesheets/App.css
index 63a5cd7a..66f8a467 100644
--- a/assets/stylesheets/App.css
+++ b/assets/stylesheets/App.css
@@ -4397,6 +4397,13 @@ body.objectKanbanColumnResizing * {
min-height: 0;
}
+.printerQueue.ant-steps .ant-steps-item-title {
+ display: flex;
+ align-items: center;
+ width: 100%;
+ padding-inline-end: 0;
+}
+
.mobile-footer-navigation.ant-layout-footer {
flex: 0 0 auto;
width: 100%;
diff --git a/src/components/Dashboard/Production/Printers/ControlPrinter.jsx b/src/components/Dashboard/Production/Printers/ControlPrinter.jsx
index 2f619c4e..ffcceeb0 100644
--- a/src/components/Dashboard/Production/Printers/ControlPrinter.jsx
+++ b/src/components/Dashboard/Production/Printers/ControlPrinter.jsx
@@ -465,6 +465,7 @@ const ControlPrinter = ({ slicerIntegration = false }) => {
currentSubJob={
objectFormState.objectData?.currentSubJob
}
+ printerId={printerId}
showHyperlink={!slicerIntegration}
/>
diff --git a/src/components/Dashboard/Production/SubJobs/CancelSubJob.jsx b/src/components/Dashboard/Production/SubJobs/CancelSubJob.jsx
new file mode 100644
index 00000000..0a8a2313
--- /dev/null
+++ b/src/components/Dashboard/Production/SubJobs/CancelSubJob.jsx
@@ -0,0 +1,69 @@
+import { useState, useContext } from 'react'
+import PropTypes from 'prop-types'
+import { ApiServerContext } from '../../context/ApiServerContext'
+import { message } from 'antd'
+import MessageDialogView from '../../common/MessageDialogView.jsx'
+
+const getRefId = (value) => {
+ if (!value) return null
+ if (typeof value === 'string') return value
+ if (typeof value._id === 'string') return value._id
+ if (typeof value._id === 'object' && value._id?._id) return value._id._id
+ return null
+}
+
+const CancelSubJob = ({ onOk, objectData }) => {
+ const [cancelLoading, setCancelLoading] = useState(false)
+ const { connected, sendObjectAction } = useContext(ApiServerContext)
+
+ const handleCancel = async () => {
+ if (!connected) {
+ message.error('Not connected to server')
+ return
+ }
+
+ const printerId = getRefId(objectData?.printer)
+ const subJobId = getRefId(objectData)
+ if (!printerId || !subJobId) {
+ message.error('Sub job is not assigned to a printer')
+ return
+ }
+
+ setCancelLoading(true)
+ try {
+ sendObjectAction(printerId, 'printer', {
+ type: 'cancelSubJob',
+ data: {
+ _id: subJobId
+ }
+ })
+ message.success('Sub job cancel initiated')
+ onOk()
+ } catch (error) {
+ console.error('Error cancelling sub job:', error)
+ } finally {
+ setCancelLoading(false)
+ }
+ }
+
+ const subJobLabel =
+ objectData?.name || objectData?._reference || objectData?._id
+
+ return (
+
+ )
+}
+
+CancelSubJob.propTypes = {
+ onOk: PropTypes.func.isRequired,
+ objectData: PropTypes.object
+}
+
+export default CancelSubJob
diff --git a/src/components/Dashboard/common/ObjectDisplay.jsx b/src/components/Dashboard/common/ObjectDisplay.jsx
index bd3477ec..4d9288f4 100644
--- a/src/components/Dashboard/common/ObjectDisplay.jsx
+++ b/src/components/Dashboard/common/ObjectDisplay.jsx
@@ -19,7 +19,8 @@ const ObjectDisplay = ({
object,
objectType,
showHyperlink = false,
- showSpotlight = true
+ showSpotlight = true,
+ onData
}) => {
const [objectData, setObjectData] = useState(object)
const [isHydrating, setIsHydrating] = useState(false)
@@ -28,12 +29,18 @@ const ObjectDisplay = ({
const { token } = useContext(AuthContext)
const { callAction } = useActions()
const idRef = useRef(null)
+ const onDataRef = useRef(onData)
+ onDataRef.current = onData
// Update event handler
const updateObjectEventHandler = useCallback((value) => {
setObjectData((prev) => merge({}, prev, value))
}, [])
+ useEffect(() => {
+ onDataRef.current?.(objectData)
+ }, [objectData])
+
// Ensure ID is valid before hydrate/subscribe (non-empty, not null/undefined)
const isValidId = useCallback((id) => {
return id != null && String(id).trim() !== ''
@@ -271,7 +278,8 @@ ObjectDisplay.propTypes = {
objectType: PropTypes.string,
style: PropTypes.object,
showHyperlink: PropTypes.bool,
- showSpotlight: PropTypes.bool
+ showSpotlight: PropTypes.bool,
+ onData: PropTypes.func
}
export default ObjectDisplay
diff --git a/src/components/Dashboard/common/PrinterQueue.jsx b/src/components/Dashboard/common/PrinterQueue.jsx
index c79fb7dd..3690806e 100644
--- a/src/components/Dashboard/common/PrinterQueue.jsx
+++ b/src/components/Dashboard/common/PrinterQueue.jsx
@@ -1,7 +1,14 @@
+import { createElement, useContext, useEffect, useRef, useState } from 'react'
import PropTypes from 'prop-types'
+import { Button, Flex, Steps } from 'antd'
import ObjectDisplay from './ObjectDisplay'
-import { Steps } from 'antd'
import MissingPlaceholder from './MissingPlaceholder'
+import Tooltip from './Tooltip'
+import { getModelByName } from '../../../database/ObjectModels'
+import { findAction } from '../../../utils/modelActions'
+import { hasActionPermission } from '../../../database/permissions'
+import { AuthContext } from '../context/AuthContext'
+import { useActions } from '../context/ActionsContext'
const getId = (item) => {
if (!item) return null
@@ -17,7 +24,96 @@ const toObject = (item) => {
return item
}
-const PrinterQueue = ({ queue, currentSubJob, showHyperlink = true }) => {
+const canCancelSubJob = (cancelAction, objectData, userProfile, model) => {
+ if (!cancelAction || !objectData) return false
+ if (!hasActionPermission(userProfile, model, cancelAction.name)) return false
+ if (typeof cancelAction.visible === 'function') {
+ if (!cancelAction.visible(objectData)) return false
+ } else if (cancelAction.visible === false) {
+ return false
+ }
+ if (typeof cancelAction.disabled === 'function') {
+ return !cancelAction.disabled({ ...objectData, _user: userProfile })
+ }
+ return !cancelAction.disabled
+}
+
+const PrinterQueueItem = ({ item, showHyperlink, printerId }) => {
+ const itemId = getId(item)
+ const [objectData, setObjectData] = useState(item)
+ const itemRef = useRef(item)
+ itemRef.current = item
+ const { userProfile } = useContext(AuthContext)
+ const { callAction } = useActions()
+ const model = getModelByName('subJob')
+ const cancelAction = findAction(model, 'cancel')
+
+ useEffect(() => {
+ setObjectData(itemRef.current)
+ }, [itemId])
+
+ const showCancel = canCancelSubJob(
+ cancelAction,
+ objectData,
+ userProfile,
+ model
+ )
+
+ const handleCancel = (event) => {
+ event.stopPropagation()
+ event.preventDefault()
+ if (!showCancel || !itemId) return
+ callAction(
+ cancelAction,
+ {
+ ...objectData,
+ _id: itemId,
+ printer: objectData?.printer || printerId,
+ _user: userProfile
+ },
+ 'subJob'
+ )
+ }
+
+ return (
+
+
+
+
+ {showCancel ? (
+
+
+
+ ) : null}
+
+ )
+}
+
+PrinterQueueItem.propTypes = {
+ item: PropTypes.object,
+ showHyperlink: PropTypes.bool,
+ printerId: PropTypes.string
+}
+
+const PrinterQueue = ({
+ queue,
+ currentSubJob,
+ showHyperlink = true,
+ printerId
+}) => {
const queueItems = Array.isArray(queue)
? queue.map(toObject).filter(Boolean)
: []
@@ -53,10 +149,11 @@ const PrinterQueue = ({ queue, currentSubJob, showHyperlink = true }) => {
size='small'
items={items.map((item) => ({
title: (
-
)
}))}
@@ -67,7 +164,8 @@ const PrinterQueue = ({ queue, currentSubJob, showHyperlink = true }) => {
PrinterQueue.propTypes = {
queue: PropTypes.array,
currentSubJob: PropTypes.oneOfType([PropTypes.object, PropTypes.string]),
- showHyperlink: PropTypes.bool
+ showHyperlink: PropTypes.bool,
+ printerId: PropTypes.string
}
export default PrinterQueue
diff --git a/src/database/models/SubJob.js b/src/database/models/SubJob.js
index 84a13efc..8d2d3a57 100644
--- a/src/database/models/SubJob.js
+++ b/src/database/models/SubJob.js
@@ -3,6 +3,9 @@ import { createElement, lazy } from 'react'
const SubJobInfo = lazy(
() => import('../../components/Dashboard/Production/SubJobs/SubJobInfo')
)
+const CancelSubJob = lazy(
+ () => import('../../components/Dashboard/Production/SubJobs/CancelSubJob')
+)
import SubJobIcon from '../../components/Icons/SubJobIcon'
import InfoCircleIcon from '../../components/Icons/InfoCircleIcon'
import XMarkIcon from '../../components/Icons/XMarkIcon'
@@ -35,13 +38,18 @@ export const SubJob = {
},
{
name: 'cancel',
- type: 'page',
- pageName: 'info',
+ type: 'modal',
+ modalWidth: 520,
+ modalCentered: true,
label: 'Cancel Sub Job',
row: true,
+ danger: true,
icon: XMarkIcon,
disabled: (objectData) => {
return objectData?.state?.type !== 'queued'
+ },
+ content: (objectData, { onOk } = {}) => {
+ return createElement(CancelSubJob, { objectData, onOk })
}
}
],