Compare commits

...

13 Commits

Author SHA1 Message Date
cd83679232 Add support for additional property types in ObjectProperty component, including variance, markdown, and object list displays. Enhance prop types to accommodate functions for prefix, suffix, and object type. Update ObjectInfo to pass custom property props. 2025-07-14 23:09:56 +01:00
38f03f8fe9 Refactor NotesPanel component to integrate ApiServerContext for fetching notes, add support for parent type in new note creation, and streamline note handling logic. 2025-07-14 23:09:21 +01:00
9a1f58aafe Add ObjectList component to display a list of objects with icons and IDs, enhancing the dashboard's UI. 2025-07-14 23:08:51 +01:00
a0ab5be6f2 Add danger property to action items in ObjectActions component and refine action filtering logic 2025-07-14 23:08:33 +01:00
5aa7355b0f Enhance PropertyChanges component to handle cases where both old and new values are absent, and improve rendering logic for displaying property changes. 2025-07-14 23:08:18 +01:00
a505e1aaba Add OperationDisplay component to visually represent operation types with icons and tags 2025-07-14 23:07:24 +01:00
a5458c6b67 Refactor InfoCollapse component to use collapseKey prop for active state management and improve structure 2025-07-14 23:07:15 +01:00
3bd2628960 Minor UI fix 2025-07-14 23:05:52 +01:00
d46402983f Add DeleteObjectModal component for confirming deletion of objects 2025-07-14 23:05:37 +01:00
b6c2cb22f4 Add VarianceDisplay component and update StockEvent model for variance type 2025-07-14 23:05:10 +01:00
0a897e663c Removed unnecessary wrapper elements and improving readability. 2025-07-14 23:04:44 +01:00
3ad0002bbb Moved more common functions into ApiServerContext 2025-07-14 23:04:18 +01:00
b71537dc64 Updated models 2025-07-14 23:02:26 +01:00
18 changed files with 921 additions and 335 deletions

View File

@ -0,0 +1,68 @@
import React from 'react'
import PropTypes from 'prop-types'
import { Modal, Button, Space, Typography } from 'antd'
import { getModelByName } from '../../../database/ObjectModels'
import BinIcon from '../../Icons/BinIcon'
const { Text } = Typography
const DeleteObjectModal = ({
open,
onOk,
onCancel,
loading,
objectType,
objectName
}) => {
const model = getModelByName(objectType)
return (
<Modal
open={open}
title={
<Space size={'middle'}>
<BinIcon />
{`Confirm Delete ${model.label}`}
</Space>
}
onOk={onOk}
onCancel={onCancel}
okText='Delete'
cancelText='Cancel'
okType='danger'
closable={false}
centered
maskClosable={false}
footer={[
<Button key='cancel' onClick={onCancel} disabled={loading}>
Cancel
</Button>,
<Button
key='delete'
type='primary'
danger
onClick={onOk}
loading={loading}
disabled={loading}
>
Delete
</Button>
]}
>
<Text>
Are you sure you want to delete this {model.label.toLowerCase()}
{objectName ? ` "${objectName}"` : ''}?
</Text>
</Modal>
)
}
DeleteObjectModal.propTypes = {
open: PropTypes.bool.isRequired,
onOk: PropTypes.func.isRequired,
onCancel: PropTypes.func.isRequired,
loading: PropTypes.bool,
objectType: PropTypes.string.isRequired,
objectName: PropTypes.string
}
export default DeleteObjectModal

View File

@ -49,7 +49,7 @@ const IdDisplay = ({
} }
return ( return (
<Flex align={'center'} className='iddisplay'> <Flex align={'end'} className='iddisplay'>
{(() => { {(() => {
const content = ( const content = (
<Flex gap={4}> <Flex gap={4}>

View File

@ -12,33 +12,33 @@ const InfoCollapse = ({
active, active,
onToggle, onToggle,
className = '', className = '',
key = 'default' collapseKey = 'default'
}) => { }) => {
return ( return (
<Collapse <Collapse
ghost ghost
expandIconPosition='end' expandIconPosition='end'
activeKey={active ? [key] : []} activeKey={active ? [collapseKey] : []}
onChange={(keys) => onToggle(keys.length > 0)} onChange={(keys) => onToggle(keys.length > 0)}
expandIcon={({ isActive }) => ( expandIcon={({ isActive }) => (
<CaretLeftOutlined rotate={isActive ? -90 : 0} /> <CaretLeftOutlined rotate={isActive ? -90 : 0} />
)} )}
className={`no-h-padding-collapse ${className}`} className={`no-h-padding-collapse ${className}`}
> items={[
<Collapse.Panel {
header={ key: collapseKey,
children: children,
label: (
<Flex align='center' gap={'middle'}> <Flex align='center' gap={'middle'}>
{icon} {icon}
<Title level={5} style={{ margin: 0 }}> <Title level={5} style={{ margin: 0 }}>
{title} {title}
</Title> </Title>
</Flex> </Flex>
)
} }
key={key} ]}
> />
{children}
</Collapse.Panel>
</Collapse>
) )
} }
@ -49,7 +49,7 @@ InfoCollapse.propTypes = {
active: PropTypes.bool.isRequired, active: PropTypes.bool.isRequired,
onToggle: PropTypes.func.isRequired, onToggle: PropTypes.func.isRequired,
className: PropTypes.string, className: PropTypes.string,
key: PropTypes.string collapseKey: PropTypes.string
} }
export default InfoCollapse export default InfoCollapse

View File

@ -26,6 +26,7 @@ import MarkdownDisplay from './MarkdownDisplay'
import axios from 'axios' import axios from 'axios'
import config from '../../../config' import config from '../../../config'
import { AuthContext } from '../context/AuthContext' import { AuthContext } from '../context/AuthContext'
import { ApiServerContext } from '../context/ApiServerContext'
import InfoCircleIcon from '../../Icons/InfoCircleIcon' import InfoCircleIcon from '../../Icons/InfoCircleIcon'
import NoteTypeSelect from './NoteTypeSelect' import NoteTypeSelect from './NoteTypeSelect'
import IdDisplay from './IdDisplay' import IdDisplay from './IdDisplay'
@ -259,7 +260,7 @@ NoteItem.propTypes = {
onChildNoteAdded: PropTypes.func onChildNoteAdded: PropTypes.func
} }
const NotesPanel = ({ _id, onNewNote }) => { const NotesPanel = ({ _id, onNewNote, type }) => {
const [newNoteOpen, setNewNoteOpen] = useState(false) const [newNoteOpen, setNewNoteOpen] = useState(false)
const [showMarkdown, setShowMarkdown] = useState(false) const [showMarkdown, setShowMarkdown] = useState(false)
const [loading, setLoading] = useState(true) const [loading, setLoading] = useState(true)
@ -273,6 +274,7 @@ const NotesPanel = ({ _id, onNewNote }) => {
const [expandedNotes, setExpandedNotes] = useState({}) const [expandedNotes, setExpandedNotes] = useState({})
const [newNoteForm] = Form.useForm() const [newNoteForm] = Form.useForm()
const [selectedParentId, setSelectedParentId] = useState(null) const [selectedParentId, setSelectedParentId] = useState(null)
const [selectedParentType, setSelectedParentType] = useState(null)
const [childNoteCallbacks, setChildNoteCallbacks] = useState({}) const [childNoteCallbacks, setChildNoteCallbacks] = useState({})
const [deleteConfirmOpen, setDeleteConfirmOpen] = useState(false) const [deleteConfirmOpen, setDeleteConfirmOpen] = useState(false)
const [noteToDelete, setNoteToDelete] = useState(null) const [noteToDelete, setNoteToDelete] = useState(null)
@ -289,22 +291,12 @@ const NotesPanel = ({ _id, onNewNote }) => {
}, [newNoteForm, newNoteFormUpdateValues]) }, [newNoteForm, newNoteFormUpdateValues])
const { authenticated, userProfile } = useContext(AuthContext) const { authenticated, userProfile } = useContext(AuthContext)
const { fetchNotes } = useContext(ApiServerContext)
const fetchData = useCallback(async (id) => { const fetchData = useCallback(
async (id) => {
try { try {
const response = await axios.get(`${config.backendUrl}/notes`, { const newData = await fetchNotes(id)
params: {
parent: id,
sort: 'createdAt',
order: 'ascend'
},
headers: {
Accept: 'application/json'
},
withCredentials: true
})
const newData = response.data
setLoading(false) setLoading(false)
return newData return newData
} catch (error) { } catch (error) {
@ -312,7 +304,20 @@ const NotesPanel = ({ _id, onNewNote }) => {
setError(error) setError(error)
setLoading(false) setLoading(false)
} }
}, []) },
[fetchNotes]
)
const handleNewChildNote = useCallback(
(parentId) => {
setSelectedParentId(parentId)
setSelectedParentType('note')
setNewNoteOpen(true)
newNoteForm.resetFields()
setNewNoteFormValues({})
},
[newNoteForm]
)
const generateNotes = useCallback( const generateNotes = useCallback(
async (id) => { async (id) => {
@ -345,7 +350,7 @@ const NotesPanel = ({ _id, onNewNote }) => {
expandedNotes={expandedNotes} expandedNotes={expandedNotes}
setExpandedNotes={setExpandedNotes} setExpandedNotes={setExpandedNotes}
fetchData={fetchData} fetchData={fetchData}
onNewNote={handleNewNoteFromDropdown} onNewNote={handleNewChildNote}
onDeleteNote={handleDeleteNote} onDeleteNote={handleDeleteNote}
userProfile={userProfile} userProfile={userProfile}
onChildNoteAdded={(noteId, callback) => { onChildNoteAdded={(noteId, callback) => {
@ -357,7 +362,7 @@ const NotesPanel = ({ _id, onNewNote }) => {
/> />
)) ))
}, },
[loading, fetchData, expandedNotes, userProfile] [loading, fetchData, expandedNotes, userProfile, handleNewChildNote]
) )
const handleNewNote = async () => { const handleNewNote = async () => {
@ -365,7 +370,11 @@ const NotesPanel = ({ _id, onNewNote }) => {
try { try {
await axios.post( await axios.post(
`${config.backendUrl}/notes`, `${config.backendUrl}/notes`,
{ ...newNoteFormValues, parent: selectedParentId || _id }, {
...newNoteFormValues,
parent: selectedParentId,
parentType: selectedParentType
},
{ {
withCredentials: true withCredentials: true
} }
@ -402,13 +411,6 @@ const NotesPanel = ({ _id, onNewNote }) => {
} }
} }
const handleNewNoteFromDropdown = (parentId) => {
setSelectedParentId(parentId)
setNewNoteOpen(true)
newNoteForm.resetFields()
setNewNoteFormValues({})
}
const handleDeleteNote = async (noteId) => { const handleDeleteNote = async (noteId) => {
setNoteToDelete(noteId) setNoteToDelete(noteId)
setDeleteConfirmOpen(true) setDeleteConfirmOpen(true)
@ -499,7 +501,8 @@ const NotesPanel = ({ _id, onNewNote }) => {
setLoading(true) setLoading(true)
handleReloadData() handleReloadData()
} else if (key === 'newNote') { } else if (key === 'newNote') {
setSelectedParentId(null) setSelectedParentId(_id)
setSelectedParentType(type)
setNewNoteOpen(true) setNewNoteOpen(true)
newNoteForm.resetFields() newNoteForm.resetFields()
setNewNoteFormValues({}) setNewNoteFormValues({})
@ -522,7 +525,8 @@ const NotesPanel = ({ _id, onNewNote }) => {
icon={<PlusIcon />} icon={<PlusIcon />}
disabled={loading} disabled={loading}
onClick={() => { onClick={() => {
setSelectedParentId(null) setSelectedParentId(_id)
setSelectedParentType(type)
setNewNoteOpen(true) setNewNoteOpen(true)
newNoteForm.resetFields() newNoteForm.resetFields()
setNewNoteFormValues({}) setNewNoteFormValues({})
@ -688,6 +692,7 @@ const NotesPanel = ({ _id, onNewNote }) => {
NotesPanel.propTypes = { NotesPanel.propTypes = {
_id: PropTypes.string.isRequired, _id: PropTypes.string.isRequired,
type: PropTypes.string.isRequired,
onNewNote: PropTypes.func onNewNote: PropTypes.func
} }

View File

@ -14,6 +14,7 @@ function mapActionsToMenuItems(actions, currentUrlWithActions, id) {
const item = { const item = {
key: action.key || action.name, key: action.key || action.name,
label: action.label, label: action.label,
danger: action?.danger || false,
icon: action.icon ? React.createElement(action.icon) : undefined, icon: action.icon ? React.createElement(action.icon) : undefined,
disabled: actionUrl && actionUrl === currentUrlWithActions disabled: actionUrl && actionUrl === currentUrlWithActions
} }
@ -53,11 +54,10 @@ const ObjectActions = ({
location.search location.search
) )
console.log('curr url', currentUrlWithoutActions)
// Filter out actions whose url matches currentUrl
const filteredActions = actions.filter( const filteredActions = actions.filter(
(action) => !(action.url(id) && action.url(id) === currentUrlWithoutActions) (action) =>
typeof action.url !== 'function' ||
action.url(id) !== currentUrlWithoutActions
) )
const currentUrlWithActions = location.pathname + location.search const currentUrlWithActions = location.pathname + location.search

View File

@ -13,6 +13,7 @@ const ObjectInfo = ({
properties = [], properties = [],
required = undefined, required = undefined,
visibleProperties = {}, visibleProperties = {},
objectPropertyProps = {},
...rest ...rest
}) => { }) => {
const allItems = getModelProperties(type) const allItems = getModelProperties(type)
@ -55,6 +56,7 @@ const ObjectInfo = ({
children: ( children: (
<ObjectProperty <ObjectProperty
{...item} {...item}
{...objectPropertyProps}
isEditing={isEditing} isEditing={isEditing}
objectData={objectData} objectData={objectData}
/> />
@ -91,7 +93,8 @@ ObjectInfo.propTypes = {
type: PropTypes.string.isRequired, type: PropTypes.string.isRequired,
objectData: PropTypes.object, objectData: PropTypes.object,
required: PropTypes.bool, required: PropTypes.bool,
visibleProperties: PropTypes.object visibleProperties: PropTypes.object,
objectPropertyProps: PropTypes.object
} }
export default ObjectInfo export default ObjectInfo

View File

@ -0,0 +1,49 @@
import React from 'react'
import PropTypes from 'prop-types'
import { List, Typography, Flex } from 'antd'
import { getModelByName } from '../../../database/ObjectModels'
import IdDisplay from './IdDisplay'
const { Text } = Typography
const ObjectList = ({ value, objectType, bordered = true }) => {
if (!value || !Array.isArray(value) || value.length === 0) {
return <Text type='secondary'>n/a</Text>
}
return (
<List
size='small'
bordered={bordered}
dataSource={value}
renderItem={(item) => {
const model = getModelByName(objectType)
const Icon = model.icon
return (
<List.Item>
<Flex gap={'small'} align='center'>
<Icon />
{item?.name ? <Text ellipsis>{item.name}</Text> : null}
{item?._id ? (
<IdDisplay
id={item?._id}
longId={false}
type={objectType}
showHyperlink={true}
/>
) : null}
</Flex>
</List.Item>
)
}}
style={{ width: '100%' }}
/>
)
}
ObjectList.propTypes = {
value: PropTypes.array,
bordered: PropTypes.bool,
objectType: PropTypes.string
}
export default ObjectList

View File

@ -37,6 +37,10 @@ import { getPropertyValue } from '../../../database/ObjectModels'
import PropertyChanges from './PropertyChanges' import PropertyChanges from './PropertyChanges'
import NetGrossDisplay from './NetGrossDisplay' import NetGrossDisplay from './NetGrossDisplay'
import NetGrossInput from './NetGrossInput' import NetGrossInput from './NetGrossInput'
import ObjectList from './ObjectList'
import VarianceDisplay from './VarianceDisplay'
import OperationDisplay from './OperationDisplay'
import MarkdownDisplay from './MarkdownDisplay'
const { Text } = Typography const { Text } = Typography
@ -71,7 +75,7 @@ const ObjectProperty = ({
initial = false, initial = false,
...rest ...rest
}) => { }) => {
if (typeof value == 'function' && objectData) { if (value && typeof value == 'function' && objectData) {
value = value(objectData) value = value(objectData)
} }
@ -87,6 +91,14 @@ const ObjectProperty = ({
difference = difference(objectData) difference = difference(objectData)
} }
if (prefix && typeof prefix == 'function' && objectData) {
prefix = prefix(objectData)
}
if (suffix && typeof suffix == 'function' && objectData) {
suffix = suffix(objectData)
}
if (!value) { if (!value) {
value = getPropertyValue(objectData, name) value = getPropertyValue(objectData, name)
} }
@ -218,6 +230,19 @@ const ObjectProperty = ({
) )
} }
} }
case 'variance': {
if (value != null) {
return (
<VarianceDisplay value={value} prefix={prefix} suffix={suffix} />
)
} else {
return (
<Text type='secondary' {...textParams}>
n/a
</Text>
)
}
}
case 'text': case 'text':
if (value != null && value != '') { if (value != null && value != '') {
return ( return (
@ -234,6 +259,16 @@ const ObjectProperty = ({
</Text> </Text>
) )
} }
case 'markdown':
if (value != null && value != '') {
return <MarkdownDisplay content={value} />
} else {
return (
<Text type='secondary' {...textParams}>
n/a
</Text>
)
}
case 'email': case 'email':
if (value != null && value != '') { if (value != null && value != '') {
return <EmailDisplay email={value} /> return <EmailDisplay email={value} />
@ -265,6 +300,9 @@ const ObjectProperty = ({
) )
} }
} }
case 'objectList': {
return <ObjectList value={value} objectType={objectType} />
}
case 'state': { case 'state': {
if (value && value?.type) { if (value && value?.type) {
switch (objectType) { switch (objectType) {
@ -346,6 +384,17 @@ const ObjectProperty = ({
) )
} }
} }
case 'operation': {
if (value != null) {
return <OperationDisplay operation={value} />
} else {
return (
<Text type='secondary' {...textParams}>
n/a
</Text>
)
}
}
case 'propertyChanges': { case 'propertyChanges': {
return <PropertyChanges type={objectType} value={value} /> return <PropertyChanges type={objectType} value={value} />
} }
@ -555,7 +604,7 @@ const ObjectProperty = ({
<PrinterSelect placeholder={label} /> <PrinterSelect placeholder={label} />
</Form.Item> </Form.Item>
) )
case 'gcodefile': case 'gcodeFile':
return ( return (
<Form.Item name={formItemName} {...mergedFormItemProps}> <Form.Item name={formItemName} {...mergedFormItemProps}>
<GCodeFileSelect placeholder={label} /> <GCodeFileSelect placeholder={label} />
@ -603,20 +652,22 @@ const ObjectProperty = ({
ObjectProperty.propTypes = { ObjectProperty.propTypes = {
type: PropTypes.string.isRequired, type: PropTypes.string.isRequired,
value: PropTypes.any, value: PropTypes.oneOfType([PropTypes.any, PropTypes.func]),
isEditing: PropTypes.bool, isEditing: PropTypes.bool,
formItemProps: PropTypes.object, formItemProps: PropTypes.object,
required: PropTypes.bool, required: PropTypes.bool,
name: PropTypes.string, name: PropTypes.string,
prefix: PropTypes.string, prefix: PropTypes.oneOfType([PropTypes.string, PropTypes.func]),
suffix: PropTypes.string, suffix: PropTypes.oneOfType([PropTypes.string, PropTypes.func]),
min: PropTypes.number, min: PropTypes.number,
max: PropTypes.number, max: PropTypes.number,
step: PropTypes.number, step: PropTypes.number,
showLabel: PropTypes.bool, showLabel: PropTypes.bool,
objectType: PropTypes.string, objectType: PropTypes.oneOfType([PropTypes.string, PropTypes.func]),
readOnly: PropTypes.bool, readOnly: PropTypes.bool,
disabled: PropTypes.bool disabled: PropTypes.oneOfType([PropTypes.bool, PropTypes.func]),
difference: PropTypes.oneOfType([PropTypes.any, PropTypes.func]),
objectData: PropTypes.object
} }
export default ObjectProperty export default ObjectProperty

View File

@ -0,0 +1,57 @@
import React from 'react'
import PropTypes from 'prop-types'
import { Space, Tag } from 'antd'
import QuestionCircleIcon from '../../Icons/QuestionCircleIcon'
import PlusIcon from '../../Icons/PlusIcon'
import BinIcon from '../../Icons/BinIcon'
import EditIcon from '../../Icons/EditIcon'
const OperationDisplay = ({
operation,
showIcon = true,
showText = true,
showColor = true
}) => {
var tagText = 'False'
var tagIcon = <QuestionCircleIcon />
var tagColor = 'default'
switch (operation) {
case 'new':
tagText = 'New'
tagIcon = <PlusIcon />
tagColor = 'success'
break
case 'delete':
tagText = 'Delete'
tagIcon = <BinIcon />
tagColor = 'error'
break
case 'edit':
tagText = 'Edit'
tagIcon = <EditIcon />
tagColor = 'blue'
break
default:
break
}
return (
<Space>
<Tag
style={{ margin: 0 }}
color={showColor ? tagColor : 'default'}
icon={showIcon ? tagIcon : undefined}
>
{showText ? tagText : null}
</Tag>
</Space>
)
}
OperationDisplay.propTypes = {
operation: PropTypes.bool.isRequired,
showIcon: PropTypes.bool,
showText: PropTypes.bool,
showColor: PropTypes.bool
}
export default OperationDisplay

View File

@ -8,7 +8,7 @@ import ArrowRightIcon from '../../Icons/ArrowRightIcon'
const { Text } = Typography const { Text } = Typography
const PropertyChanges = ({ type, value }) => { const PropertyChanges = ({ type, value }) => {
if (!value || !value.new) { if (!value || (!value.new && !value.old)) {
return <Text type='secondary'>n/a</Text> return <Text type='secondary'>n/a</Text>
} }
@ -18,7 +18,15 @@ const PropertyChanges = ({ type, value }) => {
if (Object.prototype.hasOwnProperty.call(obj, key)) { if (Object.prototype.hasOwnProperty.call(obj, key)) {
const val = obj[key] const val = obj[key]
const newKey = prefix ? `${prefix}.${key}` : key const newKey = prefix ? `${prefix}.${key}` : key
if (val && typeof val === 'object' && !Array.isArray(val)) {
// Don't flatten keys that are "state" or "netGross"
if (
val &&
typeof val === 'object' &&
!Array.isArray(val) &&
key !== 'state' &&
key !== 'netGross'
) {
flattenObject(val, newKey, res) flattenObject(val, newKey, res)
} else { } else {
res[newKey] = val res[newKey] = val
@ -34,7 +42,6 @@ const PropertyChanges = ({ type, value }) => {
...flatOld, ...flatOld,
...flatNew ...flatNew
} }
console.log('combined', combinedChanges)
return ( return (
<Descriptions size='small' column={1}> <Descriptions size='small' column={1}>
{Object.keys(combinedChanges).map((key) => { {Object.keys(combinedChanges).map((key) => {
@ -46,19 +53,25 @@ const PropertyChanges = ({ type, value }) => {
return ( return (
<Descriptions.Item key={key} label={changeProperty.label}> <Descriptions.Item key={key} label={changeProperty.label}>
<Space> <Space>
{value?.old ? (
<ObjectProperty <ObjectProperty
{...changeProperty} {...changeProperty}
longId={false} longId={false}
objectData={value?.old} objectData={value?.old}
/> />
) : null}
{value?.old && value?.new ? (
<Text type='secondary'> <Text type='secondary'>
<ArrowRightIcon /> <ArrowRightIcon />
</Text> </Text>
) : null}
{value?.new ? (
<ObjectProperty <ObjectProperty
{...changeProperty} {...changeProperty}
longId={false} longId={false}
objectData={value?.new} objectData={value?.new}
/> />
) : null}
</Space> </Space>
</Descriptions.Item> </Descriptions.Item>
) )

View File

@ -0,0 +1,32 @@
import React from 'react'
import { Typography } from 'antd'
import PropTypes from 'prop-types'
const { Text } = Typography
const VarianceDisplay = ({ value, prefix, suffix }) => {
if (value === null || value === undefined) {
return <Text type='secondary'>n/a</Text>
}
const isPositive = value > 0
const isNegative = value < 0
const displayValue = Math.abs(value).toFixed(2)
return (
<Text type={isPositive ? 'success' : isNegative ? 'danger' : undefined}>
{prefix || ''}
{isPositive ? '+' : isNegative ? '-' : ''}
{displayValue}
{suffix || ''}
</Text>
)
}
VarianceDisplay.propTypes = {
value: PropTypes.number,
prefix: PropTypes.string,
suffix: PropTypes.string
}
export default VarianceDisplay

View File

@ -4,7 +4,8 @@ import React, {
useEffect, useEffect,
useState, useState,
useContext, useContext,
useRef useRef,
useCallback
} from 'react' } from 'react'
import io from 'socket.io-client' import io from 'socket.io-client'
import { message, notification, Modal, Space, Button } from 'antd' import { message, notification, Modal, Space, Button } from 'antd'
@ -22,8 +23,9 @@ logger.setLevel(config.logLevel)
const ApiServerContext = createContext() const ApiServerContext = createContext()
const ApiServerProvider = ({ children }) => { const ApiServerProvider = ({ children }) => {
const { token, userProfile } = useContext(AuthContext) const { token, userProfile, authenticated } = useContext(AuthContext)
const socketRef = useRef(null) const socketRef = useRef(null)
const [connected, setConnected] = useState(false)
const [connecting, setConnecting] = useState(false) const [connecting, setConnecting] = useState(false)
const [error, setError] = useState(null) const [error, setError] = useState(null)
const [messageApi, contextHolder] = message.useMessage() const [messageApi, contextHolder] = message.useMessage()
@ -32,9 +34,43 @@ const ApiServerProvider = ({ children }) => {
const [showErrorModal, setShowErrorModal] = useState(false) const [showErrorModal, setShowErrorModal] = useState(false)
const [errorModalContent, setErrorModalContent] = useState('') const [errorModalContent, setErrorModalContent] = useState('')
const [retryCallback, setRetryCallback] = useState(null) const [retryCallback, setRetryCallback] = useState(null)
const subscribedCallbacksRef = useRef(new Map())
const subscribedLockCallbacksRef = useRef(new Map())
useEffect(() => { const notifyLockUpdate = useCallback(
if (token) { async (lockData) => {
logger.debug('Notifying lock update:', lockData)
const objectId = lockData._id || lockData.id
if (
objectId &&
subscribedLockCallbacksRef.current.has(objectId) &&
lockData.user != userProfile?._id
) {
const callbacks = subscribedLockCallbacksRef.current.get(objectId)
logger.debug(
`Calling ${callbacks.length} lock callbacks for object:`,
objectId
)
callbacks.forEach((callback) => {
try {
callback(lockData)
} catch (error) {
logger.error('Error in lock update callback:', error)
}
})
} else {
logger.debug(
`No lock callbacks found for object: ${objectId}, subscribed lock callbacks:`,
Array.from(subscribedLockCallbacksRef.current.keys())
)
}
},
[userProfile?._id]
)
const connectToServer = useCallback(() => {
if (token && authenticated == true) {
logger.debug('Token is available, connecting to api server...') logger.debug('Token is available, connecting to api server...')
const newSocket = io(config.apiServerUrl, { const newSocket = io(config.apiServerUrl, {
@ -48,18 +84,25 @@ const ApiServerProvider = ({ children }) => {
newSocket.on('connect', () => { newSocket.on('connect', () => {
logger.debug('Api Server connected') logger.debug('Api Server connected')
setConnecting(false) setConnecting(false)
setConnected(true)
setError(null) setError(null)
}) })
newSocket.on('notify_object_update', notifyObjectUpdate)
newSocket.on('notify_object_new', notifyObjectNew)
newSocket.on('notify_lock_update', notifyLockUpdate)
newSocket.on('disconnect', () => { newSocket.on('disconnect', () => {
logger.debug('Api Server disconnected') logger.debug('Api Server disconnected')
setError('Api Server disconnected') setError('Api Server disconnected')
setConnected(false)
}) })
newSocket.on('connect_error', (err) => { newSocket.on('connect_error', (err) => {
logger.error('Api Server connection error:', err) logger.error('Api Server connection error:', err)
messageApi.error('Api Server connection error: ' + err.message) messageApi.error('Api Server connection error: ' + err.message)
setError('Api Server connection error') setError('Api Server connection error')
setConnected(false)
}) })
newSocket.on('bridge.notification', (data) => { newSocket.on('bridge.notification', (data) => {
@ -75,6 +118,17 @@ const ApiServerProvider = ({ children }) => {
}) })
socketRef.current = newSocket socketRef.current = newSocket
}
}, [token, authenticated, messageApi, notificationApi, notifyLockUpdate])
useEffect(() => {
if (token && authenticated == true) {
connectToServer()
} else if (!token && socketRef.current) {
logger.debug('Token not available, disconnecting api server...')
socketRef.current.disconnect()
socketRef.current = null
}
// Clean up function // Clean up function
return () => { return () => {
@ -84,12 +138,7 @@ const ApiServerProvider = ({ children }) => {
socketRef.current = null socketRef.current = null
} }
} }
} else if (!token && socketRef.current) { }, [token, authenticated, connectToServer])
logger.debug('Token not available, disconnecting api server...')
socketRef.current.disconnect()
socketRef.current = null
}
}, [token, messageApi])
const lockObject = (id, type) => { const lockObject = (id, type) => {
logger.debug('Locking ' + id) logger.debug('Locking ' + id)
@ -118,8 +167,12 @@ const ApiServerProvider = ({ children }) => {
type: type type: type
}, },
(lockEvent) => { (lockEvent) => {
logger.debug('Received lock event for object:', id, lockEvent) logger.debug('Received lock status for object:', id, lockEvent)
if (lockEvent.user != userProfile?._id) {
resolve(lockEvent) resolve(lockEvent)
} else {
resolve(null)
}
} }
) )
logger.debug('Sent fetch lock command for object:', id) logger.debug('Sent fetch lock command for object:', id)
@ -127,65 +180,186 @@ const ApiServerProvider = ({ children }) => {
} }
} }
const onLockEvent = (id, callback) => { const notifyObjectUpdate = async (object) => {
if (socketRef.current && socketRef.current.connected == true) { logger.debug('Notifying object update:', object)
const eventHandler = (data) => { const objectId = object._id || object.id
if (data._id === id && data?.user !== userProfile._id) {
if (objectId && subscribedCallbacksRef.current.has(objectId)) {
const callbacks = subscribedCallbacksRef.current.get(objectId)
logger.debug( logger.debug(
'Lock update received for object:', `Calling ${callbacks.length} callbacks for object:`,
id, objectId
'locked:',
data.locked
) )
callback(data) callbacks.forEach((callback) => {
try {
callback(object)
} catch (error) {
logger.error('Error in object update callback:', error)
} }
} })
} else {
socketRef.current.on('notify_lock_update', eventHandler)
logger.debug('Registered lock event listener for object:', id)
// Return cleanup function
return () => offLockEvent(id, eventHandler)
}
}
const offLockEvent = (id, eventHandler) => {
if (socketRef.current && socketRef.current.connected == true) {
socketRef.current.off('notify_lock_update', eventHandler)
logger.debug('Removed lock event listener for object:', id)
}
}
const onUpdateEvent = (id, callback) => {
if (socketRef.current && socketRef.current.connected == true) {
const eventHandler = (data) => {
if (data._id === id && data?.user !== userProfile._id) {
logger.debug( logger.debug(
'Update event received for object:', `No callbacks found for object: ${objectId}, subscribed callbacks:`,
id, Array.from(subscribedCallbacksRef.current.keys())
'updatedAt:',
data.updatedAt
) )
callback(data)
} }
} }
socketRef.current.on('notify_object_update', eventHandler) const notifyObjectNew = async (object) => {
logger.debug('Notifying object new:', object)
const objectType = object.type || 'unknown'
if (objectType && subscribedCallbacksRef.current.has(objectType)) {
const callbacks = subscribedCallbacksRef.current.get(objectType)
logger.debug(
`Calling ${callbacks.length} callbacks for type:`,
objectType
)
callbacks.forEach((callback) => {
try {
callback(object)
} catch (error) {
logger.error('Error in object new callback:', error)
}
})
} else {
logger.debug(
`No callbacks found for object: ${objectType}, subscribed callbacks:`,
Array.from(subscribedCallbacksRef.current.keys())
)
}
}
const offUpdateEvent = useCallback((id, type, callback) => {
if (socketRef.current && socketRef.current.connected == true) {
// Remove callback from the subscribed callbacks map
if (subscribedCallbacksRef.current.has(id)) {
const callbacks = subscribedCallbacksRef.current
.get(id)
.filter((cb) => cb !== callback)
if (callbacks.length === 0) {
subscribedCallbacksRef.current.delete(id)
socketRef.current.emit('unsubscribe', { id: id, type: type })
} else {
subscribedCallbacksRef.current.set(id, callbacks)
}
}
logger.debug('Removed update event listener for object:', id)
}
}, [])
const offTypeEvent = useCallback((type, callback) => {
if (socketRef.current && socketRef.current.connected == true) {
// Remove callback from the subscribed callbacks map
if (subscribedCallbacksRef.current.has(type)) {
const callbacks = subscribedCallbacksRef.current
.get(type)
.filter((cb) => cb !== callback)
if (callbacks.length === 0) {
subscribedCallbacksRef.current.delete(type)
socketRef.current.emit('unsubscribe', { type: type })
} else {
subscribedCallbacksRef.current.set(type, callbacks)
}
}
logger.debug('Removed new event listener for type:', type)
}
}, [])
const subscribeToObject = useCallback(
(id, type, callback) => {
logger.debug('Subscribing to object:', id, 'type:', type)
if (socketRef.current && socketRef.current.connected == true) {
// Add callback to the subscribed callbacks map immediately
if (!subscribedCallbacksRef.current.has(id)) {
subscribedCallbacksRef.current.set(id, [])
}
subscribedCallbacksRef.current.get(id).push(callback)
logger.debug(
`Added callback for object ${id}, total callbacks: ${subscribedCallbacksRef.current.get(id).length}`
)
socketRef.current.emit('subscribe', { id: id, type: type })
logger.debug('Registered update event listener for object:', id) logger.debug('Registered update event listener for object:', id)
// Return cleanup function // Return cleanup function
return () => offUpdateEvent(id, eventHandler) return () => offUpdateEvent(id, type, callback)
}
} }
},
[offUpdateEvent]
)
const offUpdateEvent = (id, eventHandler) => { const subscribeToType = useCallback(
(type, callback) => {
logger.debug('Subscribing to type:', type)
if (socketRef.current && socketRef.current.connected == true) { if (socketRef.current && socketRef.current.connected == true) {
socketRef.current.off('notify_update', eventHandler) // Add callback to the subscribed callbacks map immediately
logger.debug('Removed update event listener for object:', id) if (!subscribedCallbacksRef.current.has(type)) {
subscribedCallbacksRef.current.set(type, [])
}
subscribedCallbacksRef.current.get(type).push(callback)
logger.debug(
`Added callback for type ${type}, total callbacks: ${subscribedCallbacksRef.current.get(type).length}`
)
socketRef.current.emit('subscribe', { type: type })
logger.debug('Registered update event listener for object:', type)
// Return cleanup function
return () => offTypeEvent(type, callback)
}
},
[offTypeEvent]
)
const offLockEvent = useCallback((id, callback) => {
if (socketRef.current && socketRef.current.connected == true) {
// Remove callback from the subscribed lock callbacks map
if (subscribedLockCallbacksRef.current.has(id)) {
const callbacks = subscribedLockCallbacksRef.current
.get(id)
.filter((cb) => cb !== callback)
if (callbacks.length === 0) {
subscribedLockCallbacksRef.current.delete(id)
} else {
subscribedLockCallbacksRef.current.set(id, callbacks)
} }
} }
const showError = (content, callback = null) => { logger.debug('Removed lock event listener for object:', id)
}
}, [])
const subscribeToLock = useCallback(
(id, type, callback) => {
logger.debug('Subscribing to lock for object:', id, 'type:', type)
if (socketRef.current && socketRef.current.connected == true) {
// Add callback to the subscribed lock callbacks map immediately
if (!subscribedLockCallbacksRef.current.has(id)) {
subscribedLockCallbacksRef.current.set(id, [])
}
subscribedLockCallbacksRef.current.get(id).push(callback)
logger.debug(
`Added lock callback for object ${id}, total lock callbacks: ${subscribedLockCallbacksRef.current.get(id).length}`
)
socketRef.current.emit('subscribe_lock', { id: id, type: type })
logger.debug('Registered lock event listener for object:', id)
// Return cleanup function
return () => offLockEvent(id, callback)
}
},
[offLockEvent]
)
const showError = (error, callback = null) => {
var content = `Error ${error.code} (${error.status}): ${error.message}`
if (error.response?.data?.error) {
content = `${error.response?.data?.error} (${error.status})`
}
setErrorModalContent(content) setErrorModalContent(content)
setRetryCallback(() => callback) setRetryCallback(() => callback)
setShowErrorModal(true) setShowErrorModal(true)
@ -200,8 +374,8 @@ const ApiServerProvider = ({ children }) => {
setRetryCallback(null) setRetryCallback(null)
} }
// Generalized fetchObjectInfo function // Generalized fetchObject function
const fetchObjectInfo = async (id, type) => { const fetchObject = async (id, type) => {
const fetchUrl = `${config.backendUrl}/${type}s/${id}` const fetchUrl = `${config.backendUrl}/${type}s/${id}`
setFetchLoading(true) setFetchLoading(true)
logger.debug('Fetching from ' + fetchUrl) logger.debug('Fetching from ' + fetchUrl)
@ -214,62 +388,17 @@ const ApiServerProvider = ({ children }) => {
}) })
return response.data return response.data
} catch (err) { } catch (err) {
logger.error('Failed to fetch object information:', err) showError(err, () => {
// Don't automatically show error - let the component handle it fetchObject(id, type)
throw err })
return {}
} finally { } finally {
setFetchLoading(false) setFetchLoading(false)
} }
} }
// Update filament information
const updateObjectInfo = async (id, type, value) => {
const updateUrl = `${config.backendUrl}/${type.toLowerCase()}s/${id}`
logger.debug('Updating info for ' + id)
try {
const response = await axios.put(updateUrl, value, {
headers: {
'Content-Type': 'application/json'
},
withCredentials: true
})
logger.debug('Filament updated successfully')
if (socketRef.current && socketRef.current.connected == true) {
await socketRef.current.emit('update', {
_id: id,
type: type,
updatedAt: response.data.updatedAt
})
}
return response.data
} catch (err) {
logger.error('Failed to update filament information:', err)
// Don't automatically show error - let the component handle it
throw err
}
}
// Update filament information
const createObject = async (type, value) => {
const createUrl = `${config.backendUrl}/${type.toLowerCase()}s`
logger.debug('Creating object...')
try {
const response = await axios.post(createUrl, value, {
headers: {
'Content-Type': 'application/json'
},
withCredentials: true
})
return response.data
} catch (err) {
logger.error('Failed to update filament information:', err)
// Don't automatically show error - let the component handle it
throw err
}
}
// Fetch table data with pagination, filtering, and sorting // Fetch table data with pagination, filtering, and sorting
const fetchTableData = async (type, params = {}) => { const fetchObjects = async (type, params = {}) => {
const { const {
page = 1, page = 1,
limit = 25, limit = 25,
@ -319,9 +448,122 @@ const ApiServerProvider = ({ children }) => {
hasMore, hasMore,
page page
} }
} catch (error) { } catch (err) {
logger.error('Failed to fetch table data:', error) showError(err, () => {
throw error fetchObjects(type, params)
})
return []
}
}
// Fetch table data with pagination, filtering, and sorting
const fetchObjectsByProperty = async (type, params = {}) => {
const { filter = {}, properties = [] } = params
logger.debug('Fetching property object data from:', type, {
properties,
filter
})
try {
const response = await axios.get(
`${config.backendUrl}/${type.toLowerCase()}s/properties`,
{
params: {
...filter,
properties: properties.join(',') // Convert array to comma-separated string
},
headers: {
Accept: 'application/json'
},
withCredentials: true
}
)
const newData = response.data
return newData
} catch (err) {
showError(err, () => {
fetchObjectsByProperty(type, params)
})
return []
}
}
// Update filament information
const updateObject = async (id, type, value) => {
const updateUrl = `${config.backendUrl}/${type.toLowerCase()}s/${id}`
logger.debug('Updating info for ' + id)
try {
const response = await axios.put(updateUrl, value, {
headers: {
'Content-Type': 'application/json'
},
withCredentials: true
})
logger.debug('Object updated successfully')
if (socketRef.current && socketRef.current.connected == true) {
await socketRef.current.emit('update', {
_id: id,
type: type,
updatedAt: response.data.updatedAt
})
}
return response.data
} catch (err) {
setError(err, () => {
updateObject(id, type, value)
})
return {}
}
}
// Update filament information
const deleteObject = async (id, type) => {
const deleteUrl = `${config.backendUrl}/${type.toLowerCase()}s/${id}`
logger.debug('Deleting object ID: ' + id)
try {
const response = await axios.delete(deleteUrl, {
headers: {
'Content-Type': 'application/json'
},
withCredentials: true
})
logger.debug('Object deleted successfully')
if (socketRef.current && socketRef.current.connected == true) {
await socketRef.current.emit('update', {
_id: id,
type: type,
updatedAt: response.data.updatedAt
})
}
return response.data
} catch (err) {
showError(err, () => {
deleteObject(id, type)
})
return {}
}
}
// Update filament information
const createObject = async (type, value) => {
const createUrl = `${config.backendUrl}/${type.toLowerCase()}s`
logger.debug('Creating object...')
try {
const response = await axios.post(createUrl, value, {
headers: {
'Content-Type': 'application/json'
},
withCredentials: true
})
return response.data
} catch (err) {
showError(err, () => {
createObject(type, value)
})
return {}
} }
} }
@ -348,27 +590,35 @@ const ApiServerProvider = ({ children }) => {
document.body.appendChild(fileLink) document.body.appendChild(fileLink)
fileLink.click() fileLink.click()
fileLink.parentNode.removeChild(fileLink) fileLink.parentNode.removeChild(fileLink)
} catch (error) { } catch (err) {
logger.error('Failed to download GCode file content:', error) showError(err, () => {
fetchObjectContent(id, type, fileName)
})
}
}
if (error.response) { // Fetch notes for a specific parent
if (error.response.status === 404) { const fetchNotes = async (parentId) => {
showError( logger.debug('Fetching notes for parent:', parentId)
`The ${type} file "${fileName}" was not found on the server. It may have been deleted or moved.`, try {
() => fetchObjectContent(id, type, fileName) const response = await axios.get(`${config.backendUrl}/notes`, {
) params: {
} else { parent: parentId,
showError( sort: 'createdAt',
`Error downloading ${type} file: ${error.response.status} - ${error.response.statusText}`, order: 'ascend'
() => fetchObjectContent(id, type, fileName) },
) headers: {
} Accept: 'application/json'
} else { },
showError( withCredentials: true
'An unexpected error occurred while downloading. Please check your connection and try again.', })
() => fetchObjectContent(id, type, fileName)
) const notesData = response.data
} logger.debug('Fetched notes:', notesData.length)
return notesData
} catch (error) {
logger.error('Failed to fetch notes:', error)
throw error
} }
} }
@ -378,19 +628,24 @@ const ApiServerProvider = ({ children }) => {
apiServer: socketRef.current, apiServer: socketRef.current,
error, error,
connecting, connecting,
connected,
lockObject, lockObject,
unlockObject, unlockObject,
fetchObjectLock, fetchObjectLock,
updateObjectInfo, updateObject,
createObject, createObject,
onLockEvent, deleteObject,
onUpdateEvent, subscribeToObject,
subscribeToType,
subscribeToLock,
offUpdateEvent, offUpdateEvent,
fetchObjectInfo, fetchObject,
fetchTableData, fetchObjects,
fetchObjectsByProperty,
fetchLoading, fetchLoading,
showError, showError,
fetchObjectContent fetchObjectContent,
fetchNotes
}} }}
> >
{contextHolder} {contextHolder}

View File

@ -53,8 +53,7 @@ const AuthProvider = ({ children }) => {
}) })
if (response.status === 200 && response.data) { if (response.status === 200 && response.data) {
logger.debug('User is authenticated!') logger.debug('Got auth token!')
setAuthenticated(true)
setToken(response.data.access_token) setToken(response.data.access_token)
setExpiresAt(response.data.expires_at) setExpiresAt(response.data.expires_at)
setUserProfile(response.data) setUserProfile(response.data)
@ -89,19 +88,32 @@ const AuthProvider = ({ children }) => {
} }
}, []) }, [])
const showTokenExpirationMessage = useCallback( const handleSessionExpiredModalOk = () => {
(expiresAt) => { setShowSessionExpiredModal(false)
loginWithSSO()
}
// Initialize on component mount
useEffect(() => {
let intervalId
const tokenRefresh = () => {
if (expiresAt) {
const now = new Date() const now = new Date()
const expirationDate = new Date(expiresAt) const expirationDate = new Date(expiresAt)
const timeRemaining = expirationDate - now const timeRemaining = expirationDate - now
if (timeRemaining <= 0) { if (timeRemaining <= 0) {
if (authenticated) { if (authenticated == true) {
setShowSessionExpiredModal(true)
setAuthenticated(false) setAuthenticated(false)
notificationApi.destroy('token-expiration')
} }
setShowSessionExpiredModal(true)
notificationApi.destroy('token-expiration')
} else { } else {
if (authenticated == false) {
setAuthenticated(true)
}
const minutes = Math.floor(timeRemaining / 60000) const minutes = Math.floor(timeRemaining / 60000)
const seconds = Math.floor((timeRemaining % 60000) / 1000) const seconds = Math.floor((timeRemaining % 60000) / 1000)
@ -154,35 +166,20 @@ const AuthProvider = ({ children }) => {
notificationApi.destroy('token-expiration') notificationApi.destroy('token-expiration')
} }
} }
},
[authenticated, notificationApi]
)
const handleSessionExpiredModalOk = () => {
setShowSessionExpiredModal(false)
loginWithSSO()
}
// Initialize on component mount
useEffect(() => {
let intervalId
const tokenRefreshInterval = () => {
if (expiresAt) {
showTokenExpirationMessage(expiresAt)
} }
} }
if (authenticated) { intervalId = setInterval(tokenRefresh, 1000)
intervalId = setInterval(tokenRefreshInterval, 1000)
} console.log('fresh', authenticated)
tokenRefresh()
return () => { return () => {
if (intervalId) { if (intervalId) {
clearInterval(intervalId) clearInterval(intervalId)
} }
} }
}, [expiresAt, authenticated, showTokenExpirationMessage]) }, [expiresAt, authenticated, notificationApi, refreshToken])
useEffect(() => { useEffect(() => {
checkAuthStatus() checkAuthStatus()

View File

@ -461,16 +461,9 @@ const SpotlightProvider = ({ children }) => {
align='center' align='center'
justify='space-between' justify='space-between'
> >
<Flex <Flex gap={'small'} align='center'>
gap={'small'} {Icon ? <Icon style={{ fontSize: '20px' }} /> : null}
style={{ marginBottom: '2px' }}
align='center'
>
<Text>
{Icon ? (
<Icon style={{ fontSize: '20px' }} />
) : null}
</Text>
{item.name ? ( {item.name ? (
<Text ellipsis style={{ maxWidth: 170 }}> <Text ellipsis style={{ maxWidth: 170 }}>
{item.name} {item.name}

View File

@ -6,8 +6,16 @@ export const AuditLog = {
prefix: 'ADL', prefix: 'ADL',
icon: AuditLogIcon, icon: AuditLogIcon,
actions: [], actions: [],
columns: ['_id', 'owner', 'owner._id', 'parent._id', 'changes', 'createdAt'], columns: [
filters: ['_id', 'owner._id', 'parent._id'], '_id',
'owner',
'owner._id',
'parent._id',
'operation',
'changes',
'createdAt'
],
filters: ['_id', 'owner._id', 'parent._id', 'operation'],
sorters: ['createdAt'], sorters: ['createdAt'],
properties: [ properties: [
{ {
@ -75,6 +83,12 @@ export const AuditLog = {
showHyperlink: true, showHyperlink: true,
showCopy: true showCopy: true
}, },
{
name: 'operation',
label: 'Operation',
columnWidth: 120,
type: 'operation'
},
{ {
name: 'changes', name: 'changes',
label: 'Changes', label: 'Changes',

View File

@ -1,7 +1,6 @@
import JobIcon from '../../components/Icons/JobIcon' import JobIcon from '../../components/Icons/JobIcon'
import InfoCircleIcon from '../../components/Icons/InfoCircleIcon' import InfoCircleIcon from '../../components/Icons/InfoCircleIcon'
import ReloadIcon from '../../components/Icons/ReloadIcon' import ReloadIcon from '../../components/Icons/ReloadIcon'
import EditIcon from '../../components/Icons/EditIcon'
export const Job = { export const Job = {
name: 'job', name: 'job',
@ -22,13 +21,6 @@ export const Job = {
label: 'Reload', label: 'Reload',
icon: ReloadIcon, icon: ReloadIcon,
url: (_id) => `/dashboard/production/jobs/info?jobId=${_id}&action=reload` url: (_id) => `/dashboard/production/jobs/info?jobId=${_id}&action=reload`
},
{
name: 'edit',
label: 'Edit',
row: true,
icon: EditIcon,
url: (_id) => `/dashboard/production/jobs/info?jobId=${_id}&action=edit`
} }
], ],
columns: [ columns: [
@ -68,7 +60,7 @@ export const Job = {
type: 'object', type: 'object',
columnFixed: 'left', columnFixed: 'left',
objectType: 'gcodeFile', objectType: 'gcodeFile',
readOnly: true required: true
}, },
{ {
name: 'gcodeFile._id', name: 'gcodeFile._id',
@ -98,9 +90,10 @@ export const Job = {
}, },
{ {
name: 'printers', name: 'printers',
label: 'Assigned Printers', label: 'Printers',
type: 'number', type: 'objectList',
readOnly: true objectType: 'printer',
required: true
} }
] ]
} }

View File

@ -16,5 +16,58 @@ export const Note = {
url: (_id) => `/dashboard/management/notes/info?noteId=${_id}` url: (_id) => `/dashboard/management/notes/info?noteId=${_id}`
} }
], ],
url: () => `#` properties: [
{
name: '_id',
label: 'ID',
type: 'id',
objectType: 'note',
showCopy: true
},
{
name: 'createdAt',
label: 'Created At',
type: 'dateTime',
readOnly: true
},
{
name: 'noteType',
label: 'Note Type',
type: 'object',
objectType: 'noteType',
showHyperlink: true
},
{
name: 'parent._id',
label: 'Parent ID',
type: 'id',
objectType: (objectData) => {
return objectData.parentType
},
showHyperlink: true
},
{
name: 'noteType._id',
label: 'Note Type ID',
type: 'id',
objectType: 'noteType'
},
{
name: 'user._id',
label: 'User ID',
type: 'id',
objectType: 'user'
},
{
name: 'content',
label: 'Content',
type: 'markdown'
},
{
name: 'updatedAt',
label: 'Updated At',
type: 'dateTime',
readOnly: true
}
]
} }

View File

@ -60,7 +60,7 @@ export const StockEvent = {
label: 'Parent', label: 'Parent',
type: 'object', type: 'object',
objectType: (objectData) => { objectType: (objectData) => {
return objectData.parentType return objectData?.parentType
}, },
value: null, value: null,
showCopy: true showCopy: true
@ -78,9 +78,12 @@ export const StockEvent = {
{ {
name: 'value', name: 'value',
label: 'Value', label: 'Value',
columnWidth: 100, columnWidth: 120,
type: 'number', type: 'variance',
showCopy: true showCopy: true,
suffix: (objectData) => {
return objectData.unit
}
} }
] ]
} }