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
|
// src/filaments.js
|
||||||
|
|
||||||
import React, { useState, useContext, useCallback, useEffect } from 'react'
|
import React, { useContext, useRef, useState } from 'react'
|
||||||
import { useNavigate } from 'react-router-dom'
|
import { Button, Flex, Space, Modal, message, Dropdown } from 'antd'
|
||||||
import axios from 'axios'
|
|
||||||
import {
|
|
||||||
Table,
|
|
||||||
Badge,
|
|
||||||
Button,
|
|
||||||
Flex,
|
|
||||||
Space,
|
|
||||||
Modal,
|
|
||||||
message,
|
|
||||||
Dropdown,
|
|
||||||
Typography,
|
|
||||||
Checkbox,
|
|
||||||
Popover,
|
|
||||||
Input,
|
|
||||||
Spin
|
|
||||||
} from 'antd'
|
|
||||||
import { createStyles } from 'antd-style'
|
|
||||||
import { LoadingOutlined } from '@ant-design/icons'
|
|
||||||
|
|
||||||
import { AuthContext } from '../context/AuthContext'
|
import { AuthContext } from '../context/AuthContext'
|
||||||
import NewFilament from './Filaments/NewFilament'
|
import NewFilament from './Filaments/NewFilament'
|
||||||
import IdDisplay from '../common/IdDisplay'
|
|
||||||
import FilamentIcon from '../../Icons/FilamentIcon'
|
import useColumnVisibility from '../hooks/useColumnVisibility'
|
||||||
import InfoCircleIcon from '../../Icons/InfoCircleIcon'
|
import ColumnViewButton from '../common/ColumnViewButton'
|
||||||
|
import ObjectTable from '../common/ObjectTable'
|
||||||
import PlusIcon from '../../Icons/PlusIcon'
|
import PlusIcon from '../../Icons/PlusIcon'
|
||||||
import ReloadIcon from '../../Icons/ReloadIcon'
|
import ReloadIcon from '../../Icons/ReloadIcon'
|
||||||
import XMarkIcon from '../../Icons/XMarkIcon'
|
import ListIcon from '../../Icons/ListIcon'
|
||||||
import CheckIcon from '../../Icons/CheckIcon'
|
import GridIcon from '../../Icons/GridIcon'
|
||||||
import useColumnVisibility from '../hooks/useColumnVisibility'
|
import useViewMode from '../hooks/useViewMode'
|
||||||
import TimeDisplay from '../common/TimeDisplay'
|
|
||||||
|
|
||||||
import config from '../../../config'
|
|
||||||
|
|
||||||
const { Text } = Typography
|
|
||||||
|
|
||||||
const useStyle = createStyles(({ css, token }) => {
|
|
||||||
const { antCls } = token
|
|
||||||
return {
|
|
||||||
customTable: css`
|
|
||||||
${antCls}-table {
|
|
||||||
${antCls}-table-container {
|
|
||||||
${antCls}-table-body,
|
|
||||||
${antCls}-table-content {
|
|
||||||
scrollbar-width: thin;
|
|
||||||
scrollbar-color: #eaeaea transparent;
|
|
||||||
scrollbar-gutter: stable;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
`
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
const Filaments = () => {
|
const Filaments = () => {
|
||||||
const [messageApi, contextHolder] = message.useMessage()
|
const [messageApi, contextHolder] = message.useMessage()
|
||||||
const navigate = useNavigate()
|
|
||||||
const { styles } = useStyle()
|
|
||||||
|
|
||||||
const [filamentsData, setFilamentsData] = useState([])
|
|
||||||
const [newFilamentOpen, setNewFilamentOpen] = useState(false)
|
const [newFilamentOpen, setNewFilamentOpen] = useState(false)
|
||||||
const [loading, setLoading] = useState(true)
|
const tableRef = useRef()
|
||||||
|
|
||||||
|
// View mode state (cards/list), persisted in sessionStorage via custom hook
|
||||||
|
const [viewMode, setViewMode] = useViewMode('filament')
|
||||||
|
|
||||||
|
const [columnVisibility, setColumnVisibility] =
|
||||||
|
useColumnVisibility('filament')
|
||||||
|
|
||||||
const { authenticated } = useContext(AuthContext)
|
const { authenticated } = useContext(AuthContext)
|
||||||
|
|
||||||
const [page, setPage] = useState(1)
|
|
||||||
const [hasMore, setHasMore] = useState(true)
|
|
||||||
const [lazyLoading, setLazyLoading] = useState(false)
|
|
||||||
const [filters, setFilters] = useState({})
|
|
||||||
const [sorter, setSorter] = useState({})
|
|
||||||
|
|
||||||
const fetchFilamentsData = useCallback(
|
|
||||||
async (pageNum = 1, append = false) => {
|
|
||||||
try {
|
|
||||||
const response = await axios.get(`${config.backendUrl}/filaments`, {
|
|
||||||
params: {
|
|
||||||
page: pageNum,
|
|
||||||
limit: 25,
|
|
||||||
...filters,
|
|
||||||
sort: sorter.field,
|
|
||||||
order: sorter.order
|
|
||||||
},
|
|
||||||
headers: {
|
|
||||||
Accept: 'application/json'
|
|
||||||
},
|
|
||||||
withCredentials: true
|
|
||||||
})
|
|
||||||
|
|
||||||
const newData = response.data
|
|
||||||
setHasMore(newData.length === 25)
|
|
||||||
|
|
||||||
if (append) {
|
|
||||||
setFilamentsData((prev) => [...prev, ...newData])
|
|
||||||
} else {
|
|
||||||
setFilamentsData(newData)
|
|
||||||
}
|
|
||||||
|
|
||||||
setLoading(false)
|
|
||||||
setLazyLoading(false)
|
|
||||||
} catch (err) {
|
|
||||||
messageApi.info(err)
|
|
||||||
setLoading(false)
|
|
||||||
setLazyLoading(false)
|
|
||||||
}
|
|
||||||
},
|
|
||||||
[messageApi, filters, sorter]
|
|
||||||
)
|
|
||||||
|
|
||||||
const handleScroll = useCallback(
|
|
||||||
(e) => {
|
|
||||||
const { target } = e
|
|
||||||
const scrollHeight = target.scrollHeight
|
|
||||||
const scrollTop = target.scrollTop
|
|
||||||
const clientHeight = target.clientHeight
|
|
||||||
|
|
||||||
if (
|
|
||||||
scrollHeight - scrollTop - clientHeight < 100 &&
|
|
||||||
!lazyLoading &&
|
|
||||||
hasMore
|
|
||||||
) {
|
|
||||||
setLazyLoading(true)
|
|
||||||
const nextPage = page + 1
|
|
||||||
setPage(nextPage)
|
|
||||||
fetchFilamentsData(nextPage, true)
|
|
||||||
}
|
|
||||||
},
|
|
||||||
[page, lazyLoading, hasMore, fetchFilamentsData]
|
|
||||||
)
|
|
||||||
|
|
||||||
const getFilterDropdown = ({
|
|
||||||
setSelectedKeys,
|
|
||||||
selectedKeys,
|
|
||||||
confirm,
|
|
||||||
clearFilters,
|
|
||||||
propertyName
|
|
||||||
}) => {
|
|
||||||
return (
|
|
||||||
<div style={{ padding: 8 }}>
|
|
||||||
<Space.Compact>
|
|
||||||
<Input
|
|
||||||
placeholder={'Search ' + propertyName}
|
|
||||||
value={selectedKeys[0]}
|
|
||||||
onChange={(e) =>
|
|
||||||
setSelectedKeys(e.target.value ? [e.target.value] : [])
|
|
||||||
}
|
|
||||||
onPressEnter={() => confirm()}
|
|
||||||
style={{ width: 200, display: 'block' }}
|
|
||||||
/>
|
|
||||||
<Button
|
|
||||||
onClick={() => {
|
|
||||||
clearFilters()
|
|
||||||
confirm()
|
|
||||||
}}
|
|
||||||
icon={<XMarkIcon />}
|
|
||||||
/>
|
|
||||||
<Button
|
|
||||||
type='primary'
|
|
||||||
onClick={() => confirm()}
|
|
||||||
icon={<CheckIcon />}
|
|
||||||
/>
|
|
||||||
</Space.Compact>
|
|
||||||
</div>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
const getViewDropdownItems = () => {
|
|
||||||
const columnItems = columns
|
|
||||||
.filter((col) => col.key && col.title !== '')
|
|
||||||
.map((col) => (
|
|
||||||
<Checkbox
|
|
||||||
checked={columnVisibility[col.key]}
|
|
||||||
key={col.key}
|
|
||||||
onChange={(e) => {
|
|
||||||
updateColumnVisibility(col.key, e.target.checked)
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
{col.title}
|
|
||||||
</Checkbox>
|
|
||||||
))
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Flex vertical>
|
|
||||||
<Flex vertical gap='middle' style={{ margin: '4px 8px' }}>
|
|
||||||
{columnItems}
|
|
||||||
</Flex>
|
|
||||||
</Flex>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
const handleTableChange = (pagination, filters, sorter) => {
|
|
||||||
const newFilters = {}
|
|
||||||
Object.entries(filters).forEach(([key, value]) => {
|
|
||||||
if (value && value.length > 0) {
|
|
||||||
newFilters[key] = value[0]
|
|
||||||
}
|
|
||||||
})
|
|
||||||
setPage(1)
|
|
||||||
setFilters(newFilters)
|
|
||||||
setSorter({
|
|
||||||
field: sorter.field,
|
|
||||||
order: sorter.order
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
const actionItems = {
|
const actionItems = {
|
||||||
items: [
|
items: [
|
||||||
{
|
{
|
||||||
@ -221,291 +44,65 @@ const Filaments = () => {
|
|||||||
],
|
],
|
||||||
onClick: ({ key }) => {
|
onClick: ({ key }) => {
|
||||||
if (key === 'reloadList') {
|
if (key === 'reloadList') {
|
||||||
fetchFilamentsData()
|
tableRef.current?.reload()
|
||||||
} else if (key === 'newFilament') {
|
} else if (key === 'newFilament') {
|
||||||
setNewFilamentOpen(true)
|
setNewFilamentOpen(true)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const getFilamentActionItems = (id) => {
|
|
||||||
return {
|
|
||||||
items: [
|
|
||||||
{
|
|
||||||
label: 'Info',
|
|
||||||
key: 'info',
|
|
||||||
icon: <InfoCircleIcon />
|
|
||||||
}
|
|
||||||
],
|
|
||||||
onClick: ({ key }) => {
|
|
||||||
if (key === 'info') {
|
|
||||||
navigate(`/dashboard/management/filaments/info?filamentId=${id}`)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Column definitions
|
|
||||||
const columns = [
|
|
||||||
{
|
|
||||||
title: '',
|
|
||||||
dataIndex: '',
|
|
||||||
key: 'icon',
|
|
||||||
width: 40,
|
|
||||||
fixed: 'left',
|
|
||||||
render: () => <FilamentIcon></FilamentIcon>
|
|
||||||
},
|
|
||||||
{
|
|
||||||
title: 'Name',
|
|
||||||
dataIndex: 'name',
|
|
||||||
key: 'name',
|
|
||||||
width: 200,
|
|
||||||
fixed: 'left',
|
|
||||||
filterDropdown: ({
|
|
||||||
setSelectedKeys,
|
|
||||||
selectedKeys,
|
|
||||||
confirm,
|
|
||||||
clearFilters
|
|
||||||
}) =>
|
|
||||||
getFilterDropdown({
|
|
||||||
setSelectedKeys,
|
|
||||||
selectedKeys,
|
|
||||||
confirm,
|
|
||||||
clearFilters,
|
|
||||||
propertyName: 'name'
|
|
||||||
}),
|
|
||||||
onFilter: (value, record) =>
|
|
||||||
record.name.toLowerCase().includes(value.toLowerCase()),
|
|
||||||
sorter: true
|
|
||||||
},
|
|
||||||
{
|
|
||||||
title: 'ID',
|
|
||||||
dataIndex: '_id',
|
|
||||||
key: 'id',
|
|
||||||
width: 180,
|
|
||||||
render: (text) => (
|
|
||||||
<IdDisplay id={text} type={'filament'} longId={false} />
|
|
||||||
),
|
|
||||||
filterDropdown: ({
|
|
||||||
setSelectedKeys,
|
|
||||||
selectedKeys,
|
|
||||||
confirm,
|
|
||||||
clearFilters
|
|
||||||
}) =>
|
|
||||||
getFilterDropdown({
|
|
||||||
setSelectedKeys,
|
|
||||||
selectedKeys,
|
|
||||||
confirm,
|
|
||||||
clearFilters,
|
|
||||||
propertyName: 'ID'
|
|
||||||
}),
|
|
||||||
onFilter: (value, record) =>
|
|
||||||
record._id.toLowerCase().includes(value.toLowerCase()),
|
|
||||||
sorter: true
|
|
||||||
},
|
|
||||||
{
|
|
||||||
title: 'Vendor',
|
|
||||||
dataIndex: 'vendor',
|
|
||||||
key: 'vendor',
|
|
||||||
width: 200,
|
|
||||||
render: (vendor) => {
|
|
||||||
return vendor.name
|
|
||||||
},
|
|
||||||
filterDropdown: ({
|
|
||||||
setSelectedKeys,
|
|
||||||
selectedKeys,
|
|
||||||
confirm,
|
|
||||||
clearFilters
|
|
||||||
}) =>
|
|
||||||
getFilterDropdown({
|
|
||||||
setSelectedKeys,
|
|
||||||
selectedKeys,
|
|
||||||
confirm,
|
|
||||||
clearFilters,
|
|
||||||
propertyName: 'vendor'
|
|
||||||
}),
|
|
||||||
onFilter: (value, record) =>
|
|
||||||
record.vendor.name.toLowerCase().includes(value.toLowerCase()),
|
|
||||||
sorter: true
|
|
||||||
},
|
|
||||||
{
|
|
||||||
title: 'Material',
|
|
||||||
dataIndex: 'type',
|
|
||||||
width: 150,
|
|
||||||
key: 'material',
|
|
||||||
filterDropdown: ({
|
|
||||||
setSelectedKeys,
|
|
||||||
selectedKeys,
|
|
||||||
confirm,
|
|
||||||
clearFilters
|
|
||||||
}) =>
|
|
||||||
getFilterDropdown({
|
|
||||||
setSelectedKeys,
|
|
||||||
selectedKeys,
|
|
||||||
confirm,
|
|
||||||
clearFilters,
|
|
||||||
propertyName: 'material'
|
|
||||||
}),
|
|
||||||
onFilter: (value, record) =>
|
|
||||||
record.type.toLowerCase().includes(value.toLowerCase()),
|
|
||||||
sorter: true
|
|
||||||
},
|
|
||||||
{
|
|
||||||
title: 'Cost',
|
|
||||||
dataIndex: 'cost',
|
|
||||||
width: 120,
|
|
||||||
key: 'cost',
|
|
||||||
render: (cost) => {
|
|
||||||
return <Text ellipsis>{'£' + cost + ' per kg'}</Text>
|
|
||||||
},
|
|
||||||
sorter: true
|
|
||||||
},
|
|
||||||
{
|
|
||||||
title: 'Colour',
|
|
||||||
dataIndex: 'color',
|
|
||||||
key: 'color',
|
|
||||||
width: 120,
|
|
||||||
render: (color) => {
|
|
||||||
return <Badge color={color} text={color} />
|
|
||||||
},
|
|
||||||
filterDropdown: ({
|
|
||||||
setSelectedKeys,
|
|
||||||
selectedKeys,
|
|
||||||
confirm,
|
|
||||||
clearFilters
|
|
||||||
}) =>
|
|
||||||
getFilterDropdown({
|
|
||||||
setSelectedKeys,
|
|
||||||
selectedKeys,
|
|
||||||
confirm,
|
|
||||||
clearFilters,
|
|
||||||
propertyName: 'color'
|
|
||||||
}),
|
|
||||||
onFilter: (value, record) =>
|
|
||||||
record.color.toLowerCase().includes(value.toLowerCase()),
|
|
||||||
sorter: true
|
|
||||||
},
|
|
||||||
{
|
|
||||||
title: 'Created At',
|
|
||||||
dataIndex: 'createdAt',
|
|
||||||
key: 'createdAt',
|
|
||||||
width: 180,
|
|
||||||
render: (createdAt) => {
|
|
||||||
if (createdAt) {
|
|
||||||
return <TimeDisplay dateTime={createdAt} />
|
|
||||||
} else {
|
|
||||||
return 'n/a'
|
|
||||||
}
|
|
||||||
},
|
|
||||||
sorter: true,
|
|
||||||
defaultSortOrder: 'descend'
|
|
||||||
},
|
|
||||||
{
|
|
||||||
title: 'Updated At',
|
|
||||||
dataIndex: 'updatedAt',
|
|
||||||
key: 'updatedAt',
|
|
||||||
width: 180,
|
|
||||||
render: (updatedAt) => {
|
|
||||||
if (updatedAt) {
|
|
||||||
return <TimeDisplay dateTime={updatedAt} />
|
|
||||||
} else {
|
|
||||||
return 'n/a'
|
|
||||||
}
|
|
||||||
},
|
|
||||||
sorter: true,
|
|
||||||
defaultSortOrder: 'descend'
|
|
||||||
},
|
|
||||||
{
|
|
||||||
title: 'Actions',
|
|
||||||
key: 'actions',
|
|
||||||
fixed: 'right',
|
|
||||||
width: 150,
|
|
||||||
render: (text, record) => {
|
|
||||||
return (
|
|
||||||
<Space gap='small'>
|
|
||||||
<Button
|
|
||||||
icon={<InfoCircleIcon />}
|
|
||||||
onClick={() =>
|
|
||||||
navigate(
|
|
||||||
`/dashboard/management/filaments/info?filamentId=${record._id}`
|
|
||||||
)
|
|
||||||
}
|
|
||||||
/>
|
|
||||||
<Dropdown menu={getFilamentActionItems(record._id)}>
|
|
||||||
<Button>Actions</Button>
|
|
||||||
</Dropdown>
|
|
||||||
</Space>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
]
|
|
||||||
|
|
||||||
const [columnVisibility, updateColumnVisibility] = useColumnVisibility(
|
|
||||||
'Filaments',
|
|
||||||
columns
|
|
||||||
)
|
|
||||||
|
|
||||||
const visibleColumns = columns.filter(
|
|
||||||
(col) => !col.key || columnVisibility[col.key]
|
|
||||||
)
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
if (authenticated) {
|
|
||||||
fetchFilamentsData()
|
|
||||||
}
|
|
||||||
}, [authenticated, fetchFilamentsData])
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<Flex vertical={'true'} gap='large'>
|
<Flex vertical={'true'} gap='large'>
|
||||||
{contextHolder}
|
{contextHolder}
|
||||||
<Flex justify={'space-between'}>
|
<Flex justify={'space-between'}>
|
||||||
<Space size='small'>
|
<Space>
|
||||||
<Dropdown menu={actionItems}>
|
<Dropdown menu={actionItems}>
|
||||||
<Button>Actions</Button>
|
<Button>Actions</Button>
|
||||||
</Dropdown>
|
</Dropdown>
|
||||||
<Popover
|
<ColumnViewButton
|
||||||
content={getViewDropdownItems()}
|
type='filament'
|
||||||
placement='bottomLeft'
|
loading={false}
|
||||||
arrow={false}
|
collapseState={columnVisibility}
|
||||||
>
|
updateCollapseState={setColumnVisibility}
|
||||||
<Button>View</Button>
|
|
||||||
</Popover>
|
|
||||||
</Space>
|
|
||||||
{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}
|
|
||||||
/>
|
/>
|
||||||
|
</Space>
|
||||||
|
<Space>
|
||||||
|
<Button
|
||||||
|
icon={viewMode === 'cards' ? <ListIcon /> : <GridIcon />}
|
||||||
|
onClick={() =>
|
||||||
|
setViewMode(viewMode === 'cards' ? 'list' : 'cards')
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
</Space>
|
||||||
</Flex>
|
</Flex>
|
||||||
|
|
||||||
|
<ObjectTable
|
||||||
|
ref={tableRef}
|
||||||
|
type={'filament'}
|
||||||
|
authenticated={authenticated}
|
||||||
|
cards={viewMode === 'cards'}
|
||||||
|
visibleColumns={columnVisibility}
|
||||||
|
/>
|
||||||
|
|
||||||
<Modal
|
<Modal
|
||||||
open={newFilamentOpen}
|
open={newFilamentOpen}
|
||||||
styles={{ content: { paddingBottom: '24px' } }}
|
|
||||||
footer={null}
|
footer={null}
|
||||||
width={700}
|
width={700}
|
||||||
onCancel={() => {
|
onCancel={() => {
|
||||||
setNewFilamentOpen(false)
|
setNewFilamentOpen(false)
|
||||||
}}
|
}}
|
||||||
destroyOnHidden={true}
|
|
||||||
>
|
>
|
||||||
<NewFilament
|
<NewFilament
|
||||||
onOk={() => {
|
onOk={() => {
|
||||||
setNewFilamentOpen(false)
|
setNewFilamentOpen(false)
|
||||||
messageApi.success('New filament created successfully.')
|
messageApi.success('New filament added successfully.')
|
||||||
fetchFilamentsData()
|
tableRef.current?.reload()
|
||||||
}}
|
}}
|
||||||
reset={newFilamentOpen}
|
reset={newFilamentOpen}
|
||||||
/>
|
/>
|
||||||
</Modal>
|
</Modal>
|
||||||
|
</Flex>
|
||||||
</>
|
</>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,259 +1,25 @@
|
|||||||
import React, { useState, useContext, useRef } from 'react'
|
import React, { useState, useContext, useRef } from 'react'
|
||||||
import { useNavigate } from 'react-router-dom'
|
import { Button, Flex, Space, Modal, Dropdown, message } from 'antd'
|
||||||
import {
|
|
||||||
Button,
|
|
||||||
Flex,
|
|
||||||
Space,
|
|
||||||
Modal,
|
|
||||||
Dropdown,
|
|
||||||
message,
|
|
||||||
Checkbox,
|
|
||||||
Popover,
|
|
||||||
Input,
|
|
||||||
Badge,
|
|
||||||
Typography
|
|
||||||
} from 'antd'
|
|
||||||
import { AuthContext } from '../context/AuthContext'
|
import { AuthContext } from '../context/AuthContext'
|
||||||
import IdDisplay from '../common/IdDisplay'
|
|
||||||
import NewNoteType from './NoteTypes/NewNoteType'
|
import NewNoteType from './NoteTypes/NewNoteType'
|
||||||
import TimeDisplay from '../common/TimeDisplay'
|
|
||||||
import ObjectTable from '../common/ObjectTable'
|
import ObjectTable from '../common/ObjectTable'
|
||||||
import PlusIcon from '../../Icons/PlusIcon'
|
import PlusIcon from '../../Icons/PlusIcon'
|
||||||
import ReloadIcon from '../../Icons/ReloadIcon'
|
import ReloadIcon from '../../Icons/ReloadIcon'
|
||||||
import XMarkIcon from '../../Icons/XMarkIcon'
|
|
||||||
import CheckIcon from '../../Icons/CheckIcon'
|
|
||||||
import InfoCircleIcon from '../../Icons/InfoCircleIcon'
|
|
||||||
import useColumnVisibility from '../hooks/useColumnVisibility'
|
import useColumnVisibility from '../hooks/useColumnVisibility'
|
||||||
import GridIcon from '../../Icons/GridIcon'
|
import GridIcon from '../../Icons/GridIcon'
|
||||||
import ListIcon from '../../Icons/ListIcon'
|
import ListIcon from '../../Icons/ListIcon'
|
||||||
import useViewMode from '../hooks/useViewMode'
|
import useViewMode from '../hooks/useViewMode'
|
||||||
|
import ColumnViewButton from '../common/ColumnViewButton'
|
||||||
import config from '../../../config'
|
|
||||||
import NoteTypeIcon from '../../Icons/NoteTypeIcon'
|
|
||||||
import BoolDisplay from '../common/BoolDisplay'
|
|
||||||
|
|
||||||
const { Text } = Typography
|
|
||||||
|
|
||||||
const NoteTypes = () => {
|
const NoteTypes = () => {
|
||||||
const [messageApi, contextHolder] = message.useMessage()
|
const [messageApi, contextHolder] = message.useMessage()
|
||||||
const navigate = useNavigate()
|
|
||||||
const [newNoteTypeOpen, setNewNoteTypeOpen] = useState(false)
|
const [newNoteTypeOpen, setNewNoteTypeOpen] = useState(false)
|
||||||
const tableRef = useRef()
|
const tableRef = useRef()
|
||||||
const { authenticated } = useContext(AuthContext)
|
const { authenticated } = useContext(AuthContext)
|
||||||
const [viewMode, setViewMode] = useViewMode('NoteTypes')
|
const [viewMode, setViewMode] = useViewMode('noteType')
|
||||||
|
|
||||||
const getFilterDropdown = ({
|
const [columnVisibility, setColumnVisibility] =
|
||||||
setSelectedKeys,
|
useColumnVisibility('noteType')
|
||||||
selectedKeys,
|
|
||||||
confirm,
|
|
||||||
clearFilters,
|
|
||||||
propertyName
|
|
||||||
}) => {
|
|
||||||
return (
|
|
||||||
<div style={{ padding: 8 }}>
|
|
||||||
<Space.Compact>
|
|
||||||
<Input
|
|
||||||
placeholder={'Search ' + propertyName}
|
|
||||||
value={selectedKeys[0]}
|
|
||||||
onChange={(e) =>
|
|
||||||
setSelectedKeys(e.target.value ? [e.target.value] : [])
|
|
||||||
}
|
|
||||||
onPressEnter={() => confirm()}
|
|
||||||
style={{ width: 200, display: 'block' }}
|
|
||||||
/>
|
|
||||||
<Button
|
|
||||||
onClick={() => {
|
|
||||||
clearFilters()
|
|
||||||
confirm()
|
|
||||||
}}
|
|
||||||
icon={<XMarkIcon />}
|
|
||||||
/>
|
|
||||||
<Button
|
|
||||||
type='primary'
|
|
||||||
onClick={() => confirm()}
|
|
||||||
icon={<CheckIcon />}
|
|
||||||
/>
|
|
||||||
</Space.Compact>
|
|
||||||
</div>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
const getViewDropdownItems = () => {
|
|
||||||
const columnItems = columns
|
|
||||||
.filter((col) => col.key && col.title !== '')
|
|
||||||
.map((col) => (
|
|
||||||
<Checkbox
|
|
||||||
checked={columnVisibility[col.key]}
|
|
||||||
key={col.key}
|
|
||||||
onChange={(e) => {
|
|
||||||
updateColumnVisibility(col.key, e.target.checked)
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
{col.title}
|
|
||||||
</Checkbox>
|
|
||||||
))
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Flex vertical>
|
|
||||||
<Flex vertical gap='middle' style={{ margin: '4px 8px' }}>
|
|
||||||
{columnItems}
|
|
||||||
</Flex>
|
|
||||||
</Flex>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
const getNoteTypeActionItems = (id) => {
|
|
||||||
return {
|
|
||||||
items: [
|
|
||||||
{
|
|
||||||
label: 'Info',
|
|
||||||
key: 'info',
|
|
||||||
icon: <InfoCircleIcon />
|
|
||||||
}
|
|
||||||
],
|
|
||||||
onClick: ({ key }) => {
|
|
||||||
if (key === 'info') {
|
|
||||||
navigate(`/dashboard/management/notetypes/info?noteTypeId=${id}`)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const columns = [
|
|
||||||
{
|
|
||||||
title: <NoteTypeIcon />,
|
|
||||||
key: 'icon',
|
|
||||||
width: 40,
|
|
||||||
fixed: 'left',
|
|
||||||
render: () => <NoteTypeIcon />
|
|
||||||
},
|
|
||||||
{
|
|
||||||
title: 'Name',
|
|
||||||
dataIndex: 'name',
|
|
||||||
key: 'name',
|
|
||||||
width: 200,
|
|
||||||
fixed: 'left',
|
|
||||||
filterDropdown: ({
|
|
||||||
setSelectedKeys,
|
|
||||||
selectedKeys,
|
|
||||||
confirm,
|
|
||||||
clearFilters
|
|
||||||
}) =>
|
|
||||||
getFilterDropdown({
|
|
||||||
setSelectedKeys,
|
|
||||||
selectedKeys,
|
|
||||||
confirm,
|
|
||||||
clearFilters,
|
|
||||||
propertyName: 'name'
|
|
||||||
}),
|
|
||||||
onFilter: (value, record) =>
|
|
||||||
record.name.toLowerCase().includes(value.toLowerCase()),
|
|
||||||
sorter: true
|
|
||||||
},
|
|
||||||
{
|
|
||||||
title: 'ID',
|
|
||||||
dataIndex: '_id',
|
|
||||||
key: 'id',
|
|
||||||
width: 180,
|
|
||||||
render: (text) => (
|
|
||||||
<IdDisplay id={text} type={'notetype'} longId={false} />
|
|
||||||
),
|
|
||||||
filterDropdown: ({
|
|
||||||
setSelectedKeys,
|
|
||||||
selectedKeys,
|
|
||||||
confirm,
|
|
||||||
clearFilters
|
|
||||||
}) =>
|
|
||||||
getFilterDropdown({
|
|
||||||
setSelectedKeys,
|
|
||||||
selectedKeys,
|
|
||||||
confirm,
|
|
||||||
clearFilters,
|
|
||||||
propertyName: 'ID'
|
|
||||||
}),
|
|
||||||
onFilter: (value, record) =>
|
|
||||||
record._id.toLowerCase().includes(value.toLowerCase()),
|
|
||||||
sorter: true
|
|
||||||
},
|
|
||||||
{
|
|
||||||
title: 'Color',
|
|
||||||
dataIndex: 'color',
|
|
||||||
key: 'color',
|
|
||||||
width: 120,
|
|
||||||
render: (color) =>
|
|
||||||
color ? <Badge color={color} text={color} /> : <Text>n/a</Text>
|
|
||||||
},
|
|
||||||
{
|
|
||||||
title: 'Active',
|
|
||||||
dataIndex: 'active',
|
|
||||||
key: 'active',
|
|
||||||
width: 100,
|
|
||||||
render: (active) => <BoolDisplay value={active} yesNo={true} />,
|
|
||||||
sorter: true
|
|
||||||
},
|
|
||||||
{
|
|
||||||
title: 'Created At',
|
|
||||||
dataIndex: 'createdAt',
|
|
||||||
key: 'createdAt',
|
|
||||||
width: 180,
|
|
||||||
render: (createdAt) => {
|
|
||||||
if (createdAt) {
|
|
||||||
return <TimeDisplay dateTime={createdAt} />
|
|
||||||
} else {
|
|
||||||
return 'n/a'
|
|
||||||
}
|
|
||||||
},
|
|
||||||
sorter: true,
|
|
||||||
defaultSortOrder: 'descend'
|
|
||||||
},
|
|
||||||
{
|
|
||||||
title: 'Updated At',
|
|
||||||
dataIndex: 'updatedAt',
|
|
||||||
key: 'updatedAt',
|
|
||||||
width: 180,
|
|
||||||
render: (updatedAt) => {
|
|
||||||
if (updatedAt) {
|
|
||||||
return <TimeDisplay dateTime={updatedAt} />
|
|
||||||
} else {
|
|
||||||
return 'n/a'
|
|
||||||
}
|
|
||||||
},
|
|
||||||
sorter: true,
|
|
||||||
defaultSortOrder: 'descend'
|
|
||||||
},
|
|
||||||
{
|
|
||||||
title: 'Actions',
|
|
||||||
key: 'actions',
|
|
||||||
fixed: 'right',
|
|
||||||
width: 150,
|
|
||||||
render: (record) => {
|
|
||||||
return (
|
|
||||||
<Space gap='small'>
|
|
||||||
<Button
|
|
||||||
icon={<InfoCircleIcon />}
|
|
||||||
onClick={() =>
|
|
||||||
navigate(
|
|
||||||
`/dashboard/management/notetypes/info?noteTypeId=${record._id}`
|
|
||||||
)
|
|
||||||
}
|
|
||||||
/>
|
|
||||||
<Dropdown menu={getNoteTypeActionItems(record._id)}>
|
|
||||||
<Button>Actions</Button>
|
|
||||||
</Dropdown>
|
|
||||||
</Space>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
]
|
|
||||||
|
|
||||||
const [columnVisibility, updateColumnVisibility] = useColumnVisibility(
|
|
||||||
'NoteTypes',
|
|
||||||
columns
|
|
||||||
)
|
|
||||||
|
|
||||||
const visibleColumns = columns.filter(
|
|
||||||
(col) => !col.key || columnVisibility[col.key]
|
|
||||||
)
|
|
||||||
|
|
||||||
const actionItems = {
|
const actionItems = {
|
||||||
items: [
|
items: [
|
||||||
@ -287,13 +53,12 @@ const NoteTypes = () => {
|
|||||||
<Dropdown menu={actionItems}>
|
<Dropdown menu={actionItems}>
|
||||||
<Button>Actions</Button>
|
<Button>Actions</Button>
|
||||||
</Dropdown>
|
</Dropdown>
|
||||||
<Popover
|
<ColumnViewButton
|
||||||
content={getViewDropdownItems()}
|
type='noteType'
|
||||||
placement='bottomLeft'
|
loading={false}
|
||||||
arrow={false}
|
collapseState={columnVisibility}
|
||||||
>
|
updateCollapseState={setColumnVisibility}
|
||||||
<Button>View</Button>
|
/>
|
||||||
</Popover>
|
|
||||||
</Space>
|
</Space>
|
||||||
<Space>
|
<Space>
|
||||||
<Button
|
<Button
|
||||||
@ -306,8 +71,8 @@ const NoteTypes = () => {
|
|||||||
</Flex>
|
</Flex>
|
||||||
<ObjectTable
|
<ObjectTable
|
||||||
ref={tableRef}
|
ref={tableRef}
|
||||||
columns={visibleColumns}
|
visibleColumns={columnVisibility}
|
||||||
url={`${config.backendUrl}/notetypes`}
|
type='noteType'
|
||||||
authenticated={authenticated}
|
authenticated={authenticated}
|
||||||
cards={viewMode === 'cards'}
|
cards={viewMode === 'cards'}
|
||||||
/>
|
/>
|
||||||
|
|||||||
@ -13,6 +13,10 @@ import AuditLogIcon from '../../../Icons/AuditLogIcon.jsx'
|
|||||||
import EditObjectForm from '../../common/EditObjectForm'
|
import EditObjectForm from '../../common/EditObjectForm'
|
||||||
import EditButtons from '../../common/EditButtons'
|
import EditButtons from '../../common/EditButtons'
|
||||||
import LockIndicator from '../Filaments/LockIndicator'
|
import LockIndicator from '../Filaments/LockIndicator'
|
||||||
|
import {
|
||||||
|
getModelProperties,
|
||||||
|
getPropertyValue
|
||||||
|
} from '../../../../database/ObjectModels.js'
|
||||||
|
|
||||||
const NoteTypeInfo = () => {
|
const NoteTypeInfo = () => {
|
||||||
const location = useLocation()
|
const location = useLocation()
|
||||||
@ -108,50 +112,11 @@ const NoteTypeInfo = () => {
|
|||||||
loading={loading}
|
loading={loading}
|
||||||
indicator={<LoadingOutlined />}
|
indicator={<LoadingOutlined />}
|
||||||
isEditing={isEditing}
|
isEditing={isEditing}
|
||||||
type='notetype'
|
type='noteType'
|
||||||
items={[
|
items={getModelProperties('noteType').map((prop) => ({
|
||||||
{
|
...prop,
|
||||||
name: 'id',
|
value: getPropertyValue(objectData, prop.name)
|
||||||
label: 'ID',
|
}))}
|
||||||
value: objectData?._id,
|
|
||||||
type: 'id',
|
|
||||||
objectType: 'notetype',
|
|
||||||
showCopy: true
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: 'createdAt',
|
|
||||||
label: 'Created At',
|
|
||||||
value: objectData?.createdAt,
|
|
||||||
type: 'dateTime',
|
|
||||||
readOnly: true
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: 'name',
|
|
||||||
label: 'Name',
|
|
||||||
value: objectData?.name,
|
|
||||||
required: true,
|
|
||||||
type: 'text'
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: 'updatedAt',
|
|
||||||
label: 'Updated At',
|
|
||||||
value: objectData?.updatedAt,
|
|
||||||
type: 'dateTime',
|
|
||||||
readOnly: true
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: 'color',
|
|
||||||
label: 'Color',
|
|
||||||
value: objectData?.color,
|
|
||||||
type: 'color'
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: 'active',
|
|
||||||
label: 'Active',
|
|
||||||
value: objectData?.active,
|
|
||||||
type: 'bool'
|
|
||||||
}
|
|
||||||
]}
|
|
||||||
/>
|
/>
|
||||||
</InfoCollapse>
|
</InfoCollapse>
|
||||||
|
|
||||||
|
|||||||
@ -1,237 +1,34 @@
|
|||||||
// src/gcodefiles.js
|
// src/gcodefiles.js
|
||||||
|
|
||||||
import React, { useState, useContext, useRef } from 'react'
|
import React, { useState, useContext, useRef } from 'react'
|
||||||
import { useNavigate } from 'react-router-dom'
|
|
||||||
import {
|
import { Button, Flex, Space, Modal, Dropdown, message } from 'antd'
|
||||||
Button,
|
|
||||||
Flex,
|
|
||||||
Space,
|
|
||||||
Modal,
|
|
||||||
Dropdown,
|
|
||||||
Typography,
|
|
||||||
Checkbox,
|
|
||||||
Popover,
|
|
||||||
Input,
|
|
||||||
message
|
|
||||||
} from 'antd'
|
|
||||||
import { DownloadOutlined } from '@ant-design/icons'
|
|
||||||
|
|
||||||
import { AuthContext } from '../context/AuthContext'
|
import { AuthContext } from '../context/AuthContext'
|
||||||
import IdDisplay from '../common/IdDisplay'
|
|
||||||
import ObjectTable from '../common/ObjectTable'
|
import ObjectTable from '../common/ObjectTable'
|
||||||
import NewProduct from './Products/NewProduct'
|
import NewProduct from './Products/NewProduct'
|
||||||
import PartIcon from '../../Icons/PartIcon'
|
|
||||||
import InfoCircleIcon from '../../Icons/InfoCircleIcon'
|
|
||||||
import PlusIcon from '../../Icons/PlusIcon'
|
import PlusIcon from '../../Icons/PlusIcon'
|
||||||
import ReloadIcon from '../../Icons/ReloadIcon'
|
import ReloadIcon from '../../Icons/ReloadIcon'
|
||||||
import XMarkIcon from '../../Icons/XMarkIcon'
|
|
||||||
import CheckIcon from '../../Icons/CheckIcon'
|
|
||||||
import useColumnVisibility from '../hooks/useColumnVisibility'
|
import useColumnVisibility from '../hooks/useColumnVisibility'
|
||||||
import TimeDisplay from '../common/TimeDisplay'
|
|
||||||
import GridIcon from '../../Icons/GridIcon'
|
import GridIcon from '../../Icons/GridIcon'
|
||||||
import ListIcon from '../../Icons/ListIcon'
|
import ListIcon from '../../Icons/ListIcon'
|
||||||
import useViewMode from '../hooks/useViewMode'
|
import useViewMode from '../hooks/useViewMode'
|
||||||
|
|
||||||
import config from '../../../config'
|
import ColumnViewButton from '../common/ColumnViewButton'
|
||||||
|
|
||||||
const { Text } = Typography
|
|
||||||
|
|
||||||
const Parts = () => {
|
const Parts = () => {
|
||||||
const [messageApi, contextHolder] = message.useMessage()
|
const [messageApi, contextHolder] = message.useMessage()
|
||||||
const navigate = useNavigate()
|
|
||||||
const [newProductOpen, setNewProductOpen] = useState(false)
|
const [newProductOpen, setNewProductOpen] = useState(false)
|
||||||
const tableRef = useRef()
|
const tableRef = useRef()
|
||||||
const { authenticated } = useContext(AuthContext)
|
const { authenticated } = useContext(AuthContext)
|
||||||
const [viewMode, setViewMode] = useViewMode('Parts')
|
const [viewMode, setViewMode] = useViewMode('part')
|
||||||
|
|
||||||
// Column definitions
|
const [columnVisibility, setColumnVisibility] = useColumnVisibility('part')
|
||||||
const columns = [
|
|
||||||
{
|
|
||||||
title: <PartIcon />,
|
|
||||||
key: 'icon',
|
|
||||||
width: 40,
|
|
||||||
fixed: 'left',
|
|
||||||
render: () => <PartIcon />
|
|
||||||
},
|
|
||||||
{
|
|
||||||
title: 'Name',
|
|
||||||
dataIndex: 'name',
|
|
||||||
key: 'name',
|
|
||||||
width: 200,
|
|
||||||
fixed: 'left',
|
|
||||||
render: (text) => <Text ellipsis>{text}</Text>,
|
|
||||||
filterDropdown: ({
|
|
||||||
setSelectedKeys,
|
|
||||||
selectedKeys,
|
|
||||||
confirm,
|
|
||||||
clearFilters
|
|
||||||
}) =>
|
|
||||||
getFilterDropdown({
|
|
||||||
setSelectedKeys,
|
|
||||||
selectedKeys,
|
|
||||||
confirm,
|
|
||||||
clearFilters,
|
|
||||||
propertyName: 'name'
|
|
||||||
}),
|
|
||||||
onFilter: (value, record) =>
|
|
||||||
record.name.toLowerCase().includes(value.toLowerCase())
|
|
||||||
},
|
|
||||||
{
|
|
||||||
title: 'ID',
|
|
||||||
dataIndex: '_id',
|
|
||||||
key: 'id',
|
|
||||||
width: 180,
|
|
||||||
render: (text) => <IdDisplay id={text} type={'part'} longId={false} />
|
|
||||||
},
|
|
||||||
{
|
|
||||||
title: 'Product Name',
|
|
||||||
key: 'productName',
|
|
||||||
width: 200,
|
|
||||||
render: (record) => <Text>{record?.product?.name}</Text>,
|
|
||||||
filterDropdown: ({
|
|
||||||
setSelectedKeys,
|
|
||||||
selectedKeys,
|
|
||||||
confirm,
|
|
||||||
clearFilters
|
|
||||||
}) =>
|
|
||||||
getFilterDropdown({
|
|
||||||
setSelectedKeys,
|
|
||||||
selectedKeys,
|
|
||||||
confirm,
|
|
||||||
clearFilters,
|
|
||||||
propertyName: 'product name'
|
|
||||||
}),
|
|
||||||
onFilter: (value, record) =>
|
|
||||||
record.product.name.toLowerCase().includes(value.toLowerCase())
|
|
||||||
},
|
|
||||||
{
|
|
||||||
title: 'Product ID',
|
|
||||||
key: 'productId',
|
|
||||||
width: 180,
|
|
||||||
render: (record) => (
|
|
||||||
<IdDisplay
|
|
||||||
id={record?.product?._id}
|
|
||||||
type={'product'}
|
|
||||||
longId={false}
|
|
||||||
showHyperlink={true}
|
|
||||||
/>
|
|
||||||
)
|
|
||||||
},
|
|
||||||
{
|
|
||||||
title: 'Created At',
|
|
||||||
dataIndex: 'createdAt',
|
|
||||||
key: 'createdAt',
|
|
||||||
width: 180,
|
|
||||||
render: (createdAt) => {
|
|
||||||
if (createdAt) {
|
|
||||||
return <TimeDisplay dateTime={createdAt} />
|
|
||||||
} else {
|
|
||||||
return 'n/a'
|
|
||||||
}
|
|
||||||
},
|
|
||||||
sorter: true
|
|
||||||
},
|
|
||||||
{
|
|
||||||
title: 'Updated At',
|
|
||||||
dataIndex: 'updatedAt',
|
|
||||||
key: 'updatedAt',
|
|
||||||
width: 180,
|
|
||||||
render: (updatedAt) => {
|
|
||||||
if (updatedAt) {
|
|
||||||
return <TimeDisplay dateTime={updatedAt} />
|
|
||||||
} else {
|
|
||||||
return 'n/a'
|
|
||||||
}
|
|
||||||
},
|
|
||||||
sorter: true
|
|
||||||
},
|
|
||||||
{
|
|
||||||
title: 'Actions',
|
|
||||||
key: 'actions',
|
|
||||||
fixed: 'right',
|
|
||||||
width: 150,
|
|
||||||
render: (record) => {
|
|
||||||
return (
|
|
||||||
<Space gap='small'>
|
|
||||||
<Button
|
|
||||||
icon={<InfoCircleIcon />}
|
|
||||||
onClick={() =>
|
|
||||||
navigate(
|
|
||||||
`/dashboard/management/parts/info?partId=${record._id}`
|
|
||||||
)
|
|
||||||
}
|
|
||||||
/>
|
|
||||||
<Dropdown menu={getPartActionItems(record._id)}>
|
|
||||||
<Button>Actions</Button>
|
|
||||||
</Dropdown>
|
|
||||||
</Space>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
]
|
|
||||||
|
|
||||||
const [columnVisibility, updateColumnVisibility] = useColumnVisibility(
|
|
||||||
'Parts',
|
|
||||||
columns
|
|
||||||
)
|
|
||||||
|
|
||||||
const getPartActionItems = (id) => {
|
|
||||||
return {
|
|
||||||
items: [
|
|
||||||
{
|
|
||||||
label: 'Info',
|
|
||||||
key: 'info',
|
|
||||||
icon: <InfoCircleIcon />
|
|
||||||
},
|
|
||||||
{
|
|
||||||
label: 'Download',
|
|
||||||
key: 'download',
|
|
||||||
icon: <DownloadOutlined />
|
|
||||||
}
|
|
||||||
],
|
|
||||||
onClick: ({ key }) => {
|
|
||||||
if (key === 'info') {
|
|
||||||
navigate(`/dashboard/management/parts/info?partId=${id}`)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const getFilterDropdown = ({
|
|
||||||
setSelectedKeys,
|
|
||||||
selectedKeys,
|
|
||||||
confirm,
|
|
||||||
clearFilters,
|
|
||||||
propertyName
|
|
||||||
}) => {
|
|
||||||
return (
|
|
||||||
<div style={{ padding: 8 }}>
|
|
||||||
<Space.Compact>
|
|
||||||
<Input
|
|
||||||
placeholder={'Search ' + propertyName}
|
|
||||||
value={selectedKeys[0]}
|
|
||||||
onChange={(e) =>
|
|
||||||
setSelectedKeys(e.target.value ? [e.target.value] : [])
|
|
||||||
}
|
|
||||||
onPressEnter={() => confirm()}
|
|
||||||
style={{ width: 200, display: 'block' }}
|
|
||||||
/>
|
|
||||||
<Button
|
|
||||||
onClick={() => {
|
|
||||||
clearFilters()
|
|
||||||
confirm()
|
|
||||||
}}
|
|
||||||
icon={<XMarkIcon />}
|
|
||||||
/>
|
|
||||||
<Button
|
|
||||||
type='primary'
|
|
||||||
onClick={() => confirm()}
|
|
||||||
icon={<CheckIcon />}
|
|
||||||
/>
|
|
||||||
</Space.Compact>
|
|
||||||
</div>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
const actionItems = {
|
const actionItems = {
|
||||||
items: [
|
items: [
|
||||||
@ -256,34 +53,6 @@ const Parts = () => {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const getViewDropdownItems = () => {
|
|
||||||
const columnItems = columns
|
|
||||||
.filter((col) => col.key && col.title !== '')
|
|
||||||
.map((col) => (
|
|
||||||
<Checkbox
|
|
||||||
checked={columnVisibility[col.key]}
|
|
||||||
key={col.key}
|
|
||||||
onChange={(e) => {
|
|
||||||
updateColumnVisibility(col.key, e.target.checked)
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
{col.title}
|
|
||||||
</Checkbox>
|
|
||||||
))
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Flex vertical>
|
|
||||||
<Flex vertical gap='middle' style={{ margin: '4px 8px' }}>
|
|
||||||
{columnItems}
|
|
||||||
</Flex>
|
|
||||||
</Flex>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
const visibleColumns = columns.filter(
|
|
||||||
(col) => !col.key || columnVisibility[col.key]
|
|
||||||
)
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<Flex vertical={'true'} gap='large'>
|
<Flex vertical={'true'} gap='large'>
|
||||||
@ -293,13 +62,12 @@ const Parts = () => {
|
|||||||
<Dropdown menu={actionItems}>
|
<Dropdown menu={actionItems}>
|
||||||
<Button>Actions</Button>
|
<Button>Actions</Button>
|
||||||
</Dropdown>
|
</Dropdown>
|
||||||
<Popover
|
<ColumnViewButton
|
||||||
content={getViewDropdownItems()}
|
type='part'
|
||||||
placement='bottomLeft'
|
loading={false}
|
||||||
arrow={false}
|
collapseState={columnVisibility}
|
||||||
>
|
updateCollapseState={setColumnVisibility}
|
||||||
<Button>View</Button>
|
/>
|
||||||
</Popover>
|
|
||||||
</Space>
|
</Space>
|
||||||
<Space>
|
<Space>
|
||||||
<Button
|
<Button
|
||||||
@ -312,8 +80,8 @@ const Parts = () => {
|
|||||||
</Flex>
|
</Flex>
|
||||||
<ObjectTable
|
<ObjectTable
|
||||||
ref={tableRef}
|
ref={tableRef}
|
||||||
columns={visibleColumns}
|
visibleColumns={columnVisibility}
|
||||||
url={`${config.backendUrl}/parts`}
|
type='part'
|
||||||
authenticated={authenticated}
|
authenticated={authenticated}
|
||||||
cards={viewMode === 'cards'}
|
cards={viewMode === 'cards'}
|
||||||
/>
|
/>
|
||||||
|
|||||||
@ -32,8 +32,6 @@ import GridIcon from '../../Icons/GridIcon'
|
|||||||
import ListIcon from '../../Icons/ListIcon'
|
import ListIcon from '../../Icons/ListIcon'
|
||||||
import useViewMode from '../hooks/useViewMode'
|
import useViewMode from '../hooks/useViewMode'
|
||||||
|
|
||||||
import config from '../../../config'
|
|
||||||
|
|
||||||
const Products = () => {
|
const Products = () => {
|
||||||
const [messageApi, contextHolder] = message.useMessage()
|
const [messageApi, contextHolder] = message.useMessage()
|
||||||
const navigate = useNavigate()
|
const navigate = useNavigate()
|
||||||
@ -323,10 +321,6 @@ const Products = () => {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
const visibleColumns = columns.filter(
|
|
||||||
(col) => !col.key || columnVisibility[col.key]
|
|
||||||
)
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<Flex vertical={'true'} gap='large'>
|
<Flex vertical={'true'} gap='large'>
|
||||||
@ -355,8 +349,7 @@ const Products = () => {
|
|||||||
</Flex>
|
</Flex>
|
||||||
<ObjectTable
|
<ObjectTable
|
||||||
ref={tableRef}
|
ref={tableRef}
|
||||||
columns={visibleColumns}
|
type={'product'}
|
||||||
url={`${config.backendUrl}/products`}
|
|
||||||
authenticated={authenticated}
|
authenticated={authenticated}
|
||||||
cards={viewMode === 'cards'}
|
cards={viewMode === 'cards'}
|
||||||
/>
|
/>
|
||||||
|
|||||||
@ -1,334 +1,20 @@
|
|||||||
import React, { useContext, useRef } from 'react'
|
import React, { useContext, useRef } from 'react'
|
||||||
import { useNavigate } from 'react-router-dom'
|
import { Button, Flex, Space, Dropdown } from 'antd'
|
||||||
import {
|
|
||||||
Button,
|
|
||||||
Flex,
|
|
||||||
Space,
|
|
||||||
Dropdown,
|
|
||||||
Typography,
|
|
||||||
Checkbox,
|
|
||||||
Popover,
|
|
||||||
Input
|
|
||||||
} from 'antd'
|
|
||||||
import { ExportOutlined } from '@ant-design/icons'
|
|
||||||
import { AuthContext } from '../context/AuthContext'
|
import { AuthContext } from '../context/AuthContext'
|
||||||
import IdDisplay from '../common/IdDisplay'
|
|
||||||
import TimeDisplay from '../common/TimeDisplay'
|
|
||||||
import ObjectTable from '../common/ObjectTable'
|
import ObjectTable from '../common/ObjectTable'
|
||||||
import PersonIcon from '../../Icons/PersonIcon'
|
|
||||||
import ReloadIcon from '../../Icons/ReloadIcon'
|
import ReloadIcon from '../../Icons/ReloadIcon'
|
||||||
import XMarkIcon from '../../Icons/XMarkIcon'
|
|
||||||
import CheckIcon from '../../Icons/CheckIcon'
|
|
||||||
import useColumnVisibility from '../hooks/useColumnVisibility'
|
import useColumnVisibility from '../hooks/useColumnVisibility'
|
||||||
import InfoCircleIcon from '../../Icons/InfoCircleIcon'
|
|
||||||
import GridIcon from '../../Icons/GridIcon'
|
import GridIcon from '../../Icons/GridIcon'
|
||||||
import ListIcon from '../../Icons/ListIcon'
|
import ListIcon from '../../Icons/ListIcon'
|
||||||
import useViewMode from '../hooks/useViewMode'
|
import useViewMode from '../hooks/useViewMode'
|
||||||
|
import ColumnViewButton from '../common/ColumnViewButton'
|
||||||
import config from '../../../config'
|
|
||||||
|
|
||||||
const { Link, Text } = Typography
|
|
||||||
|
|
||||||
const Users = () => {
|
const Users = () => {
|
||||||
const navigate = useNavigate()
|
|
||||||
const tableRef = useRef()
|
const tableRef = useRef()
|
||||||
const { authenticated } = useContext(AuthContext)
|
const { authenticated } = useContext(AuthContext)
|
||||||
const [viewMode, setViewMode] = useViewMode('Users')
|
const [viewMode, setViewMode] = useViewMode('user')
|
||||||
|
|
||||||
const getFilterDropdown = ({
|
const [columnVisibility, setColumnVisibility] = useColumnVisibility('user')
|
||||||
setSelectedKeys,
|
|
||||||
selectedKeys,
|
|
||||||
confirm,
|
|
||||||
clearFilters,
|
|
||||||
propertyName
|
|
||||||
}) => {
|
|
||||||
return (
|
|
||||||
<div style={{ padding: 8 }}>
|
|
||||||
<Space.Compact>
|
|
||||||
<Input
|
|
||||||
placeholder={'Search ' + propertyName}
|
|
||||||
value={selectedKeys[0]}
|
|
||||||
onChange={(e) =>
|
|
||||||
setSelectedKeys(e.target.value ? [e.target.value] : [])
|
|
||||||
}
|
|
||||||
onPressEnter={() => confirm()}
|
|
||||||
style={{ width: 200, display: 'block' }}
|
|
||||||
/>
|
|
||||||
<Button
|
|
||||||
onClick={() => {
|
|
||||||
clearFilters()
|
|
||||||
confirm()
|
|
||||||
}}
|
|
||||||
icon={<XMarkIcon />}
|
|
||||||
/>
|
|
||||||
<Button
|
|
||||||
type='primary'
|
|
||||||
onClick={() => confirm()}
|
|
||||||
icon={<CheckIcon />}
|
|
||||||
/>
|
|
||||||
</Space.Compact>
|
|
||||||
</div>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
const getViewDropdownItems = () => {
|
|
||||||
const columnItems = columns
|
|
||||||
.filter((col) => col.key && col.title !== '')
|
|
||||||
.map((col) => (
|
|
||||||
<Checkbox
|
|
||||||
checked={columnVisibility[col.key]}
|
|
||||||
key={col.key}
|
|
||||||
onChange={(e) => {
|
|
||||||
updateColumnVisibility(col.key, e.target.checked)
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
{col.title}
|
|
||||||
</Checkbox>
|
|
||||||
))
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Flex vertical>
|
|
||||||
<Flex vertical gap='middle' style={{ margin: '4px 8px' }}>
|
|
||||||
{columnItems}
|
|
||||||
</Flex>
|
|
||||||
</Flex>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
const getUserActionItems = (id) => {
|
|
||||||
return {
|
|
||||||
items: [
|
|
||||||
{
|
|
||||||
label: 'Info',
|
|
||||||
key: 'info',
|
|
||||||
icon: <InfoCircleIcon />
|
|
||||||
}
|
|
||||||
],
|
|
||||||
onClick: ({ key }) => {
|
|
||||||
if (key === 'info') {
|
|
||||||
navigate(`/dashboard/management/users/info?userId=${id}`)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const columns = [
|
|
||||||
{
|
|
||||||
title: <PersonIcon />,
|
|
||||||
key: 'icon',
|
|
||||||
width: 40,
|
|
||||||
fixed: 'left',
|
|
||||||
render: () => <PersonIcon />
|
|
||||||
},
|
|
||||||
|
|
||||||
{
|
|
||||||
title: 'Name',
|
|
||||||
dataIndex: 'name',
|
|
||||||
key: 'name',
|
|
||||||
width: 200,
|
|
||||||
render: (text) => (text ? text : 'n/a'),
|
|
||||||
filterDropdown: ({
|
|
||||||
setSelectedKeys,
|
|
||||||
selectedKeys,
|
|
||||||
confirm,
|
|
||||||
clearFilters
|
|
||||||
}) =>
|
|
||||||
getFilterDropdown({
|
|
||||||
setSelectedKeys,
|
|
||||||
selectedKeys,
|
|
||||||
confirm,
|
|
||||||
clearFilters,
|
|
||||||
propertyName: 'name'
|
|
||||||
}),
|
|
||||||
onFilter: (value, record) =>
|
|
||||||
record.name?.toLowerCase().includes(value.toLowerCase()),
|
|
||||||
sorter: true
|
|
||||||
},
|
|
||||||
{
|
|
||||||
title: 'Username',
|
|
||||||
dataIndex: 'username',
|
|
||||||
key: 'username',
|
|
||||||
width: 150,
|
|
||||||
fixed: 'left',
|
|
||||||
filterDropdown: ({
|
|
||||||
setSelectedKeys,
|
|
||||||
selectedKeys,
|
|
||||||
confirm,
|
|
||||||
clearFilters
|
|
||||||
}) =>
|
|
||||||
getFilterDropdown({
|
|
||||||
setSelectedKeys,
|
|
||||||
selectedKeys,
|
|
||||||
confirm,
|
|
||||||
clearFilters,
|
|
||||||
propertyName: 'username'
|
|
||||||
}),
|
|
||||||
onFilter: (value, record) =>
|
|
||||||
record.username.toLowerCase().includes(value.toLowerCase()),
|
|
||||||
sorter: true
|
|
||||||
},
|
|
||||||
{
|
|
||||||
title: 'First Name',
|
|
||||||
dataIndex: 'firstName',
|
|
||||||
key: 'firstName',
|
|
||||||
width: 150,
|
|
||||||
render: (text) => (text ? text : 'n/a'),
|
|
||||||
filterDropdown: ({
|
|
||||||
setSelectedKeys,
|
|
||||||
selectedKeys,
|
|
||||||
confirm,
|
|
||||||
clearFilters
|
|
||||||
}) =>
|
|
||||||
getFilterDropdown({
|
|
||||||
setSelectedKeys,
|
|
||||||
selectedKeys,
|
|
||||||
confirm,
|
|
||||||
clearFilters,
|
|
||||||
propertyName: 'first name'
|
|
||||||
}),
|
|
||||||
onFilter: (value, record) =>
|
|
||||||
record.firstName?.toLowerCase().includes(value.toLowerCase()),
|
|
||||||
sorter: true
|
|
||||||
},
|
|
||||||
{
|
|
||||||
title: 'Last Name',
|
|
||||||
dataIndex: 'lastName',
|
|
||||||
key: 'lastName',
|
|
||||||
width: 150,
|
|
||||||
render: (text) => (text ? text : 'n/a'),
|
|
||||||
filterDropdown: ({
|
|
||||||
setSelectedKeys,
|
|
||||||
selectedKeys,
|
|
||||||
confirm,
|
|
||||||
clearFilters
|
|
||||||
}) =>
|
|
||||||
getFilterDropdown({
|
|
||||||
setSelectedKeys,
|
|
||||||
selectedKeys,
|
|
||||||
confirm,
|
|
||||||
clearFilters,
|
|
||||||
propertyName: 'last name'
|
|
||||||
}),
|
|
||||||
onFilter: (value, record) =>
|
|
||||||
record.lastName?.toLowerCase().includes(value.toLowerCase()),
|
|
||||||
sorter: true
|
|
||||||
},
|
|
||||||
{
|
|
||||||
title: 'Email',
|
|
||||||
dataIndex: 'email',
|
|
||||||
key: 'email',
|
|
||||||
width: 250,
|
|
||||||
render: (email) =>
|
|
||||||
email ? (
|
|
||||||
<Link href={`mailto:${email}`}>
|
|
||||||
{email} <ExportOutlined />
|
|
||||||
</Link>
|
|
||||||
) : (
|
|
||||||
<Text>n/a</Text>
|
|
||||||
),
|
|
||||||
filterDropdown: ({
|
|
||||||
setSelectedKeys,
|
|
||||||
selectedKeys,
|
|
||||||
confirm,
|
|
||||||
clearFilters
|
|
||||||
}) =>
|
|
||||||
getFilterDropdown({
|
|
||||||
setSelectedKeys,
|
|
||||||
selectedKeys,
|
|
||||||
confirm,
|
|
||||||
clearFilters,
|
|
||||||
propertyName: 'email'
|
|
||||||
}),
|
|
||||||
onFilter: (value, record) =>
|
|
||||||
record.email?.toLowerCase().includes(value.toLowerCase()),
|
|
||||||
sorter: true
|
|
||||||
},
|
|
||||||
{
|
|
||||||
title: 'ID',
|
|
||||||
dataIndex: '_id',
|
|
||||||
key: 'id',
|
|
||||||
width: 180,
|
|
||||||
render: (text) => <IdDisplay id={text} type={'user'} longId={false} />,
|
|
||||||
filterDropdown: ({
|
|
||||||
setSelectedKeys,
|
|
||||||
selectedKeys,
|
|
||||||
confirm,
|
|
||||||
clearFilters
|
|
||||||
}) =>
|
|
||||||
getFilterDropdown({
|
|
||||||
setSelectedKeys,
|
|
||||||
selectedKeys,
|
|
||||||
confirm,
|
|
||||||
clearFilters,
|
|
||||||
propertyName: 'ID'
|
|
||||||
}),
|
|
||||||
onFilter: (value, record) =>
|
|
||||||
record._id.toLowerCase().includes(value.toLowerCase()),
|
|
||||||
sorter: true
|
|
||||||
},
|
|
||||||
{
|
|
||||||
title: 'Created At',
|
|
||||||
dataIndex: 'createdAt',
|
|
||||||
key: 'createdAt',
|
|
||||||
width: 180,
|
|
||||||
render: (createdAt) => {
|
|
||||||
if (createdAt) {
|
|
||||||
return <TimeDisplay dateTime={createdAt} />
|
|
||||||
} else {
|
|
||||||
return 'n/a'
|
|
||||||
}
|
|
||||||
},
|
|
||||||
sorter: true,
|
|
||||||
defaultSortOrder: 'descend'
|
|
||||||
},
|
|
||||||
{
|
|
||||||
title: 'Updated At',
|
|
||||||
dataIndex: 'updatedAt',
|
|
||||||
key: 'updatedAt',
|
|
||||||
width: 180,
|
|
||||||
render: (updatedAt) => {
|
|
||||||
if (updatedAt) {
|
|
||||||
return <TimeDisplay dateTime={updatedAt} />
|
|
||||||
} else {
|
|
||||||
return 'n/a'
|
|
||||||
}
|
|
||||||
},
|
|
||||||
sorter: true,
|
|
||||||
defaultSortOrder: 'descend'
|
|
||||||
},
|
|
||||||
{
|
|
||||||
title: 'Actions',
|
|
||||||
key: 'actions',
|
|
||||||
fixed: 'right',
|
|
||||||
width: 150,
|
|
||||||
render: (record) => {
|
|
||||||
return (
|
|
||||||
<Space gap='small'>
|
|
||||||
<Button
|
|
||||||
icon={<InfoCircleIcon />}
|
|
||||||
onClick={() =>
|
|
||||||
navigate(
|
|
||||||
`/dashboard/management/users/info?userId=${record._id}`
|
|
||||||
)
|
|
||||||
}
|
|
||||||
/>
|
|
||||||
<Dropdown menu={getUserActionItems(record._id)}>
|
|
||||||
<Button>Actions</Button>
|
|
||||||
</Dropdown>
|
|
||||||
</Space>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
]
|
|
||||||
|
|
||||||
const [columnVisibility, updateColumnVisibility] = useColumnVisibility(
|
|
||||||
'Users',
|
|
||||||
columns
|
|
||||||
)
|
|
||||||
|
|
||||||
const visibleColumns = columns.filter(
|
|
||||||
(col) => !col.key || columnVisibility[col.key]
|
|
||||||
)
|
|
||||||
|
|
||||||
const actionItems = {
|
const actionItems = {
|
||||||
items: [
|
items: [
|
||||||
@ -352,13 +38,12 @@ const Users = () => {
|
|||||||
<Dropdown menu={actionItems}>
|
<Dropdown menu={actionItems}>
|
||||||
<Button>Actions</Button>
|
<Button>Actions</Button>
|
||||||
</Dropdown>
|
</Dropdown>
|
||||||
<Popover
|
<ColumnViewButton
|
||||||
content={getViewDropdownItems()}
|
type='user'
|
||||||
placement='bottomLeft'
|
loading={false}
|
||||||
arrow={false}
|
collapseState={columnVisibility}
|
||||||
>
|
updateCollapseState={setColumnVisibility}
|
||||||
<Button>View</Button>
|
/>
|
||||||
</Popover>
|
|
||||||
</Space>
|
</Space>
|
||||||
<Space>
|
<Space>
|
||||||
<Button
|
<Button
|
||||||
@ -369,8 +54,8 @@ const Users = () => {
|
|||||||
</Flex>
|
</Flex>
|
||||||
<ObjectTable
|
<ObjectTable
|
||||||
ref={tableRef}
|
ref={tableRef}
|
||||||
columns={visibleColumns}
|
type={'user'}
|
||||||
url={`${config.backendUrl}/users`}
|
visibleColumns={columnVisibility}
|
||||||
authenticated={authenticated}
|
authenticated={authenticated}
|
||||||
cards={viewMode === 'cards'}
|
cards={viewMode === 'cards'}
|
||||||
/>
|
/>
|
||||||
|
|||||||
@ -15,6 +15,10 @@ import AuditLogIcon from '../../../Icons/AuditLogIcon.jsx'
|
|||||||
import EditObjectForm from '../../common/EditObjectForm'
|
import EditObjectForm from '../../common/EditObjectForm'
|
||||||
import EditButtons from '../../common/EditButtons'
|
import EditButtons from '../../common/EditButtons'
|
||||||
import LockIndicator from '../Filaments/LockIndicator'
|
import LockIndicator from '../Filaments/LockIndicator'
|
||||||
|
import {
|
||||||
|
getModelProperties,
|
||||||
|
getPropertyValue
|
||||||
|
} from '../../../../database/ObjectModels.js'
|
||||||
|
|
||||||
const UserInfo = () => {
|
const UserInfo = () => {
|
||||||
const location = useLocation()
|
const location = useLocation()
|
||||||
@ -110,63 +114,10 @@ const UserInfo = () => {
|
|||||||
indicator={<LoadingOutlined />}
|
indicator={<LoadingOutlined />}
|
||||||
isEditing={isEditing}
|
isEditing={isEditing}
|
||||||
type='user'
|
type='user'
|
||||||
items={[
|
items={getModelProperties('user').map((prop) => ({
|
||||||
{
|
...prop,
|
||||||
name: '_id',
|
value: getPropertyValue(objectData, prop.name)
|
||||||
label: 'ID',
|
}))}
|
||||||
value: objectData?._id,
|
|
||||||
type: 'id',
|
|
||||||
objectType: 'user',
|
|
||||||
showCopy: true
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: 'createdAt',
|
|
||||||
label: 'Created At',
|
|
||||||
value: objectData?.createdAt,
|
|
||||||
type: 'dateTime',
|
|
||||||
readOnly: true
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: 'name',
|
|
||||||
label: 'Name',
|
|
||||||
value: objectData?.name,
|
|
||||||
required: true,
|
|
||||||
type: 'text'
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: 'updatedAt',
|
|
||||||
label: 'Updated At',
|
|
||||||
value: objectData?.updatedAt,
|
|
||||||
type: 'dateTime',
|
|
||||||
readOnly: true
|
|
||||||
},
|
|
||||||
|
|
||||||
{
|
|
||||||
name: 'firstName',
|
|
||||||
label: 'First Name',
|
|
||||||
value: objectData?.firstName,
|
|
||||||
type: 'text'
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: 'username',
|
|
||||||
label: 'Username',
|
|
||||||
value: objectData?.username,
|
|
||||||
required: true,
|
|
||||||
type: 'text'
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: 'lastName',
|
|
||||||
label: 'Last Name',
|
|
||||||
value: objectData?.lastName,
|
|
||||||
type: 'text'
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: 'email',
|
|
||||||
label: 'Email',
|
|
||||||
value: objectData?.email,
|
|
||||||
type: 'email'
|
|
||||||
}
|
|
||||||
]}
|
|
||||||
/>
|
/>
|
||||||
</InfoCollapse>
|
</InfoCollapse>
|
||||||
|
|
||||||
|
|||||||
@ -1,318 +1,24 @@
|
|||||||
import React, { useState, useContext, useRef } from 'react'
|
import React, { useState, useContext, useRef } from 'react'
|
||||||
import { useNavigate } from 'react-router-dom'
|
import { Button, Flex, Space, Modal, Dropdown, message } from 'antd'
|
||||||
import {
|
|
||||||
Button,
|
|
||||||
Flex,
|
|
||||||
Space,
|
|
||||||
Modal,
|
|
||||||
Dropdown,
|
|
||||||
message,
|
|
||||||
Typography,
|
|
||||||
Checkbox,
|
|
||||||
Popover,
|
|
||||||
Input
|
|
||||||
} from 'antd'
|
|
||||||
import { ExportOutlined } from '@ant-design/icons'
|
|
||||||
import { AuthContext } from '../context/AuthContext'
|
import { AuthContext } from '../context/AuthContext'
|
||||||
import IdDisplay from '../common/IdDisplay'
|
|
||||||
import NewVendor from './Vendors/NewVendor'
|
import NewVendor from './Vendors/NewVendor'
|
||||||
import CountryDisplay from '../common/CountryDisplay'
|
|
||||||
import TimeDisplay from '../common/TimeDisplay'
|
|
||||||
import ObjectTable from '../common/ObjectTable'
|
import ObjectTable from '../common/ObjectTable'
|
||||||
import VendorIcon from '../../Icons/VendorIcon'
|
|
||||||
import PlusIcon from '../../Icons/PlusIcon'
|
import PlusIcon from '../../Icons/PlusIcon'
|
||||||
import ReloadIcon from '../../Icons/ReloadIcon'
|
import ReloadIcon from '../../Icons/ReloadIcon'
|
||||||
import XMarkIcon from '../../Icons/XMarkIcon'
|
|
||||||
import CheckIcon from '../../Icons/CheckIcon'
|
|
||||||
import useColumnVisibility from '../hooks/useColumnVisibility'
|
import useColumnVisibility from '../hooks/useColumnVisibility'
|
||||||
import InfoCircleIcon from '../../Icons/InfoCircleIcon'
|
|
||||||
import GridIcon from '../../Icons/GridIcon'
|
import GridIcon from '../../Icons/GridIcon'
|
||||||
import ListIcon from '../../Icons/ListIcon'
|
import ListIcon from '../../Icons/ListIcon'
|
||||||
import useViewMode from '../hooks/useViewMode'
|
import useViewMode from '../hooks/useViewMode'
|
||||||
|
import ColumnViewButton from '../common/ColumnViewButton'
|
||||||
import config from '../../../config'
|
|
||||||
|
|
||||||
const { Link } = Typography
|
|
||||||
|
|
||||||
const Vendors = () => {
|
const Vendors = () => {
|
||||||
const [messageApi, contextHolder] = message.useMessage()
|
const [messageApi, contextHolder] = message.useMessage()
|
||||||
const navigate = useNavigate()
|
|
||||||
const [newVendorOpen, setNewVendorOpen] = useState(false)
|
const [newVendorOpen, setNewVendorOpen] = useState(false)
|
||||||
const tableRef = useRef()
|
const tableRef = useRef()
|
||||||
const { authenticated } = useContext(AuthContext)
|
const { authenticated } = useContext(AuthContext)
|
||||||
const [viewMode, setViewMode] = useViewMode('Vendors')
|
const [viewMode, setViewMode] = useViewMode('vendor')
|
||||||
|
|
||||||
const getFilterDropdown = ({
|
const [columnVisibility, setColumnVisibility] = useColumnVisibility('vendor')
|
||||||
setSelectedKeys,
|
|
||||||
selectedKeys,
|
|
||||||
confirm,
|
|
||||||
clearFilters,
|
|
||||||
propertyName
|
|
||||||
}) => {
|
|
||||||
return (
|
|
||||||
<div style={{ padding: 8 }}>
|
|
||||||
<Space.Compact>
|
|
||||||
<Input
|
|
||||||
placeholder={'Search ' + propertyName}
|
|
||||||
value={selectedKeys[0]}
|
|
||||||
onChange={(e) =>
|
|
||||||
setSelectedKeys(e.target.value ? [e.target.value] : [])
|
|
||||||
}
|
|
||||||
onPressEnter={() => confirm()}
|
|
||||||
style={{ width: 200, display: 'block' }}
|
|
||||||
/>
|
|
||||||
<Button
|
|
||||||
onClick={() => {
|
|
||||||
clearFilters()
|
|
||||||
confirm()
|
|
||||||
}}
|
|
||||||
icon={<XMarkIcon />}
|
|
||||||
/>
|
|
||||||
<Button
|
|
||||||
type='primary'
|
|
||||||
onClick={() => confirm()}
|
|
||||||
icon={<CheckIcon />}
|
|
||||||
/>
|
|
||||||
</Space.Compact>
|
|
||||||
</div>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
const getViewDropdownItems = () => {
|
|
||||||
const columnItems = columns
|
|
||||||
.filter((col) => col.key && col.title !== '')
|
|
||||||
.map((col) => (
|
|
||||||
<Checkbox
|
|
||||||
checked={columnVisibility[col.key]}
|
|
||||||
key={col.key}
|
|
||||||
onChange={(e) => {
|
|
||||||
updateColumnVisibility(col.key, e.target.checked)
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
{col.title}
|
|
||||||
</Checkbox>
|
|
||||||
))
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Flex vertical>
|
|
||||||
<Flex vertical gap='middle' style={{ margin: '4px 8px' }}>
|
|
||||||
{columnItems}
|
|
||||||
</Flex>
|
|
||||||
</Flex>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
const getVendorActionItems = (id) => {
|
|
||||||
return {
|
|
||||||
items: [
|
|
||||||
{
|
|
||||||
label: 'Info',
|
|
||||||
key: 'info',
|
|
||||||
icon: <InfoCircleIcon />
|
|
||||||
}
|
|
||||||
],
|
|
||||||
onClick: ({ key }) => {
|
|
||||||
if (key === 'info') {
|
|
||||||
navigate(`/dashboard/management/vendors/info?vendorId=${id}`)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const columns = [
|
|
||||||
{
|
|
||||||
title: <VendorIcon />,
|
|
||||||
key: 'icon',
|
|
||||||
width: 40,
|
|
||||||
fixed: 'left',
|
|
||||||
render: () => <VendorIcon />
|
|
||||||
},
|
|
||||||
{
|
|
||||||
title: 'Name',
|
|
||||||
dataIndex: 'name',
|
|
||||||
key: 'name',
|
|
||||||
width: 200,
|
|
||||||
fixed: 'left',
|
|
||||||
filterDropdown: ({
|
|
||||||
setSelectedKeys,
|
|
||||||
selectedKeys,
|
|
||||||
confirm,
|
|
||||||
clearFilters
|
|
||||||
}) =>
|
|
||||||
getFilterDropdown({
|
|
||||||
setSelectedKeys,
|
|
||||||
selectedKeys,
|
|
||||||
confirm,
|
|
||||||
clearFilters,
|
|
||||||
propertyName: 'name'
|
|
||||||
}),
|
|
||||||
onFilter: (value, record) =>
|
|
||||||
record.name.toLowerCase().includes(value.toLowerCase()),
|
|
||||||
sorter: true
|
|
||||||
},
|
|
||||||
{
|
|
||||||
title: 'ID',
|
|
||||||
dataIndex: '_id',
|
|
||||||
key: 'id',
|
|
||||||
width: 180,
|
|
||||||
render: (text) => <IdDisplay id={text} type={'vendor'} longId={false} />,
|
|
||||||
filterDropdown: ({
|
|
||||||
setSelectedKeys,
|
|
||||||
selectedKeys,
|
|
||||||
confirm,
|
|
||||||
clearFilters
|
|
||||||
}) =>
|
|
||||||
getFilterDropdown({
|
|
||||||
setSelectedKeys,
|
|
||||||
selectedKeys,
|
|
||||||
confirm,
|
|
||||||
clearFilters,
|
|
||||||
propertyName: 'ID'
|
|
||||||
}),
|
|
||||||
onFilter: (value, record) =>
|
|
||||||
record._id.toLowerCase().includes(value.toLowerCase()),
|
|
||||||
sorter: true
|
|
||||||
},
|
|
||||||
{
|
|
||||||
title: 'Website',
|
|
||||||
dataIndex: 'website',
|
|
||||||
key: 'website',
|
|
||||||
width: 200,
|
|
||||||
render: (text) =>
|
|
||||||
text ? (
|
|
||||||
<Link href={text} target='_blank' rel='noopener noreferrer'>
|
|
||||||
{new URL(text).hostname + ' '}
|
|
||||||
<ExportOutlined />
|
|
||||||
</Link>
|
|
||||||
) : (
|
|
||||||
'n/a'
|
|
||||||
),
|
|
||||||
filterDropdown: ({
|
|
||||||
setSelectedKeys,
|
|
||||||
selectedKeys,
|
|
||||||
confirm,
|
|
||||||
clearFilters
|
|
||||||
}) =>
|
|
||||||
getFilterDropdown({
|
|
||||||
setSelectedKeys,
|
|
||||||
selectedKeys,
|
|
||||||
confirm,
|
|
||||||
clearFilters,
|
|
||||||
propertyName: 'website'
|
|
||||||
}),
|
|
||||||
onFilter: (value, record) =>
|
|
||||||
record.website?.toLowerCase().includes(value.toLowerCase()),
|
|
||||||
sorter: true
|
|
||||||
},
|
|
||||||
{
|
|
||||||
title: 'Country',
|
|
||||||
dataIndex: 'country',
|
|
||||||
key: 'country',
|
|
||||||
width: 200,
|
|
||||||
render: (text) => (text ? <CountryDisplay countryCode={text} /> : 'n/a'),
|
|
||||||
filterDropdown: ({
|
|
||||||
setSelectedKeys,
|
|
||||||
selectedKeys,
|
|
||||||
confirm,
|
|
||||||
clearFilters
|
|
||||||
}) =>
|
|
||||||
getFilterDropdown({
|
|
||||||
setSelectedKeys,
|
|
||||||
selectedKeys,
|
|
||||||
confirm,
|
|
||||||
clearFilters,
|
|
||||||
propertyName: 'country'
|
|
||||||
}),
|
|
||||||
onFilter: (value, record) =>
|
|
||||||
record.country?.toLowerCase().includes(value.toLowerCase()),
|
|
||||||
sorter: true
|
|
||||||
},
|
|
||||||
{
|
|
||||||
title: 'Contact',
|
|
||||||
dataIndex: 'contact',
|
|
||||||
key: 'contact',
|
|
||||||
width: 200,
|
|
||||||
render: (text) => (text ? text : 'n/a'),
|
|
||||||
filterDropdown: ({
|
|
||||||
setSelectedKeys,
|
|
||||||
selectedKeys,
|
|
||||||
confirm,
|
|
||||||
clearFilters
|
|
||||||
}) =>
|
|
||||||
getFilterDropdown({
|
|
||||||
setSelectedKeys,
|
|
||||||
selectedKeys,
|
|
||||||
confirm,
|
|
||||||
clearFilters,
|
|
||||||
propertyName: 'contact'
|
|
||||||
}),
|
|
||||||
onFilter: (value, record) =>
|
|
||||||
record.contact?.toLowerCase().includes(value.toLowerCase()),
|
|
||||||
sorter: true
|
|
||||||
},
|
|
||||||
{
|
|
||||||
title: 'Created At',
|
|
||||||
dataIndex: 'createdAt',
|
|
||||||
key: 'createdAt',
|
|
||||||
width: 180,
|
|
||||||
render: (createdAt) => {
|
|
||||||
if (createdAt) {
|
|
||||||
return <TimeDisplay dateTime={createdAt} />
|
|
||||||
} else {
|
|
||||||
return 'n/a'
|
|
||||||
}
|
|
||||||
},
|
|
||||||
sorter: true,
|
|
||||||
defaultSortOrder: 'descend'
|
|
||||||
},
|
|
||||||
{
|
|
||||||
title: 'Updated At',
|
|
||||||
dataIndex: 'updatedAt',
|
|
||||||
key: 'updatedAt',
|
|
||||||
width: 180,
|
|
||||||
render: (updatedAt) => {
|
|
||||||
if (updatedAt) {
|
|
||||||
return <TimeDisplay dateTime={updatedAt} />
|
|
||||||
} else {
|
|
||||||
return 'n/a'
|
|
||||||
}
|
|
||||||
},
|
|
||||||
sorter: true,
|
|
||||||
defaultSortOrder: 'descend'
|
|
||||||
},
|
|
||||||
{
|
|
||||||
title: 'Actions',
|
|
||||||
key: 'actions',
|
|
||||||
fixed: 'right',
|
|
||||||
width: 150,
|
|
||||||
render: (record) => {
|
|
||||||
return (
|
|
||||||
<Space gap='small'>
|
|
||||||
<Button
|
|
||||||
icon={<InfoCircleIcon />}
|
|
||||||
onClick={() =>
|
|
||||||
navigate(
|
|
||||||
`/dashboard/management/vendors/info?vendorId=${record._id}`
|
|
||||||
)
|
|
||||||
}
|
|
||||||
/>
|
|
||||||
<Dropdown menu={getVendorActionItems(record._id)}>
|
|
||||||
<Button>Actions</Button>
|
|
||||||
</Dropdown>
|
|
||||||
</Space>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
]
|
|
||||||
|
|
||||||
const [columnVisibility, updateColumnVisibility] = useColumnVisibility(
|
|
||||||
'Vendors',
|
|
||||||
columns
|
|
||||||
)
|
|
||||||
|
|
||||||
const visibleColumns = columns.filter(
|
|
||||||
(col) => !col.key || columnVisibility[col.key]
|
|
||||||
)
|
|
||||||
|
|
||||||
const actionItems = {
|
const actionItems = {
|
||||||
items: [
|
items: [
|
||||||
@ -346,13 +52,12 @@ const Vendors = () => {
|
|||||||
<Dropdown menu={actionItems}>
|
<Dropdown menu={actionItems}>
|
||||||
<Button>Actions</Button>
|
<Button>Actions</Button>
|
||||||
</Dropdown>
|
</Dropdown>
|
||||||
<Popover
|
<ColumnViewButton
|
||||||
content={getViewDropdownItems()}
|
type='vendor'
|
||||||
placement='bottomLeft'
|
loading={false}
|
||||||
arrow={false}
|
collapseState={columnVisibility}
|
||||||
>
|
updateCollapseState={setColumnVisibility}
|
||||||
<Button>View</Button>
|
/>
|
||||||
</Popover>
|
|
||||||
</Space>
|
</Space>
|
||||||
<Space>
|
<Space>
|
||||||
<Button
|
<Button
|
||||||
@ -365,8 +70,8 @@ const Vendors = () => {
|
|||||||
</Flex>
|
</Flex>
|
||||||
<ObjectTable
|
<ObjectTable
|
||||||
ref={tableRef}
|
ref={tableRef}
|
||||||
columns={visibleColumns}
|
visibleColumns={columnVisibility}
|
||||||
url={`${config.backendUrl}/vendors`}
|
type='vendor'
|
||||||
authenticated={authenticated}
|
authenticated={authenticated}
|
||||||
cards={viewMode === 'cards'}
|
cards={viewMode === 'cards'}
|
||||||
/>
|
/>
|
||||||
|
|||||||
@ -1,300 +1,30 @@
|
|||||||
// src/gcodefiles.js
|
// src/gcodefiles.js
|
||||||
|
|
||||||
import React, { useState, useContext, useRef } from 'react'
|
import React, { useState, useContext, useRef } from 'react'
|
||||||
import { useNavigate } from 'react-router-dom'
|
import { Button, Flex, Space, Modal, Dropdown, message } from 'antd'
|
||||||
import axios from 'axios'
|
|
||||||
import {
|
|
||||||
Badge,
|
|
||||||
Button,
|
|
||||||
Flex,
|
|
||||||
Space,
|
|
||||||
Modal,
|
|
||||||
Dropdown,
|
|
||||||
Typography,
|
|
||||||
message,
|
|
||||||
Checkbox,
|
|
||||||
Divider,
|
|
||||||
Popover,
|
|
||||||
Input
|
|
||||||
} from 'antd'
|
|
||||||
import { DownloadOutlined } from '@ant-design/icons'
|
|
||||||
|
|
||||||
import { AuthContext } from '../context/AuthContext'
|
import { AuthContext } from '../context/AuthContext'
|
||||||
import NewGCodeFile from './GCodeFiles/NewGCodeFile'
|
import NewGCodeFile from './GCodeFiles/NewGCodeFile'
|
||||||
import IdDisplay from '../common/IdDisplay'
|
|
||||||
import GCodeFileIcon from '../../Icons/GCodeFileIcon'
|
|
||||||
import InfoCircleIcon from '../../Icons/InfoCircleIcon'
|
|
||||||
import useColumnVisibility from '../hooks/useColumnVisibility'
|
import useColumnVisibility from '../hooks/useColumnVisibility'
|
||||||
import PlusIcon from '../../Icons/PlusIcon'
|
import PlusIcon from '../../Icons/PlusIcon'
|
||||||
import ReloadIcon from '../../Icons/ReloadIcon'
|
import ReloadIcon from '../../Icons/ReloadIcon'
|
||||||
import XMarkIcon from '../../Icons/XMarkIcon'
|
|
||||||
import CheckIcon from '../../Icons/CheckIcon'
|
|
||||||
import TimeDisplay from '../common/TimeDisplay'
|
|
||||||
import ObjectTable from '../common/ObjectTable'
|
import ObjectTable from '../common/ObjectTable'
|
||||||
import ListIcon from '../../Icons/ListIcon'
|
import ListIcon from '../../Icons/ListIcon'
|
||||||
import GridIcon from '../../Icons/GridIcon'
|
import GridIcon from '../../Icons/GridIcon'
|
||||||
import useViewMode from '../hooks/useViewMode'
|
import useViewMode from '../hooks/useViewMode'
|
||||||
|
|
||||||
import config from '../../../config'
|
import ColumnViewButton from '../common/ColumnViewButton'
|
||||||
|
|
||||||
const { Text } = Typography
|
|
||||||
|
|
||||||
const GCodeFiles = () => {
|
const GCodeFiles = () => {
|
||||||
const [messageApi, contextHolder] = message.useMessage()
|
const [messageApi, contextHolder] = message.useMessage()
|
||||||
const navigate = useNavigate()
|
|
||||||
const [newGCodeFileOpen, setNewGCodeFileOpen] = useState(false)
|
const [newGCodeFileOpen, setNewGCodeFileOpen] = useState(false)
|
||||||
const [showDeleted, setShowDeleted] = useState(false)
|
|
||||||
const tableRef = useRef()
|
const tableRef = useRef()
|
||||||
const [viewMode, setViewMode] = useViewMode('GCodeFiles')
|
const [viewMode, setViewMode] = useViewMode('gcodeFile')
|
||||||
|
|
||||||
const getFilterDropdown = ({
|
const [columnVisibility, setColumnVisibility] =
|
||||||
setSelectedKeys,
|
useColumnVisibility('gcodeFile')
|
||||||
selectedKeys,
|
|
||||||
confirm,
|
|
||||||
clearFilters,
|
|
||||||
propertyName
|
|
||||||
}) => {
|
|
||||||
return (
|
|
||||||
<div style={{ padding: 8 }}>
|
|
||||||
<Space.Compact>
|
|
||||||
<Input
|
|
||||||
placeholder={'Search ' + propertyName}
|
|
||||||
value={selectedKeys[0]}
|
|
||||||
onChange={(e) =>
|
|
||||||
setSelectedKeys(e.target.value ? [e.target.value] : [])
|
|
||||||
}
|
|
||||||
onPressEnter={() => confirm()}
|
|
||||||
style={{ width: 200, display: 'block' }}
|
|
||||||
/>
|
|
||||||
<Button
|
|
||||||
onClick={() => {
|
|
||||||
clearFilters()
|
|
||||||
confirm()
|
|
||||||
}}
|
|
||||||
icon={<XMarkIcon />}
|
|
||||||
/>
|
|
||||||
<Button
|
|
||||||
type='primary'
|
|
||||||
onClick={() => confirm()}
|
|
||||||
icon={<CheckIcon />}
|
|
||||||
/>
|
|
||||||
</Space.Compact>
|
|
||||||
</div>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Column definitions
|
|
||||||
const columns = [
|
|
||||||
{
|
|
||||||
title: <GCodeFileIcon />,
|
|
||||||
key: 'icon',
|
|
||||||
width: 40,
|
|
||||||
fixed: 'left',
|
|
||||||
render: () => <GCodeFileIcon />
|
|
||||||
},
|
|
||||||
{
|
|
||||||
title: 'Name',
|
|
||||||
dataIndex: 'name',
|
|
||||||
key: 'name',
|
|
||||||
width: 200,
|
|
||||||
fixed: 'left',
|
|
||||||
render: (text) => <Text ellipsis>{text}</Text>,
|
|
||||||
filterDropdown: ({
|
|
||||||
setSelectedKeys,
|
|
||||||
selectedKeys,
|
|
||||||
confirm,
|
|
||||||
clearFilters
|
|
||||||
}) =>
|
|
||||||
getFilterDropdown({
|
|
||||||
setSelectedKeys,
|
|
||||||
selectedKeys,
|
|
||||||
confirm,
|
|
||||||
clearFilters,
|
|
||||||
propertyName: 'name'
|
|
||||||
}),
|
|
||||||
onFilter: (value, record) =>
|
|
||||||
record.name.toLowerCase().includes(value.toLowerCase())
|
|
||||||
},
|
|
||||||
{
|
|
||||||
title: 'ID',
|
|
||||||
dataIndex: '_id',
|
|
||||||
key: 'id',
|
|
||||||
width: 180,
|
|
||||||
render: (text) => (
|
|
||||||
<IdDisplay id={text} type={'gcodefile'} longId={false} />
|
|
||||||
)
|
|
||||||
},
|
|
||||||
{
|
|
||||||
title: 'Filament',
|
|
||||||
key: 'filament',
|
|
||||||
width: 200,
|
|
||||||
render: (record) => {
|
|
||||||
return (
|
|
||||||
<Badge
|
|
||||||
color={record?.filament?.color}
|
|
||||||
text={record?.filament?.name}
|
|
||||||
/>
|
|
||||||
)
|
|
||||||
},
|
|
||||||
filterDropdown: ({
|
|
||||||
setSelectedKeys,
|
|
||||||
selectedKeys,
|
|
||||||
confirm,
|
|
||||||
clearFilters
|
|
||||||
}) =>
|
|
||||||
getFilterDropdown({
|
|
||||||
setSelectedKeys,
|
|
||||||
selectedKeys,
|
|
||||||
confirm,
|
|
||||||
clearFilters,
|
|
||||||
propertyName: 'filament'
|
|
||||||
}),
|
|
||||||
onFilter: (value, record) =>
|
|
||||||
record.filament.name.toLowerCase().includes(value.toLowerCase())
|
|
||||||
},
|
|
||||||
{
|
|
||||||
title: 'Cost',
|
|
||||||
dataIndex: 'cost',
|
|
||||||
key: 'cost',
|
|
||||||
width: 120,
|
|
||||||
render: (cost) => {
|
|
||||||
return '£' + cost?.toFixed(2)
|
|
||||||
},
|
|
||||||
sorter: true
|
|
||||||
},
|
|
||||||
{
|
|
||||||
title: 'Print Time',
|
|
||||||
key: 'estimatedPrintingTimeNormalMode',
|
|
||||||
width: 140,
|
|
||||||
render: (record) => {
|
|
||||||
return `${record?.gcodeFileInfo?.estimatedPrintingTimeNormalMode}`
|
|
||||||
},
|
|
||||||
sorter: true
|
|
||||||
},
|
|
||||||
{
|
|
||||||
title: 'Created At',
|
|
||||||
dataIndex: 'createdAt',
|
|
||||||
key: 'createdAt',
|
|
||||||
width: 180,
|
|
||||||
render: (createdAt) => {
|
|
||||||
if (createdAt) {
|
|
||||||
return <TimeDisplay dateTime={createdAt} />
|
|
||||||
} else {
|
|
||||||
return 'n/a'
|
|
||||||
}
|
|
||||||
},
|
|
||||||
sorter: true
|
|
||||||
},
|
|
||||||
{
|
|
||||||
title: 'Updated At',
|
|
||||||
dataIndex: 'updatedAt',
|
|
||||||
key: 'updatedAt',
|
|
||||||
width: 180,
|
|
||||||
render: (createdAt) => {
|
|
||||||
if (createdAt) {
|
|
||||||
return <TimeDisplay dateTime={createdAt} />
|
|
||||||
} else {
|
|
||||||
return 'n/a'
|
|
||||||
}
|
|
||||||
},
|
|
||||||
sorter: true
|
|
||||||
},
|
|
||||||
{
|
|
||||||
title: 'Actions',
|
|
||||||
key: 'actions',
|
|
||||||
fixed: 'right',
|
|
||||||
width: 150,
|
|
||||||
render: (record) => {
|
|
||||||
return (
|
|
||||||
<Space gap='small'>
|
|
||||||
<Button
|
|
||||||
icon={<InfoCircleIcon />}
|
|
||||||
onClick={() =>
|
|
||||||
navigate(
|
|
||||||
`/dashboard/production/gcodefiles/info?gcodeFileId=${record._id}`
|
|
||||||
)
|
|
||||||
}
|
|
||||||
/>
|
|
||||||
<Dropdown menu={getGCodeFileActionItems(record._id)}>
|
|
||||||
<Button>Actions</Button>
|
|
||||||
</Dropdown>
|
|
||||||
</Space>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
]
|
|
||||||
|
|
||||||
const [columnVisibility, updateColumnVisibility] = useColumnVisibility(
|
|
||||||
'GCodeFiles',
|
|
||||||
columns
|
|
||||||
)
|
|
||||||
|
|
||||||
const { authenticated } = useContext(AuthContext)
|
const { authenticated } = useContext(AuthContext)
|
||||||
|
|
||||||
const getGCodeFileActionItems = (id) => {
|
|
||||||
return {
|
|
||||||
items: [
|
|
||||||
{
|
|
||||||
label: 'Info',
|
|
||||||
key: 'info',
|
|
||||||
icon: <InfoCircleIcon />
|
|
||||||
},
|
|
||||||
{
|
|
||||||
label: 'Download',
|
|
||||||
key: 'download',
|
|
||||||
icon: <DownloadOutlined />
|
|
||||||
}
|
|
||||||
],
|
|
||||||
onClick: ({ key }) => {
|
|
||||||
if (key === 'info') {
|
|
||||||
navigate(`/dashboard/production/gcodefiles/info?gcodeFileId=${id}`)
|
|
||||||
} else if (key === 'download') {
|
|
||||||
handleDownloadGCode(
|
|
||||||
id,
|
|
||||||
tableRef.current?.getData().find((file) => file._id === id)?.name +
|
|
||||||
'.gcode'
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const handleDownloadGCode = async (id, fileName) => {
|
|
||||||
if (!authenticated) {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
try {
|
|
||||||
const response = await axios.get(
|
|
||||||
`${config.backendUrl}/gcodefiles/${id}/content`,
|
|
||||||
{
|
|
||||||
headers: {
|
|
||||||
Accept: 'application/json'
|
|
||||||
},
|
|
||||||
withCredentials: true
|
|
||||||
}
|
|
||||||
)
|
|
||||||
|
|
||||||
const fileURL = window.URL.createObjectURL(new Blob([response.data]))
|
|
||||||
const fileLink = document.createElement('a')
|
|
||||||
fileLink.href = fileURL
|
|
||||||
fileLink.setAttribute('download', fileName)
|
|
||||||
document.body.appendChild(fileLink)
|
|
||||||
fileLink.click()
|
|
||||||
fileLink.parentNode.removeChild(fileLink)
|
|
||||||
} catch (error) {
|
|
||||||
if (error.response) {
|
|
||||||
messageApi.error(
|
|
||||||
'Error updating printer details:',
|
|
||||||
error.response.status
|
|
||||||
)
|
|
||||||
} else {
|
|
||||||
messageApi.error(
|
|
||||||
'An unexpected error occurred. Please try again later.'
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const actionItems = {
|
const actionItems = {
|
||||||
items: [
|
items: [
|
||||||
{
|
{
|
||||||
@ -318,43 +48,6 @@ const GCodeFiles = () => {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const getViewDropdownItems = () => {
|
|
||||||
const columnItems = columns
|
|
||||||
.filter((col) => col.key && col.title !== '')
|
|
||||||
.map((col) => (
|
|
||||||
<Checkbox
|
|
||||||
checked={columnVisibility[col.key]}
|
|
||||||
key={col.key}
|
|
||||||
onChange={(e) => {
|
|
||||||
updateColumnVisibility(col.key, e.target.checked)
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
{col.title}
|
|
||||||
</Checkbox>
|
|
||||||
))
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Flex vertical>
|
|
||||||
<Flex vertical gap='middle' style={{ margin: '4px 8px' }}>
|
|
||||||
{columnItems}
|
|
||||||
</Flex>
|
|
||||||
<Divider style={{ margin: '8px 0' }} />
|
|
||||||
<Flex vertical gap='middle' style={{ margin: '4px 8px' }}>
|
|
||||||
<Checkbox
|
|
||||||
checked={showDeleted}
|
|
||||||
onChange={(e) => setShowDeleted(e.target.checked)}
|
|
||||||
>
|
|
||||||
Show Deleted
|
|
||||||
</Checkbox>
|
|
||||||
</Flex>
|
|
||||||
</Flex>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
const visibleColumns = columns.filter(
|
|
||||||
(col) => !col.key || columnVisibility[col.key]
|
|
||||||
)
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<Flex vertical={'true'} gap='large'>
|
<Flex vertical={'true'} gap='large'>
|
||||||
@ -364,13 +57,12 @@ const GCodeFiles = () => {
|
|||||||
<Dropdown menu={actionItems}>
|
<Dropdown menu={actionItems}>
|
||||||
<Button>Actions</Button>
|
<Button>Actions</Button>
|
||||||
</Dropdown>
|
</Dropdown>
|
||||||
<Popover
|
<ColumnViewButton
|
||||||
content={getViewDropdownItems()}
|
type='gcodeFile'
|
||||||
placement='bottomLeft'
|
loading={false}
|
||||||
arrow={false}
|
collapseState={columnVisibility}
|
||||||
>
|
updateCollapseState={setColumnVisibility}
|
||||||
<Button>View</Button>
|
/>
|
||||||
</Popover>
|
|
||||||
</Space>
|
</Space>
|
||||||
<Space>
|
<Space>
|
||||||
<Button
|
<Button
|
||||||
@ -383,10 +75,10 @@ const GCodeFiles = () => {
|
|||||||
</Flex>
|
</Flex>
|
||||||
<ObjectTable
|
<ObjectTable
|
||||||
ref={tableRef}
|
ref={tableRef}
|
||||||
columns={visibleColumns}
|
type={'gcodeFile'}
|
||||||
url={`${config.backendUrl}/gcodefiles`}
|
|
||||||
authenticated={authenticated}
|
authenticated={authenticated}
|
||||||
cards={viewMode === 'cards'}
|
cards={viewMode === 'cards'}
|
||||||
|
visibleColumns={columnVisibility}
|
||||||
/>
|
/>
|
||||||
</Flex>
|
</Flex>
|
||||||
<Modal
|
<Modal
|
||||||
|
|||||||
@ -1,4 +1,4 @@
|
|||||||
import React from 'react'
|
import React, { useContext } from 'react'
|
||||||
import { useLocation } from 'react-router-dom'
|
import { useLocation } from 'react-router-dom'
|
||||||
import { Space, Button, Flex, Dropdown, Card, Typography } from 'antd'
|
import { Space, Button, Flex, Dropdown, Card, Typography } from 'antd'
|
||||||
import { LoadingOutlined } from '@ant-design/icons'
|
import { LoadingOutlined } from '@ant-design/icons'
|
||||||
@ -12,10 +12,12 @@ import ViewButton from '../../common/ViewButton'
|
|||||||
import EditObjectForm from '../../common/EditObjectForm'
|
import EditObjectForm from '../../common/EditObjectForm'
|
||||||
import EditButtons from '../../common/EditButtons'
|
import EditButtons from '../../common/EditButtons'
|
||||||
import LockIndicator from '../../Management/Filaments/LockIndicator'
|
import LockIndicator from '../../Management/Filaments/LockIndicator'
|
||||||
|
import ActionHandler from '../../common/ActionHandler'
|
||||||
import InfoCircleIcon from '../../../Icons/InfoCircleIcon.jsx'
|
import InfoCircleIcon from '../../../Icons/InfoCircleIcon.jsx'
|
||||||
import NoteIcon from '../../../Icons/NoteIcon.jsx'
|
import NoteIcon from '../../../Icons/NoteIcon.jsx'
|
||||||
import AuditLogIcon from '../../../Icons/AuditLogIcon.jsx'
|
import AuditLogIcon from '../../../Icons/AuditLogIcon.jsx'
|
||||||
import GCodeFileIcon from '../../../Icons/GCodeFileIcon.jsx'
|
import GCodeFileIcon from '../../../Icons/GCodeFileIcon.jsx'
|
||||||
|
import { ApiServerContext } from '../../context/ApiServerContext'
|
||||||
import {
|
import {
|
||||||
getModelProperties,
|
getModelProperties,
|
||||||
getPropertyValue
|
getPropertyValue
|
||||||
@ -26,6 +28,8 @@ const { Text } = Typography
|
|||||||
const GCodeFileInfo = () => {
|
const GCodeFileInfo = () => {
|
||||||
const location = useLocation()
|
const location = useLocation()
|
||||||
const gcodeFileId = new URLSearchParams(location.search).get('gcodeFileId')
|
const gcodeFileId = new URLSearchParams(location.search).get('gcodeFileId')
|
||||||
|
|
||||||
|
const { handleDownloadContent } = useContext(ApiServerContext)
|
||||||
const [collapseState, updateCollapseState] = useCollapseState(
|
const [collapseState, updateCollapseState] = useCollapseState(
|
||||||
'GCodeFileInfo',
|
'GCodeFileInfo',
|
||||||
{
|
{
|
||||||
@ -36,7 +40,22 @@ const GCodeFileInfo = () => {
|
|||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
|
// Define actions that can be triggered via URL
|
||||||
|
const actions = {
|
||||||
|
download: () => {
|
||||||
|
if (gcodeFileId) {
|
||||||
|
handleDownloadContent(
|
||||||
|
gcodeFileId,
|
||||||
|
'gcodeFile',
|
||||||
|
`gcodeFile-${gcodeFileId}.gcode`
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
<>
|
||||||
|
<ActionHandler actions={actions} />
|
||||||
<EditObjectForm
|
<EditObjectForm
|
||||||
id={gcodeFileId}
|
id={gcodeFileId}
|
||||||
type='gcodefile'
|
type='gcodefile'
|
||||||
@ -69,11 +88,22 @@ const GCodeFileInfo = () => {
|
|||||||
label: 'Reload GCode File',
|
label: 'Reload GCode File',
|
||||||
key: 'reload',
|
key: 'reload',
|
||||||
icon: <ReloadIcon />
|
icon: <ReloadIcon />
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: 'Download GCode File',
|
||||||
|
key: 'download',
|
||||||
|
icon: <GCodeFileIcon />
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
onClick: ({ key }) => {
|
onClick: ({ key }) => {
|
||||||
if (key === 'reload') {
|
if (key === 'reload') {
|
||||||
fetchObject()
|
fetchObject()
|
||||||
|
} else if (key === 'download' && gcodeFileId) {
|
||||||
|
handleDownloadContent(
|
||||||
|
gcodeFileId,
|
||||||
|
'gcodefile',
|
||||||
|
`gcodefile-${gcodeFileId}.gcode`
|
||||||
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}}
|
}}
|
||||||
@ -156,7 +186,9 @@ const GCodeFileInfo = () => {
|
|||||||
title='Notes'
|
title='Notes'
|
||||||
icon={<NoteIcon />}
|
icon={<NoteIcon />}
|
||||||
active={collapseState.notes}
|
active={collapseState.notes}
|
||||||
onToggle={(expanded) => updateCollapseState('notes', expanded)}
|
onToggle={(expanded) =>
|
||||||
|
updateCollapseState('notes', expanded)
|
||||||
|
}
|
||||||
key='notes'
|
key='notes'
|
||||||
>
|
>
|
||||||
<Card>
|
<Card>
|
||||||
@ -184,6 +216,7 @@ const GCodeFileInfo = () => {
|
|||||||
</Flex>
|
</Flex>
|
||||||
)}
|
)}
|
||||||
</EditObjectForm>
|
</EditObjectForm>
|
||||||
|
</>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -41,8 +41,6 @@ import ListIcon from '../../Icons/ListIcon.jsx'
|
|||||||
import GridIcon from '../../Icons/GridIcon.jsx'
|
import GridIcon from '../../Icons/GridIcon.jsx'
|
||||||
import useViewMode from '../hooks/useViewMode.js'
|
import useViewMode from '../hooks/useViewMode.js'
|
||||||
|
|
||||||
import config from '../../../config.js'
|
|
||||||
|
|
||||||
const { Text } = Typography
|
const { Text } = Typography
|
||||||
|
|
||||||
const Jobs = () => {
|
const Jobs = () => {
|
||||||
@ -362,10 +360,6 @@ const Jobs = () => {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
const visibleColumns = columns.filter(
|
|
||||||
(col) => !col.key || columnVisibility[col.key]
|
|
||||||
)
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
{notificationContextHolder}
|
{notificationContextHolder}
|
||||||
@ -396,8 +390,7 @@ const Jobs = () => {
|
|||||||
|
|
||||||
<ObjectTable
|
<ObjectTable
|
||||||
ref={tableRef}
|
ref={tableRef}
|
||||||
columns={visibleColumns}
|
type={'job'}
|
||||||
url={`${config.backendUrl}/jobs`}
|
|
||||||
authenticated={authenticated}
|
authenticated={authenticated}
|
||||||
cards={viewMode === 'cards'}
|
cards={viewMode === 'cards'}
|
||||||
/>
|
/>
|
||||||
|
|||||||
@ -12,6 +12,7 @@ import EditObjectForm from '../../common/EditObjectForm'
|
|||||||
import EditButtons from '../../common/EditButtons'
|
import EditButtons from '../../common/EditButtons'
|
||||||
import LockIndicator from '../../Management/Filaments/LockIndicator'
|
import LockIndicator from '../../Management/Filaments/LockIndicator'
|
||||||
import SubJobsTree from '../../common/SubJobsTree'
|
import SubJobsTree from '../../common/SubJobsTree'
|
||||||
|
import ActionHandler from '../../common/ActionHandler'
|
||||||
import InfoCircleIcon from '../../../Icons/InfoCircleIcon'
|
import InfoCircleIcon from '../../../Icons/InfoCircleIcon'
|
||||||
import JobIcon from '../../../Icons/JobIcon'
|
import JobIcon from '../../../Icons/JobIcon'
|
||||||
import AuditLogIcon from '../../../Icons/AuditLogIcon'
|
import AuditLogIcon from '../../../Icons/AuditLogIcon'
|
||||||
@ -32,7 +33,14 @@ const JobInfo = () => {
|
|||||||
auditLogs: true
|
auditLogs: true
|
||||||
})
|
})
|
||||||
|
|
||||||
|
// Define actions that can be triggered via URL
|
||||||
|
const actions = {
|
||||||
|
// Add job-specific actions here as needed
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
<>
|
||||||
|
<ActionHandler actions={actions} />
|
||||||
<EditObjectForm
|
<EditObjectForm
|
||||||
id={jobId}
|
id={jobId}
|
||||||
type='job'
|
type='job'
|
||||||
@ -141,7 +149,9 @@ const JobInfo = () => {
|
|||||||
title='Notes'
|
title='Notes'
|
||||||
icon={<NoteIcon />}
|
icon={<NoteIcon />}
|
||||||
active={collapseState.notes}
|
active={collapseState.notes}
|
||||||
onToggle={(expanded) => updateCollapseState('notes', expanded)}
|
onToggle={(expanded) =>
|
||||||
|
updateCollapseState('notes', expanded)
|
||||||
|
}
|
||||||
key='notes'
|
key='notes'
|
||||||
>
|
>
|
||||||
<Card>
|
<Card>
|
||||||
@ -169,6 +179,7 @@ const JobInfo = () => {
|
|||||||
</Flex>
|
</Flex>
|
||||||
)}
|
)}
|
||||||
</EditObjectForm>
|
</EditObjectForm>
|
||||||
|
</>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -1,263 +1,31 @@
|
|||||||
// src/Printers.js
|
// src/Printers.js
|
||||||
|
|
||||||
import React, { useState, useContext, useRef } from 'react'
|
import React, { useState, useContext, useRef } from 'react'
|
||||||
import { useNavigate } from 'react-router-dom'
|
import { Button, message, Dropdown, Space, Flex, Modal } from 'antd'
|
||||||
import {
|
|
||||||
Button,
|
|
||||||
message,
|
|
||||||
Dropdown,
|
|
||||||
Space,
|
|
||||||
Flex,
|
|
||||||
Input,
|
|
||||||
Tag,
|
|
||||||
Modal,
|
|
||||||
Popover,
|
|
||||||
Checkbox
|
|
||||||
} from 'antd'
|
|
||||||
|
|
||||||
import { AuthContext } from '../context/AuthContext'
|
import { AuthContext } from '../context/AuthContext'
|
||||||
import PrinterState from '../common/PrinterState'
|
|
||||||
import NewPrinter from './Printers/NewPrinter'
|
import NewPrinter from './Printers/NewPrinter'
|
||||||
import IdDisplay from '../common/IdDisplay'
|
|
||||||
import PrinterIcon from '../../Icons/PrinterIcon'
|
|
||||||
import InfoCircleIcon from '../../Icons/InfoCircleIcon'
|
|
||||||
import ControlIcon from '../../Icons/ControlIcon'
|
|
||||||
import PlusIcon from '../../Icons/PlusIcon'
|
import PlusIcon from '../../Icons/PlusIcon'
|
||||||
import ReloadIcon from '../../Icons/ReloadIcon'
|
import ReloadIcon from '../../Icons/ReloadIcon'
|
||||||
import XMarkIcon from '../../Icons/XMarkIcon'
|
|
||||||
import CheckIcon from '../../Icons/CheckIcon'
|
|
||||||
import ObjectTable from '../common/ObjectTable'
|
import ObjectTable from '../common/ObjectTable'
|
||||||
|
import ColumnViewButton from '../common/ColumnViewButton'
|
||||||
|
|
||||||
import config from '../../../config'
|
|
||||||
import GridIcon from '../../Icons/GridIcon'
|
import GridIcon from '../../Icons/GridIcon'
|
||||||
import ListIcon from '../../Icons/ListIcon'
|
import ListIcon from '../../Icons/ListIcon'
|
||||||
import useViewMode from '../hooks/useViewMode'
|
import useViewMode from '../hooks/useViewMode'
|
||||||
|
import useColumnVisibility from '../hooks/useColumnVisibility'
|
||||||
|
|
||||||
const Printers = () => {
|
const Printers = () => {
|
||||||
const [messageApi, contextHolder] = message.useMessage()
|
const [messageApi, contextHolder] = message.useMessage()
|
||||||
const { authenticated } = useContext(AuthContext)
|
const { authenticated } = useContext(AuthContext)
|
||||||
const [newPrinterOpen, setNewPrinterOpen] = useState(false)
|
const [newPrinterOpen, setNewPrinterOpen] = useState(false)
|
||||||
const navigate = useNavigate()
|
|
||||||
const tableRef = useRef()
|
const tableRef = useRef()
|
||||||
|
|
||||||
// View mode state (cards/list), persisted in sessionStorage via custom hook
|
// View mode state (cards/list), persisted in sessionStorage via custom hook
|
||||||
const [viewMode, setViewMode] = useViewMode('Printers')
|
const [viewMode, setViewMode] = useViewMode('Printers')
|
||||||
|
|
||||||
// Column definitions
|
// Column visibility state, persisted in sessionStorage via custom hook
|
||||||
const columns = [
|
const [columnVisibility, setColumnVisibility] = useColumnVisibility('printer')
|
||||||
{
|
|
||||||
title: <PrinterIcon />,
|
|
||||||
key: 'icon',
|
|
||||||
width: 40,
|
|
||||||
fixed: 'left',
|
|
||||||
render: () => <PrinterIcon />
|
|
||||||
},
|
|
||||||
{
|
|
||||||
title: 'Name',
|
|
||||||
dataIndex: 'name',
|
|
||||||
key: 'name',
|
|
||||||
width: 200,
|
|
||||||
fixed: 'left',
|
|
||||||
filterDropdown: ({
|
|
||||||
setSelectedKeys,
|
|
||||||
selectedKeys,
|
|
||||||
confirm,
|
|
||||||
clearFilters
|
|
||||||
}) =>
|
|
||||||
getFilterDropdown({
|
|
||||||
setSelectedKeys,
|
|
||||||
selectedKeys,
|
|
||||||
confirm,
|
|
||||||
clearFilters,
|
|
||||||
propertyName: 'name'
|
|
||||||
}),
|
|
||||||
onFilter: (value, record) =>
|
|
||||||
record.name.toLowerCase().includes(value.toLowerCase())
|
|
||||||
},
|
|
||||||
{
|
|
||||||
title: 'ID',
|
|
||||||
dataIndex: '_id',
|
|
||||||
key: 'id',
|
|
||||||
width: 180,
|
|
||||||
render: (text) => <IdDisplay id={text} type='printer' longId={false} />
|
|
||||||
},
|
|
||||||
{
|
|
||||||
title: 'State',
|
|
||||||
dataIndex: 'state',
|
|
||||||
key: 'state',
|
|
||||||
width: 240,
|
|
||||||
render: (state) => {
|
|
||||||
return (
|
|
||||||
<PrinterState state={state} showName={false} showControls={false} />
|
|
||||||
)
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
title: 'Tags',
|
|
||||||
dataIndex: 'tags',
|
|
||||||
key: 'tags',
|
|
||||||
width: 170,
|
|
||||||
render: (tags) => {
|
|
||||||
if (!tags || !Array.isArray(tags)) return 'n/a'
|
|
||||||
if (tags.length == 0) return 'n/a'
|
|
||||||
return (
|
|
||||||
<Space size={[0, 8]} wrap>
|
|
||||||
{tags.map((tag, index) => (
|
|
||||||
<Tag key={index} color='blue'>
|
|
||||||
{tag}
|
|
||||||
</Tag>
|
|
||||||
))}
|
|
||||||
</Space>
|
|
||||||
)
|
|
||||||
},
|
|
||||||
filterDropdown: ({
|
|
||||||
setSelectedKeys,
|
|
||||||
selectedKeys,
|
|
||||||
confirm,
|
|
||||||
clearFilters
|
|
||||||
}) =>
|
|
||||||
getFilterDropdown({
|
|
||||||
setSelectedKeys,
|
|
||||||
selectedKeys,
|
|
||||||
confirm,
|
|
||||||
clearFilters,
|
|
||||||
propertyName: 'tags'
|
|
||||||
}),
|
|
||||||
onFilter: (value, record) =>
|
|
||||||
record.tags &&
|
|
||||||
record.tags.some((tag) =>
|
|
||||||
tag.toLowerCase().includes(value.toLowerCase())
|
|
||||||
)
|
|
||||||
},
|
|
||||||
{
|
|
||||||
title: 'Actions',
|
|
||||||
key: 'actions',
|
|
||||||
fixed: 'right',
|
|
||||||
width: 150,
|
|
||||||
render: (record) => {
|
|
||||||
return (
|
|
||||||
<Space gap='small'>
|
|
||||||
<Button
|
|
||||||
icon={<ControlIcon />}
|
|
||||||
onClick={() =>
|
|
||||||
navigate(
|
|
||||||
`/dashboard/production/printers/control?printerId=${record._id}`
|
|
||||||
)
|
|
||||||
}
|
|
||||||
/>
|
|
||||||
<Dropdown menu={getPrinterActionItems(record._id)}>
|
|
||||||
<Button>Actions</Button>
|
|
||||||
</Dropdown>
|
|
||||||
</Space>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
]
|
|
||||||
|
|
||||||
const [columnVisibility, setColumnVisibility] = useState(
|
|
||||||
columns.reduce((acc, col) => {
|
|
||||||
if (col.key) {
|
|
||||||
acc[col.key] = true
|
|
||||||
}
|
|
||||||
return acc
|
|
||||||
}, {})
|
|
||||||
)
|
|
||||||
|
|
||||||
const getFilterDropdown = ({
|
|
||||||
setSelectedKeys,
|
|
||||||
selectedKeys,
|
|
||||||
confirm,
|
|
||||||
clearFilters,
|
|
||||||
propertyName
|
|
||||||
}) => {
|
|
||||||
return (
|
|
||||||
<div style={{ padding: 8 }}>
|
|
||||||
<Space.Compact>
|
|
||||||
<Input
|
|
||||||
placeholder={'Search ' + propertyName}
|
|
||||||
value={selectedKeys[0]}
|
|
||||||
onChange={(e) =>
|
|
||||||
setSelectedKeys(e.target.value ? [e.target.value] : [])
|
|
||||||
}
|
|
||||||
onPressEnter={() => confirm()}
|
|
||||||
style={{ width: 200, display: 'block' }}
|
|
||||||
/>
|
|
||||||
<Button
|
|
||||||
onClick={() => {
|
|
||||||
clearFilters()
|
|
||||||
confirm()
|
|
||||||
}}
|
|
||||||
icon={<XMarkIcon />}
|
|
||||||
/>
|
|
||||||
<Button
|
|
||||||
type='primary'
|
|
||||||
onClick={() => confirm()}
|
|
||||||
icon={<CheckIcon />}
|
|
||||||
/>
|
|
||||||
</Space.Compact>
|
|
||||||
</div>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
const getPrinterActionItems = (printerId) => {
|
|
||||||
return {
|
|
||||||
items: [
|
|
||||||
{
|
|
||||||
label: 'Control',
|
|
||||||
key: 'control',
|
|
||||||
icon: <ControlIcon />
|
|
||||||
},
|
|
||||||
{
|
|
||||||
type: 'divider'
|
|
||||||
},
|
|
||||||
{
|
|
||||||
label: 'Info',
|
|
||||||
key: 'info',
|
|
||||||
icon: <InfoCircleIcon />
|
|
||||||
}
|
|
||||||
],
|
|
||||||
onClick: ({ key }) => {
|
|
||||||
if (key === 'info') {
|
|
||||||
navigate(`/dashboard/production/printers/info?printerId=${printerId}`)
|
|
||||||
} else if (key === 'control') {
|
|
||||||
navigate(
|
|
||||||
`/dashboard/production/printers/control?printerId=${printerId}`
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const getViewDropdownItems = () => {
|
|
||||||
const columnItems = columns
|
|
||||||
.filter((col) => col.key && col.title !== '')
|
|
||||||
.map((col) => (
|
|
||||||
<Checkbox
|
|
||||||
checked={columnVisibility[col.key]}
|
|
||||||
key={col.key}
|
|
||||||
onChange={(e) => {
|
|
||||||
setColumnVisibility((prev) => ({
|
|
||||||
...prev,
|
|
||||||
[col.key]: e.target.checked
|
|
||||||
}))
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
{col.title}
|
|
||||||
</Checkbox>
|
|
||||||
))
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Flex vertical>
|
|
||||||
<Flex vertical gap='middle' style={{ margin: '4px 8px' }}>
|
|
||||||
{columnItems}
|
|
||||||
</Flex>
|
|
||||||
</Flex>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
const visibleColumns = columns.filter(
|
|
||||||
(col) => !col.key || columnVisibility[col.key]
|
|
||||||
)
|
|
||||||
|
|
||||||
const actionItems = {
|
const actionItems = {
|
||||||
items: [
|
items: [
|
||||||
@ -291,13 +59,12 @@ const Printers = () => {
|
|||||||
<Dropdown menu={actionItems}>
|
<Dropdown menu={actionItems}>
|
||||||
<Button>Actions</Button>
|
<Button>Actions</Button>
|
||||||
</Dropdown>
|
</Dropdown>
|
||||||
<Popover
|
<ColumnViewButton
|
||||||
content={getViewDropdownItems()}
|
type='printer'
|
||||||
placement='bottomLeft'
|
loading={false}
|
||||||
arrow={false}
|
collapseState={columnVisibility}
|
||||||
>
|
updateCollapseState={setColumnVisibility}
|
||||||
<Button>View</Button>
|
/>
|
||||||
</Popover>
|
|
||||||
</Space>
|
</Space>
|
||||||
<Space>
|
<Space>
|
||||||
<Button
|
<Button
|
||||||
@ -311,10 +78,10 @@ const Printers = () => {
|
|||||||
|
|
||||||
<ObjectTable
|
<ObjectTable
|
||||||
ref={tableRef}
|
ref={tableRef}
|
||||||
columns={visibleColumns}
|
type={'printer'}
|
||||||
url={`${config.backendUrl}/printers`}
|
|
||||||
authenticated={authenticated}
|
authenticated={authenticated}
|
||||||
cards={viewMode === 'cards'}
|
cards={viewMode === 'cards'}
|
||||||
|
visibleColumns={columnVisibility}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<Modal
|
<Modal
|
||||||
|
|||||||
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 React from 'react'
|
||||||
import PropTypes from 'prop-types'
|
import PropTypes from 'prop-types'
|
||||||
import { Typography, Flex, Button, Tooltip } from 'antd'
|
import { Typography, Flex, Button, Tooltip } from 'antd'
|
||||||
import NewMailIcon from './NewMailIcon'
|
import NewMailIcon from '../../Icons/NewMailIcon'
|
||||||
// import CopyIcon from './CopyIcon'
|
// import CopyIcon from './CopyIcon'
|
||||||
import CopyButton from '../Dashboard/common/CopyButton'
|
import CopyButton from './CopyButton'
|
||||||
|
|
||||||
const { Text, Link } = Typography
|
const { Text, Link } = Typography
|
||||||
|
|
||||||
@ -19,7 +19,9 @@ const EmailDisplay = ({ email, showCopy = true, showLink = false }) => {
|
|||||||
</Link>
|
</Link>
|
||||||
) : (
|
) : (
|
||||||
<>
|
<>
|
||||||
<Text style={{ marginRight: 8 }}>{email}</Text>
|
<Text style={{ marginRight: 8 }} ellipsis>
|
||||||
|
{email}
|
||||||
|
</Text>
|
||||||
<Tooltip title='Email' arrow={false}>
|
<Tooltip title='Email' arrow={false}>
|
||||||
<Button
|
<Button
|
||||||
icon={<NewMailIcon style={{ fontSize: '14px' }} />}
|
icon={<NewMailIcon style={{ fontSize: '14px' }} />}
|
||||||
@ -23,10 +23,20 @@ const IdDisplay = ({
|
|||||||
|
|
||||||
const model = getModelByName(type)
|
const model = getModelByName(type)
|
||||||
const prefix = model.prefix
|
const prefix = model.prefix
|
||||||
const hyperlink = model.url(id)
|
|
||||||
const IconComponent = model.icon
|
const IconComponent = model.icon
|
||||||
const icon = <IconComponent style={{ paddingTop: '4px' }} />
|
const icon = <IconComponent style={{ paddingTop: '4px' }} />
|
||||||
|
|
||||||
|
var hyperlink = null
|
||||||
|
|
||||||
|
const defaultModelActions = model.actions.filter(
|
||||||
|
(action) => action.default == true
|
||||||
|
)
|
||||||
|
|
||||||
|
if (defaultModelActions.length >= 1) {
|
||||||
|
hyperlink = defaultModelActions[0].url(id)
|
||||||
|
}
|
||||||
|
|
||||||
if (!id) {
|
if (!id) {
|
||||||
return <Text type='secondary'>n/a</Text>
|
return <Text type='secondary'>n/a</Text>
|
||||||
}
|
}
|
||||||
@ -41,8 +51,37 @@ const IdDisplay = ({
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<Flex align={'center'} className='iddisplay'>
|
<Flex align={'center'} className='iddisplay'>
|
||||||
{showHyperlink &&
|
{(() => {
|
||||||
(showSpotlight ? (
|
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={() => navigate(hyperlink)}
|
||||||
|
style={{ marginRight: 6 }}
|
||||||
|
>
|
||||||
|
{textElement}
|
||||||
|
</Link>
|
||||||
|
)
|
||||||
|
|
||||||
|
if (showSpotlight) {
|
||||||
|
return (
|
||||||
<Popover
|
<Popover
|
||||||
content={
|
content={
|
||||||
id && type ? (
|
id && type ? (
|
||||||
@ -54,41 +93,16 @@ const IdDisplay = ({
|
|||||||
arrow={false}
|
arrow={false}
|
||||||
style={{ padding: 0 }}
|
style={{ padding: 0 }}
|
||||||
>
|
>
|
||||||
<Link
|
{linkElement}
|
||||||
onClick={() => {
|
|
||||||
if (showHyperlink) {
|
|
||||||
navigate(hyperlink)
|
|
||||||
}
|
|
||||||
}}
|
|
||||||
style={{ marginRight: 6 }}
|
|
||||||
>
|
|
||||||
<Text code ellipsis>
|
|
||||||
<Space size={4}>
|
|
||||||
{icon}
|
|
||||||
{displayId}
|
|
||||||
</Space>
|
|
||||||
</Text>
|
|
||||||
</Link>
|
|
||||||
</Popover>
|
</Popover>
|
||||||
) : (
|
)
|
||||||
<Link
|
}
|
||||||
onClick={() => {
|
return linkElement
|
||||||
if (showHyperlink) {
|
|
||||||
navigate(hyperlink)
|
|
||||||
}
|
}
|
||||||
}}
|
|
||||||
>
|
|
||||||
<Text code ellipsis>
|
|
||||||
<Space size={4}>
|
|
||||||
{icon}
|
|
||||||
{displayId}
|
|
||||||
</Space>
|
|
||||||
</Text>
|
|
||||||
</Link>
|
|
||||||
))}
|
|
||||||
|
|
||||||
{!showHyperlink &&
|
// If hyperlink is disabled
|
||||||
(showSpotlight ? (
|
if (showSpotlight) {
|
||||||
|
return (
|
||||||
<Popover
|
<Popover
|
||||||
content={
|
content={
|
||||||
id && type ? (
|
id && type ? (
|
||||||
@ -99,21 +113,13 @@ const IdDisplay = ({
|
|||||||
placement='topLeft'
|
placement='topLeft'
|
||||||
arrow={false}
|
arrow={false}
|
||||||
>
|
>
|
||||||
<Text code ellipsis style={{ marginRight: 6 }}>
|
{textElement}
|
||||||
<Space size={4}>
|
|
||||||
{icon}
|
|
||||||
{displayId}
|
|
||||||
</Space>
|
|
||||||
</Text>
|
|
||||||
</Popover>
|
</Popover>
|
||||||
) : (
|
)
|
||||||
<Text code ellipsis>
|
}
|
||||||
<Space size={4}>
|
return textElement
|
||||||
{icon}
|
})()}
|
||||||
{displayId}
|
|
||||||
</Space>
|
|
||||||
</Text>
|
|
||||||
))}
|
|
||||||
{showCopy && (
|
{showCopy && (
|
||||||
<CopyButton
|
<CopyButton
|
||||||
text={copyId}
|
text={copyId}
|
||||||
|
|||||||
@ -18,8 +18,8 @@ import dayjs from 'dayjs'
|
|||||||
import PrinterSelect from './PrinterSelect'
|
import PrinterSelect from './PrinterSelect'
|
||||||
import GCodeFileSelect from './GCodeFileSelect'
|
import GCodeFileSelect from './GCodeFileSelect'
|
||||||
import PartSelect from './PartSelect'
|
import PartSelect from './PartSelect'
|
||||||
import EmailDisplay from '../../Icons/EmailDisplay'
|
import EmailDisplay from './EmailDisplay'
|
||||||
import UrlDisplay from '../../Icons/UrlDisplay'
|
import UrlDisplay from './UrlDisplay'
|
||||||
import CountryDisplay from './CountryDisplay'
|
import CountryDisplay from './CountryDisplay'
|
||||||
import CountrySelect from './CountrySelect'
|
import CountrySelect from './CountrySelect'
|
||||||
import TagsDisplay from './TagsDisplay'
|
import TagsDisplay from './TagsDisplay'
|
||||||
@ -138,7 +138,7 @@ const ObjectProperty = ({
|
|||||||
}
|
}
|
||||||
case 'text':
|
case 'text':
|
||||||
if (value != null && value != '') {
|
if (value != null && value != '') {
|
||||||
return <Text>{value}</Text>
|
return <Text ellipsis>{value}</Text>
|
||||||
} else {
|
} else {
|
||||||
return <Text type='secondary'>n/a</Text>
|
return <Text type='secondary'>n/a</Text>
|
||||||
}
|
}
|
||||||
@ -156,7 +156,7 @@ const ObjectProperty = ({
|
|||||||
}
|
}
|
||||||
case 'object': {
|
case 'object': {
|
||||||
if (value && value.name) {
|
if (value && value.name) {
|
||||||
return <Text>{value.name}</Text>
|
return <Text ellipsis>{value.name}</Text>
|
||||||
} else {
|
} else {
|
||||||
return <Text type='secondary'>n/a</Text>
|
return <Text type='secondary'>n/a</Text>
|
||||||
}
|
}
|
||||||
|
|||||||
@ -15,32 +15,50 @@ import {
|
|||||||
Col,
|
Col,
|
||||||
Descriptions,
|
Descriptions,
|
||||||
Flex,
|
Flex,
|
||||||
Spin
|
Spin,
|
||||||
|
Button,
|
||||||
|
Input,
|
||||||
|
Space,
|
||||||
|
Tooltip
|
||||||
} from 'antd'
|
} from 'antd'
|
||||||
import { LoadingOutlined } from '@ant-design/icons'
|
import { LoadingOutlined } from '@ant-design/icons'
|
||||||
import PropTypes from 'prop-types'
|
import PropTypes from 'prop-types'
|
||||||
import { useMediaQuery } from 'react-responsive'
|
import { useMediaQuery } from 'react-responsive'
|
||||||
import axios from 'axios'
|
import { useContext } from 'react'
|
||||||
|
import { ApiServerContext } from '../context/ApiServerContext'
|
||||||
import config from '../../../config'
|
import config from '../../../config'
|
||||||
import loglevel from 'loglevel'
|
import loglevel from 'loglevel'
|
||||||
|
import {
|
||||||
|
getModelProperties,
|
||||||
|
getModelByName,
|
||||||
|
getPropertyValue
|
||||||
|
} from '../../../database/ObjectModels'
|
||||||
|
import ObjectProperty from './ObjectProperty'
|
||||||
|
import XMarkIcon from '../../Icons/XMarkIcon'
|
||||||
|
import CheckIcon from '../../Icons/CheckIcon'
|
||||||
|
import { useNavigate } from 'react-router-dom'
|
||||||
|
import QuestionCircleIcon from '../../Icons/QuestionCircleIcon'
|
||||||
const logger = loglevel.getLogger('DasboardTable')
|
const logger = loglevel.getLogger('DasboardTable')
|
||||||
logger.setLevel(config.logLevel)
|
logger.setLevel(config.logLevel)
|
||||||
|
|
||||||
const ObjectTable = forwardRef(
|
const ObjectTable = forwardRef(
|
||||||
(
|
(
|
||||||
{
|
{
|
||||||
columns,
|
type,
|
||||||
url,
|
url,
|
||||||
pageSize = 25,
|
pageSize = 25,
|
||||||
scrollHeight = 'calc(var(--unit-100vh) - 270px)',
|
scrollHeight = 'calc(var(--unit-100vh) - 270px)',
|
||||||
onDataChange,
|
onDataChange,
|
||||||
authenticated,
|
authenticated,
|
||||||
initialPage = 1,
|
initialPage = 1,
|
||||||
cards = false
|
cards = false,
|
||||||
|
visibleColumns = {}
|
||||||
},
|
},
|
||||||
ref
|
ref
|
||||||
) => {
|
) => {
|
||||||
|
const { fetchTableData } = useContext(ApiServerContext)
|
||||||
const isMobile = useMediaQuery({ maxWidth: 768 })
|
const isMobile = useMediaQuery({ maxWidth: 768 })
|
||||||
|
const navigate = useNavigate()
|
||||||
var adjustedScrollHeight = scrollHeight
|
var adjustedScrollHeight = scrollHeight
|
||||||
if (isMobile) {
|
if (isMobile) {
|
||||||
adjustedScrollHeight = 'calc(var(--unit-100vh) - 316px)'
|
adjustedScrollHeight = 'calc(var(--unit-100vh) - 316px)'
|
||||||
@ -50,8 +68,8 @@ const ObjectTable = forwardRef(
|
|||||||
}
|
}
|
||||||
const [, contextHolder] = message.useMessage()
|
const [, contextHolder] = message.useMessage()
|
||||||
const tableRef = useRef(null)
|
const tableRef = useRef(null)
|
||||||
const [filters, setFilters] = useState({})
|
const [tableFilter, setTableFilter] = useState({})
|
||||||
const [sorter, setSorter] = useState({})
|
const [tableSorter, setTableSorter] = useState({})
|
||||||
const [initialized, setInitialized] = useState(false)
|
const [initialized, setInitialized] = useState(false)
|
||||||
|
|
||||||
// Table state
|
// Table state
|
||||||
@ -59,7 +77,6 @@ const ObjectTable = forwardRef(
|
|||||||
const [hasMore, setHasMore] = useState(true)
|
const [hasMore, setHasMore] = useState(true)
|
||||||
const [loading, setLoading] = useState(true)
|
const [loading, setLoading] = useState(true)
|
||||||
const [lazyLoading, setLazyLoading] = useState(false)
|
const [lazyLoading, setLazyLoading] = useState(false)
|
||||||
const [totalPages, setTotalPages] = useState(0)
|
|
||||||
|
|
||||||
const createSkeletonData = useCallback(() => {
|
const createSkeletonData = useCallback(() => {
|
||||||
return Array(pageSize)
|
return Array(pageSize)
|
||||||
@ -71,30 +88,31 @@ const ObjectTable = forwardRef(
|
|||||||
}, [pageSize])
|
}, [pageSize])
|
||||||
|
|
||||||
const fetchData = useCallback(
|
const fetchData = useCallback(
|
||||||
async (pageNum = 1) => {
|
async (pageNum = 1, filter = null, sorter = null) => {
|
||||||
|
if (filter == null) {
|
||||||
|
filter = tableFilter
|
||||||
|
} else {
|
||||||
|
setTableFilter(filter)
|
||||||
|
}
|
||||||
|
if (sorter == null) {
|
||||||
|
sorter = tableSorter
|
||||||
|
} else {
|
||||||
|
setTableSorter({
|
||||||
|
field: sorter.field,
|
||||||
|
order: sorter.order
|
||||||
|
})
|
||||||
|
}
|
||||||
|
console.log('filter 2', filter)
|
||||||
try {
|
try {
|
||||||
const response = await axios.get(url, {
|
const result = await fetchTableData(type, {
|
||||||
params: {
|
|
||||||
page: pageNum,
|
page: pageNum,
|
||||||
limit: pageSize,
|
limit: pageSize,
|
||||||
...filters,
|
filter,
|
||||||
sort: sorter.field,
|
sorter,
|
||||||
order: sorter.order
|
onDataChange
|
||||||
},
|
|
||||||
headers: {
|
|
||||||
Accept: 'application/json'
|
|
||||||
},
|
|
||||||
withCredentials: true
|
|
||||||
})
|
})
|
||||||
|
|
||||||
const newData = response.data
|
setHasMore(result.hasMore)
|
||||||
const totalCount = parseInt(
|
|
||||||
response.headers['x-total-count'] || '0',
|
|
||||||
10
|
|
||||||
)
|
|
||||||
setTotalPages(Math.ceil(totalCount / pageSize))
|
|
||||||
|
|
||||||
setHasMore(newData.length >= pageSize)
|
|
||||||
|
|
||||||
setPages((prev) => {
|
setPages((prev) => {
|
||||||
const existingPageIndex = prev.findIndex(
|
const existingPageIndex = prev.findIndex(
|
||||||
@ -104,20 +122,16 @@ const ObjectTable = forwardRef(
|
|||||||
if (existingPageIndex !== -1) {
|
if (existingPageIndex !== -1) {
|
||||||
// Update existing page
|
// Update existing page
|
||||||
const newPages = [...prev]
|
const newPages = [...prev]
|
||||||
newPages[existingPageIndex] = { pageNum, items: newData }
|
newPages[existingPageIndex] = { pageNum, items: result.data }
|
||||||
return newPages
|
return newPages
|
||||||
}
|
}
|
||||||
// If page doesn't exist, return unchanged
|
// If page doesn't exist, return unchanged
|
||||||
return prev
|
return prev
|
||||||
})
|
})
|
||||||
|
|
||||||
if (onDataChange) {
|
|
||||||
onDataChange(newData)
|
|
||||||
}
|
|
||||||
|
|
||||||
setLoading(false)
|
setLoading(false)
|
||||||
setLazyLoading(false)
|
setLazyLoading(false)
|
||||||
return newData
|
return result.data
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
setPages((prev) =>
|
setPages((prev) =>
|
||||||
prev.map((page) => ({
|
prev.map((page) => ({
|
||||||
@ -130,7 +144,7 @@ const ObjectTable = forwardRef(
|
|||||||
throw error
|
throw error
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
[url, pageSize, filters, sorter, onDataChange]
|
[url, pageSize, tableFilter, tableSorter, onDataChange, fetchTableData]
|
||||||
)
|
)
|
||||||
|
|
||||||
const loadNextPage = useCallback(() => {
|
const loadNextPage = useCallback(() => {
|
||||||
@ -233,40 +247,34 @@ const ObjectTable = forwardRef(
|
|||||||
)
|
)
|
||||||
}, [])
|
}, [])
|
||||||
|
|
||||||
const goToPage = useCallback(
|
const loadPage = useCallback(
|
||||||
(pageNum) => {
|
async (pageNum, filter = null, sorter = null) => {
|
||||||
if (pageNum > 0 && pageNum <= totalPages) {
|
|
||||||
const pagesToLoad = [pageNum - 1, pageNum, pageNum + 1].filter(
|
|
||||||
(p) => p > 0 && p <= totalPages
|
|
||||||
)
|
|
||||||
return Promise.all(pagesToLoad.map((p) => fetchData(p)))
|
|
||||||
}
|
|
||||||
},
|
|
||||||
[fetchData, totalPages]
|
|
||||||
)
|
|
||||||
|
|
||||||
const loadInitialPage = useCallback(async () => {
|
|
||||||
// Create initial page with skeletons
|
// Create initial page with skeletons
|
||||||
setPages([{ pageNum: initialPage, items: createSkeletonData() }])
|
setPages([{ pageNum: pageNum, items: createSkeletonData() }])
|
||||||
|
|
||||||
const items = await fetchData(initialPage)
|
const items = await fetchData(pageNum, filter, sorter)
|
||||||
|
|
||||||
if (items.length >= 25) {
|
if (items.length >= 25) {
|
||||||
setPages((prev) => [
|
setPages((prev) => [
|
||||||
...prev,
|
...prev,
|
||||||
{ pageNum: initialPage + 1, items: createSkeletonData() }
|
{ pageNum: pageNum + 1, items: createSkeletonData() }
|
||||||
])
|
])
|
||||||
await fetchData(initialPage + 1)
|
await fetchData(pageNum + 1, filter, sorter)
|
||||||
}
|
}
|
||||||
}, [initialPage, createSkeletonData, fetchData])
|
},
|
||||||
|
[createSkeletonData, fetchData]
|
||||||
|
)
|
||||||
|
|
||||||
|
const loadInitialPage = useCallback(async () => {
|
||||||
|
loadPage(initialPage)
|
||||||
|
}, [initialPage, loadPage])
|
||||||
|
|
||||||
useImperativeHandle(ref, () => ({
|
useImperativeHandle(ref, () => ({
|
||||||
reload,
|
reload,
|
||||||
setData: (newData) => {
|
setData: (newData) => {
|
||||||
setPages([{ pageNum: 1, items: newData }])
|
setPages([{ pageNum: 1, items: newData }])
|
||||||
},
|
},
|
||||||
updateData,
|
updateData
|
||||||
goToPage
|
|
||||||
}))
|
}))
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
@ -276,33 +284,207 @@ const ObjectTable = forwardRef(
|
|||||||
}
|
}
|
||||||
}, [authenticated, loadInitialPage, initialPage, pages, initialized])
|
}, [authenticated, loadInitialPage, initialPage, pages, initialized])
|
||||||
|
|
||||||
|
const getFilterDropdown = ({
|
||||||
|
setSelectedKeys,
|
||||||
|
selectedKeys,
|
||||||
|
confirm,
|
||||||
|
clearFilters,
|
||||||
|
propertyName
|
||||||
|
}) => {
|
||||||
|
return (
|
||||||
|
<div style={{ padding: 8 }}>
|
||||||
|
<Space.Compact>
|
||||||
|
<Input
|
||||||
|
placeholder={'Search ' + propertyName}
|
||||||
|
value={selectedKeys[0]}
|
||||||
|
onChange={(e) =>
|
||||||
|
setSelectedKeys(e.target.value ? [e.target.value] : [])
|
||||||
|
}
|
||||||
|
onPressEnter={() => confirm()}
|
||||||
|
style={{ width: 200, display: 'block' }}
|
||||||
|
/>
|
||||||
|
<Button
|
||||||
|
onClick={() => {
|
||||||
|
clearFilters()
|
||||||
|
confirm()
|
||||||
|
}}
|
||||||
|
icon={<XMarkIcon />}
|
||||||
|
/>
|
||||||
|
<Button
|
||||||
|
type='primary'
|
||||||
|
onClick={() => confirm()}
|
||||||
|
icon={<CheckIcon />}
|
||||||
|
/>
|
||||||
|
</Space.Compact>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
const handleTableChange = (pagination, filters, sorter) => {
|
const handleTableChange = (pagination, filters, sorter) => {
|
||||||
const newFilters = {}
|
const newFilters = {}
|
||||||
|
|
||||||
Object.entries(filters).forEach(([key, value]) => {
|
Object.entries(filters).forEach(([key, value]) => {
|
||||||
if (value && value.length > 0) {
|
if (value && value.length > 0) {
|
||||||
newFilters[key] = value[0]
|
newFilters[key] = value[0]
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
setFilters(newFilters)
|
|
||||||
setSorter({
|
setPages([])
|
||||||
|
setLoading(true)
|
||||||
|
loadPage(initialPage, newFilters, {
|
||||||
field: sorter.field,
|
field: sorter.field,
|
||||||
order: sorter.order
|
order: sorter.order
|
||||||
})
|
})
|
||||||
setPages([])
|
|
||||||
fetchData(1)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const columnsWithSkeleton = columns.map((col) => ({
|
const modelProperties = getModelProperties(type)
|
||||||
...col,
|
const model = getModelByName(type)
|
||||||
|
|
||||||
|
// 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) => {
|
render: (text, record) => {
|
||||||
if (record.isSkeleton) {
|
if (record.isSkeleton) {
|
||||||
return (
|
return (
|
||||||
<Skeleton.Input active size='small' style={{ width: '100%' }} />
|
<Skeleton.Input active size='small' style={{ width: '100%' }} />
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
return col.render ? col.render(text, record) : text
|
return (
|
||||||
|
<ObjectProperty
|
||||||
|
{...prop}
|
||||||
|
longId={false}
|
||||||
|
type={prop.type}
|
||||||
|
objectType={prop.objectType}
|
||||||
|
value={getPropertyValue(record, prop.name)}
|
||||||
|
isEditing={false}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add filter configuration if the property is filterable
|
||||||
|
if (isFilterable) {
|
||||||
|
columnConfig.filterDropdown = ({
|
||||||
|
setSelectedKeys,
|
||||||
|
selectedKeys,
|
||||||
|
confirm,
|
||||||
|
clearFilters
|
||||||
|
}) =>
|
||||||
|
getFilterDropdown({
|
||||||
|
setSelectedKeys,
|
||||||
|
selectedKeys,
|
||||||
|
confirm,
|
||||||
|
clearFilters,
|
||||||
|
propertyName: prop.label
|
||||||
|
})
|
||||||
|
// Remove local filtering - let the server handle it
|
||||||
|
columnConfig.filtered = false
|
||||||
|
}
|
||||||
|
|
||||||
|
columnsWithSkeleton.push(columnConfig)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
if (model.actions.length > 0) {
|
||||||
|
const rowActions = model.actions.filter((action) => action.row == true)
|
||||||
|
if (rowActions.length > 0) {
|
||||||
|
columnsWithSkeleton.push({
|
||||||
|
title: (
|
||||||
|
<Flex gap='small' align='center' justify='center'>
|
||||||
|
{'Actions'}
|
||||||
|
</Flex>
|
||||||
|
),
|
||||||
|
key: 'actions',
|
||||||
|
fixed: 'right',
|
||||||
|
width: 80 + rowActions.length * 40, // Adjust width based on number of actions
|
||||||
|
render: (record) => {
|
||||||
|
return (
|
||||||
|
<Flex gap='small' align='center' justify='center'>
|
||||||
|
{rowActions.map((action, index) => (
|
||||||
|
<Tooltip key={index} title={action.label} arrow={false}>
|
||||||
|
<Button
|
||||||
|
icon={
|
||||||
|
action.icon ? (
|
||||||
|
React.createElement(action.icon)
|
||||||
|
) : (
|
||||||
|
<QuestionCircleIcon />
|
||||||
|
)
|
||||||
|
}
|
||||||
|
type={'text'}
|
||||||
|
size={'small'}
|
||||||
|
onClick={() => {
|
||||||
|
if (action.onClick) {
|
||||||
|
action.onClick(record)
|
||||||
|
} else if (action.url) {
|
||||||
|
navigate(action.url(record._id))
|
||||||
|
} else {
|
||||||
|
navigate(model.url(record._id))
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</Tooltip>
|
||||||
|
))}
|
||||||
|
</Flex>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}))
|
|
||||||
|
|
||||||
// Flatten pages array for table display
|
// Flatten pages array for table display
|
||||||
const tableData = pages.flatMap((page) => page.items)
|
const tableData = pages.flatMap((page) => page.items)
|
||||||
@ -355,72 +537,31 @@ const ObjectTable = forwardRef(
|
|||||||
style={{ overflowY: 'auto', maxHeight: adjustedScrollHeight }}
|
style={{ overflowY: 'auto', maxHeight: adjustedScrollHeight }}
|
||||||
ref={cardsContainerRef}
|
ref={cardsContainerRef}
|
||||||
>
|
>
|
||||||
{tableData.map((record) => {
|
{tableData.map((record) => (
|
||||||
// Special case for columns[0] if needed
|
<Col xs={24} sm={12} md={12} lg={8} xl={6} xxl={6} key={record._id}>
|
||||||
let icon = null
|
|
||||||
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}
|
|
||||||
>
|
|
||||||
<Card
|
<Card
|
||||||
style={{ width: '100%', overflow: 'hidden' }}
|
style={{ width: '100%', overflow: 'hidden' }}
|
||||||
loading={record.isSkeleton}
|
loading={record.isSkeleton}
|
||||||
>
|
>
|
||||||
<Flex align={'center'} vertical gap={'middle'}>
|
<Flex align={'center'} vertical gap={'middle'}>
|
||||||
{icon}
|
|
||||||
<Descriptions column={1} size='small' bordered={false}>
|
<Descriptions column={1} size='small' bordered={false}>
|
||||||
{columns
|
{modelProperties.map((prop) => (
|
||||||
.filter(
|
<Descriptions.Item label={prop.label} key={prop.name}>
|
||||||
(col) => col.key !== 'icon' && col.key !== 'actions'
|
<ObjectProperty
|
||||||
)
|
{...prop}
|
||||||
.map((col) => {
|
longId={false}
|
||||||
let value
|
type={prop.type}
|
||||||
if (col.render && col.dataIndex) {
|
objectType={prop.objectType}
|
||||||
value = col.render(record[col.dataIndex], record)
|
value={getPropertyValue(record, prop.name)}
|
||||||
} else if (col.render && !col.dataIndex) {
|
isEditing={false}
|
||||||
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.Item>
|
||||||
)
|
))}
|
||||||
})}
|
|
||||||
</Descriptions>
|
</Descriptions>
|
||||||
{actions}
|
|
||||||
</Flex>
|
</Flex>
|
||||||
</Card>
|
</Card>
|
||||||
</Col>
|
</Col>
|
||||||
)
|
))}
|
||||||
})}
|
|
||||||
</Row>
|
</Row>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
@ -456,7 +597,7 @@ const ObjectTable = forwardRef(
|
|||||||
ObjectTable.displayName = 'ObjectTable'
|
ObjectTable.displayName = 'ObjectTable'
|
||||||
|
|
||||||
ObjectTable.propTypes = {
|
ObjectTable.propTypes = {
|
||||||
columns: PropTypes.arrayOf(PropTypes.object).isRequired,
|
type: PropTypes.string.isRequired,
|
||||||
url: PropTypes.string.isRequired,
|
url: PropTypes.string.isRequired,
|
||||||
pageSize: PropTypes.number,
|
pageSize: PropTypes.number,
|
||||||
scrollHeight: PropTypes.string,
|
scrollHeight: PropTypes.string,
|
||||||
@ -464,7 +605,8 @@ ObjectTable.propTypes = {
|
|||||||
authenticated: PropTypes.bool.isRequired,
|
authenticated: PropTypes.bool.isRequired,
|
||||||
initialPage: PropTypes.number,
|
initialPage: PropTypes.number,
|
||||||
cards: PropTypes.bool,
|
cards: PropTypes.bool,
|
||||||
cardRenderer: PropTypes.func
|
cardRenderer: PropTypes.func,
|
||||||
|
visibleColumns: PropTypes.object
|
||||||
}
|
}
|
||||||
|
|
||||||
export default ObjectTable
|
export default ObjectTable
|
||||||
|
|||||||
@ -1,8 +1,8 @@
|
|||||||
import React from 'react'
|
import React from 'react'
|
||||||
import PropTypes from 'prop-types'
|
import PropTypes from 'prop-types'
|
||||||
import { Typography, Flex, Button, Tooltip } from 'antd'
|
import { Typography, Flex, Button, Tooltip } from 'antd'
|
||||||
import LinkIcon from './LinkIcon'
|
import LinkIcon from '../../Icons/LinkIcon'
|
||||||
import CopyButton from '../Dashboard/common/CopyButton'
|
import CopyButton from './CopyButton'
|
||||||
|
|
||||||
const { Text, Link } = Typography
|
const { Text, Link } = Typography
|
||||||
|
|
||||||
@ -4,7 +4,7 @@ import PropTypes from 'prop-types'
|
|||||||
|
|
||||||
const ViewButton = ({
|
const ViewButton = ({
|
||||||
loading = false,
|
loading = false,
|
||||||
sections = [],
|
properties = [],
|
||||||
collapseState = {},
|
collapseState = {},
|
||||||
updateCollapseState = () => {},
|
updateCollapseState = () => {},
|
||||||
...buttonProps
|
...buttonProps
|
||||||
@ -15,15 +15,15 @@ const ViewButton = ({
|
|||||||
return (
|
return (
|
||||||
<Flex vertical>
|
<Flex vertical>
|
||||||
<Flex vertical gap='middle' style={{ margin: '4px 8px' }}>
|
<Flex vertical gap='middle' style={{ margin: '4px 8px' }}>
|
||||||
{sections.map((section) => (
|
{properties.map((property) => (
|
||||||
<Checkbox
|
<Checkbox
|
||||||
checked={collapseState[section.key]}
|
checked={collapseState[property.key]}
|
||||||
key={section.key}
|
key={property.key}
|
||||||
onChange={(e) => {
|
onChange={(e) => {
|
||||||
updateCollapseState(section.key, e.target.checked)
|
updateCollapseState(property.key, e.target.checked)
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{section.label}
|
{property.label}
|
||||||
</Checkbox>
|
</Checkbox>
|
||||||
))}
|
))}
|
||||||
</Flex>
|
</Flex>
|
||||||
@ -42,7 +42,7 @@ const ViewButton = ({
|
|||||||
|
|
||||||
ViewButton.propTypes = {
|
ViewButton.propTypes = {
|
||||||
loading: PropTypes.bool,
|
loading: PropTypes.bool,
|
||||||
sections: PropTypes.arrayOf(
|
properties: PropTypes.arrayOf(
|
||||||
PropTypes.shape({
|
PropTypes.shape({
|
||||||
key: PropTypes.string.isRequired,
|
key: PropTypes.string.isRequired,
|
||||||
label: PropTypes.string.isRequired
|
label: PropTypes.string.isRequired
|
||||||
|
|||||||
@ -224,7 +224,7 @@ const ApiServerProvider = ({ children }) => {
|
|||||||
|
|
||||||
// Update filament information
|
// Update filament information
|
||||||
const updateObjectInfo = async (id, type, value) => {
|
const updateObjectInfo = async (id, type, value) => {
|
||||||
const updateUrl = `${config.backendUrl}/${type}s/${id}`
|
const updateUrl = `${config.backendUrl}/${type.toLowerCase()}s/${id}`
|
||||||
logger.debug('Updating info for ' + id)
|
logger.debug('Updating info for ' + id)
|
||||||
try {
|
try {
|
||||||
const response = await axios.put(updateUrl, value, {
|
const response = await axios.put(updateUrl, value, {
|
||||||
@ -249,6 +249,99 @@ const ApiServerProvider = ({ children }) => {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Fetch table data with pagination, filtering, and sorting
|
||||||
|
const fetchTableData = async (type, params = {}) => {
|
||||||
|
const {
|
||||||
|
page = 1,
|
||||||
|
limit = 25,
|
||||||
|
filter = {},
|
||||||
|
sorter = {},
|
||||||
|
onDataChange
|
||||||
|
} = params
|
||||||
|
|
||||||
|
logger.debug('Fetching table data from:', type, {
|
||||||
|
page,
|
||||||
|
limit,
|
||||||
|
filter,
|
||||||
|
sorter
|
||||||
|
})
|
||||||
|
|
||||||
|
try {
|
||||||
|
const response = await axios.get(
|
||||||
|
`${config.backendUrl}/${type.toLowerCase()}s`,
|
||||||
|
{
|
||||||
|
params: {
|
||||||
|
page,
|
||||||
|
limit,
|
||||||
|
...filter,
|
||||||
|
sort: sorter.field,
|
||||||
|
order: sorter.order
|
||||||
|
},
|
||||||
|
headers: {
|
||||||
|
Accept: 'application/json'
|
||||||
|
},
|
||||||
|
withCredentials: true
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
const newData = response.data
|
||||||
|
const totalCount = parseInt(response.headers['x-total-count'] || '0', 10)
|
||||||
|
const totalPages = Math.ceil(totalCount / limit)
|
||||||
|
const hasMore = newData.length >= limit
|
||||||
|
|
||||||
|
if (onDataChange) {
|
||||||
|
onDataChange(newData)
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
data: newData,
|
||||||
|
totalCount,
|
||||||
|
totalPages,
|
||||||
|
hasMore,
|
||||||
|
page
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
logger.error('Failed to fetch table data:', error)
|
||||||
|
throw error
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Download GCode file content
|
||||||
|
const handleDownloadContent = async (id, type, fileName) => {
|
||||||
|
if (!token) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
const response = await axios.get(
|
||||||
|
`${config.backendUrl}/${type.toLowerCase()}s/${id}/content`,
|
||||||
|
{
|
||||||
|
headers: {
|
||||||
|
Accept: 'application/json'
|
||||||
|
},
|
||||||
|
withCredentials: true
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
const fileURL = window.URL.createObjectURL(new Blob([response.data]))
|
||||||
|
const fileLink = document.createElement('a')
|
||||||
|
fileLink.href = fileURL
|
||||||
|
fileLink.setAttribute('download', fileName)
|
||||||
|
document.body.appendChild(fileLink)
|
||||||
|
fileLink.click()
|
||||||
|
fileLink.parentNode.removeChild(fileLink)
|
||||||
|
} catch (error) {
|
||||||
|
logger.error('Failed to download GCode file content:', error)
|
||||||
|
if (error.response) {
|
||||||
|
messageApi.error('Error downloading GCode file:', error.response.status)
|
||||||
|
} else {
|
||||||
|
messageApi.error(
|
||||||
|
'An unexpected error occurred while downloading. Please try again later.'
|
||||||
|
)
|
||||||
|
}
|
||||||
|
throw error
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<ApiServerContext.Provider
|
<ApiServerContext.Provider
|
||||||
value={{
|
value={{
|
||||||
@ -263,8 +356,10 @@ const ApiServerProvider = ({ children }) => {
|
|||||||
onUpdateEvent,
|
onUpdateEvent,
|
||||||
offUpdateEvent,
|
offUpdateEvent,
|
||||||
fetchObjectInfo,
|
fetchObjectInfo,
|
||||||
|
fetchTableData,
|
||||||
fetchLoading,
|
fetchLoading,
|
||||||
showError
|
showError,
|
||||||
|
handleDownloadContent
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{contextHolder}
|
{contextHolder}
|
||||||
|
|||||||
@ -1,15 +1,21 @@
|
|||||||
import { useState, useEffect } from 'react'
|
import { useState, useEffect } from 'react'
|
||||||
|
import { getModelByName } from '../../../database/ObjectModels'
|
||||||
|
|
||||||
const useColumnVisibility = (componentName, columns) => {
|
const useColumnVisibility = (type) => {
|
||||||
const getInitialVisibility = () => {
|
const getInitialVisibility = () => {
|
||||||
const stored = sessionStorage.getItem(`${componentName}_columnVisibility`)
|
const stored = sessionStorage.getItem(`${type}_columnVisibility`)
|
||||||
if (stored) {
|
if (stored) {
|
||||||
return JSON.parse(stored)
|
return JSON.parse(stored)
|
||||||
}
|
}
|
||||||
// Default visibility - all columns visible
|
// Default visibility - all columns visible
|
||||||
return columns.reduce((acc, col) => {
|
const model = getModelByName(type)
|
||||||
if (col.key) {
|
const columns = model.columns || []
|
||||||
acc[col.key] = true
|
return columns.reduce((acc, columnName) => {
|
||||||
|
const property = model.properties?.find(
|
||||||
|
(prop) => prop.name === columnName
|
||||||
|
)
|
||||||
|
if (property) {
|
||||||
|
acc[property.name] = true
|
||||||
}
|
}
|
||||||
return acc
|
return acc
|
||||||
}, {})
|
}, {})
|
||||||
@ -19,10 +25,10 @@ const useColumnVisibility = (componentName, columns) => {
|
|||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
sessionStorage.setItem(
|
sessionStorage.setItem(
|
||||||
`${componentName}_columnVisibility`,
|
`${type}_columnVisibility`,
|
||||||
JSON.stringify(columnVisibility)
|
JSON.stringify(columnVisibility)
|
||||||
)
|
)
|
||||||
}, [columnVisibility, componentName])
|
}, [columnVisibility, type])
|
||||||
|
|
||||||
const updateColumnVisibility = (key, value) => {
|
const updateColumnVisibility = (key, value) => {
|
||||||
setColumnVisibility((prev) => ({
|
setColumnVisibility((prev) => ({
|
||||||
|
|||||||
@ -4,6 +4,7 @@ import { Spool } from './models/Spool'
|
|||||||
import { GCodeFile } from './models/GCodeFile'
|
import { GCodeFile } from './models/GCodeFile'
|
||||||
import { Job } from './models/Job'
|
import { Job } from './models/Job'
|
||||||
import { Product } from './models/Product'
|
import { Product } from './models/Product'
|
||||||
|
import { Part } from './models/Part.js'
|
||||||
import { Vendor } from './models/Vendor'
|
import { Vendor } from './models/Vendor'
|
||||||
import { SubJob } from './models/SubJob'
|
import { SubJob } from './models/SubJob'
|
||||||
import { Initial } from './models/Initial'
|
import { Initial } from './models/Initial'
|
||||||
@ -25,6 +26,7 @@ export const objectModels = [
|
|||||||
GCodeFile,
|
GCodeFile,
|
||||||
Job,
|
Job,
|
||||||
Product,
|
Product,
|
||||||
|
Part,
|
||||||
Vendor,
|
Vendor,
|
||||||
SubJob,
|
SubJob,
|
||||||
Initial,
|
Initial,
|
||||||
@ -47,6 +49,7 @@ export {
|
|||||||
GCodeFile,
|
GCodeFile,
|
||||||
Job,
|
Job,
|
||||||
Product,
|
Product,
|
||||||
|
Part,
|
||||||
Vendor,
|
Vendor,
|
||||||
SubJob,
|
SubJob,
|
||||||
Initial,
|
Initial,
|
||||||
|
|||||||
@ -1,9 +1,20 @@
|
|||||||
import AuditLogIcon from '../../components/Icons/AuditLogIcon'
|
import AuditLogIcon from '../../components/Icons/AuditLogIcon'
|
||||||
|
import InfoCircleIcon from '../../components/Icons/InfoCircleIcon'
|
||||||
|
|
||||||
export const AuditLog = {
|
export const AuditLog = {
|
||||||
name: 'auditlog',
|
name: 'auditlog',
|
||||||
label: 'Audit Log',
|
label: 'Audit Log',
|
||||||
prefix: 'ADL',
|
prefix: 'ADL',
|
||||||
icon: AuditLogIcon,
|
icon: AuditLogIcon,
|
||||||
|
actions: [
|
||||||
|
{
|
||||||
|
name: 'info',
|
||||||
|
label: 'Info',
|
||||||
|
default: true,
|
||||||
|
row: true,
|
||||||
|
icon: InfoCircleIcon,
|
||||||
|
url: (_id) => `/dashboard/management/auditlogs/info?auditLogId=${_id}`
|
||||||
|
}
|
||||||
|
],
|
||||||
url: () => `#`
|
url: () => `#`
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,16 +1,41 @@
|
|||||||
import FilamentIcon from '../../components/Icons/FilamentIcon'
|
import FilamentIcon from '../../components/Icons/FilamentIcon'
|
||||||
|
import InfoCircleIcon from '../../components/Icons/InfoCircleIcon'
|
||||||
|
|
||||||
export const Filament = {
|
export const Filament = {
|
||||||
name: 'filament',
|
name: 'filament',
|
||||||
label: 'Filament',
|
label: 'Filament',
|
||||||
prefix: 'FIL',
|
prefix: 'FIL',
|
||||||
icon: FilamentIcon,
|
icon: FilamentIcon,
|
||||||
url: (id) => `/dashboard/management/filaments/info?filamentId=${id}`,
|
actions: [
|
||||||
|
{
|
||||||
|
name: 'info',
|
||||||
|
label: 'Info',
|
||||||
|
default: true,
|
||||||
|
row: true,
|
||||||
|
icon: InfoCircleIcon,
|
||||||
|
url: (_id) => `/dashboard/management/filaments/info?filamentId=${_id}`
|
||||||
|
}
|
||||||
|
],
|
||||||
|
columns: [
|
||||||
|
'_id',
|
||||||
|
'name',
|
||||||
|
'type',
|
||||||
|
'color',
|
||||||
|
'vendor',
|
||||||
|
'vendor._id',
|
||||||
|
'cost',
|
||||||
|
'density',
|
||||||
|
'diameter',
|
||||||
|
'createdAt',
|
||||||
|
'updatedAt'
|
||||||
|
],
|
||||||
|
filters: ['_id', 'name', 'type', 'color', 'cost', 'vendor', 'vendor._id'],
|
||||||
|
sorters: ['name', 'createdAt', 'type', 'vendor', 'cost', 'updatedAt'],
|
||||||
properties: [
|
properties: [
|
||||||
{
|
{
|
||||||
name: '_id',
|
name: '_id',
|
||||||
label: 'ID',
|
label: 'ID',
|
||||||
|
columnFixed: 'left',
|
||||||
type: 'id',
|
type: 'id',
|
||||||
objectType: 'filament',
|
objectType: 'filament',
|
||||||
showCopy: true
|
showCopy: true
|
||||||
@ -18,28 +43,25 @@ export const Filament = {
|
|||||||
{
|
{
|
||||||
name: 'createdAt',
|
name: 'createdAt',
|
||||||
label: 'Created At',
|
label: 'Created At',
|
||||||
|
|
||||||
type: 'dateTime',
|
type: 'dateTime',
|
||||||
readOnly: true
|
readOnly: true
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: 'name',
|
name: 'name',
|
||||||
label: 'Name',
|
label: 'Name',
|
||||||
|
columnFixed: 'left',
|
||||||
required: true,
|
required: true,
|
||||||
type: 'text'
|
type: 'text'
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: 'updatedAt',
|
name: 'updatedAt',
|
||||||
label: 'Updated At',
|
label: 'Updated At',
|
||||||
|
|
||||||
type: 'dateTime',
|
type: 'dateTime',
|
||||||
readOnly: true
|
readOnly: true
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: 'vendor',
|
name: 'vendor',
|
||||||
label: 'Vendor',
|
label: 'Vendor',
|
||||||
|
|
||||||
required: true,
|
required: true,
|
||||||
type: 'object',
|
type: 'object',
|
||||||
objectType: 'vendor'
|
objectType: 'vendor'
|
||||||
@ -55,34 +77,35 @@ export const Filament = {
|
|||||||
{
|
{
|
||||||
name: 'type',
|
name: 'type',
|
||||||
label: 'Material',
|
label: 'Material',
|
||||||
|
|
||||||
required: true,
|
required: true,
|
||||||
|
columnWidth: 150,
|
||||||
type: 'material'
|
type: 'material'
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: 'cost',
|
name: 'cost',
|
||||||
label: 'Cost',
|
label: 'Cost',
|
||||||
|
columnWidth: 150,
|
||||||
required: true,
|
required: true,
|
||||||
type: 'currency'
|
type: 'currency'
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: 'color',
|
name: 'color',
|
||||||
label: 'Color',
|
label: 'Color',
|
||||||
|
columnWidth: 150,
|
||||||
required: true,
|
required: true,
|
||||||
type: 'color'
|
type: 'color'
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: 'diameter',
|
name: 'diameter',
|
||||||
label: 'Diameter',
|
label: 'Diameter',
|
||||||
|
columnWidth: 150,
|
||||||
required: true,
|
required: true,
|
||||||
type: 'mm'
|
type: 'mm'
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: 'density',
|
name: 'density',
|
||||||
label: 'Density',
|
label: 'Density',
|
||||||
|
columnWidth: 150,
|
||||||
required: true,
|
required: true,
|
||||||
type: 'density'
|
type: 'density'
|
||||||
},
|
},
|
||||||
|
|||||||
@ -1,9 +1,21 @@
|
|||||||
import FilamentStockIcon from '../../components/Icons/FilamentStockIcon'
|
import FilamentStockIcon from '../../components/Icons/FilamentStockIcon'
|
||||||
|
import InfoCircleIcon from '../../components/Icons/InfoCircleIcon'
|
||||||
|
|
||||||
export const FilamentStock = {
|
export const FilamentStock = {
|
||||||
name: 'filamentstock',
|
name: 'filamentstock',
|
||||||
label: 'Filament Stock',
|
label: 'Filament Stock',
|
||||||
prefix: 'FLS',
|
prefix: 'FLS',
|
||||||
icon: FilamentStockIcon,
|
icon: FilamentStockIcon,
|
||||||
|
actions: [
|
||||||
|
{
|
||||||
|
name: 'info',
|
||||||
|
label: 'Info',
|
||||||
|
default: true,
|
||||||
|
row: true,
|
||||||
|
icon: InfoCircleIcon,
|
||||||
|
url: (_id) =>
|
||||||
|
`/dashboard/inventory/filamentstocks/info?filamentStockId=${_id}`
|
||||||
|
}
|
||||||
|
],
|
||||||
url: (id) => `/dashboard/inventory/filamentstocks/info?filamentStockId=${id}`
|
url: (id) => `/dashboard/inventory/filamentstocks/info?filamentStockId=${id}`
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,17 +1,49 @@
|
|||||||
import GCodeFileIcon from '../../components/Icons/GCodeFileIcon'
|
import GCodeFileIcon from '../../components/Icons/GCodeFileIcon'
|
||||||
|
import InfoCircleIcon from '../../components/Icons/InfoCircleIcon'
|
||||||
|
|
||||||
export const GCodeFile = {
|
export const GCodeFile = {
|
||||||
name: 'gcodeFile',
|
name: 'gcodeFile',
|
||||||
label: 'GCode File',
|
label: 'GCode File',
|
||||||
prefix: 'GCF',
|
prefix: 'GCF',
|
||||||
icon: GCodeFileIcon,
|
icon: GCodeFileIcon,
|
||||||
url: (id) => `/dashboard/production/gcodefiles/info?gcodeFileId=${id}`,
|
actions: [
|
||||||
|
{
|
||||||
|
name: 'info',
|
||||||
|
label: 'Info',
|
||||||
|
default: true,
|
||||||
|
row: true,
|
||||||
|
icon: InfoCircleIcon,
|
||||||
|
url: (_id) => `/dashboard/production/gcodefiles/info?gcodeFileId=${_id}`
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'download',
|
||||||
|
label: 'Download',
|
||||||
|
row: true,
|
||||||
|
url: (_id) =>
|
||||||
|
`/dashboard/production/gcodefiles/info?gcodeFileId=${_id}&action=download`
|
||||||
|
}
|
||||||
|
],
|
||||||
|
|
||||||
|
columns: [
|
||||||
|
'name',
|
||||||
|
'_id',
|
||||||
|
'filament',
|
||||||
|
'gcodeFileInfo.estimatedPrintingTimeNormalMode',
|
||||||
|
'gcodeFileInfo.sparseInfillDensity',
|
||||||
|
'gcodeFileInfo.sparseInfillPattern',
|
||||||
|
'gcodeFileInfo.nozzleTemperature',
|
||||||
|
'gcodeFileInfo.hotPlateTemp',
|
||||||
|
'updatedAt'
|
||||||
|
],
|
||||||
|
filters: ['_id', 'name', 'updatedAt'],
|
||||||
|
sorters: ['name', 'createdAt', 'updatedAt'],
|
||||||
properties: [
|
properties: [
|
||||||
{
|
{
|
||||||
name: '_id',
|
name: '_id',
|
||||||
label: 'ID',
|
label: 'ID',
|
||||||
type: 'id',
|
type: 'id',
|
||||||
objectType: 'gcodefile',
|
objectType: 'gcodeFile',
|
||||||
|
columnFixed: 'left',
|
||||||
value: null,
|
value: null,
|
||||||
showCopy: true
|
showCopy: true
|
||||||
},
|
},
|
||||||
@ -25,6 +57,7 @@ export const GCodeFile = {
|
|||||||
{
|
{
|
||||||
name: 'name',
|
name: 'name',
|
||||||
label: 'Name',
|
label: 'Name',
|
||||||
|
columnFixed: 'left',
|
||||||
type: 'text',
|
type: 'text',
|
||||||
value: null,
|
value: null,
|
||||||
required: true
|
required: true
|
||||||
@ -61,6 +94,7 @@ export const GCodeFile = {
|
|||||||
{
|
{
|
||||||
name: 'gcodeFileInfo.sparseInfillDensity',
|
name: 'gcodeFileInfo.sparseInfillDensity',
|
||||||
label: 'Infill Density',
|
label: 'Infill Density',
|
||||||
|
columnWidth: 150,
|
||||||
type: 'number',
|
type: 'number',
|
||||||
readOnly: true
|
readOnly: true
|
||||||
},
|
},
|
||||||
@ -86,14 +120,16 @@ export const GCodeFile = {
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: 'gcodeFileInfo.nozzleTemperature',
|
name: 'gcodeFileInfo.nozzleTemperature',
|
||||||
label: 'Hotend Temperature',
|
label: 'Hotend Temp',
|
||||||
|
columnWidth: 150,
|
||||||
value: null,
|
value: null,
|
||||||
type: 'number',
|
type: 'number',
|
||||||
readOnly: true
|
readOnly: true
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: 'gcodeFileInfo.hotPlateTemp',
|
name: 'gcodeFileInfo.hotPlateTemp',
|
||||||
label: 'Bed Temperature',
|
label: 'Bed Temp',
|
||||||
|
columnWidth: 150,
|
||||||
value: null,
|
value: null,
|
||||||
type: 'number',
|
type: 'number',
|
||||||
readOnly: true
|
readOnly: true
|
||||||
|
|||||||
@ -1,9 +1,20 @@
|
|||||||
import QuestionCircleIcon from '../../components/Icons/QuestionCircleIcon'
|
import QuestionCircleIcon from '../../components/Icons/QuestionCircleIcon'
|
||||||
|
import InfoCircleIcon from '../../components/Icons/InfoCircleIcon'
|
||||||
|
|
||||||
export const Initial = {
|
export const Initial = {
|
||||||
name: 'initial',
|
name: 'initial',
|
||||||
label: 'Initial',
|
label: 'Initial',
|
||||||
prefix: 'INT',
|
prefix: 'INT',
|
||||||
icon: QuestionCircleIcon,
|
icon: QuestionCircleIcon,
|
||||||
|
actions: [
|
||||||
|
{
|
||||||
|
name: 'info',
|
||||||
|
label: 'Info',
|
||||||
|
default: true,
|
||||||
|
row: true,
|
||||||
|
icon: InfoCircleIcon,
|
||||||
|
url: (_id) => `/dashboard/management/initials/info?initialId=${_id}`
|
||||||
|
}
|
||||||
|
],
|
||||||
url: () => `#`
|
url: () => `#`
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,16 +1,37 @@
|
|||||||
import JobIcon from '../../components/Icons/JobIcon'
|
import JobIcon from '../../components/Icons/JobIcon'
|
||||||
|
import InfoCircleIcon from '../../components/Icons/InfoCircleIcon'
|
||||||
|
|
||||||
export const Job = {
|
export const Job = {
|
||||||
name: 'job',
|
name: 'job',
|
||||||
label: 'Job',
|
label: 'Job',
|
||||||
prefix: 'JOB',
|
prefix: 'JOB',
|
||||||
icon: JobIcon,
|
icon: JobIcon,
|
||||||
url: (id) => `/dashboard/production/jobs/info?jobId=${id}`,
|
actions: [
|
||||||
|
{
|
||||||
|
name: 'info',
|
||||||
|
label: 'Info',
|
||||||
|
default: true,
|
||||||
|
row: true,
|
||||||
|
icon: InfoCircleIcon,
|
||||||
|
url: (_id) => `/dashboard/production/jobs/info?jobId=${_id}`
|
||||||
|
}
|
||||||
|
],
|
||||||
|
columns: [
|
||||||
|
'_id',
|
||||||
|
'gcodeFile',
|
||||||
|
'gcodeFile._id',
|
||||||
|
'state',
|
||||||
|
'quantity',
|
||||||
|
'createdAt'
|
||||||
|
],
|
||||||
|
filters: ['state', '_id', 'gcodeFile._id', 'quantity'],
|
||||||
|
sorters: ['createdAt', 'state', 'quantity', '_id'],
|
||||||
properties: [
|
properties: [
|
||||||
{
|
{
|
||||||
name: '_id',
|
name: '_id',
|
||||||
label: 'ID',
|
label: 'ID',
|
||||||
type: 'id',
|
type: 'id',
|
||||||
|
columnFixed: 'left',
|
||||||
objectType: 'job',
|
objectType: 'job',
|
||||||
showCopy: true
|
showCopy: true
|
||||||
},
|
},
|
||||||
@ -23,12 +44,14 @@ export const Job = {
|
|||||||
showProgress: true,
|
showProgress: true,
|
||||||
showId: false,
|
showId: false,
|
||||||
showQuantity: false,
|
showQuantity: false,
|
||||||
|
columnWidth: 150,
|
||||||
readOnly: true
|
readOnly: true
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: 'gcodeFile',
|
name: 'gcodeFile',
|
||||||
label: 'GCode File',
|
label: 'GCode File',
|
||||||
type: 'object',
|
type: 'object',
|
||||||
|
columnFixed: 'left',
|
||||||
objectType: 'gcodeFile',
|
objectType: 'gcodeFile',
|
||||||
readOnly: true
|
readOnly: true
|
||||||
},
|
},
|
||||||
@ -43,6 +66,7 @@ export const Job = {
|
|||||||
name: 'quantity',
|
name: 'quantity',
|
||||||
label: 'Quantity',
|
label: 'Quantity',
|
||||||
type: 'number',
|
type: 'number',
|
||||||
|
columnWidth: 125,
|
||||||
readOnly: true
|
readOnly: true
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
|||||||
@ -1,9 +1,20 @@
|
|||||||
import NoteIcon from '../../components/Icons/NoteIcon'
|
import NoteIcon from '../../components/Icons/NoteIcon'
|
||||||
|
import InfoCircleIcon from '../../components/Icons/InfoCircleIcon'
|
||||||
|
|
||||||
export const Note = {
|
export const Note = {
|
||||||
name: 'note',
|
name: 'note',
|
||||||
label: 'Note',
|
label: 'Note',
|
||||||
prefix: 'NTE',
|
prefix: 'NTE',
|
||||||
icon: NoteIcon,
|
icon: NoteIcon,
|
||||||
|
actions: [
|
||||||
|
{
|
||||||
|
name: 'info',
|
||||||
|
label: 'Info',
|
||||||
|
default: true,
|
||||||
|
row: true,
|
||||||
|
icon: InfoCircleIcon,
|
||||||
|
url: (_id) => `/dashboard/management/notes/info?noteId=${_id}`
|
||||||
|
}
|
||||||
|
],
|
||||||
url: () => `#`
|
url: () => `#`
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,9 +1,59 @@
|
|||||||
import NoteTypeIcon from '../../components/Icons/NoteTypeIcon'
|
import NoteTypeIcon from '../../components/Icons/NoteTypeIcon'
|
||||||
|
import InfoCircleIcon from '../../components/Icons/InfoCircleIcon'
|
||||||
|
|
||||||
export const NoteType = {
|
export const NoteType = {
|
||||||
name: 'notetype',
|
name: 'noteType',
|
||||||
label: 'Note Type',
|
label: 'Note Type',
|
||||||
prefix: 'NTY',
|
prefix: 'NTY',
|
||||||
icon: NoteTypeIcon,
|
icon: NoteTypeIcon,
|
||||||
url: (id) => `/dashboard/management/notetypes/info?noteTypeId=${id}`
|
actions: [
|
||||||
|
{
|
||||||
|
name: 'info',
|
||||||
|
label: 'Info',
|
||||||
|
default: true,
|
||||||
|
row: true,
|
||||||
|
icon: InfoCircleIcon,
|
||||||
|
url: (_id) => `/dashboard/management/notetypes/info?noteTypeId=${_id}`
|
||||||
|
}
|
||||||
|
],
|
||||||
|
columns: ['name', '_id', 'color', 'active', 'createdAt', 'updatedAt'],
|
||||||
|
filters: ['name', '_id', 'color', 'active'],
|
||||||
|
sorters: ['name', 'color', 'active', 'createdAt', 'updatedAt'],
|
||||||
|
properties: [
|
||||||
|
{
|
||||||
|
name: '_id',
|
||||||
|
label: 'ID',
|
||||||
|
type: 'id',
|
||||||
|
objectType: 'noteType',
|
||||||
|
showCopy: true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'createdAt',
|
||||||
|
label: 'Created At',
|
||||||
|
type: 'dateTime',
|
||||||
|
readOnly: true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'name',
|
||||||
|
label: 'Name',
|
||||||
|
required: true,
|
||||||
|
type: 'text'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'updatedAt',
|
||||||
|
label: 'Updated At',
|
||||||
|
type: 'dateTime',
|
||||||
|
readOnly: true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'color',
|
||||||
|
label: 'Color',
|
||||||
|
type: 'color'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'active',
|
||||||
|
label: 'Active',
|
||||||
|
type: 'bool'
|
||||||
|
}
|
||||||
|
]
|
||||||
}
|
}
|
||||||
|
|||||||
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 PartStockIcon from '../../components/Icons/PartStockIcon'
|
||||||
|
import InfoCircleIcon from '../../components/Icons/InfoCircleIcon'
|
||||||
|
|
||||||
export const PartStock = {
|
export const PartStock = {
|
||||||
name: 'partstock',
|
name: 'partstock',
|
||||||
label: 'Part Stock',
|
label: 'Part Stock',
|
||||||
prefix: 'PTS',
|
prefix: 'PTS',
|
||||||
icon: PartStockIcon,
|
icon: PartStockIcon,
|
||||||
|
actions: [
|
||||||
|
{
|
||||||
|
name: 'info',
|
||||||
|
label: 'Info',
|
||||||
|
default: true,
|
||||||
|
row: true,
|
||||||
|
icon: InfoCircleIcon,
|
||||||
|
url: (_id) => `/dashboard/management/partstocks/info?partStockId=${_id}`
|
||||||
|
}
|
||||||
|
],
|
||||||
url: (id) => `/dashboard/management/partstocks/info?partStockId=${id}`
|
url: (id) => `/dashboard/management/partstocks/info?partStockId=${id}`
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,11 +1,25 @@
|
|||||||
import PrinterIcon from '../../components/Icons/PrinterIcon'
|
import PrinterIcon from '../../components/Icons/PrinterIcon'
|
||||||
|
import InfoCircleIcon from '../../components/Icons/InfoCircleIcon'
|
||||||
|
|
||||||
export const Printer = {
|
export const Printer = {
|
||||||
name: 'printer',
|
name: 'printer',
|
||||||
label: 'Printer',
|
label: 'Printer',
|
||||||
prefix: 'PRN',
|
prefix: 'PRN',
|
||||||
icon: PrinterIcon,
|
icon: PrinterIcon,
|
||||||
|
actions: [
|
||||||
|
{
|
||||||
|
name: 'info',
|
||||||
|
label: 'Info',
|
||||||
|
default: true,
|
||||||
|
row: true,
|
||||||
|
icon: InfoCircleIcon,
|
||||||
|
url: (_id) => `/dashboard/production/printers/info?printerId=${_id}`
|
||||||
|
}
|
||||||
|
],
|
||||||
url: (id) => `/dashboard/production/printers/info?printerId=${id}`,
|
url: (id) => `/dashboard/production/printers/info?printerId=${id}`,
|
||||||
|
columns: ['name', '_id', 'state', 'tags', 'connectedAt'],
|
||||||
|
filters: ['name', '_id', 'state', 'tags'],
|
||||||
|
sorters: ['name', 'state', 'connectedAt', '_id'],
|
||||||
properties: [
|
properties: [
|
||||||
{
|
{
|
||||||
name: '_id',
|
name: '_id',
|
||||||
@ -24,7 +38,9 @@ export const Printer = {
|
|||||||
name: 'name',
|
name: 'name',
|
||||||
label: 'Name',
|
label: 'Name',
|
||||||
required: true,
|
required: true,
|
||||||
type: 'text'
|
type: 'text',
|
||||||
|
columnWidth: 200,
|
||||||
|
columnFixed: 'left'
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: 'state',
|
name: 'state',
|
||||||
|
|||||||
@ -1,9 +1,47 @@
|
|||||||
import ProductIcon from '../../components/Icons/ProductIcon'
|
import ProductIcon from '../../components/Icons/ProductIcon'
|
||||||
|
import InfoCircleIcon from '../../components/Icons/InfoCircleIcon'
|
||||||
|
|
||||||
export const Product = {
|
export const Product = {
|
||||||
name: 'product',
|
name: 'product',
|
||||||
label: 'Product',
|
label: 'Product',
|
||||||
prefix: 'PRD',
|
prefix: 'PRD',
|
||||||
icon: ProductIcon,
|
icon: ProductIcon,
|
||||||
url: (id) => `/dashboard/management/products/info?productId=${id}`
|
actions: [
|
||||||
|
{
|
||||||
|
name: 'info',
|
||||||
|
label: 'Info',
|
||||||
|
default: true,
|
||||||
|
row: true,
|
||||||
|
icon: InfoCircleIcon,
|
||||||
|
url: (_id) => `/dashboard/management/products/info?productId=${_id}`
|
||||||
|
}
|
||||||
|
],
|
||||||
|
url: (id) => `/dashboard/management/products/info?productId=${id}`,
|
||||||
|
properties: [
|
||||||
|
{
|
||||||
|
name: '_id',
|
||||||
|
label: 'ID',
|
||||||
|
type: 'id',
|
||||||
|
objectType: 'printer',
|
||||||
|
showCopy: true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'createdAt',
|
||||||
|
label: 'Created At',
|
||||||
|
type: 'dateTime',
|
||||||
|
readOnly: true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'name',
|
||||||
|
label: 'Name',
|
||||||
|
required: true,
|
||||||
|
type: 'text'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'updatedAt',
|
||||||
|
label: 'Updated At',
|
||||||
|
type: 'dateTime',
|
||||||
|
readOnly: true
|
||||||
|
}
|
||||||
|
]
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,9 +1,21 @@
|
|||||||
import ProductStockIcon from '../../components/Icons/ProductStockIcon'
|
import ProductStockIcon from '../../components/Icons/ProductStockIcon'
|
||||||
|
import InfoCircleIcon from '../../components/Icons/InfoCircleIcon'
|
||||||
|
|
||||||
export const ProductStock = {
|
export const ProductStock = {
|
||||||
name: 'productstock',
|
name: 'productstock',
|
||||||
label: 'Product Stock',
|
label: 'Product Stock',
|
||||||
prefix: 'PDS',
|
prefix: 'PDS',
|
||||||
icon: ProductStockIcon,
|
icon: ProductStockIcon,
|
||||||
|
actions: [
|
||||||
|
{
|
||||||
|
name: 'info',
|
||||||
|
label: 'Info',
|
||||||
|
default: true,
|
||||||
|
row: true,
|
||||||
|
icon: InfoCircleIcon,
|
||||||
|
url: (_id) =>
|
||||||
|
`/dashboard/management/productstocks/info?productStockId=${_id}`
|
||||||
|
}
|
||||||
|
],
|
||||||
url: (id) => `/dashboard/management/productstocks/info?productStockId=${id}`
|
url: (id) => `/dashboard/management/productstocks/info?productStockId=${id}`
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,9 +1,20 @@
|
|||||||
import FilamentIcon from '../../components/Icons/FilamentIcon'
|
import FilamentIcon from '../../components/Icons/FilamentIcon'
|
||||||
|
import InfoCircleIcon from '../../components/Icons/InfoCircleIcon'
|
||||||
|
|
||||||
export const Spool = {
|
export const Spool = {
|
||||||
name: 'spool',
|
name: 'spool',
|
||||||
label: 'Spool',
|
label: 'Spool',
|
||||||
prefix: 'SPL',
|
prefix: 'SPL',
|
||||||
icon: FilamentIcon,
|
icon: FilamentIcon,
|
||||||
|
actions: [
|
||||||
|
{
|
||||||
|
name: 'info',
|
||||||
|
label: 'Info',
|
||||||
|
default: true,
|
||||||
|
row: true,
|
||||||
|
icon: InfoCircleIcon,
|
||||||
|
url: (_id) => `/dashboard/inventory/spool/info?spoolId=${_id}`
|
||||||
|
}
|
||||||
|
],
|
||||||
url: (id) => `/dashboard/inventory/spool/info?spoolId=${id}`
|
url: (id) => `/dashboard/inventory/spool/info?spoolId=${id}`
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,9 +1,20 @@
|
|||||||
import StockAuditIcon from '../../components/Icons/StockAuditIcon'
|
import StockAuditIcon from '../../components/Icons/StockAuditIcon'
|
||||||
|
import InfoCircleIcon from '../../components/Icons/InfoCircleIcon'
|
||||||
|
|
||||||
export const StockAudit = {
|
export const StockAudit = {
|
||||||
name: 'stockaudit',
|
name: 'stockaudit',
|
||||||
label: 'Stock Audit',
|
label: 'Stock Audit',
|
||||||
prefix: 'SAU',
|
prefix: 'SAU',
|
||||||
icon: StockAuditIcon,
|
icon: StockAuditIcon,
|
||||||
|
actions: [
|
||||||
|
{
|
||||||
|
name: 'info',
|
||||||
|
label: 'Info',
|
||||||
|
default: true,
|
||||||
|
row: true,
|
||||||
|
icon: InfoCircleIcon,
|
||||||
|
url: (_id) => `/dashboard/inventory/stockaudits/info?stockAuditId=${_id}`
|
||||||
|
}
|
||||||
|
],
|
||||||
url: (id) => `/dashboard/inventory/stockaudits/info?stockAuditId=${id}`
|
url: (id) => `/dashboard/inventory/stockaudits/info?stockAuditId=${id}`
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,9 +1,20 @@
|
|||||||
import StockEventIcon from '../../components/Icons/StockEventIcon'
|
import StockEventIcon from '../../components/Icons/StockEventIcon'
|
||||||
|
import InfoCircleIcon from '../../components/Icons/InfoCircleIcon'
|
||||||
|
|
||||||
export const StockEvent = {
|
export const StockEvent = {
|
||||||
name: 'stockevent',
|
name: 'stockevent',
|
||||||
label: 'Stock Event',
|
label: 'Stock Event',
|
||||||
prefix: 'SEV',
|
prefix: 'SEV',
|
||||||
icon: StockEventIcon,
|
icon: StockEventIcon,
|
||||||
|
actions: [
|
||||||
|
{
|
||||||
|
name: 'info',
|
||||||
|
label: 'Info',
|
||||||
|
default: true,
|
||||||
|
row: true,
|
||||||
|
icon: InfoCircleIcon,
|
||||||
|
url: (_id) => `/dashboard/inventory/stockevents/info?stockEventId=${_id}`
|
||||||
|
}
|
||||||
|
],
|
||||||
url: () => `#`
|
url: () => `#`
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,9 +1,20 @@
|
|||||||
import SubJobIcon from '../../components/Icons/SubJobIcon'
|
import SubJobIcon from '../../components/Icons/SubJobIcon'
|
||||||
|
import InfoCircleIcon from '../../components/Icons/InfoCircleIcon'
|
||||||
|
|
||||||
export const SubJob = {
|
export const SubJob = {
|
||||||
name: 'subjob',
|
name: 'subjob',
|
||||||
label: 'Sub Job',
|
label: 'Sub Job',
|
||||||
prefix: 'SJB',
|
prefix: 'SJB',
|
||||||
icon: SubJobIcon,
|
icon: SubJobIcon,
|
||||||
|
actions: [
|
||||||
|
{
|
||||||
|
name: 'info',
|
||||||
|
label: 'Info',
|
||||||
|
default: true,
|
||||||
|
row: true,
|
||||||
|
icon: InfoCircleIcon,
|
||||||
|
url: (_id) => `/dashboard/production/subjobs/info?subJobId=${_id}`
|
||||||
|
}
|
||||||
|
],
|
||||||
url: () => `#`
|
url: () => `#`
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,9 +1,75 @@
|
|||||||
import PersonIcon from '../../components/Icons/PersonIcon'
|
import PersonIcon from '../../components/Icons/PersonIcon'
|
||||||
|
import InfoCircleIcon from '../../components/Icons/InfoCircleIcon'
|
||||||
|
|
||||||
export const User = {
|
export const User = {
|
||||||
name: 'user',
|
name: 'user',
|
||||||
label: 'User',
|
label: 'User',
|
||||||
prefix: 'USR',
|
prefix: 'USR',
|
||||||
icon: PersonIcon,
|
icon: PersonIcon,
|
||||||
url: (id) => `/dashboard/management/users/info?userId=${id}`
|
actions: [
|
||||||
|
{
|
||||||
|
name: 'info',
|
||||||
|
label: 'Info',
|
||||||
|
default: true,
|
||||||
|
row: true,
|
||||||
|
icon: InfoCircleIcon,
|
||||||
|
url: (_id) => `/dashboard/management/users/info?userId=${_id}`
|
||||||
|
}
|
||||||
|
],
|
||||||
|
url: (id) => `/dashboard/management/users/info?userId=${id}`,
|
||||||
|
columns: ['name', '_id', 'username', 'email', 'role', 'createdAt'],
|
||||||
|
filters: ['name', '_id', 'email', 'role'],
|
||||||
|
sorters: ['name', 'email', 'role', 'createdAt', '_id'],
|
||||||
|
properties: [
|
||||||
|
{
|
||||||
|
name: '_id',
|
||||||
|
label: 'ID',
|
||||||
|
columnFixed: 'left',
|
||||||
|
type: 'id',
|
||||||
|
objectType: 'user',
|
||||||
|
showCopy: true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'createdAt',
|
||||||
|
label: 'Created At',
|
||||||
|
type: 'dateTime',
|
||||||
|
readOnly: true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'name',
|
||||||
|
label: 'Name',
|
||||||
|
columnFixed: 'left',
|
||||||
|
required: true,
|
||||||
|
type: 'text'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'updatedAt',
|
||||||
|
label: 'Updated At',
|
||||||
|
type: 'dateTime',
|
||||||
|
readOnly: true
|
||||||
|
},
|
||||||
|
|
||||||
|
{
|
||||||
|
name: 'firstName',
|
||||||
|
label: 'First Name',
|
||||||
|
type: 'text'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'username',
|
||||||
|
label: 'Username',
|
||||||
|
required: true,
|
||||||
|
type: 'text'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'lastName',
|
||||||
|
label: 'Last Name',
|
||||||
|
type: 'text'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'email',
|
||||||
|
label: 'Email',
|
||||||
|
columnWidth: 300,
|
||||||
|
type: 'email'
|
||||||
|
}
|
||||||
|
]
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,16 +1,30 @@
|
|||||||
import VendorIcon from '../../components/Icons/VendorIcon'
|
import VendorIcon from '../../components/Icons/VendorIcon'
|
||||||
|
import InfoCircleIcon from '../../components/Icons/InfoCircleIcon'
|
||||||
|
|
||||||
export const Vendor = {
|
export const Vendor = {
|
||||||
name: 'vendor',
|
name: 'vendor',
|
||||||
label: 'Vendor',
|
label: 'Vendor',
|
||||||
prefix: 'VEN',
|
prefix: 'VEN',
|
||||||
icon: VendorIcon,
|
icon: VendorIcon,
|
||||||
|
actions: [
|
||||||
|
{
|
||||||
|
name: 'info',
|
||||||
|
label: 'Info',
|
||||||
|
default: true,
|
||||||
|
row: true,
|
||||||
|
icon: InfoCircleIcon,
|
||||||
|
url: (_id) => `/dashboard/management/vendors/info?vendorId=${_id}`
|
||||||
|
}
|
||||||
|
],
|
||||||
url: (id) => `/dashboard/management/vendors/info?vendorId=${id}`,
|
url: (id) => `/dashboard/management/vendors/info?vendorId=${id}`,
|
||||||
|
columns: ['name', '_id', 'country', 'email', 'createdAt'],
|
||||||
|
filters: ['name', '_id', 'country', 'email'],
|
||||||
|
sorters: ['name', 'country', 'email', 'createdAt', '_id'],
|
||||||
properties: [
|
properties: [
|
||||||
{
|
{
|
||||||
name: '_id',
|
name: '_id',
|
||||||
label: 'ID',
|
label: 'ID',
|
||||||
|
columnFixed: 'left',
|
||||||
type: 'id',
|
type: 'id',
|
||||||
objectType: 'vendor',
|
objectType: 'vendor',
|
||||||
showCopy: true
|
showCopy: true
|
||||||
@ -18,13 +32,13 @@ export const Vendor = {
|
|||||||
{
|
{
|
||||||
name: 'createdAt',
|
name: 'createdAt',
|
||||||
label: 'Created At',
|
label: 'Created At',
|
||||||
|
|
||||||
type: 'dateTime',
|
type: 'dateTime',
|
||||||
readOnly: true
|
readOnly: true
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: 'name',
|
name: 'name',
|
||||||
label: 'Name',
|
label: 'Name',
|
||||||
|
columnFixed: 'left',
|
||||||
required: true,
|
required: true,
|
||||||
type: 'text'
|
type: 'text'
|
||||||
},
|
},
|
||||||
@ -51,6 +65,7 @@ export const Vendor = {
|
|||||||
{
|
{
|
||||||
name: 'email',
|
name: 'email',
|
||||||
label: 'Email',
|
label: 'Email',
|
||||||
|
columnWidth: 300,
|
||||||
type: 'email',
|
type: 'email',
|
||||||
readOnly: false,
|
readOnly: false,
|
||||||
required: false
|
required: false
|
||||||
@ -65,6 +80,7 @@ export const Vendor = {
|
|||||||
{
|
{
|
||||||
name: 'website',
|
name: 'website',
|
||||||
label: 'Website',
|
label: 'Website',
|
||||||
|
columnWidth: 300,
|
||||||
type: 'url',
|
type: 'url',
|
||||||
readOnly: false,
|
readOnly: false,
|
||||||
required: false
|
required: false
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user