Refactored management components to enhance data handling and UI consistency. Integrated ObjectTable for audit logs across various entities, including Parts, Products, and Users. Updated column visibility management and removed unused imports for cleaner code. Improved error handling in API interactions and streamlined component structures for better maintainability.
This commit is contained in:
parent
fdc862d16c
commit
3c2d3ec858
BIN
src/assets/icons/downloadicon.afdesign
Normal file
BIN
src/assets/icons/downloadicon.afdesign
Normal file
Binary file not shown.
1
src/assets/icons/downloadicon.min.svg
Normal file
1
src/assets/icons/downloadicon.min.svg
Normal file
@ -0,0 +1 @@
|
|||||||
|
<svg xmlns="http://www.w3.org/2000/svg" xml:space="preserve" style="fill-rule:evenodd;clip-rule:evenodd;stroke-linejoin:round;stroke-miterlimit:2" viewBox="0 0 64 64"><path d="M56.476 33.038v30.613c0 6.668-3.507 10.148-10.252 10.148H10.251C3.506 73.799 0 70.344 0 63.651V33.038c0-6.7 3.532-10.149 10.251-10.149h9.197v6.122h-8.816c-2.906 0-4.51 1.499-4.51 4.535v29.59c0 3.042 1.579 4.541 4.484 4.541h35.238c2.88 0 4.509-1.499 4.509-4.541v-29.59c0-3.036-1.629-4.535-4.509-4.535h-8.842v-6.122h9.222c6.745 0 10.252 3.475 10.252 10.149" style="fill-rule:nonzero" transform="translate(5.274 -5.849)scale(.94647)"/><path d="M28.251 6.179c-1.586 0-2.905 1.297-2.905 2.823v31.365l.232 4.654-1.643-1.985-4.478-4.762a2.6 2.6 0 0 0-1.955-.857c-1.431 0-2.559 1.024-2.559 2.486 0 .771.309 1.333.828 1.853l10.29 9.903c.743.732 1.415.97 2.19.97.748 0 1.426-.238 2.163-.97l10.29-9.903c.525-.52.829-1.082.829-1.853 0-1.462-1.18-2.486-2.585-2.486-.732 0-1.43.295-1.929.857l-4.478 4.762-1.643 1.985.232-4.654V9.002c0-1.526-1.294-2.823-2.879-2.823" style="fill-rule:nonzero" transform="translate(5.274 -5.849)scale(.94647)"/></svg>
|
||||||
|
After Width: | Height: | Size: 1.1 KiB |
8
src/assets/icons/downloadicon.svg
Normal file
8
src/assets/icons/downloadicon.svg
Normal file
@ -0,0 +1,8 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||||
|
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
|
||||||
|
<svg width="100%" height="100%" viewBox="0 0 64 64" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" xml:space="preserve" xmlns:serif="http://www.serif.com/" style="fill-rule:evenodd;clip-rule:evenodd;stroke-linejoin:round;stroke-miterlimit:2;">
|
||||||
|
<g transform="matrix(0.946467,0,0,0.946467,5.27386,-5.84858)">
|
||||||
|
<path d="M56.476,33.038L56.476,63.651C56.476,70.319 52.969,73.799 46.224,73.799L10.251,73.799C3.506,73.799 0,70.344 0,63.651L0,33.038C0,26.338 3.532,22.889 10.251,22.889L19.448,22.889L19.448,29.011L10.632,29.011C7.726,29.011 6.122,30.51 6.122,33.546L6.122,63.136C6.122,66.178 7.701,67.677 10.606,67.677L45.844,67.677C48.724,67.677 50.353,66.178 50.353,63.136L50.353,33.546C50.353,30.51 48.724,29.011 45.844,29.011L37.002,29.011L37.002,22.889L46.224,22.889C52.969,22.889 56.476,26.364 56.476,33.038Z" style="fill-rule:nonzero;"/>
|
||||||
|
<path d="M28.251,6.179C26.665,6.179 25.346,7.476 25.346,9.002L25.346,40.367L25.578,45.021L23.935,43.036L19.457,38.274C18.958,37.712 18.234,37.417 17.502,37.417C16.071,37.417 14.943,38.441 14.943,39.903C14.943,40.674 15.252,41.236 15.771,41.756L26.061,51.659C26.804,52.391 27.476,52.629 28.251,52.629C28.999,52.629 29.677,52.391 30.414,51.659L40.704,41.756C41.229,41.236 41.533,40.674 41.533,39.903C41.533,38.441 40.353,37.417 38.948,37.417C38.216,37.417 37.518,37.712 37.019,38.274L32.541,43.036L30.898,45.021L31.13,40.367L31.13,9.002C31.13,7.476 29.836,6.179 28.251,6.179Z" style="fill-rule:nonzero;"/>
|
||||||
|
</g>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 1.6 KiB |
@ -1,4 +1,4 @@
|
|||||||
import React, { useContext, useRef } from 'react'
|
import React, { useRef } from 'react'
|
||||||
import {
|
import {
|
||||||
Button,
|
Button,
|
||||||
Flex,
|
Flex,
|
||||||
@ -12,14 +12,12 @@ import {
|
|||||||
Badge
|
Badge
|
||||||
} from 'antd'
|
} from 'antd'
|
||||||
|
|
||||||
import { AuthContext } from '../context/AuthContext'
|
|
||||||
import IdDisplay from '../common/IdDisplay'
|
import IdDisplay from '../common/IdDisplay'
|
||||||
import ReloadIcon from '../../Icons/ReloadIcon'
|
import ReloadIcon from '../../Icons/ReloadIcon'
|
||||||
import useColumnVisibility from '../hooks/useColumnVisibility'
|
import useColumnVisibility from '../hooks/useColumnVisibility'
|
||||||
import TimeDisplay from '../common/TimeDisplay'
|
import TimeDisplay from '../common/TimeDisplay'
|
||||||
import ObjectTable from '../common/ObjectTable'
|
import ObjectTable from '../common/ObjectTable'
|
||||||
|
|
||||||
import config from '../../../config'
|
|
||||||
import AuditLogIcon from '../../Icons/AuditLogIcon'
|
import AuditLogIcon from '../../Icons/AuditLogIcon'
|
||||||
import XMarkIcon from '../../Icons/XMarkIcon'
|
import XMarkIcon from '../../Icons/XMarkIcon'
|
||||||
import CheckIcon from '../../Icons/CheckIcon'
|
import CheckIcon from '../../Icons/CheckIcon'
|
||||||
@ -253,8 +251,6 @@ const AuditLogs = () => {
|
|||||||
columns
|
columns
|
||||||
)
|
)
|
||||||
|
|
||||||
const { authenticated } = useContext(AuthContext)
|
|
||||||
|
|
||||||
const actionItems = {
|
const actionItems = {
|
||||||
items: [
|
items: [
|
||||||
{
|
{
|
||||||
@ -294,10 +290,6 @@ const AuditLogs = () => {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
const visibleColumns = columns.filter(
|
|
||||||
(col) => !col.key || columnVisibility[col.key]
|
|
||||||
)
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<Flex vertical={'true'} gap='large'>
|
<Flex vertical={'true'} gap='large'>
|
||||||
@ -318,9 +310,8 @@ const AuditLogs = () => {
|
|||||||
|
|
||||||
<ObjectTable
|
<ObjectTable
|
||||||
ref={tableRef}
|
ref={tableRef}
|
||||||
columns={visibleColumns}
|
visibleColumns={columnVisibility}
|
||||||
url={`${config.backendUrl}/auditlogs`}
|
type='auditLog'
|
||||||
authenticated={authenticated}
|
|
||||||
/>
|
/>
|
||||||
</Flex>
|
</Flex>
|
||||||
</>
|
</>
|
||||||
|
|||||||
@ -1,12 +1,10 @@
|
|||||||
import React from 'react'
|
import React from 'react'
|
||||||
import { useLocation } from 'react-router-dom'
|
import { useLocation } from 'react-router-dom'
|
||||||
import { Space, Button, Flex, Dropdown, Card } from 'antd'
|
import { Space, Flex, Card } from 'antd'
|
||||||
import { LoadingOutlined } from '@ant-design/icons'
|
import { LoadingOutlined } from '@ant-design/icons'
|
||||||
import loglevel from 'loglevel'
|
import loglevel from 'loglevel'
|
||||||
import config from '../../../../config'
|
import config from '../../../../config'
|
||||||
import ReloadIcon from '../../../Icons/ReloadIcon'
|
|
||||||
import useCollapseState from '../../hooks/useCollapseState'
|
import useCollapseState from '../../hooks/useCollapseState'
|
||||||
import AuditLogTable from '../../common/AuditLogTable'
|
|
||||||
import NotesPanel from '../../common/NotesPanel'
|
import NotesPanel from '../../common/NotesPanel'
|
||||||
import InfoCollapse from '../../common/InfoCollapse'
|
import InfoCollapse from '../../common/InfoCollapse'
|
||||||
import ObjectInfo from '../../common/ObjectInfo'
|
import ObjectInfo from '../../common/ObjectInfo'
|
||||||
@ -17,10 +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 './LockIndicator'
|
import LockIndicator from './LockIndicator'
|
||||||
import {
|
import ActionHandler from '../../common/ActionHandler'
|
||||||
getModelProperties,
|
import ObjectActions from '../../common/ObjectActions.jsx'
|
||||||
getPropertyValue
|
import ObjectTable from '../../common/ObjectTable.jsx'
|
||||||
} from '../../../../database/ObjectModels'
|
import InfoCollapsePlaceholder from '../../common/InfoCollapsePlaceholder.jsx'
|
||||||
|
|
||||||
const log = loglevel.getLogger('FilamentInfo')
|
const log = loglevel.getLogger('FilamentInfo')
|
||||||
log.setLevel(config.logLevel)
|
log.setLevel(config.logLevel)
|
||||||
@ -51,111 +49,139 @@ const FilamentInfo = () => {
|
|||||||
editLoading,
|
editLoading,
|
||||||
lock,
|
lock,
|
||||||
fetchObject
|
fetchObject
|
||||||
}) => (
|
}) => {
|
||||||
<Flex
|
// Define actions for ActionHandler
|
||||||
gap='large'
|
const actions = {
|
||||||
vertical='true'
|
reload: () => {
|
||||||
style={{ height: 'calc(var(--unit-100vh) - 155px)', minHeight: 0 }}
|
fetchObject()
|
||||||
>
|
return true
|
||||||
<Flex justify={'space-between'}>
|
},
|
||||||
<Space size='middle'>
|
edit: () => {
|
||||||
<Space size='small'>
|
startEditing()
|
||||||
<Dropdown
|
return false
|
||||||
menu={{
|
},
|
||||||
items: [
|
cancelEdit: () => {
|
||||||
{
|
cancelEditing()
|
||||||
label: 'Reload Filament',
|
return true
|
||||||
key: 'reload',
|
},
|
||||||
icon: <ReloadIcon />
|
finishEdit: () => {
|
||||||
|
handleUpdate()
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<ActionHandler actions={actions} loading={loading}>
|
||||||
|
{({ callAction }) => (
|
||||||
|
<Flex
|
||||||
|
gap='large'
|
||||||
|
vertical='true'
|
||||||
|
style={{
|
||||||
|
height: 'calc(var(--unit-100vh) - 155px)',
|
||||||
|
minHeight: 0
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Flex justify={'space-between'}>
|
||||||
|
<Space size='middle'>
|
||||||
|
<Space size='small'>
|
||||||
|
<ObjectActions
|
||||||
|
type='filament'
|
||||||
|
id={filamentId}
|
||||||
|
disabled={loading}
|
||||||
|
/>
|
||||||
|
<ViewButton
|
||||||
|
disabled={loading}
|
||||||
|
items={[
|
||||||
|
{ key: 'info', label: 'Filament Information' },
|
||||||
|
{ key: 'notes', label: 'Notes' },
|
||||||
|
{ key: 'auditLogs', label: 'Audit Logs' }
|
||||||
|
]}
|
||||||
|
visibleState={collapseState}
|
||||||
|
updateVisibleState={updateCollapseState}
|
||||||
|
/>
|
||||||
|
</Space>
|
||||||
|
<LockIndicator lock={lock} />
|
||||||
|
</Space>
|
||||||
|
<Space>
|
||||||
|
<EditButtons
|
||||||
|
isEditing={isEditing}
|
||||||
|
handleUpdate={() => {
|
||||||
|
callAction('finishEdit')
|
||||||
|
}}
|
||||||
|
cancelEditing={() => {
|
||||||
|
callAction('cancelEdit')
|
||||||
|
}}
|
||||||
|
startEditing={() => {
|
||||||
|
callAction('edit')
|
||||||
|
}}
|
||||||
|
editLoading={editLoading}
|
||||||
|
formValid={formValid}
|
||||||
|
disabled={lock?.locked || loading}
|
||||||
|
loading={editLoading}
|
||||||
|
/>
|
||||||
|
</Space>
|
||||||
|
</Flex>
|
||||||
|
|
||||||
|
<div style={{ height: '100%', overflowY: 'scroll' }}>
|
||||||
|
<Flex vertical gap={'large'}>
|
||||||
|
<InfoCollapse
|
||||||
|
title='Filament Information'
|
||||||
|
icon={<InfoCircleIcon />}
|
||||||
|
active={collapseState.info}
|
||||||
|
onToggle={(expanded) =>
|
||||||
|
updateCollapseState('info', expanded)
|
||||||
}
|
}
|
||||||
],
|
key='info'
|
||||||
onClick: ({ key }) => {
|
>
|
||||||
if (key === 'reload') {
|
<ObjectInfo
|
||||||
fetchObject()
|
loading={loading}
|
||||||
|
indicator={<LoadingOutlined />}
|
||||||
|
isEditing={isEditing}
|
||||||
|
type='filament'
|
||||||
|
objectData={objectData}
|
||||||
|
/>
|
||||||
|
</InfoCollapse>
|
||||||
|
|
||||||
|
<InfoCollapse
|
||||||
|
title='Notes'
|
||||||
|
icon={<NoteIcon />}
|
||||||
|
active={collapseState.notes}
|
||||||
|
onToggle={(expanded) =>
|
||||||
|
updateCollapseState('notes', expanded)
|
||||||
}
|
}
|
||||||
}
|
key='notes'
|
||||||
}}
|
>
|
||||||
>
|
<Card>
|
||||||
<Button disabled={loading}>Actions</Button>
|
<NotesPanel _id={filamentId} />
|
||||||
</Dropdown>
|
</Card>
|
||||||
<ViewButton
|
</InfoCollapse>
|
||||||
loading={loading}
|
|
||||||
sections={[
|
|
||||||
{ key: 'info', label: 'Filament Information' },
|
|
||||||
{ key: 'notes', label: 'Notes' },
|
|
||||||
{ key: 'auditLogs', label: 'Audit Logs' }
|
|
||||||
]}
|
|
||||||
collapseState={collapseState}
|
|
||||||
updateCollapseState={updateCollapseState}
|
|
||||||
/>
|
|
||||||
</Space>
|
|
||||||
<LockIndicator lock={lock} />
|
|
||||||
</Space>
|
|
||||||
<Space>
|
|
||||||
<EditButtons
|
|
||||||
isEditing={isEditing}
|
|
||||||
handleUpdate={handleUpdate}
|
|
||||||
cancelEditing={cancelEditing}
|
|
||||||
startEditing={startEditing}
|
|
||||||
editLoading={editLoading}
|
|
||||||
formValid={formValid}
|
|
||||||
disabled={lock?.locked || loading}
|
|
||||||
loading={editLoading}
|
|
||||||
/>
|
|
||||||
</Space>
|
|
||||||
</Flex>
|
|
||||||
|
|
||||||
<div style={{ height: '100%', overflowY: 'scroll' }}>
|
<InfoCollapse
|
||||||
<Flex vertical gap={'large'}>
|
title='Audit Logs'
|
||||||
<InfoCollapse
|
icon={<AuditLogIcon />}
|
||||||
title='Filament Information'
|
active={collapseState.auditLogs}
|
||||||
icon={<InfoCircleIcon />}
|
onToggle={(expanded) =>
|
||||||
active={collapseState.info}
|
updateCollapseState('auditLogs', expanded)
|
||||||
onToggle={(expanded) => updateCollapseState('info', expanded)}
|
}
|
||||||
key='info'
|
key='auditLogs'
|
||||||
>
|
>
|
||||||
<ObjectInfo
|
{loading ? (
|
||||||
loading={loading}
|
<InfoCollapsePlaceholder />
|
||||||
indicator={<LoadingOutlined />}
|
) : (
|
||||||
isEditing={isEditing}
|
<ObjectTable
|
||||||
items={getModelProperties('filament').map((prop) => ({
|
type='auditLog'
|
||||||
...prop,
|
masterFilter={{ 'parent._id': filamentId }}
|
||||||
value: getPropertyValue(objectData, prop.name)
|
visibleColumns={{ _id: false, 'parent._id': false }}
|
||||||
}))}
|
/>
|
||||||
/>
|
)}
|
||||||
</InfoCollapse>
|
</InfoCollapse>
|
||||||
|
</Flex>
|
||||||
<InfoCollapse
|
</div>
|
||||||
title='Notes'
|
</Flex>
|
||||||
icon={<NoteIcon />}
|
)}
|
||||||
active={collapseState.notes}
|
</ActionHandler>
|
||||||
onToggle={(expanded) => updateCollapseState('notes', expanded)}
|
)
|
||||||
key='notes'
|
}}
|
||||||
>
|
|
||||||
<Card>
|
|
||||||
<NotesPanel _id={filamentId} />
|
|
||||||
</Card>
|
|
||||||
</InfoCollapse>
|
|
||||||
|
|
||||||
<InfoCollapse
|
|
||||||
title='Audit Logs'
|
|
||||||
icon={<AuditLogIcon />}
|
|
||||||
active={collapseState.auditLogs}
|
|
||||||
onToggle={(expanded) =>
|
|
||||||
updateCollapseState('auditLogs', expanded)
|
|
||||||
}
|
|
||||||
key='auditLogs'
|
|
||||||
>
|
|
||||||
<AuditLogTable
|
|
||||||
items={objectData?.auditLogs || []}
|
|
||||||
loading={loading}
|
|
||||||
showTargetColumn={false}
|
|
||||||
/>
|
|
||||||
</InfoCollapse>
|
|
||||||
</Flex>
|
|
||||||
</div>
|
|
||||||
</Flex>
|
|
||||||
)}
|
|
||||||
</EditObjectForm>
|
</EditObjectForm>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,10 +1,8 @@
|
|||||||
import React from 'react'
|
import React from 'react'
|
||||||
import { useLocation } from 'react-router-dom'
|
import { useLocation } from 'react-router-dom'
|
||||||
import { Space, Button, Flex, Dropdown } from 'antd'
|
import { Space, Flex } from 'antd'
|
||||||
import { LoadingOutlined } from '@ant-design/icons'
|
import { LoadingOutlined } from '@ant-design/icons'
|
||||||
import ReloadIcon from '../../../Icons/ReloadIcon'
|
|
||||||
import useCollapseState from '../../hooks/useCollapseState'
|
import useCollapseState from '../../hooks/useCollapseState'
|
||||||
import AuditLogTable from '../../common/AuditLogTable'
|
|
||||||
import InfoCollapse from '../../common/InfoCollapse'
|
import InfoCollapse from '../../common/InfoCollapse'
|
||||||
import ObjectInfo from '../../common/ObjectInfo'
|
import ObjectInfo from '../../common/ObjectInfo'
|
||||||
import ViewButton from '../../common/ViewButton'
|
import ViewButton from '../../common/ViewButton'
|
||||||
@ -13,10 +11,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 {
|
import ActionHandler from '../../common/ActionHandler.jsx'
|
||||||
getModelProperties,
|
import ObjectActions from '../../common/ObjectActions.jsx'
|
||||||
getPropertyValue
|
import ObjectTable from '../../common/ObjectTable.jsx'
|
||||||
} from '../../../../database/ObjectModels.js'
|
import InfoCollapsePlaceholder from '../../common/InfoCollapsePlaceholder.jsx'
|
||||||
|
|
||||||
const NoteTypeInfo = () => {
|
const NoteTypeInfo = () => {
|
||||||
const location = useLocation()
|
const location = useLocation()
|
||||||
@ -32,7 +30,7 @@ const NoteTypeInfo = () => {
|
|||||||
return (
|
return (
|
||||||
<EditObjectForm
|
<EditObjectForm
|
||||||
id={noteTypeId}
|
id={noteTypeId}
|
||||||
type='notetype'
|
type='noteType'
|
||||||
style={{ height: 'calc(var(--unit-100vh) - 155px)', minHeight: 0 }}
|
style={{ height: 'calc(var(--unit-100vh) - 155px)', minHeight: 0 }}
|
||||||
>
|
>
|
||||||
{({
|
{({
|
||||||
@ -46,99 +44,121 @@ const NoteTypeInfo = () => {
|
|||||||
editLoading,
|
editLoading,
|
||||||
lock,
|
lock,
|
||||||
fetchObject
|
fetchObject
|
||||||
}) => (
|
}) => {
|
||||||
<Flex
|
// Define actions for ActionHandler
|
||||||
gap='large'
|
const actions = {
|
||||||
vertical='true'
|
reload: () => {
|
||||||
style={{ height: '100%', minHeight: 0 }}
|
fetchObject()
|
||||||
>
|
return true
|
||||||
<Flex justify={'space-between'}>
|
},
|
||||||
<Space size='middle'>
|
edit: () => {
|
||||||
<Space size='small'>
|
startEditing()
|
||||||
<Dropdown
|
return false
|
||||||
menu={{
|
},
|
||||||
items: [
|
cancelEdit: () => {
|
||||||
{
|
cancelEditing()
|
||||||
label: 'Reload Note Type',
|
return true
|
||||||
key: 'reload',
|
},
|
||||||
icon: <ReloadIcon />
|
finishEdit: () => {
|
||||||
}
|
handleUpdate()
|
||||||
],
|
return true
|
||||||
onClick: ({ key }) => {
|
}
|
||||||
if (key === 'reload') {
|
}
|
||||||
fetchObject()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<Button disabled={loading}>Actions</Button>
|
|
||||||
</Dropdown>
|
|
||||||
<ViewButton
|
|
||||||
loading={loading}
|
|
||||||
sections={[
|
|
||||||
{ key: 'info', label: 'Note Type Information' },
|
|
||||||
{ key: 'auditLogs', label: 'Audit Logs' }
|
|
||||||
]}
|
|
||||||
collapseState={collapseState}
|
|
||||||
updateCollapseState={updateCollapseState}
|
|
||||||
/>
|
|
||||||
</Space>
|
|
||||||
<LockIndicator lock={lock} />
|
|
||||||
</Space>
|
|
||||||
<Space>
|
|
||||||
<EditButtons
|
|
||||||
isEditing={isEditing}
|
|
||||||
handleUpdate={handleUpdate}
|
|
||||||
cancelEditing={cancelEditing}
|
|
||||||
startEditing={startEditing}
|
|
||||||
editLoading={editLoading}
|
|
||||||
formValid={formValid}
|
|
||||||
disabled={lock?.locked || loading}
|
|
||||||
loading={editLoading}
|
|
||||||
/>
|
|
||||||
</Space>
|
|
||||||
</Flex>
|
|
||||||
|
|
||||||
<div style={{ height: '100%', overflow: 'auto' }}>
|
return (
|
||||||
<Flex vertical gap={'large'}>
|
<ActionHandler actions={actions} loading={loading}>
|
||||||
<InfoCollapse
|
{({ callAction }) => (
|
||||||
title='Note Type Information'
|
<Flex
|
||||||
icon={<InfoCircleIcon />}
|
gap='large'
|
||||||
active={collapseState.info}
|
vertical='true'
|
||||||
onToggle={(expanded) => updateCollapseState('info', expanded)}
|
style={{ height: '100%', minHeight: 0 }}
|
||||||
key='info'
|
|
||||||
>
|
>
|
||||||
<ObjectInfo
|
<Flex justify={'space-between'}>
|
||||||
loading={loading}
|
<Space size='middle'>
|
||||||
indicator={<LoadingOutlined />}
|
<Space size='small'>
|
||||||
isEditing={isEditing}
|
<ObjectActions
|
||||||
type='noteType'
|
type='noteType'
|
||||||
items={getModelProperties('noteType').map((prop) => ({
|
id={noteTypeId}
|
||||||
...prop,
|
disabled={loading}
|
||||||
value: getPropertyValue(objectData, prop.name)
|
/>
|
||||||
}))}
|
<ViewButton
|
||||||
/>
|
disabled={loading}
|
||||||
</InfoCollapse>
|
items={[
|
||||||
|
{ key: 'info', label: 'Note Type Information' },
|
||||||
|
{ key: 'auditLogs', label: 'Audit Logs' }
|
||||||
|
]}
|
||||||
|
visibleState={collapseState}
|
||||||
|
updateVisibleState={updateCollapseState}
|
||||||
|
/>
|
||||||
|
</Space>
|
||||||
|
<LockIndicator lock={lock} />
|
||||||
|
</Space>
|
||||||
|
<Space>
|
||||||
|
<EditButtons
|
||||||
|
isEditing={isEditing}
|
||||||
|
handleUpdate={() => {
|
||||||
|
callAction('finishEdit')
|
||||||
|
}}
|
||||||
|
cancelEditing={() => {
|
||||||
|
callAction('cancelEdit')
|
||||||
|
}}
|
||||||
|
startEditing={() => {
|
||||||
|
callAction('edit')
|
||||||
|
}}
|
||||||
|
editLoading={editLoading}
|
||||||
|
formValid={formValid}
|
||||||
|
disabled={lock?.locked || loading}
|
||||||
|
loading={editLoading}
|
||||||
|
/>
|
||||||
|
</Space>
|
||||||
|
</Flex>
|
||||||
|
|
||||||
<InfoCollapse
|
<div style={{ height: '100%', overflow: 'auto' }}>
|
||||||
title='Audit Logs'
|
<Flex vertical gap={'large'}>
|
||||||
icon={<AuditLogIcon />}
|
<InfoCollapse
|
||||||
active={collapseState.auditLogs}
|
title='Note Type Information'
|
||||||
onToggle={(expanded) =>
|
icon={<InfoCircleIcon />}
|
||||||
updateCollapseState('auditLogs', expanded)
|
active={collapseState.info}
|
||||||
}
|
onToggle={(expanded) =>
|
||||||
key='auditLogs'
|
updateCollapseState('info', expanded)
|
||||||
>
|
}
|
||||||
<AuditLogTable
|
key='info'
|
||||||
items={objectData?.auditLogs || []}
|
>
|
||||||
loading={loading}
|
<ObjectInfo
|
||||||
showTargetColumn={false}
|
loading={loading}
|
||||||
/>
|
indicator={<LoadingOutlined />}
|
||||||
</InfoCollapse>
|
isEditing={isEditing}
|
||||||
</Flex>
|
type='noteType'
|
||||||
</div>
|
objectData={objectData}
|
||||||
</Flex>
|
/>
|
||||||
)}
|
</InfoCollapse>
|
||||||
|
|
||||||
|
<InfoCollapse
|
||||||
|
title='Audit Logs'
|
||||||
|
icon={<AuditLogIcon />}
|
||||||
|
active={collapseState.auditLogs}
|
||||||
|
onToggle={(expanded) =>
|
||||||
|
updateCollapseState('auditLogs', expanded)
|
||||||
|
}
|
||||||
|
key='auditLogs'
|
||||||
|
>
|
||||||
|
{loading ? (
|
||||||
|
<InfoCollapsePlaceholder />
|
||||||
|
) : (
|
||||||
|
<ObjectTable
|
||||||
|
type='auditLog'
|
||||||
|
masterFilter={{ 'parent._id': noteTypeId }}
|
||||||
|
visibleColumns={{ _id: false, 'parent._id': false }}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</InfoCollapse>
|
||||||
|
</Flex>
|
||||||
|
</div>
|
||||||
|
</Flex>
|
||||||
|
)}
|
||||||
|
</ActionHandler>
|
||||||
|
)
|
||||||
|
}}
|
||||||
</EditObjectForm>
|
</EditObjectForm>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,11 +1,8 @@
|
|||||||
// src/gcodefiles.js
|
// src/gcodefiles.js
|
||||||
|
|
||||||
import React, { useState, useContext, useRef } from 'react'
|
import React, { useState, useRef } from 'react'
|
||||||
|
|
||||||
import { Button, Flex, Space, Modal, Dropdown, message } from 'antd'
|
import { Button, Flex, Space, Modal, Dropdown, message } from 'antd'
|
||||||
|
|
||||||
import { AuthContext } from '../context/AuthContext'
|
|
||||||
|
|
||||||
import ObjectTable from '../common/ObjectTable'
|
import ObjectTable from '../common/ObjectTable'
|
||||||
import NewProduct from './Products/NewProduct'
|
import NewProduct from './Products/NewProduct'
|
||||||
|
|
||||||
@ -20,14 +17,12 @@ import useViewMode from '../hooks/useViewMode'
|
|||||||
|
|
||||||
import ColumnViewButton from '../common/ColumnViewButton'
|
import ColumnViewButton from '../common/ColumnViewButton'
|
||||||
|
|
||||||
const Parts = () => {
|
const Parts = (filter) => {
|
||||||
const [messageApi, contextHolder] = message.useMessage()
|
const [messageApi, contextHolder] = message.useMessage()
|
||||||
|
|
||||||
const [newProductOpen, setNewProductOpen] = useState(false)
|
const [newProductOpen, setNewProductOpen] = useState(false)
|
||||||
const tableRef = useRef()
|
const tableRef = useRef()
|
||||||
const { authenticated } = useContext(AuthContext)
|
|
||||||
const [viewMode, setViewMode] = useViewMode('part')
|
const [viewMode, setViewMode] = useViewMode('part')
|
||||||
|
|
||||||
const [columnVisibility, setColumnVisibility] = useColumnVisibility('part')
|
const [columnVisibility, setColumnVisibility] = useColumnVisibility('part')
|
||||||
|
|
||||||
const actionItems = {
|
const actionItems = {
|
||||||
@ -82,8 +77,8 @@ const Parts = () => {
|
|||||||
ref={tableRef}
|
ref={tableRef}
|
||||||
visibleColumns={columnVisibility}
|
visibleColumns={columnVisibility}
|
||||||
type='part'
|
type='part'
|
||||||
authenticated={authenticated}
|
|
||||||
cards={viewMode === 'cards'}
|
cards={viewMode === 'cards'}
|
||||||
|
filter={filter}
|
||||||
/>
|
/>
|
||||||
</Flex>
|
</Flex>
|
||||||
<Modal
|
<Modal
|
||||||
|
|||||||
@ -1,709 +1,187 @@
|
|||||||
import React, { useState, useEffect, useRef } from 'react'
|
import React, { useContext } from 'react'
|
||||||
import { useLocation } from 'react-router-dom'
|
import { useLocation } from 'react-router-dom'
|
||||||
import axios from 'axios'
|
import { Space, Flex, Card } from 'antd'
|
||||||
import {
|
|
||||||
Descriptions,
|
|
||||||
Spin,
|
|
||||||
Space,
|
|
||||||
Button,
|
|
||||||
message,
|
|
||||||
Typography,
|
|
||||||
Card,
|
|
||||||
Flex,
|
|
||||||
Form,
|
|
||||||
Input,
|
|
||||||
Checkbox,
|
|
||||||
InputNumber,
|
|
||||||
Switch,
|
|
||||||
Tag,
|
|
||||||
Collapse,
|
|
||||||
Dropdown,
|
|
||||||
Popover
|
|
||||||
} from 'antd'
|
|
||||||
import { LoadingOutlined, CaretLeftOutlined } from '@ant-design/icons'
|
|
||||||
import IdDisplay from '../../common/IdDisplay.jsx'
|
|
||||||
import { StlViewer } from 'react-stl-viewer'
|
|
||||||
import ReloadIcon from '../../../Icons/ReloadIcon'
|
|
||||||
import EditIcon from '../../../Icons/EditIcon.jsx'
|
|
||||||
import XMarkIcon from '../../../Icons/XMarkIcon.jsx'
|
|
||||||
import CheckIcon from '../../../Icons/CheckIcon.jsx'
|
|
||||||
import useCollapseState from '../../hooks/useCollapseState'
|
import useCollapseState from '../../hooks/useCollapseState'
|
||||||
import TimeDisplay from '../../common/TimeDisplay.jsx'
|
|
||||||
import AuditLogTable from '../../common/AuditLogTable'
|
|
||||||
import NotesPanel from '../../common/NotesPanel'
|
import NotesPanel from '../../common/NotesPanel'
|
||||||
|
import InfoCollapse from '../../common/InfoCollapse'
|
||||||
import config from '../../../../config.js'
|
import ObjectInfo from '../../common/ObjectInfo'
|
||||||
import BoolDisplay from '../../common/BoolDisplay.jsx'
|
import ViewButton from '../../common/ViewButton'
|
||||||
|
import EditObjectForm from '../../common/EditObjectForm'
|
||||||
|
import EditButtons from '../../common/EditButtons'
|
||||||
|
import LockIndicator from '../Filaments/LockIndicator'
|
||||||
import InfoCircleIcon from '../../../Icons/InfoCircleIcon.jsx'
|
import InfoCircleIcon from '../../../Icons/InfoCircleIcon.jsx'
|
||||||
import PartIcon from '../../../Icons/PartIcon.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 loglevel from 'loglevel'
|
import ActionHandler from '../../common/ActionHandler.jsx'
|
||||||
const logger = loglevel.getLogger('PartInfo')
|
import ObjectActions from '../../common/ObjectActions.jsx'
|
||||||
logger.setLevel(config.logLevel)
|
import ObjectTable from '../../common/ObjectTable.jsx'
|
||||||
|
import InfoCollapsePlaceholder from '../../common/InfoCollapsePlaceholder.jsx'
|
||||||
const { Title, Text } = Typography
|
import { ApiServerContext } from '../../context/ApiServerContext'
|
||||||
|
|
||||||
const PartInfo = () => {
|
const PartInfo = () => {
|
||||||
const [partData, setPartData] = useState(null)
|
|
||||||
const [loading, setLoading] = useState(false)
|
|
||||||
const [error, setError] = useState(null)
|
|
||||||
const location = useLocation()
|
const location = useLocation()
|
||||||
const [messageApi, contextHolder] = message.useMessage()
|
|
||||||
const partId = new URLSearchParams(location.search).get('partId')
|
const partId = new URLSearchParams(location.search).get('partId')
|
||||||
const [marginOrPrice, setMarginOrPrice] = useState(false)
|
|
||||||
const [useGlobalPricing, setUseGlobalPricing] = useState(true)
|
const { handleDownloadContent } = useContext(ApiServerContext)
|
||||||
|
|
||||||
const [collapseState, updateCollapseState] = useCollapseState('PartInfo', {
|
const [collapseState, updateCollapseState] = useCollapseState('PartInfo', {
|
||||||
info: true,
|
info: true,
|
||||||
preview: true,
|
parts: true,
|
||||||
notes: true,
|
notes: true,
|
||||||
auditLogs: true
|
auditLogs: true
|
||||||
})
|
})
|
||||||
|
|
||||||
const [partForm] = Form.useForm()
|
|
||||||
const [partFormValues, setPartFormValues] = useState({})
|
|
||||||
|
|
||||||
// Add a ref to store the object URL
|
|
||||||
const objectUrlRef = useRef(null)
|
|
||||||
// Add a ref to store the array buffer
|
|
||||||
const arrayBufferRef = useRef(null)
|
|
||||||
|
|
||||||
const [isEditing, setIsEditing] = useState(false)
|
|
||||||
const [fetchLoading, setFetchLoading] = useState(true)
|
|
||||||
|
|
||||||
const [partFileObjectId, setPartFileObjectId] = useState(null)
|
|
||||||
const [stlLoadError, setStlLoadError] = useState(null)
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
async function fetchData() {
|
|
||||||
await fetchPartDetails()
|
|
||||||
setTimeout(async () => {
|
|
||||||
await fetchPartContent()
|
|
||||||
}, 1000)
|
|
||||||
}
|
|
||||||
if (partId) {
|
|
||||||
fetchData()
|
|
||||||
}
|
|
||||||
}, [partId])
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
if (partData) {
|
|
||||||
partForm.setFieldsValue({
|
|
||||||
name: partData.name || '',
|
|
||||||
price: partData.price || null,
|
|
||||||
margin: partData.margin || null,
|
|
||||||
marginOrPrice: partData.marginOrPrice,
|
|
||||||
useGlobalPricing: partData.useGlobalPricing,
|
|
||||||
createdAt: partData.createdAt || null,
|
|
||||||
updatedAt: partData.updatedAt || null
|
|
||||||
})
|
|
||||||
setPartFormValues(partData)
|
|
||||||
}
|
|
||||||
}, [partData, partForm])
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
setMarginOrPrice(partFormValues.marginOrPrice)
|
|
||||||
setUseGlobalPricing(partFormValues.useGlobalPricing)
|
|
||||||
}, [partFormValues])
|
|
||||||
|
|
||||||
const fetchPartDetails = async () => {
|
|
||||||
try {
|
|
||||||
setFetchLoading(true)
|
|
||||||
const response = await axios.get(`${config.backendUrl}/parts/${partId}`, {
|
|
||||||
headers: {
|
|
||||||
Accept: 'application/json'
|
|
||||||
},
|
|
||||||
withCredentials: true
|
|
||||||
})
|
|
||||||
setPartData(response.data)
|
|
||||||
setError(null)
|
|
||||||
} catch (err) {
|
|
||||||
setError('Failed to fetch part details')
|
|
||||||
logger.debug(err)
|
|
||||||
messageApi.error('Failed to fetch part details')
|
|
||||||
} finally {
|
|
||||||
setFetchLoading(false)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const fetchPartContent = async () => {
|
|
||||||
if (fetchLoading == true) {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
try {
|
|
||||||
setFetchLoading(true)
|
|
||||||
// Cleanup previous object URL if it exists
|
|
||||||
if (objectUrlRef.current) {
|
|
||||||
URL.revokeObjectURL(objectUrlRef.current)
|
|
||||||
objectUrlRef.current = null
|
|
||||||
}
|
|
||||||
const response = await axios.get(
|
|
||||||
`${config.backendUrl}/parts/${partId}/content`,
|
|
||||||
{
|
|
||||||
withCredentials: true,
|
|
||||||
responseType: 'blob'
|
|
||||||
}
|
|
||||||
)
|
|
||||||
|
|
||||||
// Check file size before processing
|
|
||||||
const MAX_FILE_SIZE = 100 * 1024 * 1024 // 100MB
|
|
||||||
if (response.data.size > MAX_FILE_SIZE) {
|
|
||||||
throw new Error(
|
|
||||||
`File size exceeds ${MAX_FILE_SIZE / (1024 * 1024)}MB limit`
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Convert blob to array buffer for better memory management
|
|
||||||
const arrayBuffer = await response.data.arrayBuffer()
|
|
||||||
|
|
||||||
// Store array buffer in ref for later cleanup
|
|
||||||
arrayBufferRef.current = arrayBuffer
|
|
||||||
|
|
||||||
// Create a new blob from the array buffer
|
|
||||||
const blob = new Blob([arrayBuffer], { type: response.data.type })
|
|
||||||
|
|
||||||
try {
|
|
||||||
// Create and store object URL
|
|
||||||
const objectUrl = URL.createObjectURL(blob)
|
|
||||||
objectUrlRef.current = objectUrl
|
|
||||||
|
|
||||||
// Update state with the new object URL
|
|
||||||
setPartFileObjectId(objectUrl)
|
|
||||||
setStlLoadError(null)
|
|
||||||
setError(null)
|
|
||||||
} catch (allocErr) {
|
|
||||||
setStlLoadError(
|
|
||||||
'Failed to load STL file: Array buffer allocation failed'
|
|
||||||
)
|
|
||||||
console.error('STL allocation error:', allocErr)
|
|
||||||
}
|
|
||||||
} catch (err) {
|
|
||||||
setError('Failed to fetch part content')
|
|
||||||
logger.debug(err)
|
|
||||||
messageApi.error('Failed to fetch part content')
|
|
||||||
} finally {
|
|
||||||
setFetchLoading(false)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const startEditing = () => {
|
|
||||||
updateCollapseState('info', true)
|
|
||||||
setIsEditing(true)
|
|
||||||
}
|
|
||||||
|
|
||||||
const cancelEditing = () => {
|
|
||||||
// Reset form values to original data
|
|
||||||
if (partData) {
|
|
||||||
partForm.setFieldsValue({
|
|
||||||
name: partData.name || '',
|
|
||||||
price: partData.price || null,
|
|
||||||
margin: partData.margin || null,
|
|
||||||
marginOrPrice: partData.marginOrPrice,
|
|
||||||
useGlobalPricing: partData.useGlobalPricing
|
|
||||||
})
|
|
||||||
}
|
|
||||||
setIsEditing(false)
|
|
||||||
}
|
|
||||||
|
|
||||||
const updateInfo = async () => {
|
|
||||||
try {
|
|
||||||
const values = await partForm.validateFields()
|
|
||||||
setLoading(true)
|
|
||||||
|
|
||||||
await axios.put(`${config.backendUrl}/parts/${partId}`, values, {
|
|
||||||
headers: {
|
|
||||||
'Content-Type': 'application/json'
|
|
||||||
},
|
|
||||||
withCredentials: true
|
|
||||||
})
|
|
||||||
|
|
||||||
// Update the local state with the new values
|
|
||||||
setPartData({ ...partData, ...values })
|
|
||||||
setIsEditing(false)
|
|
||||||
messageApi.success('Part information updated successfully')
|
|
||||||
} catch (err) {
|
|
||||||
if (err.errorFields) {
|
|
||||||
// This is a form validation error
|
|
||||||
return
|
|
||||||
}
|
|
||||||
console.error('Failed to update part information:', err)
|
|
||||||
messageApi.error('Failed to update part information')
|
|
||||||
} finally {
|
|
||||||
await fetchPartDetails()
|
|
||||||
setLoading(false)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const actionItems = {
|
|
||||||
items: [
|
|
||||||
{
|
|
||||||
label: 'Reload Part',
|
|
||||||
key: 'reload',
|
|
||||||
icon: <ReloadIcon />
|
|
||||||
}
|
|
||||||
],
|
|
||||||
onClick: ({ key }) => {
|
|
||||||
if (key === 'reload') {
|
|
||||||
fetchPartDetails()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const getViewDropdownItems = () => {
|
|
||||||
const sections = [
|
|
||||||
{ key: 'info', label: 'Part Information' },
|
|
||||||
{ key: 'preview', label: 'Part Preview' },
|
|
||||||
{ key: 'notes', label: 'Notes' },
|
|
||||||
{ key: 'auditLogs', label: 'Audit Logs' }
|
|
||||||
]
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Flex vertical>
|
|
||||||
<Flex vertical gap='middle' style={{ margin: '4px 8px' }}>
|
|
||||||
{sections.map((section) => (
|
|
||||||
<Checkbox
|
|
||||||
checked={collapseState[section.key]}
|
|
||||||
key={section.key}
|
|
||||||
onChange={(e) => {
|
|
||||||
updateCollapseState(section.key, e.target.checked)
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
{section.label}
|
|
||||||
</Checkbox>
|
|
||||||
))}
|
|
||||||
</Flex>
|
|
||||||
</Flex>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
if (error) {
|
|
||||||
return (
|
|
||||||
<Space
|
|
||||||
direction='vertical'
|
|
||||||
style={{ width: '100%', textAlign: 'center' }}
|
|
||||||
>
|
|
||||||
<p>{error || 'Part not found'}</p>
|
|
||||||
<Button icon={<ReloadIcon />} onClick={fetchPartDetails}>
|
|
||||||
Retry
|
|
||||||
</Button>
|
|
||||||
</Space>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<EditObjectForm
|
||||||
{contextHolder}
|
id={partId}
|
||||||
<Flex
|
type='part'
|
||||||
gap='large'
|
style={{ height: 'calc(var(--unit-100vh) - 155px)', minHeight: 0 }}
|
||||||
vertical='true'
|
>
|
||||||
style={{ height: '100%', minHeight: 0 }}
|
{({
|
||||||
>
|
loading,
|
||||||
<Flex justify={'space-between'}>
|
isEditing,
|
||||||
<Space size='small'>
|
startEditing,
|
||||||
<Dropdown menu={actionItems}>
|
cancelEditing,
|
||||||
<Button>Actions</Button>
|
handleUpdate,
|
||||||
</Dropdown>
|
formValid,
|
||||||
<Popover
|
objectData,
|
||||||
content={getViewDropdownItems()}
|
editLoading,
|
||||||
placement='bottomLeft'
|
lock,
|
||||||
arrow={false}
|
fetchObject
|
||||||
>
|
}) => {
|
||||||
<Button>View</Button>
|
const actions = {
|
||||||
</Popover>
|
reload: () => {
|
||||||
</Space>
|
fetchObject()
|
||||||
<Space>
|
return true
|
||||||
{isEditing ? (
|
},
|
||||||
<>
|
edit: () => {
|
||||||
<Button
|
startEditing()
|
||||||
icon={<CheckIcon />}
|
return false
|
||||||
type='primary'
|
},
|
||||||
onClick={updateInfo}
|
cancelEdit: () => {
|
||||||
loading={loading}
|
cancelEditing()
|
||||||
disabled={loading}
|
return true
|
||||||
/>
|
},
|
||||||
<Button
|
finishEdit: () => {
|
||||||
icon={<XMarkIcon />}
|
handleUpdate()
|
||||||
onClick={cancelEditing}
|
return true
|
||||||
disabled={loading}
|
},
|
||||||
/>
|
download: () => {
|
||||||
</>
|
if (partId) {
|
||||||
) : (
|
handleDownloadContent(partId, 'part', `${objectData.name}.stl`)
|
||||||
<Button icon={<EditIcon />} onClick={startEditing} />
|
return true
|
||||||
)}
|
}
|
||||||
</Space>
|
}
|
||||||
</Flex>
|
}
|
||||||
|
return (
|
||||||
{error ? (
|
<ActionHandler actions={actions} loading={loading}>
|
||||||
<Space
|
{({ callAction }) => (
|
||||||
direction='vertical'
|
<Flex
|
||||||
style={{ width: '100%', textAlign: 'center' }}
|
gap='large'
|
||||||
>
|
vertical='true'
|
||||||
<p>{error || 'Part not found'}</p>
|
style={{ height: '100%', minHeight: 0 }}
|
||||||
<Button icon={<ReloadIcon />} onClick={fetchPartDetails}>
|
|
||||||
Retry
|
|
||||||
</Button>
|
|
||||||
</Space>
|
|
||||||
) : (
|
|
||||||
<div style={{ height: '100%', overflow: 'auto' }}>
|
|
||||||
<Flex vertical gap={'large'}>
|
|
||||||
<Collapse
|
|
||||||
ghost
|
|
||||||
expandIconPosition='end'
|
|
||||||
activeKey={collapseState.info ? ['1'] : []}
|
|
||||||
onChange={(keys) =>
|
|
||||||
updateCollapseState('info', keys.length > 0)
|
|
||||||
}
|
|
||||||
expandIcon={({ isActive }) => (
|
|
||||||
<CaretLeftOutlined rotate={isActive ? -90 : 0} />
|
|
||||||
)}
|
|
||||||
className='no-h-padding-collapse no-t-padding-collapse'
|
|
||||||
>
|
>
|
||||||
<Collapse.Panel
|
<Flex justify={'space-between'}>
|
||||||
header={
|
<Space size='middle'>
|
||||||
<Flex align='center' gap={'middle'}>
|
<Space size='small'>
|
||||||
<InfoCircleIcon />
|
<ObjectActions
|
||||||
<Title level={5} style={{ margin: 0 }}>
|
type='part'
|
||||||
Part Information
|
id={partId}
|
||||||
</Title>
|
disabled={loading}
|
||||||
</Flex>
|
/>
|
||||||
}
|
<ViewButton
|
||||||
key='1'
|
disabled={loading}
|
||||||
>
|
items={[
|
||||||
<Form
|
{ key: 'info', label: 'Part Information' },
|
||||||
form={partForm}
|
{ key: 'notes', label: 'Notes' },
|
||||||
layout='vertical'
|
{ key: 'auditLogs', label: 'Audit Logs' }
|
||||||
onValuesChange={(changedValues) =>
|
]}
|
||||||
setPartFormValues((prevValues) => ({
|
visibleState={collapseState}
|
||||||
...prevValues,
|
updateVisibleState={updateCollapseState}
|
||||||
...changedValues
|
/>
|
||||||
}))
|
</Space>
|
||||||
}
|
<LockIndicator lock={lock} />
|
||||||
initialValues={{
|
</Space>
|
||||||
name: partData?.name || '',
|
<Space>
|
||||||
version: partData?.version || '',
|
<EditButtons
|
||||||
tags: partData?.tags || []
|
isEditing={isEditing}
|
||||||
}}
|
handleUpdate={() => {
|
||||||
>
|
callAction('finishEdit')
|
||||||
<Spin
|
}}
|
||||||
indicator={<LoadingOutlined />}
|
cancelEditing={() => {
|
||||||
spinning={fetchLoading}
|
callAction('cancelEdit')
|
||||||
|
}}
|
||||||
|
startEditing={() => {
|
||||||
|
callAction('edit')
|
||||||
|
}}
|
||||||
|
editLoading={editLoading}
|
||||||
|
formValid={formValid}
|
||||||
|
disabled={lock?.locked || loading}
|
||||||
|
loading={editLoading}
|
||||||
|
/>
|
||||||
|
</Space>
|
||||||
|
</Flex>
|
||||||
|
|
||||||
|
<div style={{ height: '100%', overflow: 'auto' }}>
|
||||||
|
<Flex vertical gap={'large'}>
|
||||||
|
<InfoCollapse
|
||||||
|
title='Part Information'
|
||||||
|
icon={<InfoCircleIcon />}
|
||||||
|
active={collapseState.info}
|
||||||
|
onToggle={(expanded) =>
|
||||||
|
updateCollapseState('info', expanded)
|
||||||
|
}
|
||||||
|
key='info'
|
||||||
>
|
>
|
||||||
<Descriptions
|
<ObjectInfo
|
||||||
bordered
|
loading={loading}
|
||||||
column={{
|
isEditing={isEditing}
|
||||||
xs: 1,
|
type='part'
|
||||||
sm: 1,
|
objectData={objectData}
|
||||||
md: 1,
|
/>
|
||||||
lg: 2,
|
</InfoCollapse>
|
||||||
xl: 2,
|
|
||||||
xxl: 2
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<Descriptions.Item label='ID' span={1}>
|
|
||||||
{partData?.id ? (
|
|
||||||
<IdDisplay id={partData.id} type='part'></IdDisplay>
|
|
||||||
) : (
|
|
||||||
<Text>n/a</Text>
|
|
||||||
)}
|
|
||||||
</Descriptions.Item>
|
|
||||||
<Descriptions.Item label='Created At'>
|
|
||||||
{partData?.createdAt ? (
|
|
||||||
<TimeDisplay
|
|
||||||
dateTime={partData.createdAt}
|
|
||||||
showSince={true}
|
|
||||||
/>
|
|
||||||
) : (
|
|
||||||
<Text>n/a</Text>
|
|
||||||
)}
|
|
||||||
</Descriptions.Item>
|
|
||||||
|
|
||||||
<Descriptions.Item label='Name' span={1}>
|
<InfoCollapse
|
||||||
{isEditing ? (
|
title='Notes'
|
||||||
<Form.Item
|
icon={<NoteIcon />}
|
||||||
name='name'
|
active={collapseState.notes}
|
||||||
rules={[
|
onToggle={(expanded) =>
|
||||||
{
|
updateCollapseState('notes', expanded)
|
||||||
required: true,
|
}
|
||||||
message: 'Please enter a product name'
|
key='notes'
|
||||||
},
|
>
|
||||||
{
|
<Card>
|
||||||
max: 100,
|
<NotesPanel _id={partId} />
|
||||||
message: 'Name cannot exceed 100 characters'
|
</Card>
|
||||||
}
|
</InfoCollapse>
|
||||||
]}
|
|
||||||
style={{ margin: 0 }}
|
|
||||||
>
|
|
||||||
<Input placeholder='Enter product name' />
|
|
||||||
</Form.Item>
|
|
||||||
) : partData?.name ? (
|
|
||||||
<Text>{partData.name}</Text>
|
|
||||||
) : (
|
|
||||||
<Text>n/a</Text>
|
|
||||||
)}
|
|
||||||
</Descriptions.Item>
|
|
||||||
|
|
||||||
<Descriptions.Item label='Updated At'>
|
<InfoCollapse
|
||||||
{partData?.updatedAt ? (
|
title='Audit Logs'
|
||||||
<TimeDisplay
|
icon={<AuditLogIcon />}
|
||||||
dateTime={partData.updatedAt}
|
active={collapseState.auditLogs}
|
||||||
showSince={true}
|
onToggle={(expanded) =>
|
||||||
/>
|
updateCollapseState('auditLogs', expanded)
|
||||||
) : (
|
}
|
||||||
<Text>n/a</Text>
|
key='auditLogs'
|
||||||
)}
|
>
|
||||||
</Descriptions.Item>
|
{loading ? (
|
||||||
|
<InfoCollapsePlaceholder />
|
||||||
<Descriptions.Item label='Product Name' span={1}>
|
) : (
|
||||||
{partData?.product?.name ? (
|
<ObjectTable
|
||||||
<Text>{partData.product.name}</Text>
|
type='auditLog'
|
||||||
) : (
|
masterFilter={{ 'parent._id': partId }}
|
||||||
<Text>n/a</Text>
|
visibleColumns={{ _id: false, 'parent._id': false }}
|
||||||
)}
|
/>
|
||||||
</Descriptions.Item>
|
)}
|
||||||
<Descriptions.Item label='Product ID' span={1}>
|
</InfoCollapse>
|
||||||
{partData?.product?._id ? (
|
</Flex>
|
||||||
<IdDisplay
|
</div>
|
||||||
id={partData.product._id}
|
</Flex>
|
||||||
type={'product'}
|
)}
|
||||||
showHyperlink={true}
|
</ActionHandler>
|
||||||
/>
|
)
|
||||||
) : (
|
}}
|
||||||
<Text>n/a</Text>
|
</EditObjectForm>
|
||||||
)}
|
|
||||||
</Descriptions.Item>
|
|
||||||
<Descriptions.Item
|
|
||||||
label={!marginOrPrice ? 'Margin' : 'Price'}
|
|
||||||
span={1}
|
|
||||||
>
|
|
||||||
{isEditing && useGlobalPricing == false ? (
|
|
||||||
<Flex gap='middle'>
|
|
||||||
{marginOrPrice == false ? (
|
|
||||||
<Form.Item
|
|
||||||
name='margin'
|
|
||||||
style={{ margin: 0, flexGrow: 1 }}
|
|
||||||
rules={[
|
|
||||||
{
|
|
||||||
required: true,
|
|
||||||
message: 'Please enter a margin.'
|
|
||||||
}
|
|
||||||
]}
|
|
||||||
>
|
|
||||||
<InputNumber
|
|
||||||
controls={false}
|
|
||||||
step={0.01}
|
|
||||||
style={{ width: '100%' }}
|
|
||||||
addonAfter='%'
|
|
||||||
/>
|
|
||||||
</Form.Item>
|
|
||||||
) : (
|
|
||||||
<Form.Item
|
|
||||||
name='price'
|
|
||||||
style={{ margin: 0, flexGrow: 1 }}
|
|
||||||
rules={[
|
|
||||||
{
|
|
||||||
required: true,
|
|
||||||
message: 'Please enter a price.'
|
|
||||||
}
|
|
||||||
]}
|
|
||||||
>
|
|
||||||
<InputNumber
|
|
||||||
controls={false}
|
|
||||||
step={0.01}
|
|
||||||
style={{ width: '100%' }}
|
|
||||||
addonBefore='£'
|
|
||||||
/>
|
|
||||||
</Form.Item>
|
|
||||||
)}
|
|
||||||
<Form.Item
|
|
||||||
name='marginOrPrice'
|
|
||||||
valuePropName='checked'
|
|
||||||
style={{ margin: 0 }}
|
|
||||||
>
|
|
||||||
<Checkbox>Price</Checkbox>
|
|
||||||
</Form.Item>
|
|
||||||
</Flex>
|
|
||||||
) : partData?.margin &&
|
|
||||||
marginOrPrice == false &&
|
|
||||||
partData?.useGlobalPricing == false ? (
|
|
||||||
<Text>{partData.margin + '%'}</Text>
|
|
||||||
) : partData?.price &&
|
|
||||||
marginOrPrice == true &&
|
|
||||||
partData?.useGlobalPricing == false ? (
|
|
||||||
<Text>{'£' + partData.price}</Text>
|
|
||||||
) : (
|
|
||||||
<Text>n/a</Text>
|
|
||||||
)}
|
|
||||||
</Descriptions.Item>
|
|
||||||
<Descriptions.Item label='Global Pricing'>
|
|
||||||
{isEditing ? (
|
|
||||||
<Form.Item
|
|
||||||
name='useGlobalPricing'
|
|
||||||
rules={[
|
|
||||||
{
|
|
||||||
required: true,
|
|
||||||
message: 'Please enter a global price method'
|
|
||||||
}
|
|
||||||
]}
|
|
||||||
style={{ margin: 0 }}
|
|
||||||
valuePropName='checked'
|
|
||||||
>
|
|
||||||
<Switch />
|
|
||||||
</Form.Item>
|
|
||||||
) : partData ? (
|
|
||||||
<BoolDisplay
|
|
||||||
value={partData.useGlobalPricing}
|
|
||||||
yesNo={true}
|
|
||||||
/>
|
|
||||||
) : (
|
|
||||||
<Text>n/a</Text>
|
|
||||||
)}
|
|
||||||
</Descriptions.Item>
|
|
||||||
<Descriptions.Item label='Version' span={1}>
|
|
||||||
{partData?.version ? (
|
|
||||||
<Text>{partData.version}</Text>
|
|
||||||
) : (
|
|
||||||
<Text>n/a</Text>
|
|
||||||
)}
|
|
||||||
</Descriptions.Item>
|
|
||||||
<Descriptions.Item label='Tags'>
|
|
||||||
{partData?.tags && partData.tags.length > 0 ? (
|
|
||||||
partData.tags.map((tag, index) => (
|
|
||||||
<Tag key={index}>{tag}</Tag>
|
|
||||||
))
|
|
||||||
) : (
|
|
||||||
<Text>n/a</Text>
|
|
||||||
)}
|
|
||||||
</Descriptions.Item>
|
|
||||||
</Descriptions>
|
|
||||||
</Spin>
|
|
||||||
</Form>
|
|
||||||
</Collapse.Panel>
|
|
||||||
</Collapse>
|
|
||||||
|
|
||||||
<Collapse
|
|
||||||
ghost
|
|
||||||
expandIconPosition='end'
|
|
||||||
activeKey={collapseState.preview ? ['2'] : []}
|
|
||||||
onChange={(keys) =>
|
|
||||||
updateCollapseState('preview', keys.length > 0)
|
|
||||||
}
|
|
||||||
expandIcon={({ isActive }) => (
|
|
||||||
<CaretLeftOutlined rotate={isActive ? -90 : 0} />
|
|
||||||
)}
|
|
||||||
className='no-h-padding-collapse'
|
|
||||||
>
|
|
||||||
<Collapse.Panel
|
|
||||||
header={
|
|
||||||
<Flex align='center' gap={'middle'}>
|
|
||||||
<PartIcon />
|
|
||||||
<Title level={5} style={{ margin: 0 }}>
|
|
||||||
Part Preview
|
|
||||||
</Title>
|
|
||||||
</Flex>
|
|
||||||
}
|
|
||||||
key='2'
|
|
||||||
>
|
|
||||||
<Card styles={{ body: { padding: '10px' } }}>
|
|
||||||
{stlLoadError ? (
|
|
||||||
<div
|
|
||||||
style={{
|
|
||||||
height: '40vw',
|
|
||||||
display: 'flex',
|
|
||||||
alignItems: 'center',
|
|
||||||
justifyContent: 'center',
|
|
||||||
backgroundColor: '#f5f5f5'
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<Space direction='vertical' align='center'>
|
|
||||||
<XMarkIcon
|
|
||||||
style={{ fontSize: '24px', color: '#ff4d4f' }}
|
|
||||||
/>
|
|
||||||
<Typography.Text type='danger'>
|
|
||||||
{stlLoadError}
|
|
||||||
</Typography.Text>
|
|
||||||
</Space>
|
|
||||||
</div>
|
|
||||||
) : (
|
|
||||||
partFileObjectId && (
|
|
||||||
<StlViewer
|
|
||||||
url={partFileObjectId}
|
|
||||||
orbitControls
|
|
||||||
shadows
|
|
||||||
style={{ height: '40vw' }}
|
|
||||||
modelProps={{
|
|
||||||
color: '#008675'
|
|
||||||
}}
|
|
||||||
></StlViewer>
|
|
||||||
)
|
|
||||||
)}
|
|
||||||
</Card>
|
|
||||||
</Collapse.Panel>
|
|
||||||
</Collapse>
|
|
||||||
|
|
||||||
<Collapse
|
|
||||||
ghost
|
|
||||||
expandIconPosition='end'
|
|
||||||
activeKey={collapseState.notes ? ['notes'] : []}
|
|
||||||
onChange={(keys) =>
|
|
||||||
updateCollapseState('notes', keys.length > 0)
|
|
||||||
}
|
|
||||||
expandIcon={({ isActive }) => (
|
|
||||||
<CaretLeftOutlined rotate={isActive ? -90 : 0} />
|
|
||||||
)}
|
|
||||||
className='no-h-padding-collapse'
|
|
||||||
>
|
|
||||||
<Collapse.Panel
|
|
||||||
header={
|
|
||||||
<Flex align='center' gap={'middle'}>
|
|
||||||
<NoteIcon />
|
|
||||||
<Title level={5} style={{ margin: 0 }}>
|
|
||||||
Notes
|
|
||||||
</Title>
|
|
||||||
</Flex>
|
|
||||||
}
|
|
||||||
key='notes'
|
|
||||||
>
|
|
||||||
<Card>
|
|
||||||
<NotesPanel _id={partId} />
|
|
||||||
</Card>
|
|
||||||
</Collapse.Panel>
|
|
||||||
</Collapse>
|
|
||||||
|
|
||||||
<Collapse
|
|
||||||
ghost
|
|
||||||
expandIconPosition='end'
|
|
||||||
activeKey={collapseState.auditLogs ? ['auditLogs'] : []}
|
|
||||||
onChange={(keys) =>
|
|
||||||
updateCollapseState('auditLogs', keys.length > 0)
|
|
||||||
}
|
|
||||||
expandIcon={({ isActive }) => (
|
|
||||||
<CaretLeftOutlined rotate={isActive ? -90 : 0} />
|
|
||||||
)}
|
|
||||||
className='no-h-padding-collapse'
|
|
||||||
>
|
|
||||||
<Collapse.Panel
|
|
||||||
header={
|
|
||||||
<Flex align='center' gap={'middle'}>
|
|
||||||
<AuditLogIcon />
|
|
||||||
<Title level={5} style={{ margin: 0 }}>
|
|
||||||
Audit Logs
|
|
||||||
</Title>
|
|
||||||
</Flex>
|
|
||||||
}
|
|
||||||
key='auditLogs'
|
|
||||||
>
|
|
||||||
<AuditLogTable
|
|
||||||
items={partData?.auditLogs || []}
|
|
||||||
loading={fetchLoading}
|
|
||||||
showTargetColumn={false}
|
|
||||||
/>
|
|
||||||
</Collapse.Panel>
|
|
||||||
</Collapse>
|
|
||||||
</Flex>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</Flex>
|
|
||||||
</>
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -1,9 +1,7 @@
|
|||||||
import React from 'react'
|
import React from 'react'
|
||||||
import { useLocation } from 'react-router-dom'
|
import { useLocation } from 'react-router-dom'
|
||||||
import { Space, Button, Flex, Dropdown, Card } from 'antd'
|
import { Space, Flex, Card } from 'antd'
|
||||||
import ReloadIcon from '../../../Icons/ReloadIcon'
|
|
||||||
import useCollapseState from '../../hooks/useCollapseState'
|
import useCollapseState from '../../hooks/useCollapseState'
|
||||||
import AuditLogTable from '../../common/AuditLogTable'
|
|
||||||
import NotesPanel from '../../common/NotesPanel'
|
import NotesPanel from '../../common/NotesPanel'
|
||||||
import InfoCollapse from '../../common/InfoCollapse'
|
import InfoCollapse from '../../common/InfoCollapse'
|
||||||
import ObjectInfo from '../../common/ObjectInfo'
|
import ObjectInfo from '../../common/ObjectInfo'
|
||||||
@ -11,11 +9,14 @@ 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 '../Filaments/LockIndicator'
|
import LockIndicator from '../Filaments/LockIndicator'
|
||||||
import PartsTable from '../../common/PartsTable'
|
|
||||||
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 ProductIcon from '../../../Icons/ProductIcon.jsx'
|
import ProductIcon from '../../../Icons/ProductIcon.jsx'
|
||||||
|
import ObjectTable from '../../common/ObjectTable.jsx'
|
||||||
|
import ActionHandler from '../../common/ActionHandler.jsx'
|
||||||
|
import ObjectActions from '../../common/ObjectActions.jsx'
|
||||||
|
import InfoCollapsePlaceholder from '../../common/InfoCollapsePlaceholder.jsx'
|
||||||
|
|
||||||
const ProductInfo = () => {
|
const ProductInfo = () => {
|
||||||
const location = useLocation()
|
const location = useLocation()
|
||||||
@ -44,191 +45,153 @@ const ProductInfo = () => {
|
|||||||
editLoading,
|
editLoading,
|
||||||
lock,
|
lock,
|
||||||
fetchObject
|
fetchObject
|
||||||
}) => (
|
}) => {
|
||||||
<Flex
|
const actions = {
|
||||||
gap='large'
|
reload: () => {
|
||||||
vertical='true'
|
fetchObject()
|
||||||
style={{ height: '100%', minHeight: 0 }}
|
return true
|
||||||
>
|
},
|
||||||
<Flex justify={'space-between'}>
|
edit: () => {
|
||||||
<Space size='middle'>
|
startEditing()
|
||||||
<Space size='small'>
|
return false
|
||||||
<Dropdown
|
},
|
||||||
menu={{
|
cancelEdit: () => {
|
||||||
items: [
|
cancelEditing()
|
||||||
{
|
return true
|
||||||
label: 'Reload Product',
|
},
|
||||||
key: 'reload',
|
finishEdit: () => {
|
||||||
icon: <ReloadIcon />
|
handleUpdate()
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return (
|
||||||
|
<ActionHandler actions={actions} loading={loading}>
|
||||||
|
{({ callAction }) => (
|
||||||
|
<Flex
|
||||||
|
gap='large'
|
||||||
|
vertical='true'
|
||||||
|
style={{ height: '100%', minHeight: 0 }}
|
||||||
|
>
|
||||||
|
<Flex justify={'space-between'}>
|
||||||
|
<Space size='middle'>
|
||||||
|
<Space size='small'>
|
||||||
|
<ObjectActions
|
||||||
|
type='product'
|
||||||
|
id={productId}
|
||||||
|
disabled={loading}
|
||||||
|
/>
|
||||||
|
<ViewButton
|
||||||
|
disabled={loading}
|
||||||
|
items={[
|
||||||
|
{ key: 'info', label: 'Product Information' },
|
||||||
|
{ key: 'parts', label: 'Product Parts' },
|
||||||
|
{ key: 'notes', label: 'Notes' },
|
||||||
|
{ key: 'auditLogs', label: 'Audit Logs' }
|
||||||
|
]}
|
||||||
|
visibleState={collapseState}
|
||||||
|
updateVisibleState={updateCollapseState}
|
||||||
|
/>
|
||||||
|
</Space>
|
||||||
|
<LockIndicator lock={lock} />
|
||||||
|
</Space>
|
||||||
|
<Space>
|
||||||
|
<EditButtons
|
||||||
|
isEditing={isEditing}
|
||||||
|
handleUpdate={() => {
|
||||||
|
callAction('finishEdit')
|
||||||
|
}}
|
||||||
|
cancelEditing={() => {
|
||||||
|
callAction('cancelEdit')
|
||||||
|
}}
|
||||||
|
startEditing={() => {
|
||||||
|
callAction('edit')
|
||||||
|
}}
|
||||||
|
editLoading={editLoading}
|
||||||
|
formValid={formValid}
|
||||||
|
disabled={lock?.locked || loading}
|
||||||
|
loading={editLoading}
|
||||||
|
/>
|
||||||
|
</Space>
|
||||||
|
</Flex>
|
||||||
|
|
||||||
|
<div style={{ height: '100%', overflow: 'auto' }}>
|
||||||
|
<Flex vertical gap={'large'}>
|
||||||
|
<InfoCollapse
|
||||||
|
title='Product Information'
|
||||||
|
icon={<InfoCircleIcon />}
|
||||||
|
active={collapseState.info}
|
||||||
|
onToggle={(expanded) =>
|
||||||
|
updateCollapseState('info', expanded)
|
||||||
}
|
}
|
||||||
],
|
key='info'
|
||||||
onClick: ({ key }) => {
|
>
|
||||||
if (key === 'reload') {
|
<ObjectInfo
|
||||||
fetchObject()
|
loading={loading}
|
||||||
|
isEditing={isEditing}
|
||||||
|
type='product'
|
||||||
|
objectData={objectData}
|
||||||
|
/>
|
||||||
|
</InfoCollapse>
|
||||||
|
|
||||||
|
<InfoCollapse
|
||||||
|
title='Product Parts'
|
||||||
|
icon={<ProductIcon />}
|
||||||
|
active={collapseState.parts}
|
||||||
|
onToggle={(expanded) =>
|
||||||
|
updateCollapseState('parts', expanded)
|
||||||
}
|
}
|
||||||
}
|
key='parts'
|
||||||
}}
|
>
|
||||||
>
|
<ObjectTable
|
||||||
<Button disabled={loading}>Actions</Button>
|
type='part'
|
||||||
</Dropdown>
|
visibleColumns={{
|
||||||
<ViewButton
|
product: false,
|
||||||
loading={loading}
|
'product._id': false
|
||||||
sections={[
|
}}
|
||||||
{ key: 'info', label: 'Product Information' },
|
masterFilter={{ 'product._id': productId }}
|
||||||
{ key: 'parts', label: 'Product Parts' },
|
/>
|
||||||
{ key: 'notes', label: 'Notes' },
|
</InfoCollapse>
|
||||||
{ key: 'auditLogs', label: 'Audit Logs' }
|
|
||||||
]}
|
|
||||||
collapseState={collapseState}
|
|
||||||
updateCollapseState={updateCollapseState}
|
|
||||||
/>
|
|
||||||
</Space>
|
|
||||||
<LockIndicator lock={lock} />
|
|
||||||
</Space>
|
|
||||||
<Space>
|
|
||||||
<EditButtons
|
|
||||||
isEditing={isEditing}
|
|
||||||
handleUpdate={handleUpdate}
|
|
||||||
cancelEditing={cancelEditing}
|
|
||||||
startEditing={startEditing}
|
|
||||||
editLoading={editLoading}
|
|
||||||
formValid={formValid}
|
|
||||||
disabled={lock?.locked || loading}
|
|
||||||
loading={editLoading}
|
|
||||||
/>
|
|
||||||
</Space>
|
|
||||||
</Flex>
|
|
||||||
|
|
||||||
<div style={{ height: '100%', overflow: 'auto' }}>
|
<InfoCollapse
|
||||||
<Flex vertical gap={'large'}>
|
title='Notes'
|
||||||
<InfoCollapse
|
icon={<NoteIcon />}
|
||||||
title='Product Information'
|
active={collapseState.notes}
|
||||||
icon={<InfoCircleIcon />}
|
onToggle={(expanded) =>
|
||||||
active={collapseState.info}
|
updateCollapseState('notes', expanded)
|
||||||
onToggle={(expanded) => updateCollapseState('info', expanded)}
|
}
|
||||||
key='info'
|
key='notes'
|
||||||
>
|
>
|
||||||
<ObjectInfo
|
<Card>
|
||||||
loading={loading}
|
<NotesPanel _id={productId} />
|
||||||
isEditing={isEditing}
|
</Card>
|
||||||
indicator={null}
|
</InfoCollapse>
|
||||||
type='product'
|
|
||||||
items={[
|
|
||||||
{
|
|
||||||
name: '_id',
|
|
||||||
label: 'ID',
|
|
||||||
value: objectData?._id,
|
|
||||||
type: 'id',
|
|
||||||
objectType: 'product',
|
|
||||||
showCopy: true,
|
|
||||||
readOnly: true
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: 'createdAt',
|
|
||||||
label: 'Created At',
|
|
||||||
value: objectData?.createdAt,
|
|
||||||
type: 'dateTime',
|
|
||||||
readOnly: true
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: 'name',
|
|
||||||
label: 'Name',
|
|
||||||
value: objectData?.name,
|
|
||||||
required: true,
|
|
||||||
type: 'text'
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: 'updatedAt',
|
|
||||||
label: 'Updated At',
|
|
||||||
value: objectData?.updatedAt,
|
|
||||||
type: 'dateTime',
|
|
||||||
readOnly: true
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: 'vendor',
|
|
||||||
label: 'Vendor',
|
|
||||||
value: objectData?.vendor,
|
|
||||||
required: true,
|
|
||||||
type: 'object',
|
|
||||||
objectType: 'vendor'
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: 'version',
|
|
||||||
label: 'Version',
|
|
||||||
value: objectData?.version,
|
|
||||||
type: 'text'
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: 'tags',
|
|
||||||
label: 'Tags',
|
|
||||||
value: objectData?.tags,
|
|
||||||
type: 'tags'
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: 'marginOrPrice',
|
|
||||||
label: 'Price Mode',
|
|
||||||
value: objectData?.marginOrPrice,
|
|
||||||
type: 'bool'
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: 'margin',
|
|
||||||
label: 'Margin',
|
|
||||||
value: objectData?.margin,
|
|
||||||
type: 'number',
|
|
||||||
formItemProps: { min: 0, max: 100, step: 0.01 }
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: 'price',
|
|
||||||
label: 'Price',
|
|
||||||
value: objectData?.price,
|
|
||||||
type: 'number',
|
|
||||||
formItemProps: { min: 0, step: 0.01 }
|
|
||||||
}
|
|
||||||
]}
|
|
||||||
/>
|
|
||||||
</InfoCollapse>
|
|
||||||
|
|
||||||
<InfoCollapse
|
<InfoCollapse
|
||||||
title='Product Parts'
|
title='Audit Logs'
|
||||||
icon={<ProductIcon />}
|
icon={<AuditLogIcon />}
|
||||||
active={collapseState.parts}
|
active={collapseState.auditLogs}
|
||||||
onToggle={(expanded) => updateCollapseState('parts', expanded)}
|
onToggle={(expanded) =>
|
||||||
key='parts'
|
updateCollapseState('auditLogs', expanded)
|
||||||
>
|
}
|
||||||
<PartsTable data={objectData?.parts || []} />
|
key='auditLogs'
|
||||||
</InfoCollapse>
|
>
|
||||||
|
{loading ? (
|
||||||
<InfoCollapse
|
<InfoCollapsePlaceholder />
|
||||||
title='Notes'
|
) : (
|
||||||
icon={<NoteIcon />}
|
<ObjectTable
|
||||||
active={collapseState.notes}
|
type='auditLog'
|
||||||
onToggle={(expanded) => updateCollapseState('notes', expanded)}
|
masterFilter={{ 'parent._id': productId }}
|
||||||
key='notes'
|
visibleColumns={{ _id: false, 'parent._id': false }}
|
||||||
>
|
/>
|
||||||
<Card>
|
)}
|
||||||
<NotesPanel _id={productId} />
|
</InfoCollapse>
|
||||||
</Card>
|
</Flex>
|
||||||
</InfoCollapse>
|
</div>
|
||||||
|
</Flex>
|
||||||
<InfoCollapse
|
)}
|
||||||
title='Audit Logs'
|
</ActionHandler>
|
||||||
icon={<AuditLogIcon />}
|
)
|
||||||
active={collapseState.auditLogs}
|
}}
|
||||||
onToggle={(expanded) =>
|
|
||||||
updateCollapseState('auditLogs', expanded)
|
|
||||||
}
|
|
||||||
key='auditLogs'
|
|
||||||
>
|
|
||||||
<AuditLogTable
|
|
||||||
items={objectData?.auditLogs || []}
|
|
||||||
loading={loading}
|
|
||||||
showTargetColumn={false}
|
|
||||||
/>
|
|
||||||
</InfoCollapse>
|
|
||||||
</Flex>
|
|
||||||
</div>
|
|
||||||
</Flex>
|
|
||||||
)}
|
|
||||||
</EditObjectForm>
|
</EditObjectForm>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@ -40,9 +40,9 @@ const Users = () => {
|
|||||||
</Dropdown>
|
</Dropdown>
|
||||||
<ColumnViewButton
|
<ColumnViewButton
|
||||||
type='user'
|
type='user'
|
||||||
loading={false}
|
disabled={false}
|
||||||
collapseState={columnVisibility}
|
visibleState={columnVisibility}
|
||||||
updateCollapseState={setColumnVisibility}
|
updateVisibleState={setColumnVisibility}
|
||||||
/>
|
/>
|
||||||
</Space>
|
</Space>
|
||||||
<Space>
|
<Space>
|
||||||
|
|||||||
@ -1,10 +1,8 @@
|
|||||||
import React from 'react'
|
import React from 'react'
|
||||||
import { useLocation } from 'react-router-dom'
|
import { useLocation } from 'react-router-dom'
|
||||||
import { Space, Button, Flex, Dropdown, Card } from 'antd'
|
import { Space, Flex, Card } from 'antd'
|
||||||
import { LoadingOutlined } from '@ant-design/icons'
|
import { LoadingOutlined } from '@ant-design/icons'
|
||||||
import ReloadIcon from '../../../Icons/ReloadIcon'
|
|
||||||
import useCollapseState from '../../hooks/useCollapseState'
|
import useCollapseState from '../../hooks/useCollapseState'
|
||||||
import AuditLogTable from '../../common/AuditLogTable'
|
|
||||||
import NotesPanel from '../../common/NotesPanel'
|
import NotesPanel from '../../common/NotesPanel'
|
||||||
import InfoCollapse from '../../common/InfoCollapse'
|
import InfoCollapse from '../../common/InfoCollapse'
|
||||||
import ObjectInfo from '../../common/ObjectInfo'
|
import ObjectInfo from '../../common/ObjectInfo'
|
||||||
@ -15,10 +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 {
|
import ActionHandler from '../../common/ActionHandler'
|
||||||
getModelProperties,
|
import ObjectActions from '../../common/ObjectActions.jsx'
|
||||||
getPropertyValue
|
import ObjectTable from '../../common/ObjectTable.jsx'
|
||||||
} from '../../../../database/ObjectModels.js'
|
import InfoCollapsePlaceholder from '../../common/InfoCollapsePlaceholder.jsx'
|
||||||
|
|
||||||
const UserInfo = () => {
|
const UserInfo = () => {
|
||||||
const location = useLocation()
|
const location = useLocation()
|
||||||
@ -46,112 +44,136 @@ const UserInfo = () => {
|
|||||||
editLoading,
|
editLoading,
|
||||||
lock,
|
lock,
|
||||||
fetchObject
|
fetchObject
|
||||||
}) => (
|
}) => {
|
||||||
<Flex
|
// Define actions for ActionHandler
|
||||||
gap='large'
|
const actions = {
|
||||||
vertical='true'
|
reload: () => {
|
||||||
style={{ height: '100%', minHeight: 0 }}
|
fetchObject()
|
||||||
>
|
return true
|
||||||
<Flex justify={'space-between'}>
|
},
|
||||||
<Space size='middle'>
|
edit: () => {
|
||||||
<Space size='small'>
|
startEditing()
|
||||||
<Dropdown
|
return false
|
||||||
menu={{
|
},
|
||||||
items: [
|
cancelEdit: () => {
|
||||||
{
|
cancelEditing()
|
||||||
label: 'Reload User',
|
return true
|
||||||
key: 'reload',
|
},
|
||||||
icon: <ReloadIcon />
|
finishEdit: () => {
|
||||||
|
handleUpdate()
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<ActionHandler actions={actions} loading={loading}>
|
||||||
|
{({ callAction }) => (
|
||||||
|
<Flex
|
||||||
|
gap='large'
|
||||||
|
vertical='true'
|
||||||
|
style={{ height: '100%', minHeight: 0 }}
|
||||||
|
>
|
||||||
|
<Flex justify={'space-between'}>
|
||||||
|
<Space size='middle'>
|
||||||
|
<Space size='small'>
|
||||||
|
<ObjectActions
|
||||||
|
type='user'
|
||||||
|
id={userId}
|
||||||
|
disabled={loading}
|
||||||
|
/>
|
||||||
|
<ViewButton
|
||||||
|
disabled={loading}
|
||||||
|
items={[
|
||||||
|
{ key: 'info', label: 'User Information' },
|
||||||
|
{ key: 'notes', label: 'Notes' },
|
||||||
|
{ key: 'auditLogs', label: 'Audit Logs' }
|
||||||
|
]}
|
||||||
|
visibleState={collapseState}
|
||||||
|
updateVisibleState={updateCollapseState}
|
||||||
|
/>
|
||||||
|
</Space>
|
||||||
|
<LockIndicator lock={lock} />
|
||||||
|
</Space>
|
||||||
|
<Space>
|
||||||
|
<EditButtons
|
||||||
|
isEditing={isEditing}
|
||||||
|
handleUpdate={() => {
|
||||||
|
callAction('finishEdit')
|
||||||
|
}}
|
||||||
|
cancelEditing={() => {
|
||||||
|
callAction('cancelEdit')
|
||||||
|
}}
|
||||||
|
startEditing={() => {
|
||||||
|
callAction('edit')
|
||||||
|
}}
|
||||||
|
editLoading={editLoading}
|
||||||
|
formValid={formValid}
|
||||||
|
disabled={lock?.locked || loading || true}
|
||||||
|
loading={editLoading}
|
||||||
|
/>
|
||||||
|
</Space>
|
||||||
|
</Flex>
|
||||||
|
|
||||||
|
<div style={{ height: '100%', overflow: 'auto' }}>
|
||||||
|
<Flex vertical gap={'large'}>
|
||||||
|
<InfoCollapse
|
||||||
|
title='User Information'
|
||||||
|
icon={<InfoCircleIcon />}
|
||||||
|
active={collapseState.info}
|
||||||
|
onToggle={(expanded) =>
|
||||||
|
updateCollapseState('info', expanded)
|
||||||
}
|
}
|
||||||
],
|
key='info'
|
||||||
onClick: ({ key }) => {
|
>
|
||||||
if (key === 'reload') {
|
<ObjectInfo
|
||||||
fetchObject()
|
loading={loading}
|
||||||
|
indicator={<LoadingOutlined />}
|
||||||
|
isEditing={isEditing}
|
||||||
|
type='user'
|
||||||
|
objectData={objectData}
|
||||||
|
/>
|
||||||
|
</InfoCollapse>
|
||||||
|
|
||||||
|
<InfoCollapse
|
||||||
|
title='Notes'
|
||||||
|
icon={<NoteIcon />}
|
||||||
|
active={collapseState.notes}
|
||||||
|
onToggle={(expanded) =>
|
||||||
|
updateCollapseState('notes', expanded)
|
||||||
}
|
}
|
||||||
}
|
key='notes'
|
||||||
}}
|
>
|
||||||
>
|
<Card>
|
||||||
<Button disabled={loading}>Actions</Button>
|
<NotesPanel _id={userId} />
|
||||||
</Dropdown>
|
</Card>
|
||||||
<ViewButton
|
</InfoCollapse>
|
||||||
loading={loading}
|
|
||||||
sections={[
|
|
||||||
{ key: 'info', label: 'User Information' },
|
|
||||||
{ key: 'notes', label: 'Notes' },
|
|
||||||
{ key: 'auditLogs', label: 'Audit Logs' }
|
|
||||||
]}
|
|
||||||
collapseState={collapseState}
|
|
||||||
updateCollapseState={updateCollapseState}
|
|
||||||
/>
|
|
||||||
</Space>
|
|
||||||
<LockIndicator lock={lock} />
|
|
||||||
</Space>
|
|
||||||
<Space>
|
|
||||||
<EditButtons
|
|
||||||
isEditing={isEditing}
|
|
||||||
handleUpdate={handleUpdate}
|
|
||||||
cancelEditing={cancelEditing}
|
|
||||||
startEditing={startEditing}
|
|
||||||
editLoading={editLoading}
|
|
||||||
formValid={formValid}
|
|
||||||
disabled={lock?.locked || loading || true}
|
|
||||||
loading={editLoading}
|
|
||||||
/>
|
|
||||||
</Space>
|
|
||||||
</Flex>
|
|
||||||
|
|
||||||
<div style={{ height: '100%', overflow: 'auto' }}>
|
<InfoCollapse
|
||||||
<Flex vertical gap={'large'}>
|
title='Audit Logs'
|
||||||
<InfoCollapse
|
icon={<AuditLogIcon />}
|
||||||
title='User Information'
|
active={collapseState.auditLogs}
|
||||||
icon={<InfoCircleIcon />}
|
onToggle={(expanded) =>
|
||||||
active={collapseState.info}
|
updateCollapseState('auditLogs', expanded)
|
||||||
onToggle={(expanded) => updateCollapseState('info', expanded)}
|
}
|
||||||
key='info'
|
key='auditLogs'
|
||||||
>
|
>
|
||||||
<ObjectInfo
|
{loading ? (
|
||||||
loading={loading}
|
<InfoCollapsePlaceholder />
|
||||||
indicator={<LoadingOutlined />}
|
) : (
|
||||||
isEditing={isEditing}
|
<ObjectTable
|
||||||
type='user'
|
type='auditLog'
|
||||||
items={getModelProperties('user').map((prop) => ({
|
masterFilter={{ 'parent._id': userId }}
|
||||||
...prop,
|
visibleColumns={{ _id: false, 'parent._id': false }}
|
||||||
value: getPropertyValue(objectData, prop.name)
|
/>
|
||||||
}))}
|
)}
|
||||||
/>
|
</InfoCollapse>
|
||||||
</InfoCollapse>
|
</Flex>
|
||||||
|
</div>
|
||||||
<InfoCollapse
|
</Flex>
|
||||||
title='Notes'
|
)}
|
||||||
icon={<NoteIcon />}
|
</ActionHandler>
|
||||||
active={collapseState.notes}
|
)
|
||||||
onToggle={(expanded) => updateCollapseState('notes', expanded)}
|
}}
|
||||||
key='notes'
|
|
||||||
>
|
|
||||||
<Card>
|
|
||||||
<NotesPanel _id={userId} />
|
|
||||||
</Card>
|
|
||||||
</InfoCollapse>
|
|
||||||
|
|
||||||
<InfoCollapse
|
|
||||||
title='Audit Logs'
|
|
||||||
icon={<AuditLogIcon />}
|
|
||||||
active={collapseState.auditLogs}
|
|
||||||
onToggle={(expanded) =>
|
|
||||||
updateCollapseState('auditLogs', expanded)
|
|
||||||
}
|
|
||||||
key='auditLogs'
|
|
||||||
>
|
|
||||||
<AuditLogTable
|
|
||||||
items={objectData?.auditLogs || []}
|
|
||||||
loading={loading}
|
|
||||||
showTargetColumn={false}
|
|
||||||
/>
|
|
||||||
</InfoCollapse>
|
|
||||||
</Flex>
|
|
||||||
</div>
|
|
||||||
</Flex>
|
|
||||||
)}
|
|
||||||
</EditObjectForm>
|
</EditObjectForm>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,12 +1,9 @@
|
|||||||
import React from 'react'
|
import React from 'react'
|
||||||
import { useLocation } from 'react-router-dom'
|
import { useLocation } from 'react-router-dom'
|
||||||
import { Space, Button, Flex, Dropdown, Card } from 'antd'
|
import { Space, Flex, Card } from 'antd'
|
||||||
import { LoadingOutlined } from '@ant-design/icons'
|
|
||||||
import loglevel from 'loglevel'
|
import loglevel from 'loglevel'
|
||||||
import config from '../../../../config'
|
import config from '../../../../config'
|
||||||
import ReloadIcon from '../../../Icons/ReloadIcon'
|
|
||||||
import useCollapseState from '../../hooks/useCollapseState'
|
import useCollapseState from '../../hooks/useCollapseState'
|
||||||
import AuditLogTable from '../../common/AuditLogTable'
|
|
||||||
import NotesPanel from '../../common/NotesPanel'
|
import NotesPanel from '../../common/NotesPanel'
|
||||||
import InfoCollapse from '../../common/InfoCollapse'
|
import InfoCollapse from '../../common/InfoCollapse'
|
||||||
import ObjectInfo from '../../common/ObjectInfo'
|
import ObjectInfo from '../../common/ObjectInfo'
|
||||||
@ -17,10 +14,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 {
|
import ActionHandler from '../../common/ActionHandler.jsx'
|
||||||
getModelProperties,
|
import ObjectActions from '../../common/ObjectActions.jsx'
|
||||||
getPropertyValue
|
import ObjectTable from '../../common/ObjectTable.jsx'
|
||||||
} from '../../../../database/ObjectModels'
|
import InfoCollapsePlaceholder from '../../common/InfoCollapsePlaceholder.jsx'
|
||||||
|
|
||||||
const log = loglevel.getLogger('VendorInfo')
|
const log = loglevel.getLogger('VendorInfo')
|
||||||
log.setLevel(config.logLevel)
|
log.setLevel(config.logLevel)
|
||||||
@ -51,111 +48,135 @@ const VendorInfo = () => {
|
|||||||
editLoading,
|
editLoading,
|
||||||
lock,
|
lock,
|
||||||
fetchObject
|
fetchObject
|
||||||
}) => (
|
}) => {
|
||||||
<Flex
|
// Define actions for ActionHandler
|
||||||
gap='large'
|
const actions = {
|
||||||
vertical='true'
|
reload: () => {
|
||||||
style={{ height: '100%', minHeight: 0 }}
|
fetchObject()
|
||||||
>
|
return true
|
||||||
<Flex justify={'space-between'}>
|
},
|
||||||
<Space size='middle'>
|
edit: () => {
|
||||||
<Space size='small'>
|
startEditing()
|
||||||
<Dropdown
|
return false
|
||||||
menu={{
|
},
|
||||||
items: [
|
cancelEdit: () => {
|
||||||
{
|
cancelEditing()
|
||||||
label: 'Reload Vendor',
|
return true
|
||||||
key: 'reload',
|
},
|
||||||
icon: <ReloadIcon />
|
finishEdit: () => {
|
||||||
|
handleUpdate()
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<ActionHandler actions={actions} loading={loading}>
|
||||||
|
{({ callAction }) => (
|
||||||
|
<Flex
|
||||||
|
gap='large'
|
||||||
|
vertical='true'
|
||||||
|
style={{ height: '100%', minHeight: 0 }}
|
||||||
|
>
|
||||||
|
<Flex justify={'space-between'}>
|
||||||
|
<Space size='middle'>
|
||||||
|
<Space size='small'>
|
||||||
|
<ObjectActions
|
||||||
|
type='vendor'
|
||||||
|
id={vendorId}
|
||||||
|
disabled={loading}
|
||||||
|
/>
|
||||||
|
<ViewButton
|
||||||
|
disabled={loading}
|
||||||
|
items={[
|
||||||
|
{ key: 'info', label: 'Vendor Information' },
|
||||||
|
{ key: 'notes', label: 'Notes' },
|
||||||
|
{ key: 'auditLogs', label: 'Audit Logs' }
|
||||||
|
]}
|
||||||
|
visibleState={collapseState}
|
||||||
|
updateVisibleState={updateCollapseState}
|
||||||
|
/>
|
||||||
|
</Space>
|
||||||
|
<LockIndicator lock={lock} />
|
||||||
|
</Space>
|
||||||
|
<Space>
|
||||||
|
<EditButtons
|
||||||
|
isEditing={isEditing}
|
||||||
|
handleUpdate={() => {
|
||||||
|
callAction('finishEdit')
|
||||||
|
}}
|
||||||
|
cancelEditing={() => {
|
||||||
|
callAction('cancelEdit')
|
||||||
|
}}
|
||||||
|
startEditing={() => {
|
||||||
|
callAction('edit')
|
||||||
|
}}
|
||||||
|
editLoading={editLoading}
|
||||||
|
formValid={formValid}
|
||||||
|
disabled={lock?.locked || loading}
|
||||||
|
loading={editLoading}
|
||||||
|
/>
|
||||||
|
</Space>
|
||||||
|
</Flex>
|
||||||
|
|
||||||
|
<div style={{ height: '100%', overflow: 'auto' }}>
|
||||||
|
<Flex vertical gap={'large'}>
|
||||||
|
<InfoCollapse
|
||||||
|
title='Vendor Information'
|
||||||
|
icon={<InfoCircleIcon />}
|
||||||
|
active={collapseState.info}
|
||||||
|
onToggle={(expanded) =>
|
||||||
|
updateCollapseState('info', expanded)
|
||||||
}
|
}
|
||||||
],
|
key='info'
|
||||||
onClick: ({ key }) => {
|
>
|
||||||
if (key === 'reload') {
|
<ObjectInfo
|
||||||
fetchObject()
|
loading={loading}
|
||||||
|
isEditing={isEditing}
|
||||||
|
type='vendor'
|
||||||
|
objectData={objectData}
|
||||||
|
/>
|
||||||
|
</InfoCollapse>
|
||||||
|
|
||||||
|
<InfoCollapse
|
||||||
|
title='Notes'
|
||||||
|
icon={<NoteIcon />}
|
||||||
|
active={collapseState.notes}
|
||||||
|
onToggle={(expanded) =>
|
||||||
|
updateCollapseState('notes', expanded)
|
||||||
}
|
}
|
||||||
}
|
key='notes'
|
||||||
}}
|
>
|
||||||
>
|
<Card>
|
||||||
<Button disabled={loading}>Actions</Button>
|
<NotesPanel _id={vendorId} />
|
||||||
</Dropdown>
|
</Card>
|
||||||
<ViewButton
|
</InfoCollapse>
|
||||||
loading={loading}
|
|
||||||
sections={[
|
|
||||||
{ key: 'info', label: 'Vendor Information' },
|
|
||||||
{ key: 'notes', label: 'Notes' },
|
|
||||||
{ key: 'auditLogs', label: 'Audit Logs' }
|
|
||||||
]}
|
|
||||||
collapseState={collapseState}
|
|
||||||
updateCollapseState={updateCollapseState}
|
|
||||||
/>
|
|
||||||
</Space>
|
|
||||||
<LockIndicator lock={lock} />
|
|
||||||
</Space>
|
|
||||||
<Space>
|
|
||||||
<EditButtons
|
|
||||||
isEditing={isEditing}
|
|
||||||
handleUpdate={handleUpdate}
|
|
||||||
cancelEditing={cancelEditing}
|
|
||||||
startEditing={startEditing}
|
|
||||||
editLoading={editLoading}
|
|
||||||
formValid={formValid}
|
|
||||||
disabled={lock?.locked || loading}
|
|
||||||
loading={editLoading}
|
|
||||||
/>
|
|
||||||
</Space>
|
|
||||||
</Flex>
|
|
||||||
|
|
||||||
<div style={{ height: '100%', overflow: 'auto' }}>
|
<InfoCollapse
|
||||||
<Flex vertical gap={'large'}>
|
title='Audit Logs'
|
||||||
<InfoCollapse
|
icon={<AuditLogIcon />}
|
||||||
title='Vendor Information'
|
active={collapseState.auditLogs}
|
||||||
icon={<InfoCircleIcon />}
|
onToggle={(expanded) =>
|
||||||
active={collapseState.info}
|
updateCollapseState('auditLogs', expanded)
|
||||||
onToggle={(expanded) => updateCollapseState('info', expanded)}
|
}
|
||||||
key='info'
|
key='auditLogs'
|
||||||
>
|
>
|
||||||
<ObjectInfo
|
{loading ? (
|
||||||
loading={loading}
|
<InfoCollapsePlaceholder />
|
||||||
indicator={<LoadingOutlined />}
|
) : (
|
||||||
isEditing={isEditing}
|
<ObjectTable
|
||||||
items={getModelProperties('vendor').map((prop) => ({
|
type='auditLog'
|
||||||
...prop,
|
masterFilter={{ 'parent._id': vendorId }}
|
||||||
value: getPropertyValue(objectData, prop.name)
|
visibleColumns={{ _id: false, 'parent._id': false }}
|
||||||
}))}
|
/>
|
||||||
/>
|
)}
|
||||||
</InfoCollapse>
|
</InfoCollapse>
|
||||||
|
</Flex>
|
||||||
<InfoCollapse
|
</div>
|
||||||
title='Notes'
|
</Flex>
|
||||||
icon={<NoteIcon />}
|
)}
|
||||||
active={collapseState.notes}
|
</ActionHandler>
|
||||||
onToggle={(expanded) => updateCollapseState('notes', expanded)}
|
)
|
||||||
key='notes'
|
}}
|
||||||
>
|
|
||||||
<Card>
|
|
||||||
<NotesPanel _id={vendorId} />
|
|
||||||
</Card>
|
|
||||||
</InfoCollapse>
|
|
||||||
|
|
||||||
<InfoCollapse
|
|
||||||
title='Audit Logs'
|
|
||||||
icon={<AuditLogIcon />}
|
|
||||||
active={collapseState.auditLogs}
|
|
||||||
onToggle={(expanded) =>
|
|
||||||
updateCollapseState('auditLogs', expanded)
|
|
||||||
}
|
|
||||||
key='auditLogs'
|
|
||||||
>
|
|
||||||
<AuditLogTable
|
|
||||||
items={objectData?.auditLogs || []}
|
|
||||||
loading={loading}
|
|
||||||
showTargetColumn={false}
|
|
||||||
/>
|
|
||||||
</InfoCollapse>
|
|
||||||
</Flex>
|
|
||||||
</div>
|
|
||||||
</Flex>
|
|
||||||
)}
|
|
||||||
</EditObjectForm>
|
</EditObjectForm>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,8 +1,7 @@
|
|||||||
// src/gcodefiles.js
|
// src/gcodefiles.js
|
||||||
|
|
||||||
import React, { useState, useContext, useRef } from 'react'
|
import React, { useState, useRef } from 'react'
|
||||||
import { Button, Flex, Space, Modal, Dropdown, message } from 'antd'
|
import { Button, Flex, Space, Modal, Dropdown, message } from 'antd'
|
||||||
import { AuthContext } from '../context/AuthContext'
|
|
||||||
import NewGCodeFile from './GCodeFiles/NewGCodeFile'
|
import NewGCodeFile from './GCodeFiles/NewGCodeFile'
|
||||||
import useColumnVisibility from '../hooks/useColumnVisibility'
|
import useColumnVisibility from '../hooks/useColumnVisibility'
|
||||||
import PlusIcon from '../../Icons/PlusIcon'
|
import PlusIcon from '../../Icons/PlusIcon'
|
||||||
@ -23,8 +22,6 @@ const GCodeFiles = () => {
|
|||||||
const [columnVisibility, setColumnVisibility] =
|
const [columnVisibility, setColumnVisibility] =
|
||||||
useColumnVisibility('gcodeFile')
|
useColumnVisibility('gcodeFile')
|
||||||
|
|
||||||
const { authenticated } = useContext(AuthContext)
|
|
||||||
|
|
||||||
const actionItems = {
|
const actionItems = {
|
||||||
items: [
|
items: [
|
||||||
{
|
{
|
||||||
@ -59,9 +56,9 @@ const GCodeFiles = () => {
|
|||||||
</Dropdown>
|
</Dropdown>
|
||||||
<ColumnViewButton
|
<ColumnViewButton
|
||||||
type='gcodeFile'
|
type='gcodeFile'
|
||||||
loading={false}
|
disabled={false}
|
||||||
collapseState={columnVisibility}
|
visibleState={columnVisibility}
|
||||||
updateCollapseState={setColumnVisibility}
|
updateVisibleState={setColumnVisibility}
|
||||||
/>
|
/>
|
||||||
</Space>
|
</Space>
|
||||||
<Space>
|
<Space>
|
||||||
@ -75,8 +72,7 @@ const GCodeFiles = () => {
|
|||||||
</Flex>
|
</Flex>
|
||||||
<ObjectTable
|
<ObjectTable
|
||||||
ref={tableRef}
|
ref={tableRef}
|
||||||
type={'gcodeFile'}
|
type='gcodeFile'
|
||||||
authenticated={authenticated}
|
|
||||||
cards={viewMode === 'cards'}
|
cards={viewMode === 'cards'}
|
||||||
visibleColumns={columnVisibility}
|
visibleColumns={columnVisibility}
|
||||||
/>
|
/>
|
||||||
|
|||||||
@ -1,10 +1,8 @@
|
|||||||
import React, { useContext } 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, Flex, Card, Typography } from 'antd'
|
||||||
import { LoadingOutlined } from '@ant-design/icons'
|
import { LoadingOutlined } from '@ant-design/icons'
|
||||||
import ReloadIcon from '../../../Icons/ReloadIcon'
|
|
||||||
import useCollapseState from '../../hooks/useCollapseState'
|
import useCollapseState from '../../hooks/useCollapseState'
|
||||||
import AuditLogTable from '../../common/AuditLogTable'
|
|
||||||
import NotesPanel from '../../common/NotesPanel'
|
import NotesPanel from '../../common/NotesPanel'
|
||||||
import InfoCollapse from '../../common/InfoCollapse'
|
import InfoCollapse from '../../common/InfoCollapse'
|
||||||
import ObjectInfo from '../../common/ObjectInfo'
|
import ObjectInfo from '../../common/ObjectInfo'
|
||||||
@ -18,10 +16,9 @@ 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 { ApiServerContext } from '../../context/ApiServerContext'
|
||||||
import {
|
import ObjectActions from '../../common/ObjectActions.jsx'
|
||||||
getModelProperties,
|
import ObjectTable from '../../common/ObjectTable.jsx'
|
||||||
getPropertyValue
|
import InfoCollapsePlaceholder from '../../common/InfoCollapsePlaceholder.jsx'
|
||||||
} from '../../../../database/ObjectModels.js'
|
|
||||||
|
|
||||||
const { Text } = Typography
|
const { Text } = Typography
|
||||||
|
|
||||||
@ -31,7 +28,7 @@ const GCodeFileInfo = () => {
|
|||||||
|
|
||||||
const { handleDownloadContent } = useContext(ApiServerContext)
|
const { handleDownloadContent } = useContext(ApiServerContext)
|
||||||
const [collapseState, updateCollapseState] = useCollapseState(
|
const [collapseState, updateCollapseState] = useCollapseState(
|
||||||
'GCodeFileInfo',
|
'gcodeFileInfo',
|
||||||
{
|
{
|
||||||
info: true,
|
info: true,
|
||||||
preview: true,
|
preview: true,
|
||||||
@ -40,183 +37,187 @@ const GCodeFileInfo = () => {
|
|||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
// Define actions that can be triggered via URL
|
|
||||||
const actions = {
|
|
||||||
download: () => {
|
|
||||||
if (gcodeFileId) {
|
|
||||||
handleDownloadContent(
|
|
||||||
gcodeFileId,
|
|
||||||
'gcodeFile',
|
|
||||||
`gcodeFile-${gcodeFileId}.gcode`
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<EditObjectForm
|
||||||
<ActionHandler actions={actions} />
|
id={gcodeFileId}
|
||||||
<EditObjectForm
|
type='gcodefile'
|
||||||
id={gcodeFileId}
|
style={{ height: 'calc(var(--unit-100vh) - 155px)', minHeight: 0 }}
|
||||||
type='gcodefile'
|
>
|
||||||
style={{ height: 'calc(var(--unit-100vh) - 155px)', minHeight: 0 }}
|
{({
|
||||||
>
|
loading,
|
||||||
{({
|
isEditing,
|
||||||
loading,
|
startEditing,
|
||||||
isEditing,
|
cancelEditing,
|
||||||
startEditing,
|
handleUpdate,
|
||||||
cancelEditing,
|
formValid,
|
||||||
handleUpdate,
|
objectData,
|
||||||
formValid,
|
editLoading,
|
||||||
objectData,
|
lock,
|
||||||
editLoading,
|
fetchObject
|
||||||
lock,
|
}) => {
|
||||||
fetchObject
|
// Define actions that can be triggered via URL, now with access to startEditing
|
||||||
}) => (
|
const actions = {
|
||||||
<Flex
|
reload: () => {
|
||||||
gap='large'
|
fetchObject()
|
||||||
vertical='true'
|
return true
|
||||||
style={{ height: '100%', minHeight: 0 }}
|
},
|
||||||
>
|
edit: () => {
|
||||||
<Flex justify={'space-between'}>
|
startEditing()
|
||||||
<Space size='middle'>
|
return false
|
||||||
<Space size='small'>
|
},
|
||||||
<Dropdown
|
cancelEdit: () => {
|
||||||
menu={{
|
cancelEditing()
|
||||||
items: [
|
return true
|
||||||
{
|
},
|
||||||
label: 'Reload GCode File',
|
finishEdit: () => {
|
||||||
key: 'reload',
|
handleUpdate()
|
||||||
icon: <ReloadIcon />
|
return true
|
||||||
},
|
},
|
||||||
{
|
download: () => {
|
||||||
label: 'Download GCode File',
|
if (gcodeFileId) {
|
||||||
key: 'download',
|
handleDownloadContent(
|
||||||
icon: <GCodeFileIcon />
|
gcodeFileId,
|
||||||
}
|
'gcodeFile',
|
||||||
],
|
`${objectData.name}.gcode`
|
||||||
onClick: ({ key }) => {
|
)
|
||||||
if (key === 'reload') {
|
return true
|
||||||
fetchObject()
|
}
|
||||||
} else if (key === 'download' && gcodeFileId) {
|
}
|
||||||
handleDownloadContent(
|
}
|
||||||
gcodeFileId,
|
|
||||||
'gcodefile',
|
|
||||||
`gcodefile-${gcodeFileId}.gcode`
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<Button disabled={loading}>Actions</Button>
|
|
||||||
</Dropdown>
|
|
||||||
<ViewButton
|
|
||||||
loading={loading}
|
|
||||||
sections={[
|
|
||||||
{ key: 'info', label: 'GCode File Information' },
|
|
||||||
{ key: 'preview', label: 'GCode File Preview' },
|
|
||||||
{ key: 'notes', label: 'Notes' },
|
|
||||||
{ key: 'auditLogs', label: 'Audit Logs' }
|
|
||||||
]}
|
|
||||||
collapseState={collapseState}
|
|
||||||
updateCollapseState={updateCollapseState}
|
|
||||||
/>
|
|
||||||
</Space>
|
|
||||||
<LockIndicator lock={lock} />
|
|
||||||
</Space>
|
|
||||||
<Space>
|
|
||||||
<EditButtons
|
|
||||||
isEditing={isEditing}
|
|
||||||
handleUpdate={handleUpdate}
|
|
||||||
cancelEditing={cancelEditing}
|
|
||||||
startEditing={startEditing}
|
|
||||||
editLoading={editLoading}
|
|
||||||
formValid={formValid}
|
|
||||||
disabled={lock?.locked || loading}
|
|
||||||
loading={editLoading}
|
|
||||||
/>
|
|
||||||
</Space>
|
|
||||||
</Flex>
|
|
||||||
|
|
||||||
<div style={{ height: '100%', overflow: 'auto' }}>
|
return (
|
||||||
<Flex vertical gap={'large'}>
|
<ActionHandler actions={actions} loading={loading}>
|
||||||
<InfoCollapse
|
{({ callAction }) => (
|
||||||
title='GCode File Information'
|
<Flex
|
||||||
icon={<InfoCircleIcon />}
|
gap='large'
|
||||||
active={collapseState.info}
|
vertical='true'
|
||||||
onToggle={(expanded) => updateCollapseState('info', expanded)}
|
style={{ height: '100%', minHeight: 0 }}
|
||||||
key='info'
|
>
|
||||||
>
|
<Flex justify={'space-between'}>
|
||||||
<ObjectInfo
|
<Space size='middle'>
|
||||||
loading={loading}
|
<Space size='small'>
|
||||||
indicator={<LoadingOutlined />}
|
<ObjectActions
|
||||||
isEditing={isEditing}
|
type='gcodeFile'
|
||||||
items={getModelProperties('gcodeFile').map((prop) => ({
|
id={gcodeFileId}
|
||||||
...prop,
|
disabled={loading}
|
||||||
value: getPropertyValue(objectData, prop.name)
|
|
||||||
}))}
|
|
||||||
objectData={objectData}
|
|
||||||
type='gcodefile'
|
|
||||||
/>
|
|
||||||
</InfoCollapse>
|
|
||||||
|
|
||||||
<InfoCollapse
|
|
||||||
title='GCode File Preview'
|
|
||||||
icon={<GCodeFileIcon />}
|
|
||||||
active={collapseState.preview}
|
|
||||||
onToggle={(expanded) =>
|
|
||||||
updateCollapseState('preview', expanded)
|
|
||||||
}
|
|
||||||
key='preview'
|
|
||||||
>
|
|
||||||
<Card>
|
|
||||||
{objectData?.gcodeFileInfo?.thumbnail ? (
|
|
||||||
<img
|
|
||||||
src={`data:image/png;base64,${objectData.gcodeFileInfo.thumbnail.data}`}
|
|
||||||
alt='GCodeFile'
|
|
||||||
style={{ maxWidth: '100%' }}
|
|
||||||
/>
|
/>
|
||||||
) : (
|
<ViewButton
|
||||||
<Text>n/a</Text>
|
disabled={loading}
|
||||||
)}
|
items={[
|
||||||
</Card>
|
{ key: 'info', label: 'GCode File Information' },
|
||||||
</InfoCollapse>
|
{ key: 'preview', label: 'GCode File Preview' },
|
||||||
|
{ key: 'notes', label: 'Notes' },
|
||||||
|
{ key: 'auditLogs', label: 'Audit Logs' }
|
||||||
|
]}
|
||||||
|
visibleState={collapseState}
|
||||||
|
updateVisibleState={updateCollapseState}
|
||||||
|
/>
|
||||||
|
</Space>
|
||||||
|
<LockIndicator lock={lock} />
|
||||||
|
</Space>
|
||||||
|
<Space>
|
||||||
|
<EditButtons
|
||||||
|
isEditing={isEditing}
|
||||||
|
handleUpdate={() => {
|
||||||
|
callAction('finishEdit')
|
||||||
|
}}
|
||||||
|
cancelEditing={() => {
|
||||||
|
callAction('cancelEdit')
|
||||||
|
}}
|
||||||
|
startEditing={() => {
|
||||||
|
callAction('edit')
|
||||||
|
}}
|
||||||
|
editLoading={editLoading}
|
||||||
|
formValid={formValid}
|
||||||
|
disabled={lock?.locked || loading}
|
||||||
|
loading={editLoading}
|
||||||
|
/>
|
||||||
|
</Space>
|
||||||
|
</Flex>
|
||||||
|
|
||||||
<InfoCollapse
|
<div style={{ height: '100%', overflow: 'auto' }}>
|
||||||
title='Notes'
|
<Flex vertical gap={'large'}>
|
||||||
icon={<NoteIcon />}
|
<InfoCollapse
|
||||||
active={collapseState.notes}
|
title='GCode File Information'
|
||||||
onToggle={(expanded) =>
|
icon={<InfoCircleIcon />}
|
||||||
updateCollapseState('notes', expanded)
|
active={collapseState.info}
|
||||||
}
|
onToggle={(expanded) =>
|
||||||
key='notes'
|
updateCollapseState('info', expanded)
|
||||||
>
|
}
|
||||||
<Card>
|
key='info'
|
||||||
<NotesPanel _id={gcodeFileId} />
|
>
|
||||||
</Card>
|
<ObjectInfo
|
||||||
</InfoCollapse>
|
loading={loading}
|
||||||
|
indicator={<LoadingOutlined />}
|
||||||
|
isEditing={isEditing}
|
||||||
|
objectData={objectData}
|
||||||
|
type='gcodeFile'
|
||||||
|
/>
|
||||||
|
</InfoCollapse>
|
||||||
|
|
||||||
<InfoCollapse
|
<InfoCollapse
|
||||||
title='Audit Logs'
|
title='GCode File Preview'
|
||||||
icon={<AuditLogIcon />}
|
icon={<GCodeFileIcon />}
|
||||||
active={collapseState.auditLogs}
|
active={collapseState.preview}
|
||||||
onToggle={(expanded) =>
|
onToggle={(expanded) =>
|
||||||
updateCollapseState('auditLogs', expanded)
|
updateCollapseState('preview', expanded)
|
||||||
}
|
}
|
||||||
key='auditLogs'
|
key='preview'
|
||||||
>
|
>
|
||||||
<AuditLogTable
|
<Card>
|
||||||
items={objectData?.auditLogs || []}
|
{objectData?.gcodeFileInfo?.thumbnail ? (
|
||||||
loading={loading}
|
<img
|
||||||
showTargetColumn={false}
|
src={`data:image/png;base64,${objectData.gcodeFileInfo.thumbnail.data}`}
|
||||||
/>
|
alt='GCodeFile'
|
||||||
</InfoCollapse>
|
style={{ maxWidth: '100%' }}
|
||||||
|
/>
|
||||||
|
) : (
|
||||||
|
<Text>n/a</Text>
|
||||||
|
)}
|
||||||
|
</Card>
|
||||||
|
</InfoCollapse>
|
||||||
|
|
||||||
|
<InfoCollapse
|
||||||
|
title='Notes'
|
||||||
|
icon={<NoteIcon />}
|
||||||
|
active={collapseState.notes}
|
||||||
|
onToggle={(expanded) =>
|
||||||
|
updateCollapseState('notes', expanded)
|
||||||
|
}
|
||||||
|
key='notes'
|
||||||
|
>
|
||||||
|
<Card>
|
||||||
|
<NotesPanel _id={gcodeFileId} />
|
||||||
|
</Card>
|
||||||
|
</InfoCollapse>
|
||||||
|
|
||||||
|
<InfoCollapse
|
||||||
|
title='Audit Logs'
|
||||||
|
icon={<AuditLogIcon />}
|
||||||
|
active={collapseState.auditLogs}
|
||||||
|
onToggle={(expanded) =>
|
||||||
|
updateCollapseState('auditLogs', expanded)
|
||||||
|
}
|
||||||
|
key='auditLogs'
|
||||||
|
>
|
||||||
|
{loading ? (
|
||||||
|
<InfoCollapsePlaceholder />
|
||||||
|
) : (
|
||||||
|
<ObjectTable
|
||||||
|
type='auditLog'
|
||||||
|
masterFilter={{ 'parent._id': gcodeFileId }}
|
||||||
|
visibleColumns={{ _id: false, 'parent._id': false }}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</InfoCollapse>
|
||||||
|
</Flex>
|
||||||
|
</div>
|
||||||
</Flex>
|
</Flex>
|
||||||
</div>
|
)}
|
||||||
</Flex>
|
</ActionHandler>
|
||||||
)}
|
)
|
||||||
</EditObjectForm>
|
}}
|
||||||
</>
|
</EditObjectForm>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -1,313 +1,27 @@
|
|||||||
// src/Jobs.js
|
// src/Jobs.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 {
|
|
||||||
Button,
|
|
||||||
Flex,
|
|
||||||
Space,
|
|
||||||
Modal,
|
|
||||||
Dropdown,
|
|
||||||
message,
|
|
||||||
notification,
|
|
||||||
Input,
|
|
||||||
Typography,
|
|
||||||
Checkbox,
|
|
||||||
Popover
|
|
||||||
} from 'antd'
|
|
||||||
|
|
||||||
import { AuthContext } from '../context/AuthContext.js'
|
import { AuthContext } from '../context/AuthContext.js'
|
||||||
import { PrintServerContext } from '../context/PrintServerContext.js'
|
|
||||||
import NewJob from './Jobs/NewJob.jsx'
|
import NewJob from './Jobs/NewJob.jsx'
|
||||||
import JobState from '../common/JobState.jsx'
|
|
||||||
import SubJobCounter from '../common/SubJobCounter.jsx'
|
|
||||||
import TimeDisplay from '../common/TimeDisplay.jsx'
|
|
||||||
import IdDisplay from '../common/IdDisplay.jsx'
|
|
||||||
import useColumnVisibility from '../hooks/useColumnVisibility.js'
|
import useColumnVisibility from '../hooks/useColumnVisibility.js'
|
||||||
import JobIcon from '../../Icons/JobIcon.jsx'
|
|
||||||
import InfoCircleIcon from '../../Icons/InfoCircleIcon.jsx'
|
|
||||||
import PlusIcon from '../../Icons/PlusIcon.jsx'
|
import PlusIcon from '../../Icons/PlusIcon.jsx'
|
||||||
import ReloadIcon from '../../Icons/ReloadIcon.jsx'
|
import ReloadIcon from '../../Icons/ReloadIcon.jsx'
|
||||||
import EditIcon from '../../Icons/EditIcon.jsx'
|
|
||||||
import XMarkIcon from '../../Icons/XMarkIcon.jsx'
|
|
||||||
import CheckIcon from '../../Icons/CheckIcon.jsx'
|
|
||||||
import PlayCircleIcon from '../../Icons/PlayCircleIcon.jsx'
|
|
||||||
import CheckCircleIcon from '../../Icons/CheckCircleIcon.jsx'
|
|
||||||
import PauseCircleIcon from '../../Icons/PauseCircleIcon.jsx'
|
|
||||||
import XMarkCircleIcon from '../../Icons/XMarkCircleIcon.jsx'
|
|
||||||
import QuestionCircleIcon from '../../Icons/QuestionCircleIcon.jsx'
|
|
||||||
import ObjectTable from '../common/ObjectTable.jsx'
|
import ObjectTable from '../common/ObjectTable.jsx'
|
||||||
import ListIcon from '../../Icons/ListIcon.jsx'
|
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 ColumnViewButton from '../common/ColumnViewButton.jsx'
|
||||||
const { Text } = Typography
|
|
||||||
|
|
||||||
const Jobs = () => {
|
const Jobs = () => {
|
||||||
const [messageApi, contextHolder] = message.useMessage()
|
const [messageApi, contextHolder] = message.useMessage()
|
||||||
const [notificationApi, notificationContextHolder] =
|
|
||||||
notification.useNotification()
|
|
||||||
const navigate = useNavigate()
|
|
||||||
const [newJobOpen, setNewJobOpen] = useState(false)
|
const [newJobOpen, setNewJobOpen] = useState(false)
|
||||||
const tableRef = useRef()
|
const tableRef = useRef()
|
||||||
const [viewMode, setViewMode] = useViewMode('Jobs')
|
const [viewMode, setViewMode] = useViewMode('job')
|
||||||
|
|
||||||
const getFilterDropdown = ({
|
|
||||||
setSelectedKeys,
|
|
||||||
selectedKeys,
|
|
||||||
confirm,
|
|
||||||
clearFilters,
|
|
||||||
propertyName
|
|
||||||
}) => {
|
|
||||||
return (
|
|
||||||
<div style={{ padding: 8 }}>
|
|
||||||
<Space.Compact>
|
|
||||||
<Input
|
|
||||||
placeholder={'Search ' + propertyName}
|
|
||||||
value={selectedKeys[0]}
|
|
||||||
onChange={(e) =>
|
|
||||||
setSelectedKeys(e.target.value ? [e.target.value] : [])
|
|
||||||
}
|
|
||||||
onPressEnter={() => confirm()}
|
|
||||||
style={{ width: 200, display: 'block' }}
|
|
||||||
/>
|
|
||||||
<Button
|
|
||||||
onClick={() => {
|
|
||||||
clearFilters()
|
|
||||||
confirm()
|
|
||||||
}}
|
|
||||||
icon={<XMarkIcon />}
|
|
||||||
/>
|
|
||||||
<Button
|
|
||||||
type='primary'
|
|
||||||
onClick={() => confirm()}
|
|
||||||
icon={<CheckIcon />}
|
|
||||||
/>
|
|
||||||
</Space.Compact>
|
|
||||||
</div>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Column definitions
|
|
||||||
const columns = [
|
|
||||||
{
|
|
||||||
title: <JobIcon />,
|
|
||||||
key: 'icon',
|
|
||||||
width: 40,
|
|
||||||
fixed: 'left',
|
|
||||||
render: () => <JobIcon />
|
|
||||||
},
|
|
||||||
{
|
|
||||||
title: 'GCode File Name',
|
|
||||||
key: 'gcodeFileName',
|
|
||||||
width: 200,
|
|
||||||
fixed: 'left',
|
|
||||||
render: (record) => <Text ellipsis>{record?.gcodeFile?.name}</Text>,
|
|
||||||
filterDropdown: ({
|
|
||||||
setSelectedKeys,
|
|
||||||
selectedKeys,
|
|
||||||
confirm,
|
|
||||||
clearFilters
|
|
||||||
}) =>
|
|
||||||
getFilterDropdown({
|
|
||||||
setSelectedKeys,
|
|
||||||
selectedKeys,
|
|
||||||
confirm,
|
|
||||||
clearFilters,
|
|
||||||
propertyName: 'GCode file name'
|
|
||||||
}),
|
|
||||||
onFilter: (value, record) =>
|
|
||||||
record.gcodeFile.name.toLowerCase().includes(value.toLowerCase())
|
|
||||||
},
|
|
||||||
{
|
|
||||||
title: 'ID',
|
|
||||||
dataIndex: 'id',
|
|
||||||
key: 'id',
|
|
||||||
width: 180,
|
|
||||||
render: (text) => <IdDisplay id={text} type={'job'} longId={false} />,
|
|
||||||
filterDropdown: ({
|
|
||||||
setSelectedKeys,
|
|
||||||
selectedKeys,
|
|
||||||
confirm,
|
|
||||||
clearFilters
|
|
||||||
}) =>
|
|
||||||
getFilterDropdown({
|
|
||||||
setSelectedKeys,
|
|
||||||
selectedKeys,
|
|
||||||
confirm,
|
|
||||||
clearFilters,
|
|
||||||
propertyName: 'ID'
|
|
||||||
}),
|
|
||||||
onFilter: (value, record) =>
|
|
||||||
record.id.toLowerCase().includes(value.toLowerCase())
|
|
||||||
},
|
|
||||||
{
|
|
||||||
title: 'State',
|
|
||||||
key: 'state',
|
|
||||||
dataIndex: 'state',
|
|
||||||
width: 240,
|
|
||||||
render: (state) => {
|
|
||||||
return <JobState state={state} showQuantity={false} showId={false} />
|
|
||||||
},
|
|
||||||
filterDropdown: ({
|
|
||||||
setSelectedKeys,
|
|
||||||
selectedKeys,
|
|
||||||
confirm,
|
|
||||||
clearFilters
|
|
||||||
}) =>
|
|
||||||
getFilterDropdown({
|
|
||||||
setSelectedKeys,
|
|
||||||
selectedKeys,
|
|
||||||
confirm,
|
|
||||||
clearFilters,
|
|
||||||
propertyName: 'state'
|
|
||||||
}),
|
|
||||||
onFilter: (value, record) =>
|
|
||||||
record?.state?.type?.toLowerCase().includes(value.toLowerCase())
|
|
||||||
},
|
|
||||||
{
|
|
||||||
title: <CheckCircleIcon />,
|
|
||||||
key: 'complete',
|
|
||||||
width: 70,
|
|
||||||
render: (record) => {
|
|
||||||
return <SubJobCounter job={record} state={{ type: 'complete' }} />
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
title: <PauseCircleIcon />,
|
|
||||||
key: 'queued',
|
|
||||||
width: 70,
|
|
||||||
render: (record) => {
|
|
||||||
return <SubJobCounter job={record} state={{ type: 'queued' }} />
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
title: <XMarkCircleIcon />,
|
|
||||||
key: 'failed',
|
|
||||||
width: 70,
|
|
||||||
render: (record) => {
|
|
||||||
return <SubJobCounter job={record} state={{ type: 'failed' }} />
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
title: <QuestionCircleIcon />,
|
|
||||||
key: 'draft',
|
|
||||||
width: 70,
|
|
||||||
render: (record) => {
|
|
||||||
return <SubJobCounter job={record} state={{ type: 'draft' }} />
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
title: 'Created At',
|
|
||||||
dataIndex: 'createdAt',
|
|
||||||
key: 'createdAt',
|
|
||||||
width: 180,
|
|
||||||
render: (createdAt) => {
|
|
||||||
if (createdAt) {
|
|
||||||
return <TimeDisplay dateTime={createdAt} />
|
|
||||||
} else {
|
|
||||||
return 'n/a'
|
|
||||||
}
|
|
||||||
},
|
|
||||||
sorter: true
|
|
||||||
},
|
|
||||||
{
|
|
||||||
title: 'Started At',
|
|
||||||
dataIndex: 'startedAt',
|
|
||||||
key: 'startedAt',
|
|
||||||
width: 180,
|
|
||||||
render: (startedAt) => {
|
|
||||||
if (startedAt) {
|
|
||||||
return <TimeDisplay dateTime={startedAt} />
|
|
||||||
} else {
|
|
||||||
return 'n/a'
|
|
||||||
}
|
|
||||||
},
|
|
||||||
sorter: true
|
|
||||||
},
|
|
||||||
{
|
|
||||||
title: 'Actions',
|
|
||||||
key: 'actions',
|
|
||||||
fixed: 'right',
|
|
||||||
width: 150,
|
|
||||||
render: (record) => {
|
|
||||||
return (
|
|
||||||
<Space size='small'>
|
|
||||||
{record?.state?.type === 'draft' ? (
|
|
||||||
<Button
|
|
||||||
icon={<PlayCircleIcon />}
|
|
||||||
onClick={() => handleDeployJob(record.id)}
|
|
||||||
/>
|
|
||||||
) : (
|
|
||||||
<Button
|
|
||||||
icon={<InfoCircleIcon />}
|
|
||||||
onClick={() =>
|
|
||||||
navigate(`/dashboard/production/jobs/info?jobId=${record.id}`)
|
|
||||||
}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
<Dropdown menu={getJobActionItems(record.id)}>
|
|
||||||
<Button>Actions</Button>
|
|
||||||
</Dropdown>
|
|
||||||
</Space>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
]
|
|
||||||
|
|
||||||
const { authenticated } = useContext(AuthContext)
|
const { authenticated } = useContext(AuthContext)
|
||||||
const { printServer } = useContext(PrintServerContext)
|
|
||||||
|
|
||||||
const [columnVisibility, updateColumnVisibility] = useColumnVisibility(
|
const [columnVisibility, setColumnVisibility] = useColumnVisibility('job')
|
||||||
'Jobs',
|
|
||||||
columns
|
|
||||||
)
|
|
||||||
|
|
||||||
const handleDeployJob = (jobId) => {
|
|
||||||
if (printServer) {
|
|
||||||
messageApi.info(`Print job ${jobId} deployment initiated`)
|
|
||||||
printServer.emit('server.job_queue.deploy', { jobId }, (response) => {
|
|
||||||
if (response == false) {
|
|
||||||
notificationApi.error({
|
|
||||||
message: 'Print job deployment failed',
|
|
||||||
description: 'Please try again later'
|
|
||||||
})
|
|
||||||
} else {
|
|
||||||
notificationApi.success({
|
|
||||||
message: 'Print job deployment initiated',
|
|
||||||
description: 'Please wait for the print job to start'
|
|
||||||
})
|
|
||||||
}
|
|
||||||
})
|
|
||||||
navigate(`/dashboard/production/jobs/info?jobId=${jobId}`)
|
|
||||||
} else {
|
|
||||||
messageApi.error('Socket connection not available')
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const getJobActionItems = (jobId) => {
|
|
||||||
return {
|
|
||||||
items: [
|
|
||||||
{
|
|
||||||
label: 'Info',
|
|
||||||
key: 'info',
|
|
||||||
icon: <InfoCircleIcon />
|
|
||||||
},
|
|
||||||
{
|
|
||||||
label: 'Edit',
|
|
||||||
key: 'edit',
|
|
||||||
icon: <EditIcon />
|
|
||||||
}
|
|
||||||
],
|
|
||||||
onClick: ({ key }) => {
|
|
||||||
if (key === 'edit') {
|
|
||||||
showNewJobModal(jobId)
|
|
||||||
} else if (key === 'info') {
|
|
||||||
navigate(`/dashboard/production/jobs/info?jobId=${jobId}`)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const actionItems = {
|
const actionItems = {
|
||||||
items: [
|
items: [
|
||||||
@ -336,33 +50,8 @@ const Jobs = () => {
|
|||||||
setNewJobOpen(true)
|
setNewJobOpen(true)
|
||||||
}
|
}
|
||||||
|
|
||||||
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>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
{notificationContextHolder}
|
|
||||||
<Flex vertical={'true'} gap='large' style={{ height: '100%' }}>
|
<Flex vertical={'true'} gap='large' style={{ height: '100%' }}>
|
||||||
{contextHolder}
|
{contextHolder}
|
||||||
<Flex justify={'space-between'}>
|
<Flex justify={'space-between'}>
|
||||||
@ -370,14 +59,14 @@ const Jobs = () => {
|
|||||||
<Dropdown menu={actionItems}>
|
<Dropdown menu={actionItems}>
|
||||||
<Button>Actions</Button>
|
<Button>Actions</Button>
|
||||||
</Dropdown>
|
</Dropdown>
|
||||||
<Popover
|
<ColumnViewButton
|
||||||
content={getViewDropdownItems()}
|
type='job'
|
||||||
placement='bottomLeft'
|
disabled={false}
|
||||||
arrow={false}
|
visibleState={columnVisibility}
|
||||||
>
|
updateVisibleState={setColumnVisibility}
|
||||||
<Button>View</Button>
|
/>
|
||||||
</Popover>
|
|
||||||
</Space>
|
</Space>
|
||||||
|
|
||||||
<Space>
|
<Space>
|
||||||
<Button
|
<Button
|
||||||
icon={viewMode === 'cards' ? <ListIcon /> : <GridIcon />}
|
icon={viewMode === 'cards' ? <ListIcon /> : <GridIcon />}
|
||||||
@ -391,6 +80,7 @@ const Jobs = () => {
|
|||||||
<ObjectTable
|
<ObjectTable
|
||||||
ref={tableRef}
|
ref={tableRef}
|
||||||
type={'job'}
|
type={'job'}
|
||||||
|
visibleColumns={columnVisibility}
|
||||||
authenticated={authenticated}
|
authenticated={authenticated}
|
||||||
cards={viewMode === 'cards'}
|
cards={viewMode === 'cards'}
|
||||||
/>
|
/>
|
||||||
|
|||||||
@ -1,9 +1,8 @@
|
|||||||
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 } from 'antd'
|
import { Space, Flex, Card } from 'antd'
|
||||||
import { LoadingOutlined } from '@ant-design/icons'
|
import { LoadingOutlined } from '@ant-design/icons'
|
||||||
import useCollapseState from '../../hooks/useCollapseState'
|
import useCollapseState from '../../hooks/useCollapseState'
|
||||||
import AuditLogTable from '../../common/AuditLogTable'
|
|
||||||
import NotesPanel from '../../common/NotesPanel'
|
import NotesPanel from '../../common/NotesPanel'
|
||||||
import InfoCollapse from '../../common/InfoCollapse'
|
import InfoCollapse from '../../common/InfoCollapse'
|
||||||
import ObjectInfo from '../../common/ObjectInfo'
|
import ObjectInfo from '../../common/ObjectInfo'
|
||||||
@ -17,15 +16,15 @@ 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'
|
||||||
import NoteIcon from '../../../Icons/NoteIcon'
|
import NoteIcon from '../../../Icons/NoteIcon'
|
||||||
import GCodeFileIcon from '../../../Icons/GCodeFileIcon'
|
import ObjectActions from '../../common/ObjectActions.jsx'
|
||||||
import {
|
import { ApiServerContext } from '../../context/ApiServerContext'
|
||||||
getModelProperties,
|
import ObjectTable from '../../common/ObjectTable.jsx'
|
||||||
getPropertyValue
|
import InfoCollapsePlaceholder from '../../common/InfoCollapsePlaceholder.jsx'
|
||||||
} from '../../../../database/ObjectModels.js'
|
|
||||||
|
|
||||||
const JobInfo = () => {
|
const JobInfo = () => {
|
||||||
const location = useLocation()
|
const location = useLocation()
|
||||||
const jobId = new URLSearchParams(location.search).get('jobId')
|
const jobId = new URLSearchParams(location.search).get('jobId')
|
||||||
|
const { handleDownloadContent } = useContext(ApiServerContext)
|
||||||
const [collapseState, updateCollapseState] = useCollapseState('JobInfo', {
|
const [collapseState, updateCollapseState] = useCollapseState('JobInfo', {
|
||||||
info: true,
|
info: true,
|
||||||
subJobs: true,
|
subJobs: true,
|
||||||
@ -33,153 +32,173 @@ const JobInfo = () => {
|
|||||||
auditLogs: true
|
auditLogs: true
|
||||||
})
|
})
|
||||||
|
|
||||||
// Define actions that can be triggered via URL
|
|
||||||
const actions = {
|
|
||||||
// Add job-specific actions here as needed
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<EditObjectForm
|
||||||
<ActionHandler actions={actions} />
|
id={jobId}
|
||||||
<EditObjectForm
|
type='job'
|
||||||
id={jobId}
|
style={{ height: 'calc(var(--unit-100vh) - 155px)', minHeight: 0 }}
|
||||||
type='job'
|
>
|
||||||
style={{ height: 'calc(var(--unit-100vh) - 155px)', minHeight: 0 }}
|
{({
|
||||||
>
|
loading,
|
||||||
{({
|
isEditing,
|
||||||
loading,
|
startEditing,
|
||||||
isEditing,
|
cancelEditing,
|
||||||
startEditing,
|
handleUpdate,
|
||||||
cancelEditing,
|
formValid,
|
||||||
handleUpdate,
|
objectData,
|
||||||
formValid,
|
editLoading,
|
||||||
objectData,
|
lock,
|
||||||
editLoading,
|
fetchObject
|
||||||
lock,
|
}) => {
|
||||||
fetchObject
|
// Define actions that can be triggered via URL, now with access to startEditing
|
||||||
}) => (
|
const actions = {
|
||||||
<Flex
|
reload: () => {
|
||||||
gap='large'
|
fetchObject()
|
||||||
vertical='true'
|
return true
|
||||||
style={{ height: '100%', minHeight: 0 }}
|
},
|
||||||
>
|
edit: () => {
|
||||||
<Flex justify={'space-between'}>
|
startEditing()
|
||||||
<Space size='middle'>
|
return false
|
||||||
<Space size='small'>
|
},
|
||||||
<Dropdown
|
cancelEdit: () => {
|
||||||
menu={{
|
cancelEditing()
|
||||||
items: [
|
return true
|
||||||
{
|
},
|
||||||
label: 'Reload Job',
|
finishEdit: () => {
|
||||||
key: 'reload',
|
handleUpdate()
|
||||||
icon: <GCodeFileIcon />
|
return true
|
||||||
}
|
},
|
||||||
],
|
download: () => {
|
||||||
onClick: ({ key }) => {
|
if (jobId) {
|
||||||
if (key === 'reload') {
|
handleDownloadContent(
|
||||||
fetchObject()
|
jobId,
|
||||||
}
|
'job',
|
||||||
|
`${objectData.name || 'job'}.json`
|
||||||
|
)
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<ActionHandler actions={actions} loading={loading}>
|
||||||
|
{({ callAction }) => (
|
||||||
|
<Flex
|
||||||
|
gap='large'
|
||||||
|
vertical='true'
|
||||||
|
style={{ height: '100%', minHeight: 0 }}
|
||||||
|
>
|
||||||
|
<Flex justify={'space-between'}>
|
||||||
|
<Space size='middle'>
|
||||||
|
<Space size='small'>
|
||||||
|
<ObjectActions type='job' id={jobId} disabled={loading} />
|
||||||
|
<ViewButton
|
||||||
|
disabled={loading}
|
||||||
|
items={[
|
||||||
|
{ key: 'info', label: 'Job Information' },
|
||||||
|
{ key: 'subJobs', label: 'Sub Jobs' },
|
||||||
|
{ key: 'notes', label: 'Notes' },
|
||||||
|
{ key: 'auditLogs', label: 'Audit Logs' }
|
||||||
|
]}
|
||||||
|
visibleState={collapseState}
|
||||||
|
updateVisibleState={updateCollapseState}
|
||||||
|
/>
|
||||||
|
</Space>
|
||||||
|
<LockIndicator lock={lock} />
|
||||||
|
</Space>
|
||||||
|
<Space>
|
||||||
|
<EditButtons
|
||||||
|
isEditing={isEditing}
|
||||||
|
handleUpdate={() => {
|
||||||
|
callAction('finishEdit')
|
||||||
|
}}
|
||||||
|
cancelEditing={() => {
|
||||||
|
callAction('cancelEdit')
|
||||||
|
}}
|
||||||
|
startEditing={() => {
|
||||||
|
callAction('edit')
|
||||||
|
}}
|
||||||
|
editLoading={editLoading}
|
||||||
|
formValid={formValid}
|
||||||
|
disabled={lock?.locked || loading}
|
||||||
|
loading={editLoading}
|
||||||
|
/>
|
||||||
|
</Space>
|
||||||
|
</Flex>
|
||||||
|
|
||||||
|
<div style={{ height: '100%', overflow: 'auto' }}>
|
||||||
|
<Flex vertical gap={'large'}>
|
||||||
|
<InfoCollapse
|
||||||
|
title='Job Information'
|
||||||
|
icon={<InfoCircleIcon />}
|
||||||
|
active={collapseState.info}
|
||||||
|
onToggle={(expanded) =>
|
||||||
|
updateCollapseState('info', expanded)
|
||||||
}
|
}
|
||||||
}}
|
key='info'
|
||||||
>
|
>
|
||||||
<Button disabled={loading}>Actions</Button>
|
<ObjectInfo
|
||||||
</Dropdown>
|
loading={loading}
|
||||||
<ViewButton
|
indicator={<LoadingOutlined />}
|
||||||
loading={loading}
|
isEditing={isEditing}
|
||||||
sections={[
|
type='job'
|
||||||
{ key: 'info', label: 'Job Information' },
|
objectData={objectData}
|
||||||
{ key: 'subJobs', label: 'Sub Jobs' },
|
/>
|
||||||
{ key: 'notes', label: 'Notes' },
|
</InfoCollapse>
|
||||||
{ key: 'auditLogs', label: 'Audit Logs' }
|
|
||||||
]}
|
|
||||||
collapseState={collapseState}
|
|
||||||
updateCollapseState={updateCollapseState}
|
|
||||||
/>
|
|
||||||
</Space>
|
|
||||||
<LockIndicator lock={lock} />
|
|
||||||
</Space>
|
|
||||||
<Space>
|
|
||||||
<EditButtons
|
|
||||||
isEditing={isEditing}
|
|
||||||
handleUpdate={handleUpdate}
|
|
||||||
cancelEditing={cancelEditing}
|
|
||||||
startEditing={startEditing}
|
|
||||||
editLoading={editLoading}
|
|
||||||
formValid={formValid}
|
|
||||||
disabled={lock?.locked || loading || true}
|
|
||||||
loading={editLoading}
|
|
||||||
/>
|
|
||||||
</Space>
|
|
||||||
</Flex>
|
|
||||||
|
|
||||||
<div style={{ height: '100%', overflow: 'auto' }}>
|
<InfoCollapse
|
||||||
<Flex vertical gap={'large'}>
|
title='Sub Jobs'
|
||||||
<InfoCollapse
|
icon={<JobIcon />}
|
||||||
title='Job Information'
|
active={collapseState.subJobs}
|
||||||
icon={<InfoCircleIcon />}
|
onToggle={(expanded) =>
|
||||||
active={collapseState.info}
|
updateCollapseState('subJobs', expanded)
|
||||||
onToggle={(expanded) => updateCollapseState('info', expanded)}
|
}
|
||||||
key='info'
|
key='subJobs'
|
||||||
>
|
>
|
||||||
<ObjectInfo
|
<SubJobsTree jobData={objectData} loading={loading} />
|
||||||
loading={loading}
|
</InfoCollapse>
|
||||||
indicator={<LoadingOutlined />}
|
|
||||||
isEditing={isEditing}
|
|
||||||
type='job'
|
|
||||||
items={getModelProperties('job').map((prop) => ({
|
|
||||||
...prop,
|
|
||||||
value: getPropertyValue(objectData, prop.name)
|
|
||||||
}))}
|
|
||||||
/>
|
|
||||||
</InfoCollapse>
|
|
||||||
|
|
||||||
<InfoCollapse
|
<InfoCollapse
|
||||||
title='Sub Jobs'
|
title='Notes'
|
||||||
icon={<JobIcon />}
|
icon={<NoteIcon />}
|
||||||
active={collapseState.subJobs}
|
active={collapseState.notes}
|
||||||
onToggle={(expanded) =>
|
onToggle={(expanded) =>
|
||||||
updateCollapseState('subJobs', expanded)
|
updateCollapseState('notes', expanded)
|
||||||
}
|
}
|
||||||
key='subJobs'
|
key='notes'
|
||||||
>
|
>
|
||||||
<SubJobsTree jobData={objectData} loading={loading} />
|
<Card>
|
||||||
</InfoCollapse>
|
<NotesPanel _id={jobId} />
|
||||||
|
</Card>
|
||||||
|
</InfoCollapse>
|
||||||
|
|
||||||
<InfoCollapse
|
<InfoCollapse
|
||||||
title='Notes'
|
title='Audit Logs'
|
||||||
icon={<NoteIcon />}
|
icon={<AuditLogIcon />}
|
||||||
active={collapseState.notes}
|
active={collapseState.auditLogs}
|
||||||
onToggle={(expanded) =>
|
onToggle={(expanded) =>
|
||||||
updateCollapseState('notes', expanded)
|
updateCollapseState('auditLogs', expanded)
|
||||||
}
|
}
|
||||||
key='notes'
|
key='auditLogs'
|
||||||
>
|
>
|
||||||
<Card>
|
{loading ? (
|
||||||
<NotesPanel _id={jobId} />
|
<InfoCollapsePlaceholder />
|
||||||
</Card>
|
) : (
|
||||||
</InfoCollapse>
|
<ObjectTable
|
||||||
|
type='auditLog'
|
||||||
<InfoCollapse
|
masterFilter={{ 'parent._id': jobId }}
|
||||||
title='Audit Logs'
|
visibleColumns={{ _id: false, 'parent._id': false }}
|
||||||
icon={<AuditLogIcon />}
|
/>
|
||||||
active={collapseState.auditLogs}
|
)}
|
||||||
onToggle={(expanded) =>
|
</InfoCollapse>
|
||||||
updateCollapseState('auditLogs', expanded)
|
</Flex>
|
||||||
}
|
</div>
|
||||||
key='auditLogs'
|
|
||||||
>
|
|
||||||
<AuditLogTable
|
|
||||||
items={objectData?.auditLogs || []}
|
|
||||||
loading={loading}
|
|
||||||
showTargetColumn={false}
|
|
||||||
/>
|
|
||||||
</InfoCollapse>
|
|
||||||
</Flex>
|
</Flex>
|
||||||
</div>
|
)}
|
||||||
</Flex>
|
</ActionHandler>
|
||||||
)}
|
)
|
||||||
</EditObjectForm>
|
}}
|
||||||
</>
|
</EditObjectForm>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -1,9 +1,7 @@
|
|||||||
// src/Printers.js
|
// src/Printers.js
|
||||||
|
|
||||||
import React, { useState, useContext, useRef } from 'react'
|
import React, { useState, useRef } from 'react'
|
||||||
import { Button, message, Dropdown, Space, Flex, Modal } from 'antd'
|
import { Button, message, Dropdown, Space, Flex, Modal } from 'antd'
|
||||||
|
|
||||||
import { AuthContext } from '../context/AuthContext'
|
|
||||||
import NewPrinter from './Printers/NewPrinter'
|
import NewPrinter from './Printers/NewPrinter'
|
||||||
import PlusIcon from '../../Icons/PlusIcon'
|
import PlusIcon from '../../Icons/PlusIcon'
|
||||||
import ReloadIcon from '../../Icons/ReloadIcon'
|
import ReloadIcon from '../../Icons/ReloadIcon'
|
||||||
@ -17,7 +15,6 @@ import useColumnVisibility from '../hooks/useColumnVisibility'
|
|||||||
|
|
||||||
const Printers = () => {
|
const Printers = () => {
|
||||||
const [messageApi, contextHolder] = message.useMessage()
|
const [messageApi, contextHolder] = message.useMessage()
|
||||||
const { authenticated } = useContext(AuthContext)
|
|
||||||
const [newPrinterOpen, setNewPrinterOpen] = useState(false)
|
const [newPrinterOpen, setNewPrinterOpen] = useState(false)
|
||||||
const tableRef = useRef()
|
const tableRef = useRef()
|
||||||
|
|
||||||
@ -61,9 +58,9 @@ const Printers = () => {
|
|||||||
</Dropdown>
|
</Dropdown>
|
||||||
<ColumnViewButton
|
<ColumnViewButton
|
||||||
type='printer'
|
type='printer'
|
||||||
loading={false}
|
disabled={false}
|
||||||
collapseState={columnVisibility}
|
visibleState={columnVisibility}
|
||||||
updateCollapseState={setColumnVisibility}
|
updateVisibleState={setColumnVisibility}
|
||||||
/>
|
/>
|
||||||
</Space>
|
</Space>
|
||||||
<Space>
|
<Space>
|
||||||
@ -78,8 +75,7 @@ const Printers = () => {
|
|||||||
|
|
||||||
<ObjectTable
|
<ObjectTable
|
||||||
ref={tableRef}
|
ref={tableRef}
|
||||||
type={'printer'}
|
type='printer'
|
||||||
authenticated={authenticated}
|
|
||||||
cards={viewMode === 'cards'}
|
cards={viewMode === 'cards'}
|
||||||
visibleColumns={columnVisibility}
|
visibleColumns={columnVisibility}
|
||||||
/>
|
/>
|
||||||
|
|||||||
@ -1,9 +1,8 @@
|
|||||||
import React from 'react'
|
import React from 'react'
|
||||||
import { useLocation } from 'react-router-dom'
|
import { useLocation } from 'react-router-dom'
|
||||||
import { Space, Button, Flex, Dropdown, Card } from 'antd'
|
import { Space, Flex, Card } from 'antd'
|
||||||
import { LoadingOutlined } from '@ant-design/icons'
|
import { LoadingOutlined } from '@ant-design/icons'
|
||||||
import useCollapseState from '../../hooks/useCollapseState'
|
import useCollapseState from '../../hooks/useCollapseState'
|
||||||
import AuditLogTable from '../../common/AuditLogTable'
|
|
||||||
import NotesPanel from '../../common/NotesPanel'
|
import NotesPanel from '../../common/NotesPanel'
|
||||||
import InfoCollapse from '../../common/InfoCollapse'
|
import InfoCollapse from '../../common/InfoCollapse'
|
||||||
import ObjectInfo from '../../common/ObjectInfo'
|
import ObjectInfo from '../../common/ObjectInfo'
|
||||||
@ -16,10 +15,10 @@ import InfoCircleIcon from '../../../Icons/InfoCircleIcon.jsx'
|
|||||||
import NoteIcon from '../../../Icons/NoteIcon.jsx'
|
import NoteIcon from '../../../Icons/NoteIcon.jsx'
|
||||||
import PrinterIcon from '../../../Icons/PrinterIcon.jsx'
|
import PrinterIcon from '../../../Icons/PrinterIcon.jsx'
|
||||||
import AuditLogIcon from '../../../Icons/AuditLogIcon.jsx'
|
import AuditLogIcon from '../../../Icons/AuditLogIcon.jsx'
|
||||||
import {
|
import ActionHandler from '../../common/ActionHandler'
|
||||||
getModelProperties,
|
import ObjectActions from '../../common/ObjectActions.jsx'
|
||||||
getPropertyValue
|
import ObjectTable from '../../common/ObjectTable.jsx'
|
||||||
} from '../../../../database/ObjectModels.js'
|
import InfoCollapsePlaceholder from '../../common/InfoCollapsePlaceholder.jsx'
|
||||||
|
|
||||||
const PrinterInfo = () => {
|
const PrinterInfo = () => {
|
||||||
const location = useLocation()
|
const location = useLocation()
|
||||||
@ -28,7 +27,8 @@ const PrinterInfo = () => {
|
|||||||
info: true,
|
info: true,
|
||||||
jobs: true,
|
jobs: true,
|
||||||
notes: true,
|
notes: true,
|
||||||
auditLogs: true
|
auditLogsParent: true,
|
||||||
|
auditLogsOwner: true
|
||||||
})
|
})
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@ -48,126 +48,178 @@ const PrinterInfo = () => {
|
|||||||
editLoading,
|
editLoading,
|
||||||
lock,
|
lock,
|
||||||
fetchObject
|
fetchObject
|
||||||
}) => (
|
}) => {
|
||||||
<Flex
|
// Define actions for ActionHandler
|
||||||
gap='large'
|
const actions = {
|
||||||
vertical='true'
|
reload: () => {
|
||||||
style={{ height: '100%', minHeight: 0 }}
|
fetchObject()
|
||||||
>
|
return true
|
||||||
<Flex justify={'space-between'}>
|
},
|
||||||
<Space size='middle'>
|
edit: () => {
|
||||||
<Space size='small'>
|
startEditing()
|
||||||
<Dropdown
|
return false
|
||||||
menu={{
|
},
|
||||||
items: [
|
cancelEdit: () => {
|
||||||
{
|
cancelEditing()
|
||||||
label: 'Reload Printer',
|
return true
|
||||||
key: 'reload',
|
},
|
||||||
icon: <AuditLogIcon />
|
finishEdit: () => {
|
||||||
|
handleUpdate()
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<ActionHandler actions={actions} loading={loading}>
|
||||||
|
{({ callAction }) => (
|
||||||
|
<Flex
|
||||||
|
gap='large'
|
||||||
|
vertical='true'
|
||||||
|
style={{ height: '100%', minHeight: 0 }}
|
||||||
|
>
|
||||||
|
<Flex justify={'space-between'}>
|
||||||
|
<Space size='middle'>
|
||||||
|
<Space size='small'>
|
||||||
|
<ObjectActions
|
||||||
|
type='printer'
|
||||||
|
id={printerId}
|
||||||
|
disabled={loading}
|
||||||
|
/>
|
||||||
|
<ViewButton
|
||||||
|
disabled={loading}
|
||||||
|
items={[
|
||||||
|
{ key: 'info', label: 'Printer Information' },
|
||||||
|
{ key: 'jobs', label: 'Printer Jobs' },
|
||||||
|
{ key: 'notes', label: 'Notes' },
|
||||||
|
{
|
||||||
|
key: 'auditLogsParent',
|
||||||
|
label: 'Audit Logs (By Parent)'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: 'auditLogsOwner',
|
||||||
|
label: 'Audit Logs (By Owner)'
|
||||||
|
}
|
||||||
|
]}
|
||||||
|
visibleState={collapseState}
|
||||||
|
updateVisibleState={updateCollapseState}
|
||||||
|
/>
|
||||||
|
</Space>
|
||||||
|
<LockIndicator lock={lock} />
|
||||||
|
</Space>
|
||||||
|
<Space>
|
||||||
|
<EditButtons
|
||||||
|
isEditing={isEditing}
|
||||||
|
handleUpdate={() => {
|
||||||
|
callAction('finishEdit')
|
||||||
|
}}
|
||||||
|
cancelEditing={() => {
|
||||||
|
callAction('cancelEdit')
|
||||||
|
}}
|
||||||
|
startEditing={() => {
|
||||||
|
callAction('edit')
|
||||||
|
}}
|
||||||
|
editLoading={editLoading}
|
||||||
|
formValid={formValid}
|
||||||
|
disabled={lock?.locked || loading}
|
||||||
|
loading={editLoading}
|
||||||
|
/>
|
||||||
|
</Space>
|
||||||
|
</Flex>
|
||||||
|
|
||||||
|
<div style={{ height: '100%', overflow: 'auto' }}>
|
||||||
|
<Flex vertical gap={'large'}>
|
||||||
|
<InfoCollapse
|
||||||
|
title='Printer Information'
|
||||||
|
icon={<InfoCircleIcon />}
|
||||||
|
active={collapseState.info}
|
||||||
|
onToggle={(expanded) =>
|
||||||
|
updateCollapseState('info', expanded)
|
||||||
}
|
}
|
||||||
],
|
key='info'
|
||||||
onClick: ({ key }) => {
|
>
|
||||||
if (key === 'reload') {
|
<ObjectInfo
|
||||||
fetchObject()
|
loading={loading}
|
||||||
|
indicator={<LoadingOutlined />}
|
||||||
|
isEditing={isEditing}
|
||||||
|
type='printer'
|
||||||
|
objectData={objectData}
|
||||||
|
/>
|
||||||
|
</InfoCollapse>
|
||||||
|
|
||||||
|
<InfoCollapse
|
||||||
|
title='Printer Jobs'
|
||||||
|
icon={<PrinterIcon />}
|
||||||
|
active={collapseState.jobs}
|
||||||
|
onToggle={(expanded) =>
|
||||||
|
updateCollapseState('jobs', expanded)
|
||||||
}
|
}
|
||||||
}
|
key='jobs'
|
||||||
}}
|
>
|
||||||
>
|
<PrinterJobsTree
|
||||||
<Button disabled={loading}>Actions</Button>
|
subJobs={objectData?.subJobs}
|
||||||
</Dropdown>
|
loading={loading}
|
||||||
<ViewButton
|
/>
|
||||||
loading={loading}
|
</InfoCollapse>
|
||||||
sections={[
|
|
||||||
{ key: 'info', label: 'Printer Information' },
|
|
||||||
{ key: 'jobs', label: 'Printer Jobs' },
|
|
||||||
{ key: 'notes', label: 'Notes' },
|
|
||||||
{ key: 'auditLogs', label: 'Audit Logs' }
|
|
||||||
]}
|
|
||||||
collapseState={collapseState}
|
|
||||||
updateCollapseState={updateCollapseState}
|
|
||||||
/>
|
|
||||||
</Space>
|
|
||||||
<LockIndicator lock={lock} />
|
|
||||||
</Space>
|
|
||||||
<Space>
|
|
||||||
<EditButtons
|
|
||||||
isEditing={isEditing}
|
|
||||||
handleUpdate={handleUpdate}
|
|
||||||
cancelEditing={cancelEditing}
|
|
||||||
startEditing={startEditing}
|
|
||||||
editLoading={editLoading}
|
|
||||||
formValid={formValid}
|
|
||||||
disabled={lock?.locked || loading}
|
|
||||||
loading={editLoading}
|
|
||||||
/>
|
|
||||||
</Space>
|
|
||||||
</Flex>
|
|
||||||
|
|
||||||
<div style={{ height: '100%', overflow: 'auto' }}>
|
<InfoCollapse
|
||||||
<Flex vertical gap={'large'}>
|
title='Notes'
|
||||||
<InfoCollapse
|
icon={<NoteIcon />}
|
||||||
title='Printer Information'
|
active={collapseState.notes}
|
||||||
icon={<InfoCircleIcon />}
|
onToggle={(expanded) =>
|
||||||
active={collapseState.info}
|
updateCollapseState('notes', expanded)
|
||||||
onToggle={(expanded) => updateCollapseState('info', expanded)}
|
}
|
||||||
key='info'
|
key='notes'
|
||||||
>
|
>
|
||||||
<ObjectInfo
|
<Card>
|
||||||
loading={loading}
|
<NotesPanel _id={printerId} />
|
||||||
indicator={<LoadingOutlined />}
|
</Card>
|
||||||
isEditing={isEditing}
|
</InfoCollapse>
|
||||||
type='printer'
|
|
||||||
items={getModelProperties('printer').map((prop) => ({
|
|
||||||
...prop,
|
|
||||||
value: getPropertyValue(objectData, prop.name)
|
|
||||||
}))}
|
|
||||||
/>
|
|
||||||
</InfoCollapse>
|
|
||||||
|
|
||||||
<InfoCollapse
|
<InfoCollapse
|
||||||
title='Printer Jobs'
|
title='Audit Logs (By Parent)'
|
||||||
icon={<PrinterIcon />}
|
icon={<AuditLogIcon />}
|
||||||
active={collapseState.jobs}
|
active={collapseState.auditLogsParent}
|
||||||
onToggle={(expanded) => updateCollapseState('jobs', expanded)}
|
onToggle={(expanded) =>
|
||||||
key='jobs'
|
updateCollapseState('auditLogsParent', expanded)
|
||||||
>
|
}
|
||||||
<PrinterJobsTree
|
key='auditLogs'
|
||||||
subJobs={objectData?.subJobs}
|
>
|
||||||
loading={loading}
|
{loading ? (
|
||||||
/>
|
<InfoCollapsePlaceholder />
|
||||||
</InfoCollapse>
|
) : (
|
||||||
|
<ObjectTable
|
||||||
<InfoCollapse
|
type='auditLog'
|
||||||
title='Notes'
|
masterFilter={{ 'parent._id': printerId }}
|
||||||
icon={<NoteIcon />}
|
visibleColumns={{ _id: false, 'parent._id': false }}
|
||||||
active={collapseState.notes}
|
/>
|
||||||
onToggle={(expanded) => updateCollapseState('notes', expanded)}
|
)}
|
||||||
key='notes'
|
</InfoCollapse>
|
||||||
>
|
<InfoCollapse
|
||||||
<Card>
|
title='Audit Logs (By Owner)'
|
||||||
<NotesPanel _id={printerId} />
|
icon={<AuditLogIcon />}
|
||||||
</Card>
|
active={collapseState.auditLogsOwner}
|
||||||
</InfoCollapse>
|
onToggle={(expanded) =>
|
||||||
|
updateCollapseState('auditLogsOwner', expanded)
|
||||||
<InfoCollapse
|
}
|
||||||
title='Audit Logs'
|
key='auditLogs'
|
||||||
icon={<AuditLogIcon />}
|
>
|
||||||
active={collapseState.auditLogs}
|
{loading ? (
|
||||||
onToggle={(expanded) =>
|
<InfoCollapsePlaceholder />
|
||||||
updateCollapseState('auditLogs', expanded)
|
) : (
|
||||||
}
|
<ObjectTable
|
||||||
key='auditLogs'
|
type='auditLog'
|
||||||
>
|
masterFilter={{ 'owner._id': printerId }}
|
||||||
<AuditLogTable
|
visibleColumns={{ _id: false, 'owner._id': false }}
|
||||||
items={objectData?.auditLogs || []}
|
/>
|
||||||
loading={loading}
|
)}
|
||||||
showTargetColumn={false}
|
</InfoCollapse>
|
||||||
/>
|
</Flex>
|
||||||
</InfoCollapse>
|
</div>
|
||||||
</Flex>
|
</Flex>
|
||||||
</div>
|
)}
|
||||||
</Flex>
|
</ActionHandler>
|
||||||
)}
|
)
|
||||||
|
}}
|
||||||
</EditObjectForm>
|
</EditObjectForm>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,38 +1,61 @@
|
|||||||
import { useEffect } from 'react'
|
import React, { useEffect, useRef } from 'react'
|
||||||
import { useLocation, useNavigate } from 'react-router-dom'
|
import { useLocation, useNavigate } from 'react-router-dom'
|
||||||
import PropTypes from 'prop-types'
|
import PropTypes from 'prop-types'
|
||||||
|
|
||||||
const ActionHandler = ({
|
const ActionHandler = ({
|
||||||
|
children,
|
||||||
actions = {},
|
actions = {},
|
||||||
actionParam = 'action',
|
actionParam = 'action',
|
||||||
clearAfterExecute = true,
|
clearAfterExecute = true,
|
||||||
onAction
|
onAction,
|
||||||
|
loading = true
|
||||||
}) => {
|
}) => {
|
||||||
const location = useLocation()
|
const location = useLocation()
|
||||||
const navigate = useNavigate()
|
const navigate = useNavigate()
|
||||||
const action = new URLSearchParams(location.search).get(actionParam)
|
const action = new URLSearchParams(location.search).get(actionParam)
|
||||||
|
|
||||||
|
// Ref to track last executed action
|
||||||
|
const lastExecutedAction = useRef(null)
|
||||||
|
|
||||||
|
// Method to add action as URL param
|
||||||
|
const callAction = (actionName) => {
|
||||||
|
const searchParams = new URLSearchParams(location.search)
|
||||||
|
searchParams.set(actionParam, actionName)
|
||||||
|
const newSearch = searchParams.toString()
|
||||||
|
const newPath = location.pathname + (newSearch ? `?${newSearch}` : '')
|
||||||
|
navigate(newPath, { replace: true })
|
||||||
|
}
|
||||||
|
|
||||||
// Execute action and clear from URL
|
// Execute action and clear from URL
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (action && actions[action]) {
|
if (
|
||||||
|
!loading &&
|
||||||
|
action &&
|
||||||
|
actions[action] &&
|
||||||
|
lastExecutedAction.current !== action
|
||||||
|
) {
|
||||||
// Execute the action
|
// Execute the action
|
||||||
const result = actions[action]()
|
const result = actions[action]()
|
||||||
|
// Mark this action as executed
|
||||||
|
lastExecutedAction.current = action
|
||||||
// Call optional callback
|
// Call optional callback
|
||||||
if (onAction) {
|
if (onAction) {
|
||||||
onAction(action, result)
|
onAction(action, result)
|
||||||
}
|
}
|
||||||
|
// Clear action from URL if requested and result is true
|
||||||
// Clear action from URL if requested
|
if (clearAfterExecute && result == true) {
|
||||||
if (clearAfterExecute) {
|
|
||||||
const searchParams = new URLSearchParams(location.search)
|
const searchParams = new URLSearchParams(location.search)
|
||||||
searchParams.delete(actionParam)
|
searchParams.delete(actionParam)
|
||||||
const newSearch = searchParams.toString()
|
const newSearch = searchParams.toString()
|
||||||
const newPath = location.pathname + (newSearch ? `?${newSearch}` : '')
|
const newPath = location.pathname + (newSearch ? `?${newSearch}` : '')
|
||||||
navigate(newPath, { replace: true })
|
navigate(newPath, { replace: true })
|
||||||
}
|
}
|
||||||
|
} else if (!action) {
|
||||||
|
// Reset lastExecutedAction if no action is present
|
||||||
|
lastExecutedAction.current = null
|
||||||
}
|
}
|
||||||
}, [
|
}, [
|
||||||
|
loading,
|
||||||
action,
|
action,
|
||||||
actions,
|
actions,
|
||||||
actionParam,
|
actionParam,
|
||||||
@ -44,14 +67,16 @@ const ActionHandler = ({
|
|||||||
])
|
])
|
||||||
|
|
||||||
// Return null as this is a utility component
|
// Return null as this is a utility component
|
||||||
return null
|
return <>{children({ callAction })}</>
|
||||||
}
|
}
|
||||||
|
|
||||||
ActionHandler.propTypes = {
|
ActionHandler.propTypes = {
|
||||||
|
children: PropTypes.func,
|
||||||
actions: PropTypes.objectOf(PropTypes.func),
|
actions: PropTypes.objectOf(PropTypes.func),
|
||||||
actionParam: PropTypes.string,
|
actionParam: PropTypes.string,
|
||||||
clearAfterExecute: PropTypes.bool,
|
clearAfterExecute: PropTypes.bool,
|
||||||
onAction: PropTypes.func
|
onAction: PropTypes.func,
|
||||||
|
loading: PropTypes.bool
|
||||||
}
|
}
|
||||||
|
|
||||||
export default ActionHandler
|
export default ActionHandler
|
||||||
|
|||||||
@ -5,9 +5,9 @@ import { getModelByName } from '../../../database/ObjectModels'
|
|||||||
|
|
||||||
const ColumnViewButton = ({
|
const ColumnViewButton = ({
|
||||||
type,
|
type,
|
||||||
loading = false,
|
disabled = false,
|
||||||
collapseState = {},
|
visibleState = {},
|
||||||
updateCollapseState = () => {},
|
updateVisibleState = () => {},
|
||||||
...buttonProps
|
...buttonProps
|
||||||
}) => {
|
}) => {
|
||||||
// Get the model by name
|
// Get the model by name
|
||||||
@ -31,10 +31,10 @@ const ColumnViewButton = ({
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<ViewButton
|
<ViewButton
|
||||||
loading={loading}
|
disabled={disabled}
|
||||||
properties={columnProperties}
|
items={columnProperties}
|
||||||
collapseState={collapseState}
|
visibleState={visibleState}
|
||||||
updateCollapseState={updateCollapseState}
|
updateVisibleState={updateVisibleState}
|
||||||
{...buttonProps}
|
{...buttonProps}
|
||||||
/>
|
/>
|
||||||
)
|
)
|
||||||
@ -42,9 +42,9 @@ const ColumnViewButton = ({
|
|||||||
|
|
||||||
ColumnViewButton.propTypes = {
|
ColumnViewButton.propTypes = {
|
||||||
type: PropTypes.string.isRequired,
|
type: PropTypes.string.isRequired,
|
||||||
loading: PropTypes.bool,
|
disabled: PropTypes.bool,
|
||||||
collapseState: PropTypes.object,
|
visibleState: PropTypes.object,
|
||||||
updateCollapseState: PropTypes.func
|
updateVisibleState: PropTypes.func
|
||||||
}
|
}
|
||||||
|
|
||||||
export default ColumnViewButton
|
export default ColumnViewButton
|
||||||
|
|||||||
@ -17,6 +17,7 @@ import PropTypes from 'prop-types'
|
|||||||
*/
|
*/
|
||||||
const EditObjectForm = ({ id, type, style, children }) => {
|
const EditObjectForm = ({ id, type, style, children }) => {
|
||||||
const [objectData, setObjectData] = useState(null)
|
const [objectData, setObjectData] = useState(null)
|
||||||
|
const [serverObjectData, setServerObjectData] = useState(null)
|
||||||
const [fetchLoading, setFetchLoading] = useState(true)
|
const [fetchLoading, setFetchLoading] = useState(true)
|
||||||
const [editLoading, setEditLoading] = useState(false)
|
const [editLoading, setEditLoading] = useState(false)
|
||||||
const [lock, setLock] = useState({})
|
const [lock, setLock] = useState({})
|
||||||
@ -59,12 +60,6 @@ const EditObjectForm = ({ id, type, style, children }) => {
|
|||||||
}
|
}
|
||||||
}, [id, type, unlockObject])
|
}, [id, type, unlockObject])
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
if (objectData) {
|
|
||||||
form.setFieldsValue(objectData)
|
|
||||||
}
|
|
||||||
}, [objectData, form])
|
|
||||||
|
|
||||||
const fetchObject = useCallback(async () => {
|
const fetchObject = useCallback(async () => {
|
||||||
try {
|
try {
|
||||||
setFetchLoading(true)
|
setFetchLoading(true)
|
||||||
@ -72,6 +67,7 @@ const EditObjectForm = ({ id, type, style, children }) => {
|
|||||||
const lockEvent = await fetchObjectLock(id, type)
|
const lockEvent = await fetchObjectLock(id, type)
|
||||||
setLock(lockEvent)
|
setLock(lockEvent)
|
||||||
setObjectData(data)
|
setObjectData(data)
|
||||||
|
setServerObjectData(data)
|
||||||
form.setFieldsValue(data)
|
form.setFieldsValue(data)
|
||||||
setFetchLoading(false)
|
setFetchLoading(false)
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
@ -120,8 +116,9 @@ const EditObjectForm = ({ id, type, style, children }) => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const cancelEditing = () => {
|
const cancelEditing = () => {
|
||||||
if (objectData) {
|
if (serverObjectData) {
|
||||||
form.setFieldsValue(objectData)
|
form.setFieldsValue(serverObjectData)
|
||||||
|
setObjectData(serverObjectData)
|
||||||
}
|
}
|
||||||
setIsEditing(false)
|
setIsEditing(false)
|
||||||
unlockObject(id, type)
|
unlockObject(id, type)
|
||||||
@ -151,7 +148,14 @@ const EditObjectForm = ({ id, type, style, children }) => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Form form={form} layout='vertical' style={style}>
|
<Form
|
||||||
|
form={form}
|
||||||
|
layout='vertical'
|
||||||
|
style={style}
|
||||||
|
onValuesChange={(values) => {
|
||||||
|
setObjectData((prev) => ({ ...prev, ...values }))
|
||||||
|
}}
|
||||||
|
>
|
||||||
{contextHolder}
|
{contextHolder}
|
||||||
{children({
|
{children({
|
||||||
loading: fetchLoading,
|
loading: fetchLoading,
|
||||||
|
|||||||
@ -24,9 +24,9 @@ const EmailDisplay = ({ email, showCopy = true, showLink = false }) => {
|
|||||||
</Text>
|
</Text>
|
||||||
<Tooltip title='Email' arrow={false}>
|
<Tooltip title='Email' arrow={false}>
|
||||||
<Button
|
<Button
|
||||||
icon={<NewMailIcon style={{ fontSize: '14px' }} />}
|
icon={<NewMailIcon />}
|
||||||
type='text'
|
type='text'
|
||||||
style={{ height: '22px' }}
|
size='small'
|
||||||
onClick={(e) => {
|
onClick={(e) => {
|
||||||
e.preventDefault()
|
e.preventDefault()
|
||||||
window.location.href = `mailto:${email}`
|
window.location.href = `mailto:${email}`
|
||||||
@ -40,7 +40,6 @@ const EmailDisplay = ({ email, showCopy = true, showLink = false }) => {
|
|||||||
text={email}
|
text={email}
|
||||||
tooltip='Copy Email'
|
tooltip='Copy Email'
|
||||||
style={{ marginLeft: 0 }}
|
style={{ marginLeft: 0 }}
|
||||||
iconStyle={{ fontSize: '14px' }}
|
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
</Flex>
|
</Flex>
|
||||||
|
|||||||
@ -29,9 +29,8 @@ const IdDisplay = ({
|
|||||||
|
|
||||||
var hyperlink = null
|
var hyperlink = null
|
||||||
|
|
||||||
const defaultModelActions = model.actions.filter(
|
const defaultModelActions =
|
||||||
(action) => action.default == true
|
model.actions?.filter((action) => action.default == true) || []
|
||||||
)
|
|
||||||
|
|
||||||
if (defaultModelActions.length >= 1) {
|
if (defaultModelActions.length >= 1) {
|
||||||
hyperlink = defaultModelActions[0].url(id)
|
hyperlink = defaultModelActions[0].url(id)
|
||||||
|
|||||||
13
src/components/Dashboard/common/InfoCollapsePlaceholder.jsx
Normal file
13
src/components/Dashboard/common/InfoCollapsePlaceholder.jsx
Normal file
@ -0,0 +1,13 @@
|
|||||||
|
import React from 'react'
|
||||||
|
import { Card, Spin } from 'antd'
|
||||||
|
import { LoadingOutlined } from '@ant-design/icons'
|
||||||
|
|
||||||
|
const InfoCollapsePlaceholder = () => {
|
||||||
|
return (
|
||||||
|
<Spin indicator={<LoadingOutlined />}>
|
||||||
|
<Card style={{ minHeight: 260 }} />
|
||||||
|
</Spin>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export default InfoCollapsePlaceholder
|
||||||
84
src/components/Dashboard/common/NewObjectForm.jsx
Normal file
84
src/components/Dashboard/common/NewObjectForm.jsx
Normal file
@ -0,0 +1,84 @@
|
|||||||
|
import React, { useState, useEffect, useContext } from 'react'
|
||||||
|
import { Form, message } from 'antd'
|
||||||
|
import { ApiServerContext } from '../context/ApiServerContext'
|
||||||
|
import PropTypes from 'prop-types'
|
||||||
|
|
||||||
|
/**
|
||||||
|
* NewObjectForm is a reusable form component for creating new objects.
|
||||||
|
* It handles form validation, submission, and error handling logic.
|
||||||
|
*
|
||||||
|
* Props:
|
||||||
|
* - type: string (required)
|
||||||
|
* - formItems: array (for ObjectInfo/ObjectProperty items)
|
||||||
|
* - children: function({
|
||||||
|
* loading, isSubmitting, handleSubmit, form, formValid, objectData, setObjectData
|
||||||
|
* }) => ReactNode
|
||||||
|
*/
|
||||||
|
const NewObjectForm = ({ type, style, children }) => {
|
||||||
|
const [objectData, setObjectData] = useState({})
|
||||||
|
const [submitLoading, setSubmitLoading] = useState(false)
|
||||||
|
const [formValid, setFormValid] = useState(false)
|
||||||
|
const [form] = Form.useForm()
|
||||||
|
const formUpdateValues = Form.useWatch([], form)
|
||||||
|
const [messageApi, contextHolder] = message.useMessage()
|
||||||
|
const { createObjectInfo, showError } = useContext(ApiServerContext)
|
||||||
|
|
||||||
|
// Validate form on change
|
||||||
|
useEffect(() => {
|
||||||
|
form
|
||||||
|
.validateFields({ validateOnly: true })
|
||||||
|
.then(() => setFormValid(true))
|
||||||
|
.catch(() => setFormValid(false))
|
||||||
|
}, [form, formUpdateValues])
|
||||||
|
|
||||||
|
const handleSubmit = async () => {
|
||||||
|
try {
|
||||||
|
const values = await form.validateFields()
|
||||||
|
setSubmitLoading(true)
|
||||||
|
const newObject = await createObjectInfo(type, values)
|
||||||
|
messageApi.success('Object created successfully')
|
||||||
|
return newObject
|
||||||
|
} catch (err) {
|
||||||
|
if (err.errorFields) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
messageApi.error('Failed to create object')
|
||||||
|
showError(
|
||||||
|
`Failed to create object. Message: ${err.message}. Code: ${err.code}`,
|
||||||
|
() => handleSubmit()
|
||||||
|
)
|
||||||
|
} finally {
|
||||||
|
setSubmitLoading(false)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Form
|
||||||
|
form={form}
|
||||||
|
layout='vertical'
|
||||||
|
style={style}
|
||||||
|
onValuesChange={(values) => {
|
||||||
|
setObjectData((prev) => ({ ...prev, ...values }))
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{contextHolder}
|
||||||
|
{children({
|
||||||
|
loading: false,
|
||||||
|
isSubmitting: submitLoading,
|
||||||
|
handleSubmit,
|
||||||
|
form,
|
||||||
|
formValid,
|
||||||
|
objectData,
|
||||||
|
setObjectData
|
||||||
|
})}
|
||||||
|
</Form>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
NewObjectForm.propTypes = {
|
||||||
|
type: PropTypes.string.isRequired,
|
||||||
|
children: PropTypes.func.isRequired,
|
||||||
|
style: PropTypes.object
|
||||||
|
}
|
||||||
|
|
||||||
|
export default NewObjectForm
|
||||||
104
src/components/Dashboard/common/ObjectActions.jsx
Normal file
104
src/components/Dashboard/common/ObjectActions.jsx
Normal file
@ -0,0 +1,104 @@
|
|||||||
|
import React from 'react'
|
||||||
|
import { Dropdown, Button } from 'antd'
|
||||||
|
import { getModelByName } from '../../../database/ObjectModels'
|
||||||
|
import PropTypes from 'prop-types'
|
||||||
|
import { useNavigate, useLocation } from 'react-router-dom'
|
||||||
|
|
||||||
|
// Recursively map actions to AntD Dropdown items
|
||||||
|
function mapActionsToMenuItems(actions, currentUrlWithActions, id) {
|
||||||
|
return actions.map((action) => {
|
||||||
|
if (action.type === 'divider') {
|
||||||
|
return { type: 'divider' }
|
||||||
|
}
|
||||||
|
const actionUrl = action.url ? action.url(id) : undefined
|
||||||
|
const item = {
|
||||||
|
key: action.key || action.name,
|
||||||
|
label: action.label,
|
||||||
|
icon: action.icon ? React.createElement(action.icon) : undefined,
|
||||||
|
disabled: actionUrl && actionUrl === currentUrlWithActions
|
||||||
|
}
|
||||||
|
if (action.children && Array.isArray(action.children)) {
|
||||||
|
item.children = mapActionsToMenuItems(
|
||||||
|
action.children,
|
||||||
|
currentUrlWithActions,
|
||||||
|
id
|
||||||
|
)
|
||||||
|
}
|
||||||
|
return item
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
const stripActionParam = (pathname, search) => {
|
||||||
|
const params = new URLSearchParams(search)
|
||||||
|
params.delete('action')
|
||||||
|
const query = params.toString()
|
||||||
|
return pathname + (query ? `?${query}` : '')
|
||||||
|
}
|
||||||
|
|
||||||
|
const ObjectActions = ({
|
||||||
|
type,
|
||||||
|
id,
|
||||||
|
disabled = false,
|
||||||
|
buttonProps = {},
|
||||||
|
...dropdownProps
|
||||||
|
}) => {
|
||||||
|
const model = getModelByName(type)
|
||||||
|
const actions = model.actions || []
|
||||||
|
const navigate = useNavigate()
|
||||||
|
const location = useLocation()
|
||||||
|
|
||||||
|
// Get current url without 'action' param
|
||||||
|
const currentUrlWithoutActions = stripActionParam(
|
||||||
|
location.pathname,
|
||||||
|
location.search
|
||||||
|
)
|
||||||
|
|
||||||
|
console.log('curr url', currentUrlWithoutActions)
|
||||||
|
|
||||||
|
// Filter out actions whose url matches currentUrl
|
||||||
|
const filteredActions = actions.filter(
|
||||||
|
(action) => !(action.url(id) && action.url(id) === currentUrlWithoutActions)
|
||||||
|
)
|
||||||
|
|
||||||
|
const currentUrlWithActions = location.pathname + location.search
|
||||||
|
|
||||||
|
// Compose AntD Dropdown menu items
|
||||||
|
const menu = {
|
||||||
|
items: mapActionsToMenuItems(filteredActions, currentUrlWithActions, id),
|
||||||
|
onClick: (info) => {
|
||||||
|
// Find the action by key
|
||||||
|
const findAction = (acts, key) => {
|
||||||
|
for (const act of acts) {
|
||||||
|
if ((act.key || act.name) === key) return act
|
||||||
|
if (act.children) {
|
||||||
|
const found = findAction(act.children, key)
|
||||||
|
if (found) return found
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
const action = findAction(filteredActions, info.key)
|
||||||
|
if (action && action.url) {
|
||||||
|
navigate(action.url(id))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Dropdown menu={menu} {...dropdownProps}>
|
||||||
|
<Button {...buttonProps} disabled={disabled}>
|
||||||
|
Actions
|
||||||
|
</Button>
|
||||||
|
</Dropdown>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
ObjectActions.propTypes = {
|
||||||
|
type: PropTypes.string.isRequired,
|
||||||
|
id: PropTypes.string.isRequired,
|
||||||
|
disabled: PropTypes.bool,
|
||||||
|
buttonProps: PropTypes.object,
|
||||||
|
buttonLabel: PropTypes.string
|
||||||
|
}
|
||||||
|
|
||||||
|
export default ObjectActions
|
||||||
@ -1,27 +1,37 @@
|
|||||||
import React from 'react'
|
import React from 'react'
|
||||||
import { Spin, Descriptions } from 'antd'
|
import { Spin, Descriptions } from 'antd'
|
||||||
|
import { LoadingOutlined } from '@ant-design/icons'
|
||||||
import PropTypes from 'prop-types'
|
import PropTypes from 'prop-types'
|
||||||
import ObjectProperty from './ObjectProperty'
|
import ObjectProperty from './ObjectProperty'
|
||||||
|
import { getModelProperties } from '../../../database/ObjectModels'
|
||||||
|
|
||||||
const ObjectInfo = ({
|
const ObjectInfo = ({
|
||||||
loading = false,
|
loading = false,
|
||||||
indicator = null,
|
|
||||||
bordered = true,
|
bordered = true,
|
||||||
isEditing = false,
|
isEditing = false,
|
||||||
items = []
|
type = 'unknown',
|
||||||
|
objectData = null
|
||||||
}) => {
|
}) => {
|
||||||
|
const items = getModelProperties(type)
|
||||||
|
|
||||||
// Map items to Descriptions 'items' prop format
|
// Map items to Descriptions 'items' prop format
|
||||||
const descriptionItems = items.map((item, idx) => {
|
const descriptionItems = items.map((item, idx) => {
|
||||||
const key = item.name || item.label || idx
|
const key = item.name || item.label || idx
|
||||||
return {
|
return {
|
||||||
key,
|
key,
|
||||||
label: item.label,
|
label: item.label,
|
||||||
children: <ObjectProperty {...item} isEditing={isEditing} />
|
children: (
|
||||||
|
<ObjectProperty
|
||||||
|
{...item}
|
||||||
|
isEditing={isEditing}
|
||||||
|
objectData={objectData}
|
||||||
|
/>
|
||||||
|
)
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Spin spinning={loading} indicator={indicator}>
|
<Spin spinning={loading} indicator={<LoadingOutlined />}>
|
||||||
<Descriptions
|
<Descriptions
|
||||||
bordered={bordered}
|
bordered={bordered}
|
||||||
column={{
|
column={{
|
||||||
@ -44,7 +54,8 @@ ObjectInfo.propTypes = {
|
|||||||
bordered: PropTypes.bool,
|
bordered: PropTypes.bool,
|
||||||
items: PropTypes.arrayOf(PropTypes.object),
|
items: PropTypes.arrayOf(PropTypes.object),
|
||||||
isEditing: PropTypes.bool,
|
isEditing: PropTypes.bool,
|
||||||
type: PropTypes.string.isRequired
|
type: PropTypes.string.isRequired,
|
||||||
|
objectData: PropTypes.object
|
||||||
}
|
}
|
||||||
|
|
||||||
export default ObjectInfo
|
export default ObjectInfo
|
||||||
|
|||||||
@ -33,6 +33,8 @@ import SecretDisplay from './SecretDisplay'
|
|||||||
import EyeIcon from '../../Icons/EyeIcon'
|
import EyeIcon from '../../Icons/EyeIcon'
|
||||||
import EyeSlashIcon from '../../Icons/EyeSlashIcon'
|
import EyeSlashIcon from '../../Icons/EyeSlashIcon'
|
||||||
import FilamentStockState from './FilamentStockState'
|
import FilamentStockState from './FilamentStockState'
|
||||||
|
import { getPropertyValue } from '../../../database/ObjectModels'
|
||||||
|
import PropertyChanges from './PropertyChanges'
|
||||||
|
|
||||||
const { Text } = Typography
|
const { Text } = Typography
|
||||||
|
|
||||||
@ -47,17 +49,40 @@ const MATERIAL_OPTIONS = [
|
|||||||
|
|
||||||
const ObjectProperty = ({
|
const ObjectProperty = ({
|
||||||
type = 'text',
|
type = 'text',
|
||||||
|
prefix,
|
||||||
|
suffix,
|
||||||
value,
|
value,
|
||||||
|
min,
|
||||||
|
max,
|
||||||
|
step,
|
||||||
isEditing = false,
|
isEditing = false,
|
||||||
formItemProps = {},
|
formItemProps = {},
|
||||||
required = false,
|
required = false,
|
||||||
name,
|
name,
|
||||||
label,
|
label,
|
||||||
showLabel = false,
|
showLabel = false,
|
||||||
|
objectData = null,
|
||||||
objectType = 'unknown',
|
objectType = 'unknown',
|
||||||
readOnly = false,
|
readOnly = false,
|
||||||
|
disabled = false,
|
||||||
...rest
|
...rest
|
||||||
}) => {
|
}) => {
|
||||||
|
if (typeof value == 'function' && objectData) {
|
||||||
|
value = disabled(objectData)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (objectType && typeof objectType == 'function' && objectData) {
|
||||||
|
objectType = objectType(objectData)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (disabled && typeof disabled == 'function' && objectData) {
|
||||||
|
disabled = disabled(objectData)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!value) {
|
||||||
|
value = getPropertyValue(objectData, name)
|
||||||
|
}
|
||||||
|
|
||||||
// Split the name by "." to handle nested object properties
|
// Split the name by "." to handle nested object properties
|
||||||
var formItemName = name
|
var formItemName = name
|
||||||
|
|
||||||
@ -65,6 +90,12 @@ const ObjectProperty = ({
|
|||||||
formItemName = name ? name.split('.') : undefined
|
formItemName = name ? name.split('.') : undefined
|
||||||
}
|
}
|
||||||
|
|
||||||
|
var textParams = {}
|
||||||
|
|
||||||
|
if (disabled == true) {
|
||||||
|
textParams = { ...textParams, delete: true, type: 'secondary' }
|
||||||
|
}
|
||||||
|
|
||||||
const renderProperty = () => {
|
const renderProperty = () => {
|
||||||
if (!isEditing || readOnly) {
|
if (!isEditing || readOnly) {
|
||||||
switch (type) {
|
switch (type) {
|
||||||
@ -72,93 +103,154 @@ const ObjectProperty = ({
|
|||||||
if (value != null) {
|
if (value != null) {
|
||||||
return <SecretDisplay value={value} {...rest} />
|
return <SecretDisplay value={value} {...rest} />
|
||||||
} else {
|
} else {
|
||||||
return <Text type='secondary'>n/a</Text>
|
return (
|
||||||
|
<Text type='secondary' {...textParams}>
|
||||||
|
n/a
|
||||||
|
</Text>
|
||||||
|
)
|
||||||
}
|
}
|
||||||
case 'wsprotocol':
|
case 'wsprotocol':
|
||||||
switch (value) {
|
switch (value) {
|
||||||
case 'ws':
|
case 'ws':
|
||||||
return <Text>Websocket</Text>
|
return <Text {...textParams}>Websocket</Text>
|
||||||
case 'wss':
|
case 'wss':
|
||||||
return <Text>Websocket Secure</Text>
|
return <Text {...textParams}>Websocket Secure</Text>
|
||||||
default:
|
default:
|
||||||
return <Text type='secondary'>n/a</Text>
|
return (
|
||||||
|
<Text type='secondary' {...textParams}>
|
||||||
|
n/a
|
||||||
|
</Text>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
case 'priceMode':
|
||||||
|
switch (value) {
|
||||||
|
case 'margin':
|
||||||
|
return <Text {...textParams}>Margin %</Text>
|
||||||
|
case 'amount':
|
||||||
|
return <Text {...textParams}>£ Amount</Text>
|
||||||
|
default:
|
||||||
|
return (
|
||||||
|
<Text type='secondary' {...textParams}>
|
||||||
|
n/a
|
||||||
|
</Text>
|
||||||
|
)
|
||||||
}
|
}
|
||||||
case 'bool': {
|
case 'bool': {
|
||||||
if (value != null) {
|
if (value != null) {
|
||||||
return <BoolDisplay value={value} yesNo={true} {...rest} />
|
return <BoolDisplay value={value} yesNo={true} {...rest} />
|
||||||
} else {
|
} else {
|
||||||
return <Text type='secondary'>n/a</Text>
|
return (
|
||||||
|
<Text type='secondary' {...textParams}>
|
||||||
|
n/a
|
||||||
|
</Text>
|
||||||
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
case 'dateTime': {
|
case 'dateTime': {
|
||||||
if (value != null) {
|
if (value != null) {
|
||||||
return <TimeDisplay dateTime={value} {...rest} />
|
return <TimeDisplay dateTime={value} {...rest} />
|
||||||
} else {
|
} else {
|
||||||
return <Text type='secondary'>n/a</Text>
|
return (
|
||||||
}
|
<Text type='secondary' {...textParams}>
|
||||||
}
|
n/a
|
||||||
case 'currency': {
|
</Text>
|
||||||
if (value != null) {
|
)
|
||||||
return <Text>{`£${value}/kg`}</Text>
|
|
||||||
} else {
|
|
||||||
return <Text type='secondary'>n/a</Text>
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
case 'country': {
|
case 'country': {
|
||||||
if (value != null) {
|
if (value != null) {
|
||||||
return <CountryDisplay countryCode={value} />
|
return <CountryDisplay countryCode={value} />
|
||||||
} else {
|
} else {
|
||||||
return <Text type='secondary'>n/a</Text>
|
return (
|
||||||
|
<Text type='secondary' {...textParams}>
|
||||||
|
n/a
|
||||||
|
</Text>
|
||||||
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
case 'color': {
|
case 'color': {
|
||||||
if (value) {
|
if (value) {
|
||||||
return <Badge color={value} text={value} />
|
return <Badge color={value} text={value} />
|
||||||
} else {
|
} else {
|
||||||
return <Text type='secondary'>n/a</Text>
|
return (
|
||||||
}
|
<Text type='secondary' {...textParams}>
|
||||||
}
|
n/a
|
||||||
case 'weight': {
|
</Text>
|
||||||
if (value != null) {
|
)
|
||||||
return <Text>{`${value}g`}</Text>
|
|
||||||
} else {
|
|
||||||
return <Text type='secondary'>n/a</Text>
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
case 'number': {
|
case 'number': {
|
||||||
if (value != null) {
|
if (value != null) {
|
||||||
if (Array.isArray(value)) {
|
if (Array.isArray(value)) {
|
||||||
return <Text>{value.length}</Text>
|
return (
|
||||||
|
<Text {...textParams}>
|
||||||
|
{prefix}
|
||||||
|
{value.length}
|
||||||
|
{suffix}
|
||||||
|
</Text>
|
||||||
|
)
|
||||||
} else {
|
} else {
|
||||||
return <Text>{value}</Text>
|
return (
|
||||||
|
<Text {...textParams}>
|
||||||
|
{prefix}
|
||||||
|
{value}
|
||||||
|
{suffix}
|
||||||
|
</Text>
|
||||||
|
)
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
return <Text type='secondary'>n/a</Text>
|
return (
|
||||||
|
<Text type='secondary' {...textParams}>
|
||||||
|
n/a
|
||||||
|
</Text>
|
||||||
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
case 'text':
|
case 'text':
|
||||||
if (value != null && value != '') {
|
if (value != null && value != '') {
|
||||||
return <Text ellipsis>{value}</Text>
|
return (
|
||||||
|
<Text ellipsis>
|
||||||
|
{prefix}
|
||||||
|
{value}
|
||||||
|
{suffix}
|
||||||
|
</Text>
|
||||||
|
)
|
||||||
} else {
|
} else {
|
||||||
return <Text type='secondary'>n/a</Text>
|
return (
|
||||||
|
<Text type='secondary' {...textParams}>
|
||||||
|
n/a
|
||||||
|
</Text>
|
||||||
|
)
|
||||||
}
|
}
|
||||||
case 'email':
|
case 'email':
|
||||||
if (value != null && value != '') {
|
if (value != null && value != '') {
|
||||||
return <EmailDisplay email={value} />
|
return <EmailDisplay email={value} />
|
||||||
} else {
|
} else {
|
||||||
return <Text type='secondary'>n/a</Text>
|
return (
|
||||||
|
<Text type='secondary' {...textParams}>
|
||||||
|
n/a
|
||||||
|
</Text>
|
||||||
|
)
|
||||||
}
|
}
|
||||||
case 'url':
|
case 'url':
|
||||||
if (value != null && value != '') {
|
if (value != null && value != '') {
|
||||||
return <UrlDisplay url={value} />
|
return <UrlDisplay url={value} />
|
||||||
} else {
|
} else {
|
||||||
return <Text type='secondary'>n/a</Text>
|
return (
|
||||||
|
<Text type='secondary' {...textParams}>
|
||||||
|
n/a
|
||||||
|
</Text>
|
||||||
|
)
|
||||||
}
|
}
|
||||||
case 'object': {
|
case 'object': {
|
||||||
if (value && value.name) {
|
if (value && value.name) {
|
||||||
return <Text ellipsis>{value.name}</Text>
|
return <Text ellipsis>{value.name}</Text>
|
||||||
} else {
|
} else {
|
||||||
return <Text type='secondary'>n/a</Text>
|
return (
|
||||||
|
<Text type='secondary' {...textParams}>
|
||||||
|
n/a
|
||||||
|
</Text>
|
||||||
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
case 'state': {
|
case 'state': {
|
||||||
@ -173,59 +265,87 @@ const ObjectProperty = ({
|
|||||||
case 'filamentStock':
|
case 'filamentStock':
|
||||||
return <FilamentStockState state={value} {...rest} />
|
return <FilamentStockState state={value} {...rest} />
|
||||||
default:
|
default:
|
||||||
return <Text type='secondary'>No Object Type Specified</Text>
|
return (
|
||||||
|
<Text type='secondary' {...textParams}>
|
||||||
|
No Object Type Specified
|
||||||
|
</Text>
|
||||||
|
)
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
return <Text type='secondary'>n/a</Text>
|
return (
|
||||||
|
<Text type='secondary' {...textParams}>
|
||||||
|
n/a
|
||||||
|
</Text>
|
||||||
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
case 'material': {
|
case 'material': {
|
||||||
if (value) {
|
if (value) {
|
||||||
return <Text>{value}</Text>
|
return <Text {...textParams}>{value}</Text>
|
||||||
} else {
|
} else {
|
||||||
return <Text type='secondary'>n/a</Text>
|
return (
|
||||||
|
<Text type='secondary' {...textParams}>
|
||||||
|
n/a
|
||||||
|
</Text>
|
||||||
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
case 'id': {
|
case 'id': {
|
||||||
if (value) {
|
if (value) {
|
||||||
return <IdDisplay id={value} type={objectType} {...rest} />
|
return <IdDisplay id={value} type={objectType} {...rest} />
|
||||||
} else {
|
} else {
|
||||||
return <Text type='secondary'>n/a</Text>
|
return (
|
||||||
|
<Text type='secondary' {...textParams}>
|
||||||
|
n/a
|
||||||
|
</Text>
|
||||||
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
case 'density': {
|
case 'density': {
|
||||||
if (value != null) {
|
if (value != null) {
|
||||||
return <Text>{`${value} g/cm³`}</Text>
|
return <Text {...textParams}>{`${value} g/cm³`}</Text>
|
||||||
} else {
|
} else {
|
||||||
return <Text type='secondary'>n/a</Text>
|
return (
|
||||||
|
<Text type='secondary' {...textParams}>
|
||||||
|
n/a
|
||||||
|
</Text>
|
||||||
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
case 'mm': {
|
case 'mm': {
|
||||||
if (value != null) {
|
if (value != null) {
|
||||||
return <Text>{`${value} mm`}</Text>
|
return <Text {...textParams}>{`${value} mm`}</Text>
|
||||||
} else {
|
} else {
|
||||||
return <Text type='secondary'>n/a</Text>
|
return (
|
||||||
|
<Text type='secondary' {...textParams}>
|
||||||
|
n/a
|
||||||
|
</Text>
|
||||||
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
case 'tags': {
|
case 'tags': {
|
||||||
if (value != null || value?.length != 0) {
|
if (value != null || value?.length != 0) {
|
||||||
return <TagsDisplay tags={value} />
|
return <TagsDisplay tags={value} />
|
||||||
} else {
|
} else {
|
||||||
return <Text type='secondary'>n/a</Text>
|
return (
|
||||||
|
<Text type='secondary' {...textParams}>
|
||||||
|
n/a
|
||||||
|
</Text>
|
||||||
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
case 'version': {
|
case 'propertyChanges': {
|
||||||
if (value != null) {
|
return <PropertyChanges type={objectType} value={value} />
|
||||||
return <Text>{`${value} mm`}</Text>
|
|
||||||
} else {
|
|
||||||
return <Text type='secondary'>n/a</Text>
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
default: {
|
default: {
|
||||||
if (value) {
|
if (value) {
|
||||||
return <Text>{value}</Text>
|
return <Text {...textParams}>{value}</Text>
|
||||||
} else {
|
} else {
|
||||||
return <Text type='secondary'>n/a</Text>
|
return (
|
||||||
|
<Text type='secondary' {...textParams}>
|
||||||
|
n/a
|
||||||
|
</Text>
|
||||||
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -266,6 +386,7 @@ const ObjectProperty = ({
|
|||||||
<Form.Item name={formItemName} {...mergedFormItemProps}>
|
<Form.Item name={formItemName} {...mergedFormItemProps}>
|
||||||
<Input.Password
|
<Input.Password
|
||||||
placeholder={label}
|
placeholder={label}
|
||||||
|
disabled={disabled}
|
||||||
{...mergedFormItemProps}
|
{...mergedFormItemProps}
|
||||||
iconRender={(visible) =>
|
iconRender={(visible) =>
|
||||||
visible ? <EyeSlashIcon /> : <EyeIcon />
|
visible ? <EyeSlashIcon /> : <EyeIcon />
|
||||||
@ -278,6 +399,7 @@ const ObjectProperty = ({
|
|||||||
<Form.Item name={formItemName} {...mergedFormItemProps}>
|
<Form.Item name={formItemName} {...mergedFormItemProps}>
|
||||||
<Select
|
<Select
|
||||||
defaultValue='ws'
|
defaultValue='ws'
|
||||||
|
disabled={disabled}
|
||||||
options={[
|
options={[
|
||||||
{ value: 'ws', label: 'Websocket' },
|
{ value: 'ws', label: 'Websocket' },
|
||||||
{ value: 'wss', label: 'Websocket Secure' }
|
{ value: 'wss', label: 'Websocket Secure' }
|
||||||
@ -285,6 +407,19 @@ const ObjectProperty = ({
|
|||||||
/>
|
/>
|
||||||
</Form.Item>
|
</Form.Item>
|
||||||
)
|
)
|
||||||
|
case 'priceMode':
|
||||||
|
return (
|
||||||
|
<Form.Item name={formItemName} {...mergedFormItemProps}>
|
||||||
|
<Select
|
||||||
|
defaultValue='margin'
|
||||||
|
disabled={disabled}
|
||||||
|
options={[
|
||||||
|
{ value: 'margin', label: 'Margin %' },
|
||||||
|
{ value: 'amount', label: '£ Amount' }
|
||||||
|
]}
|
||||||
|
/>
|
||||||
|
</Form.Item>
|
||||||
|
)
|
||||||
case 'bool':
|
case 'bool':
|
||||||
return (
|
return (
|
||||||
<Form.Item
|
<Form.Item
|
||||||
@ -292,7 +427,7 @@ const ObjectProperty = ({
|
|||||||
{...mergedFormItemProps}
|
{...mergedFormItemProps}
|
||||||
valuePropName='checked'
|
valuePropName='checked'
|
||||||
>
|
>
|
||||||
<Switch />
|
<Switch disabled={disabled} />
|
||||||
</Form.Item>
|
</Form.Item>
|
||||||
)
|
)
|
||||||
case 'dateTime':
|
case 'dateTime':
|
||||||
@ -302,24 +437,17 @@ const ObjectProperty = ({
|
|||||||
{...mergedFormItemProps}
|
{...mergedFormItemProps}
|
||||||
getValueProps={(v) => ({ value: v ? dayjs(v) : null })}
|
getValueProps={(v) => ({ value: v ? dayjs(v) : null })}
|
||||||
>
|
>
|
||||||
<DatePicker showTime style={{ width: '100%' }} />
|
<DatePicker
|
||||||
</Form.Item>
|
showTime
|
||||||
)
|
|
||||||
case 'currency':
|
|
||||||
return (
|
|
||||||
<Form.Item name={formItemName} {...mergedFormItemProps}>
|
|
||||||
<InputNumber
|
|
||||||
prefix='£'
|
|
||||||
suffix='/kg'
|
|
||||||
style={{ width: '100%' }}
|
style={{ width: '100%' }}
|
||||||
placeholder={label}
|
disabled={disabled}
|
||||||
/>
|
/>
|
||||||
</Form.Item>
|
</Form.Item>
|
||||||
)
|
)
|
||||||
case 'country':
|
case 'country':
|
||||||
return (
|
return (
|
||||||
<Form.Item name={formItemName} {...mergedFormItemProps}>
|
<Form.Item name={formItemName} {...mergedFormItemProps}>
|
||||||
<CountrySelect />
|
<CountrySelect disabled={disabled} />
|
||||||
</Form.Item>
|
</Form.Item>
|
||||||
)
|
)
|
||||||
case 'color':
|
case 'color':
|
||||||
@ -330,7 +458,7 @@ const ObjectProperty = ({
|
|||||||
valuePropName='value'
|
valuePropName='value'
|
||||||
getValueFromEvent={(v) => v}
|
getValueFromEvent={(v) => v}
|
||||||
>
|
>
|
||||||
<ColorSelector required={required} />
|
<ColorSelector required={required} disabled={disabled} />
|
||||||
</Form.Item>
|
</Form.Item>
|
||||||
)
|
)
|
||||||
case 'weight':
|
case 'weight':
|
||||||
@ -340,6 +468,7 @@ const ObjectProperty = ({
|
|||||||
suffix='g'
|
suffix='g'
|
||||||
style={{ width: '100%' }}
|
style={{ width: '100%' }}
|
||||||
placeholder={label}
|
placeholder={label}
|
||||||
|
disabled={disabled}
|
||||||
/>
|
/>
|
||||||
</Form.Item>
|
</Form.Item>
|
||||||
)
|
)
|
||||||
@ -347,22 +476,36 @@ const ObjectProperty = ({
|
|||||||
return (
|
return (
|
||||||
<Form.Item name={formItemName} {...mergedFormItemProps}>
|
<Form.Item name={formItemName} {...mergedFormItemProps}>
|
||||||
<InputNumber
|
<InputNumber
|
||||||
style={{ width: '100%' }}
|
|
||||||
placeholder={label}
|
placeholder={label}
|
||||||
|
disabled={disabled}
|
||||||
|
prefix={prefix}
|
||||||
|
suffix={suffix}
|
||||||
|
min={min}
|
||||||
|
max={max}
|
||||||
|
step={step}
|
||||||
{...mergedFormItemProps}
|
{...mergedFormItemProps}
|
||||||
|
style={{ width: '100%' }}
|
||||||
/>
|
/>
|
||||||
</Form.Item>
|
</Form.Item>
|
||||||
)
|
)
|
||||||
case 'text':
|
case 'text':
|
||||||
return (
|
return (
|
||||||
<Form.Item name={formItemName} {...mergedFormItemProps}>
|
<Form.Item name={formItemName} {...mergedFormItemProps}>
|
||||||
<Input placeholder={label} {...mergedFormItemProps} />
|
<Input
|
||||||
|
placeholder={label}
|
||||||
|
{...mergedFormItemProps}
|
||||||
|
disabled={disabled}
|
||||||
|
/>
|
||||||
</Form.Item>
|
</Form.Item>
|
||||||
)
|
)
|
||||||
case 'material':
|
case 'material':
|
||||||
return (
|
return (
|
||||||
<Form.Item name={formItemName} {...mergedFormItemProps}>
|
<Form.Item name={formItemName} {...mergedFormItemProps}>
|
||||||
<Select options={MATERIAL_OPTIONS} placeholder={label} />
|
<Select
|
||||||
|
options={MATERIAL_OPTIONS}
|
||||||
|
placeholder={label}
|
||||||
|
disabled={disabled}
|
||||||
|
/>
|
||||||
</Form.Item>
|
</Form.Item>
|
||||||
)
|
)
|
||||||
case 'id':
|
case 'id':
|
||||||
@ -370,7 +513,11 @@ const ObjectProperty = ({
|
|||||||
if (value) {
|
if (value) {
|
||||||
return <IdDisplay id={value} type={objectType} {...rest} />
|
return <IdDisplay id={value} type={objectType} {...rest} />
|
||||||
} else {
|
} else {
|
||||||
return <Text type='secondary'>n/a</Text>
|
return (
|
||||||
|
<Text type='secondary' {...textParams}>
|
||||||
|
n/a
|
||||||
|
</Text>
|
||||||
|
)
|
||||||
}
|
}
|
||||||
case 'object':
|
case 'object':
|
||||||
switch (objectType) {
|
switch (objectType) {
|
||||||
@ -405,7 +552,11 @@ const ObjectProperty = ({
|
|||||||
</Form.Item>
|
</Form.Item>
|
||||||
)
|
)
|
||||||
default:
|
default:
|
||||||
return <Text type='secondary'>n/a</Text>
|
return (
|
||||||
|
<Text type='secondary' {...textParams}>
|
||||||
|
n/a
|
||||||
|
</Text>
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
case 'density':
|
case 'density':
|
||||||
@ -455,10 +606,15 @@ ObjectProperty.propTypes = {
|
|||||||
formItemProps: PropTypes.object,
|
formItemProps: PropTypes.object,
|
||||||
required: PropTypes.bool,
|
required: PropTypes.bool,
|
||||||
name: PropTypes.string,
|
name: PropTypes.string,
|
||||||
label: PropTypes.string,
|
prefix: PropTypes.string,
|
||||||
|
suffix: PropTypes.string,
|
||||||
|
min: PropTypes.number,
|
||||||
|
max: PropTypes.number,
|
||||||
|
step: PropTypes.number,
|
||||||
showLabel: PropTypes.bool,
|
showLabel: PropTypes.bool,
|
||||||
objectType: PropTypes.string,
|
objectType: PropTypes.string,
|
||||||
readOnly: PropTypes.bool
|
readOnly: PropTypes.bool,
|
||||||
|
disabled: PropTypes.bool
|
||||||
}
|
}
|
||||||
|
|
||||||
export default ObjectProperty
|
export default ObjectProperty
|
||||||
|
|||||||
@ -38,6 +38,7 @@ import XMarkIcon from '../../Icons/XMarkIcon'
|
|||||||
import CheckIcon from '../../Icons/CheckIcon'
|
import CheckIcon from '../../Icons/CheckIcon'
|
||||||
import { useNavigate } from 'react-router-dom'
|
import { useNavigate } from 'react-router-dom'
|
||||||
import QuestionCircleIcon from '../../Icons/QuestionCircleIcon'
|
import QuestionCircleIcon from '../../Icons/QuestionCircleIcon'
|
||||||
|
import { AuthContext } from '../context/AuthContext'
|
||||||
const logger = loglevel.getLogger('DasboardTable')
|
const logger = loglevel.getLogger('DasboardTable')
|
||||||
logger.setLevel(config.logLevel)
|
logger.setLevel(config.logLevel)
|
||||||
|
|
||||||
@ -49,13 +50,14 @@ const ObjectTable = forwardRef(
|
|||||||
pageSize = 25,
|
pageSize = 25,
|
||||||
scrollHeight = 'calc(var(--unit-100vh) - 270px)',
|
scrollHeight = 'calc(var(--unit-100vh) - 270px)',
|
||||||
onDataChange,
|
onDataChange,
|
||||||
authenticated,
|
|
||||||
initialPage = 1,
|
initialPage = 1,
|
||||||
cards = false,
|
cards = false,
|
||||||
visibleColumns = {}
|
visibleColumns = {},
|
||||||
|
masterFilter = {}
|
||||||
},
|
},
|
||||||
ref
|
ref
|
||||||
) => {
|
) => {
|
||||||
|
const { authenticated } = useContext(AuthContext)
|
||||||
const { fetchTableData } = useContext(ApiServerContext)
|
const { fetchTableData } = useContext(ApiServerContext)
|
||||||
const isMobile = useMediaQuery({ maxWidth: 768 })
|
const isMobile = useMediaQuery({ maxWidth: 768 })
|
||||||
const navigate = useNavigate()
|
const navigate = useNavigate()
|
||||||
@ -107,7 +109,7 @@ const ObjectTable = forwardRef(
|
|||||||
const result = await fetchTableData(type, {
|
const result = await fetchTableData(type, {
|
||||||
page: pageNum,
|
page: pageNum,
|
||||||
limit: pageSize,
|
limit: pageSize,
|
||||||
filter,
|
filter: { ...filter, ...masterFilter },
|
||||||
sorter,
|
sorter,
|
||||||
onDataChange
|
onDataChange
|
||||||
})
|
})
|
||||||
@ -408,17 +410,15 @@ const ObjectTable = forwardRef(
|
|||||||
<ObjectProperty
|
<ObjectProperty
|
||||||
{...prop}
|
{...prop}
|
||||||
longId={false}
|
longId={false}
|
||||||
type={prop.type}
|
objectData={record}
|
||||||
objectType={prop.objectType}
|
|
||||||
value={getPropertyValue(record, prop.name)}
|
|
||||||
isEditing={false}
|
isEditing={false}
|
||||||
/>
|
/>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Add filter configuration if the property is filterable
|
// Add filter configuration if the property is filterable and not in masterFilter
|
||||||
if (isFilterable) {
|
if (isFilterable && !Object.keys(masterFilter).includes(prop.name)) {
|
||||||
columnConfig.filterDropdown = ({
|
columnConfig.filterDropdown = ({
|
||||||
setSelectedKeys,
|
setSelectedKeys,
|
||||||
selectedKeys,
|
selectedKeys,
|
||||||
@ -606,7 +606,8 @@ ObjectTable.propTypes = {
|
|||||||
initialPage: PropTypes.number,
|
initialPage: PropTypes.number,
|
||||||
cards: PropTypes.bool,
|
cards: PropTypes.bool,
|
||||||
cardRenderer: PropTypes.func,
|
cardRenderer: PropTypes.func,
|
||||||
visibleColumns: PropTypes.object
|
visibleColumns: PropTypes.object,
|
||||||
|
masterFilter: PropTypes.object
|
||||||
}
|
}
|
||||||
|
|
||||||
export default ObjectTable
|
export default ObjectTable
|
||||||
|
|||||||
62
src/components/Dashboard/common/PropertyChanges.jsx
Normal file
62
src/components/Dashboard/common/PropertyChanges.jsx
Normal file
@ -0,0 +1,62 @@
|
|||||||
|
import React from 'react'
|
||||||
|
import PropTypes from 'prop-types'
|
||||||
|
import { Descriptions, Typography, Space } from 'antd'
|
||||||
|
import { getModelProperty } from '../../../database/ObjectModels'
|
||||||
|
import ObjectProperty from './ObjectProperty'
|
||||||
|
import ArrowRightIcon from '../../Icons/ArrowRightIcon'
|
||||||
|
|
||||||
|
const { Text } = Typography
|
||||||
|
|
||||||
|
const PropertyChanges = ({ type, value }) => {
|
||||||
|
if (!value || !value.new) {
|
||||||
|
return <Text type='secondary'>n/a</Text>
|
||||||
|
}
|
||||||
|
console.log('combined', { ...value?.old, ...value?.new })
|
||||||
|
return (
|
||||||
|
<Descriptions size='small' column={1}>
|
||||||
|
{Object.keys({ ...value?.old, ...value?.new }).map((key) => {
|
||||||
|
console.log('tc', type, key)
|
||||||
|
var changeProperty = getModelProperty(type, key)
|
||||||
|
console.log('change prop', changeProperty)
|
||||||
|
|
||||||
|
if (changeProperty?.type == 'object') {
|
||||||
|
changeProperty = {
|
||||||
|
...changeProperty,
|
||||||
|
name: changeProperty.name + '._id',
|
||||||
|
type: 'id',
|
||||||
|
showHyperlink: true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return (
|
||||||
|
<Descriptions.Item key={key} label={changeProperty.label}>
|
||||||
|
<Space>
|
||||||
|
<ObjectProperty
|
||||||
|
{...changeProperty}
|
||||||
|
longId={false}
|
||||||
|
objectData={value?.old}
|
||||||
|
/>
|
||||||
|
<Text type='secondary'>
|
||||||
|
<ArrowRightIcon />
|
||||||
|
</Text>
|
||||||
|
<ObjectProperty
|
||||||
|
{...changeProperty}
|
||||||
|
longId={false}
|
||||||
|
objectData={value?.new}
|
||||||
|
/>
|
||||||
|
</Space>
|
||||||
|
</Descriptions.Item>
|
||||||
|
)
|
||||||
|
})}
|
||||||
|
</Descriptions>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
PropertyChanges.propTypes = {
|
||||||
|
type: PropTypes.string.isRequired,
|
||||||
|
value: PropTypes.shape({
|
||||||
|
old: PropTypes.object,
|
||||||
|
new: PropTypes.object
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
export default PropertyChanges
|
||||||
@ -23,12 +23,14 @@ const UrlDisplay = ({ url, showCopy = true, showLink = false }) => {
|
|||||||
</Link>
|
</Link>
|
||||||
) : (
|
) : (
|
||||||
<>
|
<>
|
||||||
<Text style={{ marginRight: 8 }}>{url}</Text>
|
<Text style={{ marginRight: 8 }} ellipsis>
|
||||||
|
{url}
|
||||||
|
</Text>
|
||||||
<Tooltip title='Open URL' arrow={false}>
|
<Tooltip title='Open URL' arrow={false}>
|
||||||
<Button
|
<Button
|
||||||
icon={<LinkIcon style={{ fontSize: '14px' }} />}
|
icon={<LinkIcon />}
|
||||||
type='text'
|
type='text'
|
||||||
style={{ height: '22px' }}
|
size='small'
|
||||||
onClick={(e) => {
|
onClick={(e) => {
|
||||||
e.preventDefault()
|
e.preventDefault()
|
||||||
window.open(url, '_blank', 'noopener,noreferrer')
|
window.open(url, '_blank', 'noopener,noreferrer')
|
||||||
|
|||||||
@ -3,10 +3,10 @@ import { Button, Popover, Checkbox, Flex } from 'antd'
|
|||||||
import PropTypes from 'prop-types'
|
import PropTypes from 'prop-types'
|
||||||
|
|
||||||
const ViewButton = ({
|
const ViewButton = ({
|
||||||
loading = false,
|
disabled = false,
|
||||||
properties = [],
|
items = [],
|
||||||
collapseState = {},
|
visibleState = {},
|
||||||
updateCollapseState = () => {},
|
updateVisibleState = () => {},
|
||||||
...buttonProps
|
...buttonProps
|
||||||
}) => {
|
}) => {
|
||||||
return (
|
return (
|
||||||
@ -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' }}>
|
||||||
{properties.map((property) => (
|
{items.map((item) => (
|
||||||
<Checkbox
|
<Checkbox
|
||||||
checked={collapseState[property.key]}
|
checked={visibleState[item.key]}
|
||||||
key={property.key}
|
key={item.key}
|
||||||
onChange={(e) => {
|
onChange={(e) => {
|
||||||
updateCollapseState(property.key, e.target.checked)
|
updateVisibleState(item.key, e.target.checked)
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{property.label}
|
{item.label}
|
||||||
</Checkbox>
|
</Checkbox>
|
||||||
))}
|
))}
|
||||||
</Flex>
|
</Flex>
|
||||||
@ -33,7 +33,7 @@ const ViewButton = ({
|
|||||||
placement='bottomLeft'
|
placement='bottomLeft'
|
||||||
arrow={false}
|
arrow={false}
|
||||||
>
|
>
|
||||||
<Button disabled={loading} {...buttonProps}>
|
<Button disabled={disabled} {...buttonProps}>
|
||||||
View
|
View
|
||||||
</Button>
|
</Button>
|
||||||
</Popover>
|
</Popover>
|
||||||
@ -41,15 +41,15 @@ const ViewButton = ({
|
|||||||
}
|
}
|
||||||
|
|
||||||
ViewButton.propTypes = {
|
ViewButton.propTypes = {
|
||||||
loading: PropTypes.bool,
|
disabled: PropTypes.bool,
|
||||||
properties: PropTypes.arrayOf(
|
items: PropTypes.arrayOf(
|
||||||
PropTypes.shape({
|
PropTypes.shape({
|
||||||
key: PropTypes.string.isRequired,
|
key: PropTypes.string.isRequired,
|
||||||
label: PropTypes.string.isRequired
|
label: PropTypes.string.isRequired
|
||||||
})
|
})
|
||||||
),
|
),
|
||||||
collapseState: PropTypes.object,
|
visibleState: PropTypes.object,
|
||||||
updateCollapseState: PropTypes.func
|
updateVisibleState: PropTypes.func
|
||||||
}
|
}
|
||||||
|
|
||||||
export default ViewButton
|
export default ViewButton
|
||||||
|
|||||||
@ -331,14 +331,25 @@ const ApiServerProvider = ({ children }) => {
|
|||||||
fileLink.parentNode.removeChild(fileLink)
|
fileLink.parentNode.removeChild(fileLink)
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
logger.error('Failed to download GCode file content:', error)
|
logger.error('Failed to download GCode file content:', error)
|
||||||
|
|
||||||
if (error.response) {
|
if (error.response) {
|
||||||
messageApi.error('Error downloading GCode file:', error.response.status)
|
if (error.response.status === 404) {
|
||||||
|
showError(
|
||||||
|
`The ${type} file "${fileName}" was not found on the server. It may have been deleted or moved.`,
|
||||||
|
() => handleDownloadContent(id, type, fileName)
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
showError(
|
||||||
|
`Error downloading ${type} file: ${error.response.status} - ${error.response.statusText}`,
|
||||||
|
() => handleDownloadContent(id, type, fileName)
|
||||||
|
)
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
messageApi.error(
|
showError(
|
||||||
'An unexpected error occurred while downloading. Please try again later.'
|
'An unexpected error occurred while downloading. Please check your connection and try again.',
|
||||||
|
() => handleDownloadContent(id, type, fileName)
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
throw error
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -378,6 +389,14 @@ const ApiServerProvider = ({ children }) => {
|
|||||||
centered
|
centered
|
||||||
maskClosable={true}
|
maskClosable={true}
|
||||||
footer={[
|
footer={[
|
||||||
|
<Button
|
||||||
|
key='retry'
|
||||||
|
onClick={() => {
|
||||||
|
setShowErrorModal(false)
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
Close
|
||||||
|
</Button>,
|
||||||
<Button key='retry' icon={<ReloadIcon />} onClick={handleRetry}>
|
<Button key='retry' icon={<ReloadIcon />} onClick={handleRetry}>
|
||||||
Retry
|
Retry
|
||||||
</Button>
|
</Button>
|
||||||
|
|||||||
7
src/components/Icons/DownloadIcon.jsx
Normal file
7
src/components/Icons/DownloadIcon.jsx
Normal file
@ -0,0 +1,7 @@
|
|||||||
|
import React from 'react'
|
||||||
|
import Icon from '@ant-design/icons'
|
||||||
|
import { ReactComponent as CustomIconSvg } from '../../assets/icons/downloadicon.min.svg'
|
||||||
|
|
||||||
|
const DownloadIcon = (props) => <Icon component={CustomIconSvg} {...props} />
|
||||||
|
|
||||||
|
export default DownloadIcon
|
||||||
@ -77,6 +77,16 @@ export function getModelByName(name) {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function getModelProperty(name, property) {
|
||||||
|
const model = getModelByName(name)
|
||||||
|
|
||||||
|
if (!model || !model.properties) {
|
||||||
|
return undefined
|
||||||
|
}
|
||||||
|
|
||||||
|
return model.properties.find((prop) => prop.name == property)
|
||||||
|
}
|
||||||
|
|
||||||
export function getModelProperties(name, propertyList) {
|
export function getModelProperties(name, propertyList) {
|
||||||
const model = getModelByName(name)
|
const model = getModelByName(name)
|
||||||
|
|
||||||
@ -132,3 +142,53 @@ export const getPropertyValue = (obj, path) => {
|
|||||||
return obj[path]
|
return obj[path]
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export const evaluateVariable = (expression, data) => {
|
||||||
|
if (!expression) return false
|
||||||
|
|
||||||
|
// Only treat as an expression if it starts and ends with ()
|
||||||
|
const expr = expression.trim()
|
||||||
|
if (!(expr.startsWith('(') && expr.endsWith(')'))) return false
|
||||||
|
|
||||||
|
// Remove the outer parentheses
|
||||||
|
const innerExpr = expr.slice(1, -1)
|
||||||
|
|
||||||
|
// Helper to evaluate a single condition like 'foo == "bar"' or 'foo.bar == 42' or 'foo == true'
|
||||||
|
const evalCondition = (cond, data) => {
|
||||||
|
const match = cond.trim().match(/^([a-zA-Z0-9_.]+)\s*==\s*(.+)$/)
|
||||||
|
if (!match) return false
|
||||||
|
const [, path, valueRaw] = match
|
||||||
|
let value
|
||||||
|
let raw = valueRaw.trim()
|
||||||
|
// Check for quoted string
|
||||||
|
if (
|
||||||
|
(raw.startsWith('"') && raw.endsWith('"')) ||
|
||||||
|
(raw.startsWith("'") && raw.endsWith("'"))
|
||||||
|
) {
|
||||||
|
value = raw.slice(1, -1)
|
||||||
|
} else if (raw === 'true') {
|
||||||
|
value = true
|
||||||
|
} else if (raw === 'false') {
|
||||||
|
value = false
|
||||||
|
} else if (!isNaN(Number(raw))) {
|
||||||
|
value = Number(raw)
|
||||||
|
} else {
|
||||||
|
value = raw
|
||||||
|
}
|
||||||
|
// Resolve nested property
|
||||||
|
const propValue = path
|
||||||
|
.split('.')
|
||||||
|
.reduce((acc, key) => (acc ? acc[key] : undefined), data)
|
||||||
|
return propValue === value
|
||||||
|
}
|
||||||
|
|
||||||
|
// Split by '||' first (lowest precedence)
|
||||||
|
const orParts = innerExpr.split(/\|\|/)
|
||||||
|
for (let orPart of orParts) {
|
||||||
|
// Each orPart may have '&&' (higher precedence)
|
||||||
|
const andParts = orPart.split(/&&/)
|
||||||
|
const andResult = andParts.every((andPart) => evalCondition(andPart, data))
|
||||||
|
if (andResult) return true // If any OR group is true, return true
|
||||||
|
}
|
||||||
|
return false // None of the OR groups were true
|
||||||
|
}
|
||||||
|
|||||||
@ -1,20 +1,89 @@
|
|||||||
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: [
|
actions: [],
|
||||||
|
columns: ['_id', 'owner', 'owner._id', 'parent._id', 'changes', 'createdAt'],
|
||||||
|
filters: ['_id', 'owner._id', 'parent._id'],
|
||||||
|
sorters: [],
|
||||||
|
properties: [
|
||||||
{
|
{
|
||||||
name: 'info',
|
name: '_id',
|
||||||
label: 'Info',
|
label: 'ID',
|
||||||
default: true,
|
type: 'id',
|
||||||
row: true,
|
objectType: 'auditLog',
|
||||||
icon: InfoCircleIcon,
|
columnFixed: 'left',
|
||||||
url: (_id) => `/dashboard/management/auditlogs/info?auditLogId=${_id}`
|
value: null,
|
||||||
|
showCopy: true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'createdAt',
|
||||||
|
label: 'Created At',
|
||||||
|
type: 'dateTime',
|
||||||
|
value: null,
|
||||||
|
readOnly: true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'updatedAt',
|
||||||
|
label: 'Updated At',
|
||||||
|
type: 'dateTime',
|
||||||
|
value: null,
|
||||||
|
readOnly: true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'owner',
|
||||||
|
label: 'Owner',
|
||||||
|
type: 'object',
|
||||||
|
objectType: (objectData) => {
|
||||||
|
return objectData.ownerType
|
||||||
|
},
|
||||||
|
columnFixed: 'left',
|
||||||
|
value: null,
|
||||||
|
showCopy: true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'owner._id',
|
||||||
|
label: 'Owner ID',
|
||||||
|
type: 'id',
|
||||||
|
objectType: (objectData) => {
|
||||||
|
return objectData.ownerType
|
||||||
|
},
|
||||||
|
columnFixed: 'left',
|
||||||
|
showHyperlink: true,
|
||||||
|
showCopy: true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'parent',
|
||||||
|
label: 'Parent',
|
||||||
|
type: 'object',
|
||||||
|
objectType: (objectData) => {
|
||||||
|
return objectData.parentType
|
||||||
|
},
|
||||||
|
value: null,
|
||||||
|
showCopy: true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'parent._id',
|
||||||
|
label: 'Parent ID',
|
||||||
|
type: 'id',
|
||||||
|
objectType: (objectData) => {
|
||||||
|
return objectData.parentType
|
||||||
|
},
|
||||||
|
showHyperlink: true,
|
||||||
|
showCopy: true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'changes',
|
||||||
|
label: 'Changes',
|
||||||
|
columnWidth: 500,
|
||||||
|
type: 'propertyChanges',
|
||||||
|
objectType: (objectData) => {
|
||||||
|
return objectData.parentType
|
||||||
|
},
|
||||||
|
showCopy: true
|
||||||
}
|
}
|
||||||
],
|
]
|
||||||
url: () => `#`
|
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,5 +1,7 @@
|
|||||||
|
import EditIcon from '../../components/Icons/EditIcon'
|
||||||
import FilamentIcon from '../../components/Icons/FilamentIcon'
|
import FilamentIcon from '../../components/Icons/FilamentIcon'
|
||||||
import InfoCircleIcon from '../../components/Icons/InfoCircleIcon'
|
import InfoCircleIcon from '../../components/Icons/InfoCircleIcon'
|
||||||
|
import ReloadIcon from '../../components/Icons/ReloadIcon'
|
||||||
|
|
||||||
export const Filament = {
|
export const Filament = {
|
||||||
name: 'filament',
|
name: 'filament',
|
||||||
@ -14,6 +16,21 @@ export const Filament = {
|
|||||||
row: true,
|
row: true,
|
||||||
icon: InfoCircleIcon,
|
icon: InfoCircleIcon,
|
||||||
url: (_id) => `/dashboard/management/filaments/info?filamentId=${_id}`
|
url: (_id) => `/dashboard/management/filaments/info?filamentId=${_id}`
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'reload',
|
||||||
|
label: 'Reload',
|
||||||
|
icon: ReloadIcon,
|
||||||
|
url: (_id) =>
|
||||||
|
`/dashboard/management/filaments/info?filamentId=${_id}&action=reload`
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'edit',
|
||||||
|
label: 'Edit',
|
||||||
|
row: true,
|
||||||
|
icon: EditIcon,
|
||||||
|
url: (_id) =>
|
||||||
|
`/dashboard/management/filaments/info?filamentId=${_id}&action=edit`
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
columns: [
|
columns: [
|
||||||
|
|||||||
@ -1,5 +1,8 @@
|
|||||||
|
import DownloadIcon from '../../components/Icons/DownloadIcon'
|
||||||
|
import EditIcon from '../../components/Icons/EditIcon'
|
||||||
import GCodeFileIcon from '../../components/Icons/GCodeFileIcon'
|
import GCodeFileIcon from '../../components/Icons/GCodeFileIcon'
|
||||||
import InfoCircleIcon from '../../components/Icons/InfoCircleIcon'
|
import InfoCircleIcon from '../../components/Icons/InfoCircleIcon'
|
||||||
|
import ReloadIcon from '../../components/Icons/ReloadIcon'
|
||||||
|
|
||||||
export const GCodeFile = {
|
export const GCodeFile = {
|
||||||
name: 'gcodeFile',
|
name: 'gcodeFile',
|
||||||
@ -15,12 +18,28 @@ export const GCodeFile = {
|
|||||||
icon: InfoCircleIcon,
|
icon: InfoCircleIcon,
|
||||||
url: (_id) => `/dashboard/production/gcodefiles/info?gcodeFileId=${_id}`
|
url: (_id) => `/dashboard/production/gcodefiles/info?gcodeFileId=${_id}`
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
name: 'reload',
|
||||||
|
label: 'Reload',
|
||||||
|
icon: ReloadIcon,
|
||||||
|
url: (_id) =>
|
||||||
|
`/dashboard/production/gcodefiles/info?gcodeFileId=${_id}&action=reload`
|
||||||
|
},
|
||||||
{
|
{
|
||||||
name: 'download',
|
name: 'download',
|
||||||
label: 'Download',
|
label: 'Download',
|
||||||
row: true,
|
row: true,
|
||||||
|
icon: DownloadIcon,
|
||||||
url: (_id) =>
|
url: (_id) =>
|
||||||
`/dashboard/production/gcodefiles/info?gcodeFileId=${_id}&action=download`
|
`/dashboard/production/gcodefiles/info?gcodeFileId=${_id}&action=download`
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'edit',
|
||||||
|
label: 'Edit',
|
||||||
|
row: true,
|
||||||
|
icon: EditIcon,
|
||||||
|
url: (_id) =>
|
||||||
|
`/dashboard/production/gcodefiles/info?gcodeFileId=${_id}&action=edit`
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
|
|
||||||
|
|||||||
@ -1,5 +1,7 @@
|
|||||||
import JobIcon from '../../components/Icons/JobIcon'
|
import JobIcon from '../../components/Icons/JobIcon'
|
||||||
import InfoCircleIcon from '../../components/Icons/InfoCircleIcon'
|
import InfoCircleIcon from '../../components/Icons/InfoCircleIcon'
|
||||||
|
import ReloadIcon from '../../components/Icons/ReloadIcon'
|
||||||
|
import EditIcon from '../../components/Icons/EditIcon'
|
||||||
|
|
||||||
export const Job = {
|
export const Job = {
|
||||||
name: 'job',
|
name: 'job',
|
||||||
@ -14,6 +16,19 @@ export const Job = {
|
|||||||
row: true,
|
row: true,
|
||||||
icon: InfoCircleIcon,
|
icon: InfoCircleIcon,
|
||||||
url: (_id) => `/dashboard/production/jobs/info?jobId=${_id}`
|
url: (_id) => `/dashboard/production/jobs/info?jobId=${_id}`
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'reload',
|
||||||
|
label: 'Reload',
|
||||||
|
icon: ReloadIcon,
|
||||||
|
url: (_id) => `/dashboard/production/jobs/info?jobId=${_id}&action=reload`
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'edit',
|
||||||
|
label: 'Edit',
|
||||||
|
row: true,
|
||||||
|
icon: EditIcon,
|
||||||
|
url: (_id) => `/dashboard/production/jobs/info?jobId=${_id}&action=edit`
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
columns: [
|
columns: [
|
||||||
@ -25,7 +40,7 @@ export const Job = {
|
|||||||
'createdAt'
|
'createdAt'
|
||||||
],
|
],
|
||||||
filters: ['state', '_id', 'gcodeFile._id', 'quantity'],
|
filters: ['state', '_id', 'gcodeFile._id', 'quantity'],
|
||||||
sorters: ['createdAt', 'state', 'quantity', '_id'],
|
sorters: ['createdAt', 'state', 'quantity', 'gcodeFile'],
|
||||||
properties: [
|
properties: [
|
||||||
{
|
{
|
||||||
name: '_id',
|
name: '_id',
|
||||||
|
|||||||
@ -1,5 +1,7 @@
|
|||||||
import NoteTypeIcon from '../../components/Icons/NoteTypeIcon'
|
import NoteTypeIcon from '../../components/Icons/NoteTypeIcon'
|
||||||
import InfoCircleIcon from '../../components/Icons/InfoCircleIcon'
|
import InfoCircleIcon from '../../components/Icons/InfoCircleIcon'
|
||||||
|
import ReloadIcon from '../../components/Icons/ReloadIcon'
|
||||||
|
import EditIcon from '../../components/Icons/EditIcon'
|
||||||
|
|
||||||
export const NoteType = {
|
export const NoteType = {
|
||||||
name: 'noteType',
|
name: 'noteType',
|
||||||
@ -14,6 +16,21 @@ export const NoteType = {
|
|||||||
row: true,
|
row: true,
|
||||||
icon: InfoCircleIcon,
|
icon: InfoCircleIcon,
|
||||||
url: (_id) => `/dashboard/management/notetypes/info?noteTypeId=${_id}`
|
url: (_id) => `/dashboard/management/notetypes/info?noteTypeId=${_id}`
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'reload',
|
||||||
|
label: 'Reload',
|
||||||
|
icon: ReloadIcon,
|
||||||
|
url: (_id) =>
|
||||||
|
`/dashboard/management/notetypes/info?noteTypeId=${_id}&action=reload`
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'edit',
|
||||||
|
label: 'Edit',
|
||||||
|
row: true,
|
||||||
|
icon: EditIcon,
|
||||||
|
url: (_id) =>
|
||||||
|
`/dashboard/management/notetypes/info?noteTypeId=${_id}&action=edit`
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
columns: ['name', '_id', 'color', 'active', 'createdAt', 'updatedAt'],
|
columns: ['name', '_id', 'color', 'active', 'createdAt', 'updatedAt'],
|
||||||
|
|||||||
@ -1,5 +1,8 @@
|
|||||||
|
import DownloadIcon from '../../components/Icons/DownloadIcon'
|
||||||
|
import EditIcon from '../../components/Icons/EditIcon'
|
||||||
import InfoCircleIcon from '../../components/Icons/InfoCircleIcon'
|
import InfoCircleIcon from '../../components/Icons/InfoCircleIcon'
|
||||||
import PartIcon from '../../components/Icons/PartIcon'
|
import PartIcon from '../../components/Icons/PartIcon'
|
||||||
|
import ReloadIcon from '../../components/Icons/ReloadIcon'
|
||||||
|
|
||||||
export const Part = {
|
export const Part = {
|
||||||
name: 'part',
|
name: 'part',
|
||||||
@ -14,10 +17,39 @@ export const Part = {
|
|||||||
row: true,
|
row: true,
|
||||||
icon: InfoCircleIcon,
|
icon: InfoCircleIcon,
|
||||||
url: (_id) => `/dashboard/management/parts/info?partId=${_id}`
|
url: (_id) => `/dashboard/management/parts/info?partId=${_id}`
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'reload',
|
||||||
|
label: 'Reload',
|
||||||
|
icon: ReloadIcon,
|
||||||
|
url: (_id) =>
|
||||||
|
`/dashboard/management/parts/info?partId=${_id}&action=reload`
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'download',
|
||||||
|
label: 'Download',
|
||||||
|
row: true,
|
||||||
|
icon: DownloadIcon,
|
||||||
|
url: (_id) =>
|
||||||
|
`/dashboard/management/parts/info?partId=${_id}&action=download`
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'edit',
|
||||||
|
label: 'Edit',
|
||||||
|
row: true,
|
||||||
|
icon: EditIcon,
|
||||||
|
url: (_id) => `/dashboard/management/parts/info?partId=${_id}&action=edit`
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
columns: ['name', '_id', 'product', 'product._id', 'createdAt'],
|
columns: [
|
||||||
filters: ['name', '_id', 'product', 'product._id'],
|
'name',
|
||||||
|
'_id',
|
||||||
|
'product',
|
||||||
|
'product._id',
|
||||||
|
'globalPricing',
|
||||||
|
'createdAt'
|
||||||
|
],
|
||||||
|
filters: ['name', '_id', 'product', 'product._id', 'globalPricing'],
|
||||||
sorters: ['name', 'email', 'role', 'createdAt', '_id'],
|
sorters: ['name', 'email', 'role', 'createdAt', '_id'],
|
||||||
properties: [
|
properties: [
|
||||||
{
|
{
|
||||||
@ -25,8 +57,9 @@ export const Part = {
|
|||||||
label: 'ID',
|
label: 'ID',
|
||||||
columnFixed: 'left',
|
columnFixed: 'left',
|
||||||
type: 'id',
|
type: 'id',
|
||||||
objectType: 'user',
|
objectType: 'part',
|
||||||
showCopy: true
|
showCopy: true,
|
||||||
|
readOnly: true
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: 'createdAt',
|
name: 'createdAt',
|
||||||
@ -52,13 +85,58 @@ export const Part = {
|
|||||||
name: 'product',
|
name: 'product',
|
||||||
label: 'Product',
|
label: 'Product',
|
||||||
type: 'object',
|
type: 'object',
|
||||||
|
required: true,
|
||||||
objectType: 'product'
|
objectType: 'product'
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: 'product._id',
|
name: 'product._id',
|
||||||
label: 'Product ID',
|
label: 'Product ID',
|
||||||
type: 'id',
|
type: 'id',
|
||||||
|
readOnly: true,
|
||||||
|
showHyperlink: true,
|
||||||
objectType: 'product'
|
objectType: 'product'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'globalPricing',
|
||||||
|
label: 'Global Price',
|
||||||
|
columnWidth: 150,
|
||||||
|
required: true,
|
||||||
|
type: 'bool'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'priceMode',
|
||||||
|
label: 'Price Mode',
|
||||||
|
type: 'priceMode',
|
||||||
|
disabled: (objectData) => {
|
||||||
|
return objectData.globalPricing == true
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'margin',
|
||||||
|
label: 'Margin',
|
||||||
|
type: 'number',
|
||||||
|
disabled: (objectData) => {
|
||||||
|
return (
|
||||||
|
objectData.globalPricing == true || objectData.priceMode == 'amount'
|
||||||
|
)
|
||||||
|
},
|
||||||
|
suffix: '%',
|
||||||
|
min: 0,
|
||||||
|
max: 100,
|
||||||
|
step: 0.01
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'amount',
|
||||||
|
label: 'Amount',
|
||||||
|
disabled: (objectData) => {
|
||||||
|
return (
|
||||||
|
objectData.globalPricing == true || objectData.priceMode == 'margin'
|
||||||
|
)
|
||||||
|
},
|
||||||
|
type: 'number',
|
||||||
|
prefix: '£',
|
||||||
|
min: 0,
|
||||||
|
step: 0.1
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,5 +1,8 @@
|
|||||||
import PrinterIcon from '../../components/Icons/PrinterIcon'
|
import PrinterIcon from '../../components/Icons/PrinterIcon'
|
||||||
import InfoCircleIcon from '../../components/Icons/InfoCircleIcon'
|
import InfoCircleIcon from '../../components/Icons/InfoCircleIcon'
|
||||||
|
import ReloadIcon from '../../components/Icons/ReloadIcon'
|
||||||
|
import EditIcon from '../../components/Icons/EditIcon'
|
||||||
|
import PlayCircleIcon from '../../components/Icons/PlayCircleIcon'
|
||||||
|
|
||||||
export const Printer = {
|
export const Printer = {
|
||||||
name: 'printer',
|
name: 'printer',
|
||||||
@ -14,12 +17,34 @@ export const Printer = {
|
|||||||
row: true,
|
row: true,
|
||||||
icon: InfoCircleIcon,
|
icon: InfoCircleIcon,
|
||||||
url: (_id) => `/dashboard/production/printers/info?printerId=${_id}`
|
url: (_id) => `/dashboard/production/printers/info?printerId=${_id}`
|
||||||
|
},
|
||||||
|
|
||||||
|
{
|
||||||
|
name: 'reload',
|
||||||
|
label: 'Reload',
|
||||||
|
icon: ReloadIcon,
|
||||||
|
url: (_id) =>
|
||||||
|
`/dashboard/production/printers/info?printerId=${_id}&action=reload`
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'control',
|
||||||
|
label: 'Control',
|
||||||
|
row: true,
|
||||||
|
icon: PlayCircleIcon,
|
||||||
|
url: (_id) => `/dashboard/production/printers/control?printerId=${_id}`
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'edit',
|
||||||
|
label: 'Edit',
|
||||||
|
row: true,
|
||||||
|
icon: EditIcon,
|
||||||
|
url: (_id) =>
|
||||||
|
`/dashboard/production/printers/info?printerId=${_id}&action=edit`
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
url: (id) => `/dashboard/production/printers/info?printerId=${id}`,
|
|
||||||
columns: ['name', '_id', 'state', 'tags', 'connectedAt'],
|
columns: ['name', '_id', 'state', 'tags', 'connectedAt'],
|
||||||
filters: ['name', '_id', 'state', 'tags'],
|
filters: ['name', '_id', 'state', 'tags'],
|
||||||
sorters: ['name', 'state', 'connectedAt', '_id'],
|
sorters: ['name', 'state', 'connectedAt'],
|
||||||
properties: [
|
properties: [
|
||||||
{
|
{
|
||||||
name: '_id',
|
name: '_id',
|
||||||
|
|||||||
@ -1,5 +1,7 @@
|
|||||||
import ProductIcon from '../../components/Icons/ProductIcon'
|
import ProductIcon from '../../components/Icons/ProductIcon'
|
||||||
import InfoCircleIcon from '../../components/Icons/InfoCircleIcon'
|
import InfoCircleIcon from '../../components/Icons/InfoCircleIcon'
|
||||||
|
import ReloadIcon from '../../components/Icons/ReloadIcon'
|
||||||
|
import EditIcon from '../../components/Icons/EditIcon'
|
||||||
|
|
||||||
export const Product = {
|
export const Product = {
|
||||||
name: 'product',
|
name: 'product',
|
||||||
@ -14,16 +16,43 @@ export const Product = {
|
|||||||
row: true,
|
row: true,
|
||||||
icon: InfoCircleIcon,
|
icon: InfoCircleIcon,
|
||||||
url: (_id) => `/dashboard/management/products/info?productId=${_id}`
|
url: (_id) => `/dashboard/management/products/info?productId=${_id}`
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'reload',
|
||||||
|
label: 'Reload',
|
||||||
|
icon: ReloadIcon,
|
||||||
|
url: (_id) =>
|
||||||
|
`/dashboard/management/products/info?productId=${_id}&action=reload`
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'edit',
|
||||||
|
label: 'Edit',
|
||||||
|
row: true,
|
||||||
|
icon: EditIcon,
|
||||||
|
url: (_id) =>
|
||||||
|
`/dashboard/management/products/info?productId=${_id}&action=edit`
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
url: (id) => `/dashboard/management/products/info?productId=${id}`,
|
columns: [
|
||||||
|
'_id',
|
||||||
|
'name',
|
||||||
|
'tags',
|
||||||
|
'vendor',
|
||||||
|
'vendor._id',
|
||||||
|
'price',
|
||||||
|
'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',
|
||||||
type: 'id',
|
type: 'id',
|
||||||
objectType: 'printer',
|
objectType: 'product',
|
||||||
showCopy: true
|
showCopy: true,
|
||||||
|
readOnly: true
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: 'createdAt',
|
name: 'createdAt',
|
||||||
@ -42,6 +71,59 @@ export const Product = {
|
|||||||
label: 'Updated At',
|
label: 'Updated At',
|
||||||
type: 'dateTime',
|
type: 'dateTime',
|
||||||
readOnly: true
|
readOnly: true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'vendor',
|
||||||
|
label: 'Vendor',
|
||||||
|
required: true,
|
||||||
|
type: 'object',
|
||||||
|
objectType: 'vendor'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'vendor._id',
|
||||||
|
label: 'Vendor ID',
|
||||||
|
readOnly: true,
|
||||||
|
type: 'id',
|
||||||
|
showHyperlink: true,
|
||||||
|
objectType: 'vendor'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'version',
|
||||||
|
label: 'Version',
|
||||||
|
type: 'text'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'tags',
|
||||||
|
label: 'Tags',
|
||||||
|
type: 'tags'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'priceMode',
|
||||||
|
label: 'Price Mode',
|
||||||
|
type: 'priceMode'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'margin',
|
||||||
|
label: 'Margin',
|
||||||
|
type: 'number',
|
||||||
|
disabled: (objectData) => {
|
||||||
|
return objectData.priceMode == 'amount'
|
||||||
|
},
|
||||||
|
suffix: '%',
|
||||||
|
min: 0,
|
||||||
|
max: 100,
|
||||||
|
step: 0.01
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'amount',
|
||||||
|
label: 'Amount',
|
||||||
|
disabled: (objectData) => {
|
||||||
|
return objectData.priceMode == 'margin'
|
||||||
|
},
|
||||||
|
type: 'number',
|
||||||
|
prefix: '£',
|
||||||
|
min: 0,
|
||||||
|
step: 0.1
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,5 +1,6 @@
|
|||||||
import PersonIcon from '../../components/Icons/PersonIcon'
|
import PersonIcon from '../../components/Icons/PersonIcon'
|
||||||
import InfoCircleIcon from '../../components/Icons/InfoCircleIcon'
|
import InfoCircleIcon from '../../components/Icons/InfoCircleIcon'
|
||||||
|
import ReloadIcon from '../../components/Icons/ReloadIcon'
|
||||||
|
|
||||||
export const User = {
|
export const User = {
|
||||||
name: 'user',
|
name: 'user',
|
||||||
@ -14,9 +15,15 @@ export const User = {
|
|||||||
row: true,
|
row: true,
|
||||||
icon: InfoCircleIcon,
|
icon: InfoCircleIcon,
|
||||||
url: (_id) => `/dashboard/management/users/info?userId=${_id}`
|
url: (_id) => `/dashboard/management/users/info?userId=${_id}`
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'reload',
|
||||||
|
label: 'Reload',
|
||||||
|
icon: ReloadIcon,
|
||||||
|
url: (_id) =>
|
||||||
|
`/dashboard/management/users/info?userId=${_id}&action=reload`
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
url: (id) => `/dashboard/management/users/info?userId=${id}`,
|
|
||||||
columns: ['name', '_id', 'username', 'email', 'role', 'createdAt'],
|
columns: ['name', '_id', 'username', 'email', 'role', 'createdAt'],
|
||||||
filters: ['name', '_id', 'email', 'role'],
|
filters: ['name', '_id', 'email', 'role'],
|
||||||
sorters: ['name', 'email', 'role', 'createdAt', '_id'],
|
sorters: ['name', 'email', 'role', 'createdAt', '_id'],
|
||||||
|
|||||||
@ -1,5 +1,7 @@
|
|||||||
import VendorIcon from '../../components/Icons/VendorIcon'
|
import VendorIcon from '../../components/Icons/VendorIcon'
|
||||||
import InfoCircleIcon from '../../components/Icons/InfoCircleIcon'
|
import InfoCircleIcon from '../../components/Icons/InfoCircleIcon'
|
||||||
|
import EditIcon from '../../components/Icons/EditIcon'
|
||||||
|
import ReloadIcon from '../../components/Icons/ReloadIcon'
|
||||||
|
|
||||||
export const Vendor = {
|
export const Vendor = {
|
||||||
name: 'vendor',
|
name: 'vendor',
|
||||||
@ -14,10 +16,25 @@ export const Vendor = {
|
|||||||
row: true,
|
row: true,
|
||||||
icon: InfoCircleIcon,
|
icon: InfoCircleIcon,
|
||||||
url: (_id) => `/dashboard/management/vendors/info?vendorId=${_id}`
|
url: (_id) => `/dashboard/management/vendors/info?vendorId=${_id}`
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'reload',
|
||||||
|
label: 'Reload',
|
||||||
|
icon: ReloadIcon,
|
||||||
|
url: (_id) =>
|
||||||
|
`/dashboard/management/vendors/info?vendorId=${_id}&action=reload`
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'edit',
|
||||||
|
label: 'Edit',
|
||||||
|
row: true,
|
||||||
|
icon: EditIcon,
|
||||||
|
url: (_id) =>
|
||||||
|
`/dashboard/management/vendors/info?vendorId=${_id}&action=edit`
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
url: (id) => `/dashboard/management/vendors/info?vendorId=${id}`,
|
url: (id) => `/dashboard/management/vendors/info?vendorId=${id}`,
|
||||||
columns: ['name', '_id', 'country', 'email', 'createdAt'],
|
columns: ['name', '_id', 'country', 'email', 'website', 'createdAt'],
|
||||||
filters: ['name', '_id', 'country', 'email'],
|
filters: ['name', '_id', 'country', 'email'],
|
||||||
sorters: ['name', 'country', 'email', 'createdAt', '_id'],
|
sorters: ['name', 'country', 'email', 'createdAt', '_id'],
|
||||||
properties: [
|
properties: [
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user