From ae8d4532c017367d32293fd23a188d75f7b10d4d Mon Sep 17 00:00:00 2001 From: Tom Butcher Date: Mon, 9 Nov 2026 00:00:05 +0000 Subject: [PATCH] Implement Cancel Sub Job Feature and Enhance Printer Queue Management - Added CancelSubJob component for handling sub job cancellation with user confirmation. - Updated PrinterQueue and PrinterQueueItem components to support cancellation actions and display appropriate controls based on user permissions. - Enhanced ObjectDisplay component to trigger data updates via onData callback, improving state management. - Introduced new styles for printer queue steps in App.css to improve layout consistency. --- assets/stylesheets/App.css | 7 ++ .../Production/Printers/ControlPrinter.jsx | 1 + .../Production/SubJobs/CancelSubJob.jsx | 69 +++++++++++ .../Dashboard/common/ObjectDisplay.jsx | 12 +- .../Dashboard/common/PrinterQueue.jsx | 110 +++++++++++++++++- src/database/models/SubJob.js | 12 +- 6 files changed, 201 insertions(+), 10 deletions(-) create mode 100644 src/components/Dashboard/Production/SubJobs/CancelSubJob.jsx 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 ? ( + +