Enhance Dashboard Components with New Features and Styling Improvements

- Updated App.css to refine styling for object display tags and introduced a border for better visual distinction.
- Enhanced NewDocumentJob component by adding a default quantity value for improved user experience.
- Introduced PrinterQueue component to manage and display printer job queues, enhancing the ControlPrinter functionality.
- Improved MissingPlaceholder component with additional props for customizable border and padding options.
- Adjusted ObjectDisplay and ObjectList components for better layout and interaction, including cursor changes for clickable elements.
- Updated ThemeContext to manage new color variables for consistent theming across components.
This commit is contained in:
Tom Butcher 2026-08-22 20:02:50 +01:00
parent 39f1b31ac8
commit 90d9ed286d
11 changed files with 186 additions and 55 deletions

View File

@ -680,9 +680,11 @@ body {
border-radius: 0px !important; border-radius: 0px !important;
} }
.ant-select-selection-item .object-display-tag, .ant-select-selection-item .ant-tag.object-display-tag,
.ant-select-tree-title .object-display-tag { .ant-select-tree-title .ant-tag.object-display-tag,
.ant-select-dropdown .ant-tag.object-display-tag {
background: transparent !important; background: transparent !important;
border: none;
padding: 0; padding: 0;
margin-right: 1px !important; margin-right: 1px !important;
} }
@ -1304,7 +1306,8 @@ span.ant-skeleton-input.ant-skeleton-input-sm.text-skeleton {
min-width: 0; min-width: 0;
max-width: 100%; max-width: 100%;
width: auto; width: auto;
overflow: hidden; overflow-y: hidden;
overflow-x: visible;
flex-grow: 0; flex-grow: 0;
flex-shrink: 1; flex-shrink: 1;
flex-basis: auto; flex-basis: auto;
@ -1686,6 +1689,10 @@ span.ant-skeleton-input.ant-skeleton-input-sm.text-skeleton {
font-size: 95%; font-size: 95%;
} }
.ant-tag.object-display-tag {
border: 1px solid color-mix(in srgb, var(--color-text) 5%, transparent);
}
.code-block-editor .simplebar-content-wrapper { .code-block-editor .simplebar-content-wrapper {
background-color: var(--layout-modal-bg); background-color: var(--layout-modal-bg);
} }

View File

@ -73,6 +73,7 @@ const NewDocumentJob = ({ onOk, defaultValues = {} }) => {
// Capture initial default values so later prop changes don't re-initialize the form // Capture initial default values so later prop changes don't re-initialize the form
const defaultValuesRef = useRef({ const defaultValuesRef = useRef({
objectType: 'documentJob', objectType: 'documentJob',
quantity: 1,
...defaultValues, ...defaultValues,
saveToFile: false saveToFile: false
}) })

View File

@ -34,6 +34,8 @@ import { ApiServerContext } from '../../context/ApiServerContext.jsx'
import ScrollBox from '../../common/ScrollBox.jsx' import ScrollBox from '../../common/ScrollBox.jsx'
import ObjectTableNavigationButtons from '../../common/ObjectTableNavigationButtons.jsx' import ObjectTableNavigationButtons from '../../common/ObjectTableNavigationButtons.jsx'
import PrinterControlButtons from '../../common/PrinterControlButtons.jsx' import PrinterControlButtons from '../../common/PrinterControlButtons.jsx'
import PrinterQueue from '../../common/PrinterQueue.jsx'
import ListIcon from '../../../Icons/ListIcon.jsx'
const log = loglevel.getLogger('ControlPrinter') const log = loglevel.getLogger('ControlPrinter')
log.setLevel(config.logLevel) log.setLevel(config.logLevel)
@ -50,6 +52,7 @@ const ControlPrinter = ({ slicerIntegration = false }) => {
printer: true, printer: true,
job: true, job: true,
subjob: true, subjob: true,
queue: true,
filamentStock: true, filamentStock: true,
temperature: true, temperature: true,
position: true, position: true,
@ -271,7 +274,7 @@ const ControlPrinter = ({ slicerIntegration = false }) => {
<Space size='small'> <Space size='small'>
<ObjectActions <ObjectActions
type='printer' type='printer'
pageName='info' pageName='info'
id={printerId} id={printerId}
disabled={objectFormState.loading} disabled={objectFormState.loading}
visibleActions={{ visibleActions={{
@ -297,6 +300,10 @@ const ControlPrinter = ({ slicerIntegration = false }) => {
key: 'subJob', key: 'subJob',
label: 'Sub Job' label: 'Sub Job'
}, },
{
key: 'queue',
label: 'Queue'
},
{ {
key: 'filamentStock', key: 'filamentStock',
label: 'Filament Stock' label: 'Filament Stock'
@ -401,6 +408,7 @@ const ControlPrinter = ({ slicerIntegration = false }) => {
'currentJob._id': false, 'currentJob._id': false,
currentSubJob: false, currentSubJob: false,
'currentSubJob._id': false, 'currentSubJob._id': false,
queue: false,
createdAt: false, createdAt: false,
updatedAt: false, updatedAt: false,
state: !slicerIntegration, state: !slicerIntegration,
@ -418,6 +426,29 @@ const ControlPrinter = ({ slicerIntegration = false }) => {
}} }}
</ObjectForm> </ObjectForm>
</InfoCollapse> </InfoCollapse>
<InfoCollapse
title={'Queue'}
icon={<ListIcon />}
collapseKey='queue'
active={collapseState.queue}
onToggle={(expanded) =>
updateCollapseState('queue', expanded)
}
>
<Card
style={{
backgroundColor: 'transparent',
border: '1px solid var(--color-descriptions-border)'
}}
>
<PrinterQueue
queue={objectFormState.objectData?.queue}
currentSubJob={
objectFormState.objectData?.currentSubJob
}
/>
</Card>
</InfoCollapse>
<InfoCollapse <InfoCollapse
title={'Job'} title={'Job'}
icon={<JobIcon />} icon={<JobIcon />}
@ -459,7 +490,9 @@ const ControlPrinter = ({ slicerIntegration = false }) => {
) : ( ) : (
<MissingPlaceholder <MissingPlaceholder
message={'No job.'} message={'No job.'}
padding='24px'
hasBackground={false} hasBackground={false}
mutedBorder={true}
/> />
)} )}
</InfoCollapse> </InfoCollapse>
@ -507,7 +540,9 @@ const ControlPrinter = ({ slicerIntegration = false }) => {
) : ( ) : (
<MissingPlaceholder <MissingPlaceholder
message={'No sub job.'} message={'No sub job.'}
padding='24px'
hasBackground={false} hasBackground={false}
mutedBorder={true}
/> />
)} )}
</InfoCollapse> </InfoCollapse>
@ -552,7 +587,9 @@ const ControlPrinter = ({ slicerIntegration = false }) => {
) : ( ) : (
<MissingPlaceholder <MissingPlaceholder
message={'No filament stock.'} message={'No filament stock.'}
padding='24px'
hasBackground={false} hasBackground={false}
mutedBorder={true}
/> />
)} )}
</InfoCollapse> </InfoCollapse>

View File

@ -7,19 +7,27 @@ const { Text } = Typography
const MissingPlaceholder = ({ const MissingPlaceholder = ({
message, message,
hasBackground = true, hasBackground = true,
hasBorder = true hasBorder = true,
mutedBorder = false,
padding = '16px'
}) => { }) => {
var border = undefined
if (hasBorder == false) {
border = 'none'
} else if (mutedBorder == true && hasBorder == true) {
border = '1px solid var(--color-descriptions-border)'
}
return ( return (
<Card <Card
size='small' size='small'
style={{ style={{
background: hasBackground == false ? 'transparent' : undefined, background: hasBackground == false ? 'transparent' : undefined,
border: border: border
hasBorder == false }}
? 'none' styles={{
: hasBackground == false body: {
? '1px solid rgb(0 0 0 / 7%)' padding: padding
: undefined }
}} }}
> >
<Flex <Flex
@ -42,7 +50,9 @@ const MissingPlaceholder = ({
MissingPlaceholder.propTypes = { MissingPlaceholder.propTypes = {
message: PropTypes.string.isRequired, message: PropTypes.string.isRequired,
hasBackground: PropTypes.bool, hasBackground: PropTypes.bool,
hasBorder: PropTypes.bool hasBorder: PropTypes.bool,
mutedBorder: PropTypes.bool,
padding: PropTypes.string
} }
export default MissingPlaceholder export default MissingPlaceholder

View File

@ -13,7 +13,7 @@ import ElipsisText from './ElipsisText'
import SpotlightTooltip from './SpotlightTooltip' import SpotlightTooltip from './SpotlightTooltip'
import StateTag from './StateTag' import StateTag from './StateTag'
const { Text, Link } = Typography const { Text } = Typography
const ObjectDisplay = ({ const ObjectDisplay = ({
object, object,
@ -159,7 +159,6 @@ const ObjectDisplay = ({
<Tag <Tag
style={{ style={{
margin: 0, margin: 0,
border: 'none',
minWidth: 0, minWidth: 0,
maxWidth: '100%' maxWidth: '100%'
}} }}
@ -187,7 +186,7 @@ const ObjectDisplay = ({
hyperlink = buildActionUrl(model, defaultModelActions[0], objectId, '', '') hyperlink = buildActionUrl(model, defaultModelActions[0], objectId, '', '')
} }
// Render name with hyperlink/spotlight support // Render name with spotlight support
const renderNameDisplay = () => { const renderNameDisplay = () => {
if (!objectData?.name) return null if (!objectData?.name) return null
@ -195,36 +194,6 @@ const ObjectDisplay = ({
<ElipsisText style={{ lineHeight: '1' }}>{objectData.name}</ElipsisText> <ElipsisText style={{ lineHeight: '1' }}>{objectData.name}</ElipsisText>
) )
// If hyperlink is enabled
if (showHyperlink && hyperlink != null) {
const linkElement = (
<Link onClick={() => navigate(hyperlink)} ellipsis>
{textElement}
</Link>
)
if (showSpotlight && objectId) {
return (
<Popover
content={
<SpotlightTooltip
query={prefix + ':' + objectId}
type={objectType}
/>
}
trigger={['hover', 'click']}
placement='topLeft'
arrow={false}
style={{ padding: 0 }}
>
{linkElement}
</Popover>
)
}
return linkElement
}
// If hyperlink is disabled
if (showSpotlight && objectId) { if (showSpotlight && objectId) {
return ( return (
<Popover <Popover
@ -237,6 +206,7 @@ const ObjectDisplay = ({
trigger={['hover', 'click']} trigger={['hover', 'click']}
placement='topLeft' placement='topLeft'
arrow={false} arrow={false}
style={{ padding: 0 }}
> >
{textElement} {textElement}
</Popover> </Popover>
@ -245,20 +215,23 @@ const ObjectDisplay = ({
return textElement return textElement
} }
const canNavigate = showHyperlink && hyperlink != null
return ( return (
<Tag <Tag
style={{ style={{
margin: 0, margin: 0,
border: 'none',
minWidth: 0, minWidth: 0,
maxWidth: '100%' maxWidth: '100%',
cursor: canNavigate ? 'pointer' : undefined
}} }}
className='object-display-tag' className='object-display-tag'
onClick={canNavigate ? () => navigate(hyperlink) : undefined}
> >
<Flex <Flex
gap={objectData?.color || objectData?.state ? 'small' : '5px'} gap={objectData?.color || objectData?.state ? 'small' : '5px'}
align='center' align='center'
style={{ minWidth: 0, height: '24px' }} style={{ minWidth: 0, height: '22px' }}
> >
<Icon /> <Icon />
<Flex gap={'small'} align='center' style={{ minWidth: 0, flex: 1 }}> <Flex gap={'small'} align='center' style={{ minWidth: 0, flex: 1 }}>
@ -285,7 +258,7 @@ const ObjectDisplay = ({
type={objectType} type={objectType}
longId={false} longId={false}
showCopy={false} showCopy={false}
showHyperlink={showHyperlink} showHyperlink={false}
showSpotlight={showSpotlight} showSpotlight={showSpotlight}
/> />
) : null} ) : null}

View File

@ -17,7 +17,15 @@ const ObjectList = ({
} }
const listContents = ( const listContents = (
<Flex gap={'4px'} wrap={!scrollHorizontal} style={style} justify={'start'}> <Flex
gap={'4px'}
wrap={!scrollHorizontal}
style={{
...style,
width: 'fit-content'
}}
justify={'start'}
>
{value.map((item) => ( {value.map((item) => (
<div key={item._id} style={{ flexShrink: 0 }}> <div key={item._id} style={{ flexShrink: 0 }}>
<ObjectDisplay <ObjectDisplay
@ -31,7 +39,11 @@ const ObjectList = ({
) )
if (scrollHorizontal) { if (scrollHorizontal) {
return <ScrollBox horizontalBottomPadding={10}>{listContents}</ScrollBox> return (
<ScrollBox horizontalBottomPadding={9}>
{listContents}
</ScrollBox>
)
} else { } else {
return listContents return listContents
} }

View File

@ -944,6 +944,11 @@ const ObjectTable = forwardRef(
if (connected !== true) return if (connected !== true) return
if (subscribedTypeRef.current === type) return if (subscribedTypeRef.current === type) return
console.log(
'subscribing to object type updates',
type,
subscriptionFilterRef.current
)
const unsubscribe = subscribeToObjectTypeUpdatesFnRef.current( const unsubscribe = subscribeToObjectTypeUpdatesFnRef.current(
type, type,
subscriptionFilterRef.current, subscriptionFilterRef.current,

View File

@ -0,0 +1,65 @@
import PropTypes from 'prop-types'
import ObjectDisplay from './ObjectDisplay'
import { Steps } from 'antd'
import MissingPlaceholder from './MissingPlaceholder'
const getId = (item) => {
if (!item) return null
if (typeof item === 'string') return item
if (typeof item._id === 'string') return item._id
if (typeof item._id === 'object' && item._id?._id) return item._id._id
return null
}
const toObject = (item) => {
if (!item) return null
if (typeof item === 'string') return { _id: item }
return item
}
const PrinterQueue = ({ queue, currentSubJob }) => {
const queueItems = Array.isArray(queue)
? queue.map(toObject).filter(Boolean)
: []
const current = toObject(currentSubJob)
const currentId = getId(current)
const items = []
if (current && !queueItems.some((item) => getId(item) === currentId)) {
items.push(current)
}
items.push(...queueItems)
if (items.length === 0) {
return (
<MissingPlaceholder
message='No sub jobs in queue.'
hasBackground={false}
hasBorder={false}
padding={0}
/>
)
}
const currentIndex = currentId
? items.findIndex((item) => getId(item) === currentId)
: -1
return (
<Steps
current={currentIndex}
direction='vertical'
size='small'
items={items.map((item) => ({
title: <ObjectDisplay object={item} objectType='subJob' showHyperlink />
}))}
/>
)
}
PrinterQueue.propTypes = {
queue: PropTypes.array,
currentSubJob: PropTypes.oneOfType([PropTypes.object, PropTypes.string])
}
export default PrinterQueue

View File

@ -9,6 +9,7 @@ const ScrollBox = ({
verticalRightPadding = 16, verticalRightPadding = 16,
inner = false, inner = false,
smallPadding = false, smallPadding = false,
className = '',
...rest ...rest
}) => { }) => {
return ( return (
@ -19,7 +20,7 @@ const ScrollBox = ({
'--scrollbox-vertical-right-padding': `${verticalRightPadding}px`, '--scrollbox-vertical-right-padding': `${verticalRightPadding}px`,
'--scrollbox-horizontal-bottom-padding': `${horizontalBottomPadding}px` '--scrollbox-horizontal-bottom-padding': `${horizontalBottomPadding}px`
}} }}
className={`${inner ? 'scrollbox-inner' : ''} ${smallPadding ? 'scrollbox-inner-small-padding' : ''}`} className={`${inner ? 'scrollbox-inner' : ''} ${smallPadding ? 'scrollbox-inner-small-padding' : ''} ${className}`}
> >
<SimpleBar style={{ height: '100%', ...style }} {...rest}> <SimpleBar style={{ height: '100%', ...style }} {...rest}>
{children} {children}
@ -34,7 +35,8 @@ ScrollBox.propTypes = {
horizontalBottomPadding: PropTypes.number, horizontalBottomPadding: PropTypes.number,
verticalRightPadding: PropTypes.number, verticalRightPadding: PropTypes.number,
inner: PropTypes.bool, inner: PropTypes.bool,
smallPadding: PropTypes.bool smallPadding: PropTypes.bool,
className: PropTypes.string
} }
export default ScrollBox export default ScrollBox

View File

@ -157,6 +157,10 @@ export const ThemeProvider = ({ children }) => {
isDarkMode ? '#1f1f1f' : '#ffffff' isDarkMode ? '#1f1f1f' : '#ffffff'
) )
root.style.setProperty('--color-text', isDarkMode ? '#ffffff' : '#000000') root.style.setProperty('--color-text', isDarkMode ? '#ffffff' : '#000000')
root.style.setProperty(
'--color-descriptions-border',
isDarkMode ? 'rgba(253,253,253,0.12)' : 'rgba(5,5,5,0.06)'
)
}, [isDarkMode, primaryColorOverride]) }, [isDarkMode, primaryColorOverride])
const themeConfig = { const themeConfig = {

View File

@ -88,9 +88,10 @@ export const DocumentJob = {
content: () => createElement(DocumentJobInfo) content: () => createElement(DocumentJobInfo)
} }
], ],
columns: ['_reference', 'name', 'state', 'createdAt', 'updatedAt'], columns: ['_reference', 'name', 'quantity', 'state', 'createdAt', 'updatedAt'],
filters: [ filters: [
'name', 'name',
'quantity',
'width', 'width',
'height', 'height',
'state', 'state',
@ -98,7 +99,7 @@ export const DocumentJob = {
'updatedAt', 'updatedAt',
'_reference' '_reference'
], ],
sorters: ['name', 'state', 'createdAt', 'updatedAt'], sorters: ['name', 'quantity', 'state', 'createdAt', 'updatedAt'],
properties: [ properties: [
{ {
name: '_id', name: '_id',
@ -203,6 +204,20 @@ export const DocumentJob = {
online: true online: true
} }
} }
},
{
name: 'quantity',
label: 'Quantity',
type: 'number',
required: true,
min: 1,
step: 1,
columnWidth: 125,
value: (objectData) => {
if (objectData?.createdAt == undefined && objectData?.quantity == null) {
return 1
}
}
} }
] ]
} }