Refactored management components to utilize ObjectTable for consistent data representation and improved functionality. Updated column visibility handling and integrated new model properties for better data management. Removed unused imports and streamlined code for enhanced readability.

This commit is contained in:
Tom Butcher 2025-07-06 01:51:14 +01:00
parent fe85250838
commit fdc862d16c
43 changed files with 1536 additions and 2805 deletions

View File

@ -1,210 +1,33 @@
// src/filaments.js // src/filaments.js
import React, { useState, useContext, useCallback, useEffect } from 'react' import React, { useContext, useRef, useState } from 'react'
import { useNavigate } from 'react-router-dom' import { Button, Flex, Space, Modal, message, Dropdown } from 'antd'
import axios from 'axios'
import {
Table,
Badge,
Button,
Flex,
Space,
Modal,
message,
Dropdown,
Typography,
Checkbox,
Popover,
Input,
Spin
} from 'antd'
import { createStyles } from 'antd-style'
import { LoadingOutlined } from '@ant-design/icons'
import { AuthContext } from '../context/AuthContext' import { AuthContext } from '../context/AuthContext'
import NewFilament from './Filaments/NewFilament' import NewFilament from './Filaments/NewFilament'
import IdDisplay from '../common/IdDisplay'
import FilamentIcon from '../../Icons/FilamentIcon' import useColumnVisibility from '../hooks/useColumnVisibility'
import InfoCircleIcon from '../../Icons/InfoCircleIcon' import ColumnViewButton from '../common/ColumnViewButton'
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 ListIcon from '../../Icons/ListIcon'
import CheckIcon from '../../Icons/CheckIcon' import GridIcon from '../../Icons/GridIcon'
import useColumnVisibility from '../hooks/useColumnVisibility' import useViewMode from '../hooks/useViewMode'
import TimeDisplay from '../common/TimeDisplay'
import config from '../../../config'
const { Text } = Typography
const useStyle = createStyles(({ css, token }) => {
const { antCls } = token
return {
customTable: css`
${antCls}-table {
${antCls}-table-container {
${antCls}-table-body,
${antCls}-table-content {
scrollbar-width: thin;
scrollbar-color: #eaeaea transparent;
scrollbar-gutter: stable;
}
}
}
`
}
})
const Filaments = () => { const Filaments = () => {
const [messageApi, contextHolder] = message.useMessage() const [messageApi, contextHolder] = message.useMessage()
const navigate = useNavigate()
const { styles } = useStyle()
const [filamentsData, setFilamentsData] = useState([])
const [newFilamentOpen, setNewFilamentOpen] = useState(false) const [newFilamentOpen, setNewFilamentOpen] = useState(false)
const [loading, setLoading] = useState(true) const tableRef = useRef()
// View mode state (cards/list), persisted in sessionStorage via custom hook
const [viewMode, setViewMode] = useViewMode('filament')
const [columnVisibility, setColumnVisibility] =
useColumnVisibility('filament')
const { authenticated } = useContext(AuthContext) const { authenticated } = useContext(AuthContext)
const [page, setPage] = useState(1)
const [hasMore, setHasMore] = useState(true)
const [lazyLoading, setLazyLoading] = useState(false)
const [filters, setFilters] = useState({})
const [sorter, setSorter] = useState({})
const fetchFilamentsData = useCallback(
async (pageNum = 1, append = false) => {
try {
const response = await axios.get(`${config.backendUrl}/filaments`, {
params: {
page: pageNum,
limit: 25,
...filters,
sort: sorter.field,
order: sorter.order
},
headers: {
Accept: 'application/json'
},
withCredentials: true
})
const newData = response.data
setHasMore(newData.length === 25)
if (append) {
setFilamentsData((prev) => [...prev, ...newData])
} else {
setFilamentsData(newData)
}
setLoading(false)
setLazyLoading(false)
} catch (err) {
messageApi.info(err)
setLoading(false)
setLazyLoading(false)
}
},
[messageApi, filters, sorter]
)
const handleScroll = useCallback(
(e) => {
const { target } = e
const scrollHeight = target.scrollHeight
const scrollTop = target.scrollTop
const clientHeight = target.clientHeight
if (
scrollHeight - scrollTop - clientHeight < 100 &&
!lazyLoading &&
hasMore
) {
setLazyLoading(true)
const nextPage = page + 1
setPage(nextPage)
fetchFilamentsData(nextPage, true)
}
},
[page, lazyLoading, hasMore, fetchFilamentsData]
)
const getFilterDropdown = ({
setSelectedKeys,
selectedKeys,
confirm,
clearFilters,
propertyName
}) => {
return (
<div style={{ padding: 8 }}>
<Space.Compact>
<Input
placeholder={'Search ' + propertyName}
value={selectedKeys[0]}
onChange={(e) =>
setSelectedKeys(e.target.value ? [e.target.value] : [])
}
onPressEnter={() => confirm()}
style={{ width: 200, display: 'block' }}
/>
<Button
onClick={() => {
clearFilters()
confirm()
}}
icon={<XMarkIcon />}
/>
<Button
type='primary'
onClick={() => confirm()}
icon={<CheckIcon />}
/>
</Space.Compact>
</div>
)
}
const getViewDropdownItems = () => {
const columnItems = columns
.filter((col) => col.key && col.title !== '')
.map((col) => (
<Checkbox
checked={columnVisibility[col.key]}
key={col.key}
onChange={(e) => {
updateColumnVisibility(col.key, e.target.checked)
}}
>
{col.title}
</Checkbox>
))
return (
<Flex vertical>
<Flex vertical gap='middle' style={{ margin: '4px 8px' }}>
{columnItems}
</Flex>
</Flex>
)
}
const handleTableChange = (pagination, filters, sorter) => {
const newFilters = {}
Object.entries(filters).forEach(([key, value]) => {
if (value && value.length > 0) {
newFilters[key] = value[0]
}
})
setPage(1)
setFilters(newFilters)
setSorter({
field: sorter.field,
order: sorter.order
})
}
const actionItems = { const actionItems = {
items: [ items: [
{ {
@ -221,291 +44,65 @@ const Filaments = () => {
], ],
onClick: ({ key }) => { onClick: ({ key }) => {
if (key === 'reloadList') { if (key === 'reloadList') {
fetchFilamentsData() tableRef.current?.reload()
} else if (key === 'newFilament') { } else if (key === 'newFilament') {
setNewFilamentOpen(true) setNewFilamentOpen(true)
} }
} }
} }
const getFilamentActionItems = (id) => {
return {
items: [
{
label: 'Info',
key: 'info',
icon: <InfoCircleIcon />
}
],
onClick: ({ key }) => {
if (key === 'info') {
navigate(`/dashboard/management/filaments/info?filamentId=${id}`)
}
}
}
}
// Column definitions
const columns = [
{
title: '',
dataIndex: '',
key: 'icon',
width: 40,
fixed: 'left',
render: () => <FilamentIcon></FilamentIcon>
},
{
title: 'Name',
dataIndex: 'name',
key: 'name',
width: 200,
fixed: 'left',
filterDropdown: ({
setSelectedKeys,
selectedKeys,
confirm,
clearFilters
}) =>
getFilterDropdown({
setSelectedKeys,
selectedKeys,
confirm,
clearFilters,
propertyName: 'name'
}),
onFilter: (value, record) =>
record.name.toLowerCase().includes(value.toLowerCase()),
sorter: true
},
{
title: 'ID',
dataIndex: '_id',
key: 'id',
width: 180,
render: (text) => (
<IdDisplay id={text} type={'filament'} longId={false} />
),
filterDropdown: ({
setSelectedKeys,
selectedKeys,
confirm,
clearFilters
}) =>
getFilterDropdown({
setSelectedKeys,
selectedKeys,
confirm,
clearFilters,
propertyName: 'ID'
}),
onFilter: (value, record) =>
record._id.toLowerCase().includes(value.toLowerCase()),
sorter: true
},
{
title: 'Vendor',
dataIndex: 'vendor',
key: 'vendor',
width: 200,
render: (vendor) => {
return vendor.name
},
filterDropdown: ({
setSelectedKeys,
selectedKeys,
confirm,
clearFilters
}) =>
getFilterDropdown({
setSelectedKeys,
selectedKeys,
confirm,
clearFilters,
propertyName: 'vendor'
}),
onFilter: (value, record) =>
record.vendor.name.toLowerCase().includes(value.toLowerCase()),
sorter: true
},
{
title: 'Material',
dataIndex: 'type',
width: 150,
key: 'material',
filterDropdown: ({
setSelectedKeys,
selectedKeys,
confirm,
clearFilters
}) =>
getFilterDropdown({
setSelectedKeys,
selectedKeys,
confirm,
clearFilters,
propertyName: 'material'
}),
onFilter: (value, record) =>
record.type.toLowerCase().includes(value.toLowerCase()),
sorter: true
},
{
title: 'Cost',
dataIndex: 'cost',
width: 120,
key: 'cost',
render: (cost) => {
return <Text ellipsis>{'£' + cost + ' per kg'}</Text>
},
sorter: true
},
{
title: 'Colour',
dataIndex: 'color',
key: 'color',
width: 120,
render: (color) => {
return <Badge color={color} text={color} />
},
filterDropdown: ({
setSelectedKeys,
selectedKeys,
confirm,
clearFilters
}) =>
getFilterDropdown({
setSelectedKeys,
selectedKeys,
confirm,
clearFilters,
propertyName: 'color'
}),
onFilter: (value, record) =>
record.color.toLowerCase().includes(value.toLowerCase()),
sorter: true
},
{
title: 'Created At',
dataIndex: 'createdAt',
key: 'createdAt',
width: 180,
render: (createdAt) => {
if (createdAt) {
return <TimeDisplay dateTime={createdAt} />
} else {
return 'n/a'
}
},
sorter: true,
defaultSortOrder: 'descend'
},
{
title: 'Updated At',
dataIndex: 'updatedAt',
key: 'updatedAt',
width: 180,
render: (updatedAt) => {
if (updatedAt) {
return <TimeDisplay dateTime={updatedAt} />
} else {
return 'n/a'
}
},
sorter: true,
defaultSortOrder: 'descend'
},
{
title: 'Actions',
key: 'actions',
fixed: 'right',
width: 150,
render: (text, record) => {
return (
<Space gap='small'>
<Button
icon={<InfoCircleIcon />}
onClick={() =>
navigate(
`/dashboard/management/filaments/info?filamentId=${record._id}`
)
}
/>
<Dropdown menu={getFilamentActionItems(record._id)}>
<Button>Actions</Button>
</Dropdown>
</Space>
)
}
}
]
const [columnVisibility, updateColumnVisibility] = useColumnVisibility(
'Filaments',
columns
)
const visibleColumns = columns.filter(
(col) => !col.key || columnVisibility[col.key]
)
useEffect(() => {
if (authenticated) {
fetchFilamentsData()
}
}, [authenticated, fetchFilamentsData])
return ( return (
<> <>
<Flex vertical={'true'} gap='large'> <Flex vertical={'true'} gap='large'>
{contextHolder} {contextHolder}
<Flex justify={'space-between'}> <Flex justify={'space-between'}>
<Space size='small'> <Space>
<Dropdown menu={actionItems}> <Dropdown menu={actionItems}>
<Button>Actions</Button> <Button>Actions</Button>
</Dropdown> </Dropdown>
<Popover <ColumnViewButton
content={getViewDropdownItems()} type='filament'
placement='bottomLeft' loading={false}
arrow={false} collapseState={columnVisibility}
> updateCollapseState={setColumnVisibility}
<Button>View</Button> />
</Popover> </Space>
<Space>
<Button
icon={viewMode === 'cards' ? <ListIcon /> : <GridIcon />}
onClick={() =>
setViewMode(viewMode === 'cards' ? 'list' : 'cards')
}
/>
</Space> </Space>
{lazyLoading && <Spin indicator={<LoadingOutlined />} />}
</Flex> </Flex>
<Table
dataSource={filamentsData} <ObjectTable
columns={visibleColumns} ref={tableRef}
className={styles.customTable} type={'filament'}
pagination={false} authenticated={authenticated}
scroll={{ y: 'calc(100vh - 270px)' }} cards={viewMode === 'cards'}
rowKey='_id' visibleColumns={columnVisibility}
loading={{ spinning: loading, indicator: <LoadingOutlined spin /> }}
onScroll={handleScroll}
onChange={handleTableChange}
showSorterTooltip={false}
/> />
</Flex>
<Modal <Modal
open={newFilamentOpen} open={newFilamentOpen}
styles={{ content: { paddingBottom: '24px' } }} footer={null}
footer={null} width={700}
width={700} onCancel={() => {
onCancel={() => {
setNewFilamentOpen(false)
}}
destroyOnHidden={true}
>
<NewFilament
onOk={() => {
setNewFilamentOpen(false) setNewFilamentOpen(false)
messageApi.success('New filament created successfully.')
fetchFilamentsData()
}} }}
reset={newFilamentOpen} >
/> <NewFilament
</Modal> onOk={() => {
setNewFilamentOpen(false)
messageApi.success('New filament added successfully.')
tableRef.current?.reload()
}}
reset={newFilamentOpen}
/>
</Modal>
</Flex>
</> </>
) )
} }

View File

@ -1,259 +1,25 @@
import React, { useState, useContext, useRef } from 'react' import React, { useState, useContext, useRef } from 'react'
import { useNavigate } from 'react-router-dom' import { Button, Flex, Space, Modal, Dropdown, message } from 'antd'
import {
Button,
Flex,
Space,
Modal,
Dropdown,
message,
Checkbox,
Popover,
Input,
Badge,
Typography
} from 'antd'
import { AuthContext } from '../context/AuthContext' import { AuthContext } from '../context/AuthContext'
import IdDisplay from '../common/IdDisplay'
import NewNoteType from './NoteTypes/NewNoteType' import NewNoteType from './NoteTypes/NewNoteType'
import TimeDisplay from '../common/TimeDisplay'
import ObjectTable from '../common/ObjectTable' 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 CheckIcon from '../../Icons/CheckIcon'
import InfoCircleIcon from '../../Icons/InfoCircleIcon'
import useColumnVisibility from '../hooks/useColumnVisibility' import useColumnVisibility from '../hooks/useColumnVisibility'
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 ColumnViewButton from '../common/ColumnViewButton'
import config from '../../../config'
import NoteTypeIcon from '../../Icons/NoteTypeIcon'
import BoolDisplay from '../common/BoolDisplay'
const { Text } = Typography
const NoteTypes = () => { const NoteTypes = () => {
const [messageApi, contextHolder] = message.useMessage() const [messageApi, contextHolder] = message.useMessage()
const navigate = useNavigate()
const [newNoteTypeOpen, setNewNoteTypeOpen] = useState(false) const [newNoteTypeOpen, setNewNoteTypeOpen] = useState(false)
const tableRef = useRef() const tableRef = useRef()
const { authenticated } = useContext(AuthContext) const { authenticated } = useContext(AuthContext)
const [viewMode, setViewMode] = useViewMode('NoteTypes') const [viewMode, setViewMode] = useViewMode('noteType')
const getFilterDropdown = ({ const [columnVisibility, setColumnVisibility] =
setSelectedKeys, useColumnVisibility('noteType')
selectedKeys,
confirm,
clearFilters,
propertyName
}) => {
return (
<div style={{ padding: 8 }}>
<Space.Compact>
<Input
placeholder={'Search ' + propertyName}
value={selectedKeys[0]}
onChange={(e) =>
setSelectedKeys(e.target.value ? [e.target.value] : [])
}
onPressEnter={() => confirm()}
style={{ width: 200, display: 'block' }}
/>
<Button
onClick={() => {
clearFilters()
confirm()
}}
icon={<XMarkIcon />}
/>
<Button
type='primary'
onClick={() => confirm()}
icon={<CheckIcon />}
/>
</Space.Compact>
</div>
)
}
const getViewDropdownItems = () => {
const columnItems = columns
.filter((col) => col.key && col.title !== '')
.map((col) => (
<Checkbox
checked={columnVisibility[col.key]}
key={col.key}
onChange={(e) => {
updateColumnVisibility(col.key, e.target.checked)
}}
>
{col.title}
</Checkbox>
))
return (
<Flex vertical>
<Flex vertical gap='middle' style={{ margin: '4px 8px' }}>
{columnItems}
</Flex>
</Flex>
)
}
const getNoteTypeActionItems = (id) => {
return {
items: [
{
label: 'Info',
key: 'info',
icon: <InfoCircleIcon />
}
],
onClick: ({ key }) => {
if (key === 'info') {
navigate(`/dashboard/management/notetypes/info?noteTypeId=${id}`)
}
}
}
}
const columns = [
{
title: <NoteTypeIcon />,
key: 'icon',
width: 40,
fixed: 'left',
render: () => <NoteTypeIcon />
},
{
title: 'Name',
dataIndex: 'name',
key: 'name',
width: 200,
fixed: 'left',
filterDropdown: ({
setSelectedKeys,
selectedKeys,
confirm,
clearFilters
}) =>
getFilterDropdown({
setSelectedKeys,
selectedKeys,
confirm,
clearFilters,
propertyName: 'name'
}),
onFilter: (value, record) =>
record.name.toLowerCase().includes(value.toLowerCase()),
sorter: true
},
{
title: 'ID',
dataIndex: '_id',
key: 'id',
width: 180,
render: (text) => (
<IdDisplay id={text} type={'notetype'} longId={false} />
),
filterDropdown: ({
setSelectedKeys,
selectedKeys,
confirm,
clearFilters
}) =>
getFilterDropdown({
setSelectedKeys,
selectedKeys,
confirm,
clearFilters,
propertyName: 'ID'
}),
onFilter: (value, record) =>
record._id.toLowerCase().includes(value.toLowerCase()),
sorter: true
},
{
title: 'Color',
dataIndex: 'color',
key: 'color',
width: 120,
render: (color) =>
color ? <Badge color={color} text={color} /> : <Text>n/a</Text>
},
{
title: 'Active',
dataIndex: 'active',
key: 'active',
width: 100,
render: (active) => <BoolDisplay value={active} yesNo={true} />,
sorter: true
},
{
title: 'Created At',
dataIndex: 'createdAt',
key: 'createdAt',
width: 180,
render: (createdAt) => {
if (createdAt) {
return <TimeDisplay dateTime={createdAt} />
} else {
return 'n/a'
}
},
sorter: true,
defaultSortOrder: 'descend'
},
{
title: 'Updated At',
dataIndex: 'updatedAt',
key: 'updatedAt',
width: 180,
render: (updatedAt) => {
if (updatedAt) {
return <TimeDisplay dateTime={updatedAt} />
} else {
return 'n/a'
}
},
sorter: true,
defaultSortOrder: 'descend'
},
{
title: 'Actions',
key: 'actions',
fixed: 'right',
width: 150,
render: (record) => {
return (
<Space gap='small'>
<Button
icon={<InfoCircleIcon />}
onClick={() =>
navigate(
`/dashboard/management/notetypes/info?noteTypeId=${record._id}`
)
}
/>
<Dropdown menu={getNoteTypeActionItems(record._id)}>
<Button>Actions</Button>
</Dropdown>
</Space>
)
}
}
]
const [columnVisibility, updateColumnVisibility] = useColumnVisibility(
'NoteTypes',
columns
)
const visibleColumns = columns.filter(
(col) => !col.key || columnVisibility[col.key]
)
const actionItems = { const actionItems = {
items: [ items: [
@ -287,13 +53,12 @@ const NoteTypes = () => {
<Dropdown menu={actionItems}> <Dropdown menu={actionItems}>
<Button>Actions</Button> <Button>Actions</Button>
</Dropdown> </Dropdown>
<Popover <ColumnViewButton
content={getViewDropdownItems()} type='noteType'
placement='bottomLeft' loading={false}
arrow={false} collapseState={columnVisibility}
> updateCollapseState={setColumnVisibility}
<Button>View</Button> />
</Popover>
</Space> </Space>
<Space> <Space>
<Button <Button
@ -306,8 +71,8 @@ const NoteTypes = () => {
</Flex> </Flex>
<ObjectTable <ObjectTable
ref={tableRef} ref={tableRef}
columns={visibleColumns} visibleColumns={columnVisibility}
url={`${config.backendUrl}/notetypes`} type='noteType'
authenticated={authenticated} authenticated={authenticated}
cards={viewMode === 'cards'} cards={viewMode === 'cards'}
/> />

View File

@ -13,6 +13,10 @@ 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.js'
const NoteTypeInfo = () => { const NoteTypeInfo = () => {
const location = useLocation() const location = useLocation()
@ -108,50 +112,11 @@ const NoteTypeInfo = () => {
loading={loading} loading={loading}
indicator={<LoadingOutlined />} indicator={<LoadingOutlined />}
isEditing={isEditing} isEditing={isEditing}
type='notetype' type='noteType'
items={[ items={getModelProperties('noteType').map((prop) => ({
{ ...prop,
name: 'id', value: getPropertyValue(objectData, prop.name)
label: 'ID', }))}
value: objectData?._id,
type: 'id',
objectType: 'notetype',
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: 'color',
label: 'Color',
value: objectData?.color,
type: 'color'
},
{
name: 'active',
label: 'Active',
value: objectData?.active,
type: 'bool'
}
]}
/> />
</InfoCollapse> </InfoCollapse>

View File

@ -1,237 +1,34 @@
// src/gcodefiles.js // src/gcodefiles.js
import React, { useState, useContext, useRef } from 'react' import React, { useState, useContext, useRef } from 'react'
import { useNavigate } from 'react-router-dom'
import { import { Button, Flex, Space, Modal, Dropdown, message } from 'antd'
Button,
Flex,
Space,
Modal,
Dropdown,
Typography,
Checkbox,
Popover,
Input,
message
} from 'antd'
import { DownloadOutlined } from '@ant-design/icons'
import { AuthContext } from '../context/AuthContext' import { AuthContext } from '../context/AuthContext'
import IdDisplay from '../common/IdDisplay'
import ObjectTable from '../common/ObjectTable' import ObjectTable from '../common/ObjectTable'
import NewProduct from './Products/NewProduct' import NewProduct from './Products/NewProduct'
import PartIcon from '../../Icons/PartIcon'
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 XMarkIcon from '../../Icons/XMarkIcon'
import CheckIcon from '../../Icons/CheckIcon'
import useColumnVisibility from '../hooks/useColumnVisibility' import useColumnVisibility from '../hooks/useColumnVisibility'
import TimeDisplay from '../common/TimeDisplay'
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 ColumnViewButton from '../common/ColumnViewButton'
const { Text } = Typography
const Parts = () => { const Parts = () => {
const [messageApi, contextHolder] = message.useMessage() const [messageApi, contextHolder] = message.useMessage()
const navigate = useNavigate()
const [newProductOpen, setNewProductOpen] = useState(false) const [newProductOpen, setNewProductOpen] = useState(false)
const tableRef = useRef() const tableRef = useRef()
const { authenticated } = useContext(AuthContext) const { authenticated } = useContext(AuthContext)
const [viewMode, setViewMode] = useViewMode('Parts') const [viewMode, setViewMode] = useViewMode('part')
// Column definitions const [columnVisibility, setColumnVisibility] = useColumnVisibility('part')
const columns = [
{
title: <PartIcon />,
key: 'icon',
width: 40,
fixed: 'left',
render: () => <PartIcon />
},
{
title: 'Name',
dataIndex: 'name',
key: 'name',
width: 200,
fixed: 'left',
render: (text) => <Text ellipsis>{text}</Text>,
filterDropdown: ({
setSelectedKeys,
selectedKeys,
confirm,
clearFilters
}) =>
getFilterDropdown({
setSelectedKeys,
selectedKeys,
confirm,
clearFilters,
propertyName: 'name'
}),
onFilter: (value, record) =>
record.name.toLowerCase().includes(value.toLowerCase())
},
{
title: 'ID',
dataIndex: '_id',
key: 'id',
width: 180,
render: (text) => <IdDisplay id={text} type={'part'} longId={false} />
},
{
title: 'Product Name',
key: 'productName',
width: 200,
render: (record) => <Text>{record?.product?.name}</Text>,
filterDropdown: ({
setSelectedKeys,
selectedKeys,
confirm,
clearFilters
}) =>
getFilterDropdown({
setSelectedKeys,
selectedKeys,
confirm,
clearFilters,
propertyName: 'product name'
}),
onFilter: (value, record) =>
record.product.name.toLowerCase().includes(value.toLowerCase())
},
{
title: 'Product ID',
key: 'productId',
width: 180,
render: (record) => (
<IdDisplay
id={record?.product?._id}
type={'product'}
longId={false}
showHyperlink={true}
/>
)
},
{
title: 'Created At',
dataIndex: 'createdAt',
key: 'createdAt',
width: 180,
render: (createdAt) => {
if (createdAt) {
return <TimeDisplay dateTime={createdAt} />
} else {
return 'n/a'
}
},
sorter: true
},
{
title: 'Updated At',
dataIndex: 'updatedAt',
key: 'updatedAt',
width: 180,
render: (updatedAt) => {
if (updatedAt) {
return <TimeDisplay dateTime={updatedAt} />
} else {
return 'n/a'
}
},
sorter: true
},
{
title: 'Actions',
key: 'actions',
fixed: 'right',
width: 150,
render: (record) => {
return (
<Space gap='small'>
<Button
icon={<InfoCircleIcon />}
onClick={() =>
navigate(
`/dashboard/management/parts/info?partId=${record._id}`
)
}
/>
<Dropdown menu={getPartActionItems(record._id)}>
<Button>Actions</Button>
</Dropdown>
</Space>
)
}
}
]
const [columnVisibility, updateColumnVisibility] = useColumnVisibility(
'Parts',
columns
)
const getPartActionItems = (id) => {
return {
items: [
{
label: 'Info',
key: 'info',
icon: <InfoCircleIcon />
},
{
label: 'Download',
key: 'download',
icon: <DownloadOutlined />
}
],
onClick: ({ key }) => {
if (key === 'info') {
navigate(`/dashboard/management/parts/info?partId=${id}`)
}
}
}
}
const getFilterDropdown = ({
setSelectedKeys,
selectedKeys,
confirm,
clearFilters,
propertyName
}) => {
return (
<div style={{ padding: 8 }}>
<Space.Compact>
<Input
placeholder={'Search ' + propertyName}
value={selectedKeys[0]}
onChange={(e) =>
setSelectedKeys(e.target.value ? [e.target.value] : [])
}
onPressEnter={() => confirm()}
style={{ width: 200, display: 'block' }}
/>
<Button
onClick={() => {
clearFilters()
confirm()
}}
icon={<XMarkIcon />}
/>
<Button
type='primary'
onClick={() => confirm()}
icon={<CheckIcon />}
/>
</Space.Compact>
</div>
)
}
const actionItems = { const actionItems = {
items: [ items: [
@ -256,34 +53,6 @@ const Parts = () => {
} }
} }
const getViewDropdownItems = () => {
const columnItems = columns
.filter((col) => col.key && col.title !== '')
.map((col) => (
<Checkbox
checked={columnVisibility[col.key]}
key={col.key}
onChange={(e) => {
updateColumnVisibility(col.key, e.target.checked)
}}
>
{col.title}
</Checkbox>
))
return (
<Flex vertical>
<Flex vertical gap='middle' style={{ margin: '4px 8px' }}>
{columnItems}
</Flex>
</Flex>
)
}
const visibleColumns = columns.filter(
(col) => !col.key || columnVisibility[col.key]
)
return ( return (
<> <>
<Flex vertical={'true'} gap='large'> <Flex vertical={'true'} gap='large'>
@ -293,13 +62,12 @@ const Parts = () => {
<Dropdown menu={actionItems}> <Dropdown menu={actionItems}>
<Button>Actions</Button> <Button>Actions</Button>
</Dropdown> </Dropdown>
<Popover <ColumnViewButton
content={getViewDropdownItems()} type='part'
placement='bottomLeft' loading={false}
arrow={false} collapseState={columnVisibility}
> updateCollapseState={setColumnVisibility}
<Button>View</Button> />
</Popover>
</Space> </Space>
<Space> <Space>
<Button <Button
@ -312,8 +80,8 @@ const Parts = () => {
</Flex> </Flex>
<ObjectTable <ObjectTable
ref={tableRef} ref={tableRef}
columns={visibleColumns} visibleColumns={columnVisibility}
url={`${config.backendUrl}/parts`} type='part'
authenticated={authenticated} authenticated={authenticated}
cards={viewMode === 'cards'} cards={viewMode === 'cards'}
/> />

View File

@ -32,8 +32,6 @@ 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'
const Products = () => { const Products = () => {
const [messageApi, contextHolder] = message.useMessage() const [messageApi, contextHolder] = message.useMessage()
const navigate = useNavigate() const navigate = useNavigate()
@ -323,10 +321,6 @@ const Products = () => {
) )
} }
const visibleColumns = columns.filter(
(col) => !col.key || columnVisibility[col.key]
)
return ( return (
<> <>
<Flex vertical={'true'} gap='large'> <Flex vertical={'true'} gap='large'>
@ -355,8 +349,7 @@ const Products = () => {
</Flex> </Flex>
<ObjectTable <ObjectTable
ref={tableRef} ref={tableRef}
columns={visibleColumns} type={'product'}
url={`${config.backendUrl}/products`}
authenticated={authenticated} authenticated={authenticated}
cards={viewMode === 'cards'} cards={viewMode === 'cards'}
/> />

View File

@ -1,334 +1,20 @@
import React, { useContext, useRef } from 'react' import React, { useContext, useRef } from 'react'
import { useNavigate } from 'react-router-dom' import { Button, Flex, Space, Dropdown } from 'antd'
import {
Button,
Flex,
Space,
Dropdown,
Typography,
Checkbox,
Popover,
Input
} from 'antd'
import { ExportOutlined } from '@ant-design/icons'
import { AuthContext } from '../context/AuthContext' import { AuthContext } from '../context/AuthContext'
import IdDisplay from '../common/IdDisplay'
import TimeDisplay from '../common/TimeDisplay'
import ObjectTable from '../common/ObjectTable' import ObjectTable from '../common/ObjectTable'
import PersonIcon from '../../Icons/PersonIcon'
import ReloadIcon from '../../Icons/ReloadIcon' import ReloadIcon from '../../Icons/ReloadIcon'
import XMarkIcon from '../../Icons/XMarkIcon'
import CheckIcon from '../../Icons/CheckIcon'
import useColumnVisibility from '../hooks/useColumnVisibility' import useColumnVisibility from '../hooks/useColumnVisibility'
import InfoCircleIcon from '../../Icons/InfoCircleIcon'
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 ColumnViewButton from '../common/ColumnViewButton'
import config from '../../../config'
const { Link, Text } = Typography
const Users = () => { const Users = () => {
const navigate = useNavigate()
const tableRef = useRef() const tableRef = useRef()
const { authenticated } = useContext(AuthContext) const { authenticated } = useContext(AuthContext)
const [viewMode, setViewMode] = useViewMode('Users') const [viewMode, setViewMode] = useViewMode('user')
const getFilterDropdown = ({ const [columnVisibility, setColumnVisibility] = useColumnVisibility('user')
setSelectedKeys,
selectedKeys,
confirm,
clearFilters,
propertyName
}) => {
return (
<div style={{ padding: 8 }}>
<Space.Compact>
<Input
placeholder={'Search ' + propertyName}
value={selectedKeys[0]}
onChange={(e) =>
setSelectedKeys(e.target.value ? [e.target.value] : [])
}
onPressEnter={() => confirm()}
style={{ width: 200, display: 'block' }}
/>
<Button
onClick={() => {
clearFilters()
confirm()
}}
icon={<XMarkIcon />}
/>
<Button
type='primary'
onClick={() => confirm()}
icon={<CheckIcon />}
/>
</Space.Compact>
</div>
)
}
const getViewDropdownItems = () => {
const columnItems = columns
.filter((col) => col.key && col.title !== '')
.map((col) => (
<Checkbox
checked={columnVisibility[col.key]}
key={col.key}
onChange={(e) => {
updateColumnVisibility(col.key, e.target.checked)
}}
>
{col.title}
</Checkbox>
))
return (
<Flex vertical>
<Flex vertical gap='middle' style={{ margin: '4px 8px' }}>
{columnItems}
</Flex>
</Flex>
)
}
const getUserActionItems = (id) => {
return {
items: [
{
label: 'Info',
key: 'info',
icon: <InfoCircleIcon />
}
],
onClick: ({ key }) => {
if (key === 'info') {
navigate(`/dashboard/management/users/info?userId=${id}`)
}
}
}
}
const columns = [
{
title: <PersonIcon />,
key: 'icon',
width: 40,
fixed: 'left',
render: () => <PersonIcon />
},
{
title: 'Name',
dataIndex: 'name',
key: 'name',
width: 200,
render: (text) => (text ? text : 'n/a'),
filterDropdown: ({
setSelectedKeys,
selectedKeys,
confirm,
clearFilters
}) =>
getFilterDropdown({
setSelectedKeys,
selectedKeys,
confirm,
clearFilters,
propertyName: 'name'
}),
onFilter: (value, record) =>
record.name?.toLowerCase().includes(value.toLowerCase()),
sorter: true
},
{
title: 'Username',
dataIndex: 'username',
key: 'username',
width: 150,
fixed: 'left',
filterDropdown: ({
setSelectedKeys,
selectedKeys,
confirm,
clearFilters
}) =>
getFilterDropdown({
setSelectedKeys,
selectedKeys,
confirm,
clearFilters,
propertyName: 'username'
}),
onFilter: (value, record) =>
record.username.toLowerCase().includes(value.toLowerCase()),
sorter: true
},
{
title: 'First Name',
dataIndex: 'firstName',
key: 'firstName',
width: 150,
render: (text) => (text ? text : 'n/a'),
filterDropdown: ({
setSelectedKeys,
selectedKeys,
confirm,
clearFilters
}) =>
getFilterDropdown({
setSelectedKeys,
selectedKeys,
confirm,
clearFilters,
propertyName: 'first name'
}),
onFilter: (value, record) =>
record.firstName?.toLowerCase().includes(value.toLowerCase()),
sorter: true
},
{
title: 'Last Name',
dataIndex: 'lastName',
key: 'lastName',
width: 150,
render: (text) => (text ? text : 'n/a'),
filterDropdown: ({
setSelectedKeys,
selectedKeys,
confirm,
clearFilters
}) =>
getFilterDropdown({
setSelectedKeys,
selectedKeys,
confirm,
clearFilters,
propertyName: 'last name'
}),
onFilter: (value, record) =>
record.lastName?.toLowerCase().includes(value.toLowerCase()),
sorter: true
},
{
title: 'Email',
dataIndex: 'email',
key: 'email',
width: 250,
render: (email) =>
email ? (
<Link href={`mailto:${email}`}>
{email} <ExportOutlined />
</Link>
) : (
<Text>n/a</Text>
),
filterDropdown: ({
setSelectedKeys,
selectedKeys,
confirm,
clearFilters
}) =>
getFilterDropdown({
setSelectedKeys,
selectedKeys,
confirm,
clearFilters,
propertyName: 'email'
}),
onFilter: (value, record) =>
record.email?.toLowerCase().includes(value.toLowerCase()),
sorter: true
},
{
title: 'ID',
dataIndex: '_id',
key: 'id',
width: 180,
render: (text) => <IdDisplay id={text} type={'user'} longId={false} />,
filterDropdown: ({
setSelectedKeys,
selectedKeys,
confirm,
clearFilters
}) =>
getFilterDropdown({
setSelectedKeys,
selectedKeys,
confirm,
clearFilters,
propertyName: 'ID'
}),
onFilter: (value, record) =>
record._id.toLowerCase().includes(value.toLowerCase()),
sorter: true
},
{
title: 'Created At',
dataIndex: 'createdAt',
key: 'createdAt',
width: 180,
render: (createdAt) => {
if (createdAt) {
return <TimeDisplay dateTime={createdAt} />
} else {
return 'n/a'
}
},
sorter: true,
defaultSortOrder: 'descend'
},
{
title: 'Updated At',
dataIndex: 'updatedAt',
key: 'updatedAt',
width: 180,
render: (updatedAt) => {
if (updatedAt) {
return <TimeDisplay dateTime={updatedAt} />
} else {
return 'n/a'
}
},
sorter: true,
defaultSortOrder: 'descend'
},
{
title: 'Actions',
key: 'actions',
fixed: 'right',
width: 150,
render: (record) => {
return (
<Space gap='small'>
<Button
icon={<InfoCircleIcon />}
onClick={() =>
navigate(
`/dashboard/management/users/info?userId=${record._id}`
)
}
/>
<Dropdown menu={getUserActionItems(record._id)}>
<Button>Actions</Button>
</Dropdown>
</Space>
)
}
}
]
const [columnVisibility, updateColumnVisibility] = useColumnVisibility(
'Users',
columns
)
const visibleColumns = columns.filter(
(col) => !col.key || columnVisibility[col.key]
)
const actionItems = { const actionItems = {
items: [ items: [
@ -352,13 +38,12 @@ const Users = () => {
<Dropdown menu={actionItems}> <Dropdown menu={actionItems}>
<Button>Actions</Button> <Button>Actions</Button>
</Dropdown> </Dropdown>
<Popover <ColumnViewButton
content={getViewDropdownItems()} type='user'
placement='bottomLeft' loading={false}
arrow={false} collapseState={columnVisibility}
> updateCollapseState={setColumnVisibility}
<Button>View</Button> />
</Popover>
</Space> </Space>
<Space> <Space>
<Button <Button
@ -369,8 +54,8 @@ const Users = () => {
</Flex> </Flex>
<ObjectTable <ObjectTable
ref={tableRef} ref={tableRef}
columns={visibleColumns} type={'user'}
url={`${config.backendUrl}/users`} visibleColumns={columnVisibility}
authenticated={authenticated} authenticated={authenticated}
cards={viewMode === 'cards'} cards={viewMode === 'cards'}
/> />

View File

@ -15,6 +15,10 @@ 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.js'
const UserInfo = () => { const UserInfo = () => {
const location = useLocation() const location = useLocation()
@ -110,63 +114,10 @@ const UserInfo = () => {
indicator={<LoadingOutlined />} indicator={<LoadingOutlined />}
isEditing={isEditing} isEditing={isEditing}
type='user' type='user'
items={[ items={getModelProperties('user').map((prop) => ({
{ ...prop,
name: '_id', value: getPropertyValue(objectData, prop.name)
label: 'ID', }))}
value: objectData?._id,
type: 'id',
objectType: 'user',
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: 'firstName',
label: 'First Name',
value: objectData?.firstName,
type: 'text'
},
{
name: 'username',
label: 'Username',
value: objectData?.username,
required: true,
type: 'text'
},
{
name: 'lastName',
label: 'Last Name',
value: objectData?.lastName,
type: 'text'
},
{
name: 'email',
label: 'Email',
value: objectData?.email,
type: 'email'
}
]}
/> />
</InfoCollapse> </InfoCollapse>

View File

@ -1,318 +1,24 @@
import React, { useState, useContext, useRef } from 'react' import React, { useState, useContext, useRef } from 'react'
import { useNavigate } from 'react-router-dom' import { Button, Flex, Space, Modal, Dropdown, message } from 'antd'
import {
Button,
Flex,
Space,
Modal,
Dropdown,
message,
Typography,
Checkbox,
Popover,
Input
} from 'antd'
import { ExportOutlined } from '@ant-design/icons'
import { AuthContext } from '../context/AuthContext' import { AuthContext } from '../context/AuthContext'
import IdDisplay from '../common/IdDisplay'
import NewVendor from './Vendors/NewVendor' import NewVendor from './Vendors/NewVendor'
import CountryDisplay from '../common/CountryDisplay'
import TimeDisplay from '../common/TimeDisplay'
import ObjectTable from '../common/ObjectTable' import ObjectTable from '../common/ObjectTable'
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'
import XMarkIcon from '../../Icons/XMarkIcon'
import CheckIcon from '../../Icons/CheckIcon'
import useColumnVisibility from '../hooks/useColumnVisibility' import useColumnVisibility from '../hooks/useColumnVisibility'
import InfoCircleIcon from '../../Icons/InfoCircleIcon'
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 ColumnViewButton from '../common/ColumnViewButton'
import config from '../../../config'
const { Link } = Typography
const Vendors = () => { const Vendors = () => {
const [messageApi, contextHolder] = message.useMessage() const [messageApi, contextHolder] = message.useMessage()
const navigate = useNavigate()
const [newVendorOpen, setNewVendorOpen] = useState(false) const [newVendorOpen, setNewVendorOpen] = useState(false)
const tableRef = useRef() const tableRef = useRef()
const { authenticated } = useContext(AuthContext) const { authenticated } = useContext(AuthContext)
const [viewMode, setViewMode] = useViewMode('Vendors') const [viewMode, setViewMode] = useViewMode('vendor')
const getFilterDropdown = ({ const [columnVisibility, setColumnVisibility] = useColumnVisibility('vendor')
setSelectedKeys,
selectedKeys,
confirm,
clearFilters,
propertyName
}) => {
return (
<div style={{ padding: 8 }}>
<Space.Compact>
<Input
placeholder={'Search ' + propertyName}
value={selectedKeys[0]}
onChange={(e) =>
setSelectedKeys(e.target.value ? [e.target.value] : [])
}
onPressEnter={() => confirm()}
style={{ width: 200, display: 'block' }}
/>
<Button
onClick={() => {
clearFilters()
confirm()
}}
icon={<XMarkIcon />}
/>
<Button
type='primary'
onClick={() => confirm()}
icon={<CheckIcon />}
/>
</Space.Compact>
</div>
)
}
const getViewDropdownItems = () => {
const columnItems = columns
.filter((col) => col.key && col.title !== '')
.map((col) => (
<Checkbox
checked={columnVisibility[col.key]}
key={col.key}
onChange={(e) => {
updateColumnVisibility(col.key, e.target.checked)
}}
>
{col.title}
</Checkbox>
))
return (
<Flex vertical>
<Flex vertical gap='middle' style={{ margin: '4px 8px' }}>
{columnItems}
</Flex>
</Flex>
)
}
const getVendorActionItems = (id) => {
return {
items: [
{
label: 'Info',
key: 'info',
icon: <InfoCircleIcon />
}
],
onClick: ({ key }) => {
if (key === 'info') {
navigate(`/dashboard/management/vendors/info?vendorId=${id}`)
}
}
}
}
const columns = [
{
title: <VendorIcon />,
key: 'icon',
width: 40,
fixed: 'left',
render: () => <VendorIcon />
},
{
title: 'Name',
dataIndex: 'name',
key: 'name',
width: 200,
fixed: 'left',
filterDropdown: ({
setSelectedKeys,
selectedKeys,
confirm,
clearFilters
}) =>
getFilterDropdown({
setSelectedKeys,
selectedKeys,
confirm,
clearFilters,
propertyName: 'name'
}),
onFilter: (value, record) =>
record.name.toLowerCase().includes(value.toLowerCase()),
sorter: true
},
{
title: 'ID',
dataIndex: '_id',
key: 'id',
width: 180,
render: (text) => <IdDisplay id={text} type={'vendor'} longId={false} />,
filterDropdown: ({
setSelectedKeys,
selectedKeys,
confirm,
clearFilters
}) =>
getFilterDropdown({
setSelectedKeys,
selectedKeys,
confirm,
clearFilters,
propertyName: 'ID'
}),
onFilter: (value, record) =>
record._id.toLowerCase().includes(value.toLowerCase()),
sorter: true
},
{
title: 'Website',
dataIndex: 'website',
key: 'website',
width: 200,
render: (text) =>
text ? (
<Link href={text} target='_blank' rel='noopener noreferrer'>
{new URL(text).hostname + ' '}
<ExportOutlined />
</Link>
) : (
'n/a'
),
filterDropdown: ({
setSelectedKeys,
selectedKeys,
confirm,
clearFilters
}) =>
getFilterDropdown({
setSelectedKeys,
selectedKeys,
confirm,
clearFilters,
propertyName: 'website'
}),
onFilter: (value, record) =>
record.website?.toLowerCase().includes(value.toLowerCase()),
sorter: true
},
{
title: 'Country',
dataIndex: 'country',
key: 'country',
width: 200,
render: (text) => (text ? <CountryDisplay countryCode={text} /> : 'n/a'),
filterDropdown: ({
setSelectedKeys,
selectedKeys,
confirm,
clearFilters
}) =>
getFilterDropdown({
setSelectedKeys,
selectedKeys,
confirm,
clearFilters,
propertyName: 'country'
}),
onFilter: (value, record) =>
record.country?.toLowerCase().includes(value.toLowerCase()),
sorter: true
},
{
title: 'Contact',
dataIndex: 'contact',
key: 'contact',
width: 200,
render: (text) => (text ? text : 'n/a'),
filterDropdown: ({
setSelectedKeys,
selectedKeys,
confirm,
clearFilters
}) =>
getFilterDropdown({
setSelectedKeys,
selectedKeys,
confirm,
clearFilters,
propertyName: 'contact'
}),
onFilter: (value, record) =>
record.contact?.toLowerCase().includes(value.toLowerCase()),
sorter: true
},
{
title: 'Created At',
dataIndex: 'createdAt',
key: 'createdAt',
width: 180,
render: (createdAt) => {
if (createdAt) {
return <TimeDisplay dateTime={createdAt} />
} else {
return 'n/a'
}
},
sorter: true,
defaultSortOrder: 'descend'
},
{
title: 'Updated At',
dataIndex: 'updatedAt',
key: 'updatedAt',
width: 180,
render: (updatedAt) => {
if (updatedAt) {
return <TimeDisplay dateTime={updatedAt} />
} else {
return 'n/a'
}
},
sorter: true,
defaultSortOrder: 'descend'
},
{
title: 'Actions',
key: 'actions',
fixed: 'right',
width: 150,
render: (record) => {
return (
<Space gap='small'>
<Button
icon={<InfoCircleIcon />}
onClick={() =>
navigate(
`/dashboard/management/vendors/info?vendorId=${record._id}`
)
}
/>
<Dropdown menu={getVendorActionItems(record._id)}>
<Button>Actions</Button>
</Dropdown>
</Space>
)
}
}
]
const [columnVisibility, updateColumnVisibility] = useColumnVisibility(
'Vendors',
columns
)
const visibleColumns = columns.filter(
(col) => !col.key || columnVisibility[col.key]
)
const actionItems = { const actionItems = {
items: [ items: [
@ -346,13 +52,12 @@ const Vendors = () => {
<Dropdown menu={actionItems}> <Dropdown menu={actionItems}>
<Button>Actions</Button> <Button>Actions</Button>
</Dropdown> </Dropdown>
<Popover <ColumnViewButton
content={getViewDropdownItems()} type='vendor'
placement='bottomLeft' loading={false}
arrow={false} collapseState={columnVisibility}
> updateCollapseState={setColumnVisibility}
<Button>View</Button> />
</Popover>
</Space> </Space>
<Space> <Space>
<Button <Button
@ -365,8 +70,8 @@ const Vendors = () => {
</Flex> </Flex>
<ObjectTable <ObjectTable
ref={tableRef} ref={tableRef}
columns={visibleColumns} visibleColumns={columnVisibility}
url={`${config.backendUrl}/vendors`} type='vendor'
authenticated={authenticated} authenticated={authenticated}
cards={viewMode === 'cards'} cards={viewMode === 'cards'}
/> />

View File

@ -1,300 +1,30 @@
// src/gcodefiles.js // src/gcodefiles.js
import React, { useState, useContext, useRef } from 'react' import React, { useState, useContext, useRef } from 'react'
import { useNavigate } from 'react-router-dom' import { Button, Flex, Space, Modal, Dropdown, message } from 'antd'
import axios from 'axios'
import {
Badge,
Button,
Flex,
Space,
Modal,
Dropdown,
Typography,
message,
Checkbox,
Divider,
Popover,
Input
} from 'antd'
import { DownloadOutlined } from '@ant-design/icons'
import { AuthContext } from '../context/AuthContext' import { AuthContext } from '../context/AuthContext'
import NewGCodeFile from './GCodeFiles/NewGCodeFile' import NewGCodeFile from './GCodeFiles/NewGCodeFile'
import IdDisplay from '../common/IdDisplay'
import GCodeFileIcon from '../../Icons/GCodeFileIcon'
import InfoCircleIcon from '../../Icons/InfoCircleIcon'
import useColumnVisibility from '../hooks/useColumnVisibility' import useColumnVisibility from '../hooks/useColumnVisibility'
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 CheckIcon from '../../Icons/CheckIcon'
import TimeDisplay from '../common/TimeDisplay'
import ObjectTable from '../common/ObjectTable' 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'
import config from '../../../config' import ColumnViewButton from '../common/ColumnViewButton'
const { Text } = Typography
const GCodeFiles = () => { const GCodeFiles = () => {
const [messageApi, contextHolder] = message.useMessage() const [messageApi, contextHolder] = message.useMessage()
const navigate = useNavigate()
const [newGCodeFileOpen, setNewGCodeFileOpen] = useState(false) const [newGCodeFileOpen, setNewGCodeFileOpen] = useState(false)
const [showDeleted, setShowDeleted] = useState(false)
const tableRef = useRef() const tableRef = useRef()
const [viewMode, setViewMode] = useViewMode('GCodeFiles') const [viewMode, setViewMode] = useViewMode('gcodeFile')
const getFilterDropdown = ({ const [columnVisibility, setColumnVisibility] =
setSelectedKeys, useColumnVisibility('gcodeFile')
selectedKeys,
confirm,
clearFilters,
propertyName
}) => {
return (
<div style={{ padding: 8 }}>
<Space.Compact>
<Input
placeholder={'Search ' + propertyName}
value={selectedKeys[0]}
onChange={(e) =>
setSelectedKeys(e.target.value ? [e.target.value] : [])
}
onPressEnter={() => confirm()}
style={{ width: 200, display: 'block' }}
/>
<Button
onClick={() => {
clearFilters()
confirm()
}}
icon={<XMarkIcon />}
/>
<Button
type='primary'
onClick={() => confirm()}
icon={<CheckIcon />}
/>
</Space.Compact>
</div>
)
}
// Column definitions
const columns = [
{
title: <GCodeFileIcon />,
key: 'icon',
width: 40,
fixed: 'left',
render: () => <GCodeFileIcon />
},
{
title: 'Name',
dataIndex: 'name',
key: 'name',
width: 200,
fixed: 'left',
render: (text) => <Text ellipsis>{text}</Text>,
filterDropdown: ({
setSelectedKeys,
selectedKeys,
confirm,
clearFilters
}) =>
getFilterDropdown({
setSelectedKeys,
selectedKeys,
confirm,
clearFilters,
propertyName: 'name'
}),
onFilter: (value, record) =>
record.name.toLowerCase().includes(value.toLowerCase())
},
{
title: 'ID',
dataIndex: '_id',
key: 'id',
width: 180,
render: (text) => (
<IdDisplay id={text} type={'gcodefile'} longId={false} />
)
},
{
title: 'Filament',
key: 'filament',
width: 200,
render: (record) => {
return (
<Badge
color={record?.filament?.color}
text={record?.filament?.name}
/>
)
},
filterDropdown: ({
setSelectedKeys,
selectedKeys,
confirm,
clearFilters
}) =>
getFilterDropdown({
setSelectedKeys,
selectedKeys,
confirm,
clearFilters,
propertyName: 'filament'
}),
onFilter: (value, record) =>
record.filament.name.toLowerCase().includes(value.toLowerCase())
},
{
title: 'Cost',
dataIndex: 'cost',
key: 'cost',
width: 120,
render: (cost) => {
return '£' + cost?.toFixed(2)
},
sorter: true
},
{
title: 'Print Time',
key: 'estimatedPrintingTimeNormalMode',
width: 140,
render: (record) => {
return `${record?.gcodeFileInfo?.estimatedPrintingTimeNormalMode}`
},
sorter: true
},
{
title: 'Created At',
dataIndex: 'createdAt',
key: 'createdAt',
width: 180,
render: (createdAt) => {
if (createdAt) {
return <TimeDisplay dateTime={createdAt} />
} else {
return 'n/a'
}
},
sorter: true
},
{
title: 'Updated At',
dataIndex: 'updatedAt',
key: 'updatedAt',
width: 180,
render: (createdAt) => {
if (createdAt) {
return <TimeDisplay dateTime={createdAt} />
} else {
return 'n/a'
}
},
sorter: true
},
{
title: 'Actions',
key: 'actions',
fixed: 'right',
width: 150,
render: (record) => {
return (
<Space gap='small'>
<Button
icon={<InfoCircleIcon />}
onClick={() =>
navigate(
`/dashboard/production/gcodefiles/info?gcodeFileId=${record._id}`
)
}
/>
<Dropdown menu={getGCodeFileActionItems(record._id)}>
<Button>Actions</Button>
</Dropdown>
</Space>
)
}
}
]
const [columnVisibility, updateColumnVisibility] = useColumnVisibility(
'GCodeFiles',
columns
)
const { authenticated } = useContext(AuthContext) const { authenticated } = useContext(AuthContext)
const getGCodeFileActionItems = (id) => {
return {
items: [
{
label: 'Info',
key: 'info',
icon: <InfoCircleIcon />
},
{
label: 'Download',
key: 'download',
icon: <DownloadOutlined />
}
],
onClick: ({ key }) => {
if (key === 'info') {
navigate(`/dashboard/production/gcodefiles/info?gcodeFileId=${id}`)
} else if (key === 'download') {
handleDownloadGCode(
id,
tableRef.current?.getData().find((file) => file._id === id)?.name +
'.gcode'
)
}
}
}
}
const handleDownloadGCode = async (id, fileName) => {
if (!authenticated) {
return
}
try {
const response = await axios.get(
`${config.backendUrl}/gcodefiles/${id}/content`,
{
headers: {
Accept: 'application/json'
},
withCredentials: true
}
)
const fileURL = window.URL.createObjectURL(new Blob([response.data]))
const fileLink = document.createElement('a')
fileLink.href = fileURL
fileLink.setAttribute('download', fileName)
document.body.appendChild(fileLink)
fileLink.click()
fileLink.parentNode.removeChild(fileLink)
} catch (error) {
if (error.response) {
messageApi.error(
'Error updating printer details:',
error.response.status
)
} else {
messageApi.error(
'An unexpected error occurred. Please try again later.'
)
}
}
}
const actionItems = { const actionItems = {
items: [ items: [
{ {
@ -318,43 +48,6 @@ const GCodeFiles = () => {
} }
} }
const getViewDropdownItems = () => {
const columnItems = columns
.filter((col) => col.key && col.title !== '')
.map((col) => (
<Checkbox
checked={columnVisibility[col.key]}
key={col.key}
onChange={(e) => {
updateColumnVisibility(col.key, e.target.checked)
}}
>
{col.title}
</Checkbox>
))
return (
<Flex vertical>
<Flex vertical gap='middle' style={{ margin: '4px 8px' }}>
{columnItems}
</Flex>
<Divider style={{ margin: '8px 0' }} />
<Flex vertical gap='middle' style={{ margin: '4px 8px' }}>
<Checkbox
checked={showDeleted}
onChange={(e) => setShowDeleted(e.target.checked)}
>
Show Deleted
</Checkbox>
</Flex>
</Flex>
)
}
const visibleColumns = columns.filter(
(col) => !col.key || columnVisibility[col.key]
)
return ( return (
<> <>
<Flex vertical={'true'} gap='large'> <Flex vertical={'true'} gap='large'>
@ -364,13 +57,12 @@ const GCodeFiles = () => {
<Dropdown menu={actionItems}> <Dropdown menu={actionItems}>
<Button>Actions</Button> <Button>Actions</Button>
</Dropdown> </Dropdown>
<Popover <ColumnViewButton
content={getViewDropdownItems()} type='gcodeFile'
placement='bottomLeft' loading={false}
arrow={false} collapseState={columnVisibility}
> updateCollapseState={setColumnVisibility}
<Button>View</Button> />
</Popover>
</Space> </Space>
<Space> <Space>
<Button <Button
@ -383,10 +75,10 @@ const GCodeFiles = () => {
</Flex> </Flex>
<ObjectTable <ObjectTable
ref={tableRef} ref={tableRef}
columns={visibleColumns} type={'gcodeFile'}
url={`${config.backendUrl}/gcodefiles`}
authenticated={authenticated} authenticated={authenticated}
cards={viewMode === 'cards'} cards={viewMode === 'cards'}
visibleColumns={columnVisibility}
/> />
</Flex> </Flex>
<Modal <Modal

View File

@ -1,4 +1,4 @@
import React from 'react' import React, { useContext } from 'react'
import { useLocation } from 'react-router-dom' import { useLocation } from 'react-router-dom'
import { Space, Button, Flex, Dropdown, Card, Typography } from 'antd' import { Space, Button, Flex, Dropdown, Card, Typography } from 'antd'
import { LoadingOutlined } from '@ant-design/icons' import { LoadingOutlined } from '@ant-design/icons'
@ -12,10 +12,12 @@ import ViewButton from '../../common/ViewButton'
import EditObjectForm from '../../common/EditObjectForm' import EditObjectForm from '../../common/EditObjectForm'
import EditButtons from '../../common/EditButtons' import EditButtons from '../../common/EditButtons'
import LockIndicator from '../../Management/Filaments/LockIndicator' import LockIndicator from '../../Management/Filaments/LockIndicator'
import ActionHandler from '../../common/ActionHandler'
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 GCodeFileIcon from '../../../Icons/GCodeFileIcon.jsx' import GCodeFileIcon from '../../../Icons/GCodeFileIcon.jsx'
import { ApiServerContext } from '../../context/ApiServerContext'
import { import {
getModelProperties, getModelProperties,
getPropertyValue getPropertyValue
@ -26,6 +28,8 @@ const { Text } = Typography
const GCodeFileInfo = () => { const GCodeFileInfo = () => {
const location = useLocation() const location = useLocation()
const gcodeFileId = new URLSearchParams(location.search).get('gcodeFileId') const gcodeFileId = new URLSearchParams(location.search).get('gcodeFileId')
const { handleDownloadContent } = useContext(ApiServerContext)
const [collapseState, updateCollapseState] = useCollapseState( const [collapseState, updateCollapseState] = useCollapseState(
'GCodeFileInfo', 'GCodeFileInfo',
{ {
@ -36,154 +40,183 @@ const GCodeFileInfo = () => {
} }
) )
// Define actions that can be triggered via URL
const actions = {
download: () => {
if (gcodeFileId) {
handleDownloadContent(
gcodeFileId,
'gcodeFile',
`gcodeFile-${gcodeFileId}.gcode`
)
}
}
}
return ( return (
<EditObjectForm <>
id={gcodeFileId} <ActionHandler actions={actions} />
type='gcodefile' <EditObjectForm
style={{ height: 'calc(var(--unit-100vh) - 155px)', minHeight: 0 }} id={gcodeFileId}
> type='gcodefile'
{({ style={{ height: 'calc(var(--unit-100vh) - 155px)', minHeight: 0 }}
loading, >
isEditing, {({
startEditing, loading,
cancelEditing, isEditing,
handleUpdate, startEditing,
formValid, cancelEditing,
objectData, handleUpdate,
editLoading, formValid,
lock, objectData,
fetchObject editLoading,
}) => ( lock,
<Flex fetchObject
gap='large' }) => (
vertical='true' <Flex
style={{ height: '100%', minHeight: 0 }} gap='large'
> vertical='true'
<Flex justify={'space-between'}> style={{ height: '100%', minHeight: 0 }}
<Space size='middle'> >
<Space size='small'> <Flex justify={'space-between'}>
<Dropdown <Space size='middle'>
menu={{ <Space size='small'>
items: [ <Dropdown
{ menu={{
label: 'Reload GCode File', items: [
key: 'reload', {
icon: <ReloadIcon /> label: 'Reload GCode File',
key: 'reload',
icon: <ReloadIcon />
},
{
label: 'Download GCode File',
key: 'download',
icon: <GCodeFileIcon />
}
],
onClick: ({ key }) => {
if (key === 'reload') {
fetchObject()
} else if (key === 'download' && gcodeFileId) {
handleDownloadContent(
gcodeFileId,
'gcodefile',
`gcodefile-${gcodeFileId}.gcode`
)
}
} }
], }}
onClick: ({ key }) => { >
if (key === 'reload') { <Button disabled={loading}>Actions</Button>
fetchObject() </Dropdown>
} <ViewButton
} loading={loading}
}} sections={[
> { key: 'info', label: 'GCode File Information' },
<Button disabled={loading}>Actions</Button> { key: 'preview', label: 'GCode File Preview' },
</Dropdown> { key: 'notes', label: 'Notes' },
<ViewButton { key: 'auditLogs', label: 'Audit Logs' }
loading={loading} ]}
sections={[ collapseState={collapseState}
{ key: 'info', label: 'GCode File Information' }, updateCollapseState={updateCollapseState}
{ key: 'preview', label: 'GCode File Preview' }, />
{ key: 'notes', label: 'Notes' }, </Space>
{ key: 'auditLogs', label: 'Audit Logs' } <LockIndicator lock={lock} />
]} </Space>
collapseState={collapseState} <Space>
updateCollapseState={updateCollapseState} <EditButtons
isEditing={isEditing}
handleUpdate={handleUpdate}
cancelEditing={cancelEditing}
startEditing={startEditing}
editLoading={editLoading}
formValid={formValid}
disabled={lock?.locked || loading}
loading={editLoading}
/> />
</Space> </Space>
<LockIndicator lock={lock} />
</Space>
<Space>
<EditButtons
isEditing={isEditing}
handleUpdate={handleUpdate}
cancelEditing={cancelEditing}
startEditing={startEditing}
editLoading={editLoading}
formValid={formValid}
disabled={lock?.locked || loading}
loading={editLoading}
/>
</Space>
</Flex>
<div style={{ height: '100%', overflow: 'auto' }}>
<Flex vertical gap={'large'}>
<InfoCollapse
title='GCode File Information'
icon={<InfoCircleIcon />}
active={collapseState.info}
onToggle={(expanded) => updateCollapseState('info', expanded)}
key='info'
>
<ObjectInfo
loading={loading}
indicator={<LoadingOutlined />}
isEditing={isEditing}
items={getModelProperties('gcodeFile').map((prop) => ({
...prop,
value: getPropertyValue(objectData, prop.name)
}))}
objectData={objectData}
type='gcodefile'
/>
</InfoCollapse>
<InfoCollapse
title='GCode File Preview'
icon={<GCodeFileIcon />}
active={collapseState.preview}
onToggle={(expanded) =>
updateCollapseState('preview', expanded)
}
key='preview'
>
<Card>
{objectData?.gcodeFileInfo?.thumbnail ? (
<img
src={`data:image/png;base64,${objectData.gcodeFileInfo.thumbnail.data}`}
alt='GCodeFile'
style={{ maxWidth: '100%' }}
/>
) : (
<Text>n/a</Text>
)}
</Card>
</InfoCollapse>
<InfoCollapse
title='Notes'
icon={<NoteIcon />}
active={collapseState.notes}
onToggle={(expanded) => updateCollapseState('notes', expanded)}
key='notes'
>
<Card>
<NotesPanel _id={gcodeFileId} />
</Card>
</InfoCollapse>
<InfoCollapse
title='Audit Logs'
icon={<AuditLogIcon />}
active={collapseState.auditLogs}
onToggle={(expanded) =>
updateCollapseState('auditLogs', expanded)
}
key='auditLogs'
>
<AuditLogTable
items={objectData?.auditLogs || []}
loading={loading}
showTargetColumn={false}
/>
</InfoCollapse>
</Flex> </Flex>
</div>
</Flex> <div style={{ height: '100%', overflow: 'auto' }}>
)} <Flex vertical gap={'large'}>
</EditObjectForm> <InfoCollapse
title='GCode File Information'
icon={<InfoCircleIcon />}
active={collapseState.info}
onToggle={(expanded) => updateCollapseState('info', expanded)}
key='info'
>
<ObjectInfo
loading={loading}
indicator={<LoadingOutlined />}
isEditing={isEditing}
items={getModelProperties('gcodeFile').map((prop) => ({
...prop,
value: getPropertyValue(objectData, prop.name)
}))}
objectData={objectData}
type='gcodefile'
/>
</InfoCollapse>
<InfoCollapse
title='GCode File Preview'
icon={<GCodeFileIcon />}
active={collapseState.preview}
onToggle={(expanded) =>
updateCollapseState('preview', expanded)
}
key='preview'
>
<Card>
{objectData?.gcodeFileInfo?.thumbnail ? (
<img
src={`data:image/png;base64,${objectData.gcodeFileInfo.thumbnail.data}`}
alt='GCodeFile'
style={{ maxWidth: '100%' }}
/>
) : (
<Text>n/a</Text>
)}
</Card>
</InfoCollapse>
<InfoCollapse
title='Notes'
icon={<NoteIcon />}
active={collapseState.notes}
onToggle={(expanded) =>
updateCollapseState('notes', expanded)
}
key='notes'
>
<Card>
<NotesPanel _id={gcodeFileId} />
</Card>
</InfoCollapse>
<InfoCollapse
title='Audit Logs'
icon={<AuditLogIcon />}
active={collapseState.auditLogs}
onToggle={(expanded) =>
updateCollapseState('auditLogs', expanded)
}
key='auditLogs'
>
<AuditLogTable
items={objectData?.auditLogs || []}
loading={loading}
showTargetColumn={false}
/>
</InfoCollapse>
</Flex>
</div>
</Flex>
)}
</EditObjectForm>
</>
) )
} }

View File

@ -41,8 +41,6 @@ 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'
import config from '../../../config.js'
const { Text } = Typography const { Text } = Typography
const Jobs = () => { const Jobs = () => {
@ -362,10 +360,6 @@ const Jobs = () => {
) )
} }
const visibleColumns = columns.filter(
(col) => !col.key || columnVisibility[col.key]
)
return ( return (
<> <>
{notificationContextHolder} {notificationContextHolder}
@ -396,8 +390,7 @@ const Jobs = () => {
<ObjectTable <ObjectTable
ref={tableRef} ref={tableRef}
columns={visibleColumns} type={'job'}
url={`${config.backendUrl}/jobs`}
authenticated={authenticated} authenticated={authenticated}
cards={viewMode === 'cards'} cards={viewMode === 'cards'}
/> />

View File

@ -12,6 +12,7 @@ import EditObjectForm from '../../common/EditObjectForm'
import EditButtons from '../../common/EditButtons' import EditButtons from '../../common/EditButtons'
import LockIndicator from '../../Management/Filaments/LockIndicator' import LockIndicator from '../../Management/Filaments/LockIndicator'
import SubJobsTree from '../../common/SubJobsTree' import SubJobsTree from '../../common/SubJobsTree'
import ActionHandler from '../../common/ActionHandler'
import InfoCircleIcon from '../../../Icons/InfoCircleIcon' import InfoCircleIcon from '../../../Icons/InfoCircleIcon'
import JobIcon from '../../../Icons/JobIcon' import JobIcon from '../../../Icons/JobIcon'
import AuditLogIcon from '../../../Icons/AuditLogIcon' import AuditLogIcon from '../../../Icons/AuditLogIcon'
@ -32,143 +33,153 @@ const JobInfo = () => {
auditLogs: true auditLogs: true
}) })
// Define actions that can be triggered via URL
const actions = {
// Add job-specific actions here as needed
}
return ( return (
<EditObjectForm <>
id={jobId} <ActionHandler actions={actions} />
type='job' <EditObjectForm
style={{ height: 'calc(var(--unit-100vh) - 155px)', minHeight: 0 }} id={jobId}
> type='job'
{({ style={{ height: 'calc(var(--unit-100vh) - 155px)', minHeight: 0 }}
loading, >
isEditing, {({
startEditing, loading,
cancelEditing, isEditing,
handleUpdate, startEditing,
formValid, cancelEditing,
objectData, handleUpdate,
editLoading, formValid,
lock, objectData,
fetchObject editLoading,
}) => ( lock,
<Flex fetchObject
gap='large' }) => (
vertical='true' <Flex
style={{ height: '100%', minHeight: 0 }} gap='large'
> vertical='true'
<Flex justify={'space-between'}> style={{ height: '100%', minHeight: 0 }}
<Space size='middle'> >
<Space size='small'> <Flex justify={'space-between'}>
<Dropdown <Space size='middle'>
menu={{ <Space size='small'>
items: [ <Dropdown
{ menu={{
label: 'Reload Job', items: [
key: 'reload', {
icon: <GCodeFileIcon /> label: 'Reload Job',
key: 'reload',
icon: <GCodeFileIcon />
}
],
onClick: ({ key }) => {
if (key === 'reload') {
fetchObject()
}
} }
], }}
onClick: ({ key }) => { >
if (key === 'reload') { <Button disabled={loading}>Actions</Button>
fetchObject() </Dropdown>
} <ViewButton
} loading={loading}
}} sections={[
> { key: 'info', label: 'Job Information' },
<Button disabled={loading}>Actions</Button> { key: 'subJobs', label: 'Sub Jobs' },
</Dropdown> { key: 'notes', label: 'Notes' },
<ViewButton { key: 'auditLogs', label: 'Audit Logs' }
loading={loading} ]}
sections={[ collapseState={collapseState}
{ key: 'info', label: 'Job Information' }, updateCollapseState={updateCollapseState}
{ key: 'subJobs', label: 'Sub Jobs' }, />
{ key: 'notes', label: 'Notes' }, </Space>
{ key: 'auditLogs', label: 'Audit Logs' } <LockIndicator lock={lock} />
]} </Space>
collapseState={collapseState} <Space>
updateCollapseState={updateCollapseState} <EditButtons
isEditing={isEditing}
handleUpdate={handleUpdate}
cancelEditing={cancelEditing}
startEditing={startEditing}
editLoading={editLoading}
formValid={formValid}
disabled={lock?.locked || loading || true}
loading={editLoading}
/> />
</Space> </Space>
<LockIndicator lock={lock} />
</Space>
<Space>
<EditButtons
isEditing={isEditing}
handleUpdate={handleUpdate}
cancelEditing={cancelEditing}
startEditing={startEditing}
editLoading={editLoading}
formValid={formValid}
disabled={lock?.locked || loading || true}
loading={editLoading}
/>
</Space>
</Flex>
<div style={{ height: '100%', overflow: 'auto' }}>
<Flex vertical gap={'large'}>
<InfoCollapse
title='Job Information'
icon={<InfoCircleIcon />}
active={collapseState.info}
onToggle={(expanded) => updateCollapseState('info', expanded)}
key='info'
>
<ObjectInfo
loading={loading}
indicator={<LoadingOutlined />}
isEditing={isEditing}
type='job'
items={getModelProperties('job').map((prop) => ({
...prop,
value: getPropertyValue(objectData, prop.name)
}))}
/>
</InfoCollapse>
<InfoCollapse
title='Sub Jobs'
icon={<JobIcon />}
active={collapseState.subJobs}
onToggle={(expanded) =>
updateCollapseState('subJobs', expanded)
}
key='subJobs'
>
<SubJobsTree jobData={objectData} loading={loading} />
</InfoCollapse>
<InfoCollapse
title='Notes'
icon={<NoteIcon />}
active={collapseState.notes}
onToggle={(expanded) => updateCollapseState('notes', expanded)}
key='notes'
>
<Card>
<NotesPanel _id={jobId} />
</Card>
</InfoCollapse>
<InfoCollapse
title='Audit Logs'
icon={<AuditLogIcon />}
active={collapseState.auditLogs}
onToggle={(expanded) =>
updateCollapseState('auditLogs', expanded)
}
key='auditLogs'
>
<AuditLogTable
items={objectData?.auditLogs || []}
loading={loading}
showTargetColumn={false}
/>
</InfoCollapse>
</Flex> </Flex>
</div>
</Flex> <div style={{ height: '100%', overflow: 'auto' }}>
)} <Flex vertical gap={'large'}>
</EditObjectForm> <InfoCollapse
title='Job Information'
icon={<InfoCircleIcon />}
active={collapseState.info}
onToggle={(expanded) => updateCollapseState('info', expanded)}
key='info'
>
<ObjectInfo
loading={loading}
indicator={<LoadingOutlined />}
isEditing={isEditing}
type='job'
items={getModelProperties('job').map((prop) => ({
...prop,
value: getPropertyValue(objectData, prop.name)
}))}
/>
</InfoCollapse>
<InfoCollapse
title='Sub Jobs'
icon={<JobIcon />}
active={collapseState.subJobs}
onToggle={(expanded) =>
updateCollapseState('subJobs', expanded)
}
key='subJobs'
>
<SubJobsTree jobData={objectData} loading={loading} />
</InfoCollapse>
<InfoCollapse
title='Notes'
icon={<NoteIcon />}
active={collapseState.notes}
onToggle={(expanded) =>
updateCollapseState('notes', expanded)
}
key='notes'
>
<Card>
<NotesPanel _id={jobId} />
</Card>
</InfoCollapse>
<InfoCollapse
title='Audit Logs'
icon={<AuditLogIcon />}
active={collapseState.auditLogs}
onToggle={(expanded) =>
updateCollapseState('auditLogs', expanded)
}
key='auditLogs'
>
<AuditLogTable
items={objectData?.auditLogs || []}
loading={loading}
showTargetColumn={false}
/>
</InfoCollapse>
</Flex>
</div>
</Flex>
)}
</EditObjectForm>
</>
) )
} }

View File

@ -1,263 +1,31 @@
// src/Printers.js // src/Printers.js
import React, { useState, useContext, useRef } from 'react' import React, { useState, useContext, useRef } from 'react'
import { useNavigate } from 'react-router-dom' import { Button, message, Dropdown, Space, Flex, Modal } from 'antd'
import {
Button,
message,
Dropdown,
Space,
Flex,
Input,
Tag,
Modal,
Popover,
Checkbox
} from 'antd'
import { AuthContext } from '../context/AuthContext' import { AuthContext } from '../context/AuthContext'
import PrinterState from '../common/PrinterState'
import NewPrinter from './Printers/NewPrinter' import NewPrinter from './Printers/NewPrinter'
import IdDisplay from '../common/IdDisplay'
import PrinterIcon from '../../Icons/PrinterIcon'
import InfoCircleIcon from '../../Icons/InfoCircleIcon'
import ControlIcon from '../../Icons/ControlIcon'
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 CheckIcon from '../../Icons/CheckIcon'
import ObjectTable from '../common/ObjectTable' import ObjectTable from '../common/ObjectTable'
import ColumnViewButton from '../common/ColumnViewButton'
import config from '../../../config'
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 useColumnVisibility from '../hooks/useColumnVisibility'
const Printers = () => { const Printers = () => {
const [messageApi, contextHolder] = message.useMessage() const [messageApi, contextHolder] = message.useMessage()
const { authenticated } = useContext(AuthContext) const { authenticated } = useContext(AuthContext)
const [newPrinterOpen, setNewPrinterOpen] = useState(false) const [newPrinterOpen, setNewPrinterOpen] = useState(false)
const navigate = useNavigate()
const tableRef = useRef() const tableRef = useRef()
// View mode state (cards/list), persisted in sessionStorage via custom hook // View mode state (cards/list), persisted in sessionStorage via custom hook
const [viewMode, setViewMode] = useViewMode('Printers') const [viewMode, setViewMode] = useViewMode('Printers')
// Column definitions // Column visibility state, persisted in sessionStorage via custom hook
const columns = [ const [columnVisibility, setColumnVisibility] = useColumnVisibility('printer')
{
title: <PrinterIcon />,
key: 'icon',
width: 40,
fixed: 'left',
render: () => <PrinterIcon />
},
{
title: 'Name',
dataIndex: 'name',
key: 'name',
width: 200,
fixed: 'left',
filterDropdown: ({
setSelectedKeys,
selectedKeys,
confirm,
clearFilters
}) =>
getFilterDropdown({
setSelectedKeys,
selectedKeys,
confirm,
clearFilters,
propertyName: 'name'
}),
onFilter: (value, record) =>
record.name.toLowerCase().includes(value.toLowerCase())
},
{
title: 'ID',
dataIndex: '_id',
key: 'id',
width: 180,
render: (text) => <IdDisplay id={text} type='printer' longId={false} />
},
{
title: 'State',
dataIndex: 'state',
key: 'state',
width: 240,
render: (state) => {
return (
<PrinterState state={state} showName={false} showControls={false} />
)
}
},
{
title: 'Tags',
dataIndex: 'tags',
key: 'tags',
width: 170,
render: (tags) => {
if (!tags || !Array.isArray(tags)) return 'n/a'
if (tags.length == 0) return 'n/a'
return (
<Space size={[0, 8]} wrap>
{tags.map((tag, index) => (
<Tag key={index} color='blue'>
{tag}
</Tag>
))}
</Space>
)
},
filterDropdown: ({
setSelectedKeys,
selectedKeys,
confirm,
clearFilters
}) =>
getFilterDropdown({
setSelectedKeys,
selectedKeys,
confirm,
clearFilters,
propertyName: 'tags'
}),
onFilter: (value, record) =>
record.tags &&
record.tags.some((tag) =>
tag.toLowerCase().includes(value.toLowerCase())
)
},
{
title: 'Actions',
key: 'actions',
fixed: 'right',
width: 150,
render: (record) => {
return (
<Space gap='small'>
<Button
icon={<ControlIcon />}
onClick={() =>
navigate(
`/dashboard/production/printers/control?printerId=${record._id}`
)
}
/>
<Dropdown menu={getPrinterActionItems(record._id)}>
<Button>Actions</Button>
</Dropdown>
</Space>
)
}
}
]
const [columnVisibility, setColumnVisibility] = useState(
columns.reduce((acc, col) => {
if (col.key) {
acc[col.key] = true
}
return acc
}, {})
)
const getFilterDropdown = ({
setSelectedKeys,
selectedKeys,
confirm,
clearFilters,
propertyName
}) => {
return (
<div style={{ padding: 8 }}>
<Space.Compact>
<Input
placeholder={'Search ' + propertyName}
value={selectedKeys[0]}
onChange={(e) =>
setSelectedKeys(e.target.value ? [e.target.value] : [])
}
onPressEnter={() => confirm()}
style={{ width: 200, display: 'block' }}
/>
<Button
onClick={() => {
clearFilters()
confirm()
}}
icon={<XMarkIcon />}
/>
<Button
type='primary'
onClick={() => confirm()}
icon={<CheckIcon />}
/>
</Space.Compact>
</div>
)
}
const getPrinterActionItems = (printerId) => {
return {
items: [
{
label: 'Control',
key: 'control',
icon: <ControlIcon />
},
{
type: 'divider'
},
{
label: 'Info',
key: 'info',
icon: <InfoCircleIcon />
}
],
onClick: ({ key }) => {
if (key === 'info') {
navigate(`/dashboard/production/printers/info?printerId=${printerId}`)
} else if (key === 'control') {
navigate(
`/dashboard/production/printers/control?printerId=${printerId}`
)
}
}
}
}
const getViewDropdownItems = () => {
const columnItems = columns
.filter((col) => col.key && col.title !== '')
.map((col) => (
<Checkbox
checked={columnVisibility[col.key]}
key={col.key}
onChange={(e) => {
setColumnVisibility((prev) => ({
...prev,
[col.key]: e.target.checked
}))
}}
>
{col.title}
</Checkbox>
))
return (
<Flex vertical>
<Flex vertical gap='middle' style={{ margin: '4px 8px' }}>
{columnItems}
</Flex>
</Flex>
)
}
const visibleColumns = columns.filter(
(col) => !col.key || columnVisibility[col.key]
)
const actionItems = { const actionItems = {
items: [ items: [
@ -291,13 +59,12 @@ const Printers = () => {
<Dropdown menu={actionItems}> <Dropdown menu={actionItems}>
<Button>Actions</Button> <Button>Actions</Button>
</Dropdown> </Dropdown>
<Popover <ColumnViewButton
content={getViewDropdownItems()} type='printer'
placement='bottomLeft' loading={false}
arrow={false} collapseState={columnVisibility}
> updateCollapseState={setColumnVisibility}
<Button>View</Button> />
</Popover>
</Space> </Space>
<Space> <Space>
<Button <Button
@ -311,10 +78,10 @@ const Printers = () => {
<ObjectTable <ObjectTable
ref={tableRef} ref={tableRef}
columns={visibleColumns} type={'printer'}
url={`${config.backendUrl}/printers`}
authenticated={authenticated} authenticated={authenticated}
cards={viewMode === 'cards'} cards={viewMode === 'cards'}
visibleColumns={columnVisibility}
/> />
<Modal <Modal

View File

@ -0,0 +1,57 @@
import { useEffect } from 'react'
import { useLocation, useNavigate } from 'react-router-dom'
import PropTypes from 'prop-types'
const ActionHandler = ({
actions = {},
actionParam = 'action',
clearAfterExecute = true,
onAction
}) => {
const location = useLocation()
const navigate = useNavigate()
const action = new URLSearchParams(location.search).get(actionParam)
// Execute action and clear from URL
useEffect(() => {
if (action && actions[action]) {
// Execute the action
const result = actions[action]()
// Call optional callback
if (onAction) {
onAction(action, result)
}
// Clear action from URL if requested
if (clearAfterExecute) {
const searchParams = new URLSearchParams(location.search)
searchParams.delete(actionParam)
const newSearch = searchParams.toString()
const newPath = location.pathname + (newSearch ? `?${newSearch}` : '')
navigate(newPath, { replace: true })
}
}
}, [
action,
actions,
actionParam,
clearAfterExecute,
onAction,
location.pathname,
location.search,
navigate
])
// Return null as this is a utility component
return null
}
ActionHandler.propTypes = {
actions: PropTypes.objectOf(PropTypes.func),
actionParam: PropTypes.string,
clearAfterExecute: PropTypes.bool,
onAction: PropTypes.func
}
export default ActionHandler

View File

@ -0,0 +1,50 @@
import React from 'react'
import PropTypes from 'prop-types'
import ViewButton from './ViewButton'
import { getModelByName } from '../../../database/ObjectModels'
const ColumnViewButton = ({
type,
loading = false,
collapseState = {},
updateCollapseState = () => {},
...buttonProps
}) => {
// Get the model by name
const model = getModelByName(type)
// Get the properties that correspond to the model's columns
const columnProperties =
model.columns
?.map((columnName) => {
const property = model.properties.find(
(prop) => prop.name === columnName
)
if (property) {
return {
key: property.name,
label: property.label
}
}
})
.filter(Boolean) || []
return (
<ViewButton
loading={loading}
properties={columnProperties}
collapseState={collapseState}
updateCollapseState={updateCollapseState}
{...buttonProps}
/>
)
}
ColumnViewButton.propTypes = {
type: PropTypes.string.isRequired,
loading: PropTypes.bool,
collapseState: PropTypes.object,
updateCollapseState: PropTypes.func
}
export default ColumnViewButton

View File

@ -1,9 +1,9 @@
import React from 'react' import React from 'react'
import PropTypes from 'prop-types' import PropTypes from 'prop-types'
import { Typography, Flex, Button, Tooltip } from 'antd' import { Typography, Flex, Button, Tooltip } from 'antd'
import NewMailIcon from './NewMailIcon' import NewMailIcon from '../../Icons/NewMailIcon'
// import CopyIcon from './CopyIcon' // import CopyIcon from './CopyIcon'
import CopyButton from '../Dashboard/common/CopyButton' import CopyButton from './CopyButton'
const { Text, Link } = Typography const { Text, Link } = Typography
@ -19,7 +19,9 @@ const EmailDisplay = ({ email, showCopy = true, showLink = false }) => {
</Link> </Link>
) : ( ) : (
<> <>
<Text style={{ marginRight: 8 }}>{email}</Text> <Text style={{ marginRight: 8 }} ellipsis>
{email}
</Text>
<Tooltip title='Email' arrow={false}> <Tooltip title='Email' arrow={false}>
<Button <Button
icon={<NewMailIcon style={{ fontSize: '14px' }} />} icon={<NewMailIcon style={{ fontSize: '14px' }} />}

View File

@ -23,10 +23,20 @@ const IdDisplay = ({
const model = getModelByName(type) const model = getModelByName(type)
const prefix = model.prefix const prefix = model.prefix
const hyperlink = model.url(id)
const IconComponent = model.icon const IconComponent = model.icon
const icon = <IconComponent style={{ paddingTop: '4px' }} /> const icon = <IconComponent style={{ paddingTop: '4px' }} />
var hyperlink = null
const defaultModelActions = model.actions.filter(
(action) => action.default == true
)
if (defaultModelActions.length >= 1) {
hyperlink = defaultModelActions[0].url(id)
}
if (!id) { if (!id) {
return <Text type='secondary'>n/a</Text> return <Text type='secondary'>n/a</Text>
} }
@ -41,79 +51,75 @@ const IdDisplay = ({
return ( return (
<Flex align={'center'} className='iddisplay'> <Flex align={'center'} className='iddisplay'>
{showHyperlink && {(() => {
(showSpotlight ? ( const content = (
<Popover <Space size={4}>
content={ {icon}
id && type ? ( {displayId}
<SpotlightTooltip query={prefix + ':' + id} type={type} /> </Space>
) : null )
}
trigger={['hover', 'click']} const textElement = (
placement='topLeft' <Text
arrow={false} code
style={{ padding: 0 }} ellipsis
style={showHyperlink ? { marginRight: 6 } : undefined}
> >
{content}
</Text>
)
// If hyperlink is enabled
if (showHyperlink && hyperlink != null) {
const linkElement = (
<Link <Link
onClick={() => { onClick={() => navigate(hyperlink)}
if (showHyperlink) {
navigate(hyperlink)
}
}}
style={{ marginRight: 6 }} style={{ marginRight: 6 }}
> >
<Text code ellipsis> {textElement}
<Space size={4}>
{icon}
{displayId}
</Space>
</Text>
</Link> </Link>
</Popover> )
) : (
<Link if (showSpotlight) {
onClick={() => { return (
if (showHyperlink) { <Popover
navigate(hyperlink) content={
} id && type ? (
}} <SpotlightTooltip query={prefix + ':' + id} type={type} />
> ) : null
<Text code ellipsis> }
<Space size={4}> trigger={['hover', 'click']}
{icon} placement='topLeft'
{displayId} arrow={false}
</Space> style={{ padding: 0 }}
</Text> >
</Link> {linkElement}
))} </Popover>
)
}
return linkElement
}
// If hyperlink is disabled
if (showSpotlight) {
return (
<Popover
content={
id && type ? (
<SpotlightTooltip query={prefix + ':' + id} type={type} />
) : null
}
trigger={['hover', 'click']}
placement='topLeft'
arrow={false}
>
{textElement}
</Popover>
)
}
return textElement
})()}
{!showHyperlink &&
(showSpotlight ? (
<Popover
content={
id && type ? (
<SpotlightTooltip query={prefix + ':' + id} type={type} />
) : null
}
trigger={['hover', 'click']}
placement='topLeft'
arrow={false}
>
<Text code ellipsis style={{ marginRight: 6 }}>
<Space size={4}>
{icon}
{displayId}
</Space>
</Text>
</Popover>
) : (
<Text code ellipsis>
<Space size={4}>
{icon}
{displayId}
</Space>
</Text>
))}
{showCopy && ( {showCopy && (
<CopyButton <CopyButton
text={copyId} text={copyId}

View File

@ -18,8 +18,8 @@ import dayjs from 'dayjs'
import PrinterSelect from './PrinterSelect' import PrinterSelect from './PrinterSelect'
import GCodeFileSelect from './GCodeFileSelect' import GCodeFileSelect from './GCodeFileSelect'
import PartSelect from './PartSelect' import PartSelect from './PartSelect'
import EmailDisplay from '../../Icons/EmailDisplay' import EmailDisplay from './EmailDisplay'
import UrlDisplay from '../../Icons/UrlDisplay' import UrlDisplay from './UrlDisplay'
import CountryDisplay from './CountryDisplay' import CountryDisplay from './CountryDisplay'
import CountrySelect from './CountrySelect' import CountrySelect from './CountrySelect'
import TagsDisplay from './TagsDisplay' import TagsDisplay from './TagsDisplay'
@ -138,7 +138,7 @@ const ObjectProperty = ({
} }
case 'text': case 'text':
if (value != null && value != '') { if (value != null && value != '') {
return <Text>{value}</Text> return <Text ellipsis>{value}</Text>
} else { } else {
return <Text type='secondary'>n/a</Text> return <Text type='secondary'>n/a</Text>
} }
@ -156,7 +156,7 @@ const ObjectProperty = ({
} }
case 'object': { case 'object': {
if (value && value.name) { if (value && value.name) {
return <Text>{value.name}</Text> return <Text ellipsis>{value.name}</Text>
} else { } else {
return <Text type='secondary'>n/a</Text> return <Text type='secondary'>n/a</Text>
} }

View File

@ -15,32 +15,50 @@ import {
Col, Col,
Descriptions, Descriptions,
Flex, Flex,
Spin Spin,
Button,
Input,
Space,
Tooltip
} from 'antd' } from 'antd'
import { LoadingOutlined } from '@ant-design/icons' import { LoadingOutlined } from '@ant-design/icons'
import PropTypes from 'prop-types' import PropTypes from 'prop-types'
import { useMediaQuery } from 'react-responsive' import { useMediaQuery } from 'react-responsive'
import axios from 'axios' import { useContext } from 'react'
import { ApiServerContext } from '../context/ApiServerContext'
import config from '../../../config' import config from '../../../config'
import loglevel from 'loglevel' import loglevel from 'loglevel'
import {
getModelProperties,
getModelByName,
getPropertyValue
} from '../../../database/ObjectModels'
import ObjectProperty from './ObjectProperty'
import XMarkIcon from '../../Icons/XMarkIcon'
import CheckIcon from '../../Icons/CheckIcon'
import { useNavigate } from 'react-router-dom'
import QuestionCircleIcon from '../../Icons/QuestionCircleIcon'
const logger = loglevel.getLogger('DasboardTable') const logger = loglevel.getLogger('DasboardTable')
logger.setLevel(config.logLevel) logger.setLevel(config.logLevel)
const ObjectTable = forwardRef( const ObjectTable = forwardRef(
( (
{ {
columns, type,
url, url,
pageSize = 25, pageSize = 25,
scrollHeight = 'calc(var(--unit-100vh) - 270px)', scrollHeight = 'calc(var(--unit-100vh) - 270px)',
onDataChange, onDataChange,
authenticated, authenticated,
initialPage = 1, initialPage = 1,
cards = false cards = false,
visibleColumns = {}
}, },
ref ref
) => { ) => {
const { fetchTableData } = useContext(ApiServerContext)
const isMobile = useMediaQuery({ maxWidth: 768 }) const isMobile = useMediaQuery({ maxWidth: 768 })
const navigate = useNavigate()
var adjustedScrollHeight = scrollHeight var adjustedScrollHeight = scrollHeight
if (isMobile) { if (isMobile) {
adjustedScrollHeight = 'calc(var(--unit-100vh) - 316px)' adjustedScrollHeight = 'calc(var(--unit-100vh) - 316px)'
@ -50,8 +68,8 @@ const ObjectTable = forwardRef(
} }
const [, contextHolder] = message.useMessage() const [, contextHolder] = message.useMessage()
const tableRef = useRef(null) const tableRef = useRef(null)
const [filters, setFilters] = useState({}) const [tableFilter, setTableFilter] = useState({})
const [sorter, setSorter] = useState({}) const [tableSorter, setTableSorter] = useState({})
const [initialized, setInitialized] = useState(false) const [initialized, setInitialized] = useState(false)
// Table state // Table state
@ -59,7 +77,6 @@ const ObjectTable = forwardRef(
const [hasMore, setHasMore] = useState(true) const [hasMore, setHasMore] = useState(true)
const [loading, setLoading] = useState(true) const [loading, setLoading] = useState(true)
const [lazyLoading, setLazyLoading] = useState(false) const [lazyLoading, setLazyLoading] = useState(false)
const [totalPages, setTotalPages] = useState(0)
const createSkeletonData = useCallback(() => { const createSkeletonData = useCallback(() => {
return Array(pageSize) return Array(pageSize)
@ -71,30 +88,31 @@ const ObjectTable = forwardRef(
}, [pageSize]) }, [pageSize])
const fetchData = useCallback( const fetchData = useCallback(
async (pageNum = 1) => { async (pageNum = 1, filter = null, sorter = null) => {
if (filter == null) {
filter = tableFilter
} else {
setTableFilter(filter)
}
if (sorter == null) {
sorter = tableSorter
} else {
setTableSorter({
field: sorter.field,
order: sorter.order
})
}
console.log('filter 2', filter)
try { try {
const response = await axios.get(url, { const result = await fetchTableData(type, {
params: { page: pageNum,
page: pageNum, limit: pageSize,
limit: pageSize, filter,
...filters, sorter,
sort: sorter.field, onDataChange
order: sorter.order
},
headers: {
Accept: 'application/json'
},
withCredentials: true
}) })
const newData = response.data setHasMore(result.hasMore)
const totalCount = parseInt(
response.headers['x-total-count'] || '0',
10
)
setTotalPages(Math.ceil(totalCount / pageSize))
setHasMore(newData.length >= pageSize)
setPages((prev) => { setPages((prev) => {
const existingPageIndex = prev.findIndex( const existingPageIndex = prev.findIndex(
@ -104,20 +122,16 @@ const ObjectTable = forwardRef(
if (existingPageIndex !== -1) { if (existingPageIndex !== -1) {
// Update existing page // Update existing page
const newPages = [...prev] const newPages = [...prev]
newPages[existingPageIndex] = { pageNum, items: newData } newPages[existingPageIndex] = { pageNum, items: result.data }
return newPages return newPages
} }
// If page doesn't exist, return unchanged // If page doesn't exist, return unchanged
return prev return prev
}) })
if (onDataChange) {
onDataChange(newData)
}
setLoading(false) setLoading(false)
setLazyLoading(false) setLazyLoading(false)
return newData return result.data
} catch (error) { } catch (error) {
setPages((prev) => setPages((prev) =>
prev.map((page) => ({ prev.map((page) => ({
@ -130,7 +144,7 @@ const ObjectTable = forwardRef(
throw error throw error
} }
}, },
[url, pageSize, filters, sorter, onDataChange] [url, pageSize, tableFilter, tableSorter, onDataChange, fetchTableData]
) )
const loadNextPage = useCallback(() => { const loadNextPage = useCallback(() => {
@ -233,40 +247,34 @@ const ObjectTable = forwardRef(
) )
}, []) }, [])
const goToPage = useCallback( const loadPage = useCallback(
(pageNum) => { async (pageNum, filter = null, sorter = null) => {
if (pageNum > 0 && pageNum <= totalPages) { // Create initial page with skeletons
const pagesToLoad = [pageNum - 1, pageNum, pageNum + 1].filter( setPages([{ pageNum: pageNum, items: createSkeletonData() }])
(p) => p > 0 && p <= totalPages
) const items = await fetchData(pageNum, filter, sorter)
return Promise.all(pagesToLoad.map((p) => fetchData(p)))
if (items.length >= 25) {
setPages((prev) => [
...prev,
{ pageNum: pageNum + 1, items: createSkeletonData() }
])
await fetchData(pageNum + 1, filter, sorter)
} }
}, },
[fetchData, totalPages] [createSkeletonData, fetchData]
) )
const loadInitialPage = useCallback(async () => { const loadInitialPage = useCallback(async () => {
// Create initial page with skeletons loadPage(initialPage)
setPages([{ pageNum: initialPage, items: createSkeletonData() }]) }, [initialPage, loadPage])
const items = await fetchData(initialPage)
if (items.length >= 25) {
setPages((prev) => [
...prev,
{ pageNum: initialPage + 1, items: createSkeletonData() }
])
await fetchData(initialPage + 1)
}
}, [initialPage, createSkeletonData, fetchData])
useImperativeHandle(ref, () => ({ useImperativeHandle(ref, () => ({
reload, reload,
setData: (newData) => { setData: (newData) => {
setPages([{ pageNum: 1, items: newData }]) setPages([{ pageNum: 1, items: newData }])
}, },
updateData, updateData
goToPage
})) }))
useEffect(() => { useEffect(() => {
@ -276,33 +284,207 @@ const ObjectTable = forwardRef(
} }
}, [authenticated, loadInitialPage, initialPage, pages, initialized]) }, [authenticated, loadInitialPage, initialPage, pages, initialized])
const getFilterDropdown = ({
setSelectedKeys,
selectedKeys,
confirm,
clearFilters,
propertyName
}) => {
return (
<div style={{ padding: 8 }}>
<Space.Compact>
<Input
placeholder={'Search ' + propertyName}
value={selectedKeys[0]}
onChange={(e) =>
setSelectedKeys(e.target.value ? [e.target.value] : [])
}
onPressEnter={() => confirm()}
style={{ width: 200, display: 'block' }}
/>
<Button
onClick={() => {
clearFilters()
confirm()
}}
icon={<XMarkIcon />}
/>
<Button
type='primary'
onClick={() => confirm()}
icon={<CheckIcon />}
/>
</Space.Compact>
</div>
)
}
const handleTableChange = (pagination, filters, sorter) => { const handleTableChange = (pagination, filters, sorter) => {
const newFilters = {} const newFilters = {}
Object.entries(filters).forEach(([key, value]) => { Object.entries(filters).forEach(([key, value]) => {
if (value && value.length > 0) { if (value && value.length > 0) {
newFilters[key] = value[0] newFilters[key] = value[0]
} }
}) })
setFilters(newFilters)
setSorter({ setPages([])
setLoading(true)
loadPage(initialPage, newFilters, {
field: sorter.field, field: sorter.field,
order: sorter.order order: sorter.order
}) })
setPages([])
fetchData(1)
} }
const columnsWithSkeleton = columns.map((col) => ({ const modelProperties = getModelProperties(type)
...col, const model = getModelByName(type)
render: (text, record) => {
if (record.isSkeleton) { // Table columns from model properties
return ( const columnsWithSkeleton = [
<Skeleton.Input active size='small' style={{ width: '100%' }} /> {
) title: model.icon,
} key: 'icon',
return col.render ? col.render(text, record) : text width: 45,
fixed: 'left',
render: model.icon
} }
})) ]
// Add columns in the order specified by model.columns
model.columns.forEach((colName) => {
const prop = modelProperties.find((p) => p.name === colName)
if (prop) {
// Check if column should be visible based on visibleColumns prop
if (
Object.keys(visibleColumns).length > 0 &&
visibleColumns[prop.name] === false
) {
return // Skip this column if it's not visible
}
var fixed = prop.columnFixed || undefined
var width = 200
switch (prop.type) {
case 'text':
width = 200
break
case 'number':
width = 100
break
case 'dateTime':
width = 200
break
case 'state':
width = 200
break
case 'id':
width = 180
break
default:
break
}
// Check if this property should be filterable based on model.filters
const isFilterable = model.filters && model.filters.includes(prop.name)
// Check if this property should be sortable based on model.sorters
const isSortable = model.sorters && model.sorters.includes(prop.name)
const columnConfig = {
sorter: isSortable,
title: prop.label,
dataIndex: prop.name,
width: prop.columnWidth || width,
fixed: fixed,
key: prop.name,
render: (text, record) => {
if (record.isSkeleton) {
return (
<Skeleton.Input active size='small' style={{ width: '100%' }} />
)
}
return (
<ObjectProperty
{...prop}
longId={false}
type={prop.type}
objectType={prop.objectType}
value={getPropertyValue(record, prop.name)}
isEditing={false}
/>
)
}
}
// Add filter configuration if the property is filterable
if (isFilterable) {
columnConfig.filterDropdown = ({
setSelectedKeys,
selectedKeys,
confirm,
clearFilters
}) =>
getFilterDropdown({
setSelectedKeys,
selectedKeys,
confirm,
clearFilters,
propertyName: prop.label
})
// Remove local filtering - let the server handle it
columnConfig.filtered = false
}
columnsWithSkeleton.push(columnConfig)
}
})
if (model.actions.length > 0) {
const rowActions = model.actions.filter((action) => action.row == true)
if (rowActions.length > 0) {
columnsWithSkeleton.push({
title: (
<Flex gap='small' align='center' justify='center'>
{'Actions'}
</Flex>
),
key: 'actions',
fixed: 'right',
width: 80 + rowActions.length * 40, // Adjust width based on number of actions
render: (record) => {
return (
<Flex gap='small' align='center' justify='center'>
{rowActions.map((action, index) => (
<Tooltip key={index} title={action.label} arrow={false}>
<Button
icon={
action.icon ? (
React.createElement(action.icon)
) : (
<QuestionCircleIcon />
)
}
type={'text'}
size={'small'}
onClick={() => {
if (action.onClick) {
action.onClick(record)
} else if (action.url) {
navigate(action.url(record._id))
} else {
navigate(model.url(record._id))
}
}}
/>
</Tooltip>
))}
</Flex>
)
}
})
}
}
// Flatten pages array for table display // Flatten pages array for table display
const tableData = pages.flatMap((page) => page.items) const tableData = pages.flatMap((page) => page.items)
@ -355,72 +537,31 @@ const ObjectTable = forwardRef(
style={{ overflowY: 'auto', maxHeight: adjustedScrollHeight }} style={{ overflowY: 'auto', maxHeight: adjustedScrollHeight }}
ref={cardsContainerRef} ref={cardsContainerRef}
> >
{tableData.map((record) => { {tableData.map((record) => (
// Special case for columns[0] if needed <Col xs={24} sm={12} md={12} lg={8} xl={6} xxl={6} key={record._id}>
let icon = null <Card
if (columns[0].key === 'icon' && columns[0].render) { style={{ width: '100%', overflow: 'hidden' }}
const renderedIcon = columns[0].render() loading={record.isSkeleton}
icon = React.cloneElement(renderedIcon, {
style: {
fontSize: 32,
...(renderedIcon.props.style || {})
}
})
}
let actions = null
const endColumn = columns.length - 1
if (
columns[endColumn].key === 'actions' &&
columns[endColumn].render
) {
actions = columns[endColumn].render(record)
}
return (
<Col
xs={24}
sm={12}
md={12}
lg={8}
xl={6}
xxl={6}
key={record._id}
> >
<Card <Flex align={'center'} vertical gap={'middle'}>
style={{ width: '100%', overflow: 'hidden' }} <Descriptions column={1} size='small' bordered={false}>
loading={record.isSkeleton} {modelProperties.map((prop) => (
> <Descriptions.Item label={prop.label} key={prop.name}>
<Flex align={'center'} vertical gap={'middle'}> <ObjectProperty
{icon} {...prop}
<Descriptions column={1} size='small' bordered={false}> longId={false}
{columns type={prop.type}
.filter( objectType={prop.objectType}
(col) => col.key !== 'icon' && col.key !== 'actions' value={getPropertyValue(record, prop.name)}
) isEditing={false}
.map((col) => { />
let value </Descriptions.Item>
if (col.render && col.dataIndex) { ))}
value = col.render(record[col.dataIndex], record) </Descriptions>
} else if (col.render && !col.dataIndex) { </Flex>
value = col.render(record) </Card>
} else { </Col>
value = String(record[col.dataIndex] ?? '') ))}
}
return (
<Descriptions.Item
label={col.title}
key={col.key || col.dataIndex}
>
{value}
</Descriptions.Item>
)
})}
</Descriptions>
{actions}
</Flex>
</Card>
</Col>
)
})}
</Row> </Row>
) )
} }
@ -456,7 +597,7 @@ const ObjectTable = forwardRef(
ObjectTable.displayName = 'ObjectTable' ObjectTable.displayName = 'ObjectTable'
ObjectTable.propTypes = { ObjectTable.propTypes = {
columns: PropTypes.arrayOf(PropTypes.object).isRequired, type: PropTypes.string.isRequired,
url: PropTypes.string.isRequired, url: PropTypes.string.isRequired,
pageSize: PropTypes.number, pageSize: PropTypes.number,
scrollHeight: PropTypes.string, scrollHeight: PropTypes.string,
@ -464,7 +605,8 @@ ObjectTable.propTypes = {
authenticated: PropTypes.bool.isRequired, authenticated: PropTypes.bool.isRequired,
initialPage: PropTypes.number, initialPage: PropTypes.number,
cards: PropTypes.bool, cards: PropTypes.bool,
cardRenderer: PropTypes.func cardRenderer: PropTypes.func,
visibleColumns: PropTypes.object
} }
export default ObjectTable export default ObjectTable

View File

@ -1,8 +1,8 @@
import React from 'react' import React from 'react'
import PropTypes from 'prop-types' import PropTypes from 'prop-types'
import { Typography, Flex, Button, Tooltip } from 'antd' import { Typography, Flex, Button, Tooltip } from 'antd'
import LinkIcon from './LinkIcon' import LinkIcon from '../../Icons/LinkIcon'
import CopyButton from '../Dashboard/common/CopyButton' import CopyButton from './CopyButton'
const { Text, Link } = Typography const { Text, Link } = Typography

View File

@ -4,7 +4,7 @@ import PropTypes from 'prop-types'
const ViewButton = ({ const ViewButton = ({
loading = false, loading = false,
sections = [], properties = [],
collapseState = {}, collapseState = {},
updateCollapseState = () => {}, updateCollapseState = () => {},
...buttonProps ...buttonProps
@ -15,15 +15,15 @@ const ViewButton = ({
return ( return (
<Flex vertical> <Flex vertical>
<Flex vertical gap='middle' style={{ margin: '4px 8px' }}> <Flex vertical gap='middle' style={{ margin: '4px 8px' }}>
{sections.map((section) => ( {properties.map((property) => (
<Checkbox <Checkbox
checked={collapseState[section.key]} checked={collapseState[property.key]}
key={section.key} key={property.key}
onChange={(e) => { onChange={(e) => {
updateCollapseState(section.key, e.target.checked) updateCollapseState(property.key, e.target.checked)
}} }}
> >
{section.label} {property.label}
</Checkbox> </Checkbox>
))} ))}
</Flex> </Flex>
@ -42,7 +42,7 @@ const ViewButton = ({
ViewButton.propTypes = { ViewButton.propTypes = {
loading: PropTypes.bool, loading: PropTypes.bool,
sections: PropTypes.arrayOf( properties: PropTypes.arrayOf(
PropTypes.shape({ PropTypes.shape({
key: PropTypes.string.isRequired, key: PropTypes.string.isRequired,
label: PropTypes.string.isRequired label: PropTypes.string.isRequired

View File

@ -224,7 +224,7 @@ const ApiServerProvider = ({ children }) => {
// Update filament information // Update filament information
const updateObjectInfo = async (id, type, value) => { const updateObjectInfo = async (id, type, value) => {
const updateUrl = `${config.backendUrl}/${type}s/${id}` const updateUrl = `${config.backendUrl}/${type.toLowerCase()}s/${id}`
logger.debug('Updating info for ' + id) logger.debug('Updating info for ' + id)
try { try {
const response = await axios.put(updateUrl, value, { const response = await axios.put(updateUrl, value, {
@ -249,6 +249,99 @@ const ApiServerProvider = ({ children }) => {
} }
} }
// Fetch table data with pagination, filtering, and sorting
const fetchTableData = async (type, params = {}) => {
const {
page = 1,
limit = 25,
filter = {},
sorter = {},
onDataChange
} = params
logger.debug('Fetching table data from:', type, {
page,
limit,
filter,
sorter
})
try {
const response = await axios.get(
`${config.backendUrl}/${type.toLowerCase()}s`,
{
params: {
page,
limit,
...filter,
sort: sorter.field,
order: sorter.order
},
headers: {
Accept: 'application/json'
},
withCredentials: true
}
)
const newData = response.data
const totalCount = parseInt(response.headers['x-total-count'] || '0', 10)
const totalPages = Math.ceil(totalCount / limit)
const hasMore = newData.length >= limit
if (onDataChange) {
onDataChange(newData)
}
return {
data: newData,
totalCount,
totalPages,
hasMore,
page
}
} catch (error) {
logger.error('Failed to fetch table data:', error)
throw error
}
}
// Download GCode file content
const handleDownloadContent = async (id, type, fileName) => {
if (!token) {
return
}
try {
const response = await axios.get(
`${config.backendUrl}/${type.toLowerCase()}s/${id}/content`,
{
headers: {
Accept: 'application/json'
},
withCredentials: true
}
)
const fileURL = window.URL.createObjectURL(new Blob([response.data]))
const fileLink = document.createElement('a')
fileLink.href = fileURL
fileLink.setAttribute('download', fileName)
document.body.appendChild(fileLink)
fileLink.click()
fileLink.parentNode.removeChild(fileLink)
} catch (error) {
logger.error('Failed to download GCode file content:', error)
if (error.response) {
messageApi.error('Error downloading GCode file:', error.response.status)
} else {
messageApi.error(
'An unexpected error occurred while downloading. Please try again later.'
)
}
throw error
}
}
return ( return (
<ApiServerContext.Provider <ApiServerContext.Provider
value={{ value={{
@ -263,8 +356,10 @@ const ApiServerProvider = ({ children }) => {
onUpdateEvent, onUpdateEvent,
offUpdateEvent, offUpdateEvent,
fetchObjectInfo, fetchObjectInfo,
fetchTableData,
fetchLoading, fetchLoading,
showError showError,
handleDownloadContent
}} }}
> >
{contextHolder} {contextHolder}

View File

@ -1,15 +1,21 @@
import { useState, useEffect } from 'react' import { useState, useEffect } from 'react'
import { getModelByName } from '../../../database/ObjectModels'
const useColumnVisibility = (componentName, columns) => { const useColumnVisibility = (type) => {
const getInitialVisibility = () => { const getInitialVisibility = () => {
const stored = sessionStorage.getItem(`${componentName}_columnVisibility`) const stored = sessionStorage.getItem(`${type}_columnVisibility`)
if (stored) { if (stored) {
return JSON.parse(stored) return JSON.parse(stored)
} }
// Default visibility - all columns visible // Default visibility - all columns visible
return columns.reduce((acc, col) => { const model = getModelByName(type)
if (col.key) { const columns = model.columns || []
acc[col.key] = true return columns.reduce((acc, columnName) => {
const property = model.properties?.find(
(prop) => prop.name === columnName
)
if (property) {
acc[property.name] = true
} }
return acc return acc
}, {}) }, {})
@ -19,10 +25,10 @@ const useColumnVisibility = (componentName, columns) => {
useEffect(() => { useEffect(() => {
sessionStorage.setItem( sessionStorage.setItem(
`${componentName}_columnVisibility`, `${type}_columnVisibility`,
JSON.stringify(columnVisibility) JSON.stringify(columnVisibility)
) )
}, [columnVisibility, componentName]) }, [columnVisibility, type])
const updateColumnVisibility = (key, value) => { const updateColumnVisibility = (key, value) => {
setColumnVisibility((prev) => ({ setColumnVisibility((prev) => ({

View File

@ -4,6 +4,7 @@ import { Spool } from './models/Spool'
import { GCodeFile } from './models/GCodeFile' import { GCodeFile } from './models/GCodeFile'
import { Job } from './models/Job' import { Job } from './models/Job'
import { Product } from './models/Product' import { Product } from './models/Product'
import { Part } from './models/Part.js'
import { Vendor } from './models/Vendor' import { Vendor } from './models/Vendor'
import { SubJob } from './models/SubJob' import { SubJob } from './models/SubJob'
import { Initial } from './models/Initial' import { Initial } from './models/Initial'
@ -25,6 +26,7 @@ export const objectModels = [
GCodeFile, GCodeFile,
Job, Job,
Product, Product,
Part,
Vendor, Vendor,
SubJob, SubJob,
Initial, Initial,
@ -47,6 +49,7 @@ export {
GCodeFile, GCodeFile,
Job, Job,
Product, Product,
Part,
Vendor, Vendor,
SubJob, SubJob,
Initial, Initial,

View File

@ -1,9 +1,20 @@
import AuditLogIcon from '../../components/Icons/AuditLogIcon' import AuditLogIcon from '../../components/Icons/AuditLogIcon'
import InfoCircleIcon from '../../components/Icons/InfoCircleIcon'
export const AuditLog = { export const AuditLog = {
name: 'auditlog', name: 'auditlog',
label: 'Audit Log', label: 'Audit Log',
prefix: 'ADL', prefix: 'ADL',
icon: AuditLogIcon, icon: AuditLogIcon,
actions: [
{
name: 'info',
label: 'Info',
default: true,
row: true,
icon: InfoCircleIcon,
url: (_id) => `/dashboard/management/auditlogs/info?auditLogId=${_id}`
}
],
url: () => `#` url: () => `#`
} }

View File

@ -1,16 +1,41 @@
import FilamentIcon from '../../components/Icons/FilamentIcon' import FilamentIcon from '../../components/Icons/FilamentIcon'
import InfoCircleIcon from '../../components/Icons/InfoCircleIcon'
export const Filament = { export const Filament = {
name: 'filament', name: 'filament',
label: 'Filament', label: 'Filament',
prefix: 'FIL', prefix: 'FIL',
icon: FilamentIcon, icon: FilamentIcon,
url: (id) => `/dashboard/management/filaments/info?filamentId=${id}`, actions: [
{
name: 'info',
label: 'Info',
default: true,
row: true,
icon: InfoCircleIcon,
url: (_id) => `/dashboard/management/filaments/info?filamentId=${_id}`
}
],
columns: [
'_id',
'name',
'type',
'color',
'vendor',
'vendor._id',
'cost',
'density',
'diameter',
'createdAt',
'updatedAt'
],
filters: ['_id', 'name', 'type', 'color', 'cost', 'vendor', 'vendor._id'],
sorters: ['name', 'createdAt', 'type', 'vendor', 'cost', 'updatedAt'],
properties: [ properties: [
{ {
name: '_id', name: '_id',
label: 'ID', label: 'ID',
columnFixed: 'left',
type: 'id', type: 'id',
objectType: 'filament', objectType: 'filament',
showCopy: true showCopy: true
@ -18,28 +43,25 @@ export const Filament = {
{ {
name: 'createdAt', name: 'createdAt',
label: 'Created At', label: 'Created At',
type: 'dateTime', type: 'dateTime',
readOnly: true readOnly: true
}, },
{ {
name: 'name', name: 'name',
label: 'Name', label: 'Name',
columnFixed: 'left',
required: true, required: true,
type: 'text' type: 'text'
}, },
{ {
name: 'updatedAt', name: 'updatedAt',
label: 'Updated At', label: 'Updated At',
type: 'dateTime', type: 'dateTime',
readOnly: true readOnly: true
}, },
{ {
name: 'vendor', name: 'vendor',
label: 'Vendor', label: 'Vendor',
required: true, required: true,
type: 'object', type: 'object',
objectType: 'vendor' objectType: 'vendor'
@ -55,34 +77,35 @@ export const Filament = {
{ {
name: 'type', name: 'type',
label: 'Material', label: 'Material',
required: true, required: true,
columnWidth: 150,
type: 'material' type: 'material'
}, },
{ {
name: 'cost', name: 'cost',
label: 'Cost', label: 'Cost',
columnWidth: 150,
required: true, required: true,
type: 'currency' type: 'currency'
}, },
{ {
name: 'color', name: 'color',
label: 'Color', label: 'Color',
columnWidth: 150,
required: true, required: true,
type: 'color' type: 'color'
}, },
{ {
name: 'diameter', name: 'diameter',
label: 'Diameter', label: 'Diameter',
columnWidth: 150,
required: true, required: true,
type: 'mm' type: 'mm'
}, },
{ {
name: 'density', name: 'density',
label: 'Density', label: 'Density',
columnWidth: 150,
required: true, required: true,
type: 'density' type: 'density'
}, },

View File

@ -1,9 +1,21 @@
import FilamentStockIcon from '../../components/Icons/FilamentStockIcon' import FilamentStockIcon from '../../components/Icons/FilamentStockIcon'
import InfoCircleIcon from '../../components/Icons/InfoCircleIcon'
export const FilamentStock = { export const FilamentStock = {
name: 'filamentstock', name: 'filamentstock',
label: 'Filament Stock', label: 'Filament Stock',
prefix: 'FLS', prefix: 'FLS',
icon: FilamentStockIcon, icon: FilamentStockIcon,
actions: [
{
name: 'info',
label: 'Info',
default: true,
row: true,
icon: InfoCircleIcon,
url: (_id) =>
`/dashboard/inventory/filamentstocks/info?filamentStockId=${_id}`
}
],
url: (id) => `/dashboard/inventory/filamentstocks/info?filamentStockId=${id}` url: (id) => `/dashboard/inventory/filamentstocks/info?filamentStockId=${id}`
} }

View File

@ -1,17 +1,49 @@
import GCodeFileIcon from '../../components/Icons/GCodeFileIcon' import GCodeFileIcon from '../../components/Icons/GCodeFileIcon'
import InfoCircleIcon from '../../components/Icons/InfoCircleIcon'
export const GCodeFile = { export const GCodeFile = {
name: 'gcodeFile', name: 'gcodeFile',
label: 'GCode File', label: 'GCode File',
prefix: 'GCF', prefix: 'GCF',
icon: GCodeFileIcon, icon: GCodeFileIcon,
url: (id) => `/dashboard/production/gcodefiles/info?gcodeFileId=${id}`, actions: [
{
name: 'info',
label: 'Info',
default: true,
row: true,
icon: InfoCircleIcon,
url: (_id) => `/dashboard/production/gcodefiles/info?gcodeFileId=${_id}`
},
{
name: 'download',
label: 'Download',
row: true,
url: (_id) =>
`/dashboard/production/gcodefiles/info?gcodeFileId=${_id}&action=download`
}
],
columns: [
'name',
'_id',
'filament',
'gcodeFileInfo.estimatedPrintingTimeNormalMode',
'gcodeFileInfo.sparseInfillDensity',
'gcodeFileInfo.sparseInfillPattern',
'gcodeFileInfo.nozzleTemperature',
'gcodeFileInfo.hotPlateTemp',
'updatedAt'
],
filters: ['_id', 'name', 'updatedAt'],
sorters: ['name', 'createdAt', 'updatedAt'],
properties: [ properties: [
{ {
name: '_id', name: '_id',
label: 'ID', label: 'ID',
type: 'id', type: 'id',
objectType: 'gcodefile', objectType: 'gcodeFile',
columnFixed: 'left',
value: null, value: null,
showCopy: true showCopy: true
}, },
@ -25,6 +57,7 @@ export const GCodeFile = {
{ {
name: 'name', name: 'name',
label: 'Name', label: 'Name',
columnFixed: 'left',
type: 'text', type: 'text',
value: null, value: null,
required: true required: true
@ -61,6 +94,7 @@ export const GCodeFile = {
{ {
name: 'gcodeFileInfo.sparseInfillDensity', name: 'gcodeFileInfo.sparseInfillDensity',
label: 'Infill Density', label: 'Infill Density',
columnWidth: 150,
type: 'number', type: 'number',
readOnly: true readOnly: true
}, },
@ -86,14 +120,16 @@ export const GCodeFile = {
}, },
{ {
name: 'gcodeFileInfo.nozzleTemperature', name: 'gcodeFileInfo.nozzleTemperature',
label: 'Hotend Temperature', label: 'Hotend Temp',
columnWidth: 150,
value: null, value: null,
type: 'number', type: 'number',
readOnly: true readOnly: true
}, },
{ {
name: 'gcodeFileInfo.hotPlateTemp', name: 'gcodeFileInfo.hotPlateTemp',
label: 'Bed Temperature', label: 'Bed Temp',
columnWidth: 150,
value: null, value: null,
type: 'number', type: 'number',
readOnly: true readOnly: true

View File

@ -1,9 +1,20 @@
import QuestionCircleIcon from '../../components/Icons/QuestionCircleIcon' import QuestionCircleIcon from '../../components/Icons/QuestionCircleIcon'
import InfoCircleIcon from '../../components/Icons/InfoCircleIcon'
export const Initial = { export const Initial = {
name: 'initial', name: 'initial',
label: 'Initial', label: 'Initial',
prefix: 'INT', prefix: 'INT',
icon: QuestionCircleIcon, icon: QuestionCircleIcon,
actions: [
{
name: 'info',
label: 'Info',
default: true,
row: true,
icon: InfoCircleIcon,
url: (_id) => `/dashboard/management/initials/info?initialId=${_id}`
}
],
url: () => `#` url: () => `#`
} }

View File

@ -1,16 +1,37 @@
import JobIcon from '../../components/Icons/JobIcon' import JobIcon from '../../components/Icons/JobIcon'
import InfoCircleIcon from '../../components/Icons/InfoCircleIcon'
export const Job = { export const Job = {
name: 'job', name: 'job',
label: 'Job', label: 'Job',
prefix: 'JOB', prefix: 'JOB',
icon: JobIcon, icon: JobIcon,
url: (id) => `/dashboard/production/jobs/info?jobId=${id}`, actions: [
{
name: 'info',
label: 'Info',
default: true,
row: true,
icon: InfoCircleIcon,
url: (_id) => `/dashboard/production/jobs/info?jobId=${_id}`
}
],
columns: [
'_id',
'gcodeFile',
'gcodeFile._id',
'state',
'quantity',
'createdAt'
],
filters: ['state', '_id', 'gcodeFile._id', 'quantity'],
sorters: ['createdAt', 'state', 'quantity', '_id'],
properties: [ properties: [
{ {
name: '_id', name: '_id',
label: 'ID', label: 'ID',
type: 'id', type: 'id',
columnFixed: 'left',
objectType: 'job', objectType: 'job',
showCopy: true showCopy: true
}, },
@ -23,12 +44,14 @@ export const Job = {
showProgress: true, showProgress: true,
showId: false, showId: false,
showQuantity: false, showQuantity: false,
columnWidth: 150,
readOnly: true readOnly: true
}, },
{ {
name: 'gcodeFile', name: 'gcodeFile',
label: 'GCode File', label: 'GCode File',
type: 'object', type: 'object',
columnFixed: 'left',
objectType: 'gcodeFile', objectType: 'gcodeFile',
readOnly: true readOnly: true
}, },
@ -43,6 +66,7 @@ export const Job = {
name: 'quantity', name: 'quantity',
label: 'Quantity', label: 'Quantity',
type: 'number', type: 'number',
columnWidth: 125,
readOnly: true readOnly: true
}, },
{ {

View File

@ -1,9 +1,20 @@
import NoteIcon from '../../components/Icons/NoteIcon' import NoteIcon from '../../components/Icons/NoteIcon'
import InfoCircleIcon from '../../components/Icons/InfoCircleIcon'
export const Note = { export const Note = {
name: 'note', name: 'note',
label: 'Note', label: 'Note',
prefix: 'NTE', prefix: 'NTE',
icon: NoteIcon, icon: NoteIcon,
actions: [
{
name: 'info',
label: 'Info',
default: true,
row: true,
icon: InfoCircleIcon,
url: (_id) => `/dashboard/management/notes/info?noteId=${_id}`
}
],
url: () => `#` url: () => `#`
} }

View File

@ -1,9 +1,59 @@
import NoteTypeIcon from '../../components/Icons/NoteTypeIcon' import NoteTypeIcon from '../../components/Icons/NoteTypeIcon'
import InfoCircleIcon from '../../components/Icons/InfoCircleIcon'
export const NoteType = { export const NoteType = {
name: 'notetype', name: 'noteType',
label: 'Note Type', label: 'Note Type',
prefix: 'NTY', prefix: 'NTY',
icon: NoteTypeIcon, icon: NoteTypeIcon,
url: (id) => `/dashboard/management/notetypes/info?noteTypeId=${id}` actions: [
{
name: 'info',
label: 'Info',
default: true,
row: true,
icon: InfoCircleIcon,
url: (_id) => `/dashboard/management/notetypes/info?noteTypeId=${_id}`
}
],
columns: ['name', '_id', 'color', 'active', 'createdAt', 'updatedAt'],
filters: ['name', '_id', 'color', 'active'],
sorters: ['name', 'color', 'active', 'createdAt', 'updatedAt'],
properties: [
{
name: '_id',
label: 'ID',
type: 'id',
objectType: 'noteType',
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: 'color',
label: 'Color',
type: 'color'
},
{
name: 'active',
label: 'Active',
type: 'bool'
}
]
} }

View File

@ -0,0 +1,64 @@
import InfoCircleIcon from '../../components/Icons/InfoCircleIcon'
import PartIcon from '../../components/Icons/PartIcon'
export const Part = {
name: 'part',
label: 'Part',
prefix: 'PRT',
icon: PartIcon,
actions: [
{
name: 'info',
label: 'Info',
default: true,
row: true,
icon: InfoCircleIcon,
url: (_id) => `/dashboard/management/parts/info?partId=${_id}`
}
],
columns: ['name', '_id', 'product', 'product._id', 'createdAt'],
filters: ['name', '_id', 'product', 'product._id'],
sorters: ['name', 'email', 'role', 'createdAt', '_id'],
properties: [
{
name: '_id',
label: 'ID',
columnFixed: 'left',
type: 'id',
objectType: 'user',
showCopy: true
},
{
name: 'createdAt',
label: 'Created At',
type: 'dateTime',
readOnly: true
},
{
name: 'name',
label: 'Name',
columnFixed: 'left',
required: true,
type: 'text'
},
{
name: 'updatedAt',
label: 'Updated At',
type: 'dateTime',
readOnly: true
},
{
name: 'product',
label: 'Product',
type: 'object',
objectType: 'product'
},
{
name: 'product._id',
label: 'Product ID',
type: 'id',
objectType: 'product'
}
]
}

View File

@ -1,9 +1,20 @@
import PartStockIcon from '../../components/Icons/PartStockIcon' import PartStockIcon from '../../components/Icons/PartStockIcon'
import InfoCircleIcon from '../../components/Icons/InfoCircleIcon'
export const PartStock = { export const PartStock = {
name: 'partstock', name: 'partstock',
label: 'Part Stock', label: 'Part Stock',
prefix: 'PTS', prefix: 'PTS',
icon: PartStockIcon, icon: PartStockIcon,
actions: [
{
name: 'info',
label: 'Info',
default: true,
row: true,
icon: InfoCircleIcon,
url: (_id) => `/dashboard/management/partstocks/info?partStockId=${_id}`
}
],
url: (id) => `/dashboard/management/partstocks/info?partStockId=${id}` url: (id) => `/dashboard/management/partstocks/info?partStockId=${id}`
} }

View File

@ -1,11 +1,25 @@
import PrinterIcon from '../../components/Icons/PrinterIcon' import PrinterIcon from '../../components/Icons/PrinterIcon'
import InfoCircleIcon from '../../components/Icons/InfoCircleIcon'
export const Printer = { export const Printer = {
name: 'printer', name: 'printer',
label: 'Printer', label: 'Printer',
prefix: 'PRN', prefix: 'PRN',
icon: PrinterIcon, icon: PrinterIcon,
actions: [
{
name: 'info',
label: 'Info',
default: true,
row: true,
icon: InfoCircleIcon,
url: (_id) => `/dashboard/production/printers/info?printerId=${_id}`
}
],
url: (id) => `/dashboard/production/printers/info?printerId=${id}`, url: (id) => `/dashboard/production/printers/info?printerId=${id}`,
columns: ['name', '_id', 'state', 'tags', 'connectedAt'],
filters: ['name', '_id', 'state', 'tags'],
sorters: ['name', 'state', 'connectedAt', '_id'],
properties: [ properties: [
{ {
name: '_id', name: '_id',
@ -24,7 +38,9 @@ export const Printer = {
name: 'name', name: 'name',
label: 'Name', label: 'Name',
required: true, required: true,
type: 'text' type: 'text',
columnWidth: 200,
columnFixed: 'left'
}, },
{ {
name: 'state', name: 'state',

View File

@ -1,9 +1,47 @@
import ProductIcon from '../../components/Icons/ProductIcon' import ProductIcon from '../../components/Icons/ProductIcon'
import InfoCircleIcon from '../../components/Icons/InfoCircleIcon'
export const Product = { export const Product = {
name: 'product', name: 'product',
label: 'Product', label: 'Product',
prefix: 'PRD', prefix: 'PRD',
icon: ProductIcon, icon: ProductIcon,
url: (id) => `/dashboard/management/products/info?productId=${id}` actions: [
{
name: 'info',
label: 'Info',
default: true,
row: true,
icon: InfoCircleIcon,
url: (_id) => `/dashboard/management/products/info?productId=${_id}`
}
],
url: (id) => `/dashboard/management/products/info?productId=${id}`,
properties: [
{
name: '_id',
label: 'ID',
type: 'id',
objectType: 'printer',
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
}
]
} }

View File

@ -1,9 +1,21 @@
import ProductStockIcon from '../../components/Icons/ProductStockIcon' import ProductStockIcon from '../../components/Icons/ProductStockIcon'
import InfoCircleIcon from '../../components/Icons/InfoCircleIcon'
export const ProductStock = { export const ProductStock = {
name: 'productstock', name: 'productstock',
label: 'Product Stock', label: 'Product Stock',
prefix: 'PDS', prefix: 'PDS',
icon: ProductStockIcon, icon: ProductStockIcon,
actions: [
{
name: 'info',
label: 'Info',
default: true,
row: true,
icon: InfoCircleIcon,
url: (_id) =>
`/dashboard/management/productstocks/info?productStockId=${_id}`
}
],
url: (id) => `/dashboard/management/productstocks/info?productStockId=${id}` url: (id) => `/dashboard/management/productstocks/info?productStockId=${id}`
} }

View File

@ -1,9 +1,20 @@
import FilamentIcon from '../../components/Icons/FilamentIcon' import FilamentIcon from '../../components/Icons/FilamentIcon'
import InfoCircleIcon from '../../components/Icons/InfoCircleIcon'
export const Spool = { export const Spool = {
name: 'spool', name: 'spool',
label: 'Spool', label: 'Spool',
prefix: 'SPL', prefix: 'SPL',
icon: FilamentIcon, icon: FilamentIcon,
actions: [
{
name: 'info',
label: 'Info',
default: true,
row: true,
icon: InfoCircleIcon,
url: (_id) => `/dashboard/inventory/spool/info?spoolId=${_id}`
}
],
url: (id) => `/dashboard/inventory/spool/info?spoolId=${id}` url: (id) => `/dashboard/inventory/spool/info?spoolId=${id}`
} }

View File

@ -1,9 +1,20 @@
import StockAuditIcon from '../../components/Icons/StockAuditIcon' import StockAuditIcon from '../../components/Icons/StockAuditIcon'
import InfoCircleIcon from '../../components/Icons/InfoCircleIcon'
export const StockAudit = { export const StockAudit = {
name: 'stockaudit', name: 'stockaudit',
label: 'Stock Audit', label: 'Stock Audit',
prefix: 'SAU', prefix: 'SAU',
icon: StockAuditIcon, icon: StockAuditIcon,
actions: [
{
name: 'info',
label: 'Info',
default: true,
row: true,
icon: InfoCircleIcon,
url: (_id) => `/dashboard/inventory/stockaudits/info?stockAuditId=${_id}`
}
],
url: (id) => `/dashboard/inventory/stockaudits/info?stockAuditId=${id}` url: (id) => `/dashboard/inventory/stockaudits/info?stockAuditId=${id}`
} }

View File

@ -1,9 +1,20 @@
import StockEventIcon from '../../components/Icons/StockEventIcon' import StockEventIcon from '../../components/Icons/StockEventIcon'
import InfoCircleIcon from '../../components/Icons/InfoCircleIcon'
export const StockEvent = { export const StockEvent = {
name: 'stockevent', name: 'stockevent',
label: 'Stock Event', label: 'Stock Event',
prefix: 'SEV', prefix: 'SEV',
icon: StockEventIcon, icon: StockEventIcon,
actions: [
{
name: 'info',
label: 'Info',
default: true,
row: true,
icon: InfoCircleIcon,
url: (_id) => `/dashboard/inventory/stockevents/info?stockEventId=${_id}`
}
],
url: () => `#` url: () => `#`
} }

View File

@ -1,9 +1,20 @@
import SubJobIcon from '../../components/Icons/SubJobIcon' import SubJobIcon from '../../components/Icons/SubJobIcon'
import InfoCircleIcon from '../../components/Icons/InfoCircleIcon'
export const SubJob = { export const SubJob = {
name: 'subjob', name: 'subjob',
label: 'Sub Job', label: 'Sub Job',
prefix: 'SJB', prefix: 'SJB',
icon: SubJobIcon, icon: SubJobIcon,
actions: [
{
name: 'info',
label: 'Info',
default: true,
row: true,
icon: InfoCircleIcon,
url: (_id) => `/dashboard/production/subjobs/info?subJobId=${_id}`
}
],
url: () => `#` url: () => `#`
} }

View File

@ -1,9 +1,75 @@
import PersonIcon from '../../components/Icons/PersonIcon' import PersonIcon from '../../components/Icons/PersonIcon'
import InfoCircleIcon from '../../components/Icons/InfoCircleIcon'
export const User = { export const User = {
name: 'user', name: 'user',
label: 'User', label: 'User',
prefix: 'USR', prefix: 'USR',
icon: PersonIcon, icon: PersonIcon,
url: (id) => `/dashboard/management/users/info?userId=${id}` actions: [
{
name: 'info',
label: 'Info',
default: true,
row: true,
icon: InfoCircleIcon,
url: (_id) => `/dashboard/management/users/info?userId=${_id}`
}
],
url: (id) => `/dashboard/management/users/info?userId=${id}`,
columns: ['name', '_id', 'username', 'email', 'role', 'createdAt'],
filters: ['name', '_id', 'email', 'role'],
sorters: ['name', 'email', 'role', 'createdAt', '_id'],
properties: [
{
name: '_id',
label: 'ID',
columnFixed: 'left',
type: 'id',
objectType: 'user',
showCopy: true
},
{
name: 'createdAt',
label: 'Created At',
type: 'dateTime',
readOnly: true
},
{
name: 'name',
label: 'Name',
columnFixed: 'left',
required: true,
type: 'text'
},
{
name: 'updatedAt',
label: 'Updated At',
type: 'dateTime',
readOnly: true
},
{
name: 'firstName',
label: 'First Name',
type: 'text'
},
{
name: 'username',
label: 'Username',
required: true,
type: 'text'
},
{
name: 'lastName',
label: 'Last Name',
type: 'text'
},
{
name: 'email',
label: 'Email',
columnWidth: 300,
type: 'email'
}
]
} }

View File

@ -1,16 +1,30 @@
import VendorIcon from '../../components/Icons/VendorIcon' import VendorIcon from '../../components/Icons/VendorIcon'
import InfoCircleIcon from '../../components/Icons/InfoCircleIcon'
export const Vendor = { export const Vendor = {
name: 'vendor', name: 'vendor',
label: 'Vendor', label: 'Vendor',
prefix: 'VEN', prefix: 'VEN',
icon: VendorIcon, icon: VendorIcon,
actions: [
{
name: 'info',
label: 'Info',
default: true,
row: true,
icon: InfoCircleIcon,
url: (_id) => `/dashboard/management/vendors/info?vendorId=${_id}`
}
],
url: (id) => `/dashboard/management/vendors/info?vendorId=${id}`, url: (id) => `/dashboard/management/vendors/info?vendorId=${id}`,
columns: ['name', '_id', 'country', 'email', 'createdAt'],
filters: ['name', '_id', 'country', 'email'],
sorters: ['name', 'country', 'email', 'createdAt', '_id'],
properties: [ properties: [
{ {
name: '_id', name: '_id',
label: 'ID', label: 'ID',
columnFixed: 'left',
type: 'id', type: 'id',
objectType: 'vendor', objectType: 'vendor',
showCopy: true showCopy: true
@ -18,13 +32,13 @@ export const Vendor = {
{ {
name: 'createdAt', name: 'createdAt',
label: 'Created At', label: 'Created At',
type: 'dateTime', type: 'dateTime',
readOnly: true readOnly: true
}, },
{ {
name: 'name', name: 'name',
label: 'Name', label: 'Name',
columnFixed: 'left',
required: true, required: true,
type: 'text' type: 'text'
}, },
@ -51,6 +65,7 @@ export const Vendor = {
{ {
name: 'email', name: 'email',
label: 'Email', label: 'Email',
columnWidth: 300,
type: 'email', type: 'email',
readOnly: false, readOnly: false,
required: false required: false
@ -65,6 +80,7 @@ export const Vendor = {
{ {
name: 'website', name: 'website',
label: 'Website', label: 'Website',
columnWidth: 300,
type: 'url', type: 'url',
readOnly: false, readOnly: false,
required: false required: false