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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -7,17 +7,20 @@ import config from '../../../../config'
import ReloadIcon from '../../../Icons/ReloadIcon'
import useCollapseState from '../../hooks/useCollapseState'
import AuditLogTable from '../../common/AuditLogTable'
import DashboardNotes from '../../common/DashboardNotes'
import NotesPanel from '../../common/NotesPanel'
import InfoCollapse from '../../common/InfoCollapse'
import ObjectInfo from '../../common/ObjectInfo'
import ViewButton from '../../common/ViewButton'
import InfoCircleIcon from '../../../Icons/InfoCircleIcon.jsx'
import NoteIcon from '../../../Icons/NoteIcon.jsx'
import AuditLogIcon from '../../../Icons/AuditLogIcon.jsx'
import EditObjectForm from '../../common/EditObjectForm'
import EditButtons from '../../common/EditButtons'
import LockIndicator from './LockIndicator'
import {
getModelProperties,
getPropertyValue
} from '../../../../database/ObjectModels'
const log = loglevel.getLogger('FilamentInfo')
log.setLevel(config.logLevel)
@ -115,101 +118,10 @@ const FilamentInfo = () => {
loading={loading}
indicator={<LoadingOutlined />}
isEditing={isEditing}
items={[
{
name: 'id',
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'
}
]}
items={getModelProperties('filament').map((prop) => ({
...prop,
value: getPropertyValue(objectData, prop.name)
}))}
/>
</InfoCollapse>
@ -221,7 +133,7 @@ const FilamentInfo = () => {
key='notes'
>
<Card>
<DashboardNotes _id={filamentId} />
<NotesPanel _id={filamentId} />
</Card>
</InfoCollapse>

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -7,17 +7,20 @@ import config from '../../../../config'
import ReloadIcon from '../../../Icons/ReloadIcon'
import useCollapseState from '../../hooks/useCollapseState'
import AuditLogTable from '../../common/AuditLogTable'
import DashboardNotes from '../../common/DashboardNotes'
import NotesPanel from '../../common/NotesPanel'
import InfoCollapse from '../../common/InfoCollapse'
import ObjectInfo from '../../common/ObjectInfo'
import ViewButton from '../../common/ViewButton'
import InfoCircleIcon from '../../../Icons/InfoCircleIcon.jsx'
import NoteIcon from '../../../Icons/NoteIcon.jsx'
import AuditLogIcon from '../../../Icons/AuditLogIcon.jsx'
import EditObjectForm from '../../common/EditObjectForm'
import EditButtons from '../../common/EditButtons'
import LockIndicator from '../Filaments/LockIndicator'
import {
getModelProperties,
getPropertyValue
} from '../../../../database/ObjectModels'
const log = loglevel.getLogger('VendorInfo')
log.setLevel(config.logLevel)
@ -115,67 +118,10 @@ const VendorInfo = () => {
loading={loading}
indicator={<LoadingOutlined />}
isEditing={isEditing}
items={[
{
name: 'id',
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'
}
]}
items={getModelProperties('vendor').map((prop) => ({
...prop,
value: getPropertyValue(objectData, prop.name)
}))}
/>
</InfoCollapse>
@ -187,7 +133,7 @@ const VendorInfo = () => {
key='notes'
>
<Card>
<DashboardNotes _id={vendorId} />
<NotesPanel _id={vendorId} />
</Card>
</InfoCollapse>

View File

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

View File

@ -5,7 +5,7 @@ import { LoadingOutlined } from '@ant-design/icons'
import ReloadIcon from '../../../Icons/ReloadIcon'
import useCollapseState from '../../hooks/useCollapseState'
import AuditLogTable from '../../common/AuditLogTable'
import DashboardNotes from '../../common/DashboardNotes'
import NotesPanel from '../../common/NotesPanel'
import InfoCollapse from '../../common/InfoCollapse'
import ObjectInfo from '../../common/ObjectInfo'
import ViewButton from '../../common/ViewButton'
@ -16,6 +16,10 @@ import InfoCircleIcon from '../../../Icons/InfoCircleIcon.jsx'
import NoteIcon from '../../../Icons/NoteIcon.jsx'
import AuditLogIcon from '../../../Icons/AuditLogIcon.jsx'
import GCodeFileIcon from '../../../Icons/GCodeFileIcon.jsx'
import {
getModelProperties,
getPropertyValue
} from '../../../../database/ObjectModels.js'
const { Text } = Typography
@ -117,120 +121,10 @@ const GCodeFileInfo = () => {
loading={loading}
indicator={<LoadingOutlined />}
isEditing={isEditing}
items={[
{
name: '_id',
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
}
]}
items={getModelProperties('gcodeFile').map((prop) => ({
...prop,
value: getPropertyValue(objectData, prop.name)
}))}
objectData={objectData}
type='gcodefile'
/>
@ -266,7 +160,7 @@ const GCodeFileInfo = () => {
key='notes'
>
<Card>
<DashboardNotes _id={gcodeFileId} />
<NotesPanel _id={gcodeFileId} />
</Card>
</InfoCollapse>

View File

@ -36,7 +36,7 @@ import CheckCircleIcon from '../../Icons/CheckCircleIcon.jsx'
import PauseCircleIcon from '../../Icons/PauseCircleIcon.jsx'
import XMarkCircleIcon from '../../Icons/XMarkCircleIcon.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 GridIcon from '../../Icons/GridIcon.jsx'
import useViewMode from '../hooks/useViewMode.js'
@ -146,9 +146,10 @@ const Jobs = () => {
{
title: 'State',
key: 'state',
dataIndex: 'state',
width: 240,
render: (record) => {
return <JobState job={record} showQuantity={false} showId={false} />
render: (state) => {
return <JobState state={state} showQuantity={false} showId={false} />
},
filterDropdown: ({
setSelectedKeys,
@ -393,7 +394,7 @@ const Jobs = () => {
</Space>
</Flex>
<DashboardTable
<ObjectTable
ref={tableRef}
columns={visibleColumns}
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 useCollapseState from '../../hooks/useCollapseState'
import AuditLogTable from '../../common/AuditLogTable'
import DashboardNotes from '../../common/DashboardNotes'
import NotesPanel from '../../common/NotesPanel'
import InfoCollapse from '../../common/InfoCollapse'
import ObjectInfo from '../../common/ObjectInfo'
import ViewButton from '../../common/ViewButton'
@ -17,6 +17,10 @@ import JobIcon from '../../../Icons/JobIcon'
import AuditLogIcon from '../../../Icons/AuditLogIcon'
import NoteIcon from '../../../Icons/NoteIcon'
import GCodeFileIcon from '../../../Icons/GCodeFileIcon'
import {
getModelProperties,
getPropertyValue
} from '../../../../database/ObjectModels.js'
const JobInfo = () => {
const location = useLocation()
@ -114,72 +118,10 @@ const JobInfo = () => {
indicator={<LoadingOutlined />}
isEditing={isEditing}
type='job'
items={[
{
name: '_id',
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
}
]}
items={getModelProperties('job').map((prop) => ({
...prop,
value: getPropertyValue(objectData, prop.name)
}))}
/>
</InfoCollapse>
@ -203,7 +145,7 @@ const JobInfo = () => {
key='notes'
>
<Card>
<DashboardNotes _id={jobId} />
<NotesPanel _id={jobId} />
</Card>
</InfoCollapse>

View File

@ -26,7 +26,7 @@ import PlusIcon from '../../Icons/PlusIcon'
import ReloadIcon from '../../Icons/ReloadIcon'
import XMarkIcon from '../../Icons/XMarkIcon'
import CheckIcon from '../../Icons/CheckIcon'
import DashboardTable from '../common/DashboardTable'
import ObjectTable from '../common/ObjectTable'
import config from '../../../config'
import GridIcon from '../../Icons/GridIcon'
@ -83,15 +83,12 @@ const Printers = () => {
},
{
title: 'State',
dataIndex: 'state',
key: 'state',
width: 240,
render: (record) => {
render: (state) => {
return (
<PrinterState
printer={record}
showName={false}
showControls={false}
/>
<PrinterState state={state} showName={false} showControls={false} />
)
}
},
@ -312,7 +309,7 @@ const Printers = () => {
</Space>
</Flex>
<DashboardTable
<ObjectTable
ref={tableRef}
columns={visibleColumns}
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 useCollapseState from '../../hooks/useCollapseState'
import AuditLogTable from '../../common/AuditLogTable'
import DashboardNotes from '../../common/DashboardNotes'
import NotesPanel from '../../common/NotesPanel'
import InfoCollapse from '../../common/InfoCollapse'
import ObjectInfo from '../../common/ObjectInfo'
import ViewButton from '../../common/ViewButton'
@ -16,6 +16,10 @@ import InfoCircleIcon from '../../../Icons/InfoCircleIcon.jsx'
import NoteIcon from '../../../Icons/NoteIcon.jsx'
import PrinterIcon from '../../../Icons/PrinterIcon.jsx'
import AuditLogIcon from '../../../Icons/AuditLogIcon.jsx'
import {
getModelProperties,
getPropertyValue
} from '../../../../database/ObjectModels.js'
const PrinterInfo = () => {
const location = useLocation()
@ -113,102 +117,10 @@ const PrinterInfo = () => {
indicator={<LoadingOutlined />}
isEditing={isEditing}
type='printer'
items={[
{
name: '_id',
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
}
]}
items={getModelProperties('printer').map((prop) => ({
...prop,
value: getPropertyValue(objectData, prop.name)
}))}
/>
</InfoCollapse>
@ -233,7 +145,7 @@ const PrinterInfo = () => {
key='notes'
>
<Card>
<DashboardNotes _id={printerId} />
<NotesPanel _id={printerId} />
</Card>
</InfoCollapse>

View File

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

View File

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

View File

@ -1,126 +1,26 @@
import { TreeSelect } from 'antd'
import React, { useEffect, useState, useCallback } from 'react'
import React from 'react'
import PropTypes from 'prop-types'
import axios from 'axios'
import config from '../../../config'
import ObjectSelect from './ObjectSelect'
import FilamentStockDisplay from './FilamentStockDisplay'
const FilamentStockSelect = ({ onChange, filter, useFilter, value }) => {
const [filamentStocksTreeData, setFilamentStocksTreeData] = useState([])
const [filamentStocksData, setFilamentStocksData] = useState([])
const [loading, setLoading] = useState(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])
const FilamentStockSelect = ({
onChange,
filter = {},
useFilter = false,
value,
disabled = false
}) => {
return (
<TreeSelect
treeDataSimpleMode
value={defaultValue?._id}
loadData={handleFilamentStocksTreeLoad}
treeData={filamentStocksTreeData}
onChange={handleOnChange}
loading={loading}
<ObjectSelect
endpoint={`${config.backendUrl}/filamentstocks`}
propertyOrder={['tags']}
filter={filter}
useFilter={useFilter}
value={value}
onChange={onChange}
disabled={disabled}
placeholder='Select a filament stock'
type='filamentstock'
/>
)
}
@ -129,12 +29,8 @@ FilamentStockSelect.propTypes = {
onChange: PropTypes.func.isRequired,
value: PropTypes.object,
filter: PropTypes.object,
useFilter: PropTypes.bool
}
FilamentStockSelect.defaultProps = {
filter: {},
useFilter: false
useFilter: PropTypes.bool,
disabled: PropTypes.bool
}
export default FilamentStockSelect

View File

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

View File

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

View File

@ -259,7 +259,7 @@ NoteItem.propTypes = {
onChildNoteAdded: PropTypes.func
}
const DashboardNotes = ({ _id, onNewNote }) => {
const NotesPanel = ({ _id, onNewNote }) => {
const [newNoteOpen, setNewNoteOpen] = useState(false)
const [showMarkdown, setShowMarkdown] = useState(false)
const [loading, setLoading] = useState(true)
@ -686,9 +686,9 @@ const DashboardNotes = ({ _id, onNewNote }) => {
)
}
DashboardNotes.propTypes = {
NotesPanel.propTypes = {
_id: PropTypes.string.isRequired,
onNewNote: PropTypes.func
}
export default DashboardNotes
export default NotesPanel

View File

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

View File

@ -2,12 +2,41 @@ import React, { useEffect, useState, useCallback } from 'react'
import PropTypes from 'prop-types'
import { TreeSelect, Typography, Flex, Badge, Space, Button, Input } from 'antd'
import axios from 'axios'
import { getTypeMeta } from '../utils/Utils'
import { getModelByName } from '../../../database/ObjectModels'
import IdDisplay from './IdDisplay'
import CountryDisplay from './CountryDisplay'
import ReloadIcon from '../../Icons/ReloadIcon'
import ObjectProperty from './ObjectProperty'
const { Text } = Typography
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.
*
@ -35,29 +64,14 @@ const ObjectSelect = ({
type = 'unknown',
...rest
}) => {
// --- State ---
const [treeData, setTreeData] = useState([])
const [loading, setLoading] = useState(false)
const [defaultValue, setDefaultValue] = useState(treeCheckable ? [] : value)
const [searchValue, setSearchValue] = useState('')
const [error, setError] = useState(false)
// Helper to get filter object for a node
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
// --- API: Fetch data for a property level or leaf ---
const fetchData = useCallback(
async (property, filter, search) => {
setLoading(true)
@ -74,14 +88,13 @@ const ObjectSelect = ({
} catch (err) {
setLoading(false)
setError(true)
// Optionally handle error
return []
}
},
[endpoint]
)
// Fetch single object by ID
// --- API: Fetch a single object by ID ---
const fetchObjectById = useCallback(
async (objectId) => {
setLoading(true)
@ -95,305 +108,134 @@ const ObjectSelect = ({
} catch (err) {
setLoading(false)
setError(true)
console.error('Failed to fetch object by ID:', err)
return null
}
},
[endpoint]
)
// Helper to render the title for a node
// --- Render node title ---
const renderTitle = useCallback(
(item, isLeaf) => {
if (!isLeaf) {
// For category nodes, check if it's a country property
const currentProperty = propertyOrder[item.propertyId]
if (currentProperty === 'country' && item.value) {
return <CountryDisplay countryCode={item.value} />
}
// 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 }
(item) => {
if (item.propertyType) {
return (
<ObjectProperty
type={item.propertyType}
value={item.value}
objectType={type}
/>
)
} else {
filterObj = getFilter(node)
propertyId = node.propertyId + 1
const model = getModelByName(type)
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)
const newNodes = data.map((item) => {
const isLeaf = false
// Handle both cases: when item is a simple value or when it's an object
let value
if (typeof item === 'object' && item !== null) {
// Handle nested property access (e.g., 'filament.diameter')
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 {
// 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)
},
[type]
)
// --- Build tree nodes for a property level ---
const buildCategoryNodes = useCallback(
(data, propertyName, propertyId, parentId) => {
return data.map((item) => {
let resolved = resolvePropertyPath(item, propertyName)
let value = resolved.value
let propertyType = resolved.finalProperty
return {
id: value,
pId: node.id,
pId: parentId,
value: value,
key: value,
propertyId: propertyId,
title: title,
title: renderTitle({ ...item, value, propertyType }),
isLeaf: false,
selectable: false,
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(
async (node) => {
if (!propertyOrder.length) return
if (node) {
// Not at leaf level yet
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 {
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 {
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) => {
if (onChange) {
if (treeCheckable) {
// Handle multiple selections with checkboxes
const selectedObjects = []
// Multi-select
let selectedObjects = []
if (Array.isArray(val)) {
val.forEach((selectedValue) => {
selectedObjects = val.map((selectedValue) => {
const node = treeData.find((n) => n.value === selectedValue)
if (node) {
selectedObjects.push(node.raw)
} else {
selectedObjects.push(selectedValue)
}
return node ? node.raw : selectedValue
})
}
onChange(selectedObjects, selectedOptions)
} else {
// Handle single selection
// Single select
const node = treeData.find((n) => n.value === val)
onChange(node ? node.raw : val, selectedOptions)
}
@ -401,21 +243,18 @@ const ObjectSelect = ({
setDefaultValue(val)
}
// Search handler
// --- Search handler ---
const handleSearch = (val) => {
setSearchValue(val)
setTreeData([])
}
// Keep defaultValue in sync and handle object values
// --- Sync defaultValue and load tree path for object values ---
useEffect(() => {
if (treeCheckable) {
// Handle array of values for multi-select
if (Array.isArray(value)) {
const valueIds = value.map((v) => v._id || v.id || v)
setDefaultValue(valueIds)
// Load tree paths for any objects that aren't already loaded
value.forEach((item) => {
if (item && typeof item === 'object' && item._id) {
const existingNode = treeData.find(
@ -424,7 +263,14 @@ const ObjectSelect = ({
if (!existingNode) {
fetchObjectById(item._id).then((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([])
}
} else {
// Handle single value
if (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)
if (!existingNode) {
fetchObjectById(value._id).then((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(() => {
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) {
return
}
if (useFilter || searchValue) {
generateLeafNodes({ id: 0 }, filter, searchValue)
// Flat filter mode
fetchData(null, filter, searchValue).then((data) => {
setTreeData(buildLeafNodes(data, 0))
})
} else {
handleTreeLoad(null)
}
@ -473,7 +327,8 @@ const ObjectSelect = ({
useFilter,
filter,
searchValue,
generateLeafNodes,
buildLeafNodes,
fetchData,
handleTreeLoad,
error,
loading,
@ -481,20 +336,25 @@ const ObjectSelect = ({
treeCheckable
])
return error ? (
<Space.Compact style={{ width: '100%' }}>
<Input value='Failed to load data.' status='error' disabled />
// --- Error UI ---
if (error) {
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
icon={<ReloadIcon />}
onClick={() => {
setError(false)
setTreeData([])
}}
danger
/>
</Space.Compact>
) : (
// --- Main TreeSelect UI ---
return (
<TreeSelect
treeDataSimpleMode
treeDefaultExpandAll={true}

View File

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

View File

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

View File

@ -1,50 +1,18 @@
// PrinterSelect.js
import PropTypes from 'prop-types'
import { Progress, Flex, Space, Typography, Button, Tooltip } from 'antd'
import React, { useState, useContext, useEffect } from 'react'
import { PrintServerContext } from '../context/PrintServerContext'
import { CaretLeftOutlined } from '@ant-design/icons'
import XMarkIcon from '../../Icons/XMarkIcon'
import PauseIcon from '../../Icons/PauseIcon'
import { Progress, Flex, Space } from 'antd'
import React from 'react'
import StateTag from './StateTag'
const PrinterState = ({
printer,
showProgress = true,
showStatus = true,
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])
const PrinterState = ({ state, showProgress = true, showState = true }) => {
const currentState = state || {
type: 'unknown',
progress: 0
}
return (
<Flex gap='small' align={'center'}>
{showName && <Text>{printer.name}</Text>}
{showStatus && (
{showState && (
<Space>
<StateTag state={currentState.type} />
</Space>
@ -58,72 +26,14 @@ const PrinterState = ({
style={{ width: '150px', marginBottom: '2px' }}
/>
) : 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>
)
}
PrinterState.propTypes = {
printer: PropTypes.shape({
id: PropTypes.string,
name: PropTypes.string,
state: PropTypes.shape({
type: PropTypes.string,
progress: PropTypes.number
})
}),
state: PropTypes.object,
showProgress: PropTypes.bool,
showStatus: PropTypes.bool,
showName: PropTypes.bool,
showControls: PropTypes.bool
showState: PropTypes.bool
}
export default PrinterState

View File

@ -7,24 +7,17 @@ import {
Flex,
Typography,
Skeleton,
Spin,
Badge
Spin
} from 'antd'
import { LoadingOutlined, ExportOutlined } from '@ant-design/icons'
import { LoadingOutlined } from '@ant-design/icons'
import React, { useEffect, useState, useContext, useCallback } from 'react'
import axios from 'axios'
import { AuthContext } from '../context/AuthContext'
import config from '../../../config'
import IdDisplay from './IdDisplay'
import TimeDisplay from './TimeDisplay'
import { Tag } from 'antd'
import ObjectProperty from './ObjectProperty'
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 [spotlightData, setSpotlightData] = useState(null)
@ -89,101 +82,18 @@ const SpotlightTooltip = ({ query, type }) => {
)
}
// Helper to render value nicely
const renderValue = (key, value) => {
if (key === '_id' || key === 'id') {
return (
<IdDisplay
id={value}
type={type}
showCopy={true}
longId={false}
showSpotlight={false}
/>
)
// Helper to determine property type based on key and value
const getPropertyType = (key, value) => {
if (key === '_id') {
return 'id'
}
if (key === 'state') {
if (type === 'printer') {
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 === 'createdAt' || key === 'updatedAt') {
return 'dateTime'
}
if (key === 'tags' && Array.isArray(value)) {
if (value.length == 0) {
return <Text>n/a</Text>
}
return value.map((tag) => (
<Tag key={tag} color='blue'>
{tag}
</Tag>
))
if (typeof value === 'boolean') {
return 'bool'
}
if (key === 'email') {
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>
return key
}
// 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)
}
>
{renderValue(
key,
key === 'state' && value.type ? value.type : value
)}
<ObjectProperty
type={getPropertyType(key)}
value={key == 'state' ? spotlightData : value}
objectType={type}
isEditing={false}
longId={false}
showSpotlight={false}
showLabel={false}
showName={false}
showId={false}
showQuantity={false}
/>
</Descriptions.Item>
) : null
)

View File

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

View File

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

View File

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

View File

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