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 {
|
||||
Button,
|
||||
Flex,
|
||||
@ -12,14 +12,12 @@ import {
|
||||
Badge
|
||||
} from 'antd'
|
||||
|
||||
import { AuthContext } from '../context/AuthContext'
|
||||
import IdDisplay from '../common/IdDisplay'
|
||||
import ReloadIcon from '../../Icons/ReloadIcon'
|
||||
import useColumnVisibility from '../hooks/useColumnVisibility'
|
||||
import TimeDisplay from '../common/TimeDisplay'
|
||||
import ObjectTable from '../common/ObjectTable'
|
||||
|
||||
import config from '../../../config'
|
||||
import AuditLogIcon from '../../Icons/AuditLogIcon'
|
||||
import XMarkIcon from '../../Icons/XMarkIcon'
|
||||
import CheckIcon from '../../Icons/CheckIcon'
|
||||
@ -253,8 +251,6 @@ const AuditLogs = () => {
|
||||
columns
|
||||
)
|
||||
|
||||
const { authenticated } = useContext(AuthContext)
|
||||
|
||||
const actionItems = {
|
||||
items: [
|
||||
{
|
||||
@ -294,10 +290,6 @@ const AuditLogs = () => {
|
||||
)
|
||||
}
|
||||
|
||||
const visibleColumns = columns.filter(
|
||||
(col) => !col.key || columnVisibility[col.key]
|
||||
)
|
||||
|
||||
return (
|
||||
<>
|
||||
<Flex vertical={'true'} gap='large'>
|
||||
@ -318,9 +310,8 @@ const AuditLogs = () => {
|
||||
|
||||
<ObjectTable
|
||||
ref={tableRef}
|
||||
columns={visibleColumns}
|
||||
url={`${config.backendUrl}/auditlogs`}
|
||||
authenticated={authenticated}
|
||||
visibleColumns={columnVisibility}
|
||||
type='auditLog'
|
||||
/>
|
||||
</Flex>
|
||||
</>
|
||||
|
||||
@ -1,12 +1,10 @@
|
||||
import React from 'react'
|
||||
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 config from '../../../../config'
|
||||
import ReloadIcon from '../../../Icons/ReloadIcon'
|
||||
import useCollapseState from '../../hooks/useCollapseState'
|
||||
import AuditLogTable from '../../common/AuditLogTable'
|
||||
import NotesPanel from '../../common/NotesPanel'
|
||||
import InfoCollapse from '../../common/InfoCollapse'
|
||||
import ObjectInfo from '../../common/ObjectInfo'
|
||||
@ -17,10 +15,10 @@ import AuditLogIcon from '../../../Icons/AuditLogIcon.jsx'
|
||||
import EditObjectForm from '../../common/EditObjectForm'
|
||||
import EditButtons from '../../common/EditButtons'
|
||||
import LockIndicator from './LockIndicator'
|
||||
import {
|
||||
getModelProperties,
|
||||
getPropertyValue
|
||||
} from '../../../../database/ObjectModels'
|
||||
import ActionHandler from '../../common/ActionHandler'
|
||||
import ObjectActions from '../../common/ObjectActions.jsx'
|
||||
import ObjectTable from '../../common/ObjectTable.jsx'
|
||||
import InfoCollapsePlaceholder from '../../common/InfoCollapsePlaceholder.jsx'
|
||||
|
||||
const log = loglevel.getLogger('FilamentInfo')
|
||||
log.setLevel(config.logLevel)
|
||||
@ -51,111 +49,139 @@ const FilamentInfo = () => {
|
||||
editLoading,
|
||||
lock,
|
||||
fetchObject
|
||||
}) => (
|
||||
<Flex
|
||||
gap='large'
|
||||
vertical='true'
|
||||
style={{ height: 'calc(var(--unit-100vh) - 155px)', minHeight: 0 }}
|
||||
>
|
||||
<Flex justify={'space-between'}>
|
||||
<Space size='middle'>
|
||||
<Space size='small'>
|
||||
<Dropdown
|
||||
menu={{
|
||||
items: [
|
||||
{
|
||||
label: 'Reload Filament',
|
||||
key: 'reload',
|
||||
icon: <ReloadIcon />
|
||||
}) => {
|
||||
// Define actions for ActionHandler
|
||||
const actions = {
|
||||
reload: () => {
|
||||
fetchObject()
|
||||
return true
|
||||
},
|
||||
edit: () => {
|
||||
startEditing()
|
||||
return false
|
||||
},
|
||||
cancelEdit: () => {
|
||||
cancelEditing()
|
||||
return true
|
||||
},
|
||||
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)
|
||||
}
|
||||
],
|
||||
onClick: ({ key }) => {
|
||||
if (key === 'reload') {
|
||||
fetchObject()
|
||||
key='info'
|
||||
>
|
||||
<ObjectInfo
|
||||
loading={loading}
|
||||
indicator={<LoadingOutlined />}
|
||||
isEditing={isEditing}
|
||||
type='filament'
|
||||
objectData={objectData}
|
||||
/>
|
||||
</InfoCollapse>
|
||||
|
||||
<InfoCollapse
|
||||
title='Notes'
|
||||
icon={<NoteIcon />}
|
||||
active={collapseState.notes}
|
||||
onToggle={(expanded) =>
|
||||
updateCollapseState('notes', expanded)
|
||||
}
|
||||
}
|
||||
}}
|
||||
>
|
||||
<Button disabled={loading}>Actions</Button>
|
||||
</Dropdown>
|
||||
<ViewButton
|
||||
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>
|
||||
key='notes'
|
||||
>
|
||||
<Card>
|
||||
<NotesPanel _id={filamentId} />
|
||||
</Card>
|
||||
</InfoCollapse>
|
||||
|
||||
<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'
|
||||
>
|
||||
<ObjectInfo
|
||||
loading={loading}
|
||||
indicator={<LoadingOutlined />}
|
||||
isEditing={isEditing}
|
||||
items={getModelProperties('filament').map((prop) => ({
|
||||
...prop,
|
||||
value: getPropertyValue(objectData, prop.name)
|
||||
}))}
|
||||
/>
|
||||
</InfoCollapse>
|
||||
|
||||
<InfoCollapse
|
||||
title='Notes'
|
||||
icon={<NoteIcon />}
|
||||
active={collapseState.notes}
|
||||
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>
|
||||
)}
|
||||
<InfoCollapse
|
||||
title='Audit Logs'
|
||||
icon={<AuditLogIcon />}
|
||||
active={collapseState.auditLogs}
|
||||
onToggle={(expanded) =>
|
||||
updateCollapseState('auditLogs', expanded)
|
||||
}
|
||||
key='auditLogs'
|
||||
>
|
||||
{loading ? (
|
||||
<InfoCollapsePlaceholder />
|
||||
) : (
|
||||
<ObjectTable
|
||||
type='auditLog'
|
||||
masterFilter={{ 'parent._id': filamentId }}
|
||||
visibleColumns={{ _id: false, 'parent._id': false }}
|
||||
/>
|
||||
)}
|
||||
</InfoCollapse>
|
||||
</Flex>
|
||||
</div>
|
||||
</Flex>
|
||||
)}
|
||||
</ActionHandler>
|
||||
)
|
||||
}}
|
||||
</EditObjectForm>
|
||||
)
|
||||
}
|
||||
|
||||
@ -1,10 +1,8 @@
|
||||
import React from 'react'
|
||||
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 ReloadIcon from '../../../Icons/ReloadIcon'
|
||||
import useCollapseState from '../../hooks/useCollapseState'
|
||||
import AuditLogTable from '../../common/AuditLogTable'
|
||||
import InfoCollapse from '../../common/InfoCollapse'
|
||||
import ObjectInfo from '../../common/ObjectInfo'
|
||||
import ViewButton from '../../common/ViewButton'
|
||||
@ -13,10 +11,10 @@ import AuditLogIcon from '../../../Icons/AuditLogIcon.jsx'
|
||||
import EditObjectForm from '../../common/EditObjectForm'
|
||||
import EditButtons from '../../common/EditButtons'
|
||||
import LockIndicator from '../Filaments/LockIndicator'
|
||||
import {
|
||||
getModelProperties,
|
||||
getPropertyValue
|
||||
} from '../../../../database/ObjectModels.js'
|
||||
import ActionHandler from '../../common/ActionHandler.jsx'
|
||||
import ObjectActions from '../../common/ObjectActions.jsx'
|
||||
import ObjectTable from '../../common/ObjectTable.jsx'
|
||||
import InfoCollapsePlaceholder from '../../common/InfoCollapsePlaceholder.jsx'
|
||||
|
||||
const NoteTypeInfo = () => {
|
||||
const location = useLocation()
|
||||
@ -32,7 +30,7 @@ const NoteTypeInfo = () => {
|
||||
return (
|
||||
<EditObjectForm
|
||||
id={noteTypeId}
|
||||
type='notetype'
|
||||
type='noteType'
|
||||
style={{ height: 'calc(var(--unit-100vh) - 155px)', minHeight: 0 }}
|
||||
>
|
||||
{({
|
||||
@ -46,99 +44,121 @@ const NoteTypeInfo = () => {
|
||||
editLoading,
|
||||
lock,
|
||||
fetchObject
|
||||
}) => (
|
||||
<Flex
|
||||
gap='large'
|
||||
vertical='true'
|
||||
style={{ height: '100%', minHeight: 0 }}
|
||||
>
|
||||
<Flex justify={'space-between'}>
|
||||
<Space size='middle'>
|
||||
<Space size='small'>
|
||||
<Dropdown
|
||||
menu={{
|
||||
items: [
|
||||
{
|
||||
label: 'Reload Note Type',
|
||||
key: 'reload',
|
||||
icon: <ReloadIcon />
|
||||
}
|
||||
],
|
||||
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>
|
||||
}) => {
|
||||
// Define actions for ActionHandler
|
||||
const actions = {
|
||||
reload: () => {
|
||||
fetchObject()
|
||||
return true
|
||||
},
|
||||
edit: () => {
|
||||
startEditing()
|
||||
return false
|
||||
},
|
||||
cancelEdit: () => {
|
||||
cancelEditing()
|
||||
return true
|
||||
},
|
||||
finishEdit: () => {
|
||||
handleUpdate()
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
<div style={{ height: '100%', overflow: 'auto' }}>
|
||||
<Flex vertical gap={'large'}>
|
||||
<InfoCollapse
|
||||
title='Note Type Information'
|
||||
icon={<InfoCircleIcon />}
|
||||
active={collapseState.info}
|
||||
onToggle={(expanded) => updateCollapseState('info', expanded)}
|
||||
key='info'
|
||||
return (
|
||||
<ActionHandler actions={actions} loading={loading}>
|
||||
{({ callAction }) => (
|
||||
<Flex
|
||||
gap='large'
|
||||
vertical='true'
|
||||
style={{ height: '100%', minHeight: 0 }}
|
||||
>
|
||||
<ObjectInfo
|
||||
loading={loading}
|
||||
indicator={<LoadingOutlined />}
|
||||
isEditing={isEditing}
|
||||
type='noteType'
|
||||
items={getModelProperties('noteType').map((prop) => ({
|
||||
...prop,
|
||||
value: getPropertyValue(objectData, prop.name)
|
||||
}))}
|
||||
/>
|
||||
</InfoCollapse>
|
||||
<Flex justify={'space-between'}>
|
||||
<Space size='middle'>
|
||||
<Space size='small'>
|
||||
<ObjectActions
|
||||
type='noteType'
|
||||
id={noteTypeId}
|
||||
disabled={loading}
|
||||
/>
|
||||
<ViewButton
|
||||
disabled={loading}
|
||||
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
|
||||
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>
|
||||
)}
|
||||
<div style={{ height: '100%', overflow: 'auto' }}>
|
||||
<Flex vertical gap={'large'}>
|
||||
<InfoCollapse
|
||||
title='Note Type Information'
|
||||
icon={<InfoCircleIcon />}
|
||||
active={collapseState.info}
|
||||
onToggle={(expanded) =>
|
||||
updateCollapseState('info', expanded)
|
||||
}
|
||||
key='info'
|
||||
>
|
||||
<ObjectInfo
|
||||
loading={loading}
|
||||
indicator={<LoadingOutlined />}
|
||||
isEditing={isEditing}
|
||||
type='noteType'
|
||||
objectData={objectData}
|
||||
/>
|
||||
</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>
|
||||
)
|
||||
}
|
||||
|
||||
@ -1,11 +1,8 @@
|
||||
// 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 { AuthContext } from '../context/AuthContext'
|
||||
|
||||
import ObjectTable from '../common/ObjectTable'
|
||||
import NewProduct from './Products/NewProduct'
|
||||
|
||||
@ -20,14 +17,12 @@ import useViewMode from '../hooks/useViewMode'
|
||||
|
||||
import ColumnViewButton from '../common/ColumnViewButton'
|
||||
|
||||
const Parts = () => {
|
||||
const Parts = (filter) => {
|
||||
const [messageApi, contextHolder] = message.useMessage()
|
||||
|
||||
const [newProductOpen, setNewProductOpen] = useState(false)
|
||||
const tableRef = useRef()
|
||||
const { authenticated } = useContext(AuthContext)
|
||||
const [viewMode, setViewMode] = useViewMode('part')
|
||||
|
||||
const [columnVisibility, setColumnVisibility] = useColumnVisibility('part')
|
||||
|
||||
const actionItems = {
|
||||
@ -82,8 +77,8 @@ const Parts = () => {
|
||||
ref={tableRef}
|
||||
visibleColumns={columnVisibility}
|
||||
type='part'
|
||||
authenticated={authenticated}
|
||||
cards={viewMode === 'cards'}
|
||||
filter={filter}
|
||||
/>
|
||||
</Flex>
|
||||
<Modal
|
||||
|
||||
@ -1,709 +1,187 @@
|
||||
import React, { useState, useEffect, useRef } from 'react'
|
||||
import React, { useContext } from 'react'
|
||||
import { useLocation } from 'react-router-dom'
|
||||
import axios from 'axios'
|
||||
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 { Space, Flex, Card } from 'antd'
|
||||
import useCollapseState from '../../hooks/useCollapseState'
|
||||
import TimeDisplay from '../../common/TimeDisplay.jsx'
|
||||
import AuditLogTable from '../../common/AuditLogTable'
|
||||
import NotesPanel from '../../common/NotesPanel'
|
||||
|
||||
import config from '../../../../config.js'
|
||||
import BoolDisplay from '../../common/BoolDisplay.jsx'
|
||||
import InfoCollapse from '../../common/InfoCollapse'
|
||||
import ObjectInfo from '../../common/ObjectInfo'
|
||||
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 PartIcon from '../../../Icons/PartIcon.jsx'
|
||||
import NoteIcon from '../../../Icons/NoteIcon.jsx'
|
||||
import AuditLogIcon from '../../../Icons/AuditLogIcon.jsx'
|
||||
import loglevel from 'loglevel'
|
||||
const logger = loglevel.getLogger('PartInfo')
|
||||
logger.setLevel(config.logLevel)
|
||||
|
||||
const { Title, Text } = Typography
|
||||
import ActionHandler from '../../common/ActionHandler.jsx'
|
||||
import ObjectActions from '../../common/ObjectActions.jsx'
|
||||
import ObjectTable from '../../common/ObjectTable.jsx'
|
||||
import InfoCollapsePlaceholder from '../../common/InfoCollapsePlaceholder.jsx'
|
||||
import { ApiServerContext } from '../../context/ApiServerContext'
|
||||
|
||||
const PartInfo = () => {
|
||||
const [partData, setPartData] = useState(null)
|
||||
const [loading, setLoading] = useState(false)
|
||||
const [error, setError] = useState(null)
|
||||
const location = useLocation()
|
||||
const [messageApi, contextHolder] = message.useMessage()
|
||||
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', {
|
||||
info: true,
|
||||
preview: true,
|
||||
parts: true,
|
||||
notes: 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 (
|
||||
<>
|
||||
{contextHolder}
|
||||
<Flex
|
||||
gap='large'
|
||||
vertical='true'
|
||||
style={{ height: '100%', minHeight: 0 }}
|
||||
>
|
||||
<Flex justify={'space-between'}>
|
||||
<Space size='small'>
|
||||
<Dropdown menu={actionItems}>
|
||||
<Button>Actions</Button>
|
||||
</Dropdown>
|
||||
<Popover
|
||||
content={getViewDropdownItems()}
|
||||
placement='bottomLeft'
|
||||
arrow={false}
|
||||
>
|
||||
<Button>View</Button>
|
||||
</Popover>
|
||||
</Space>
|
||||
<Space>
|
||||
{isEditing ? (
|
||||
<>
|
||||
<Button
|
||||
icon={<CheckIcon />}
|
||||
type='primary'
|
||||
onClick={updateInfo}
|
||||
loading={loading}
|
||||
disabled={loading}
|
||||
/>
|
||||
<Button
|
||||
icon={<XMarkIcon />}
|
||||
onClick={cancelEditing}
|
||||
disabled={loading}
|
||||
/>
|
||||
</>
|
||||
) : (
|
||||
<Button icon={<EditIcon />} onClick={startEditing} />
|
||||
)}
|
||||
</Space>
|
||||
</Flex>
|
||||
|
||||
{error ? (
|
||||
<Space
|
||||
direction='vertical'
|
||||
style={{ width: '100%', textAlign: 'center' }}
|
||||
>
|
||||
<p>{error || 'Part not found'}</p>
|
||||
<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'
|
||||
<EditObjectForm
|
||||
id={partId}
|
||||
type='part'
|
||||
style={{ height: 'calc(var(--unit-100vh) - 155px)', minHeight: 0 }}
|
||||
>
|
||||
{({
|
||||
loading,
|
||||
isEditing,
|
||||
startEditing,
|
||||
cancelEditing,
|
||||
handleUpdate,
|
||||
formValid,
|
||||
objectData,
|
||||
editLoading,
|
||||
lock,
|
||||
fetchObject
|
||||
}) => {
|
||||
const actions = {
|
||||
reload: () => {
|
||||
fetchObject()
|
||||
return true
|
||||
},
|
||||
edit: () => {
|
||||
startEditing()
|
||||
return false
|
||||
},
|
||||
cancelEdit: () => {
|
||||
cancelEditing()
|
||||
return true
|
||||
},
|
||||
finishEdit: () => {
|
||||
handleUpdate()
|
||||
return true
|
||||
},
|
||||
download: () => {
|
||||
if (partId) {
|
||||
handleDownloadContent(partId, 'part', `${objectData.name}.stl`)
|
||||
return true
|
||||
}
|
||||
}
|
||||
}
|
||||
return (
|
||||
<ActionHandler actions={actions} loading={loading}>
|
||||
{({ callAction }) => (
|
||||
<Flex
|
||||
gap='large'
|
||||
vertical='true'
|
||||
style={{ height: '100%', minHeight: 0 }}
|
||||
>
|
||||
<Collapse.Panel
|
||||
header={
|
||||
<Flex align='center' gap={'middle'}>
|
||||
<InfoCircleIcon />
|
||||
<Title level={5} style={{ margin: 0 }}>
|
||||
Part Information
|
||||
</Title>
|
||||
</Flex>
|
||||
}
|
||||
key='1'
|
||||
>
|
||||
<Form
|
||||
form={partForm}
|
||||
layout='vertical'
|
||||
onValuesChange={(changedValues) =>
|
||||
setPartFormValues((prevValues) => ({
|
||||
...prevValues,
|
||||
...changedValues
|
||||
}))
|
||||
}
|
||||
initialValues={{
|
||||
name: partData?.name || '',
|
||||
version: partData?.version || '',
|
||||
tags: partData?.tags || []
|
||||
}}
|
||||
>
|
||||
<Spin
|
||||
indicator={<LoadingOutlined />}
|
||||
spinning={fetchLoading}
|
||||
<Flex justify={'space-between'}>
|
||||
<Space size='middle'>
|
||||
<Space size='small'>
|
||||
<ObjectActions
|
||||
type='part'
|
||||
id={partId}
|
||||
disabled={loading}
|
||||
/>
|
||||
<ViewButton
|
||||
disabled={loading}
|
||||
items={[
|
||||
{ key: 'info', label: 'Part 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='Part Information'
|
||||
icon={<InfoCircleIcon />}
|
||||
active={collapseState.info}
|
||||
onToggle={(expanded) =>
|
||||
updateCollapseState('info', expanded)
|
||||
}
|
||||
key='info'
|
||||
>
|
||||
<Descriptions
|
||||
bordered
|
||||
column={{
|
||||
xs: 1,
|
||||
sm: 1,
|
||||
md: 1,
|
||||
lg: 2,
|
||||
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>
|
||||
<ObjectInfo
|
||||
loading={loading}
|
||||
isEditing={isEditing}
|
||||
type='part'
|
||||
objectData={objectData}
|
||||
/>
|
||||
</InfoCollapse>
|
||||
|
||||
<Descriptions.Item label='Name' span={1}>
|
||||
{isEditing ? (
|
||||
<Form.Item
|
||||
name='name'
|
||||
rules={[
|
||||
{
|
||||
required: true,
|
||||
message: 'Please enter a product name'
|
||||
},
|
||||
{
|
||||
max: 100,
|
||||
message: 'Name cannot exceed 100 characters'
|
||||
}
|
||||
]}
|
||||
style={{ margin: 0 }}
|
||||
>
|
||||
<Input placeholder='Enter product name' />
|
||||
</Form.Item>
|
||||
) : partData?.name ? (
|
||||
<Text>{partData.name}</Text>
|
||||
) : (
|
||||
<Text>n/a</Text>
|
||||
)}
|
||||
</Descriptions.Item>
|
||||
<InfoCollapse
|
||||
title='Notes'
|
||||
icon={<NoteIcon />}
|
||||
active={collapseState.notes}
|
||||
onToggle={(expanded) =>
|
||||
updateCollapseState('notes', expanded)
|
||||
}
|
||||
key='notes'
|
||||
>
|
||||
<Card>
|
||||
<NotesPanel _id={partId} />
|
||||
</Card>
|
||||
</InfoCollapse>
|
||||
|
||||
<Descriptions.Item label='Updated At'>
|
||||
{partData?.updatedAt ? (
|
||||
<TimeDisplay
|
||||
dateTime={partData.updatedAt}
|
||||
showSince={true}
|
||||
/>
|
||||
) : (
|
||||
<Text>n/a</Text>
|
||||
)}
|
||||
</Descriptions.Item>
|
||||
|
||||
<Descriptions.Item label='Product Name' span={1}>
|
||||
{partData?.product?.name ? (
|
||||
<Text>{partData.product.name}</Text>
|
||||
) : (
|
||||
<Text>n/a</Text>
|
||||
)}
|
||||
</Descriptions.Item>
|
||||
<Descriptions.Item label='Product ID' span={1}>
|
||||
{partData?.product?._id ? (
|
||||
<IdDisplay
|
||||
id={partData.product._id}
|
||||
type={'product'}
|
||||
showHyperlink={true}
|
||||
/>
|
||||
) : (
|
||||
<Text>n/a</Text>
|
||||
)}
|
||||
</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>
|
||||
</>
|
||||
<InfoCollapse
|
||||
title='Audit Logs'
|
||||
icon={<AuditLogIcon />}
|
||||
active={collapseState.auditLogs}
|
||||
onToggle={(expanded) =>
|
||||
updateCollapseState('auditLogs', expanded)
|
||||
}
|
||||
key='auditLogs'
|
||||
>
|
||||
{loading ? (
|
||||
<InfoCollapsePlaceholder />
|
||||
) : (
|
||||
<ObjectTable
|
||||
type='auditLog'
|
||||
masterFilter={{ 'parent._id': partId }}
|
||||
visibleColumns={{ _id: false, 'parent._id': false }}
|
||||
/>
|
||||
)}
|
||||
</InfoCollapse>
|
||||
</Flex>
|
||||
</div>
|
||||
</Flex>
|
||||
)}
|
||||
</ActionHandler>
|
||||
)
|
||||
}}
|
||||
</EditObjectForm>
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
@ -1,9 +1,7 @@
|
||||
import React from 'react'
|
||||
import { useLocation } from 'react-router-dom'
|
||||
import { Space, Button, Flex, Dropdown, Card } from 'antd'
|
||||
import ReloadIcon from '../../../Icons/ReloadIcon'
|
||||
import { Space, Flex, Card } from 'antd'
|
||||
import useCollapseState from '../../hooks/useCollapseState'
|
||||
import AuditLogTable from '../../common/AuditLogTable'
|
||||
import NotesPanel from '../../common/NotesPanel'
|
||||
import InfoCollapse from '../../common/InfoCollapse'
|
||||
import ObjectInfo from '../../common/ObjectInfo'
|
||||
@ -11,11 +9,14 @@ import ViewButton from '../../common/ViewButton'
|
||||
import EditObjectForm from '../../common/EditObjectForm'
|
||||
import EditButtons from '../../common/EditButtons'
|
||||
import LockIndicator from '../Filaments/LockIndicator'
|
||||
import PartsTable from '../../common/PartsTable'
|
||||
import InfoCircleIcon from '../../../Icons/InfoCircleIcon.jsx'
|
||||
import NoteIcon from '../../../Icons/NoteIcon.jsx'
|
||||
import AuditLogIcon from '../../../Icons/AuditLogIcon.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 location = useLocation()
|
||||
@ -44,191 +45,153 @@ const ProductInfo = () => {
|
||||
editLoading,
|
||||
lock,
|
||||
fetchObject
|
||||
}) => (
|
||||
<Flex
|
||||
gap='large'
|
||||
vertical='true'
|
||||
style={{ height: '100%', minHeight: 0 }}
|
||||
>
|
||||
<Flex justify={'space-between'}>
|
||||
<Space size='middle'>
|
||||
<Space size='small'>
|
||||
<Dropdown
|
||||
menu={{
|
||||
items: [
|
||||
{
|
||||
label: 'Reload Product',
|
||||
key: 'reload',
|
||||
icon: <ReloadIcon />
|
||||
}) => {
|
||||
const actions = {
|
||||
reload: () => {
|
||||
fetchObject()
|
||||
return true
|
||||
},
|
||||
edit: () => {
|
||||
startEditing()
|
||||
return false
|
||||
},
|
||||
cancelEdit: () => {
|
||||
cancelEditing()
|
||||
return true
|
||||
},
|
||||
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='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)
|
||||
}
|
||||
],
|
||||
onClick: ({ key }) => {
|
||||
if (key === 'reload') {
|
||||
fetchObject()
|
||||
key='info'
|
||||
>
|
||||
<ObjectInfo
|
||||
loading={loading}
|
||||
isEditing={isEditing}
|
||||
type='product'
|
||||
objectData={objectData}
|
||||
/>
|
||||
</InfoCollapse>
|
||||
|
||||
<InfoCollapse
|
||||
title='Product Parts'
|
||||
icon={<ProductIcon />}
|
||||
active={collapseState.parts}
|
||||
onToggle={(expanded) =>
|
||||
updateCollapseState('parts', expanded)
|
||||
}
|
||||
}
|
||||
}}
|
||||
>
|
||||
<Button disabled={loading}>Actions</Button>
|
||||
</Dropdown>
|
||||
<ViewButton
|
||||
loading={loading}
|
||||
sections={[
|
||||
{ key: 'info', label: 'Product Information' },
|
||||
{ key: 'parts', label: 'Product Parts' },
|
||||
{ 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>
|
||||
key='parts'
|
||||
>
|
||||
<ObjectTable
|
||||
type='part'
|
||||
visibleColumns={{
|
||||
product: false,
|
||||
'product._id': false
|
||||
}}
|
||||
masterFilter={{ 'product._id': productId }}
|
||||
/>
|
||||
</InfoCollapse>
|
||||
|
||||
<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'
|
||||
>
|
||||
<ObjectInfo
|
||||
loading={loading}
|
||||
isEditing={isEditing}
|
||||
indicator={null}
|
||||
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
|
||||
title='Notes'
|
||||
icon={<NoteIcon />}
|
||||
active={collapseState.notes}
|
||||
onToggle={(expanded) =>
|
||||
updateCollapseState('notes', expanded)
|
||||
}
|
||||
key='notes'
|
||||
>
|
||||
<Card>
|
||||
<NotesPanel _id={productId} />
|
||||
</Card>
|
||||
</InfoCollapse>
|
||||
|
||||
<InfoCollapse
|
||||
title='Product Parts'
|
||||
icon={<ProductIcon />}
|
||||
active={collapseState.parts}
|
||||
onToggle={(expanded) => updateCollapseState('parts', expanded)}
|
||||
key='parts'
|
||||
>
|
||||
<PartsTable data={objectData?.parts || []} />
|
||||
</InfoCollapse>
|
||||
|
||||
<InfoCollapse
|
||||
title='Notes'
|
||||
icon={<NoteIcon />}
|
||||
active={collapseState.notes}
|
||||
onToggle={(expanded) => updateCollapseState('notes', expanded)}
|
||||
key='notes'
|
||||
>
|
||||
<Card>
|
||||
<NotesPanel _id={productId} />
|
||||
</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>
|
||||
)}
|
||||
<InfoCollapse
|
||||
title='Audit Logs'
|
||||
icon={<AuditLogIcon />}
|
||||
active={collapseState.auditLogs}
|
||||
onToggle={(expanded) =>
|
||||
updateCollapseState('auditLogs', expanded)
|
||||
}
|
||||
key='auditLogs'
|
||||
>
|
||||
{loading ? (
|
||||
<InfoCollapsePlaceholder />
|
||||
) : (
|
||||
<ObjectTable
|
||||
type='auditLog'
|
||||
masterFilter={{ 'parent._id': productId }}
|
||||
visibleColumns={{ _id: false, 'parent._id': false }}
|
||||
/>
|
||||
)}
|
||||
</InfoCollapse>
|
||||
</Flex>
|
||||
</div>
|
||||
</Flex>
|
||||
)}
|
||||
</ActionHandler>
|
||||
)
|
||||
}}
|
||||
</EditObjectForm>
|
||||
)
|
||||
}
|
||||
|
||||
@ -40,9 +40,9 @@ const Users = () => {
|
||||
</Dropdown>
|
||||
<ColumnViewButton
|
||||
type='user'
|
||||
loading={false}
|
||||
collapseState={columnVisibility}
|
||||
updateCollapseState={setColumnVisibility}
|
||||
disabled={false}
|
||||
visibleState={columnVisibility}
|
||||
updateVisibleState={setColumnVisibility}
|
||||
/>
|
||||
</Space>
|
||||
<Space>
|
||||
|
||||
@ -1,10 +1,8 @@
|
||||
import React from 'react'
|
||||
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 ReloadIcon from '../../../Icons/ReloadIcon'
|
||||
import useCollapseState from '../../hooks/useCollapseState'
|
||||
import AuditLogTable from '../../common/AuditLogTable'
|
||||
import NotesPanel from '../../common/NotesPanel'
|
||||
import InfoCollapse from '../../common/InfoCollapse'
|
||||
import ObjectInfo from '../../common/ObjectInfo'
|
||||
@ -15,10 +13,10 @@ import AuditLogIcon from '../../../Icons/AuditLogIcon.jsx'
|
||||
import EditObjectForm from '../../common/EditObjectForm'
|
||||
import EditButtons from '../../common/EditButtons'
|
||||
import LockIndicator from '../Filaments/LockIndicator'
|
||||
import {
|
||||
getModelProperties,
|
||||
getPropertyValue
|
||||
} from '../../../../database/ObjectModels.js'
|
||||
import ActionHandler from '../../common/ActionHandler'
|
||||
import ObjectActions from '../../common/ObjectActions.jsx'
|
||||
import ObjectTable from '../../common/ObjectTable.jsx'
|
||||
import InfoCollapsePlaceholder from '../../common/InfoCollapsePlaceholder.jsx'
|
||||
|
||||
const UserInfo = () => {
|
||||
const location = useLocation()
|
||||
@ -46,112 +44,136 @@ const UserInfo = () => {
|
||||
editLoading,
|
||||
lock,
|
||||
fetchObject
|
||||
}) => (
|
||||
<Flex
|
||||
gap='large'
|
||||
vertical='true'
|
||||
style={{ height: '100%', minHeight: 0 }}
|
||||
>
|
||||
<Flex justify={'space-between'}>
|
||||
<Space size='middle'>
|
||||
<Space size='small'>
|
||||
<Dropdown
|
||||
menu={{
|
||||
items: [
|
||||
{
|
||||
label: 'Reload User',
|
||||
key: 'reload',
|
||||
icon: <ReloadIcon />
|
||||
}) => {
|
||||
// Define actions for ActionHandler
|
||||
const actions = {
|
||||
reload: () => {
|
||||
fetchObject()
|
||||
return true
|
||||
},
|
||||
edit: () => {
|
||||
startEditing()
|
||||
return false
|
||||
},
|
||||
cancelEdit: () => {
|
||||
cancelEditing()
|
||||
return true
|
||||
},
|
||||
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)
|
||||
}
|
||||
],
|
||||
onClick: ({ key }) => {
|
||||
if (key === 'reload') {
|
||||
fetchObject()
|
||||
key='info'
|
||||
>
|
||||
<ObjectInfo
|
||||
loading={loading}
|
||||
indicator={<LoadingOutlined />}
|
||||
isEditing={isEditing}
|
||||
type='user'
|
||||
objectData={objectData}
|
||||
/>
|
||||
</InfoCollapse>
|
||||
|
||||
<InfoCollapse
|
||||
title='Notes'
|
||||
icon={<NoteIcon />}
|
||||
active={collapseState.notes}
|
||||
onToggle={(expanded) =>
|
||||
updateCollapseState('notes', expanded)
|
||||
}
|
||||
}
|
||||
}}
|
||||
>
|
||||
<Button disabled={loading}>Actions</Button>
|
||||
</Dropdown>
|
||||
<ViewButton
|
||||
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>
|
||||
key='notes'
|
||||
>
|
||||
<Card>
|
||||
<NotesPanel _id={userId} />
|
||||
</Card>
|
||||
</InfoCollapse>
|
||||
|
||||
<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'
|
||||
>
|
||||
<ObjectInfo
|
||||
loading={loading}
|
||||
indicator={<LoadingOutlined />}
|
||||
isEditing={isEditing}
|
||||
type='user'
|
||||
items={getModelProperties('user').map((prop) => ({
|
||||
...prop,
|
||||
value: getPropertyValue(objectData, prop.name)
|
||||
}))}
|
||||
/>
|
||||
</InfoCollapse>
|
||||
|
||||
<InfoCollapse
|
||||
title='Notes'
|
||||
icon={<NoteIcon />}
|
||||
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>
|
||||
)}
|
||||
<InfoCollapse
|
||||
title='Audit Logs'
|
||||
icon={<AuditLogIcon />}
|
||||
active={collapseState.auditLogs}
|
||||
onToggle={(expanded) =>
|
||||
updateCollapseState('auditLogs', expanded)
|
||||
}
|
||||
key='auditLogs'
|
||||
>
|
||||
{loading ? (
|
||||
<InfoCollapsePlaceholder />
|
||||
) : (
|
||||
<ObjectTable
|
||||
type='auditLog'
|
||||
masterFilter={{ 'parent._id': userId }}
|
||||
visibleColumns={{ _id: false, 'parent._id': false }}
|
||||
/>
|
||||
)}
|
||||
</InfoCollapse>
|
||||
</Flex>
|
||||
</div>
|
||||
</Flex>
|
||||
)}
|
||||
</ActionHandler>
|
||||
)
|
||||
}}
|
||||
</EditObjectForm>
|
||||
)
|
||||
}
|
||||
|
||||
@ -1,12 +1,9 @@
|
||||
import React from 'react'
|
||||
import { useLocation } from 'react-router-dom'
|
||||
import { Space, Button, Flex, Dropdown, Card } from 'antd'
|
||||
import { LoadingOutlined } from '@ant-design/icons'
|
||||
import { Space, Flex, Card } from 'antd'
|
||||
import loglevel from 'loglevel'
|
||||
import config from '../../../../config'
|
||||
import ReloadIcon from '../../../Icons/ReloadIcon'
|
||||
import useCollapseState from '../../hooks/useCollapseState'
|
||||
import AuditLogTable from '../../common/AuditLogTable'
|
||||
import NotesPanel from '../../common/NotesPanel'
|
||||
import InfoCollapse from '../../common/InfoCollapse'
|
||||
import ObjectInfo from '../../common/ObjectInfo'
|
||||
@ -17,10 +14,10 @@ import AuditLogIcon from '../../../Icons/AuditLogIcon.jsx'
|
||||
import EditObjectForm from '../../common/EditObjectForm'
|
||||
import EditButtons from '../../common/EditButtons'
|
||||
import LockIndicator from '../Filaments/LockIndicator'
|
||||
import {
|
||||
getModelProperties,
|
||||
getPropertyValue
|
||||
} from '../../../../database/ObjectModels'
|
||||
import ActionHandler from '../../common/ActionHandler.jsx'
|
||||
import ObjectActions from '../../common/ObjectActions.jsx'
|
||||
import ObjectTable from '../../common/ObjectTable.jsx'
|
||||
import InfoCollapsePlaceholder from '../../common/InfoCollapsePlaceholder.jsx'
|
||||
|
||||
const log = loglevel.getLogger('VendorInfo')
|
||||
log.setLevel(config.logLevel)
|
||||
@ -51,111 +48,135 @@ const VendorInfo = () => {
|
||||
editLoading,
|
||||
lock,
|
||||
fetchObject
|
||||
}) => (
|
||||
<Flex
|
||||
gap='large'
|
||||
vertical='true'
|
||||
style={{ height: '100%', minHeight: 0 }}
|
||||
>
|
||||
<Flex justify={'space-between'}>
|
||||
<Space size='middle'>
|
||||
<Space size='small'>
|
||||
<Dropdown
|
||||
menu={{
|
||||
items: [
|
||||
{
|
||||
label: 'Reload Vendor',
|
||||
key: 'reload',
|
||||
icon: <ReloadIcon />
|
||||
}) => {
|
||||
// Define actions for ActionHandler
|
||||
const actions = {
|
||||
reload: () => {
|
||||
fetchObject()
|
||||
return true
|
||||
},
|
||||
edit: () => {
|
||||
startEditing()
|
||||
return false
|
||||
},
|
||||
cancelEdit: () => {
|
||||
cancelEditing()
|
||||
return true
|
||||
},
|
||||
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)
|
||||
}
|
||||
],
|
||||
onClick: ({ key }) => {
|
||||
if (key === 'reload') {
|
||||
fetchObject()
|
||||
key='info'
|
||||
>
|
||||
<ObjectInfo
|
||||
loading={loading}
|
||||
isEditing={isEditing}
|
||||
type='vendor'
|
||||
objectData={objectData}
|
||||
/>
|
||||
</InfoCollapse>
|
||||
|
||||
<InfoCollapse
|
||||
title='Notes'
|
||||
icon={<NoteIcon />}
|
||||
active={collapseState.notes}
|
||||
onToggle={(expanded) =>
|
||||
updateCollapseState('notes', expanded)
|
||||
}
|
||||
}
|
||||
}}
|
||||
>
|
||||
<Button disabled={loading}>Actions</Button>
|
||||
</Dropdown>
|
||||
<ViewButton
|
||||
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>
|
||||
key='notes'
|
||||
>
|
||||
<Card>
|
||||
<NotesPanel _id={vendorId} />
|
||||
</Card>
|
||||
</InfoCollapse>
|
||||
|
||||
<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'
|
||||
>
|
||||
<ObjectInfo
|
||||
loading={loading}
|
||||
indicator={<LoadingOutlined />}
|
||||
isEditing={isEditing}
|
||||
items={getModelProperties('vendor').map((prop) => ({
|
||||
...prop,
|
||||
value: getPropertyValue(objectData, prop.name)
|
||||
}))}
|
||||
/>
|
||||
</InfoCollapse>
|
||||
|
||||
<InfoCollapse
|
||||
title='Notes'
|
||||
icon={<NoteIcon />}
|
||||
active={collapseState.notes}
|
||||
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>
|
||||
)}
|
||||
<InfoCollapse
|
||||
title='Audit Logs'
|
||||
icon={<AuditLogIcon />}
|
||||
active={collapseState.auditLogs}
|
||||
onToggle={(expanded) =>
|
||||
updateCollapseState('auditLogs', expanded)
|
||||
}
|
||||
key='auditLogs'
|
||||
>
|
||||
{loading ? (
|
||||
<InfoCollapsePlaceholder />
|
||||
) : (
|
||||
<ObjectTable
|
||||
type='auditLog'
|
||||
masterFilter={{ 'parent._id': vendorId }}
|
||||
visibleColumns={{ _id: false, 'parent._id': false }}
|
||||
/>
|
||||
)}
|
||||
</InfoCollapse>
|
||||
</Flex>
|
||||
</div>
|
||||
</Flex>
|
||||
)}
|
||||
</ActionHandler>
|
||||
)
|
||||
}}
|
||||
</EditObjectForm>
|
||||
)
|
||||
}
|
||||
|
||||
@ -1,8 +1,7 @@
|
||||
// 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 { AuthContext } from '../context/AuthContext'
|
||||
import NewGCodeFile from './GCodeFiles/NewGCodeFile'
|
||||
import useColumnVisibility from '../hooks/useColumnVisibility'
|
||||
import PlusIcon from '../../Icons/PlusIcon'
|
||||
@ -23,8 +22,6 @@ const GCodeFiles = () => {
|
||||
const [columnVisibility, setColumnVisibility] =
|
||||
useColumnVisibility('gcodeFile')
|
||||
|
||||
const { authenticated } = useContext(AuthContext)
|
||||
|
||||
const actionItems = {
|
||||
items: [
|
||||
{
|
||||
@ -59,9 +56,9 @@ const GCodeFiles = () => {
|
||||
</Dropdown>
|
||||
<ColumnViewButton
|
||||
type='gcodeFile'
|
||||
loading={false}
|
||||
collapseState={columnVisibility}
|
||||
updateCollapseState={setColumnVisibility}
|
||||
disabled={false}
|
||||
visibleState={columnVisibility}
|
||||
updateVisibleState={setColumnVisibility}
|
||||
/>
|
||||
</Space>
|
||||
<Space>
|
||||
@ -75,8 +72,7 @@ const GCodeFiles = () => {
|
||||
</Flex>
|
||||
<ObjectTable
|
||||
ref={tableRef}
|
||||
type={'gcodeFile'}
|
||||
authenticated={authenticated}
|
||||
type='gcodeFile'
|
||||
cards={viewMode === 'cards'}
|
||||
visibleColumns={columnVisibility}
|
||||
/>
|
||||
|
||||
@ -1,10 +1,8 @@
|
||||
import React, { useContext } from 'react'
|
||||
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 ReloadIcon from '../../../Icons/ReloadIcon'
|
||||
import useCollapseState from '../../hooks/useCollapseState'
|
||||
import AuditLogTable from '../../common/AuditLogTable'
|
||||
import NotesPanel from '../../common/NotesPanel'
|
||||
import InfoCollapse from '../../common/InfoCollapse'
|
||||
import ObjectInfo from '../../common/ObjectInfo'
|
||||
@ -18,10 +16,9 @@ import NoteIcon from '../../../Icons/NoteIcon.jsx'
|
||||
import AuditLogIcon from '../../../Icons/AuditLogIcon.jsx'
|
||||
import GCodeFileIcon from '../../../Icons/GCodeFileIcon.jsx'
|
||||
import { ApiServerContext } from '../../context/ApiServerContext'
|
||||
import {
|
||||
getModelProperties,
|
||||
getPropertyValue
|
||||
} from '../../../../database/ObjectModels.js'
|
||||
import ObjectActions from '../../common/ObjectActions.jsx'
|
||||
import ObjectTable from '../../common/ObjectTable.jsx'
|
||||
import InfoCollapsePlaceholder from '../../common/InfoCollapsePlaceholder.jsx'
|
||||
|
||||
const { Text } = Typography
|
||||
|
||||
@ -31,7 +28,7 @@ const GCodeFileInfo = () => {
|
||||
|
||||
const { handleDownloadContent } = useContext(ApiServerContext)
|
||||
const [collapseState, updateCollapseState] = useCollapseState(
|
||||
'GCodeFileInfo',
|
||||
'gcodeFileInfo',
|
||||
{
|
||||
info: 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 (
|
||||
<>
|
||||
<ActionHandler actions={actions} />
|
||||
<EditObjectForm
|
||||
id={gcodeFileId}
|
||||
type='gcodefile'
|
||||
style={{ height: 'calc(var(--unit-100vh) - 155px)', minHeight: 0 }}
|
||||
>
|
||||
{({
|
||||
loading,
|
||||
isEditing,
|
||||
startEditing,
|
||||
cancelEditing,
|
||||
handleUpdate,
|
||||
formValid,
|
||||
objectData,
|
||||
editLoading,
|
||||
lock,
|
||||
fetchObject
|
||||
}) => (
|
||||
<Flex
|
||||
gap='large'
|
||||
vertical='true'
|
||||
style={{ height: '100%', minHeight: 0 }}
|
||||
>
|
||||
<Flex justify={'space-between'}>
|
||||
<Space size='middle'>
|
||||
<Space size='small'>
|
||||
<Dropdown
|
||||
menu={{
|
||||
items: [
|
||||
{
|
||||
label: 'Reload GCode File',
|
||||
key: 'reload',
|
||||
icon: <ReloadIcon />
|
||||
},
|
||||
{
|
||||
label: 'Download GCode File',
|
||||
key: 'download',
|
||||
icon: <GCodeFileIcon />
|
||||
}
|
||||
],
|
||||
onClick: ({ key }) => {
|
||||
if (key === 'reload') {
|
||||
fetchObject()
|
||||
} else if (key === 'download' && gcodeFileId) {
|
||||
handleDownloadContent(
|
||||
gcodeFileId,
|
||||
'gcodefile',
|
||||
`gcodefile-${gcodeFileId}.gcode`
|
||||
)
|
||||
}
|
||||
}
|
||||
}}
|
||||
>
|
||||
<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>
|
||||
<EditObjectForm
|
||||
id={gcodeFileId}
|
||||
type='gcodefile'
|
||||
style={{ height: 'calc(var(--unit-100vh) - 155px)', minHeight: 0 }}
|
||||
>
|
||||
{({
|
||||
loading,
|
||||
isEditing,
|
||||
startEditing,
|
||||
cancelEditing,
|
||||
handleUpdate,
|
||||
formValid,
|
||||
objectData,
|
||||
editLoading,
|
||||
lock,
|
||||
fetchObject
|
||||
}) => {
|
||||
// Define actions that can be triggered via URL, now with access to startEditing
|
||||
const actions = {
|
||||
reload: () => {
|
||||
fetchObject()
|
||||
return true
|
||||
},
|
||||
edit: () => {
|
||||
startEditing()
|
||||
return false
|
||||
},
|
||||
cancelEdit: () => {
|
||||
cancelEditing()
|
||||
return true
|
||||
},
|
||||
finishEdit: () => {
|
||||
handleUpdate()
|
||||
return true
|
||||
},
|
||||
download: () => {
|
||||
if (gcodeFileId) {
|
||||
handleDownloadContent(
|
||||
gcodeFileId,
|
||||
'gcodeFile',
|
||||
`${objectData.name}.gcode`
|
||||
)
|
||||
return true
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
<div style={{ height: '100%', overflow: 'auto' }}>
|
||||
<Flex vertical gap={'large'}>
|
||||
<InfoCollapse
|
||||
title='GCode File Information'
|
||||
icon={<InfoCircleIcon />}
|
||||
active={collapseState.info}
|
||||
onToggle={(expanded) => updateCollapseState('info', expanded)}
|
||||
key='info'
|
||||
>
|
||||
<ObjectInfo
|
||||
loading={loading}
|
||||
indicator={<LoadingOutlined />}
|
||||
isEditing={isEditing}
|
||||
items={getModelProperties('gcodeFile').map((prop) => ({
|
||||
...prop,
|
||||
value: getPropertyValue(objectData, prop.name)
|
||||
}))}
|
||||
objectData={objectData}
|
||||
type='gcodefile'
|
||||
/>
|
||||
</InfoCollapse>
|
||||
|
||||
<InfoCollapse
|
||||
title='GCode File Preview'
|
||||
icon={<GCodeFileIcon />}
|
||||
active={collapseState.preview}
|
||||
onToggle={(expanded) =>
|
||||
updateCollapseState('preview', expanded)
|
||||
}
|
||||
key='preview'
|
||||
>
|
||||
<Card>
|
||||
{objectData?.gcodeFileInfo?.thumbnail ? (
|
||||
<img
|
||||
src={`data:image/png;base64,${objectData.gcodeFileInfo.thumbnail.data}`}
|
||||
alt='GCodeFile'
|
||||
style={{ maxWidth: '100%' }}
|
||||
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='gcodeFile'
|
||||
id={gcodeFileId}
|
||||
disabled={loading}
|
||||
/>
|
||||
) : (
|
||||
<Text>n/a</Text>
|
||||
)}
|
||||
</Card>
|
||||
</InfoCollapse>
|
||||
<ViewButton
|
||||
disabled={loading}
|
||||
items={[
|
||||
{ key: 'info', label: 'GCode File Information' },
|
||||
{ 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
|
||||
title='Notes'
|
||||
icon={<NoteIcon />}
|
||||
active={collapseState.notes}
|
||||
onToggle={(expanded) =>
|
||||
updateCollapseState('notes', expanded)
|
||||
}
|
||||
key='notes'
|
||||
>
|
||||
<Card>
|
||||
<NotesPanel _id={gcodeFileId} />
|
||||
</Card>
|
||||
</InfoCollapse>
|
||||
<div style={{ height: '100%', overflow: 'auto' }}>
|
||||
<Flex vertical gap={'large'}>
|
||||
<InfoCollapse
|
||||
title='GCode File Information'
|
||||
icon={<InfoCircleIcon />}
|
||||
active={collapseState.info}
|
||||
onToggle={(expanded) =>
|
||||
updateCollapseState('info', expanded)
|
||||
}
|
||||
key='info'
|
||||
>
|
||||
<ObjectInfo
|
||||
loading={loading}
|
||||
indicator={<LoadingOutlined />}
|
||||
isEditing={isEditing}
|
||||
objectData={objectData}
|
||||
type='gcodeFile'
|
||||
/>
|
||||
</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>
|
||||
<InfoCollapse
|
||||
title='GCode File Preview'
|
||||
icon={<GCodeFileIcon />}
|
||||
active={collapseState.preview}
|
||||
onToggle={(expanded) =>
|
||||
updateCollapseState('preview', expanded)
|
||||
}
|
||||
key='preview'
|
||||
>
|
||||
<Card>
|
||||
{objectData?.gcodeFileInfo?.thumbnail ? (
|
||||
<img
|
||||
src={`data:image/png;base64,${objectData.gcodeFileInfo.thumbnail.data}`}
|
||||
alt='GCodeFile'
|
||||
style={{ maxWidth: '100%' }}
|
||||
/>
|
||||
) : (
|
||||
<Text>n/a</Text>
|
||||
)}
|
||||
</Card>
|
||||
</InfoCollapse>
|
||||
|
||||
<InfoCollapse
|
||||
title='Notes'
|
||||
icon={<NoteIcon />}
|
||||
active={collapseState.notes}
|
||||
onToggle={(expanded) =>
|
||||
updateCollapseState('notes', expanded)
|
||||
}
|
||||
key='notes'
|
||||
>
|
||||
<Card>
|
||||
<NotesPanel _id={gcodeFileId} />
|
||||
</Card>
|
||||
</InfoCollapse>
|
||||
|
||||
<InfoCollapse
|
||||
title='Audit Logs'
|
||||
icon={<AuditLogIcon />}
|
||||
active={collapseState.auditLogs}
|
||||
onToggle={(expanded) =>
|
||||
updateCollapseState('auditLogs', expanded)
|
||||
}
|
||||
key='auditLogs'
|
||||
>
|
||||
{loading ? (
|
||||
<InfoCollapsePlaceholder />
|
||||
) : (
|
||||
<ObjectTable
|
||||
type='auditLog'
|
||||
masterFilter={{ 'parent._id': gcodeFileId }}
|
||||
visibleColumns={{ _id: false, 'parent._id': false }}
|
||||
/>
|
||||
)}
|
||||
</InfoCollapse>
|
||||
</Flex>
|
||||
</div>
|
||||
</Flex>
|
||||
</div>
|
||||
</Flex>
|
||||
)}
|
||||
</EditObjectForm>
|
||||
</>
|
||||
)}
|
||||
</ActionHandler>
|
||||
)
|
||||
}}
|
||||
</EditObjectForm>
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
@ -1,313 +1,27 @@
|
||||
// src/Jobs.js
|
||||
|
||||
import React, { useState, useContext, useRef } from 'react'
|
||||
import { useNavigate } from 'react-router-dom'
|
||||
import {
|
||||
Button,
|
||||
Flex,
|
||||
Space,
|
||||
Modal,
|
||||
Dropdown,
|
||||
message,
|
||||
notification,
|
||||
Input,
|
||||
Typography,
|
||||
Checkbox,
|
||||
Popover
|
||||
} from 'antd'
|
||||
import { Button, Flex, Space, Modal, Dropdown, message } from 'antd'
|
||||
|
||||
import { AuthContext } from '../context/AuthContext.js'
|
||||
import { PrintServerContext } from '../context/PrintServerContext.js'
|
||||
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 JobIcon from '../../Icons/JobIcon.jsx'
|
||||
import InfoCircleIcon from '../../Icons/InfoCircleIcon.jsx'
|
||||
import PlusIcon from '../../Icons/PlusIcon.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 ListIcon from '../../Icons/ListIcon.jsx'
|
||||
import GridIcon from '../../Icons/GridIcon.jsx'
|
||||
import useViewMode from '../hooks/useViewMode.js'
|
||||
|
||||
const { Text } = Typography
|
||||
import ColumnViewButton from '../common/ColumnViewButton.jsx'
|
||||
|
||||
const Jobs = () => {
|
||||
const [messageApi, contextHolder] = message.useMessage()
|
||||
const [notificationApi, notificationContextHolder] =
|
||||
notification.useNotification()
|
||||
const navigate = useNavigate()
|
||||
const [newJobOpen, setNewJobOpen] = useState(false)
|
||||
const tableRef = useRef()
|
||||
const [viewMode, setViewMode] = useViewMode('Jobs')
|
||||
|
||||
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 [viewMode, setViewMode] = useViewMode('job')
|
||||
const { authenticated } = useContext(AuthContext)
|
||||
const { printServer } = useContext(PrintServerContext)
|
||||
|
||||
const [columnVisibility, updateColumnVisibility] = useColumnVisibility(
|
||||
'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 [columnVisibility, setColumnVisibility] = useColumnVisibility('job')
|
||||
|
||||
const actionItems = {
|
||||
items: [
|
||||
@ -336,33 +50,8 @@ const Jobs = () => {
|
||||
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 (
|
||||
<>
|
||||
{notificationContextHolder}
|
||||
<Flex vertical={'true'} gap='large' style={{ height: '100%' }}>
|
||||
{contextHolder}
|
||||
<Flex justify={'space-between'}>
|
||||
@ -370,14 +59,14 @@ const Jobs = () => {
|
||||
<Dropdown menu={actionItems}>
|
||||
<Button>Actions</Button>
|
||||
</Dropdown>
|
||||
<Popover
|
||||
content={getViewDropdownItems()}
|
||||
placement='bottomLeft'
|
||||
arrow={false}
|
||||
>
|
||||
<Button>View</Button>
|
||||
</Popover>
|
||||
<ColumnViewButton
|
||||
type='job'
|
||||
disabled={false}
|
||||
visibleState={columnVisibility}
|
||||
updateVisibleState={setColumnVisibility}
|
||||
/>
|
||||
</Space>
|
||||
|
||||
<Space>
|
||||
<Button
|
||||
icon={viewMode === 'cards' ? <ListIcon /> : <GridIcon />}
|
||||
@ -391,6 +80,7 @@ const Jobs = () => {
|
||||
<ObjectTable
|
||||
ref={tableRef}
|
||||
type={'job'}
|
||||
visibleColumns={columnVisibility}
|
||||
authenticated={authenticated}
|
||||
cards={viewMode === 'cards'}
|
||||
/>
|
||||
|
||||
@ -1,9 +1,8 @@
|
||||
import React from 'react'
|
||||
import React, { useContext } from 'react'
|
||||
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 useCollapseState from '../../hooks/useCollapseState'
|
||||
import AuditLogTable from '../../common/AuditLogTable'
|
||||
import NotesPanel from '../../common/NotesPanel'
|
||||
import InfoCollapse from '../../common/InfoCollapse'
|
||||
import ObjectInfo from '../../common/ObjectInfo'
|
||||
@ -17,15 +16,15 @@ import InfoCircleIcon from '../../../Icons/InfoCircleIcon'
|
||||
import JobIcon from '../../../Icons/JobIcon'
|
||||
import AuditLogIcon from '../../../Icons/AuditLogIcon'
|
||||
import NoteIcon from '../../../Icons/NoteIcon'
|
||||
import GCodeFileIcon from '../../../Icons/GCodeFileIcon'
|
||||
import {
|
||||
getModelProperties,
|
||||
getPropertyValue
|
||||
} from '../../../../database/ObjectModels.js'
|
||||
import ObjectActions from '../../common/ObjectActions.jsx'
|
||||
import { ApiServerContext } from '../../context/ApiServerContext'
|
||||
import ObjectTable from '../../common/ObjectTable.jsx'
|
||||
import InfoCollapsePlaceholder from '../../common/InfoCollapsePlaceholder.jsx'
|
||||
|
||||
const JobInfo = () => {
|
||||
const location = useLocation()
|
||||
const jobId = new URLSearchParams(location.search).get('jobId')
|
||||
const { handleDownloadContent } = useContext(ApiServerContext)
|
||||
const [collapseState, updateCollapseState] = useCollapseState('JobInfo', {
|
||||
info: true,
|
||||
subJobs: true,
|
||||
@ -33,153 +32,173 @@ const JobInfo = () => {
|
||||
auditLogs: true
|
||||
})
|
||||
|
||||
// Define actions that can be triggered via URL
|
||||
const actions = {
|
||||
// Add job-specific actions here as needed
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<ActionHandler actions={actions} />
|
||||
<EditObjectForm
|
||||
id={jobId}
|
||||
type='job'
|
||||
style={{ height: 'calc(var(--unit-100vh) - 155px)', minHeight: 0 }}
|
||||
>
|
||||
{({
|
||||
loading,
|
||||
isEditing,
|
||||
startEditing,
|
||||
cancelEditing,
|
||||
handleUpdate,
|
||||
formValid,
|
||||
objectData,
|
||||
editLoading,
|
||||
lock,
|
||||
fetchObject
|
||||
}) => (
|
||||
<Flex
|
||||
gap='large'
|
||||
vertical='true'
|
||||
style={{ height: '100%', minHeight: 0 }}
|
||||
>
|
||||
<Flex justify={'space-between'}>
|
||||
<Space size='middle'>
|
||||
<Space size='small'>
|
||||
<Dropdown
|
||||
menu={{
|
||||
items: [
|
||||
{
|
||||
label: 'Reload Job',
|
||||
key: 'reload',
|
||||
icon: <GCodeFileIcon />
|
||||
}
|
||||
],
|
||||
onClick: ({ key }) => {
|
||||
if (key === 'reload') {
|
||||
fetchObject()
|
||||
}
|
||||
<EditObjectForm
|
||||
id={jobId}
|
||||
type='job'
|
||||
style={{ height: 'calc(var(--unit-100vh) - 155px)', minHeight: 0 }}
|
||||
>
|
||||
{({
|
||||
loading,
|
||||
isEditing,
|
||||
startEditing,
|
||||
cancelEditing,
|
||||
handleUpdate,
|
||||
formValid,
|
||||
objectData,
|
||||
editLoading,
|
||||
lock,
|
||||
fetchObject
|
||||
}) => {
|
||||
// Define actions that can be triggered via URL, now with access to startEditing
|
||||
const actions = {
|
||||
reload: () => {
|
||||
fetchObject()
|
||||
return true
|
||||
},
|
||||
edit: () => {
|
||||
startEditing()
|
||||
return false
|
||||
},
|
||||
cancelEdit: () => {
|
||||
cancelEditing()
|
||||
return true
|
||||
},
|
||||
finishEdit: () => {
|
||||
handleUpdate()
|
||||
return true
|
||||
},
|
||||
download: () => {
|
||||
if (jobId) {
|
||||
handleDownloadContent(
|
||||
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)
|
||||
}
|
||||
}}
|
||||
>
|
||||
<Button disabled={loading}>Actions</Button>
|
||||
</Dropdown>
|
||||
<ViewButton
|
||||
loading={loading}
|
||||
sections={[
|
||||
{ key: 'info', label: 'Job Information' },
|
||||
{ key: 'subJobs', label: 'Sub Jobs' },
|
||||
{ key: 'notes', label: 'Notes' },
|
||||
{ key: 'auditLogs', label: 'Audit Logs' }
|
||||
]}
|
||||
collapseState={collapseState}
|
||||
updateCollapseState={updateCollapseState}
|
||||
/>
|
||||
</Space>
|
||||
<LockIndicator lock={lock} />
|
||||
</Space>
|
||||
<Space>
|
||||
<EditButtons
|
||||
isEditing={isEditing}
|
||||
handleUpdate={handleUpdate}
|
||||
cancelEditing={cancelEditing}
|
||||
startEditing={startEditing}
|
||||
editLoading={editLoading}
|
||||
formValid={formValid}
|
||||
disabled={lock?.locked || loading || true}
|
||||
loading={editLoading}
|
||||
/>
|
||||
</Space>
|
||||
</Flex>
|
||||
key='info'
|
||||
>
|
||||
<ObjectInfo
|
||||
loading={loading}
|
||||
indicator={<LoadingOutlined />}
|
||||
isEditing={isEditing}
|
||||
type='job'
|
||||
objectData={objectData}
|
||||
/>
|
||||
</InfoCollapse>
|
||||
|
||||
<div style={{ height: '100%', overflow: 'auto' }}>
|
||||
<Flex vertical gap={'large'}>
|
||||
<InfoCollapse
|
||||
title='Job Information'
|
||||
icon={<InfoCircleIcon />}
|
||||
active={collapseState.info}
|
||||
onToggle={(expanded) => updateCollapseState('info', expanded)}
|
||||
key='info'
|
||||
>
|
||||
<ObjectInfo
|
||||
loading={loading}
|
||||
indicator={<LoadingOutlined />}
|
||||
isEditing={isEditing}
|
||||
type='job'
|
||||
items={getModelProperties('job').map((prop) => ({
|
||||
...prop,
|
||||
value: getPropertyValue(objectData, prop.name)
|
||||
}))}
|
||||
/>
|
||||
</InfoCollapse>
|
||||
<InfoCollapse
|
||||
title='Sub Jobs'
|
||||
icon={<JobIcon />}
|
||||
active={collapseState.subJobs}
|
||||
onToggle={(expanded) =>
|
||||
updateCollapseState('subJobs', expanded)
|
||||
}
|
||||
key='subJobs'
|
||||
>
|
||||
<SubJobsTree jobData={objectData} loading={loading} />
|
||||
</InfoCollapse>
|
||||
|
||||
<InfoCollapse
|
||||
title='Sub Jobs'
|
||||
icon={<JobIcon />}
|
||||
active={collapseState.subJobs}
|
||||
onToggle={(expanded) =>
|
||||
updateCollapseState('subJobs', expanded)
|
||||
}
|
||||
key='subJobs'
|
||||
>
|
||||
<SubJobsTree jobData={objectData} loading={loading} />
|
||||
</InfoCollapse>
|
||||
<InfoCollapse
|
||||
title='Notes'
|
||||
icon={<NoteIcon />}
|
||||
active={collapseState.notes}
|
||||
onToggle={(expanded) =>
|
||||
updateCollapseState('notes', expanded)
|
||||
}
|
||||
key='notes'
|
||||
>
|
||||
<Card>
|
||||
<NotesPanel _id={jobId} />
|
||||
</Card>
|
||||
</InfoCollapse>
|
||||
|
||||
<InfoCollapse
|
||||
title='Notes'
|
||||
icon={<NoteIcon />}
|
||||
active={collapseState.notes}
|
||||
onToggle={(expanded) =>
|
||||
updateCollapseState('notes', expanded)
|
||||
}
|
||||
key='notes'
|
||||
>
|
||||
<Card>
|
||||
<NotesPanel _id={jobId} />
|
||||
</Card>
|
||||
</InfoCollapse>
|
||||
|
||||
<InfoCollapse
|
||||
title='Audit Logs'
|
||||
icon={<AuditLogIcon />}
|
||||
active={collapseState.auditLogs}
|
||||
onToggle={(expanded) =>
|
||||
updateCollapseState('auditLogs', expanded)
|
||||
}
|
||||
key='auditLogs'
|
||||
>
|
||||
<AuditLogTable
|
||||
items={objectData?.auditLogs || []}
|
||||
loading={loading}
|
||||
showTargetColumn={false}
|
||||
/>
|
||||
</InfoCollapse>
|
||||
<InfoCollapse
|
||||
title='Audit Logs'
|
||||
icon={<AuditLogIcon />}
|
||||
active={collapseState.auditLogs}
|
||||
onToggle={(expanded) =>
|
||||
updateCollapseState('auditLogs', expanded)
|
||||
}
|
||||
key='auditLogs'
|
||||
>
|
||||
{loading ? (
|
||||
<InfoCollapsePlaceholder />
|
||||
) : (
|
||||
<ObjectTable
|
||||
type='auditLog'
|
||||
masterFilter={{ 'parent._id': jobId }}
|
||||
visibleColumns={{ _id: false, 'parent._id': false }}
|
||||
/>
|
||||
)}
|
||||
</InfoCollapse>
|
||||
</Flex>
|
||||
</div>
|
||||
</Flex>
|
||||
</div>
|
||||
</Flex>
|
||||
)}
|
||||
</EditObjectForm>
|
||||
</>
|
||||
)}
|
||||
</ActionHandler>
|
||||
)
|
||||
}}
|
||||
</EditObjectForm>
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
@ -1,9 +1,7 @@
|
||||
// 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 { AuthContext } from '../context/AuthContext'
|
||||
import NewPrinter from './Printers/NewPrinter'
|
||||
import PlusIcon from '../../Icons/PlusIcon'
|
||||
import ReloadIcon from '../../Icons/ReloadIcon'
|
||||
@ -17,7 +15,6 @@ import useColumnVisibility from '../hooks/useColumnVisibility'
|
||||
|
||||
const Printers = () => {
|
||||
const [messageApi, contextHolder] = message.useMessage()
|
||||
const { authenticated } = useContext(AuthContext)
|
||||
const [newPrinterOpen, setNewPrinterOpen] = useState(false)
|
||||
const tableRef = useRef()
|
||||
|
||||
@ -61,9 +58,9 @@ const Printers = () => {
|
||||
</Dropdown>
|
||||
<ColumnViewButton
|
||||
type='printer'
|
||||
loading={false}
|
||||
collapseState={columnVisibility}
|
||||
updateCollapseState={setColumnVisibility}
|
||||
disabled={false}
|
||||
visibleState={columnVisibility}
|
||||
updateVisibleState={setColumnVisibility}
|
||||
/>
|
||||
</Space>
|
||||
<Space>
|
||||
@ -78,8 +75,7 @@ const Printers = () => {
|
||||
|
||||
<ObjectTable
|
||||
ref={tableRef}
|
||||
type={'printer'}
|
||||
authenticated={authenticated}
|
||||
type='printer'
|
||||
cards={viewMode === 'cards'}
|
||||
visibleColumns={columnVisibility}
|
||||
/>
|
||||
|
||||
@ -1,9 +1,8 @@
|
||||
import React from 'react'
|
||||
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 useCollapseState from '../../hooks/useCollapseState'
|
||||
import AuditLogTable from '../../common/AuditLogTable'
|
||||
import NotesPanel from '../../common/NotesPanel'
|
||||
import InfoCollapse from '../../common/InfoCollapse'
|
||||
import ObjectInfo from '../../common/ObjectInfo'
|
||||
@ -16,10 +15,10 @@ import InfoCircleIcon from '../../../Icons/InfoCircleIcon.jsx'
|
||||
import NoteIcon from '../../../Icons/NoteIcon.jsx'
|
||||
import PrinterIcon from '../../../Icons/PrinterIcon.jsx'
|
||||
import AuditLogIcon from '../../../Icons/AuditLogIcon.jsx'
|
||||
import {
|
||||
getModelProperties,
|
||||
getPropertyValue
|
||||
} from '../../../../database/ObjectModels.js'
|
||||
import ActionHandler from '../../common/ActionHandler'
|
||||
import ObjectActions from '../../common/ObjectActions.jsx'
|
||||
import ObjectTable from '../../common/ObjectTable.jsx'
|
||||
import InfoCollapsePlaceholder from '../../common/InfoCollapsePlaceholder.jsx'
|
||||
|
||||
const PrinterInfo = () => {
|
||||
const location = useLocation()
|
||||
@ -28,7 +27,8 @@ const PrinterInfo = () => {
|
||||
info: true,
|
||||
jobs: true,
|
||||
notes: true,
|
||||
auditLogs: true
|
||||
auditLogsParent: true,
|
||||
auditLogsOwner: true
|
||||
})
|
||||
|
||||
return (
|
||||
@ -48,126 +48,178 @@ const PrinterInfo = () => {
|
||||
editLoading,
|
||||
lock,
|
||||
fetchObject
|
||||
}) => (
|
||||
<Flex
|
||||
gap='large'
|
||||
vertical='true'
|
||||
style={{ height: '100%', minHeight: 0 }}
|
||||
>
|
||||
<Flex justify={'space-between'}>
|
||||
<Space size='middle'>
|
||||
<Space size='small'>
|
||||
<Dropdown
|
||||
menu={{
|
||||
items: [
|
||||
{
|
||||
label: 'Reload Printer',
|
||||
key: 'reload',
|
||||
icon: <AuditLogIcon />
|
||||
}) => {
|
||||
// Define actions for ActionHandler
|
||||
const actions = {
|
||||
reload: () => {
|
||||
fetchObject()
|
||||
return true
|
||||
},
|
||||
edit: () => {
|
||||
startEditing()
|
||||
return false
|
||||
},
|
||||
cancelEdit: () => {
|
||||
cancelEditing()
|
||||
return true
|
||||
},
|
||||
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)
|
||||
}
|
||||
],
|
||||
onClick: ({ key }) => {
|
||||
if (key === 'reload') {
|
||||
fetchObject()
|
||||
key='info'
|
||||
>
|
||||
<ObjectInfo
|
||||
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)
|
||||
}
|
||||
}
|
||||
}}
|
||||
>
|
||||
<Button disabled={loading}>Actions</Button>
|
||||
</Dropdown>
|
||||
<ViewButton
|
||||
loading={loading}
|
||||
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>
|
||||
key='jobs'
|
||||
>
|
||||
<PrinterJobsTree
|
||||
subJobs={objectData?.subJobs}
|
||||
loading={loading}
|
||||
/>
|
||||
</InfoCollapse>
|
||||
|
||||
<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'
|
||||
>
|
||||
<ObjectInfo
|
||||
loading={loading}
|
||||
indicator={<LoadingOutlined />}
|
||||
isEditing={isEditing}
|
||||
type='printer'
|
||||
items={getModelProperties('printer').map((prop) => ({
|
||||
...prop,
|
||||
value: getPropertyValue(objectData, prop.name)
|
||||
}))}
|
||||
/>
|
||||
</InfoCollapse>
|
||||
<InfoCollapse
|
||||
title='Notes'
|
||||
icon={<NoteIcon />}
|
||||
active={collapseState.notes}
|
||||
onToggle={(expanded) =>
|
||||
updateCollapseState('notes', expanded)
|
||||
}
|
||||
key='notes'
|
||||
>
|
||||
<Card>
|
||||
<NotesPanel _id={printerId} />
|
||||
</Card>
|
||||
</InfoCollapse>
|
||||
|
||||
<InfoCollapse
|
||||
title='Printer Jobs'
|
||||
icon={<PrinterIcon />}
|
||||
active={collapseState.jobs}
|
||||
onToggle={(expanded) => updateCollapseState('jobs', expanded)}
|
||||
key='jobs'
|
||||
>
|
||||
<PrinterJobsTree
|
||||
subJobs={objectData?.subJobs}
|
||||
loading={loading}
|
||||
/>
|
||||
</InfoCollapse>
|
||||
|
||||
<InfoCollapse
|
||||
title='Notes'
|
||||
icon={<NoteIcon />}
|
||||
active={collapseState.notes}
|
||||
onToggle={(expanded) => updateCollapseState('notes', expanded)}
|
||||
key='notes'
|
||||
>
|
||||
<Card>
|
||||
<NotesPanel _id={printerId} />
|
||||
</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>
|
||||
)}
|
||||
<InfoCollapse
|
||||
title='Audit Logs (By Parent)'
|
||||
icon={<AuditLogIcon />}
|
||||
active={collapseState.auditLogsParent}
|
||||
onToggle={(expanded) =>
|
||||
updateCollapseState('auditLogsParent', expanded)
|
||||
}
|
||||
key='auditLogs'
|
||||
>
|
||||
{loading ? (
|
||||
<InfoCollapsePlaceholder />
|
||||
) : (
|
||||
<ObjectTable
|
||||
type='auditLog'
|
||||
masterFilter={{ 'parent._id': printerId }}
|
||||
visibleColumns={{ _id: false, 'parent._id': false }}
|
||||
/>
|
||||
)}
|
||||
</InfoCollapse>
|
||||
<InfoCollapse
|
||||
title='Audit Logs (By Owner)'
|
||||
icon={<AuditLogIcon />}
|
||||
active={collapseState.auditLogsOwner}
|
||||
onToggle={(expanded) =>
|
||||
updateCollapseState('auditLogsOwner', expanded)
|
||||
}
|
||||
key='auditLogs'
|
||||
>
|
||||
{loading ? (
|
||||
<InfoCollapsePlaceholder />
|
||||
) : (
|
||||
<ObjectTable
|
||||
type='auditLog'
|
||||
masterFilter={{ 'owner._id': printerId }}
|
||||
visibleColumns={{ _id: false, 'owner._id': false }}
|
||||
/>
|
||||
)}
|
||||
</InfoCollapse>
|
||||
</Flex>
|
||||
</div>
|
||||
</Flex>
|
||||
)}
|
||||
</ActionHandler>
|
||||
)
|
||||
}}
|
||||
</EditObjectForm>
|
||||
)
|
||||
}
|
||||
|
||||
@ -1,38 +1,61 @@
|
||||
import { useEffect } from 'react'
|
||||
import React, { useEffect, useRef } from 'react'
|
||||
import { useLocation, useNavigate } from 'react-router-dom'
|
||||
import PropTypes from 'prop-types'
|
||||
|
||||
const ActionHandler = ({
|
||||
children,
|
||||
actions = {},
|
||||
actionParam = 'action',
|
||||
clearAfterExecute = true,
|
||||
onAction
|
||||
onAction,
|
||||
loading = true
|
||||
}) => {
|
||||
const location = useLocation()
|
||||
const navigate = useNavigate()
|
||||
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
|
||||
useEffect(() => {
|
||||
if (action && actions[action]) {
|
||||
if (
|
||||
!loading &&
|
||||
action &&
|
||||
actions[action] &&
|
||||
lastExecutedAction.current !== action
|
||||
) {
|
||||
// Execute the action
|
||||
const result = actions[action]()
|
||||
|
||||
// Mark this action as executed
|
||||
lastExecutedAction.current = action
|
||||
// Call optional callback
|
||||
if (onAction) {
|
||||
onAction(action, result)
|
||||
}
|
||||
|
||||
// Clear action from URL if requested
|
||||
if (clearAfterExecute) {
|
||||
// Clear action from URL if requested and result is true
|
||||
if (clearAfterExecute && result == true) {
|
||||
const searchParams = new URLSearchParams(location.search)
|
||||
searchParams.delete(actionParam)
|
||||
const newSearch = searchParams.toString()
|
||||
const newPath = location.pathname + (newSearch ? `?${newSearch}` : '')
|
||||
navigate(newPath, { replace: true })
|
||||
}
|
||||
} else if (!action) {
|
||||
// Reset lastExecutedAction if no action is present
|
||||
lastExecutedAction.current = null
|
||||
}
|
||||
}, [
|
||||
loading,
|
||||
action,
|
||||
actions,
|
||||
actionParam,
|
||||
@ -44,14 +67,16 @@ const ActionHandler = ({
|
||||
])
|
||||
|
||||
// Return null as this is a utility component
|
||||
return null
|
||||
return <>{children({ callAction })}</>
|
||||
}
|
||||
|
||||
ActionHandler.propTypes = {
|
||||
children: PropTypes.func,
|
||||
actions: PropTypes.objectOf(PropTypes.func),
|
||||
actionParam: PropTypes.string,
|
||||
clearAfterExecute: PropTypes.bool,
|
||||
onAction: PropTypes.func
|
||||
onAction: PropTypes.func,
|
||||
loading: PropTypes.bool
|
||||
}
|
||||
|
||||
export default ActionHandler
|
||||
|
||||
@ -5,9 +5,9 @@ import { getModelByName } from '../../../database/ObjectModels'
|
||||
|
||||
const ColumnViewButton = ({
|
||||
type,
|
||||
loading = false,
|
||||
collapseState = {},
|
||||
updateCollapseState = () => {},
|
||||
disabled = false,
|
||||
visibleState = {},
|
||||
updateVisibleState = () => {},
|
||||
...buttonProps
|
||||
}) => {
|
||||
// Get the model by name
|
||||
@ -31,10 +31,10 @@ const ColumnViewButton = ({
|
||||
|
||||
return (
|
||||
<ViewButton
|
||||
loading={loading}
|
||||
properties={columnProperties}
|
||||
collapseState={collapseState}
|
||||
updateCollapseState={updateCollapseState}
|
||||
disabled={disabled}
|
||||
items={columnProperties}
|
||||
visibleState={visibleState}
|
||||
updateVisibleState={updateVisibleState}
|
||||
{...buttonProps}
|
||||
/>
|
||||
)
|
||||
@ -42,9 +42,9 @@ const ColumnViewButton = ({
|
||||
|
||||
ColumnViewButton.propTypes = {
|
||||
type: PropTypes.string.isRequired,
|
||||
loading: PropTypes.bool,
|
||||
collapseState: PropTypes.object,
|
||||
updateCollapseState: PropTypes.func
|
||||
disabled: PropTypes.bool,
|
||||
visibleState: PropTypes.object,
|
||||
updateVisibleState: PropTypes.func
|
||||
}
|
||||
|
||||
export default ColumnViewButton
|
||||
|
||||
@ -17,6 +17,7 @@ import PropTypes from 'prop-types'
|
||||
*/
|
||||
const EditObjectForm = ({ id, type, style, children }) => {
|
||||
const [objectData, setObjectData] = useState(null)
|
||||
const [serverObjectData, setServerObjectData] = useState(null)
|
||||
const [fetchLoading, setFetchLoading] = useState(true)
|
||||
const [editLoading, setEditLoading] = useState(false)
|
||||
const [lock, setLock] = useState({})
|
||||
@ -59,12 +60,6 @@ const EditObjectForm = ({ id, type, style, children }) => {
|
||||
}
|
||||
}, [id, type, unlockObject])
|
||||
|
||||
useEffect(() => {
|
||||
if (objectData) {
|
||||
form.setFieldsValue(objectData)
|
||||
}
|
||||
}, [objectData, form])
|
||||
|
||||
const fetchObject = useCallback(async () => {
|
||||
try {
|
||||
setFetchLoading(true)
|
||||
@ -72,6 +67,7 @@ const EditObjectForm = ({ id, type, style, children }) => {
|
||||
const lockEvent = await fetchObjectLock(id, type)
|
||||
setLock(lockEvent)
|
||||
setObjectData(data)
|
||||
setServerObjectData(data)
|
||||
form.setFieldsValue(data)
|
||||
setFetchLoading(false)
|
||||
} catch (err) {
|
||||
@ -120,8 +116,9 @@ const EditObjectForm = ({ id, type, style, children }) => {
|
||||
}
|
||||
|
||||
const cancelEditing = () => {
|
||||
if (objectData) {
|
||||
form.setFieldsValue(objectData)
|
||||
if (serverObjectData) {
|
||||
form.setFieldsValue(serverObjectData)
|
||||
setObjectData(serverObjectData)
|
||||
}
|
||||
setIsEditing(false)
|
||||
unlockObject(id, type)
|
||||
@ -151,7 +148,14 @@ const EditObjectForm = ({ id, type, style, children }) => {
|
||||
}
|
||||
|
||||
return (
|
||||
<Form form={form} layout='vertical' style={style}>
|
||||
<Form
|
||||
form={form}
|
||||
layout='vertical'
|
||||
style={style}
|
||||
onValuesChange={(values) => {
|
||||
setObjectData((prev) => ({ ...prev, ...values }))
|
||||
}}
|
||||
>
|
||||
{contextHolder}
|
||||
{children({
|
||||
loading: fetchLoading,
|
||||
|
||||
@ -24,9 +24,9 @@ const EmailDisplay = ({ email, showCopy = true, showLink = false }) => {
|
||||
</Text>
|
||||
<Tooltip title='Email' arrow={false}>
|
||||
<Button
|
||||
icon={<NewMailIcon style={{ fontSize: '14px' }} />}
|
||||
icon={<NewMailIcon />}
|
||||
type='text'
|
||||
style={{ height: '22px' }}
|
||||
size='small'
|
||||
onClick={(e) => {
|
||||
e.preventDefault()
|
||||
window.location.href = `mailto:${email}`
|
||||
@ -40,7 +40,6 @@ const EmailDisplay = ({ email, showCopy = true, showLink = false }) => {
|
||||
text={email}
|
||||
tooltip='Copy Email'
|
||||
style={{ marginLeft: 0 }}
|
||||
iconStyle={{ fontSize: '14px' }}
|
||||
/>
|
||||
)}
|
||||
</Flex>
|
||||
|
||||
@ -29,9 +29,8 @@ const IdDisplay = ({
|
||||
|
||||
var hyperlink = null
|
||||
|
||||
const defaultModelActions = model.actions.filter(
|
||||
(action) => action.default == true
|
||||
)
|
||||
const defaultModelActions =
|
||||
model.actions?.filter((action) => action.default == true) || []
|
||||
|
||||
if (defaultModelActions.length >= 1) {
|
||||
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 { Spin, Descriptions } from 'antd'
|
||||
import { LoadingOutlined } from '@ant-design/icons'
|
||||
import PropTypes from 'prop-types'
|
||||
import ObjectProperty from './ObjectProperty'
|
||||
import { getModelProperties } from '../../../database/ObjectModels'
|
||||
|
||||
const ObjectInfo = ({
|
||||
loading = false,
|
||||
indicator = null,
|
||||
bordered = true,
|
||||
isEditing = false,
|
||||
items = []
|
||||
type = 'unknown',
|
||||
objectData = null
|
||||
}) => {
|
||||
const items = getModelProperties(type)
|
||||
|
||||
// Map items to Descriptions 'items' prop format
|
||||
const descriptionItems = items.map((item, idx) => {
|
||||
const key = item.name || item.label || idx
|
||||
return {
|
||||
key,
|
||||
label: item.label,
|
||||
children: <ObjectProperty {...item} isEditing={isEditing} />
|
||||
children: (
|
||||
<ObjectProperty
|
||||
{...item}
|
||||
isEditing={isEditing}
|
||||
objectData={objectData}
|
||||
/>
|
||||
)
|
||||
}
|
||||
})
|
||||
|
||||
return (
|
||||
<Spin spinning={loading} indicator={indicator}>
|
||||
<Spin spinning={loading} indicator={<LoadingOutlined />}>
|
||||
<Descriptions
|
||||
bordered={bordered}
|
||||
column={{
|
||||
@ -44,7 +54,8 @@ ObjectInfo.propTypes = {
|
||||
bordered: PropTypes.bool,
|
||||
items: PropTypes.arrayOf(PropTypes.object),
|
||||
isEditing: PropTypes.bool,
|
||||
type: PropTypes.string.isRequired
|
||||
type: PropTypes.string.isRequired,
|
||||
objectData: PropTypes.object
|
||||
}
|
||||
|
||||
export default ObjectInfo
|
||||
|
||||
@ -33,6 +33,8 @@ import SecretDisplay from './SecretDisplay'
|
||||
import EyeIcon from '../../Icons/EyeIcon'
|
||||
import EyeSlashIcon from '../../Icons/EyeSlashIcon'
|
||||
import FilamentStockState from './FilamentStockState'
|
||||
import { getPropertyValue } from '../../../database/ObjectModels'
|
||||
import PropertyChanges from './PropertyChanges'
|
||||
|
||||
const { Text } = Typography
|
||||
|
||||
@ -47,17 +49,40 @@ const MATERIAL_OPTIONS = [
|
||||
|
||||
const ObjectProperty = ({
|
||||
type = 'text',
|
||||
prefix,
|
||||
suffix,
|
||||
value,
|
||||
min,
|
||||
max,
|
||||
step,
|
||||
isEditing = false,
|
||||
formItemProps = {},
|
||||
required = false,
|
||||
name,
|
||||
label,
|
||||
showLabel = false,
|
||||
objectData = null,
|
||||
objectType = 'unknown',
|
||||
readOnly = false,
|
||||
disabled = false,
|
||||
...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
|
||||
var formItemName = name
|
||||
|
||||
@ -65,6 +90,12 @@ const ObjectProperty = ({
|
||||
formItemName = name ? name.split('.') : undefined
|
||||
}
|
||||
|
||||
var textParams = {}
|
||||
|
||||
if (disabled == true) {
|
||||
textParams = { ...textParams, delete: true, type: 'secondary' }
|
||||
}
|
||||
|
||||
const renderProperty = () => {
|
||||
if (!isEditing || readOnly) {
|
||||
switch (type) {
|
||||
@ -72,93 +103,154 @@ const ObjectProperty = ({
|
||||
if (value != null) {
|
||||
return <SecretDisplay value={value} {...rest} />
|
||||
} else {
|
||||
return <Text type='secondary'>n/a</Text>
|
||||
return (
|
||||
<Text type='secondary' {...textParams}>
|
||||
n/a
|
||||
</Text>
|
||||
)
|
||||
}
|
||||
case 'wsprotocol':
|
||||
switch (value) {
|
||||
case 'ws':
|
||||
return <Text>Websocket</Text>
|
||||
return <Text {...textParams}>Websocket</Text>
|
||||
case 'wss':
|
||||
return <Text>Websocket Secure</Text>
|
||||
return <Text {...textParams}>Websocket Secure</Text>
|
||||
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': {
|
||||
if (value != null) {
|
||||
return <BoolDisplay value={value} yesNo={true} {...rest} />
|
||||
} else {
|
||||
return <Text type='secondary'>n/a</Text>
|
||||
return (
|
||||
<Text type='secondary' {...textParams}>
|
||||
n/a
|
||||
</Text>
|
||||
)
|
||||
}
|
||||
}
|
||||
case 'dateTime': {
|
||||
if (value != null) {
|
||||
return <TimeDisplay dateTime={value} {...rest} />
|
||||
} else {
|
||||
return <Text type='secondary'>n/a</Text>
|
||||
}
|
||||
}
|
||||
case 'currency': {
|
||||
if (value != null) {
|
||||
return <Text>{`£${value}/kg`}</Text>
|
||||
} else {
|
||||
return <Text type='secondary'>n/a</Text>
|
||||
return (
|
||||
<Text type='secondary' {...textParams}>
|
||||
n/a
|
||||
</Text>
|
||||
)
|
||||
}
|
||||
}
|
||||
case 'country': {
|
||||
if (value != null) {
|
||||
return <CountryDisplay countryCode={value} />
|
||||
} else {
|
||||
return <Text type='secondary'>n/a</Text>
|
||||
return (
|
||||
<Text type='secondary' {...textParams}>
|
||||
n/a
|
||||
</Text>
|
||||
)
|
||||
}
|
||||
}
|
||||
case 'color': {
|
||||
if (value) {
|
||||
return <Badge color={value} text={value} />
|
||||
} else {
|
||||
return <Text type='secondary'>n/a</Text>
|
||||
}
|
||||
}
|
||||
case 'weight': {
|
||||
if (value != null) {
|
||||
return <Text>{`${value}g`}</Text>
|
||||
} else {
|
||||
return <Text type='secondary'>n/a</Text>
|
||||
return (
|
||||
<Text type='secondary' {...textParams}>
|
||||
n/a
|
||||
</Text>
|
||||
)
|
||||
}
|
||||
}
|
||||
case 'number': {
|
||||
if (value != null) {
|
||||
if (Array.isArray(value)) {
|
||||
return <Text>{value.length}</Text>
|
||||
return (
|
||||
<Text {...textParams}>
|
||||
{prefix}
|
||||
{value.length}
|
||||
{suffix}
|
||||
</Text>
|
||||
)
|
||||
} else {
|
||||
return <Text>{value}</Text>
|
||||
return (
|
||||
<Text {...textParams}>
|
||||
{prefix}
|
||||
{value}
|
||||
{suffix}
|
||||
</Text>
|
||||
)
|
||||
}
|
||||
} else {
|
||||
return <Text type='secondary'>n/a</Text>
|
||||
return (
|
||||
<Text type='secondary' {...textParams}>
|
||||
n/a
|
||||
</Text>
|
||||
)
|
||||
}
|
||||
}
|
||||
case 'text':
|
||||
if (value != null && value != '') {
|
||||
return <Text ellipsis>{value}</Text>
|
||||
return (
|
||||
<Text ellipsis>
|
||||
{prefix}
|
||||
{value}
|
||||
{suffix}
|
||||
</Text>
|
||||
)
|
||||
} else {
|
||||
return <Text type='secondary'>n/a</Text>
|
||||
return (
|
||||
<Text type='secondary' {...textParams}>
|
||||
n/a
|
||||
</Text>
|
||||
)
|
||||
}
|
||||
case 'email':
|
||||
if (value != null && value != '') {
|
||||
return <EmailDisplay email={value} />
|
||||
} else {
|
||||
return <Text type='secondary'>n/a</Text>
|
||||
return (
|
||||
<Text type='secondary' {...textParams}>
|
||||
n/a
|
||||
</Text>
|
||||
)
|
||||
}
|
||||
case 'url':
|
||||
if (value != null && value != '') {
|
||||
return <UrlDisplay url={value} />
|
||||
} else {
|
||||
return <Text type='secondary'>n/a</Text>
|
||||
return (
|
||||
<Text type='secondary' {...textParams}>
|
||||
n/a
|
||||
</Text>
|
||||
)
|
||||
}
|
||||
case 'object': {
|
||||
if (value && value.name) {
|
||||
return <Text ellipsis>{value.name}</Text>
|
||||
} else {
|
||||
return <Text type='secondary'>n/a</Text>
|
||||
return (
|
||||
<Text type='secondary' {...textParams}>
|
||||
n/a
|
||||
</Text>
|
||||
)
|
||||
}
|
||||
}
|
||||
case 'state': {
|
||||
@ -173,59 +265,87 @@ const ObjectProperty = ({
|
||||
case 'filamentStock':
|
||||
return <FilamentStockState state={value} {...rest} />
|
||||
default:
|
||||
return <Text type='secondary'>No Object Type Specified</Text>
|
||||
return (
|
||||
<Text type='secondary' {...textParams}>
|
||||
No Object Type Specified
|
||||
</Text>
|
||||
)
|
||||
}
|
||||
} else {
|
||||
return <Text type='secondary'>n/a</Text>
|
||||
return (
|
||||
<Text type='secondary' {...textParams}>
|
||||
n/a
|
||||
</Text>
|
||||
)
|
||||
}
|
||||
}
|
||||
case 'material': {
|
||||
if (value) {
|
||||
return <Text>{value}</Text>
|
||||
return <Text {...textParams}>{value}</Text>
|
||||
} else {
|
||||
return <Text type='secondary'>n/a</Text>
|
||||
return (
|
||||
<Text type='secondary' {...textParams}>
|
||||
n/a
|
||||
</Text>
|
||||
)
|
||||
}
|
||||
}
|
||||
case 'id': {
|
||||
if (value) {
|
||||
return <IdDisplay id={value} type={objectType} {...rest} />
|
||||
} else {
|
||||
return <Text type='secondary'>n/a</Text>
|
||||
return (
|
||||
<Text type='secondary' {...textParams}>
|
||||
n/a
|
||||
</Text>
|
||||
)
|
||||
}
|
||||
}
|
||||
case 'density': {
|
||||
if (value != null) {
|
||||
return <Text>{`${value} g/cm³`}</Text>
|
||||
return <Text {...textParams}>{`${value} g/cm³`}</Text>
|
||||
} else {
|
||||
return <Text type='secondary'>n/a</Text>
|
||||
return (
|
||||
<Text type='secondary' {...textParams}>
|
||||
n/a
|
||||
</Text>
|
||||
)
|
||||
}
|
||||
}
|
||||
case 'mm': {
|
||||
if (value != null) {
|
||||
return <Text>{`${value} mm`}</Text>
|
||||
return <Text {...textParams}>{`${value} mm`}</Text>
|
||||
} else {
|
||||
return <Text type='secondary'>n/a</Text>
|
||||
return (
|
||||
<Text type='secondary' {...textParams}>
|
||||
n/a
|
||||
</Text>
|
||||
)
|
||||
}
|
||||
}
|
||||
case 'tags': {
|
||||
if (value != null || value?.length != 0) {
|
||||
return <TagsDisplay tags={value} />
|
||||
} else {
|
||||
return <Text type='secondary'>n/a</Text>
|
||||
return (
|
||||
<Text type='secondary' {...textParams}>
|
||||
n/a
|
||||
</Text>
|
||||
)
|
||||
}
|
||||
}
|
||||
case 'version': {
|
||||
if (value != null) {
|
||||
return <Text>{`${value} mm`}</Text>
|
||||
} else {
|
||||
return <Text type='secondary'>n/a</Text>
|
||||
}
|
||||
case 'propertyChanges': {
|
||||
return <PropertyChanges type={objectType} value={value} />
|
||||
}
|
||||
default: {
|
||||
if (value) {
|
||||
return <Text>{value}</Text>
|
||||
return <Text {...textParams}>{value}</Text>
|
||||
} 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}>
|
||||
<Input.Password
|
||||
placeholder={label}
|
||||
disabled={disabled}
|
||||
{...mergedFormItemProps}
|
||||
iconRender={(visible) =>
|
||||
visible ? <EyeSlashIcon /> : <EyeIcon />
|
||||
@ -278,6 +399,7 @@ const ObjectProperty = ({
|
||||
<Form.Item name={formItemName} {...mergedFormItemProps}>
|
||||
<Select
|
||||
defaultValue='ws'
|
||||
disabled={disabled}
|
||||
options={[
|
||||
{ value: 'ws', label: 'Websocket' },
|
||||
{ value: 'wss', label: 'Websocket Secure' }
|
||||
@ -285,6 +407,19 @@ const ObjectProperty = ({
|
||||
/>
|
||||
</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':
|
||||
return (
|
||||
<Form.Item
|
||||
@ -292,7 +427,7 @@ const ObjectProperty = ({
|
||||
{...mergedFormItemProps}
|
||||
valuePropName='checked'
|
||||
>
|
||||
<Switch />
|
||||
<Switch disabled={disabled} />
|
||||
</Form.Item>
|
||||
)
|
||||
case 'dateTime':
|
||||
@ -302,24 +437,17 @@ const ObjectProperty = ({
|
||||
{...mergedFormItemProps}
|
||||
getValueProps={(v) => ({ value: v ? dayjs(v) : null })}
|
||||
>
|
||||
<DatePicker showTime style={{ width: '100%' }} />
|
||||
</Form.Item>
|
||||
)
|
||||
case 'currency':
|
||||
return (
|
||||
<Form.Item name={formItemName} {...mergedFormItemProps}>
|
||||
<InputNumber
|
||||
prefix='£'
|
||||
suffix='/kg'
|
||||
<DatePicker
|
||||
showTime
|
||||
style={{ width: '100%' }}
|
||||
placeholder={label}
|
||||
disabled={disabled}
|
||||
/>
|
||||
</Form.Item>
|
||||
)
|
||||
case 'country':
|
||||
return (
|
||||
<Form.Item name={formItemName} {...mergedFormItemProps}>
|
||||
<CountrySelect />
|
||||
<CountrySelect disabled={disabled} />
|
||||
</Form.Item>
|
||||
)
|
||||
case 'color':
|
||||
@ -330,7 +458,7 @@ const ObjectProperty = ({
|
||||
valuePropName='value'
|
||||
getValueFromEvent={(v) => v}
|
||||
>
|
||||
<ColorSelector required={required} />
|
||||
<ColorSelector required={required} disabled={disabled} />
|
||||
</Form.Item>
|
||||
)
|
||||
case 'weight':
|
||||
@ -340,6 +468,7 @@ const ObjectProperty = ({
|
||||
suffix='g'
|
||||
style={{ width: '100%' }}
|
||||
placeholder={label}
|
||||
disabled={disabled}
|
||||
/>
|
||||
</Form.Item>
|
||||
)
|
||||
@ -347,22 +476,36 @@ const ObjectProperty = ({
|
||||
return (
|
||||
<Form.Item name={formItemName} {...mergedFormItemProps}>
|
||||
<InputNumber
|
||||
style={{ width: '100%' }}
|
||||
placeholder={label}
|
||||
disabled={disabled}
|
||||
prefix={prefix}
|
||||
suffix={suffix}
|
||||
min={min}
|
||||
max={max}
|
||||
step={step}
|
||||
{...mergedFormItemProps}
|
||||
style={{ width: '100%' }}
|
||||
/>
|
||||
</Form.Item>
|
||||
)
|
||||
case 'text':
|
||||
return (
|
||||
<Form.Item name={formItemName} {...mergedFormItemProps}>
|
||||
<Input placeholder={label} {...mergedFormItemProps} />
|
||||
<Input
|
||||
placeholder={label}
|
||||
{...mergedFormItemProps}
|
||||
disabled={disabled}
|
||||
/>
|
||||
</Form.Item>
|
||||
)
|
||||
case 'material':
|
||||
return (
|
||||
<Form.Item name={formItemName} {...mergedFormItemProps}>
|
||||
<Select options={MATERIAL_OPTIONS} placeholder={label} />
|
||||
<Select
|
||||
options={MATERIAL_OPTIONS}
|
||||
placeholder={label}
|
||||
disabled={disabled}
|
||||
/>
|
||||
</Form.Item>
|
||||
)
|
||||
case 'id':
|
||||
@ -370,7 +513,11 @@ const ObjectProperty = ({
|
||||
if (value) {
|
||||
return <IdDisplay id={value} type={objectType} {...rest} />
|
||||
} else {
|
||||
return <Text type='secondary'>n/a</Text>
|
||||
return (
|
||||
<Text type='secondary' {...textParams}>
|
||||
n/a
|
||||
</Text>
|
||||
)
|
||||
}
|
||||
case 'object':
|
||||
switch (objectType) {
|
||||
@ -405,7 +552,11 @@ const ObjectProperty = ({
|
||||
</Form.Item>
|
||||
)
|
||||
default:
|
||||
return <Text type='secondary'>n/a</Text>
|
||||
return (
|
||||
<Text type='secondary' {...textParams}>
|
||||
n/a
|
||||
</Text>
|
||||
)
|
||||
}
|
||||
|
||||
case 'density':
|
||||
@ -455,10 +606,15 @@ ObjectProperty.propTypes = {
|
||||
formItemProps: PropTypes.object,
|
||||
required: PropTypes.bool,
|
||||
name: PropTypes.string,
|
||||
label: PropTypes.string,
|
||||
prefix: PropTypes.string,
|
||||
suffix: PropTypes.string,
|
||||
min: PropTypes.number,
|
||||
max: PropTypes.number,
|
||||
step: PropTypes.number,
|
||||
showLabel: PropTypes.bool,
|
||||
objectType: PropTypes.string,
|
||||
readOnly: PropTypes.bool
|
||||
readOnly: PropTypes.bool,
|
||||
disabled: PropTypes.bool
|
||||
}
|
||||
|
||||
export default ObjectProperty
|
||||
|
||||
@ -38,6 +38,7 @@ import XMarkIcon from '../../Icons/XMarkIcon'
|
||||
import CheckIcon from '../../Icons/CheckIcon'
|
||||
import { useNavigate } from 'react-router-dom'
|
||||
import QuestionCircleIcon from '../../Icons/QuestionCircleIcon'
|
||||
import { AuthContext } from '../context/AuthContext'
|
||||
const logger = loglevel.getLogger('DasboardTable')
|
||||
logger.setLevel(config.logLevel)
|
||||
|
||||
@ -49,13 +50,14 @@ const ObjectTable = forwardRef(
|
||||
pageSize = 25,
|
||||
scrollHeight = 'calc(var(--unit-100vh) - 270px)',
|
||||
onDataChange,
|
||||
authenticated,
|
||||
initialPage = 1,
|
||||
cards = false,
|
||||
visibleColumns = {}
|
||||
visibleColumns = {},
|
||||
masterFilter = {}
|
||||
},
|
||||
ref
|
||||
) => {
|
||||
const { authenticated } = useContext(AuthContext)
|
||||
const { fetchTableData } = useContext(ApiServerContext)
|
||||
const isMobile = useMediaQuery({ maxWidth: 768 })
|
||||
const navigate = useNavigate()
|
||||
@ -107,7 +109,7 @@ const ObjectTable = forwardRef(
|
||||
const result = await fetchTableData(type, {
|
||||
page: pageNum,
|
||||
limit: pageSize,
|
||||
filter,
|
||||
filter: { ...filter, ...masterFilter },
|
||||
sorter,
|
||||
onDataChange
|
||||
})
|
||||
@ -408,17 +410,15 @@ const ObjectTable = forwardRef(
|
||||
<ObjectProperty
|
||||
{...prop}
|
||||
longId={false}
|
||||
type={prop.type}
|
||||
objectType={prop.objectType}
|
||||
value={getPropertyValue(record, prop.name)}
|
||||
objectData={record}
|
||||
isEditing={false}
|
||||
/>
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
// Add filter configuration if the property is filterable
|
||||
if (isFilterable) {
|
||||
// Add filter configuration if the property is filterable and not in masterFilter
|
||||
if (isFilterable && !Object.keys(masterFilter).includes(prop.name)) {
|
||||
columnConfig.filterDropdown = ({
|
||||
setSelectedKeys,
|
||||
selectedKeys,
|
||||
@ -606,7 +606,8 @@ ObjectTable.propTypes = {
|
||||
initialPage: PropTypes.number,
|
||||
cards: PropTypes.bool,
|
||||
cardRenderer: PropTypes.func,
|
||||
visibleColumns: PropTypes.object
|
||||
visibleColumns: PropTypes.object,
|
||||
masterFilter: PropTypes.object
|
||||
}
|
||||
|
||||
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>
|
||||
) : (
|
||||
<>
|
||||
<Text style={{ marginRight: 8 }}>{url}</Text>
|
||||
<Text style={{ marginRight: 8 }} ellipsis>
|
||||
{url}
|
||||
</Text>
|
||||
<Tooltip title='Open URL' arrow={false}>
|
||||
<Button
|
||||
icon={<LinkIcon style={{ fontSize: '14px' }} />}
|
||||
icon={<LinkIcon />}
|
||||
type='text'
|
||||
style={{ height: '22px' }}
|
||||
size='small'
|
||||
onClick={(e) => {
|
||||
e.preventDefault()
|
||||
window.open(url, '_blank', 'noopener,noreferrer')
|
||||
|
||||
@ -3,10 +3,10 @@ import { Button, Popover, Checkbox, Flex } from 'antd'
|
||||
import PropTypes from 'prop-types'
|
||||
|
||||
const ViewButton = ({
|
||||
loading = false,
|
||||
properties = [],
|
||||
collapseState = {},
|
||||
updateCollapseState = () => {},
|
||||
disabled = false,
|
||||
items = [],
|
||||
visibleState = {},
|
||||
updateVisibleState = () => {},
|
||||
...buttonProps
|
||||
}) => {
|
||||
return (
|
||||
@ -15,15 +15,15 @@ const ViewButton = ({
|
||||
return (
|
||||
<Flex vertical>
|
||||
<Flex vertical gap='middle' style={{ margin: '4px 8px' }}>
|
||||
{properties.map((property) => (
|
||||
{items.map((item) => (
|
||||
<Checkbox
|
||||
checked={collapseState[property.key]}
|
||||
key={property.key}
|
||||
checked={visibleState[item.key]}
|
||||
key={item.key}
|
||||
onChange={(e) => {
|
||||
updateCollapseState(property.key, e.target.checked)
|
||||
updateVisibleState(item.key, e.target.checked)
|
||||
}}
|
||||
>
|
||||
{property.label}
|
||||
{item.label}
|
||||
</Checkbox>
|
||||
))}
|
||||
</Flex>
|
||||
@ -33,7 +33,7 @@ const ViewButton = ({
|
||||
placement='bottomLeft'
|
||||
arrow={false}
|
||||
>
|
||||
<Button disabled={loading} {...buttonProps}>
|
||||
<Button disabled={disabled} {...buttonProps}>
|
||||
View
|
||||
</Button>
|
||||
</Popover>
|
||||
@ -41,15 +41,15 @@ const ViewButton = ({
|
||||
}
|
||||
|
||||
ViewButton.propTypes = {
|
||||
loading: PropTypes.bool,
|
||||
properties: PropTypes.arrayOf(
|
||||
disabled: PropTypes.bool,
|
||||
items: PropTypes.arrayOf(
|
||||
PropTypes.shape({
|
||||
key: PropTypes.string.isRequired,
|
||||
label: PropTypes.string.isRequired
|
||||
})
|
||||
),
|
||||
collapseState: PropTypes.object,
|
||||
updateCollapseState: PropTypes.func
|
||||
visibleState: PropTypes.object,
|
||||
updateVisibleState: PropTypes.func
|
||||
}
|
||||
|
||||
export default ViewButton
|
||||
|
||||
@ -331,14 +331,25 @@ const ApiServerProvider = ({ children }) => {
|
||||
fileLink.parentNode.removeChild(fileLink)
|
||||
} catch (error) {
|
||||
logger.error('Failed to download GCode file content:', error)
|
||||
|
||||
if (error.response) {
|
||||
messageApi.error('Error downloading GCode file:', error.response.status)
|
||||
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 {
|
||||
messageApi.error(
|
||||
'An unexpected error occurred while downloading. Please try again later.'
|
||||
showError(
|
||||
'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
|
||||
maskClosable={true}
|
||||
footer={[
|
||||
<Button
|
||||
key='retry'
|
||||
onClick={() => {
|
||||
setShowErrorModal(false)
|
||||
}}
|
||||
>
|
||||
Close
|
||||
</Button>,
|
||||
<Button key='retry' icon={<ReloadIcon />} onClick={handleRetry}>
|
||||
Retry
|
||||
</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) {
|
||||
const model = getModelByName(name)
|
||||
|
||||
@ -132,3 +142,53 @@ export const getPropertyValue = (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 InfoCircleIcon from '../../components/Icons/InfoCircleIcon'
|
||||
|
||||
export const AuditLog = {
|
||||
name: 'auditlog',
|
||||
name: 'auditLog',
|
||||
label: 'Audit Log',
|
||||
prefix: 'ADL',
|
||||
icon: AuditLogIcon,
|
||||
actions: [
|
||||
actions: [],
|
||||
columns: ['_id', 'owner', 'owner._id', 'parent._id', 'changes', 'createdAt'],
|
||||
filters: ['_id', 'owner._id', 'parent._id'],
|
||||
sorters: [],
|
||||
properties: [
|
||||
{
|
||||
name: 'info',
|
||||
label: 'Info',
|
||||
default: true,
|
||||
row: true,
|
||||
icon: InfoCircleIcon,
|
||||
url: (_id) => `/dashboard/management/auditlogs/info?auditLogId=${_id}`
|
||||
name: '_id',
|
||||
label: 'ID',
|
||||
type: 'id',
|
||||
objectType: 'auditLog',
|
||||
columnFixed: 'left',
|
||||
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 InfoCircleIcon from '../../components/Icons/InfoCircleIcon'
|
||||
import ReloadIcon from '../../components/Icons/ReloadIcon'
|
||||
|
||||
export const Filament = {
|
||||
name: 'filament',
|
||||
@ -14,6 +16,21 @@ export const Filament = {
|
||||
row: true,
|
||||
icon: InfoCircleIcon,
|
||||
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: [
|
||||
|
||||
@ -1,5 +1,8 @@
|
||||
import DownloadIcon from '../../components/Icons/DownloadIcon'
|
||||
import EditIcon from '../../components/Icons/EditIcon'
|
||||
import GCodeFileIcon from '../../components/Icons/GCodeFileIcon'
|
||||
import InfoCircleIcon from '../../components/Icons/InfoCircleIcon'
|
||||
import ReloadIcon from '../../components/Icons/ReloadIcon'
|
||||
|
||||
export const GCodeFile = {
|
||||
name: 'gcodeFile',
|
||||
@ -15,12 +18,28 @@ export const GCodeFile = {
|
||||
icon: InfoCircleIcon,
|
||||
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',
|
||||
label: 'Download',
|
||||
row: true,
|
||||
icon: DownloadIcon,
|
||||
url: (_id) =>
|
||||
`/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 InfoCircleIcon from '../../components/Icons/InfoCircleIcon'
|
||||
import ReloadIcon from '../../components/Icons/ReloadIcon'
|
||||
import EditIcon from '../../components/Icons/EditIcon'
|
||||
|
||||
export const Job = {
|
||||
name: 'job',
|
||||
@ -14,6 +16,19 @@ export const Job = {
|
||||
row: true,
|
||||
icon: InfoCircleIcon,
|
||||
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: [
|
||||
@ -25,7 +40,7 @@ export const Job = {
|
||||
'createdAt'
|
||||
],
|
||||
filters: ['state', '_id', 'gcodeFile._id', 'quantity'],
|
||||
sorters: ['createdAt', 'state', 'quantity', '_id'],
|
||||
sorters: ['createdAt', 'state', 'quantity', 'gcodeFile'],
|
||||
properties: [
|
||||
{
|
||||
name: '_id',
|
||||
|
||||
@ -1,5 +1,7 @@
|
||||
import NoteTypeIcon from '../../components/Icons/NoteTypeIcon'
|
||||
import InfoCircleIcon from '../../components/Icons/InfoCircleIcon'
|
||||
import ReloadIcon from '../../components/Icons/ReloadIcon'
|
||||
import EditIcon from '../../components/Icons/EditIcon'
|
||||
|
||||
export const NoteType = {
|
||||
name: 'noteType',
|
||||
@ -14,6 +16,21 @@ export const NoteType = {
|
||||
row: true,
|
||||
icon: InfoCircleIcon,
|
||||
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'],
|
||||
|
||||
@ -1,5 +1,8 @@
|
||||
import DownloadIcon from '../../components/Icons/DownloadIcon'
|
||||
import EditIcon from '../../components/Icons/EditIcon'
|
||||
import InfoCircleIcon from '../../components/Icons/InfoCircleIcon'
|
||||
import PartIcon from '../../components/Icons/PartIcon'
|
||||
import ReloadIcon from '../../components/Icons/ReloadIcon'
|
||||
|
||||
export const Part = {
|
||||
name: 'part',
|
||||
@ -14,10 +17,39 @@ export const Part = {
|
||||
row: true,
|
||||
icon: InfoCircleIcon,
|
||||
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'],
|
||||
filters: ['name', '_id', 'product', 'product._id'],
|
||||
columns: [
|
||||
'name',
|
||||
'_id',
|
||||
'product',
|
||||
'product._id',
|
||||
'globalPricing',
|
||||
'createdAt'
|
||||
],
|
||||
filters: ['name', '_id', 'product', 'product._id', 'globalPricing'],
|
||||
sorters: ['name', 'email', 'role', 'createdAt', '_id'],
|
||||
properties: [
|
||||
{
|
||||
@ -25,8 +57,9 @@ export const Part = {
|
||||
label: 'ID',
|
||||
columnFixed: 'left',
|
||||
type: 'id',
|
||||
objectType: 'user',
|
||||
showCopy: true
|
||||
objectType: 'part',
|
||||
showCopy: true,
|
||||
readOnly: true
|
||||
},
|
||||
{
|
||||
name: 'createdAt',
|
||||
@ -52,13 +85,58 @@ export const Part = {
|
||||
name: 'product',
|
||||
label: 'Product',
|
||||
type: 'object',
|
||||
required: true,
|
||||
objectType: 'product'
|
||||
},
|
||||
{
|
||||
name: 'product._id',
|
||||
label: 'Product ID',
|
||||
type: 'id',
|
||||
readOnly: true,
|
||||
showHyperlink: true,
|
||||
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 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 = {
|
||||
name: 'printer',
|
||||
@ -14,12 +17,34 @@ export const Printer = {
|
||||
row: true,
|
||||
icon: InfoCircleIcon,
|
||||
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'],
|
||||
filters: ['name', '_id', 'state', 'tags'],
|
||||
sorters: ['name', 'state', 'connectedAt', '_id'],
|
||||
sorters: ['name', 'state', 'connectedAt'],
|
||||
properties: [
|
||||
{
|
||||
name: '_id',
|
||||
|
||||
@ -1,5 +1,7 @@
|
||||
import ProductIcon from '../../components/Icons/ProductIcon'
|
||||
import InfoCircleIcon from '../../components/Icons/InfoCircleIcon'
|
||||
import ReloadIcon from '../../components/Icons/ReloadIcon'
|
||||
import EditIcon from '../../components/Icons/EditIcon'
|
||||
|
||||
export const Product = {
|
||||
name: 'product',
|
||||
@ -14,16 +16,43 @@ export const Product = {
|
||||
row: true,
|
||||
icon: InfoCircleIcon,
|
||||
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: [
|
||||
{
|
||||
name: '_id',
|
||||
label: 'ID',
|
||||
type: 'id',
|
||||
objectType: 'printer',
|
||||
showCopy: true
|
||||
objectType: 'product',
|
||||
showCopy: true,
|
||||
readOnly: true
|
||||
},
|
||||
{
|
||||
name: 'createdAt',
|
||||
@ -42,6 +71,59 @@ export const Product = {
|
||||
label: 'Updated At',
|
||||
type: 'dateTime',
|
||||
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 InfoCircleIcon from '../../components/Icons/InfoCircleIcon'
|
||||
import ReloadIcon from '../../components/Icons/ReloadIcon'
|
||||
|
||||
export const User = {
|
||||
name: 'user',
|
||||
@ -14,9 +15,15 @@ export const User = {
|
||||
row: true,
|
||||
icon: InfoCircleIcon,
|
||||
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'],
|
||||
filters: ['name', '_id', 'email', 'role'],
|
||||
sorters: ['name', 'email', 'role', 'createdAt', '_id'],
|
||||
|
||||
@ -1,5 +1,7 @@
|
||||
import VendorIcon from '../../components/Icons/VendorIcon'
|
||||
import InfoCircleIcon from '../../components/Icons/InfoCircleIcon'
|
||||
import EditIcon from '../../components/Icons/EditIcon'
|
||||
import ReloadIcon from '../../components/Icons/ReloadIcon'
|
||||
|
||||
export const Vendor = {
|
||||
name: 'vendor',
|
||||
@ -14,10 +16,25 @@ export const Vendor = {
|
||||
row: true,
|
||||
icon: InfoCircleIcon,
|
||||
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}`,
|
||||
columns: ['name', '_id', 'country', 'email', 'createdAt'],
|
||||
columns: ['name', '_id', 'country', 'email', 'website', 'createdAt'],
|
||||
filters: ['name', '_id', 'country', 'email'],
|
||||
sorters: ['name', 'country', 'email', 'createdAt', '_id'],
|
||||
properties: [
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user