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:
parent
fe85250838
commit
fdc862d16c
@ -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>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
@ -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'}
|
||||
/>
|
||||
|
||||
@ -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>
|
||||
|
||||
|
||||
@ -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'}
|
||||
/>
|
||||
|
||||
@ -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'}
|
||||
/>
|
||||
|
||||
@ -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'}
|
||||
/>
|
||||
|
||||
@ -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>
|
||||
|
||||
|
||||
@ -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'}
|
||||
/>
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
@ -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'}
|
||||
/>
|
||||
|
||||
@ -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>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
@ -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
|
||||
|
||||
57
src/components/Dashboard/common/ActionHandler.jsx
Normal file
57
src/components/Dashboard/common/ActionHandler.jsx
Normal 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
|
||||
50
src/components/Dashboard/common/ColumnViewButton.jsx
Normal file
50
src/components/Dashboard/common/ColumnViewButton.jsx
Normal 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
|
||||
@ -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' }} />}
|
||||
@ -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}
|
||||
|
||||
@ -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>
|
||||
}
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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}
|
||||
|
||||
@ -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) => ({
|
||||
|
||||
@ -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,
|
||||
|
||||
@ -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: () => `#`
|
||||
}
|
||||
|
||||
@ -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'
|
||||
},
|
||||
|
||||
@ -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}`
|
||||
}
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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: () => `#`
|
||||
}
|
||||
|
||||
@ -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
|
||||
},
|
||||
{
|
||||
|
||||
@ -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: () => `#`
|
||||
}
|
||||
|
||||
@ -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'
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
64
src/database/models/Part.js
Normal file
64
src/database/models/Part.js
Normal 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'
|
||||
}
|
||||
]
|
||||
}
|
||||
@ -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}`
|
||||
}
|
||||
|
||||
@ -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',
|
||||
|
||||
@ -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
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
@ -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}`
|
||||
}
|
||||
|
||||
@ -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}`
|
||||
}
|
||||
|
||||
@ -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}`
|
||||
}
|
||||
|
||||
@ -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: () => `#`
|
||||
}
|
||||
|
||||
@ -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: () => `#`
|
||||
}
|
||||
|
||||
@ -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'
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
@ -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
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user