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
import React, { useState, useContext, useCallback, useEffect } from 'react'
import { useNavigate } from 'react-router-dom'
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 React, { useContext, useRef, useState } from 'react'
import { Button, Flex, Space, Modal, message, Dropdown } from 'antd'
import { AuthContext } from '../context/AuthContext'
import NewFilament from './Filaments/NewFilament'
import IdDisplay from '../common/IdDisplay'
import FilamentIcon from '../../Icons/FilamentIcon'
import InfoCircleIcon from '../../Icons/InfoCircleIcon'
import useColumnVisibility from '../hooks/useColumnVisibility'
import ColumnViewButton from '../common/ColumnViewButton'
import ObjectTable from '../common/ObjectTable'
import PlusIcon from '../../Icons/PlusIcon'
import ReloadIcon from '../../Icons/ReloadIcon'
import XMarkIcon from '../../Icons/XMarkIcon'
import CheckIcon from '../../Icons/CheckIcon'
import useColumnVisibility from '../hooks/useColumnVisibility'
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;
}
}
}
`
}
})
import ListIcon from '../../Icons/ListIcon'
import GridIcon from '../../Icons/GridIcon'
import useViewMode from '../hooks/useViewMode'
const Filaments = () => {
const [messageApi, contextHolder] = message.useMessage()
const navigate = useNavigate()
const { styles } = useStyle()
const [filamentsData, setFilamentsData] = useState([])
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 [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 = {
items: [
{
@ -221,291 +44,65 @@ const Filaments = () => {
],
onClick: ({ key }) => {
if (key === 'reloadList') {
fetchFilamentsData()
tableRef.current?.reload()
} else if (key === 'newFilament') {
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 (
<>
<Flex vertical={'true'} gap='large'>
{contextHolder}
<Flex justify={'space-between'}>
<Space size='small'>
<Space>
<Dropdown menu={actionItems}>
<Button>Actions</Button>
</Dropdown>
<Popover
content={getViewDropdownItems()}
placement='bottomLeft'
arrow={false}
>
<Button>View</Button>
</Popover>
<ColumnViewButton
type='filament'
loading={false}
collapseState={columnVisibility}
updateCollapseState={setColumnVisibility}
/>
</Space>
<Space>
<Button
icon={viewMode === 'cards' ? <ListIcon /> : <GridIcon />}
onClick={() =>
setViewMode(viewMode === 'cards' ? 'list' : 'cards')
}
/>
</Space>
{lazyLoading && <Spin indicator={<LoadingOutlined />} />}
</Flex>
<Table
dataSource={filamentsData}
columns={visibleColumns}
className={styles.customTable}
pagination={false}
scroll={{ y: 'calc(100vh - 270px)' }}
rowKey='_id'
loading={{ spinning: loading, indicator: <LoadingOutlined spin /> }}
onScroll={handleScroll}
onChange={handleTableChange}
showSorterTooltip={false}
<ObjectTable
ref={tableRef}
type={'filament'}
authenticated={authenticated}
cards={viewMode === 'cards'}
visibleColumns={columnVisibility}
/>
</Flex>
<Modal
open={newFilamentOpen}
styles={{ content: { paddingBottom: '24px' } }}
footer={null}
width={700}
onCancel={() => {
setNewFilamentOpen(false)
}}
destroyOnHidden={true}
>
<NewFilament
onOk={() => {
<Modal
open={newFilamentOpen}
footer={null}
width={700}
onCancel={() => {
setNewFilamentOpen(false)
messageApi.success('New filament created successfully.')
fetchFilamentsData()
}}
reset={newFilamentOpen}
/>
</Modal>
>
<NewFilament
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 { useNavigate } from 'react-router-dom'
import {
Button,
Flex,
Space,
Modal,
Dropdown,
message,
Checkbox,
Popover,
Input,
Badge,
Typography
} from 'antd'
import { Button, Flex, Space, Modal, Dropdown, message } from 'antd'
import { AuthContext } from '../context/AuthContext'
import IdDisplay from '../common/IdDisplay'
import NewNoteType from './NoteTypes/NewNoteType'
import TimeDisplay from '../common/TimeDisplay'
import ObjectTable from '../common/ObjectTable'
import PlusIcon from '../../Icons/PlusIcon'
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 GridIcon from '../../Icons/GridIcon'
import ListIcon from '../../Icons/ListIcon'
import useViewMode from '../hooks/useViewMode'
import config from '../../../config'
import NoteTypeIcon from '../../Icons/NoteTypeIcon'
import BoolDisplay from '../common/BoolDisplay'
const { Text } = Typography
import ColumnViewButton from '../common/ColumnViewButton'
const NoteTypes = () => {
const [messageApi, contextHolder] = message.useMessage()
const navigate = useNavigate()
const [newNoteTypeOpen, setNewNoteTypeOpen] = useState(false)
const tableRef = useRef()
const { authenticated } = useContext(AuthContext)
const [viewMode, setViewMode] = useViewMode('NoteTypes')
const [viewMode, setViewMode] = useViewMode('noteType')
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 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 [columnVisibility, setColumnVisibility] =
useColumnVisibility('noteType')
const actionItems = {
items: [
@ -287,13 +53,12 @@ const NoteTypes = () => {
<Dropdown menu={actionItems}>
<Button>Actions</Button>
</Dropdown>
<Popover
content={getViewDropdownItems()}
placement='bottomLeft'
arrow={false}
>
<Button>View</Button>
</Popover>
<ColumnViewButton
type='noteType'
loading={false}
collapseState={columnVisibility}
updateCollapseState={setColumnVisibility}
/>
</Space>
<Space>
<Button
@ -306,8 +71,8 @@ const NoteTypes = () => {
</Flex>
<ObjectTable
ref={tableRef}
columns={visibleColumns}
url={`${config.backendUrl}/notetypes`}
visibleColumns={columnVisibility}
type='noteType'
authenticated={authenticated}
cards={viewMode === 'cards'}
/>

View File

@ -13,6 +13,10 @@ import AuditLogIcon from '../../../Icons/AuditLogIcon.jsx'
import EditObjectForm from '../../common/EditObjectForm'
import EditButtons from '../../common/EditButtons'
import LockIndicator from '../Filaments/LockIndicator'
import {
getModelProperties,
getPropertyValue
} from '../../../../database/ObjectModels.js'
const NoteTypeInfo = () => {
const location = useLocation()
@ -108,50 +112,11 @@ const NoteTypeInfo = () => {
loading={loading}
indicator={<LoadingOutlined />}
isEditing={isEditing}
type='notetype'
items={[
{
name: 'id',
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'
}
]}
type='noteType'
items={getModelProperties('noteType').map((prop) => ({
...prop,
value: getPropertyValue(objectData, prop.name)
}))}
/>
</InfoCollapse>

View File

@ -1,237 +1,34 @@
// src/gcodefiles.js
import React, { useState, useContext, useRef } from 'react'
import { useNavigate } from 'react-router-dom'
import {
Button,
Flex,
Space,
Modal,
Dropdown,
Typography,
Checkbox,
Popover,
Input,
message
} from 'antd'
import { DownloadOutlined } from '@ant-design/icons'
import { Button, Flex, Space, Modal, Dropdown, message } from 'antd'
import { AuthContext } from '../context/AuthContext'
import IdDisplay from '../common/IdDisplay'
import ObjectTable from '../common/ObjectTable'
import NewProduct from './Products/NewProduct'
import PartIcon from '../../Icons/PartIcon'
import InfoCircleIcon from '../../Icons/InfoCircleIcon'
import PlusIcon from '../../Icons/PlusIcon'
import ReloadIcon from '../../Icons/ReloadIcon'
import XMarkIcon from '../../Icons/XMarkIcon'
import CheckIcon from '../../Icons/CheckIcon'
import useColumnVisibility from '../hooks/useColumnVisibility'
import TimeDisplay from '../common/TimeDisplay'
import GridIcon from '../../Icons/GridIcon'
import ListIcon from '../../Icons/ListIcon'
import useViewMode from '../hooks/useViewMode'
import config from '../../../config'
const { Text } = Typography
import ColumnViewButton from '../common/ColumnViewButton'
const Parts = () => {
const [messageApi, contextHolder] = message.useMessage()
const navigate = useNavigate()
const [newProductOpen, setNewProductOpen] = useState(false)
const tableRef = useRef()
const { authenticated } = useContext(AuthContext)
const [viewMode, setViewMode] = useViewMode('Parts')
const [viewMode, setViewMode] = useViewMode('part')
// Column definitions
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 [columnVisibility, setColumnVisibility] = useColumnVisibility('part')
const actionItems = {
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 (
<>
<Flex vertical={'true'} gap='large'>
@ -293,13 +62,12 @@ const Parts = () => {
<Dropdown menu={actionItems}>
<Button>Actions</Button>
</Dropdown>
<Popover
content={getViewDropdownItems()}
placement='bottomLeft'
arrow={false}
>
<Button>View</Button>
</Popover>
<ColumnViewButton
type='part'
loading={false}
collapseState={columnVisibility}
updateCollapseState={setColumnVisibility}
/>
</Space>
<Space>
<Button
@ -312,8 +80,8 @@ const Parts = () => {
</Flex>
<ObjectTable
ref={tableRef}
columns={visibleColumns}
url={`${config.backendUrl}/parts`}
visibleColumns={columnVisibility}
type='part'
authenticated={authenticated}
cards={viewMode === 'cards'}
/>

View File

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

View File

@ -1,334 +1,20 @@
import React, { useContext, useRef } from 'react'
import { useNavigate } from 'react-router-dom'
import {
Button,
Flex,
Space,
Dropdown,
Typography,
Checkbox,
Popover,
Input
} from 'antd'
import { ExportOutlined } from '@ant-design/icons'
import { Button, Flex, Space, Dropdown } from 'antd'
import { AuthContext } from '../context/AuthContext'
import IdDisplay from '../common/IdDisplay'
import TimeDisplay from '../common/TimeDisplay'
import ObjectTable from '../common/ObjectTable'
import PersonIcon from '../../Icons/PersonIcon'
import ReloadIcon from '../../Icons/ReloadIcon'
import XMarkIcon from '../../Icons/XMarkIcon'
import CheckIcon from '../../Icons/CheckIcon'
import useColumnVisibility from '../hooks/useColumnVisibility'
import InfoCircleIcon from '../../Icons/InfoCircleIcon'
import GridIcon from '../../Icons/GridIcon'
import ListIcon from '../../Icons/ListIcon'
import useViewMode from '../hooks/useViewMode'
import config from '../../../config'
const { Link, Text } = Typography
import ColumnViewButton from '../common/ColumnViewButton'
const Users = () => {
const navigate = useNavigate()
const tableRef = useRef()
const { authenticated } = useContext(AuthContext)
const [viewMode, setViewMode] = useViewMode('Users')
const [viewMode, setViewMode] = useViewMode('user')
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 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 [columnVisibility, setColumnVisibility] = useColumnVisibility('user')
const actionItems = {
items: [
@ -352,13 +38,12 @@ const Users = () => {
<Dropdown menu={actionItems}>
<Button>Actions</Button>
</Dropdown>
<Popover
content={getViewDropdownItems()}
placement='bottomLeft'
arrow={false}
>
<Button>View</Button>
</Popover>
<ColumnViewButton
type='user'
loading={false}
collapseState={columnVisibility}
updateCollapseState={setColumnVisibility}
/>
</Space>
<Space>
<Button
@ -369,8 +54,8 @@ const Users = () => {
</Flex>
<ObjectTable
ref={tableRef}
columns={visibleColumns}
url={`${config.backendUrl}/users`}
type={'user'}
visibleColumns={columnVisibility}
authenticated={authenticated}
cards={viewMode === 'cards'}
/>

View File

@ -15,6 +15,10 @@ import AuditLogIcon from '../../../Icons/AuditLogIcon.jsx'
import EditObjectForm from '../../common/EditObjectForm'
import EditButtons from '../../common/EditButtons'
import LockIndicator from '../Filaments/LockIndicator'
import {
getModelProperties,
getPropertyValue
} from '../../../../database/ObjectModels.js'
const UserInfo = () => {
const location = useLocation()
@ -110,63 +114,10 @@ const UserInfo = () => {
indicator={<LoadingOutlined />}
isEditing={isEditing}
type='user'
items={[
{
name: '_id',
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'
}
]}
items={getModelProperties('user').map((prop) => ({
...prop,
value: getPropertyValue(objectData, prop.name)
}))}
/>
</InfoCollapse>

View File

@ -1,318 +1,24 @@
import React, { useState, useContext, useRef } from 'react'
import { useNavigate } from 'react-router-dom'
import {
Button,
Flex,
Space,
Modal,
Dropdown,
message,
Typography,
Checkbox,
Popover,
Input
} from 'antd'
import { ExportOutlined } from '@ant-design/icons'
import { Button, Flex, Space, Modal, Dropdown, message } from 'antd'
import { AuthContext } from '../context/AuthContext'
import IdDisplay from '../common/IdDisplay'
import NewVendor from './Vendors/NewVendor'
import CountryDisplay from '../common/CountryDisplay'
import TimeDisplay from '../common/TimeDisplay'
import ObjectTable from '../common/ObjectTable'
import VendorIcon from '../../Icons/VendorIcon'
import PlusIcon from '../../Icons/PlusIcon'
import ReloadIcon from '../../Icons/ReloadIcon'
import XMarkIcon from '../../Icons/XMarkIcon'
import CheckIcon from '../../Icons/CheckIcon'
import useColumnVisibility from '../hooks/useColumnVisibility'
import InfoCircleIcon from '../../Icons/InfoCircleIcon'
import GridIcon from '../../Icons/GridIcon'
import ListIcon from '../../Icons/ListIcon'
import useViewMode from '../hooks/useViewMode'
import config from '../../../config'
const { Link } = Typography
import ColumnViewButton from '../common/ColumnViewButton'
const Vendors = () => {
const [messageApi, contextHolder] = message.useMessage()
const navigate = useNavigate()
const [newVendorOpen, setNewVendorOpen] = useState(false)
const tableRef = useRef()
const { authenticated } = useContext(AuthContext)
const [viewMode, setViewMode] = useViewMode('Vendors')
const [viewMode, setViewMode] = useViewMode('vendor')
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 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 [columnVisibility, setColumnVisibility] = useColumnVisibility('vendor')
const actionItems = {
items: [
@ -346,13 +52,12 @@ const Vendors = () => {
<Dropdown menu={actionItems}>
<Button>Actions</Button>
</Dropdown>
<Popover
content={getViewDropdownItems()}
placement='bottomLeft'
arrow={false}
>
<Button>View</Button>
</Popover>
<ColumnViewButton
type='vendor'
loading={false}
collapseState={columnVisibility}
updateCollapseState={setColumnVisibility}
/>
</Space>
<Space>
<Button
@ -365,8 +70,8 @@ const Vendors = () => {
</Flex>
<ObjectTable
ref={tableRef}
columns={visibleColumns}
url={`${config.backendUrl}/vendors`}
visibleColumns={columnVisibility}
type='vendor'
authenticated={authenticated}
cards={viewMode === 'cards'}
/>

View File

@ -1,300 +1,30 @@
// src/gcodefiles.js
import React, { useState, useContext, useRef } from 'react'
import { useNavigate } from 'react-router-dom'
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 { Button, Flex, Space, Modal, Dropdown, message } from 'antd'
import { AuthContext } from '../context/AuthContext'
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 PlusIcon from '../../Icons/PlusIcon'
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 ListIcon from '../../Icons/ListIcon'
import GridIcon from '../../Icons/GridIcon'
import useViewMode from '../hooks/useViewMode'
import config from '../../../config'
const { Text } = Typography
import ColumnViewButton from '../common/ColumnViewButton'
const GCodeFiles = () => {
const [messageApi, contextHolder] = message.useMessage()
const navigate = useNavigate()
const [newGCodeFileOpen, setNewGCodeFileOpen] = useState(false)
const [showDeleted, setShowDeleted] = useState(false)
const tableRef = useRef()
const [viewMode, setViewMode] = useViewMode('GCodeFiles')
const [viewMode, setViewMode] = useViewMode('gcodeFile')
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>
)
}
// 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 [columnVisibility, setColumnVisibility] =
useColumnVisibility('gcodeFile')
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 = {
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 (
<>
<Flex vertical={'true'} gap='large'>
@ -364,13 +57,12 @@ const GCodeFiles = () => {
<Dropdown menu={actionItems}>
<Button>Actions</Button>
</Dropdown>
<Popover
content={getViewDropdownItems()}
placement='bottomLeft'
arrow={false}
>
<Button>View</Button>
</Popover>
<ColumnViewButton
type='gcodeFile'
loading={false}
collapseState={columnVisibility}
updateCollapseState={setColumnVisibility}
/>
</Space>
<Space>
<Button
@ -383,10 +75,10 @@ const GCodeFiles = () => {
</Flex>
<ObjectTable
ref={tableRef}
columns={visibleColumns}
url={`${config.backendUrl}/gcodefiles`}
type={'gcodeFile'}
authenticated={authenticated}
cards={viewMode === 'cards'}
visibleColumns={columnVisibility}
/>
</Flex>
<Modal

View File

@ -1,4 +1,4 @@
import React from 'react'
import React, { useContext } from 'react'
import { useLocation } from 'react-router-dom'
import { Space, Button, Flex, Dropdown, Card, Typography } from 'antd'
import { LoadingOutlined } from '@ant-design/icons'
@ -12,10 +12,12 @@ import ViewButton from '../../common/ViewButton'
import EditObjectForm from '../../common/EditObjectForm'
import EditButtons from '../../common/EditButtons'
import LockIndicator from '../../Management/Filaments/LockIndicator'
import ActionHandler from '../../common/ActionHandler'
import InfoCircleIcon from '../../../Icons/InfoCircleIcon.jsx'
import NoteIcon from '../../../Icons/NoteIcon.jsx'
import AuditLogIcon from '../../../Icons/AuditLogIcon.jsx'
import GCodeFileIcon from '../../../Icons/GCodeFileIcon.jsx'
import { ApiServerContext } from '../../context/ApiServerContext'
import {
getModelProperties,
getPropertyValue
@ -26,6 +28,8 @@ const { Text } = Typography
const GCodeFileInfo = () => {
const location = useLocation()
const gcodeFileId = new URLSearchParams(location.search).get('gcodeFileId')
const { handleDownloadContent } = useContext(ApiServerContext)
const [collapseState, updateCollapseState] = useCollapseState(
'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 (
<EditObjectForm
id={gcodeFileId}
type='gcodefile'
style={{ height: 'calc(var(--unit-100vh) - 155px)', minHeight: 0 }}
>
{({
loading,
isEditing,
startEditing,
cancelEditing,
handleUpdate,
formValid,
objectData,
editLoading,
lock,
fetchObject
}) => (
<Flex
gap='large'
vertical='true'
style={{ height: '100%', minHeight: 0 }}
>
<Flex justify={'space-between'}>
<Space size='middle'>
<Space size='small'>
<Dropdown
menu={{
items: [
{
label: 'Reload GCode File',
key: 'reload',
icon: <ReloadIcon />
<>
<ActionHandler actions={actions} />
<EditObjectForm
id={gcodeFileId}
type='gcodefile'
style={{ height: 'calc(var(--unit-100vh) - 155px)', minHeight: 0 }}
>
{({
loading,
isEditing,
startEditing,
cancelEditing,
handleUpdate,
formValid,
objectData,
editLoading,
lock,
fetchObject
}) => (
<Flex
gap='large'
vertical='true'
style={{ height: '100%', minHeight: 0 }}
>
<Flex justify={'space-between'}>
<Space size='middle'>
<Space size='small'>
<Dropdown
menu={{
items: [
{
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') {
fetchObject()
}
}
}}
>
<Button disabled={loading}>Actions</Button>
</Dropdown>
<ViewButton
loading={loading}
sections={[
{ key: 'info', label: 'GCode File Information' },
{ key: 'preview', label: 'GCode File Preview' },
{ key: 'notes', label: 'Notes' },
{ key: 'auditLogs', label: 'Audit Logs' }
]}
collapseState={collapseState}
updateCollapseState={updateCollapseState}
}}
>
<Button disabled={loading}>Actions</Button>
</Dropdown>
<ViewButton
loading={loading}
sections={[
{ key: 'info', label: 'GCode File Information' },
{ key: 'preview', label: 'GCode File Preview' },
{ key: 'notes', label: 'Notes' },
{ key: 'auditLogs', label: 'Audit Logs' }
]}
collapseState={collapseState}
updateCollapseState={updateCollapseState}
/>
</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>
<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>
</div>
</Flex>
)}
</EditObjectForm>
<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>
</div>
</Flex>
)}
</EditObjectForm>
</>
)
}

View File

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

View File

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

View File

@ -1,263 +1,31 @@
// src/Printers.js
import React, { useState, useContext, useRef } from 'react'
import { useNavigate } from 'react-router-dom'
import {
Button,
message,
Dropdown,
Space,
Flex,
Input,
Tag,
Modal,
Popover,
Checkbox
} from 'antd'
import { Button, message, Dropdown, Space, Flex, Modal } from 'antd'
import { AuthContext } from '../context/AuthContext'
import PrinterState from '../common/PrinterState'
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 ReloadIcon from '../../Icons/ReloadIcon'
import XMarkIcon from '../../Icons/XMarkIcon'
import CheckIcon from '../../Icons/CheckIcon'
import ObjectTable from '../common/ObjectTable'
import ColumnViewButton from '../common/ColumnViewButton'
import config from '../../../config'
import GridIcon from '../../Icons/GridIcon'
import ListIcon from '../../Icons/ListIcon'
import useViewMode from '../hooks/useViewMode'
import useColumnVisibility from '../hooks/useColumnVisibility'
const Printers = () => {
const [messageApi, contextHolder] = message.useMessage()
const { authenticated } = useContext(AuthContext)
const [newPrinterOpen, setNewPrinterOpen] = useState(false)
const navigate = useNavigate()
const tableRef = useRef()
// View mode state (cards/list), persisted in sessionStorage via custom hook
const [viewMode, setViewMode] = useViewMode('Printers')
// Column definitions
const columns = [
{
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]
)
// Column visibility state, persisted in sessionStorage via custom hook
const [columnVisibility, setColumnVisibility] = useColumnVisibility('printer')
const actionItems = {
items: [
@ -291,13 +59,12 @@ const Printers = () => {
<Dropdown menu={actionItems}>
<Button>Actions</Button>
</Dropdown>
<Popover
content={getViewDropdownItems()}
placement='bottomLeft'
arrow={false}
>
<Button>View</Button>
</Popover>
<ColumnViewButton
type='printer'
loading={false}
collapseState={columnVisibility}
updateCollapseState={setColumnVisibility}
/>
</Space>
<Space>
<Button
@ -311,10 +78,10 @@ const Printers = () => {
<ObjectTable
ref={tableRef}
columns={visibleColumns}
url={`${config.backendUrl}/printers`}
type={'printer'}
authenticated={authenticated}
cards={viewMode === 'cards'}
visibleColumns={columnVisibility}
/>
<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 PropTypes from 'prop-types'
import { Typography, Flex, Button, Tooltip } from 'antd'
import NewMailIcon from './NewMailIcon'
import NewMailIcon from '../../Icons/NewMailIcon'
// import CopyIcon from './CopyIcon'
import CopyButton from '../Dashboard/common/CopyButton'
import CopyButton from './CopyButton'
const { Text, Link } = Typography
@ -19,7 +19,9 @@ const EmailDisplay = ({ email, showCopy = true, showLink = false }) => {
</Link>
) : (
<>
<Text style={{ marginRight: 8 }}>{email}</Text>
<Text style={{ marginRight: 8 }} ellipsis>
{email}
</Text>
<Tooltip title='Email' arrow={false}>
<Button
icon={<NewMailIcon style={{ fontSize: '14px' }} />}

View File

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

View File

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

View File

@ -15,32 +15,50 @@ import {
Col,
Descriptions,
Flex,
Spin
Spin,
Button,
Input,
Space,
Tooltip
} from 'antd'
import { LoadingOutlined } from '@ant-design/icons'
import PropTypes from 'prop-types'
import { useMediaQuery } from 'react-responsive'
import axios from 'axios'
import { useContext } from 'react'
import { ApiServerContext } from '../context/ApiServerContext'
import config from '../../../config'
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')
logger.setLevel(config.logLevel)
const ObjectTable = forwardRef(
(
{
columns,
type,
url,
pageSize = 25,
scrollHeight = 'calc(var(--unit-100vh) - 270px)',
onDataChange,
authenticated,
initialPage = 1,
cards = false
cards = false,
visibleColumns = {}
},
ref
) => {
const { fetchTableData } = useContext(ApiServerContext)
const isMobile = useMediaQuery({ maxWidth: 768 })
const navigate = useNavigate()
var adjustedScrollHeight = scrollHeight
if (isMobile) {
adjustedScrollHeight = 'calc(var(--unit-100vh) - 316px)'
@ -50,8 +68,8 @@ const ObjectTable = forwardRef(
}
const [, contextHolder] = message.useMessage()
const tableRef = useRef(null)
const [filters, setFilters] = useState({})
const [sorter, setSorter] = useState({})
const [tableFilter, setTableFilter] = useState({})
const [tableSorter, setTableSorter] = useState({})
const [initialized, setInitialized] = useState(false)
// Table state
@ -59,7 +77,6 @@ const ObjectTable = forwardRef(
const [hasMore, setHasMore] = useState(true)
const [loading, setLoading] = useState(true)
const [lazyLoading, setLazyLoading] = useState(false)
const [totalPages, setTotalPages] = useState(0)
const createSkeletonData = useCallback(() => {
return Array(pageSize)
@ -71,30 +88,31 @@ const ObjectTable = forwardRef(
}, [pageSize])
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 {
const response = await axios.get(url, {
params: {
page: pageNum,
limit: pageSize,
...filters,
sort: sorter.field,
order: sorter.order
},
headers: {
Accept: 'application/json'
},
withCredentials: true
const result = await fetchTableData(type, {
page: pageNum,
limit: pageSize,
filter,
sorter,
onDataChange
})
const newData = response.data
const totalCount = parseInt(
response.headers['x-total-count'] || '0',
10
)
setTotalPages(Math.ceil(totalCount / pageSize))
setHasMore(newData.length >= pageSize)
setHasMore(result.hasMore)
setPages((prev) => {
const existingPageIndex = prev.findIndex(
@ -104,20 +122,16 @@ const ObjectTable = forwardRef(
if (existingPageIndex !== -1) {
// Update existing page
const newPages = [...prev]
newPages[existingPageIndex] = { pageNum, items: newData }
newPages[existingPageIndex] = { pageNum, items: result.data }
return newPages
}
// If page doesn't exist, return unchanged
return prev
})
if (onDataChange) {
onDataChange(newData)
}
setLoading(false)
setLazyLoading(false)
return newData
return result.data
} catch (error) {
setPages((prev) =>
prev.map((page) => ({
@ -130,7 +144,7 @@ const ObjectTable = forwardRef(
throw error
}
},
[url, pageSize, filters, sorter, onDataChange]
[url, pageSize, tableFilter, tableSorter, onDataChange, fetchTableData]
)
const loadNextPage = useCallback(() => {
@ -233,40 +247,34 @@ const ObjectTable = forwardRef(
)
}, [])
const goToPage = useCallback(
(pageNum) => {
if (pageNum > 0 && pageNum <= totalPages) {
const pagesToLoad = [pageNum - 1, pageNum, pageNum + 1].filter(
(p) => p > 0 && p <= totalPages
)
return Promise.all(pagesToLoad.map((p) => fetchData(p)))
const loadPage = useCallback(
async (pageNum, filter = null, sorter = null) => {
// Create initial page with skeletons
setPages([{ pageNum: pageNum, items: createSkeletonData() }])
const items = await fetchData(pageNum, filter, sorter)
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 () => {
// Create initial page with skeletons
setPages([{ pageNum: initialPage, items: createSkeletonData() }])
const items = await fetchData(initialPage)
if (items.length >= 25) {
setPages((prev) => [
...prev,
{ pageNum: initialPage + 1, items: createSkeletonData() }
])
await fetchData(initialPage + 1)
}
}, [initialPage, createSkeletonData, fetchData])
loadPage(initialPage)
}, [initialPage, loadPage])
useImperativeHandle(ref, () => ({
reload,
setData: (newData) => {
setPages([{ pageNum: 1, items: newData }])
},
updateData,
goToPage
updateData
}))
useEffect(() => {
@ -276,33 +284,207 @@ const ObjectTable = forwardRef(
}
}, [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 newFilters = {}
Object.entries(filters).forEach(([key, value]) => {
if (value && value.length > 0) {
newFilters[key] = value[0]
}
})
setFilters(newFilters)
setSorter({
setPages([])
setLoading(true)
loadPage(initialPage, newFilters, {
field: sorter.field,
order: sorter.order
})
setPages([])
fetchData(1)
}
const columnsWithSkeleton = columns.map((col) => ({
...col,
render: (text, record) => {
if (record.isSkeleton) {
return (
<Skeleton.Input active size='small' style={{ width: '100%' }} />
)
}
return col.render ? col.render(text, record) : text
const modelProperties = getModelProperties(type)
const model = getModelByName(type)
// Table columns from model properties
const columnsWithSkeleton = [
{
title: model.icon,
key: 'icon',
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
const tableData = pages.flatMap((page) => page.items)
@ -355,72 +537,31 @@ const ObjectTable = forwardRef(
style={{ overflowY: 'auto', maxHeight: adjustedScrollHeight }}
ref={cardsContainerRef}
>
{tableData.map((record) => {
// Special case for columns[0] if needed
let icon = null
if (columns[0].key === 'icon' && columns[0].render) {
const renderedIcon = columns[0].render()
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}
{tableData.map((record) => (
<Col xs={24} sm={12} md={12} lg={8} xl={6} xxl={6} key={record._id}>
<Card
style={{ width: '100%', overflow: 'hidden' }}
loading={record.isSkeleton}
>
<Card
style={{ width: '100%', overflow: 'hidden' }}
loading={record.isSkeleton}
>
<Flex align={'center'} vertical gap={'middle'}>
{icon}
<Descriptions column={1} size='small' bordered={false}>
{columns
.filter(
(col) => col.key !== 'icon' && col.key !== 'actions'
)
.map((col) => {
let value
if (col.render && col.dataIndex) {
value = col.render(record[col.dataIndex], record)
} else if (col.render && !col.dataIndex) {
value = col.render(record)
} else {
value = String(record[col.dataIndex] ?? '')
}
return (
<Descriptions.Item
label={col.title}
key={col.key || col.dataIndex}
>
{value}
</Descriptions.Item>
)
})}
</Descriptions>
{actions}
</Flex>
</Card>
</Col>
)
})}
<Flex align={'center'} vertical gap={'middle'}>
<Descriptions column={1} size='small' bordered={false}>
{modelProperties.map((prop) => (
<Descriptions.Item label={prop.label} key={prop.name}>
<ObjectProperty
{...prop}
longId={false}
type={prop.type}
objectType={prop.objectType}
value={getPropertyValue(record, prop.name)}
isEditing={false}
/>
</Descriptions.Item>
))}
</Descriptions>
</Flex>
</Card>
</Col>
))}
</Row>
)
}
@ -456,7 +597,7 @@ const ObjectTable = forwardRef(
ObjectTable.displayName = 'ObjectTable'
ObjectTable.propTypes = {
columns: PropTypes.arrayOf(PropTypes.object).isRequired,
type: PropTypes.string.isRequired,
url: PropTypes.string.isRequired,
pageSize: PropTypes.number,
scrollHeight: PropTypes.string,
@ -464,7 +605,8 @@ ObjectTable.propTypes = {
authenticated: PropTypes.bool.isRequired,
initialPage: PropTypes.number,
cards: PropTypes.bool,
cardRenderer: PropTypes.func
cardRenderer: PropTypes.func,
visibleColumns: PropTypes.object
}
export default ObjectTable

View File

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

View File

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

View File

@ -224,7 +224,7 @@ const ApiServerProvider = ({ children }) => {
// Update filament information
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)
try {
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 (
<ApiServerContext.Provider
value={{
@ -263,8 +356,10 @@ const ApiServerProvider = ({ children }) => {
onUpdateEvent,
offUpdateEvent,
fetchObjectInfo,
fetchTableData,
fetchLoading,
showError
showError,
handleDownloadContent
}}
>
{contextHolder}

View File

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

View File

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

View File

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

View File

@ -1,16 +1,41 @@
import FilamentIcon from '../../components/Icons/FilamentIcon'
import InfoCircleIcon from '../../components/Icons/InfoCircleIcon'
export const Filament = {
name: 'filament',
label: 'Filament',
prefix: 'FIL',
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: [
{
name: '_id',
label: 'ID',
columnFixed: 'left',
type: 'id',
objectType: 'filament',
showCopy: true
@ -18,28 +43,25 @@ export const Filament = {
{
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: 'vendor',
label: 'Vendor',
required: true,
type: 'object',
objectType: 'vendor'
@ -55,34 +77,35 @@ export const Filament = {
{
name: 'type',
label: 'Material',
required: true,
columnWidth: 150,
type: 'material'
},
{
name: 'cost',
label: 'Cost',
columnWidth: 150,
required: true,
type: 'currency'
},
{
name: 'color',
label: 'Color',
columnWidth: 150,
required: true,
type: 'color'
},
{
name: 'diameter',
label: 'Diameter',
columnWidth: 150,
required: true,
type: 'mm'
},
{
name: 'density',
label: 'Density',
columnWidth: 150,
required: true,
type: 'density'
},

View File

@ -1,9 +1,21 @@
import FilamentStockIcon from '../../components/Icons/FilamentStockIcon'
import InfoCircleIcon from '../../components/Icons/InfoCircleIcon'
export const FilamentStock = {
name: 'filamentstock',
label: 'Filament Stock',
prefix: 'FLS',
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}`
}

View File

@ -1,17 +1,49 @@
import GCodeFileIcon from '../../components/Icons/GCodeFileIcon'
import InfoCircleIcon from '../../components/Icons/InfoCircleIcon'
export const GCodeFile = {
name: 'gcodeFile',
label: 'GCode File',
prefix: 'GCF',
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: [
{
name: '_id',
label: 'ID',
type: 'id',
objectType: 'gcodefile',
objectType: 'gcodeFile',
columnFixed: 'left',
value: null,
showCopy: true
},
@ -25,6 +57,7 @@ export const GCodeFile = {
{
name: 'name',
label: 'Name',
columnFixed: 'left',
type: 'text',
value: null,
required: true
@ -61,6 +94,7 @@ export const GCodeFile = {
{
name: 'gcodeFileInfo.sparseInfillDensity',
label: 'Infill Density',
columnWidth: 150,
type: 'number',
readOnly: true
},
@ -86,14 +120,16 @@ export const GCodeFile = {
},
{
name: 'gcodeFileInfo.nozzleTemperature',
label: 'Hotend Temperature',
label: 'Hotend Temp',
columnWidth: 150,
value: null,
type: 'number',
readOnly: true
},
{
name: 'gcodeFileInfo.hotPlateTemp',
label: 'Bed Temperature',
label: 'Bed Temp',
columnWidth: 150,
value: null,
type: 'number',
readOnly: true

View File

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

View File

@ -1,16 +1,37 @@
import JobIcon from '../../components/Icons/JobIcon'
import InfoCircleIcon from '../../components/Icons/InfoCircleIcon'
export const Job = {
name: 'job',
label: 'Job',
prefix: 'JOB',
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: [
{
name: '_id',
label: 'ID',
type: 'id',
columnFixed: 'left',
objectType: 'job',
showCopy: true
},
@ -23,12 +44,14 @@ export const Job = {
showProgress: true,
showId: false,
showQuantity: false,
columnWidth: 150,
readOnly: true
},
{
name: 'gcodeFile',
label: 'GCode File',
type: 'object',
columnFixed: 'left',
objectType: 'gcodeFile',
readOnly: true
},
@ -43,6 +66,7 @@ export const Job = {
name: 'quantity',
label: 'Quantity',
type: 'number',
columnWidth: 125,
readOnly: true
},
{

View File

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

View File

@ -1,9 +1,59 @@
import NoteTypeIcon from '../../components/Icons/NoteTypeIcon'
import InfoCircleIcon from '../../components/Icons/InfoCircleIcon'
export const NoteType = {
name: 'notetype',
name: 'noteType',
label: 'Note Type',
prefix: 'NTY',
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 InfoCircleIcon from '../../components/Icons/InfoCircleIcon'
export const PartStock = {
name: 'partstock',
label: 'Part Stock',
prefix: 'PTS',
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}`
}

View File

@ -1,11 +1,25 @@
import PrinterIcon from '../../components/Icons/PrinterIcon'
import InfoCircleIcon from '../../components/Icons/InfoCircleIcon'
export const Printer = {
name: 'printer',
label: 'Printer',
prefix: 'PRN',
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}`,
columns: ['name', '_id', 'state', 'tags', 'connectedAt'],
filters: ['name', '_id', 'state', 'tags'],
sorters: ['name', 'state', 'connectedAt', '_id'],
properties: [
{
name: '_id',
@ -24,7 +38,9 @@ export const Printer = {
name: 'name',
label: 'Name',
required: true,
type: 'text'
type: 'text',
columnWidth: 200,
columnFixed: 'left'
},
{
name: 'state',

View File

@ -1,9 +1,47 @@
import ProductIcon from '../../components/Icons/ProductIcon'
import InfoCircleIcon from '../../components/Icons/InfoCircleIcon'
export const Product = {
name: 'product',
label: 'Product',
prefix: 'PRD',
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 InfoCircleIcon from '../../components/Icons/InfoCircleIcon'
export const ProductStock = {
name: 'productstock',
label: 'Product Stock',
prefix: 'PDS',
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}`
}

View File

@ -1,9 +1,20 @@
import FilamentIcon from '../../components/Icons/FilamentIcon'
import InfoCircleIcon from '../../components/Icons/InfoCircleIcon'
export const Spool = {
name: 'spool',
label: 'Spool',
prefix: 'SPL',
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}`
}

View File

@ -1,9 +1,20 @@
import StockAuditIcon from '../../components/Icons/StockAuditIcon'
import InfoCircleIcon from '../../components/Icons/InfoCircleIcon'
export const StockAudit = {
name: 'stockaudit',
label: 'Stock Audit',
prefix: 'SAU',
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}`
}

View File

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

View File

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

View File

@ -1,9 +1,75 @@
import PersonIcon from '../../components/Icons/PersonIcon'
import InfoCircleIcon from '../../components/Icons/InfoCircleIcon'
export const User = {
name: 'user',
label: 'User',
prefix: 'USR',
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 InfoCircleIcon from '../../components/Icons/InfoCircleIcon'
export const Vendor = {
name: 'vendor',
label: 'Vendor',
prefix: 'VEN',
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}`,
columns: ['name', '_id', 'country', 'email', 'createdAt'],
filters: ['name', '_id', 'country', 'email'],
sorters: ['name', 'country', 'email', 'createdAt', '_id'],
properties: [
{
name: '_id',
label: 'ID',
columnFixed: 'left',
type: 'id',
objectType: 'vendor',
showCopy: true
@ -18,13 +32,13 @@ export const Vendor = {
{
name: 'createdAt',
label: 'Created At',
type: 'dateTime',
readOnly: true
},
{
name: 'name',
label: 'Name',
columnFixed: 'left',
required: true,
type: 'text'
},
@ -51,6 +65,7 @@ export const Vendor = {
{
name: 'email',
label: 'Email',
columnWidth: 300,
type: 'email',
readOnly: false,
required: false
@ -65,6 +80,7 @@ export const Vendor = {
{
name: 'website',
label: 'Website',
columnWidth: 300,
type: 'url',
readOnly: false,
required: false