Implement Cancel Sub Job Feature and Enhance Printer Queue Management
Some checks reported errors
farmcontrol/farmcontrol-ui/pipeline/head Something is wrong with the build of this commit

- 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.
This commit is contained in:
Tom Butcher 2026-11-09 00:00:05 +00:00
parent 9b86114d99
commit ae8d4532c0
6 changed files with 201 additions and 10 deletions

View File

@ -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%;

View File

@ -465,6 +465,7 @@ const ControlPrinter = ({ slicerIntegration = false }) => {
currentSubJob={
objectFormState.objectData?.currentSubJob
}
printerId={printerId}
showHyperlink={!slicerIntegration}
/>
</Card>

View File

@ -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 (
<MessageDialogView
title='Are you sure you want to cancel this sub job?'
description={`Cancelling sub job ${subJobLabel} will remove it from the printer queue.`}
onOk={handleCancel}
okText='Cancel Sub Job'
okLoading={cancelLoading}
danger
/>
)
}
CancelSubJob.propTypes = {
onOk: PropTypes.func.isRequired,
objectData: PropTypes.object
}
export default CancelSubJob

View File

@ -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

View File

@ -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 (
<Flex align='center' gap={4} style={{ width: '100%', minWidth: 0 }}>
<div style={{ minWidth: 0, flex: 1 }}>
<ObjectDisplay
object={item}
objectType='subJob'
showHyperlink={showHyperlink}
onData={setObjectData}
/>
</div>
{showCancel ? (
<Tooltip title={cancelAction.label}>
<Button
icon={
cancelAction.icon ? createElement(cancelAction.icon) : undefined
}
type='text'
size='small'
danger={cancelAction.danger || false}
onClick={handleCancel}
/>
</Tooltip>
) : null}
</Flex>
)
}
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: (
<ObjectDisplay
object={item}
objectType='subJob'
<PrinterQueueItem
key={getId(item)}
item={item}
showHyperlink={showHyperlink}
printerId={printerId}
/>
)
}))}
@ -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

View File

@ -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 })
}
}
],