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
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:
parent
9b86114d99
commit
ae8d4532c0
@ -4397,6 +4397,13 @@ body.objectKanbanColumnResizing * {
|
|||||||
min-height: 0;
|
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 {
|
.mobile-footer-navigation.ant-layout-footer {
|
||||||
flex: 0 0 auto;
|
flex: 0 0 auto;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
|
|||||||
@ -465,6 +465,7 @@ const ControlPrinter = ({ slicerIntegration = false }) => {
|
|||||||
currentSubJob={
|
currentSubJob={
|
||||||
objectFormState.objectData?.currentSubJob
|
objectFormState.objectData?.currentSubJob
|
||||||
}
|
}
|
||||||
|
printerId={printerId}
|
||||||
showHyperlink={!slicerIntegration}
|
showHyperlink={!slicerIntegration}
|
||||||
/>
|
/>
|
||||||
</Card>
|
</Card>
|
||||||
|
|||||||
69
src/components/Dashboard/Production/SubJobs/CancelSubJob.jsx
Normal file
69
src/components/Dashboard/Production/SubJobs/CancelSubJob.jsx
Normal 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
|
||||||
@ -19,7 +19,8 @@ const ObjectDisplay = ({
|
|||||||
object,
|
object,
|
||||||
objectType,
|
objectType,
|
||||||
showHyperlink = false,
|
showHyperlink = false,
|
||||||
showSpotlight = true
|
showSpotlight = true,
|
||||||
|
onData
|
||||||
}) => {
|
}) => {
|
||||||
const [objectData, setObjectData] = useState(object)
|
const [objectData, setObjectData] = useState(object)
|
||||||
const [isHydrating, setIsHydrating] = useState(false)
|
const [isHydrating, setIsHydrating] = useState(false)
|
||||||
@ -28,12 +29,18 @@ const ObjectDisplay = ({
|
|||||||
const { token } = useContext(AuthContext)
|
const { token } = useContext(AuthContext)
|
||||||
const { callAction } = useActions()
|
const { callAction } = useActions()
|
||||||
const idRef = useRef(null)
|
const idRef = useRef(null)
|
||||||
|
const onDataRef = useRef(onData)
|
||||||
|
onDataRef.current = onData
|
||||||
|
|
||||||
// Update event handler
|
// Update event handler
|
||||||
const updateObjectEventHandler = useCallback((value) => {
|
const updateObjectEventHandler = useCallback((value) => {
|
||||||
setObjectData((prev) => merge({}, prev, value))
|
setObjectData((prev) => merge({}, prev, value))
|
||||||
}, [])
|
}, [])
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
onDataRef.current?.(objectData)
|
||||||
|
}, [objectData])
|
||||||
|
|
||||||
// Ensure ID is valid before hydrate/subscribe (non-empty, not null/undefined)
|
// Ensure ID is valid before hydrate/subscribe (non-empty, not null/undefined)
|
||||||
const isValidId = useCallback((id) => {
|
const isValidId = useCallback((id) => {
|
||||||
return id != null && String(id).trim() !== ''
|
return id != null && String(id).trim() !== ''
|
||||||
@ -271,7 +278,8 @@ ObjectDisplay.propTypes = {
|
|||||||
objectType: PropTypes.string,
|
objectType: PropTypes.string,
|
||||||
style: PropTypes.object,
|
style: PropTypes.object,
|
||||||
showHyperlink: PropTypes.bool,
|
showHyperlink: PropTypes.bool,
|
||||||
showSpotlight: PropTypes.bool
|
showSpotlight: PropTypes.bool,
|
||||||
|
onData: PropTypes.func
|
||||||
}
|
}
|
||||||
|
|
||||||
export default ObjectDisplay
|
export default ObjectDisplay
|
||||||
|
|||||||
@ -1,7 +1,14 @@
|
|||||||
|
import { createElement, useContext, useEffect, useRef, useState } from 'react'
|
||||||
import PropTypes from 'prop-types'
|
import PropTypes from 'prop-types'
|
||||||
|
import { Button, Flex, Steps } from 'antd'
|
||||||
import ObjectDisplay from './ObjectDisplay'
|
import ObjectDisplay from './ObjectDisplay'
|
||||||
import { Steps } from 'antd'
|
|
||||||
import MissingPlaceholder from './MissingPlaceholder'
|
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) => {
|
const getId = (item) => {
|
||||||
if (!item) return null
|
if (!item) return null
|
||||||
@ -17,7 +24,96 @@ const toObject = (item) => {
|
|||||||
return 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)
|
const queueItems = Array.isArray(queue)
|
||||||
? queue.map(toObject).filter(Boolean)
|
? queue.map(toObject).filter(Boolean)
|
||||||
: []
|
: []
|
||||||
@ -53,10 +149,11 @@ const PrinterQueue = ({ queue, currentSubJob, showHyperlink = true }) => {
|
|||||||
size='small'
|
size='small'
|
||||||
items={items.map((item) => ({
|
items={items.map((item) => ({
|
||||||
title: (
|
title: (
|
||||||
<ObjectDisplay
|
<PrinterQueueItem
|
||||||
object={item}
|
key={getId(item)}
|
||||||
objectType='subJob'
|
item={item}
|
||||||
showHyperlink={showHyperlink}
|
showHyperlink={showHyperlink}
|
||||||
|
printerId={printerId}
|
||||||
/>
|
/>
|
||||||
)
|
)
|
||||||
}))}
|
}))}
|
||||||
@ -67,7 +164,8 @@ const PrinterQueue = ({ queue, currentSubJob, showHyperlink = true }) => {
|
|||||||
PrinterQueue.propTypes = {
|
PrinterQueue.propTypes = {
|
||||||
queue: PropTypes.array,
|
queue: PropTypes.array,
|
||||||
currentSubJob: PropTypes.oneOfType([PropTypes.object, PropTypes.string]),
|
currentSubJob: PropTypes.oneOfType([PropTypes.object, PropTypes.string]),
|
||||||
showHyperlink: PropTypes.bool
|
showHyperlink: PropTypes.bool,
|
||||||
|
printerId: PropTypes.string
|
||||||
}
|
}
|
||||||
|
|
||||||
export default PrinterQueue
|
export default PrinterQueue
|
||||||
|
|||||||
@ -3,6 +3,9 @@ import { createElement, lazy } from 'react'
|
|||||||
const SubJobInfo = lazy(
|
const SubJobInfo = lazy(
|
||||||
() => import('../../components/Dashboard/Production/SubJobs/SubJobInfo')
|
() => import('../../components/Dashboard/Production/SubJobs/SubJobInfo')
|
||||||
)
|
)
|
||||||
|
const CancelSubJob = lazy(
|
||||||
|
() => import('../../components/Dashboard/Production/SubJobs/CancelSubJob')
|
||||||
|
)
|
||||||
import SubJobIcon from '../../components/Icons/SubJobIcon'
|
import SubJobIcon from '../../components/Icons/SubJobIcon'
|
||||||
import InfoCircleIcon from '../../components/Icons/InfoCircleIcon'
|
import InfoCircleIcon from '../../components/Icons/InfoCircleIcon'
|
||||||
import XMarkIcon from '../../components/Icons/XMarkIcon'
|
import XMarkIcon from '../../components/Icons/XMarkIcon'
|
||||||
@ -35,13 +38,18 @@ export const SubJob = {
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: 'cancel',
|
name: 'cancel',
|
||||||
type: 'page',
|
type: 'modal',
|
||||||
pageName: 'info',
|
modalWidth: 520,
|
||||||
|
modalCentered: true,
|
||||||
label: 'Cancel Sub Job',
|
label: 'Cancel Sub Job',
|
||||||
row: true,
|
row: true,
|
||||||
|
danger: true,
|
||||||
icon: XMarkIcon,
|
icon: XMarkIcon,
|
||||||
disabled: (objectData) => {
|
disabled: (objectData) => {
|
||||||
return objectData?.state?.type !== 'queued'
|
return objectData?.state?.type !== 'queued'
|
||||||
|
},
|
||||||
|
content: (objectData, { onOk } = {}) => {
|
||||||
|
return createElement(CancelSubJob, { objectData, onOk })
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user