Refactored components to replace DashboardTable with ObjectTable for improved consistency and functionality. Updated FilamentStockInfo, PartStocks, StockAudits, and other inventory management components to utilize NotesPanel instead of DashboardNotes. Removed unused DashboardNotes and DashboardTable components to streamline the codebase.

This commit is contained in:
Tom Butcher 2025-07-04 21:29:27 +01:00
parent 9ccf7faa2f
commit fe85250838
64 changed files with 1136 additions and 1522 deletions

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.8 KiB

After

Width:  |  Height:  |  Size: 132 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 9.4 KiB

After

Width:  |  Height:  |  Size: 153 KiB

Binary file not shown.

Binary file not shown.

After

Width:  |  Height:  |  Size: 533 KiB

Binary file not shown.

Binary file not shown.

After

Width:  |  Height:  |  Size: 550 KiB

View File

@ -29,7 +29,7 @@ import TimeDisplay from '../common/TimeDisplay'
import XMarkIcon from '../../Icons/XMarkIcon' import XMarkIcon from '../../Icons/XMarkIcon'
import CheckIcon from '../../Icons/CheckIcon' import CheckIcon from '../../Icons/CheckIcon'
import useColumnVisibility from '../hooks/useColumnVisibility' import useColumnVisibility from '../hooks/useColumnVisibility'
import DashboardTable from '../common/DashboardTable' import ObjectTable from '../common/ObjectTable'
import ListIcon from '../../Icons/ListIcon' import ListIcon from '../../Icons/ListIcon'
import GridIcon from '../../Icons/GridIcon' import GridIcon from '../../Icons/GridIcon'
import useViewMode from '../hooks/useViewMode' import useViewMode from '../hooks/useViewMode'
@ -326,7 +326,7 @@ const FilamentStocks = () => {
</Space> </Space>
</Flex> </Flex>
<DashboardTable <ObjectTable
ref={tableRef} ref={tableRef}
columns={visibleColumns} columns={visibleColumns}
url={`${config.backendUrl}/filamentstocks`} url={`${config.backendUrl}/filamentstocks`}

View File

@ -27,7 +27,7 @@ import TimeDisplay from '../../common/TimeDisplay'
import FilamentIcon from '../../../Icons/FilamentIcon' import FilamentIcon from '../../../Icons/FilamentIcon'
import ReloadIcon from '../../../Icons/ReloadIcon' import ReloadIcon from '../../../Icons/ReloadIcon'
import AuditLogTable from '../../common/AuditLogTable' import AuditLogTable from '../../common/AuditLogTable'
import DashboardNotes from '../../common/DashboardNotes' import NotesPanel from '../../common/NotesPanel'
import config from '../../../../config' import config from '../../../../config'
import FilamentStockIcon from '../../../Icons/FilamentStockIcon' import FilamentStockIcon from '../../../Icons/FilamentStockIcon'
@ -424,7 +424,7 @@ const FilamentStockInfo = () => {
key='notes' key='notes'
> >
<Card> <Card>
<DashboardNotes _id={filamentStockId} /> <NotesPanel _id={filamentStockId} />
</Card> </Card>
</Collapse.Panel> </Collapse.Panel>
</Collapse> </Collapse>

View File

@ -14,7 +14,7 @@ import PlusIcon from '../../Icons/PlusIcon'
import ReloadIcon from '../../Icons/ReloadIcon' import ReloadIcon from '../../Icons/ReloadIcon'
import PartStockState from '../common/PartStockState' import PartStockState from '../common/PartStockState'
import TimeDisplay from '../common/TimeDisplay' import TimeDisplay from '../common/TimeDisplay'
import DashboardTable from '../common/DashboardTable' import ObjectTable from '../common/ObjectTable'
import config from '../../../config' import config from '../../../config'
@ -176,7 +176,7 @@ const PartStocks = () => {
<Button>Actions</Button> <Button>Actions</Button>
</Dropdown> </Dropdown>
</Space> </Space>
<DashboardTable <ObjectTable
ref={tableRef} ref={tableRef}
columns={columns} columns={columns}
url={`${config.backendUrl}/partstocks`} url={`${config.backendUrl}/partstocks`}

View File

@ -11,7 +11,7 @@ import InfoCircleIcon from '../../Icons/InfoCircleIcon'
import PlusIcon from '../../Icons/PlusIcon' import PlusIcon from '../../Icons/PlusIcon'
import ReloadIcon from '../../Icons/ReloadIcon' import ReloadIcon from '../../Icons/ReloadIcon'
import TimeDisplay from '../common/TimeDisplay' import TimeDisplay from '../common/TimeDisplay'
import DashboardTable from '../common/DashboardTable' import ObjectTable from '../common/ObjectTable'
import config from '../../../config' import config from '../../../config'
@ -167,7 +167,7 @@ const StockAudits = () => {
<Button>Actions</Button> <Button>Actions</Button>
</Dropdown> </Dropdown>
</Space> </Space>
<DashboardTable <ObjectTable
ref={tableRef} ref={tableRef}
columns={columns} columns={columns}
url={`${config.backendUrl}/stockaudits`} url={`${config.backendUrl}/stockaudits`}

View File

@ -19,13 +19,13 @@ import PlusMinusIcon from '../../Icons/PlusMinusIcon'
import XMarkIcon from '../../Icons/XMarkIcon' import XMarkIcon from '../../Icons/XMarkIcon'
import CheckIcon from '../../Icons/CheckIcon' import CheckIcon from '../../Icons/CheckIcon'
import useColumnVisibility from '../hooks/useColumnVisibility' import useColumnVisibility from '../hooks/useColumnVisibility'
import DashboardTable from '../common/DashboardTable' import ObjectTable from '../common/ObjectTable'
import GridIcon from '../../Icons/GridIcon' import GridIcon from '../../Icons/GridIcon'
import ListIcon from '../../Icons/ListIcon' import ListIcon from '../../Icons/ListIcon'
import useViewMode from '../hooks/useViewMode' import useViewMode from '../hooks/useViewMode'
import config from '../../../config' import config from '../../../config'
import { getTypeMeta } from '../utils/Utils' import { getModelByName } from '../../../database/ObjectModels'
import StockEventIcon from '../../Icons/StockEventIcon' import StockEventIcon from '../../Icons/StockEventIcon'
const { Text } = Typography const { Text } = Typography
@ -53,7 +53,7 @@ const StockEvents = () => {
fixed: 'left', fixed: 'left',
sorter: true, sorter: true,
render: (type) => { render: (type) => {
return <Text>{getTypeMeta(type?.toLowerCase()).title}</Text> return <Text>{getModelByName(type).title}</Text>
}, },
filterDropdown: ({ filterDropdown: ({
setSelectedKeys, setSelectedKeys,
@ -128,7 +128,7 @@ const StockEvents = () => {
) : null} ) : null}
{record.subJob?.number ? ( {record.subJob?.number ? (
<IdDisplay <IdDisplay
id={record.subJob.number.toString().padStart(6, '0')} id={record.subJob._id}
longId={false} longId={false}
type={'subjob'} type={'subjob'}
/> />
@ -310,7 +310,7 @@ const StockEvents = () => {
</Space> </Space>
</Flex> </Flex>
<DashboardTable <ObjectTable
ref={tableRef} ref={tableRef}
columns={visibleColumns} columns={visibleColumns}
url={`${config.backendUrl}/stockevents`} url={`${config.backendUrl}/stockevents`}

View File

@ -17,7 +17,7 @@ import IdDisplay from '../common/IdDisplay'
import ReloadIcon from '../../Icons/ReloadIcon' import ReloadIcon from '../../Icons/ReloadIcon'
import useColumnVisibility from '../hooks/useColumnVisibility' import useColumnVisibility from '../hooks/useColumnVisibility'
import TimeDisplay from '../common/TimeDisplay' import TimeDisplay from '../common/TimeDisplay'
import DashboardTable from '../common/DashboardTable' import ObjectTable from '../common/ObjectTable'
import config from '../../../config' import config from '../../../config'
import AuditLogIcon from '../../Icons/AuditLogIcon' import AuditLogIcon from '../../Icons/AuditLogIcon'
@ -316,7 +316,7 @@ const AuditLogs = () => {
</Space> </Space>
</Flex> </Flex>
<DashboardTable <ObjectTable
ref={tableRef} ref={tableRef}
columns={visibleColumns} columns={visibleColumns}
url={`${config.backendUrl}/auditlogs`} url={`${config.backendUrl}/auditlogs`}

View File

@ -7,17 +7,20 @@ import config from '../../../../config'
import ReloadIcon from '../../../Icons/ReloadIcon' import ReloadIcon from '../../../Icons/ReloadIcon'
import useCollapseState from '../../hooks/useCollapseState' import useCollapseState from '../../hooks/useCollapseState'
import AuditLogTable from '../../common/AuditLogTable' import AuditLogTable from '../../common/AuditLogTable'
import DashboardNotes from '../../common/DashboardNotes' import NotesPanel from '../../common/NotesPanel'
import InfoCollapse from '../../common/InfoCollapse' import InfoCollapse from '../../common/InfoCollapse'
import ObjectInfo from '../../common/ObjectInfo' import ObjectInfo from '../../common/ObjectInfo'
import ViewButton from '../../common/ViewButton' import ViewButton from '../../common/ViewButton'
import InfoCircleIcon from '../../../Icons/InfoCircleIcon.jsx' import InfoCircleIcon from '../../../Icons/InfoCircleIcon.jsx'
import NoteIcon from '../../../Icons/NoteIcon.jsx' import NoteIcon from '../../../Icons/NoteIcon.jsx'
import AuditLogIcon from '../../../Icons/AuditLogIcon.jsx' import AuditLogIcon from '../../../Icons/AuditLogIcon.jsx'
import EditObjectForm from '../../common/EditObjectForm' import EditObjectForm from '../../common/EditObjectForm'
import EditButtons from '../../common/EditButtons' import EditButtons from '../../common/EditButtons'
import LockIndicator from './LockIndicator' import LockIndicator from './LockIndicator'
import {
getModelProperties,
getPropertyValue
} from '../../../../database/ObjectModels'
const log = loglevel.getLogger('FilamentInfo') const log = loglevel.getLogger('FilamentInfo')
log.setLevel(config.logLevel) log.setLevel(config.logLevel)
@ -115,101 +118,10 @@ const FilamentInfo = () => {
loading={loading} loading={loading}
indicator={<LoadingOutlined />} indicator={<LoadingOutlined />}
isEditing={isEditing} isEditing={isEditing}
items={[ items={getModelProperties('filament').map((prop) => ({
{ ...prop,
name: 'id', value: getPropertyValue(objectData, prop.name)
label: 'ID', }))}
value: objectData?._id,
type: 'id',
objectType: 'filament',
showCopy: true
},
{
name: 'createdAt',
label: 'Created At',
value: objectData?.createdAt,
type: 'dateTime',
readOnly: true
},
{
name: 'name',
label: 'Name',
value: objectData?.name,
required: true,
type: 'text'
},
{
name: 'updatedAt',
label: 'Updated At',
value: objectData?.updatedAt,
type: 'dateTime',
readOnly: true
},
{
name: 'vendor',
label: 'Vendor',
value: objectData?.vendor,
required: true,
type: 'object',
objectType: 'vendor'
},
{
name: 'vendorId',
label: 'Vendor ID',
value: objectData?.vendor?.id,
type: 'id',
objectType: 'vendor',
showCopy: true,
showHyperlink: true
},
{
name: 'type',
label: 'Material',
value: objectData?.type,
required: true,
type: 'material'
},
{
name: 'cost',
label: 'Cost',
value: objectData?.cost,
required: true,
type: 'currency'
},
{
name: 'color',
label: 'Color',
value: objectData?.color,
required: true,
type: 'color'
},
{
name: 'diameter',
label: 'Diameter',
value: objectData?.diameter,
required: true,
type: 'mm'
},
{
name: 'density',
label: 'Density',
value: objectData?.density,
required: true,
type: 'density'
},
{
name: 'url',
label: 'URL',
value: objectData?.url,
type: 'text'
},
{
name: 'barcode',
label: 'Barcode',
value: objectData?.barcode,
type: 'text'
}
]}
/> />
</InfoCollapse> </InfoCollapse>
@ -221,7 +133,7 @@ const FilamentInfo = () => {
key='notes' key='notes'
> >
<Card> <Card>
<DashboardNotes _id={filamentId} /> <NotesPanel _id={filamentId} />
</Card> </Card>
</InfoCollapse> </InfoCollapse>

View File

@ -17,7 +17,7 @@ import { AuthContext } from '../context/AuthContext'
import IdDisplay from '../common/IdDisplay' import IdDisplay from '../common/IdDisplay'
import NewNoteType from './NoteTypes/NewNoteType' import NewNoteType from './NoteTypes/NewNoteType'
import TimeDisplay from '../common/TimeDisplay' import TimeDisplay from '../common/TimeDisplay'
import DashboardTable from '../common/DashboardTable' import ObjectTable from '../common/ObjectTable'
import PlusIcon from '../../Icons/PlusIcon' import PlusIcon from '../../Icons/PlusIcon'
import ReloadIcon from '../../Icons/ReloadIcon' import ReloadIcon from '../../Icons/ReloadIcon'
import XMarkIcon from '../../Icons/XMarkIcon' import XMarkIcon from '../../Icons/XMarkIcon'
@ -304,7 +304,7 @@ const NoteTypes = () => {
/> />
</Space> </Space>
</Flex> </Flex>
<DashboardTable <ObjectTable
ref={tableRef} ref={tableRef}
columns={visibleColumns} columns={visibleColumns}
url={`${config.backendUrl}/notetypes`} url={`${config.backendUrl}/notetypes`}

View File

@ -18,7 +18,7 @@ import { DownloadOutlined } from '@ant-design/icons'
import { AuthContext } from '../context/AuthContext' import { AuthContext } from '../context/AuthContext'
import IdDisplay from '../common/IdDisplay' import IdDisplay from '../common/IdDisplay'
import DashboardTable from '../common/DashboardTable' import ObjectTable from '../common/ObjectTable'
import NewProduct from './Products/NewProduct' import NewProduct from './Products/NewProduct'
import PartIcon from '../../Icons/PartIcon' import PartIcon from '../../Icons/PartIcon'
import InfoCircleIcon from '../../Icons/InfoCircleIcon' import InfoCircleIcon from '../../Icons/InfoCircleIcon'
@ -310,7 +310,7 @@ const Parts = () => {
/> />
</Space> </Space>
</Flex> </Flex>
<DashboardTable <ObjectTable
ref={tableRef} ref={tableRef}
columns={visibleColumns} columns={visibleColumns}
url={`${config.backendUrl}/parts`} url={`${config.backendUrl}/parts`}

View File

@ -30,7 +30,7 @@ import CheckIcon from '../../../Icons/CheckIcon.jsx'
import useCollapseState from '../../hooks/useCollapseState' import useCollapseState from '../../hooks/useCollapseState'
import TimeDisplay from '../../common/TimeDisplay.jsx' import TimeDisplay from '../../common/TimeDisplay.jsx'
import AuditLogTable from '../../common/AuditLogTable' import AuditLogTable from '../../common/AuditLogTable'
import DashboardNotes from '../../common/DashboardNotes' import NotesPanel from '../../common/NotesPanel'
import config from '../../../../config.js' import config from '../../../../config.js'
import BoolDisplay from '../../common/BoolDisplay.jsx' import BoolDisplay from '../../common/BoolDisplay.jsx'
@ -664,7 +664,7 @@ const PartInfo = () => {
key='notes' key='notes'
> >
<Card> <Card>
<DashboardNotes _id={partId} /> <NotesPanel _id={partId} />
</Card> </Card>
</Collapse.Panel> </Collapse.Panel>
</Collapse> </Collapse>

View File

@ -19,7 +19,7 @@ import { DownloadOutlined } from '@ant-design/icons'
import { AuthContext } from '../context/AuthContext' import { AuthContext } from '../context/AuthContext'
import IdDisplay from '../common/IdDisplay' import IdDisplay from '../common/IdDisplay'
import TimeDisplay from '../common/TimeDisplay' import TimeDisplay from '../common/TimeDisplay'
import DashboardTable from '../common/DashboardTable' import ObjectTable from '../common/ObjectTable'
import NewProduct from './Products/NewProduct' import NewProduct from './Products/NewProduct'
import ProductIcon from '../../Icons/ProductIcon' import ProductIcon from '../../Icons/ProductIcon'
import InfoCircleIcon from '../../Icons/InfoCircleIcon' import InfoCircleIcon from '../../Icons/InfoCircleIcon'
@ -353,7 +353,7 @@ const Products = () => {
/> />
</Space> </Space>
</Flex> </Flex>
<DashboardTable <ObjectTable
ref={tableRef} ref={tableRef}
columns={visibleColumns} columns={visibleColumns}
url={`${config.backendUrl}/products`} url={`${config.backendUrl}/products`}

View File

@ -4,7 +4,7 @@ import { Space, Button, Flex, Dropdown, Card } from 'antd'
import ReloadIcon from '../../../Icons/ReloadIcon' import ReloadIcon from '../../../Icons/ReloadIcon'
import useCollapseState from '../../hooks/useCollapseState' import useCollapseState from '../../hooks/useCollapseState'
import AuditLogTable from '../../common/AuditLogTable' import AuditLogTable from '../../common/AuditLogTable'
import DashboardNotes from '../../common/DashboardNotes' import NotesPanel from '../../common/NotesPanel'
import InfoCollapse from '../../common/InfoCollapse' import InfoCollapse from '../../common/InfoCollapse'
import ObjectInfo from '../../common/ObjectInfo' import ObjectInfo from '../../common/ObjectInfo'
import ViewButton from '../../common/ViewButton' import ViewButton from '../../common/ViewButton'
@ -206,7 +206,7 @@ const ProductInfo = () => {
key='notes' key='notes'
> >
<Card> <Card>
<DashboardNotes _id={productId} /> <NotesPanel _id={productId} />
</Card> </Card>
</InfoCollapse> </InfoCollapse>

View File

@ -14,7 +14,7 @@ import { ExportOutlined } from '@ant-design/icons'
import { AuthContext } from '../context/AuthContext' import { AuthContext } from '../context/AuthContext'
import IdDisplay from '../common/IdDisplay' import IdDisplay from '../common/IdDisplay'
import TimeDisplay from '../common/TimeDisplay' import TimeDisplay from '../common/TimeDisplay'
import DashboardTable from '../common/DashboardTable' import ObjectTable from '../common/ObjectTable'
import PersonIcon from '../../Icons/PersonIcon' import PersonIcon from '../../Icons/PersonIcon'
import ReloadIcon from '../../Icons/ReloadIcon' import ReloadIcon from '../../Icons/ReloadIcon'
import XMarkIcon from '../../Icons/XMarkIcon' import XMarkIcon from '../../Icons/XMarkIcon'
@ -367,7 +367,7 @@ const Users = () => {
/> />
</Space> </Space>
</Flex> </Flex>
<DashboardTable <ObjectTable
ref={tableRef} ref={tableRef}
columns={visibleColumns} columns={visibleColumns}
url={`${config.backendUrl}/users`} url={`${config.backendUrl}/users`}

View File

@ -5,7 +5,7 @@ import { LoadingOutlined } from '@ant-design/icons'
import ReloadIcon from '../../../Icons/ReloadIcon' import ReloadIcon from '../../../Icons/ReloadIcon'
import useCollapseState from '../../hooks/useCollapseState' import useCollapseState from '../../hooks/useCollapseState'
import AuditLogTable from '../../common/AuditLogTable' import AuditLogTable from '../../common/AuditLogTable'
import DashboardNotes from '../../common/DashboardNotes' import NotesPanel from '../../common/NotesPanel'
import InfoCollapse from '../../common/InfoCollapse' import InfoCollapse from '../../common/InfoCollapse'
import ObjectInfo from '../../common/ObjectInfo' import ObjectInfo from '../../common/ObjectInfo'
import ViewButton from '../../common/ViewButton' import ViewButton from '../../common/ViewButton'
@ -178,7 +178,7 @@ const UserInfo = () => {
key='notes' key='notes'
> >
<Card> <Card>
<DashboardNotes _id={userId} /> <NotesPanel _id={userId} />
</Card> </Card>
</InfoCollapse> </InfoCollapse>

View File

@ -18,7 +18,7 @@ import IdDisplay from '../common/IdDisplay'
import NewVendor from './Vendors/NewVendor' import NewVendor from './Vendors/NewVendor'
import CountryDisplay from '../common/CountryDisplay' import CountryDisplay from '../common/CountryDisplay'
import TimeDisplay from '../common/TimeDisplay' import TimeDisplay from '../common/TimeDisplay'
import DashboardTable from '../common/DashboardTable' import ObjectTable from '../common/ObjectTable'
import VendorIcon from '../../Icons/VendorIcon' import VendorIcon from '../../Icons/VendorIcon'
import PlusIcon from '../../Icons/PlusIcon' import PlusIcon from '../../Icons/PlusIcon'
import ReloadIcon from '../../Icons/ReloadIcon' import ReloadIcon from '../../Icons/ReloadIcon'
@ -363,7 +363,7 @@ const Vendors = () => {
/> />
</Space> </Space>
</Flex> </Flex>
<DashboardTable <ObjectTable
ref={tableRef} ref={tableRef}
columns={visibleColumns} columns={visibleColumns}
url={`${config.backendUrl}/vendors`} url={`${config.backendUrl}/vendors`}

View File

@ -7,17 +7,20 @@ import config from '../../../../config'
import ReloadIcon from '../../../Icons/ReloadIcon' import ReloadIcon from '../../../Icons/ReloadIcon'
import useCollapseState from '../../hooks/useCollapseState' import useCollapseState from '../../hooks/useCollapseState'
import AuditLogTable from '../../common/AuditLogTable' import AuditLogTable from '../../common/AuditLogTable'
import DashboardNotes from '../../common/DashboardNotes' import NotesPanel from '../../common/NotesPanel'
import InfoCollapse from '../../common/InfoCollapse' import InfoCollapse from '../../common/InfoCollapse'
import ObjectInfo from '../../common/ObjectInfo' import ObjectInfo from '../../common/ObjectInfo'
import ViewButton from '../../common/ViewButton' import ViewButton from '../../common/ViewButton'
import InfoCircleIcon from '../../../Icons/InfoCircleIcon.jsx' import InfoCircleIcon from '../../../Icons/InfoCircleIcon.jsx'
import NoteIcon from '../../../Icons/NoteIcon.jsx' import NoteIcon from '../../../Icons/NoteIcon.jsx'
import AuditLogIcon from '../../../Icons/AuditLogIcon.jsx' import AuditLogIcon from '../../../Icons/AuditLogIcon.jsx'
import EditObjectForm from '../../common/EditObjectForm' import EditObjectForm from '../../common/EditObjectForm'
import EditButtons from '../../common/EditButtons' import EditButtons from '../../common/EditButtons'
import LockIndicator from '../Filaments/LockIndicator' import LockIndicator from '../Filaments/LockIndicator'
import {
getModelProperties,
getPropertyValue
} from '../../../../database/ObjectModels'
const log = loglevel.getLogger('VendorInfo') const log = loglevel.getLogger('VendorInfo')
log.setLevel(config.logLevel) log.setLevel(config.logLevel)
@ -115,67 +118,10 @@ const VendorInfo = () => {
loading={loading} loading={loading}
indicator={<LoadingOutlined />} indicator={<LoadingOutlined />}
isEditing={isEditing} isEditing={isEditing}
items={[ items={getModelProperties('vendor').map((prop) => ({
{ ...prop,
name: 'id', value: getPropertyValue(objectData, prop.name)
label: 'ID', }))}
value: objectData?._id,
type: 'id',
objectType: 'vendor',
showCopy: true
},
{
name: 'createdAt',
label: 'Created At',
value: objectData?.createdAt,
type: 'dateTime',
readOnly: true
},
{
name: 'name',
label: 'Name',
value: objectData?.name,
required: true,
type: 'text'
},
{
name: 'updatedAt',
label: 'Updated At',
value: objectData?.updatedAt,
type: 'dateTime',
readOnly: true
},
{
name: 'website',
label: 'Website',
value: objectData?.website,
type: 'url'
},
{
name: 'country',
label: 'Country',
value: objectData?.country,
type: 'country'
},
{
name: 'contact',
label: 'Contact',
value: objectData?.contact,
type: 'text'
},
{
name: 'phone',
label: 'Phone',
value: objectData?.phone,
type: 'text'
},
{
name: 'email',
label: 'Email',
value: objectData?.email,
type: 'email'
}
]}
/> />
</InfoCollapse> </InfoCollapse>
@ -187,7 +133,7 @@ const VendorInfo = () => {
key='notes' key='notes'
> >
<Card> <Card>
<DashboardNotes _id={vendorId} /> <NotesPanel _id={vendorId} />
</Card> </Card>
</InfoCollapse> </InfoCollapse>

View File

@ -30,7 +30,7 @@ import ReloadIcon from '../../Icons/ReloadIcon'
import XMarkIcon from '../../Icons/XMarkIcon' import XMarkIcon from '../../Icons/XMarkIcon'
import CheckIcon from '../../Icons/CheckIcon' import CheckIcon from '../../Icons/CheckIcon'
import TimeDisplay from '../common/TimeDisplay' import TimeDisplay from '../common/TimeDisplay'
import DashboardTable from '../common/DashboardTable' import ObjectTable from '../common/ObjectTable'
import ListIcon from '../../Icons/ListIcon' import ListIcon from '../../Icons/ListIcon'
import GridIcon from '../../Icons/GridIcon' import GridIcon from '../../Icons/GridIcon'
import useViewMode from '../hooks/useViewMode' import useViewMode from '../hooks/useViewMode'
@ -381,7 +381,7 @@ const GCodeFiles = () => {
/> />
</Space> </Space>
</Flex> </Flex>
<DashboardTable <ObjectTable
ref={tableRef} ref={tableRef}
columns={visibleColumns} columns={visibleColumns}
url={`${config.backendUrl}/gcodefiles`} url={`${config.backendUrl}/gcodefiles`}

View File

@ -5,7 +5,7 @@ import { LoadingOutlined } from '@ant-design/icons'
import ReloadIcon from '../../../Icons/ReloadIcon' import ReloadIcon from '../../../Icons/ReloadIcon'
import useCollapseState from '../../hooks/useCollapseState' import useCollapseState from '../../hooks/useCollapseState'
import AuditLogTable from '../../common/AuditLogTable' import AuditLogTable from '../../common/AuditLogTable'
import DashboardNotes from '../../common/DashboardNotes' import NotesPanel from '../../common/NotesPanel'
import InfoCollapse from '../../common/InfoCollapse' import InfoCollapse from '../../common/InfoCollapse'
import ObjectInfo from '../../common/ObjectInfo' import ObjectInfo from '../../common/ObjectInfo'
import ViewButton from '../../common/ViewButton' import ViewButton from '../../common/ViewButton'
@ -16,6 +16,10 @@ import InfoCircleIcon from '../../../Icons/InfoCircleIcon.jsx'
import NoteIcon from '../../../Icons/NoteIcon.jsx' import NoteIcon from '../../../Icons/NoteIcon.jsx'
import AuditLogIcon from '../../../Icons/AuditLogIcon.jsx' import AuditLogIcon from '../../../Icons/AuditLogIcon.jsx'
import GCodeFileIcon from '../../../Icons/GCodeFileIcon.jsx' import GCodeFileIcon from '../../../Icons/GCodeFileIcon.jsx'
import {
getModelProperties,
getPropertyValue
} from '../../../../database/ObjectModels.js'
const { Text } = Typography const { Text } = Typography
@ -117,120 +121,10 @@ const GCodeFileInfo = () => {
loading={loading} loading={loading}
indicator={<LoadingOutlined />} indicator={<LoadingOutlined />}
isEditing={isEditing} isEditing={isEditing}
items={[ items={getModelProperties('gcodeFile').map((prop) => ({
{ ...prop,
name: '_id', value: getPropertyValue(objectData, prop.name)
label: 'ID', }))}
type: 'id',
objectType: 'gcodefile',
value: objectData?._id,
showCopy: true
},
{
name: 'createdAt',
label: 'Created At',
type: 'dateTime',
value: objectData?.createdAt,
readOnly: true
},
{
name: 'name',
label: 'Name',
type: 'text',
value: objectData?.name,
required: true
},
{
name: 'updatedAt',
label: 'Updated At',
type: 'dateTime',
value: objectData?.updatedAt,
readOnly: true
},
{
name: 'filament',
label: 'Filament',
type: 'object',
value: objectData?.filament,
objectType: 'filament',
required: true
},
{
name: 'cost',
label: 'Cost',
type: 'currency',
value: objectData?.cost,
readOnly: true
},
{
name: [
'gcodeFileInfo',
'estimatedPrintingTimeNormalMode'
],
label: 'Est Print Time',
value:
objectData?.gcodeFileInfo
?.estimatedPrintingTimeNormalMode,
type: 'text',
readOnly: true
},
{
name: ['gcodeFileInfo', 'sparseInfillDensity'],
label: 'Infill Density',
value: objectData?.gcodeFileInfo?.sparseInfillDensity,
type: 'number',
readOnly: true
},
{
name: ['gcodeFileInfo', 'sparseInfillPattern'],
label: 'Infill Pattern',
value: objectData?.gcodeFileInfo?.sparseInfillPattern,
type: 'text',
readOnly: true
},
{
name: ['gcodeFileInfo', 'filamentUsedMm'],
label: 'Filament Used (mm)',
value: objectData?.gcodeFileInfo?.filamentUsedMm,
type: 'mm',
readOnly: true
},
{
name: ['gcodeFileInfo', 'filamentUsedG'],
label: 'Filament Used (g)',
value: objectData?.gcodeFileInfo?.filamentUsedG,
type: 'weight',
readOnly: true
},
{
name: ['gcodeFileInfo', 'nozzleTemperature'],
label: 'Hotend Temperature',
value: objectData?.gcodeFileInfo?.nozzleTemperature,
type: 'number',
readOnly: true
},
{
name: ['gcodeFileInfo', 'hotPlateTemp'],
label: 'Bed Temperature',
value: objectData?.gcodeFileInfo?.hotPlateTemp,
type: 'number',
readOnly: true
},
{
name: ['gcodeFileInfo', 'filamentSettingsId'],
label: 'Filament Profile',
value: objectData?.gcodeFileInfo?.filamentSettingsId,
type: 'text',
readOnly: true
},
{
name: ['gcodeFileInfo', 'printSettingsId'],
label: 'Print Profile',
value: objectData?.gcodeFileInfo?.printSettingsId,
type: 'text',
readOnly: true
}
]}
objectData={objectData} objectData={objectData}
type='gcodefile' type='gcodefile'
/> />
@ -266,7 +160,7 @@ const GCodeFileInfo = () => {
key='notes' key='notes'
> >
<Card> <Card>
<DashboardNotes _id={gcodeFileId} /> <NotesPanel _id={gcodeFileId} />
</Card> </Card>
</InfoCollapse> </InfoCollapse>

View File

@ -36,7 +36,7 @@ import CheckCircleIcon from '../../Icons/CheckCircleIcon.jsx'
import PauseCircleIcon from '../../Icons/PauseCircleIcon.jsx' import PauseCircleIcon from '../../Icons/PauseCircleIcon.jsx'
import XMarkCircleIcon from '../../Icons/XMarkCircleIcon.jsx' import XMarkCircleIcon from '../../Icons/XMarkCircleIcon.jsx'
import QuestionCircleIcon from '../../Icons/QuestionCircleIcon.jsx' import QuestionCircleIcon from '../../Icons/QuestionCircleIcon.jsx'
import DashboardTable from '../common/DashboardTable' import ObjectTable from '../common/ObjectTable.jsx'
import ListIcon from '../../Icons/ListIcon.jsx' import ListIcon from '../../Icons/ListIcon.jsx'
import GridIcon from '../../Icons/GridIcon.jsx' import GridIcon from '../../Icons/GridIcon.jsx'
import useViewMode from '../hooks/useViewMode.js' import useViewMode from '../hooks/useViewMode.js'
@ -146,9 +146,10 @@ const Jobs = () => {
{ {
title: 'State', title: 'State',
key: 'state', key: 'state',
dataIndex: 'state',
width: 240, width: 240,
render: (record) => { render: (state) => {
return <JobState job={record} showQuantity={false} showId={false} /> return <JobState state={state} showQuantity={false} showId={false} />
}, },
filterDropdown: ({ filterDropdown: ({
setSelectedKeys, setSelectedKeys,
@ -393,7 +394,7 @@ const Jobs = () => {
</Space> </Space>
</Flex> </Flex>
<DashboardTable <ObjectTable
ref={tableRef} ref={tableRef}
columns={visibleColumns} columns={visibleColumns}
url={`${config.backendUrl}/jobs`} url={`${config.backendUrl}/jobs`}

View File

@ -4,7 +4,7 @@ import { Space, Button, Flex, Dropdown, Card } from 'antd'
import { LoadingOutlined } from '@ant-design/icons' import { LoadingOutlined } from '@ant-design/icons'
import useCollapseState from '../../hooks/useCollapseState' import useCollapseState from '../../hooks/useCollapseState'
import AuditLogTable from '../../common/AuditLogTable' import AuditLogTable from '../../common/AuditLogTable'
import DashboardNotes from '../../common/DashboardNotes' import NotesPanel from '../../common/NotesPanel'
import InfoCollapse from '../../common/InfoCollapse' import InfoCollapse from '../../common/InfoCollapse'
import ObjectInfo from '../../common/ObjectInfo' import ObjectInfo from '../../common/ObjectInfo'
import ViewButton from '../../common/ViewButton' import ViewButton from '../../common/ViewButton'
@ -17,6 +17,10 @@ import JobIcon from '../../../Icons/JobIcon'
import AuditLogIcon from '../../../Icons/AuditLogIcon' import AuditLogIcon from '../../../Icons/AuditLogIcon'
import NoteIcon from '../../../Icons/NoteIcon' import NoteIcon from '../../../Icons/NoteIcon'
import GCodeFileIcon from '../../../Icons/GCodeFileIcon' import GCodeFileIcon from '../../../Icons/GCodeFileIcon'
import {
getModelProperties,
getPropertyValue
} from '../../../../database/ObjectModels.js'
const JobInfo = () => { const JobInfo = () => {
const location = useLocation() const location = useLocation()
@ -114,72 +118,10 @@ const JobInfo = () => {
indicator={<LoadingOutlined />} indicator={<LoadingOutlined />}
isEditing={isEditing} isEditing={isEditing}
type='job' type='job'
items={[ items={getModelProperties('job').map((prop) => ({
{ ...prop,
name: '_id', value: getPropertyValue(objectData, prop.name)
label: 'ID', }))}
value: objectData?._id,
type: 'id',
objectType: 'job',
showCopy: true
},
{
name: 'state',
label: 'Status',
value: objectData,
type: 'state',
objectType: 'job',
showStatus: true,
showProgress: true,
showId: false,
showQuantity: false,
readOnly: true
},
{
name: 'gcodeFile',
label: 'GCode File',
value: objectData?.gcodeFile,
type: 'object',
objectType: 'gcodeFile',
readOnly: true
},
{
name: 'gcodeFileId',
label: 'GCode File ID',
value: objectData?.gcodeFile?._id,
type: 'id',
objectType: 'gcodefile',
showHyperlink: true
},
{
name: 'quantity',
label: 'Quantity',
value: objectData?.quantity,
type: 'number',
readOnly: true
},
{
name: 'createdAt',
label: 'Created At',
value: objectData?.createdAt,
type: 'dateTime',
readOnly: true
},
{
name: 'startedAt',
label: 'Started At',
value: objectData?.startedAt,
type: 'dateTime',
readOnly: true
},
{
name: 'assignedPrinters',
label: 'Assigned Printers',
value: objectData?.printers?.length,
type: 'number',
readOnly: true
}
]}
/> />
</InfoCollapse> </InfoCollapse>
@ -203,7 +145,7 @@ const JobInfo = () => {
key='notes' key='notes'
> >
<Card> <Card>
<DashboardNotes _id={jobId} /> <NotesPanel _id={jobId} />
</Card> </Card>
</InfoCollapse> </InfoCollapse>

View File

@ -26,7 +26,7 @@ import PlusIcon from '../../Icons/PlusIcon'
import ReloadIcon from '../../Icons/ReloadIcon' import ReloadIcon from '../../Icons/ReloadIcon'
import XMarkIcon from '../../Icons/XMarkIcon' import XMarkIcon from '../../Icons/XMarkIcon'
import CheckIcon from '../../Icons/CheckIcon' import CheckIcon from '../../Icons/CheckIcon'
import DashboardTable from '../common/DashboardTable' import ObjectTable from '../common/ObjectTable'
import config from '../../../config' import config from '../../../config'
import GridIcon from '../../Icons/GridIcon' import GridIcon from '../../Icons/GridIcon'
@ -83,15 +83,12 @@ const Printers = () => {
}, },
{ {
title: 'State', title: 'State',
dataIndex: 'state',
key: 'state', key: 'state',
width: 240, width: 240,
render: (record) => { render: (state) => {
return ( return (
<PrinterState <PrinterState state={state} showName={false} showControls={false} />
printer={record}
showName={false}
showControls={false}
/>
) )
} }
}, },
@ -312,7 +309,7 @@ const Printers = () => {
</Space> </Space>
</Flex> </Flex>
<DashboardTable <ObjectTable
ref={tableRef} ref={tableRef}
columns={visibleColumns} columns={visibleColumns}
url={`${config.backendUrl}/printers`} url={`${config.backendUrl}/printers`}

View File

@ -4,7 +4,7 @@ import { Space, Button, Flex, Dropdown, Card } from 'antd'
import { LoadingOutlined } from '@ant-design/icons' import { LoadingOutlined } from '@ant-design/icons'
import useCollapseState from '../../hooks/useCollapseState' import useCollapseState from '../../hooks/useCollapseState'
import AuditLogTable from '../../common/AuditLogTable' import AuditLogTable from '../../common/AuditLogTable'
import DashboardNotes from '../../common/DashboardNotes' import NotesPanel from '../../common/NotesPanel'
import InfoCollapse from '../../common/InfoCollapse' import InfoCollapse from '../../common/InfoCollapse'
import ObjectInfo from '../../common/ObjectInfo' import ObjectInfo from '../../common/ObjectInfo'
import ViewButton from '../../common/ViewButton' import ViewButton from '../../common/ViewButton'
@ -16,6 +16,10 @@ import InfoCircleIcon from '../../../Icons/InfoCircleIcon.jsx'
import NoteIcon from '../../../Icons/NoteIcon.jsx' import NoteIcon from '../../../Icons/NoteIcon.jsx'
import PrinterIcon from '../../../Icons/PrinterIcon.jsx' import PrinterIcon from '../../../Icons/PrinterIcon.jsx'
import AuditLogIcon from '../../../Icons/AuditLogIcon.jsx' import AuditLogIcon from '../../../Icons/AuditLogIcon.jsx'
import {
getModelProperties,
getPropertyValue
} from '../../../../database/ObjectModels.js'
const PrinterInfo = () => { const PrinterInfo = () => {
const location = useLocation() const location = useLocation()
@ -113,102 +117,10 @@ const PrinterInfo = () => {
indicator={<LoadingOutlined />} indicator={<LoadingOutlined />}
isEditing={isEditing} isEditing={isEditing}
type='printer' type='printer'
items={[ items={getModelProperties('printer').map((prop) => ({
{ ...prop,
name: '_id', value: getPropertyValue(objectData, prop.name)
label: 'ID', }))}
value: objectData?._id,
type: 'id',
objectType: 'printer',
showCopy: true
},
{
name: 'connectedAt',
label: 'Connected At',
value: objectData?.connectedAt,
type: 'dateTime',
readOnly: true
},
{
name: 'name',
label: 'Name',
value: objectData?.name,
required: true,
type: 'text'
},
{
name: 'state',
label: 'Status',
value: objectData,
type: 'state',
objectType: 'printer',
showName: false,
readOnly: true
},
{
name: 'vendor',
label: 'Vendor',
value: objectData?.vendor,
type: 'object',
objectType: 'vendor',
required: true
},
{
name: ['moonraker', 'host'],
label: 'Host',
value: objectData?.moonraker?.host,
type: 'text',
required: true
},
{
name: 'vendorId',
label: 'Vendor ID',
value: objectData?.vendor?.id,
type: 'id',
objectType: 'vendor',
showHyperlink: true,
readOnly: true
},
{
name: ['moonraker', 'port'],
label: 'Port',
value: objectData?.moonraker?.port,
type: 'number',
required: true
},
{
name: ['moonraker', 'apiKey'],
label: 'API Key',
value: objectData?.moonraker?.apiKey,
type: 'secret',
reveal: true,
required: false
},
{
name: ['moonraker', 'protocol'],
label: 'Protocol',
value: objectData?.moonraker?.protocol,
type: 'wsprotocol',
required: true
},
{
name: 'tags',
label: 'Tags',
value: objectData?.tags,
type: 'tags',
required: false
},
{
name: 'firmware',
label: 'Firmware Version',
value: objectData?.firmware,
type: 'text',
required: false,
readOnly: true
}
]}
/> />
</InfoCollapse> </InfoCollapse>
@ -233,7 +145,7 @@ const PrinterInfo = () => {
key='notes' key='notes'
> >
<Card> <Card>
<DashboardNotes _id={printerId} /> <NotesPanel _id={printerId} />
</Card> </Card>
</InfoCollapse> </InfoCollapse>

View File

@ -68,7 +68,10 @@ const formatValue = (value, propertyName) => {
} }
const AuditLogTable = forwardRef( const AuditLogTable = forwardRef(
({ items, loading, showTargetColumn, showOwnerColumn }, ref) => { (
{ items, loading = false, showTargetColumn = true, showOwnerColumn = true },
ref
) => {
const [sortedInfo, setSortedInfo] = useState({ const [sortedInfo, setSortedInfo] = useState({
columnKey: 'createdAt', columnKey: 'createdAt',
order: 'descend' order: 'descend'
@ -207,10 +210,4 @@ AuditLogTable.propTypes = {
showOwnerColumn: PropTypes.bool showOwnerColumn: PropTypes.bool
} }
AuditLogTable.defaultProps = {
loading: false,
showTargetColumn: true,
showOwnerColumn: true
}
export default AuditLogTable export default AuditLogTable

View File

@ -47,7 +47,7 @@ const ColorSelector = ({ value, onChange, disabled, required = false }) => {
ColorSelector.propTypes = { ColorSelector.propTypes = {
value: PropTypes.string, value: PropTypes.string,
onChange: PropTypes.func.isRequired, onChange: PropTypes.func,
disabled: PropTypes.bool, disabled: PropTypes.bool,
required: PropTypes.bool required: PropTypes.bool
} }

View File

@ -1,126 +1,26 @@
import { TreeSelect } from 'antd' import React from 'react'
import React, { useEffect, useState, useCallback } from 'react'
import PropTypes from 'prop-types' import PropTypes from 'prop-types'
import axios from 'axios'
import config from '../../../config' import config from '../../../config'
import ObjectSelect from './ObjectSelect'
import FilamentStockDisplay from './FilamentStockDisplay' const FilamentStockSelect = ({
onChange,
const FilamentStockSelect = ({ onChange, filter, useFilter, value }) => { filter = {},
const [filamentStocksTreeData, setFilamentStocksTreeData] = useState([]) useFilter = false,
const [filamentStocksData, setFilamentStocksData] = useState([]) value,
const [loading, setLoading] = useState(false) disabled = false
const [defaultValue, setDefaultValue] = useState(value) }) => {
const getFilamentStockTitle = (filamentStock) => {
return (
<FilamentStockDisplay filamentStock={filamentStock} showCopy={false} />
)
}
const fetchFilamentStocksData = async (property, filter) => {
setLoading(true)
try {
const response = await axios.get(`${config.backendUrl}/filamentstocks`, {
params: {
...filter,
property
},
headers: {
Accept: 'application/json'
},
withCredentials: true
})
setLoading(false)
return response.data
} catch (err) {
console.error(err)
}
}
const generateFilamentStockTreeNodes = useCallback(
async (node = null, filter = null) => {
if (!node) {
return
}
const filamentStockData = await fetchFilamentStocksData(null, filter)
setFilamentStocksData(filamentStockData)
for (const filamentStock of filamentStockData) {
const newNode = {
id: filamentStock._id,
pId: node.id,
value: filamentStock._id,
key: filamentStock._id,
title: getFilamentStockTitle(filamentStock),
isLeaf: true
}
setFilamentStocksTreeData((prev) => {
const filtered = prev.filter((node) => node.id !== newNode.id)
return [...filtered, newNode]
})
}
},
[]
)
const handleFilamentStocksTreeLoad = useCallback(
async (node) => {
if (node) {
await generateFilamentStockTreeNodes(node)
} else {
await generateFilamentStockTreeNodes({ id: 0 })
}
},
[generateFilamentStockTreeNodes]
)
const handleOnChange = (value, selectedOptions) => {
const filamentStockObject = filamentStocksData.filter(
(filamentStock) => filamentStock._id === value
)[0]
onChange(filamentStockObject, selectedOptions)
}
useEffect(() => {
if (value?._id != null) {
setDefaultValue(value)
}
}, [value])
useEffect(() => {
if (defaultValue != undefined) {
const newNode = {
id: defaultValue._id,
pId: 0,
value: defaultValue._id,
key: defaultValue._id,
title: getFilamentStockTitle(defaultValue),
isLeaf: true
}
setFilamentStocksTreeData([newNode])
} else {
setFilamentStocksTreeData([])
}
if (useFilter === true) {
generateFilamentStockTreeNodes({ id: 0 }, filter)
} else {
handleFilamentStocksTreeLoad(null)
}
}, [useFilter, defaultValue, filter])
return ( return (
<TreeSelect <ObjectSelect
treeDataSimpleMode endpoint={`${config.backendUrl}/filamentstocks`}
value={defaultValue?._id} propertyOrder={['tags']}
loadData={handleFilamentStocksTreeLoad} filter={filter}
treeData={filamentStocksTreeData} useFilter={useFilter}
onChange={handleOnChange} value={value}
loading={loading} onChange={onChange}
disabled={disabled}
placeholder='Select a filament stock' placeholder='Select a filament stock'
type='filamentstock'
/> />
) )
} }
@ -129,12 +29,8 @@ FilamentStockSelect.propTypes = {
onChange: PropTypes.func.isRequired, onChange: PropTypes.func.isRequired,
value: PropTypes.object, value: PropTypes.object,
filter: PropTypes.object, filter: PropTypes.object,
useFilter: PropTypes.bool useFilter: PropTypes.bool,
} disabled: PropTypes.bool
FilamentStockSelect.defaultProps = {
filter: {},
useFilter: false
} }
export default FilamentStockSelect export default FilamentStockSelect

View File

@ -6,7 +6,7 @@ import { useNavigate } from 'react-router-dom'
import { useMediaQuery } from 'react-responsive' import { useMediaQuery } from 'react-responsive'
import CopyButton from './CopyButton' import CopyButton from './CopyButton'
import SpotlightTooltip from './SpotlightTooltip' import SpotlightTooltip from './SpotlightTooltip'
import { getTypeMeta } from '../utils/Utils' import { getModelByName } from '../../../database/ObjectModels'
const { Text, Link } = Typography const { Text, Link } = Typography
@ -21,10 +21,10 @@ const IdDisplay = ({
const navigate = useNavigate() const navigate = useNavigate()
const isMobile = useMediaQuery({ maxWidth: 768 }) const isMobile = useMediaQuery({ maxWidth: 768 })
const meta = getTypeMeta(type) const model = getModelByName(type)
const prefix = meta.prefix const prefix = model.prefix
const hyperlink = meta.url(id) const hyperlink = model.url(id)
const IconComponent = meta.icon const IconComponent = model.icon
const icon = <IconComponent style={{ paddingTop: '4px' }} /> const icon = <IconComponent style={{ paddingTop: '4px' }} />
if (!id) { if (!id) {

View File

@ -1,47 +1,22 @@
import PropTypes from 'prop-types' import PropTypes from 'prop-types'
import { Progress, Flex, Typography, Space } from 'antd' import { Progress, Flex, Space } from 'antd'
import React, { useState, useContext, useEffect } from 'react' import React, { useState, useEffect } from 'react'
import { PrintServerContext } from '../context/PrintServerContext'
import IdDisplay from './IdDisplay'
import StateTag from './StateTag' import StateTag from './StateTag'
const JobState = ({ const JobState = ({ state, showProgress = true, showState = true }) => {
job,
showProgress = true,
showStatus = true,
showId = true,
showQuantity = true
}) => {
const { printServer } = useContext(PrintServerContext)
const [currentState, setCurrentState] = useState( const [currentState, setCurrentState] = useState(
job?.state || { type: 'unknown', progress: 0 } state || { type: 'unknown', progress: 0 }
) )
const [initialized, setInitialized] = useState(false)
const { Text } = Typography
useEffect(() => { useEffect(() => {
if (printServer && !initialized && job?._id) { if (state) {
setInitialized(true) setCurrentState(state)
printServer.on('notify_job_update', (statusUpdate) => {
if (statusUpdate?._id === job._id && statusUpdate?.state) {
setCurrentState(statusUpdate.state)
}
})
} }
return () => { }, [state])
if (printServer && initialized) {
printServer.off('notify_job_update')
}
}
}, [printServer, initialized, job?._id])
return ( return (
<Flex gap='small' align={'center'}> <Flex gap='small' align={'center'}>
{showId && ( {showState && (
<IdDisplay id={job._id} showCopy={false} type='job' longId={false} />
)}
{showQuantity && <Text>({job.quantity})</Text>}
{showStatus && (
<Space> <Space>
<StateTag state={currentState?.type} /> <StateTag state={currentState?.type} />
</Space> </Space>
@ -60,18 +35,9 @@ const JobState = ({
} }
JobState.propTypes = { JobState.propTypes = {
job: PropTypes.shape({ state: PropTypes.object,
_id: PropTypes.string,
quantity: PropTypes.number,
state: PropTypes.shape({
type: PropTypes.string,
progress: PropTypes.number
})
}),
showProgress: PropTypes.bool, showProgress: PropTypes.bool,
showQuantity: PropTypes.bool, showState: PropTypes.bool
showId: PropTypes.bool,
showStatus: PropTypes.bool
} }
export default JobState export default JobState

View File

@ -259,7 +259,7 @@ NoteItem.propTypes = {
onChildNoteAdded: PropTypes.func onChildNoteAdded: PropTypes.func
} }
const DashboardNotes = ({ _id, onNewNote }) => { const NotesPanel = ({ _id, onNewNote }) => {
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)
@ -686,9 +686,9 @@ const DashboardNotes = ({ _id, onNewNote }) => {
) )
} }
DashboardNotes.propTypes = { NotesPanel.propTypes = {
_id: PropTypes.string.isRequired, _id: PropTypes.string.isRequired,
onNewNote: PropTypes.func onNewNote: PropTypes.func
} }
export default DashboardNotes export default NotesPanel

View File

@ -32,6 +32,7 @@ import ColorSelector from './ColorSelector'
import SecretDisplay from './SecretDisplay' import SecretDisplay from './SecretDisplay'
import EyeIcon from '../../Icons/EyeIcon' import EyeIcon from '../../Icons/EyeIcon'
import EyeSlashIcon from '../../Icons/EyeSlashIcon' import EyeSlashIcon from '../../Icons/EyeSlashIcon'
import FilamentStockState from './FilamentStockState'
const { Text } = Typography const { Text } = Typography
@ -57,8 +58,14 @@ const ObjectProperty = ({
readOnly = false, readOnly = false,
...rest ...rest
}) => { }) => {
// Split the name by "." to handle nested object properties
var formItemName = name
if (name?.includes('.')) {
formItemName = name ? name.split('.') : undefined
}
const renderProperty = () => { const renderProperty = () => {
console.log('Rendering')
if (!isEditing || readOnly) { if (!isEditing || readOnly) {
switch (type) { switch (type) {
case 'secret': case 'secret':
@ -120,7 +127,11 @@ const ObjectProperty = ({
} }
case 'number': { case 'number': {
if (value != null) { if (value != null) {
return <Text>{value}</Text> if (Array.isArray(value)) {
return <Text>{value.length}</Text>
} else {
return <Text>{value}</Text>
}
} else { } else {
return <Text type='secondary'>n/a</Text> return <Text type='secondary'>n/a</Text>
} }
@ -151,17 +162,18 @@ const ObjectProperty = ({
} }
} }
case 'state': { case 'state': {
if (value && value?.state) { if (value && value?.type) {
switch (objectType) { switch (objectType) {
case 'printer': case 'printer':
return <PrinterState printer={value} {...rest} /> return <PrinterState state={value} {...rest} />
case 'job': case 'job':
return <JobState job={value} {...rest} /> return <JobState state={value} {...rest} />
case 'subjob': case 'subJob':
return <SubJobState subJob={value} {...rest} /> return <SubJobState state={value} {...rest} />
case 'filamentStock':
return <FilamentStockState state={value} {...rest} />
default: default:
return <Text type='secondary'>n/a</Text> return <Text type='secondary'>No Object Type Specified</Text>
} }
} else { } else {
return <Text type='secondary'>n/a</Text> return <Text type='secondary'>n/a</Text>
@ -251,7 +263,7 @@ const ObjectProperty = ({
switch (type) { switch (type) {
case 'secret': case 'secret':
return ( return (
<Form.Item name={name} {...mergedFormItemProps}> <Form.Item name={formItemName} {...mergedFormItemProps}>
<Input.Password <Input.Password
placeholder={label} placeholder={label}
{...mergedFormItemProps} {...mergedFormItemProps}
@ -263,7 +275,7 @@ const ObjectProperty = ({
) )
case 'wsprotocol': case 'wsprotocol':
return ( return (
<Form.Item name={name} {...mergedFormItemProps}> <Form.Item name={formItemName} {...mergedFormItemProps}>
<Select <Select
defaultValue='ws' defaultValue='ws'
options={[ options={[
@ -276,7 +288,7 @@ const ObjectProperty = ({
case 'bool': case 'bool':
return ( return (
<Form.Item <Form.Item
name={name} name={formItemName}
{...mergedFormItemProps} {...mergedFormItemProps}
valuePropName='checked' valuePropName='checked'
> >
@ -286,7 +298,7 @@ const ObjectProperty = ({
case 'dateTime': case 'dateTime':
return ( return (
<Form.Item <Form.Item
name={name} name={formItemName}
{...mergedFormItemProps} {...mergedFormItemProps}
getValueProps={(v) => ({ value: v ? dayjs(v) : null })} getValueProps={(v) => ({ value: v ? dayjs(v) : null })}
> >
@ -295,7 +307,7 @@ const ObjectProperty = ({
) )
case 'currency': case 'currency':
return ( return (
<Form.Item name={name} {...mergedFormItemProps}> <Form.Item name={formItemName} {...mergedFormItemProps}>
<InputNumber <InputNumber
prefix='£' prefix='£'
suffix='/kg' suffix='/kg'
@ -306,14 +318,14 @@ const ObjectProperty = ({
) )
case 'country': case 'country':
return ( return (
<Form.Item name={name} {...mergedFormItemProps}> <Form.Item name={formItemName} {...mergedFormItemProps}>
<CountrySelect /> <CountrySelect />
</Form.Item> </Form.Item>
) )
case 'color': case 'color':
return ( return (
<Form.Item <Form.Item
name={name} name={formItemName}
{...mergedFormItemProps} {...mergedFormItemProps}
valuePropName='value' valuePropName='value'
getValueFromEvent={(v) => v} getValueFromEvent={(v) => v}
@ -323,7 +335,7 @@ const ObjectProperty = ({
) )
case 'weight': case 'weight':
return ( return (
<Form.Item name={name} {...mergedFormItemProps}> <Form.Item name={formItemName} {...mergedFormItemProps}>
<InputNumber <InputNumber
suffix='g' suffix='g'
style={{ width: '100%' }} style={{ width: '100%' }}
@ -333,7 +345,7 @@ const ObjectProperty = ({
) )
case 'number': case 'number':
return ( return (
<Form.Item name={name} {...mergedFormItemProps}> <Form.Item name={formItemName} {...mergedFormItemProps}>
<InputNumber <InputNumber
style={{ width: '100%' }} style={{ width: '100%' }}
placeholder={label} placeholder={label}
@ -343,13 +355,13 @@ const ObjectProperty = ({
) )
case 'text': case 'text':
return ( return (
<Form.Item name={name} {...mergedFormItemProps}> <Form.Item name={formItemName} {...mergedFormItemProps}>
<Input placeholder={label} {...mergedFormItemProps} /> <Input placeholder={label} {...mergedFormItemProps} />
</Form.Item> </Form.Item>
) )
case 'material': case 'material':
return ( return (
<Form.Item name={name} {...mergedFormItemProps}> <Form.Item name={formItemName} {...mergedFormItemProps}>
<Select options={MATERIAL_OPTIONS} placeholder={label} /> <Select options={MATERIAL_OPTIONS} placeholder={label} />
</Form.Item> </Form.Item>
) )
@ -364,31 +376,31 @@ const ObjectProperty = ({
switch (objectType) { switch (objectType) {
case 'vendor': case 'vendor':
return ( return (
<Form.Item name={name} {...mergedFormItemProps}> <Form.Item name={formItemName} {...mergedFormItemProps}>
<VendorSelect placeholder={label} /> <VendorSelect placeholder={label} />
</Form.Item> </Form.Item>
) )
case 'printer': case 'printer':
return ( return (
<Form.Item name={name} {...mergedFormItemProps}> <Form.Item name={formItemName} {...mergedFormItemProps}>
<PrinterSelect placeholder={label} /> <PrinterSelect placeholder={label} />
</Form.Item> </Form.Item>
) )
case 'gcodefile': case 'gcodefile':
return ( return (
<Form.Item name={name} {...mergedFormItemProps}> <Form.Item name={formItemName} {...mergedFormItemProps}>
<GCodeFileSelect placeholder={label} /> <GCodeFileSelect placeholder={label} />
</Form.Item> </Form.Item>
) )
case 'filament': case 'filament':
return ( return (
<Form.Item name={name} {...mergedFormItemProps}> <Form.Item name={formItemName} {...mergedFormItemProps}>
<FilamentSelect placeholder={label} /> <FilamentSelect placeholder={label} />
</Form.Item> </Form.Item>
) )
case 'part': case 'part':
return ( return (
<Form.Item name={name} {...mergedFormItemProps}> <Form.Item name={formItemName} {...mergedFormItemProps}>
<PartSelect placeholder={label} /> <PartSelect placeholder={label} />
</Form.Item> </Form.Item>
) )
@ -398,7 +410,7 @@ const ObjectProperty = ({
case 'density': case 'density':
return ( return (
<Form.Item name={name} {...mergedFormItemProps}> <Form.Item name={formItemName} {...mergedFormItemProps}>
<InputNumber <InputNumber
suffix='g/cm³' suffix='g/cm³'
style={{ width: '100%' }} style={{ width: '100%' }}
@ -408,7 +420,7 @@ const ObjectProperty = ({
) )
case 'mm': case 'mm':
return ( return (
<Form.Item name={name} {...mergedFormItemProps}> <Form.Item name={formItemName} {...mergedFormItemProps}>
<InputNumber <InputNumber
suffix='mm' suffix='mm'
style={{ width: '100%' }} style={{ width: '100%' }}
@ -418,13 +430,13 @@ const ObjectProperty = ({
) )
case 'tags': case 'tags':
return ( return (
<Form.Item name={name} {...mergedFormItemProps}> <Form.Item name={formItemName} {...mergedFormItemProps}>
<TagsInput /> <TagsInput />
</Form.Item> </Form.Item>
) )
default: default:
return ( return (
<Form.Item name={name} {...mergedFormItemProps}> <Form.Item name={formItemName} {...mergedFormItemProps}>
<Input placeholder={label} {...mergedFormItemProps} /> <Input placeholder={label} {...mergedFormItemProps} />
</Form.Item> </Form.Item>
) )
@ -437,18 +449,7 @@ const ObjectProperty = ({
} }
ObjectProperty.propTypes = { ObjectProperty.propTypes = {
type: PropTypes.oneOf([ type: PropTypes.string.isRequired,
'text',
'number',
'currency',
'color',
'weight',
'vendor',
'material',
'id',
'density',
'mm'
]),
value: PropTypes.any, value: PropTypes.any,
isEditing: PropTypes.bool, isEditing: PropTypes.bool,
formItemProps: PropTypes.object, formItemProps: PropTypes.object,
@ -456,10 +457,8 @@ ObjectProperty.propTypes = {
name: PropTypes.string, name: PropTypes.string,
label: PropTypes.string, label: PropTypes.string,
showLabel: PropTypes.bool, showLabel: PropTypes.bool,
objectType: PropTypes.string.isRequired, objectType: PropTypes.string,
readOnly: PropTypes.bool readOnly: PropTypes.bool
} }
ObjectProperty.defaultProps = {}
export default ObjectProperty export default ObjectProperty

View File

@ -2,12 +2,41 @@ import React, { useEffect, useState, useCallback } from 'react'
import PropTypes from 'prop-types' import PropTypes from 'prop-types'
import { TreeSelect, Typography, Flex, Badge, Space, Button, Input } from 'antd' import { TreeSelect, Typography, Flex, Badge, Space, Button, Input } from 'antd'
import axios from 'axios' import axios from 'axios'
import { getTypeMeta } from '../utils/Utils' import { getModelByName } from '../../../database/ObjectModels'
import IdDisplay from './IdDisplay' import IdDisplay from './IdDisplay'
import CountryDisplay from './CountryDisplay'
import ReloadIcon from '../../Icons/ReloadIcon' import ReloadIcon from '../../Icons/ReloadIcon'
import ObjectProperty from './ObjectProperty'
const { Text } = Typography const { Text } = Typography
const { SHOW_CHILD } = TreeSelect const { SHOW_CHILD } = TreeSelect
// --- Utility: Resolve nested property path (e.g., 'filament.diameter') ---
function resolvePropertyPath(obj, path) {
if (!obj || !path) return { value: undefined, finalProperty: undefined }
const props = path.split('.')
let value = obj
for (const prop of props) {
if (value && typeof value === 'object') {
value = value[prop]
} else {
return { value: undefined, finalProperty: prop }
}
}
return { value, finalProperty: props[props.length - 1] }
}
// --- Utility: Build filter object for a node based on propertyOrder ---
function buildFilterForNode(node, treeData, propertyOrder) {
let filterObj = {}
let currentId = node.id
while (currentId !== 0) {
const currentNode = treeData.find((d) => d.id === currentId)
if (!currentNode) break
filterObj[propertyOrder[currentNode.propertyId]] = currentNode.value
currentId = currentNode.pId
}
return filterObj
}
/** /**
* ObjectSelect - a generic, reusable async TreeSelect for hierarchical object selection. * ObjectSelect - a generic, reusable async TreeSelect for hierarchical object selection.
* *
@ -35,29 +64,14 @@ const ObjectSelect = ({
type = 'unknown', type = 'unknown',
...rest ...rest
}) => { }) => {
// --- State ---
const [treeData, setTreeData] = useState([]) const [treeData, setTreeData] = useState([])
const [loading, setLoading] = useState(false) const [loading, setLoading] = useState(false)
const [defaultValue, setDefaultValue] = useState(treeCheckable ? [] : value) const [defaultValue, setDefaultValue] = useState(treeCheckable ? [] : value)
const [searchValue, setSearchValue] = useState('') const [searchValue, setSearchValue] = useState('')
const [error, setError] = useState(false) const [error, setError] = useState(false)
// Helper to get filter object for a node // --- API: Fetch data for a property level or leaf ---
const getFilter = useCallback(
(node) => {
let filterObj = {}
let currentId = node.id
while (currentId !== 0) {
const currentNode = treeData.find((d) => d.id === currentId)
if (!currentNode) break
filterObj[propertyOrder[currentNode.propertyId]] = currentNode.value
currentId = currentNode.pId
}
return filterObj
},
[treeData, propertyOrder]
)
// Fetch data from API
const fetchData = useCallback( const fetchData = useCallback(
async (property, filter, search) => { async (property, filter, search) => {
setLoading(true) setLoading(true)
@ -74,14 +88,13 @@ const ObjectSelect = ({
} catch (err) { } catch (err) {
setLoading(false) setLoading(false)
setError(true) setError(true)
// Optionally handle error
return [] return []
} }
}, },
[endpoint] [endpoint]
) )
// Fetch single object by ID // --- API: Fetch a single object by ID ---
const fetchObjectById = useCallback( const fetchObjectById = useCallback(
async (objectId) => { async (objectId) => {
setLoading(true) setLoading(true)
@ -95,305 +108,134 @@ const ObjectSelect = ({
} catch (err) { } catch (err) {
setLoading(false) setLoading(false)
setError(true) setError(true)
console.error('Failed to fetch object by ID:', err)
return null return null
} }
}, },
[endpoint] [endpoint]
) )
// Helper to render the title for a node // --- Render node title ---
const renderTitle = useCallback( const renderTitle = useCallback(
(item, isLeaf) => { (item) => {
if (!isLeaf) { if (item.propertyType) {
// For category nodes, check if it's a country property return (
const currentProperty = propertyOrder[item.propertyId] <ObjectProperty
if (currentProperty === 'country' && item.value) { type={item.propertyType}
return <CountryDisplay countryCode={item.value} /> value={item.value}
} objectType={type}
// For other category nodes, just show the value />
return <Text>{item[propertyOrder[item.propertyId]] || item.value}</Text> )
}
// For leaf nodes, show icon, name, and id
const meta = getTypeMeta(type)
const Icon = meta.icon
return (
<Flex gap={'small'} align='center' style={{ width: '100%' }}>
{Icon && <Icon />}
{item?.color && <Badge color={item.color}></Badge>}
<Text ellipsis>{item.name || type.title}</Text>
<IdDisplay id={item._id} longId={false} type={type} />
</Flex>
)
},
[propertyOrder, type]
)
// Build tree path for a default object
const buildTreePathForObject = useCallback(
async (object) => {
if (!object || !propertyOrder || propertyOrder.length === 0) return
const newNodes = []
let currentPId = 0
// Build category nodes for each property level and load all available options
for (let i = 0; i < propertyOrder.length; i++) {
const propertyName = propertyOrder[i]
let propertyValue
// Handle nested property access (e.g., 'filament.diameter')
if (propertyName.includes('.')) {
const propertyPath = propertyName.split('.')
let currentValue = object
for (const prop of propertyPath) {
if (currentValue && typeof currentValue === 'object') {
currentValue = currentValue[prop]
} else {
currentValue = undefined
break
}
}
propertyValue = currentValue
} else {
propertyValue = object[propertyName]
}
// Build filter for this level
let filterObj = {}
for (let j = 0; j < i; j++) {
const prevPropertyName = propertyOrder[j]
let prevPropertyValue
if (prevPropertyName.includes('.')) {
const propertyPath = prevPropertyName.split('.')
let currentValue = object
for (const prop of propertyPath) {
if (currentValue && typeof currentValue === 'object') {
currentValue = currentValue[prop]
} else {
currentValue = undefined
break
}
}
prevPropertyValue = currentValue
} else {
prevPropertyValue = object[prevPropertyName]
}
if (prevPropertyValue !== undefined && prevPropertyValue !== null) {
filterObj[prevPropertyName] = prevPropertyValue
}
}
// Fetch all available options for this property level
const data = await fetchData(propertyName, filterObj, '')
// Create nodes for all available options at this level
const levelNodes = data.map((item) => {
let value
if (typeof item === 'object' && item !== null) {
if (propertyName.includes('.')) {
const propertyPath = propertyName.split('.')
let currentValue = item
for (const prop of propertyPath) {
if (currentValue && typeof currentValue === 'object') {
currentValue = currentValue[prop]
} else {
currentValue = undefined
break
}
}
value = currentValue
} else {
value = item[propertyName]
}
} else {
value = item
}
return {
id: value,
pId: currentPId,
value: value,
key: value,
propertyId: i,
title: renderTitle({ ...item, value }, false),
isLeaf: false,
selectable: false,
raw: item
}
})
newNodes.push(...levelNodes)
// Update currentPId to the object's property value for the next level
if (propertyValue !== undefined && propertyValue !== null) {
currentPId = propertyValue
}
}
// Load all leaf nodes at the final level
let finalFilterObj = {}
for (let j = 0; j < propertyOrder.length - 1; j++) {
const prevPropertyName = propertyOrder[j]
let prevPropertyValue
if (prevPropertyName.includes('.')) {
const propertyPath = prevPropertyName.split('.')
let currentValue = object
for (const prop of propertyPath) {
if (currentValue && typeof currentValue === 'object') {
currentValue = currentValue[prop]
} else {
currentValue = undefined
break
}
}
prevPropertyValue = currentValue
} else {
prevPropertyValue = object[prevPropertyName]
}
if (prevPropertyValue !== undefined && prevPropertyValue !== null) {
finalFilterObj[prevPropertyName] = prevPropertyValue
}
}
const leafData = await fetchData(null, finalFilterObj, '')
const leafNodes = leafData.map((item) => ({
id: item._id || item.id || item.value,
pId: currentPId,
value: item._id || item.id || item.value,
key: item._id || item.id || item.value,
title: renderTitle(item, true),
isLeaf: true,
raw: item
}))
newNodes.push(...leafNodes)
setTreeData(newNodes)
setDefaultValue(object._id || object.id)
},
[propertyOrder, renderTitle, fetchData]
)
// Generate leaf nodes
const generateLeafNodes = useCallback(
async (node = null, filterArg = null, search = '') => {
if (!node) return
const actualFilter = filterArg === null ? getFilter(node) : filterArg
const data = await fetchData(null, actualFilter, search)
const newNodes = data.map((item) => {
const isLeaf = true
return {
id: item._id || item.id || item.value,
pId: node.id,
value: item._id || item.id || item.value,
key: item._id || item.id || item.value,
title: renderTitle(item, isLeaf),
isLeaf: true,
raw: item
}
})
setTreeData((prev) => [...prev, ...newNodes])
},
[fetchData, getFilter, renderTitle]
)
// Generate category nodes
const generateCategoryNodes = useCallback(
async (node = null, search = '') => {
let filterObj = {}
let propertyId = 0
if (!node) {
node = { id: 0 }
} else { } else {
filterObj = getFilter(node) const model = getModelByName(type)
propertyId = node.propertyId + 1 const Icon = model.icon
return (
<Flex gap={'small'} align='center' style={{ width: '100%' }}>
{Icon && <Icon />}
{item?.color && <Badge color={item.color}></Badge>}
<Text ellipsis>{item.name || type.title}</Text>
<IdDisplay id={item._id} longId={false} type={type} />
</Flex>
)
} }
const propertyName = propertyOrder[propertyId] },
const data = await fetchData(propertyName, filterObj, search) [type]
const newNodes = data.map((item) => { )
const isLeaf = false
// Handle both cases: when item is a simple value or when it's an object // --- Build tree nodes for a property level ---
let value const buildCategoryNodes = useCallback(
if (typeof item === 'object' && item !== null) { (data, propertyName, propertyId, parentId) => {
// Handle nested property access (e.g., 'filament.diameter') return data.map((item) => {
if (propertyName.includes('.')) { let resolved = resolvePropertyPath(item, propertyName)
const propertyPath = propertyName.split('.') let value = resolved.value
let currentValue = item let propertyType = resolved.finalProperty
for (const prop of propertyPath) {
if (currentValue && typeof currentValue === 'object') {
currentValue = currentValue[prop]
} else {
currentValue = undefined
break
}
}
value = currentValue
} else {
// If item is an object, try to get the property value
value = item[propertyName]
}
} else {
// If item is a simple value (string, number, etc.), use it directly
value = item
}
const title = renderTitle({ ...item, value }, isLeaf)
return { return {
id: value, id: value,
pId: node.id, pId: parentId,
value: value, value: value,
key: value, key: value,
propertyId: propertyId, propertyId: propertyId,
title: title, title: renderTitle({ ...item, value, propertyType }),
isLeaf: false, isLeaf: false,
selectable: false, selectable: false,
raw: item raw: item
} }
}) })
setTreeData((prev) => [...prev, ...newNodes])
}, },
[fetchData, getFilter, propertyOrder, renderTitle] [renderTitle]
) )
// Tree loader // --- Build tree nodes for leaf level ---
const buildLeafNodes = useCallback(
(data, parentId) => {
return data.map((item) => {
const value = item._id || item.id || item.value
return {
id: value,
pId: parentId,
value: value,
key: value,
title: renderTitle(item),
isLeaf: true,
raw: item
}
})
},
[renderTitle]
)
// --- Tree loader: load children for a node or root ---
const handleTreeLoad = useCallback( const handleTreeLoad = useCallback(
async (node) => { async (node) => {
if (!propertyOrder.length) return
if (node) { if (node) {
// Not at leaf level yet
if (node.propertyId !== propertyOrder.length - 1) { if (node.propertyId !== propertyOrder.length - 1) {
await generateCategoryNodes(node, searchValue) const nextPropertyId = node.propertyId + 1
const propertyName = propertyOrder[nextPropertyId]
const filterObj = buildFilterForNode(node, treeData, propertyOrder)
const data = await fetchData(propertyName, filterObj, searchValue)
setTreeData((prev) => [
...prev,
...buildCategoryNodes(data, propertyName, nextPropertyId, node.id)
])
} else { } else {
await generateLeafNodes(node, null, searchValue) // At leaf level
const filterObj = buildFilterForNode(node, treeData, propertyOrder)
const data = await fetchData(null, filterObj, searchValue)
setTreeData((prev) => [...prev, ...buildLeafNodes(data, node.id)])
} }
} else { } else {
await generateCategoryNodes(null, searchValue) // Root load
const propertyName = propertyOrder[0]
const data = await fetchData(propertyName, {}, searchValue)
setTreeData(buildCategoryNodes(data, propertyName, 0, 0))
} }
}, },
[propertyOrder, generateCategoryNodes, generateLeafNodes, searchValue] [
propertyOrder,
treeData,
fetchData,
buildCategoryNodes,
buildLeafNodes,
searchValue
]
) )
// OnChange handler // --- OnChange handler ---
const handleOnChange = (val, selectedOptions) => { const handleOnChange = (val, selectedOptions) => {
if (onChange) { if (onChange) {
if (treeCheckable) { if (treeCheckable) {
// Handle multiple selections with checkboxes // Multi-select
const selectedObjects = [] let selectedObjects = []
if (Array.isArray(val)) { if (Array.isArray(val)) {
val.forEach((selectedValue) => { selectedObjects = val.map((selectedValue) => {
const node = treeData.find((n) => n.value === selectedValue) const node = treeData.find((n) => n.value === selectedValue)
if (node) { return node ? node.raw : selectedValue
selectedObjects.push(node.raw)
} else {
selectedObjects.push(selectedValue)
}
}) })
} }
onChange(selectedObjects, selectedOptions) onChange(selectedObjects, selectedOptions)
} else { } else {
// Handle single selection // Single select
const node = treeData.find((n) => n.value === val) const node = treeData.find((n) => n.value === val)
onChange(node ? node.raw : val, selectedOptions) onChange(node ? node.raw : val, selectedOptions)
} }
@ -401,21 +243,18 @@ const ObjectSelect = ({
setDefaultValue(val) setDefaultValue(val)
} }
// Search handler // --- Search handler ---
const handleSearch = (val) => { const handleSearch = (val) => {
setSearchValue(val) setSearchValue(val)
setTreeData([]) setTreeData([])
} }
// Keep defaultValue in sync and handle object values // --- Sync defaultValue and load tree path for object values ---
useEffect(() => { useEffect(() => {
if (treeCheckable) { if (treeCheckable) {
// Handle array of values for multi-select
if (Array.isArray(value)) { if (Array.isArray(value)) {
const valueIds = value.map((v) => v._id || v.id || v) const valueIds = value.map((v) => v._id || v.id || v)
setDefaultValue(valueIds) setDefaultValue(valueIds)
// Load tree paths for any objects that aren't already loaded
value.forEach((item) => { value.forEach((item) => {
if (item && typeof item === 'object' && item._id) { if (item && typeof item === 'object' && item._id) {
const existingNode = treeData.find( const existingNode = treeData.find(
@ -424,7 +263,14 @@ const ObjectSelect = ({
if (!existingNode) { if (!existingNode) {
fetchObjectById(item._id).then((object) => { fetchObjectById(item._id).then((object) => {
if (object) { if (object) {
buildTreePathForObject(object) // For multi-select, just add the leaf node
setTreeData((prev) => [
...prev,
...buildLeafNodes(
[object],
object[propertyOrder[propertyOrder.length - 2]] || 0
)
])
} }
}) })
} }
@ -434,36 +280,44 @@ const ObjectSelect = ({
setDefaultValue([]) setDefaultValue([])
} }
} else { } else {
// Handle single value
if (value?._id) { if (value?._id) {
setDefaultValue(value._id) setDefaultValue(value._id)
}
// Check if value is an object with _id (default object case)
if (value && typeof value === 'object' && value._id) {
// If we already have this object loaded, don't fetch again
const existingNode = treeData.find((node) => node.value === value._id) const existingNode = treeData.find((node) => node.value === value._id)
if (!existingNode) { if (!existingNode) {
fetchObjectById(value._id).then((object) => { fetchObjectById(value._id).then((object) => {
if (object) { if (object) {
buildTreePathForObject(object) setTreeData((prev) => [
...prev,
...buildLeafNodes(
[object],
object[propertyOrder[propertyOrder.length - 2]] || 0
)
])
} }
}) })
} }
} }
} }
}, [value, treeData, fetchObjectById, buildTreePathForObject, treeCheckable]) }, [
value,
treeData,
fetchObjectById,
buildLeafNodes,
propertyOrder,
treeCheckable
])
// Initial load // --- Initial load ---
useEffect(() => { useEffect(() => {
if (treeData.length === 0 && !error && !loading) { if (treeData.length === 0 && !error && !loading) {
// If we have a default object value, don't load the regular tree
if (!treeCheckable && value && typeof value === 'object' && value._id) { if (!treeCheckable && value && typeof value === 'object' && value._id) {
return return
} }
if (useFilter || searchValue) { if (useFilter || searchValue) {
generateLeafNodes({ id: 0 }, filter, searchValue) // Flat filter mode
fetchData(null, filter, searchValue).then((data) => {
setTreeData(buildLeafNodes(data, 0))
})
} else { } else {
handleTreeLoad(null) handleTreeLoad(null)
} }
@ -473,7 +327,8 @@ const ObjectSelect = ({
useFilter, useFilter,
filter, filter,
searchValue, searchValue,
generateLeafNodes, buildLeafNodes,
fetchData,
handleTreeLoad, handleTreeLoad,
error, error,
loading, loading,
@ -481,20 +336,25 @@ const ObjectSelect = ({
treeCheckable treeCheckable
]) ])
return error ? ( // --- Error UI ---
<Space.Compact style={{ width: '100%' }}> if (error) {
<Input value='Failed to load data.' status='error' disabled /> return (
<Space.Compact style={{ width: '100%' }}>
<Input value='Failed to load data.' status='error' disabled />
<Button
icon={<ReloadIcon />}
onClick={() => {
setError(false)
setTreeData([])
}}
danger
/>
</Space.Compact>
)
}
<Button // --- Main TreeSelect UI ---
icon={<ReloadIcon />} return (
onClick={() => {
setError(false)
setTreeData([])
}}
danger
/>
</Space.Compact>
) : (
<TreeSelect <TreeSelect
treeDataSimpleMode treeDataSimpleMode
treeDefaultExpandAll={true} treeDefaultExpandAll={true}

View File

@ -26,7 +26,7 @@ import loglevel from 'loglevel'
const logger = loglevel.getLogger('DasboardTable') const logger = loglevel.getLogger('DasboardTable')
logger.setLevel(config.logLevel) logger.setLevel(config.logLevel)
const DashboardTable = forwardRef( const ObjectTable = forwardRef(
( (
{ {
columns, columns,
@ -453,9 +453,9 @@ const DashboardTable = forwardRef(
} }
) )
DashboardTable.displayName = 'DashboardTable' ObjectTable.displayName = 'ObjectTable'
DashboardTable.propTypes = { ObjectTable.propTypes = {
columns: PropTypes.arrayOf(PropTypes.object).isRequired, columns: PropTypes.arrayOf(PropTypes.object).isRequired,
url: PropTypes.string.isRequired, url: PropTypes.string.isRequired,
pageSize: PropTypes.number, pageSize: PropTypes.number,
@ -467,4 +467,4 @@ DashboardTable.propTypes = {
cardRenderer: PropTypes.func cardRenderer: PropTypes.func
} }
export default DashboardTable export default ObjectTable

View File

@ -62,7 +62,7 @@ const PrinterJobsTree = ({
<Space size={5}> <Space size={5}>
<JobIcon /> <JobIcon />
{'Job'} {'Job'}
<JobState job={job} /> <JobState state={job?.state} />
</Space> </Space>
), ),
key: `job-${job._id}`, key: `job-${job._id}`,

View File

@ -1,50 +1,18 @@
// PrinterSelect.js // PrinterSelect.js
import PropTypes from 'prop-types' import PropTypes from 'prop-types'
import { Progress, Flex, Space, Typography, Button, Tooltip } from 'antd' import { Progress, Flex, Space } from 'antd'
import React, { useState, useContext, useEffect } from 'react' import React from 'react'
import { PrintServerContext } from '../context/PrintServerContext'
import { CaretLeftOutlined } from '@ant-design/icons'
import XMarkIcon from '../../Icons/XMarkIcon'
import PauseIcon from '../../Icons/PauseIcon'
import StateTag from './StateTag' import StateTag from './StateTag'
const PrinterState = ({ const PrinterState = ({ state, showProgress = true, showState = true }) => {
printer, const currentState = state || {
showProgress = true, type: 'unknown',
showStatus = true, progress: 0
showName = true, }
showControls = true
}) => {
const { printServer } = useContext(PrintServerContext)
const [currentState, setCurrentState] = useState(
printer?.state || {
type: 'unknown',
progress: 0
}
)
const [initialized, setInitialized] = useState(false)
const { Text } = Typography
useEffect(() => {
if (printServer && !initialized && printer?.id) {
setInitialized(true)
printServer.on('notify_printer_update', (statusUpdate) => {
if (statusUpdate?._id === printer.id && statusUpdate?.state) {
setCurrentState(statusUpdate.state)
}
})
}
return () => {
if (printServer && initialized) {
printServer.off('notify_printer_update')
}
}
}, [printServer, initialized, printer?.id])
return ( return (
<Flex gap='small' align={'center'}> <Flex gap='small' align={'center'}>
{showName && <Text>{printer.name}</Text>} {showState && (
{showStatus && (
<Space> <Space>
<StateTag state={currentState.type} /> <StateTag state={currentState.type} />
</Space> </Space>
@ -58,72 +26,14 @@ const PrinterState = ({
style={{ width: '150px', marginBottom: '2px' }} style={{ width: '150px', marginBottom: '2px' }}
/> />
) : null} ) : null}
{showControls && currentState.type === 'printing' ? (
<Space.Compact>
<Tooltip
title={currentState.type === 'printing' ? 'Pause' : 'Resume'}
arrow={false}
>
<Button
onClick={() => {
if (currentState.type === 'printing') {
printServer.emit('printer.print.pause', {
printerId: printer.id
})
} else {
printServer.emit('printer.print.resume', {
printerId: printer.id
})
}
}}
style={{ height: '22px' }}
type='text'
icon={
currentState.type === 'printing' ? (
<PauseIcon
style={{ fontSize: '12px', marginBottom: '3px' }}
/>
) : (
<CaretLeftOutlined
style={{ fontSize: '10px', marginBottom: '3px' }}
/>
)
}
></Button>
</Tooltip>
<Tooltip title='Cancel' arrow={false}>
<Button
onClick={() => {
printServer.emit('printer.print.cancel', {
printerId: printer.id
})
}}
type='text'
style={{ height: '22px' }}
icon={
<XMarkIcon style={{ fontSize: '12px', marginBottom: '3px' }} />
}
/>
</Tooltip>
</Space.Compact>
) : null}
</Flex> </Flex>
) )
} }
PrinterState.propTypes = { PrinterState.propTypes = {
printer: PropTypes.shape({ state: PropTypes.object,
id: PropTypes.string,
name: PropTypes.string,
state: PropTypes.shape({
type: PropTypes.string,
progress: PropTypes.number
})
}),
showProgress: PropTypes.bool, showProgress: PropTypes.bool,
showStatus: PropTypes.bool, showState: PropTypes.bool
showName: PropTypes.bool,
showControls: PropTypes.bool
} }
export default PrinterState export default PrinterState

View File

@ -7,24 +7,17 @@ import {
Flex, Flex,
Typography, Typography,
Skeleton, Skeleton,
Spin, Spin
Badge
} from 'antd' } from 'antd'
import { LoadingOutlined, ExportOutlined } from '@ant-design/icons' import { LoadingOutlined } from '@ant-design/icons'
import React, { useEffect, useState, useContext, useCallback } from 'react' import React, { useEffect, useState, useContext, useCallback } from 'react'
import axios from 'axios' import axios from 'axios'
import { AuthContext } from '../context/AuthContext' import { AuthContext } from '../context/AuthContext'
import config from '../../../config' import config from '../../../config'
import IdDisplay from './IdDisplay' import ObjectProperty from './ObjectProperty'
import TimeDisplay from './TimeDisplay'
import { Tag } from 'antd'
import InfoCircleIcon from '../../Icons/InfoCircleIcon' import InfoCircleIcon from '../../Icons/InfoCircleIcon'
import PrinterState from './PrinterState'
import JobState from './JobState'
import FilamentStockState from './FilamentStockState'
import SubJobState from './SubJobState'
const { Text, Link } = Typography const { Text } = Typography
const SpotlightTooltip = ({ query, type }) => { const SpotlightTooltip = ({ query, type }) => {
const [spotlightData, setSpotlightData] = useState(null) const [spotlightData, setSpotlightData] = useState(null)
@ -89,101 +82,18 @@ const SpotlightTooltip = ({ query, type }) => {
) )
} }
// Helper to render value nicely // Helper to determine property type based on key and value
const renderValue = (key, value) => { const getPropertyType = (key, value) => {
if (key === '_id' || key === 'id') { if (key === '_id') {
return ( return 'id'
<IdDisplay
id={value}
type={type}
showCopy={true}
longId={false}
showSpotlight={false}
/>
)
} }
if (key === 'state') { if (key === 'createdAt' || key === 'updatedAt') {
if (type === 'printer') { return 'dateTime'
return (
<PrinterState
printer={spotlightData}
showControls={false}
showName={false}
/>
)
}
if (type === 'job') {
return (
<JobState job={spotlightData} showId={false} showQuantity={false} />
)
}
if (type === 'subjob') {
return (
<SubJobState
subJob={spotlightData}
showId={false}
showQuantity={false}
/>
)
}
if (type === 'filamentstock') {
return (
<FilamentStockState
filamentStock={spotlightData}
showProgress={false}
/>
)
}
} }
if (key === 'tags' && Array.isArray(value)) { if (typeof value === 'boolean') {
if (value.length == 0) { return 'bool'
return <Text>n/a</Text>
}
return value.map((tag) => (
<Tag key={tag} color='blue'>
{tag}
</Tag>
))
} }
if (key === 'email') { return key
return (
<Link href={`mailto:${value}`}>
{value + ' '}
<ExportOutlined />
</Link>
)
}
if (key === 'color') {
return <Badge color={value} text={value} />
}
if (
typeof value === 'string' &&
value.match(/^[0-9]{4}-[0-9]{2}-[0-9]{2}T[0-9]{2}:[0-9]{2}:[0-9]{2}/)
) {
// Format ISO date strings
return (
<TimeDisplay
dateTime={value}
showDate={true}
showTime={true}
showSince={true}
/>
)
}
if (Array.isArray(value)) {
return value.join(', ')
}
if (typeof value === 'object' && value !== null) {
// For nested objects, show JSON string
return JSON.stringify(value)
}
if (value == '' || value.length == 0) {
return <Text>n/a</Text>
}
if (value != null) {
return <Text>{value.toString()}</Text>
}
return <Text>n/a</Text>
} }
// Map of property names to user-friendly labels // Map of property names to user-friendly labels
@ -234,10 +144,18 @@ const SpotlightTooltip = ({ query, type }) => {
LABEL_MAP[key] || key.charAt(0).toUpperCase() + key.slice(1) LABEL_MAP[key] || key.charAt(0).toUpperCase() + key.slice(1)
} }
> >
{renderValue( <ObjectProperty
key, type={getPropertyType(key)}
key === 'state' && value.type ? value.type : value value={key == 'state' ? spotlightData : value}
)} objectType={type}
isEditing={false}
longId={false}
showSpotlight={false}
showLabel={false}
showName={false}
showId={false}
showQuantity={false}
/>
</Descriptions.Item> </Descriptions.Item>
) : null ) : null
) )

View File

@ -1,7 +1,7 @@
import PropTypes from 'prop-types' import PropTypes from 'prop-types'
import { Progress, Flex, Button, Space, Tooltip } from 'antd' // eslint-disable-line import { Progress, Flex, Button, Space, Tooltip } from 'antd' // eslint-disable-line
import { CaretLeftOutlined } from '@ant-design/icons' // eslint-disable-line import { CaretLeftOutlined } from '@ant-design/icons' // eslint-disable-line
import React, { useState, useContext, useEffect } from 'react' import React, { useContext } from 'react'
import { PrintServerContext } from '../context/PrintServerContext' import { PrintServerContext } from '../context/PrintServerContext'
import IdDisplay from './IdDisplay' import IdDisplay from './IdDisplay'
import StateTag from './StateTag' import StateTag from './StateTag'
@ -14,45 +14,22 @@ const logger = loglevel.getLogger('SubJobState')
logger.setLevel(config.logLevel) logger.setLevel(config.logLevel)
const SubJobState = ({ const SubJobState = ({
subJob, state,
showStatus = true, showStatus = true,
showId = true, showId = true,
showProgress = true, showProgress = true,
showControls = true //eslint-disable-line showControls = true //eslint-disable-line
}) => { }) => {
const { printServer } = useContext(PrintServerContext) const { printServer } = useContext(PrintServerContext)
const [currentState, setCurrentState] = useState( const currentState = state || {
subJob?.state || { type: 'unknown',
type: 'unknown', progress: 0
progress: 0 }
}
)
const [initialized, setInitialized] = useState(false)
useEffect(() => {
if (printServer && !initialized && subJob?._id) {
setInitialized(true)
logger.debug('on notify_subjob_update')
printServer.on('notify_subjob_update', (statusUpdate) => {
if (statusUpdate?._id === subJob._id && statusUpdate?.state) {
logger.debug('statusUpdate', statusUpdate)
setCurrentState(statusUpdate.state)
}
})
}
return () => {
if (printServer && initialized) {
logger.debug('off notify_subjob_update')
printServer.off('notify_subjob_update')
}
}
}, [printServer, initialized, subJob?._id])
return ( return (
<Flex gap='small' align={'center'}> <Flex gap='small' align={'center'}>
{showId && ( {showId && state?._id && (
<IdDisplay <IdDisplay
id={subJob._id} id={state._id}
showCopy={false} showCopy={false}
type='subjob' type='subjob'
longId={false} longId={false}
@ -73,7 +50,8 @@ const SubJobState = ({
/> />
) : null} ) : null}
{showControls && {showControls &&
(currentState.type === 'printing' || currentState.type === 'paused') ? ( (currentState.type === 'printing' || currentState.type === 'paused') &&
state?.printer ? (
<Space.Compact> <Space.Compact>
<Tooltip <Tooltip
title={currentState.type === 'printing' ? 'Pause' : 'Resume'} title={currentState.type === 'printing' ? 'Pause' : 'Resume'}
@ -83,11 +61,11 @@ const SubJobState = ({
onClick={() => { onClick={() => {
if (currentState.type === 'printing') { if (currentState.type === 'printing') {
printServer.emit('printer.print.pause', { printServer.emit('printer.print.pause', {
printerId: subJob.printer printerId: state.printer
}) })
} else { } else {
printServer.emit('printer.print.resume', { printServer.emit('printer.print.resume', {
printerId: subJob.printer printerId: state.printer
}) })
} }
}} }}
@ -110,7 +88,7 @@ const SubJobState = ({
<Button <Button
onClick={() => { onClick={() => {
printServer.emit('printer.print.cancel', { printServer.emit('printer.print.cancel', {
printerId: subJob.printer printerId: state.printer
}) })
}} }}
type='text' type='text'
@ -122,12 +100,12 @@ const SubJobState = ({
</Tooltip> </Tooltip>
</Space.Compact> </Space.Compact>
) : null} ) : null}
{showControls && currentState.type === 'queued' ? ( {showControls && currentState.type === 'queued' && state?._id ? (
<Tooltip title='Cancel' arrow={false}> <Tooltip title='Cancel' arrow={false}>
<Button <Button
onClick={() => { onClick={() => {
printServer.emit('server.job_queue.cancel', { printServer.emit('server.job_queue.cancel', {
subJobId: subJob._id subJobId: state._id
}) })
}} }}
style={{ height: '22px' }} style={{ height: '22px' }}
@ -159,16 +137,7 @@ const SubJobState = ({
} }
SubJobState.propTypes = { SubJobState.propTypes = {
subJob: PropTypes.shape({ state: PropTypes.object,
_id: PropTypes.string,
subJobId: PropTypes.string,
printer: PropTypes.string,
number: PropTypes.number,
state: PropTypes.shape({
type: PropTypes.string,
progress: PropTypes.number
})
}),
showProgress: PropTypes.bool, showProgress: PropTypes.bool,
showControls: PropTypes.bool, showControls: PropTypes.bool,
showId: PropTypes.bool, showId: PropTypes.bool,

View File

@ -1,6 +1,6 @@
// PrinterSelect.js // PrinterSelect.js
import PropTypes from 'prop-types' import PropTypes from 'prop-types'
import { Tree, Card, Spin, Space, Button, message } from 'antd' import { Tree, Card, Spin, Space, Button, message, Typography } from 'antd'
import { LoadingOutlined } from '@ant-design/icons' import { LoadingOutlined } from '@ant-design/icons'
import React, { useState, useEffect, useContext, useCallback } from 'react' import React, { useState, useEffect, useContext, useCallback } from 'react'
import { useNavigate } from 'react-router-dom' import { useNavigate } from 'react-router-dom'
@ -11,6 +11,9 @@ import PrinterIcon from '../../Icons/PrinterIcon'
import SubJobState from './SubJobState' import SubJobState from './SubJobState'
import SubJobIcon from '../../Icons/SubJobIcon' import SubJobIcon from '../../Icons/SubJobIcon'
import ReloadIcon from '../../Icons/ReloadIcon' import ReloadIcon from '../../Icons/ReloadIcon'
import IdDisplay from './IdDisplay'
const { Text } = Typography
import config from '../../../config' import config from '../../../config'
@ -41,13 +44,11 @@ const SubJobsTree = ({ jobData, loading }) => {
setExpandedKeys((prev) => [...prev, `printer-${printerData.id}`]) setExpandedKeys((prev) => [...prev, `printer-${printerData.id}`])
return { return {
title: printerData.state ? ( title: printerData.state ? (
<Space size={5}> <Space size={'small'}>
<PrinterIcon /> <PrinterIcon />
<PrinterState <Text>{printerData.name}</Text>
printer={printerData} <IdDisplay id={printerData._id} type='printer' longId={false} />
text={printerData.name} <PrinterState state={printerData.state} showProgress={false} />
showProgress={false}
/>
</Space> </Space>
) : ( ) : (
<Spin indicator={<LoadingOutlined />} /> <Spin indicator={<LoadingOutlined />} />
@ -56,10 +57,12 @@ const SubJobsTree = ({ jobData, loading }) => {
children: printerSubJobs.map((subJob) => { children: printerSubJobs.map((subJob) => {
return { return {
title: ( title: (
<Space> <Space size={'small'}>
<SubJobIcon /> <SubJobIcon />
{'Sub Job #' + subJob?.number.toString().padStart(2, '0')} <Text>
<SubJobState subJob={subJob} showProgress={true} /> {'Sub Job #' + subJob?.number.toString().padStart(2, '0')}
</Text>
<SubJobState state={subJob?.state} showProgress={true} />
</Space> </Space>
), ),
key: `subjob-${subJob._id}`, key: `subjob-${subJob._id}`,
@ -114,7 +117,6 @@ const SubJobsTree = ({ jobData, loading }) => {
// Add printServer.io event listener for deployment updates // Add printServer.io event listener for deployment updates
if (printServer) { if (printServer) {
printServer.on('notify_deployment_update', (updateData) => { printServer.on('notify_deployment_update', (updateData) => {
logger.debug('Received deployment update:', updateData)
setCurrentJobData((prevData) => { setCurrentJobData((prevData) => {
if (!prevData) return prevData if (!prevData) return prevData
@ -151,7 +153,6 @@ const SubJobsTree = ({ jobData, loading }) => {
printServer.on('notify_subjob_update', (updateData) => { printServer.on('notify_subjob_update', (updateData) => {
// Handle sub-job updates // Handle sub-job updates
if (updateData.subJobId) { if (updateData.subJobId) {
logger.debug('Received subjob update:', updateData)
setCurrentJobData((prevData) => { setCurrentJobData((prevData) => {
if (!prevData) return prevData if (!prevData) return prevData
return { return {

View File

@ -19,7 +19,10 @@ import JobState from '../common/JobState'
import IdDisplay from '../common/IdDisplay' import IdDisplay from '../common/IdDisplay'
import config from '../../../config' import config from '../../../config'
import { getTypeMeta, getPrefixMeta } from '../utils/Utils' import {
getModelByName,
getModelByPrefix
} from '../../../database/ObjectModels'
import InfoCircleIcon from '../../Icons/InfoCircleIcon' import InfoCircleIcon from '../../Icons/InfoCircleIcon'
import FilamentStockState from '../common/FilamentStockState' import FilamentStockState from '../common/FilamentStockState'
import SubJobState from '../common/SubJobState' import SubJobState from '../common/SubJobState'
@ -85,11 +88,11 @@ const SpotlightProvider = ({ children }) => {
// Check if it's a valid mode character // Check if it's a valid mode character
if ([':', '?', '^'].includes(modeChar)) { if ([':', '?', '^'].includes(modeChar)) {
const prefixMeta = getPrefixMeta(potentialPrefix) const prefixModel = getModelByPrefix(potentialPrefix)
if (prefixMeta.prefix === potentialPrefix) { if (prefixModel.prefix === potentialPrefix) {
return { return {
...prefixMeta, ...prefixModel,
mode: modeChar mode: modeChar
} }
} }
@ -310,7 +313,7 @@ const SpotlightProvider = ({ children }) => {
// Function to navigate to item URL // Function to navigate to item URL
const navigateToItem = (item) => { const navigateToItem = (item) => {
// Determine type for meta lookup // Determine type for model lookup
let type = item.type || inputPrefix?.type let type = item.type || inputPrefix?.type
// Fallback: try to infer type from known keys // Fallback: try to infer type from known keys
if (!type) { if (!type) {
@ -319,7 +322,7 @@ const SpotlightProvider = ({ children }) => {
// Add more inference as needed // Add more inference as needed
} }
const meta = getTypeMeta(type) const model = getModelByName(type)
// Get the appropriate ID for the item // Get the appropriate ID for the item
let itemId = item._id || item.id let itemId = item._id || item.id
@ -334,8 +337,8 @@ const SpotlightProvider = ({ children }) => {
itemId = item.job.id itemId = item.job.id
} }
if (itemId && meta.url) { if (itemId && model.url) {
const url = meta.url(itemId) const url = model.url(itemId)
if (url && url !== '#') { if (url && url !== '#') {
navigate(url) navigate(url)
setShowModal(false) setShowModal(false)
@ -451,7 +454,7 @@ const SpotlightProvider = ({ children }) => {
<List <List
dataSource={listData} dataSource={listData}
renderItem={(item, index) => { renderItem={(item, index) => {
// Determine type for meta lookup // Determine type for model lookup
let type = item.type || inputPrefix?.type let type = item.type || inputPrefix?.type
// Fallback: try to infer type from known keys // Fallback: try to infer type from known keys
if (!type) { if (!type) {
@ -459,8 +462,8 @@ const SpotlightProvider = ({ children }) => {
else if (item.job) type = 'job' else if (item.job) type = 'job'
// Add more inference as needed // Add more inference as needed
} }
const meta = getTypeMeta(type) const model = getModelByName(type)
const Icon = meta.icon const Icon = model.icon
// Determine shortcut text // Determine shortcut text
let shortcutText = '' let shortcutText = ''
@ -472,7 +475,7 @@ const SpotlightProvider = ({ children }) => {
return ( return (
<List.Item> <List.Item>
<List.Item.Meta <List.Item.Model
description={ description={
<Flex gap={'middle'} align='center'> <Flex gap={'middle'} align='center'>
<Text> <Text>
@ -483,7 +486,7 @@ const SpotlightProvider = ({ children }) => {
<Flex gap={'small'} style={{ marginBottom: '2px' }}> <Flex gap={'small'} style={{ marginBottom: '2px' }}>
{item.name ? <Text>{item.name}</Text> : null} {item.name ? <Text>{item.name}</Text> : null}
{meta.type == 'printer' ? ( {model.type == 'printer' ? (
<PrinterState <PrinterState
printer={item} printer={item}
showName={false} showName={false}
@ -491,7 +494,7 @@ const SpotlightProvider = ({ children }) => {
showId={false} showId={false}
/> />
) : null} ) : null}
{meta.type == 'job' ? ( {model.type == 'job' ? (
<JobState <JobState
job={item} job={item}
showQuantity={false} showQuantity={false}
@ -500,7 +503,7 @@ const SpotlightProvider = ({ children }) => {
/> />
) : null} ) : null}
{meta.type == 'subjob' ? ( {model.type == 'subjob' ? (
<SubJobState <SubJobState
subJob={item} subJob={item}
showProgress={false} showProgress={false}
@ -508,7 +511,7 @@ const SpotlightProvider = ({ children }) => {
/> />
) : null} ) : null}
{meta.type == 'filamentstock' ? ( {model.type == 'filamentstock' ? (
<Flex gap={'small'}> <Flex gap={'small'}>
<FilamentStockState <FilamentStockState
filamentStock={item} filamentStock={item}
@ -519,7 +522,7 @@ const SpotlightProvider = ({ children }) => {
) : null} ) : null}
<IdDisplay <IdDisplay
id={item._id} id={item._id}
type={meta.type} type={model.type}
longId={false} longId={false}
/> />
</Flex> </Flex>

View File

@ -1,22 +1,3 @@
import PrinterIcon from '../../Icons/PrinterIcon'
import FilamentIcon from '../../Icons/FilamentIcon'
import FilamentStockIcon from '../../Icons/FilamentStockIcon'
import GCodeFileIcon from '../../Icons/GCodeFileIcon'
import JobIcon from '../../Icons/JobIcon'
import PartIcon from '../../Icons/PartIcon'
import ProductIcon from '../../Icons/ProductIcon'
import VendorIcon from '../../Icons/VendorIcon'
import SubJobIcon from '../../Icons/SubJobIcon'
import StockEventIcon from '../../Icons/StockEventIcon'
import StockAuditIcon from '../../Icons/StockAuditIcon'
import PartStockIcon from '../../Icons/PartStockIcon'
import ProductStockIcon from '../../Icons/ProductStockIcon'
import AuditLogIcon from '../../Icons/AuditLogIcon'
import PersonIcon from '../../Icons/PersonIcon'
import NoteTypeIcon from '../../Icons/NoteTypeIcon'
import NoteIcon from '../../Icons/NoteIcon'
import QuestionCircleIcon from '../../Icons/QuestionCircleIcon'
export function capitalizeFirstLetter(string) { export function capitalizeFirstLetter(string) {
try { try {
return string[0].toUpperCase() + string.slice(1) return string[0].toUpperCase() + string.slice(1)
@ -49,190 +30,5 @@ export function timeStringToMinutes(timeString) {
return Math.floor(totalMinutes) return Math.floor(totalMinutes)
} }
export const TYPE_META = [ // Re-export the functions for backward compatibility
{ export {}
type: 'printer',
title: 'Printer',
prefix: 'PRN',
icon: PrinterIcon,
url: (id) => `/dashboard/production/printers/info?printerId=${id}`,
properties: {
name: 'text'
}
},
{
type: 'filament',
title: 'Filament',
prefix: 'FIL',
icon: FilamentIcon,
url: (id) => `/dashboard/management/filaments/info?filamentId=${id}`,
properties: {
id: 'id',
createdAt: 'dateTime',
name: 'text',
updatedAt: 'dateTime',
vendor: 'object', // objectType: vendor
vendorId: 'id', // objectType: vendor
type: 'material',
cost: 'currency',
color: 'color',
diameter: 'mm',
density: 'density',
url: 'text',
barcode: 'text'
}
},
{
type: 'spool',
title: 'Spool',
prefix: 'SPL',
icon: FilamentIcon,
url: (id) => `/dashboard/inventory/spool/info?spoolId=${id}`
},
{
type: 'gcodefile',
title: 'GCode File',
prefix: 'GCF',
icon: GCodeFileIcon,
url: (id) => `/dashboard/production/gcodefiles/info?gcodeFileId=${id}`
},
{
type: 'job',
title: 'Job',
prefix: 'JOB',
icon: JobIcon,
url: (id) => `/dashboard/production/jobs/info?jobId=${id}`
},
{
type: 'part',
title: 'Part',
prefix: 'PRT',
icon: PartIcon,
url: (id) => `/dashboard/management/parts/info?partId=${id}`
},
{
type: 'product',
title: 'Product',
prefix: 'PRD',
icon: ProductIcon,
url: (id) => `/dashboard/management/products/info?productId=${id}`
},
{
type: 'vendor',
title: 'Vendor',
prefix: 'VEN',
icon: VendorIcon,
url: (id) => `/dashboard/management/vendors/info?vendorId=${id}`,
properties: {
id: 'id', // objectType: vendor
createdAt: 'dateTime',
name: 'text',
updatedAt: 'dateTime',
website: 'url',
country: 'country',
contact: 'text',
phone: 'text',
email: 'email'
}
},
{
type: 'subjob',
title: 'Sub Job',
prefix: 'SJB',
icon: SubJobIcon,
url: () => `#`
},
{
type: 'initial',
title: 'Initial',
prefix: 'INT',
icon: QuestionCircleIcon,
url: () => `#`
},
{
type: 'filamentstock',
title: 'Filament Stock',
prefix: 'FLS',
icon: FilamentStockIcon,
url: (id) =>
`/dashboard/inventory/filamentstocks/info?filamentStockId=${id}`
},
{
type: 'stockevent',
title: 'Stock Event',
prefix: 'SEV',
icon: StockEventIcon,
url: () => `#`
},
{
type: 'stockaudit',
title: 'Stock Audit',
prefix: 'SAU',
icon: StockAuditIcon,
url: (id) => `/dashboard/inventory/stockaudits/info?stockAuditId=${id}`
},
{
type: 'partstock',
title: 'Part Stock',
prefix: 'PTS',
icon: PartStockIcon,
url: (id) => `/dashboard/management/partstocks/info?partStockId=${id}`
},
{
type: 'productstock',
title: 'Product Stock',
prefix: 'PDS',
icon: ProductStockIcon,
url: (id) => `/dashboard/management/productstocks/info?productStockId=${id}`
},
{
type: 'auditlog',
title: 'Audit Log',
prefix: 'ADL',
icon: AuditLogIcon,
url: () => `#`
},
{
type: 'user',
title: 'User',
prefix: 'USR',
icon: PersonIcon,
url: (id) => `/dashboard/management/users/info?userId=${id}`
},
{
type: 'notetype',
title: 'Note Type',
prefix: 'NTY',
icon: NoteTypeIcon,
url: (id) => `/dashboard/management/notetypes/info?noteTypeId=${id}`
},
{
type: 'note',
title: 'Note',
prefix: 'NTE',
icon: NoteIcon,
url: () => `#`
}
]
export function getTypeMeta(type) {
return (
TYPE_META.find((meta) => meta.type === type) || {
type: 'unknown',
prefix: 'UNK',
icon: QuestionCircleIcon,
url: () => '#'
}
)
}
export function getPrefixMeta(prefix) {
return (
TYPE_META.find((meta) => meta.prefix === prefix) || {
type: 'unknown',
prefix: 'UNK',
icon: QuestionCircleIcon,
url: () => '#'
}
)
}

View File

@ -19,7 +19,7 @@ const UrlDisplay = ({ url, showCopy = true, showLink = false }) => {
rel='noopener noreferrer' rel='noopener noreferrer'
style={{ marginRight: 8 }} style={{ marginRight: 8 }}
> >
{url} <Text ellipsis>{url}</Text>
</Link> </Link>
) : ( ) : (
<> <>

View File

@ -0,0 +1,131 @@
import { Printer } from './models/Printer.js'
import { Filament } from './models/Filament.js'
import { Spool } from './models/Spool'
import { GCodeFile } from './models/GCodeFile'
import { Job } from './models/Job'
import { Product } from './models/Product'
import { Vendor } from './models/Vendor'
import { SubJob } from './models/SubJob'
import { Initial } from './models/Initial'
import { FilamentStock } from './models/FilamentStock'
import { StockEvent } from './models/StockEvent'
import { StockAudit } from './models/StockAudit'
import { PartStock } from './models/PartStock'
import { ProductStock } from './models/ProductStock'
import { AuditLog } from './models/AuditLog'
import { User } from './models/User'
import { NoteType } from './models/NoteType'
import { Note } from './models/Note'
import QuestionCircleIcon from '../components/Icons/QuestionCircleIcon'
export const objectModels = [
Printer,
Filament,
Spool,
GCodeFile,
Job,
Product,
Vendor,
SubJob,
Initial,
FilamentStock,
StockEvent,
StockAudit,
PartStock,
ProductStock,
AuditLog,
User,
NoteType,
Note
]
// Re-export individual models for direct access
export {
Printer,
Filament,
Spool,
GCodeFile,
Job,
Product,
Vendor,
SubJob,
Initial,
FilamentStock,
StockEvent,
StockAudit,
PartStock,
ProductStock,
AuditLog,
User,
NoteType,
Note
}
export function getModelByName(name) {
return (
objectModels.find((meta) => meta.name === name) || {
name: 'unknown',
label: 'Unknown',
prefix: 'UNK',
icon: QuestionCircleIcon,
url: () => '#',
properties: {}
}
)
}
export function getModelProperties(name, propertyList) {
const model = getModelByName(name)
if (!model || !model.properties) {
return []
}
// If no propertyList is provided, return all properties
if (!propertyList || propertyList.length === 0) {
return model.properties
}
// Create a map of property names to properties for efficient lookup
const propertyMap = new Map(
model.properties.map((property) => [property.name, property])
)
// Return properties in the same order as propertyList
return propertyList
.map((propertyName) => propertyMap.get(propertyName))
.filter((property) => property !== undefined)
}
export function getModelByPrefix(prefix) {
return (
objectModels.find((meta) => meta.prefix === prefix) || {
name: 'unknown',
label: 'Unknown',
prefix: 'UNK',
icon: QuestionCircleIcon,
url: () => '#',
properties: {}
}
)
}
// Utility function to get nested object values
export const getPropertyValue = (obj, path) => {
if (!obj || !path) return undefined
if (path.includes('.')) {
const propertyPath = path.split('.')
let currentValue = obj
for (const prop of propertyPath) {
if (currentValue && typeof currentValue === 'object') {
currentValue = currentValue[prop]
} else {
currentValue = undefined
break
}
}
return currentValue
} else {
return obj[path]
}
}

View File

@ -0,0 +1,9 @@
import AuditLogIcon from '../../components/Icons/AuditLogIcon'
export const AuditLog = {
name: 'auditlog',
label: 'Audit Log',
prefix: 'ADL',
icon: AuditLogIcon,
url: () => `#`
}

View File

@ -0,0 +1,100 @@
import FilamentIcon from '../../components/Icons/FilamentIcon'
export const Filament = {
name: 'filament',
label: 'Filament',
prefix: 'FIL',
icon: FilamentIcon,
url: (id) => `/dashboard/management/filaments/info?filamentId=${id}`,
properties: [
{
name: '_id',
label: 'ID',
type: 'id',
objectType: 'filament',
showCopy: true
},
{
name: 'createdAt',
label: 'Created At',
type: 'dateTime',
readOnly: true
},
{
name: 'name',
label: 'Name',
required: true,
type: 'text'
},
{
name: 'updatedAt',
label: 'Updated At',
type: 'dateTime',
readOnly: true
},
{
name: 'vendor',
label: 'Vendor',
required: true,
type: 'object',
objectType: 'vendor'
},
{
name: 'vendor._id',
label: 'Vendor ID',
type: 'id',
objectType: 'vendor',
showCopy: true,
showHyperlink: true
},
{
name: 'type',
label: 'Material',
required: true,
type: 'material'
},
{
name: 'cost',
label: 'Cost',
required: true,
type: 'currency'
},
{
name: 'color',
label: 'Color',
required: true,
type: 'color'
},
{
name: 'diameter',
label: 'Diameter',
required: true,
type: 'mm'
},
{
name: 'density',
label: 'Density',
required: true,
type: 'density'
},
{
name: 'url',
label: 'Link',
type: 'url'
},
{
name: 'barcode',
label: 'Barcode',
type: 'text'
}
]
}

View File

@ -0,0 +1,9 @@
import FilamentStockIcon from '../../components/Icons/FilamentStockIcon'
export const FilamentStock = {
name: 'filamentstock',
label: 'Filament Stock',
prefix: 'FLS',
icon: FilamentStockIcon,
url: (id) => `/dashboard/inventory/filamentstocks/info?filamentStockId=${id}`
}

View File

@ -0,0 +1,116 @@
import GCodeFileIcon from '../../components/Icons/GCodeFileIcon'
export const GCodeFile = {
name: 'gcodeFile',
label: 'GCode File',
prefix: 'GCF',
icon: GCodeFileIcon,
url: (id) => `/dashboard/production/gcodefiles/info?gcodeFileId=${id}`,
properties: [
{
name: '_id',
label: 'ID',
type: 'id',
objectType: 'gcodefile',
value: null,
showCopy: true
},
{
name: 'createdAt',
label: 'Created At',
type: 'dateTime',
value: null,
readOnly: true
},
{
name: 'name',
label: 'Name',
type: 'text',
value: null,
required: true
},
{
name: 'updatedAt',
label: 'Updated At',
type: 'dateTime',
value: null,
readOnly: true
},
{
name: 'filament',
label: 'Filament',
type: 'object',
value: null,
objectType: 'filament',
required: true
},
{
name: 'cost',
label: 'Cost',
type: 'currency',
value: null,
readOnly: true
},
{
name: 'gcodeFileInfo.estimatedPrintingTimeNormalMode',
label: 'Est Print Time',
value: null,
type: 'text',
readOnly: true
},
{
name: 'gcodeFileInfo.sparseInfillDensity',
label: 'Infill Density',
type: 'number',
readOnly: true
},
{
name: 'gcodeFileInfo.sparseInfillPattern',
label: 'Infill Pattern',
type: 'text',
readOnly: true
},
{
name: 'gcodeFileInfo.filamentUsedMm',
label: 'Filament Used (mm)',
value: null,
type: 'mm',
readOnly: true
},
{
name: 'gcodeFileInfo.filamentUsedG',
label: 'Filament Used (g)',
value: null,
type: 'weight',
readOnly: true
},
{
name: 'gcodeFileInfo.nozzleTemperature',
label: 'Hotend Temperature',
value: null,
type: 'number',
readOnly: true
},
{
name: 'gcodeFileInfo.hotPlateTemp',
label: 'Bed Temperature',
value: null,
type: 'number',
readOnly: true
},
{
name: 'gcodeFileInfo.filamentSettingsId',
label: 'Filament Profile',
value: null,
type: 'text',
readOnly: true
},
{
name: 'gcodeFileInfo.printSettingsId',
label: 'Print Profile',
value: null,
type: 'text',
readOnly: true
}
]
}

View File

@ -0,0 +1,9 @@
import QuestionCircleIcon from '../../components/Icons/QuestionCircleIcon'
export const Initial = {
name: 'initial',
label: 'Initial',
prefix: 'INT',
icon: QuestionCircleIcon,
url: () => `#`
}

View File

@ -0,0 +1,67 @@
import JobIcon from '../../components/Icons/JobIcon'
export const Job = {
name: 'job',
label: 'Job',
prefix: 'JOB',
icon: JobIcon,
url: (id) => `/dashboard/production/jobs/info?jobId=${id}`,
properties: [
{
name: '_id',
label: 'ID',
type: 'id',
objectType: 'job',
showCopy: true
},
{
name: 'state',
label: 'Status',
type: 'state',
objectType: 'job',
showStatus: true,
showProgress: true,
showId: false,
showQuantity: false,
readOnly: true
},
{
name: 'gcodeFile',
label: 'GCode File',
type: 'object',
objectType: 'gcodeFile',
readOnly: true
},
{
name: 'gcodeFile._id',
label: 'GCode File ID',
type: 'id',
objectType: 'gcodeFile',
showHyperlink: true
},
{
name: 'quantity',
label: 'Quantity',
type: 'number',
readOnly: true
},
{
name: 'createdAt',
label: 'Created At',
type: 'dateTime',
readOnly: true
},
{
name: 'startedAt',
label: 'Started At',
type: 'dateTime',
readOnly: true
},
{
name: 'printers',
label: 'Assigned Printers',
type: 'number',
readOnly: true
}
]
}

View File

@ -0,0 +1,9 @@
import NoteIcon from '../../components/Icons/NoteIcon'
export const Note = {
name: 'note',
label: 'Note',
prefix: 'NTE',
icon: NoteIcon,
url: () => `#`
}

View File

@ -0,0 +1,9 @@
import NoteTypeIcon from '../../components/Icons/NoteTypeIcon'
export const NoteType = {
name: 'notetype',
label: 'Note Type',
prefix: 'NTY',
icon: NoteTypeIcon,
url: (id) => `/dashboard/management/notetypes/info?noteTypeId=${id}`
}

View File

@ -0,0 +1,9 @@
import PartStockIcon from '../../components/Icons/PartStockIcon'
export const PartStock = {
name: 'partstock',
label: 'Part Stock',
prefix: 'PTS',
icon: PartStockIcon,
url: (id) => `/dashboard/management/partstocks/info?partStockId=${id}`
}

View File

@ -0,0 +1,91 @@
import PrinterIcon from '../../components/Icons/PrinterIcon'
export const Printer = {
name: 'printer',
label: 'Printer',
prefix: 'PRN',
icon: PrinterIcon,
url: (id) => `/dashboard/production/printers/info?printerId=${id}`,
properties: [
{
name: '_id',
label: 'ID',
type: 'id',
objectType: 'printer',
showCopy: true
},
{
name: 'connectedAt',
label: 'Connected At',
type: 'dateTime',
readOnly: true
},
{
name: 'name',
label: 'Name',
required: true,
type: 'text'
},
{
name: 'state',
label: 'Status',
type: 'state',
objectType: 'printer',
showName: false,
readOnly: true
},
{
name: 'vendor',
label: 'Vendor',
type: 'object',
objectType: 'vendor',
required: true
},
{
name: 'moonraker.host',
label: 'Host',
type: 'text',
required: true
},
{
name: 'vendor._id',
label: 'Vendor ID',
type: 'id',
objectType: 'vendor',
showHyperlink: true,
readOnly: true
},
{
name: 'moonraker.port',
label: 'Port',
type: 'number',
required: true
},
{
name: 'moonraker.apiKey',
label: 'API Key',
type: 'secret',
reveal: true,
required: false
},
{
name: 'moonraker.protocol',
label: 'Protocol',
type: 'wsprotocol',
required: true
},
{
name: 'tags',
label: 'Tags',
type: 'tags',
required: false
},
{
name: 'firmware',
label: 'Firmware Version',
type: 'text',
required: false,
readOnly: true
}
]
}

View File

@ -0,0 +1,9 @@
import ProductIcon from '../../components/Icons/ProductIcon'
export const Product = {
name: 'product',
label: 'Product',
prefix: 'PRD',
icon: ProductIcon,
url: (id) => `/dashboard/management/products/info?productId=${id}`
}

View File

@ -0,0 +1,9 @@
import ProductStockIcon from '../../components/Icons/ProductStockIcon'
export const ProductStock = {
name: 'productstock',
label: 'Product Stock',
prefix: 'PDS',
icon: ProductStockIcon,
url: (id) => `/dashboard/management/productstocks/info?productStockId=${id}`
}

View File

@ -0,0 +1,9 @@
import FilamentIcon from '../../components/Icons/FilamentIcon'
export const Spool = {
name: 'spool',
label: 'Spool',
prefix: 'SPL',
icon: FilamentIcon,
url: (id) => `/dashboard/inventory/spool/info?spoolId=${id}`
}

View File

@ -0,0 +1,9 @@
import StockAuditIcon from '../../components/Icons/StockAuditIcon'
export const StockAudit = {
name: 'stockaudit',
label: 'Stock Audit',
prefix: 'SAU',
icon: StockAuditIcon,
url: (id) => `/dashboard/inventory/stockaudits/info?stockAuditId=${id}`
}

View File

@ -0,0 +1,9 @@
import StockEventIcon from '../../components/Icons/StockEventIcon'
export const StockEvent = {
name: 'stockevent',
label: 'Stock Event',
prefix: 'SEV',
icon: StockEventIcon,
url: () => `#`
}

View File

@ -0,0 +1,9 @@
import SubJobIcon from '../../components/Icons/SubJobIcon'
export const SubJob = {
name: 'subjob',
label: 'Sub Job',
prefix: 'SJB',
icon: SubJobIcon,
url: () => `#`
}

View File

@ -0,0 +1,9 @@
import PersonIcon from '../../components/Icons/PersonIcon'
export const User = {
name: 'user',
label: 'User',
prefix: 'USR',
icon: PersonIcon,
url: (id) => `/dashboard/management/users/info?userId=${id}`
}

View File

@ -0,0 +1,73 @@
import VendorIcon from '../../components/Icons/VendorIcon'
export const Vendor = {
name: 'vendor',
label: 'Vendor',
prefix: 'VEN',
icon: VendorIcon,
url: (id) => `/dashboard/management/vendors/info?vendorId=${id}`,
properties: [
{
name: '_id',
label: 'ID',
type: 'id',
objectType: 'vendor',
showCopy: true
},
{
name: 'createdAt',
label: 'Created At',
type: 'dateTime',
readOnly: true
},
{
name: 'name',
label: 'Name',
required: true,
type: 'text'
},
{
name: 'updatedAt',
label: 'Updated At',
type: 'dateTime',
readOnly: true
},
{
name: 'contact',
label: 'Contact',
type: 'text',
readOnly: false,
required: false
},
{
name: 'country',
label: 'Country',
type: 'country',
readOnly: false,
required: true
},
{
name: 'email',
label: 'Email',
type: 'email',
readOnly: false,
required: false
},
{
name: 'phone',
label: 'Phone',
type: 'phone',
readOnly: false,
required: false
},
{
name: 'website',
label: 'Website',
type: 'url',
readOnly: false,
required: false
}
]
}