Refactor LockIndicator imports to use common path across components, remove unused LockIndicator file, and update related components for consistency. Enhance DashboardBreadcrumb for improved path mapping and add Hosts section to ManagementSidebar. Adjust ObjectSelect for better object handling and update ObjectTable for improved loading behavior.
This commit is contained in:
parent
587ef7f480
commit
66e137fac2
75
src/components/App/AuthCallback.jsx
Normal file
75
src/components/App/AuthCallback.jsx
Normal file
@ -0,0 +1,75 @@
|
||||
import React, { useState, useEffect, useContext, useCallback } from 'react'
|
||||
import { Flex, Card, Alert } from 'antd'
|
||||
import { LoadingOutlined } from '@ant-design/icons'
|
||||
import AuthParticles from './AppParticles'
|
||||
import FarmControlLogo from '../Logos/FarmControlLogo'
|
||||
import { AuthContext } from '../Dashboard/context/AuthContext'
|
||||
import { useLocation, useNavigate } from 'react-router-dom'
|
||||
|
||||
const AuthCallback = () => {
|
||||
const [isVisible, setIsVisible] = useState(false)
|
||||
const [loading, setLoading] = useState(false)
|
||||
const [initialized, setInitialized] = useState(false)
|
||||
const { getLoginToken } = useContext(AuthContext)
|
||||
const location = useLocation()
|
||||
const navigate = useNavigate()
|
||||
const code = new URLSearchParams(location.search).get('code')
|
||||
const state = new URLSearchParams(location.search).get('state')
|
||||
|
||||
const handleGetloginToken = useCallback(async () => {
|
||||
await getLoginToken(code)
|
||||
setLoading(false)
|
||||
navigate(state)
|
||||
}, [code, state, navigate, getLoginToken])
|
||||
|
||||
useEffect(() => {
|
||||
const timer = setTimeout(() => {
|
||||
setIsVisible(true)
|
||||
}, 1000)
|
||||
if (!initialized && !loading) {
|
||||
handleGetloginToken()
|
||||
setInitialized(true)
|
||||
setLoading(true)
|
||||
}
|
||||
return () => clearTimeout(timer)
|
||||
}, [handleGetloginToken, initialized, loading])
|
||||
|
||||
return (
|
||||
<div
|
||||
style={{
|
||||
backgroundColor: 'black'
|
||||
}}
|
||||
>
|
||||
<div
|
||||
style={{
|
||||
backgroundColor: 'black',
|
||||
minHeight: '100vh',
|
||||
transition: 'opacity 0.5s ease-in-out',
|
||||
opacity: isVisible ? 1 : 0
|
||||
}}
|
||||
>
|
||||
<AuthParticles />
|
||||
<Flex
|
||||
align='center'
|
||||
justify='center'
|
||||
vertical
|
||||
style={{ height: '100vh' }}
|
||||
gap={'large'}
|
||||
>
|
||||
<Card style={{ borderRadius: 20 }}>
|
||||
<Flex vertical align='center'>
|
||||
<FarmControlLogo style={{ fontSize: '500px', height: '40px' }} />
|
||||
</Flex>
|
||||
</Card>
|
||||
<Alert
|
||||
message='Loading Farm Control please wait...'
|
||||
icon={<LoadingOutlined />}
|
||||
showIcon
|
||||
/>
|
||||
</Flex>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default AuthCallback
|
||||
@ -11,7 +11,7 @@ import EditButtons from '../../common/EditButtons'
|
||||
import ActionHandler from '../../common/ActionHandler'
|
||||
import InfoCollapse from '../../common/InfoCollapse'
|
||||
import NotesPanel from '../../common/NotesPanel'
|
||||
import LockIndicator from '../../Management/Filaments/LockIndicator'
|
||||
import LockIndicator from '../../common/LockIndicator'
|
||||
import InfoCircleIcon from '../../../Icons/InfoCircleIcon'
|
||||
import FilamentStockIcon from '../../../Icons/FilamentStockIcon'
|
||||
import NoteIcon from '../../../Icons/NoteIcon'
|
||||
|
||||
@ -90,7 +90,6 @@ const LoadFilamentStock = ({
|
||||
)
|
||||
)
|
||||
}
|
||||
logger.debug(statusUpdate)
|
||||
}
|
||||
|
||||
printServer.emit('printer.objects.subscribe', params)
|
||||
|
||||
@ -14,7 +14,7 @@ import NoteIcon from '../../../Icons/NoteIcon.jsx'
|
||||
import AuditLogIcon from '../../../Icons/AuditLogIcon.jsx'
|
||||
import EditObjectForm from '../../common/EditObjectForm'
|
||||
import EditButtons from '../../common/EditButtons'
|
||||
import LockIndicator from './LockIndicator'
|
||||
import LockIndicator from '../../common/LockIndicator.jsx'
|
||||
import ActionHandler from '../../common/ActionHandler'
|
||||
import ObjectActions from '../../common/ObjectActions.jsx'
|
||||
import ObjectTable from '../../common/ObjectTable.jsx'
|
||||
|
||||
105
src/components/Dashboard/Management/Hosts.jsx
Normal file
105
src/components/Dashboard/Management/Hosts.jsx
Normal file
@ -0,0 +1,105 @@
|
||||
// src/hosts.js
|
||||
|
||||
import React, { useRef, useState } from 'react'
|
||||
import { Button, Flex, Space, Modal, message, Dropdown } from 'antd'
|
||||
|
||||
import NewHost from './Hosts/NewHost'
|
||||
|
||||
import useColumnVisibility from '../hooks/useColumnVisibility'
|
||||
import ColumnViewButton from '../common/ColumnViewButton'
|
||||
import ObjectTable from '../common/ObjectTable'
|
||||
import PlusIcon from '../../Icons/PlusIcon'
|
||||
import ReloadIcon from '../../Icons/ReloadIcon'
|
||||
import ListIcon from '../../Icons/ListIcon'
|
||||
import GridIcon from '../../Icons/GridIcon'
|
||||
import useViewMode from '../hooks/useViewMode'
|
||||
|
||||
const Hosts = () => {
|
||||
const [messageApi, contextHolder] = message.useMessage()
|
||||
const [newHostOpen, setNewHostOpen] = useState(false)
|
||||
const tableRef = useRef()
|
||||
|
||||
// View mode state (cards/list), persisted in sessionStorage via custom hook
|
||||
const [viewMode, setViewMode] = useViewMode('host')
|
||||
|
||||
const [columnVisibility, setColumnVisibility] = useColumnVisibility('host')
|
||||
|
||||
const actionItems = {
|
||||
items: [
|
||||
{
|
||||
label: 'New Host',
|
||||
key: 'newHost',
|
||||
icon: <PlusIcon />
|
||||
},
|
||||
{ type: 'divider' },
|
||||
{
|
||||
label: 'Reload List',
|
||||
key: 'reloadList',
|
||||
icon: <ReloadIcon />
|
||||
}
|
||||
],
|
||||
onClick: ({ key }) => {
|
||||
if (key === 'reloadList') {
|
||||
tableRef.current?.reload()
|
||||
} else if (key === 'newHost') {
|
||||
setNewHostOpen(true)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<Flex vertical={'true'} gap='large'>
|
||||
{contextHolder}
|
||||
<Flex justify={'space-between'}>
|
||||
<Space>
|
||||
<Dropdown menu={actionItems}>
|
||||
<Button>Actions</Button>
|
||||
</Dropdown>
|
||||
<ColumnViewButton
|
||||
type='host'
|
||||
loading={false}
|
||||
visibleState={columnVisibility}
|
||||
updateVisibleState={setColumnVisibility}
|
||||
/>
|
||||
</Space>
|
||||
<Space>
|
||||
<Button
|
||||
icon={viewMode === 'cards' ? <ListIcon /> : <GridIcon />}
|
||||
onClick={() =>
|
||||
setViewMode(viewMode === 'cards' ? 'list' : 'cards')
|
||||
}
|
||||
/>
|
||||
</Space>
|
||||
</Flex>
|
||||
|
||||
<ObjectTable
|
||||
ref={tableRef}
|
||||
type='host'
|
||||
cards={viewMode === 'cards'}
|
||||
visibleColumns={columnVisibility}
|
||||
/>
|
||||
|
||||
<Modal
|
||||
open={newHostOpen}
|
||||
footer={null}
|
||||
width={700}
|
||||
onCancel={() => {
|
||||
setNewHostOpen(false)
|
||||
}}
|
||||
>
|
||||
<NewHost
|
||||
onOk={() => {
|
||||
setNewHostOpen(false)
|
||||
messageApi.success('New host added successfully.')
|
||||
tableRef.current?.reload()
|
||||
}}
|
||||
reset={newHostOpen}
|
||||
/>
|
||||
</Modal>
|
||||
</Flex>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
export default Hosts
|
||||
186
src/components/Dashboard/Management/Hosts/HostInfo.jsx
Normal file
186
src/components/Dashboard/Management/Hosts/HostInfo.jsx
Normal file
@ -0,0 +1,186 @@
|
||||
import React from 'react'
|
||||
import { useLocation } from 'react-router-dom'
|
||||
import { Space, Flex, Card } from 'antd'
|
||||
import { LoadingOutlined } from '@ant-design/icons'
|
||||
import loglevel from 'loglevel'
|
||||
import config from '../../../../config.js'
|
||||
import useCollapseState from '../../hooks/useCollapseState.js'
|
||||
import NotesPanel from '../../common/NotesPanel.jsx'
|
||||
import InfoCollapse from '../../common/InfoCollapse.jsx'
|
||||
import ObjectInfo from '../../common/ObjectInfo.jsx'
|
||||
import ViewButton from '../../common/ViewButton.jsx'
|
||||
import InfoCircleIcon from '../../../Icons/InfoCircleIcon.jsx'
|
||||
import NoteIcon from '../../../Icons/NoteIcon.jsx'
|
||||
import AuditLogIcon from '../../../Icons/AuditLogIcon.jsx'
|
||||
import EditObjectForm from '../../common/EditObjectForm.jsx'
|
||||
import EditButtons from '../../common/EditButtons.jsx'
|
||||
import LockIndicator from '../../common/LockIndicator.jsx'
|
||||
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('HostInfo')
|
||||
log.setLevel(config.logLevel)
|
||||
|
||||
const HostInfo = () => {
|
||||
const location = useLocation()
|
||||
const hostId = new URLSearchParams(location.search).get('hostId')
|
||||
const [collapseState, updateCollapseState] = useCollapseState('HostInfo', {
|
||||
info: true,
|
||||
stocks: true,
|
||||
notes: true,
|
||||
auditLogs: true
|
||||
})
|
||||
|
||||
return (
|
||||
<EditObjectForm id={hostId} type='host' style={{ height: '100%' }}>
|
||||
{({
|
||||
loading,
|
||||
isEditing,
|
||||
startEditing,
|
||||
cancelEditing,
|
||||
handleUpdate,
|
||||
formValid,
|
||||
objectData,
|
||||
editLoading,
|
||||
lock,
|
||||
fetchObject
|
||||
}) => {
|
||||
// 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='host'
|
||||
id={hostId}
|
||||
disabled={loading}
|
||||
/>
|
||||
<ViewButton
|
||||
disabled={loading}
|
||||
items={[
|
||||
{ key: 'info', label: 'Host 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='Host Information'
|
||||
icon={<InfoCircleIcon />}
|
||||
active={collapseState.info}
|
||||
onToggle={(expanded) =>
|
||||
updateCollapseState('info', expanded)
|
||||
}
|
||||
collapseKey='info'
|
||||
>
|
||||
<ObjectInfo
|
||||
loading={loading}
|
||||
indicator={<LoadingOutlined />}
|
||||
isEditing={isEditing}
|
||||
type='host'
|
||||
objectData={objectData}
|
||||
/>
|
||||
</InfoCollapse>
|
||||
|
||||
<InfoCollapse
|
||||
title='Notes'
|
||||
icon={<NoteIcon />}
|
||||
active={collapseState.notes}
|
||||
onToggle={(expanded) =>
|
||||
updateCollapseState('notes', expanded)
|
||||
}
|
||||
collapseKey='notes'
|
||||
>
|
||||
<Card>
|
||||
<NotesPanel _id={hostId} type='host' />
|
||||
</Card>
|
||||
</InfoCollapse>
|
||||
|
||||
<InfoCollapse
|
||||
title='Audit Logs'
|
||||
icon={<AuditLogIcon />}
|
||||
active={collapseState.auditLogs}
|
||||
onToggle={(expanded) =>
|
||||
updateCollapseState('auditLogs', expanded)
|
||||
}
|
||||
collapseKey='auditLogs'
|
||||
>
|
||||
{loading ? (
|
||||
<InfoCollapsePlaceholder />
|
||||
) : (
|
||||
<ObjectTable
|
||||
type='auditLog'
|
||||
masterFilter={{ 'parent._id': hostId }}
|
||||
visibleColumns={{ _id: false, 'parent._id': false }}
|
||||
/>
|
||||
)}
|
||||
</InfoCollapse>
|
||||
</Flex>
|
||||
</div>
|
||||
</Flex>
|
||||
)}
|
||||
</ActionHandler>
|
||||
)
|
||||
}}
|
||||
</EditObjectForm>
|
||||
)
|
||||
}
|
||||
|
||||
export default HostInfo
|
||||
117
src/components/Dashboard/Management/Hosts/NewHost.jsx
Normal file
117
src/components/Dashboard/Management/Hosts/NewHost.jsx
Normal file
@ -0,0 +1,117 @@
|
||||
import PropTypes from 'prop-types'
|
||||
import React, { useState } from 'react'
|
||||
import { useMediaQuery } from 'react-responsive'
|
||||
import { Typography, Flex, Steps, Divider } from 'antd'
|
||||
|
||||
import ObjectInfo from '../../common/ObjectInfo'
|
||||
import NewObjectForm from '../../common/NewObjectForm'
|
||||
import NewObjectButtons from '../../common/NewObjectButtons'
|
||||
|
||||
const { Title } = Typography
|
||||
|
||||
const NewHost = ({ onOk }) => {
|
||||
const [currentStep, setCurrentStep] = useState(0)
|
||||
|
||||
const isMobile = useMediaQuery({ maxWidth: 768 })
|
||||
|
||||
return (
|
||||
<NewObjectForm type={'host'}>
|
||||
{({ handleSubmit, submitLoading, objectData, formValid }) => {
|
||||
const steps = [
|
||||
{
|
||||
title: 'Required',
|
||||
key: 'required',
|
||||
content: (
|
||||
<ObjectInfo
|
||||
type='host'
|
||||
column={1}
|
||||
bordered={false}
|
||||
isEditing={true}
|
||||
required={true}
|
||||
objectData={objectData}
|
||||
/>
|
||||
)
|
||||
},
|
||||
{
|
||||
title: 'Optional',
|
||||
key: 'optional',
|
||||
content: (
|
||||
<ObjectInfo
|
||||
type='host'
|
||||
column={1}
|
||||
bordered={false}
|
||||
isEditing={true}
|
||||
required={false}
|
||||
objectData={objectData}
|
||||
/>
|
||||
)
|
||||
},
|
||||
{
|
||||
title: 'Summary',
|
||||
key: 'summary',
|
||||
content: (
|
||||
<ObjectInfo
|
||||
type='host'
|
||||
column={1}
|
||||
bordered={false}
|
||||
visibleProperties={{
|
||||
_id: false,
|
||||
createdAt: false,
|
||||
updatedAt: false
|
||||
}}
|
||||
isEditing={false}
|
||||
objectData={objectData}
|
||||
/>
|
||||
)
|
||||
}
|
||||
]
|
||||
return (
|
||||
<Flex gap='middle'>
|
||||
{!isMobile && (
|
||||
<div style={{ minWidth: '160px' }}>
|
||||
<Steps
|
||||
current={currentStep}
|
||||
items={steps}
|
||||
direction='vertical'
|
||||
style={{ width: 'fit-content' }}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{!isMobile && (
|
||||
<Divider type='vertical' style={{ height: 'unset' }} />
|
||||
)}
|
||||
|
||||
<Flex vertical gap='middle' style={{ flexGrow: 1 }}>
|
||||
<Title level={2} style={{ margin: 0 }}>
|
||||
New Host
|
||||
</Title>
|
||||
<div style={{ minHeight: '260px', marginBottom: 8 }}>
|
||||
{steps[currentStep].content}
|
||||
</div>
|
||||
<NewObjectButtons
|
||||
currentStep={currentStep}
|
||||
totalSteps={steps.length}
|
||||
onPrevious={() => setCurrentStep((prev) => prev - 1)}
|
||||
onNext={() => setCurrentStep((prev) => prev + 1)}
|
||||
onSubmit={() => {
|
||||
handleSubmit()
|
||||
onOk()
|
||||
}}
|
||||
formValid={formValid}
|
||||
submitLoading={submitLoading}
|
||||
/>
|
||||
</Flex>
|
||||
</Flex>
|
||||
)
|
||||
}}
|
||||
</NewObjectForm>
|
||||
)
|
||||
}
|
||||
|
||||
NewHost.propTypes = {
|
||||
onOk: PropTypes.func.isRequired,
|
||||
reset: PropTypes.bool
|
||||
}
|
||||
|
||||
export default NewHost
|
||||
@ -11,6 +11,7 @@ import SettingsIcon from '../../Icons/SettingsIcon'
|
||||
import AuditLogIcon from '../../Icons/AuditLogIcon'
|
||||
import DeveloperIcon from '../../Icons/DeveloperIcon'
|
||||
import PersonIcon from '../../Icons/PersonIcon'
|
||||
import HostIcon from '../../Icons/HostIcon'
|
||||
|
||||
const items = [
|
||||
{
|
||||
@ -50,6 +51,12 @@ const items = [
|
||||
label: 'Note Types',
|
||||
path: '/dashboard/management/notetypes'
|
||||
},
|
||||
{
|
||||
key: 'hosts',
|
||||
icon: <HostIcon />,
|
||||
label: 'Hosts',
|
||||
path: '/dashboard/management/hosts'
|
||||
},
|
||||
{
|
||||
key: 'users',
|
||||
icon: <PersonIcon />,
|
||||
@ -91,7 +98,8 @@ const routeKeyMap = {
|
||||
'/dashboard/management/materials': 'materials',
|
||||
'/dashboard/management/notetypes': 'notetypes',
|
||||
'/dashboard/management/settings': 'settings',
|
||||
'/dashboard/management/auditlogs': 'auditlogs'
|
||||
'/dashboard/management/auditlogs': 'auditlogs',
|
||||
'/dashboard/management/hosts': 'hosts'
|
||||
}
|
||||
|
||||
const ManagementSidebar = (props) => {
|
||||
|
||||
@ -10,7 +10,7 @@ import InfoCircleIcon from '../../../Icons/InfoCircleIcon.jsx'
|
||||
import AuditLogIcon from '../../../Icons/AuditLogIcon.jsx'
|
||||
import EditObjectForm from '../../common/EditObjectForm'
|
||||
import EditButtons from '../../common/EditButtons'
|
||||
import LockIndicator from '../Filaments/LockIndicator'
|
||||
import LockIndicator from '../../common/LockIndicator.jsx'
|
||||
import ActionHandler from '../../common/ActionHandler.jsx'
|
||||
import ObjectActions from '../../common/ObjectActions.jsx'
|
||||
import ObjectTable from '../../common/ObjectTable.jsx'
|
||||
|
||||
@ -8,7 +8,7 @@ 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 LockIndicator from '../../common/LockIndicator.jsx'
|
||||
import InfoCircleIcon from '../../../Icons/InfoCircleIcon.jsx'
|
||||
import NoteIcon from '../../../Icons/NoteIcon.jsx'
|
||||
import AuditLogIcon from '../../../Icons/AuditLogIcon.jsx'
|
||||
|
||||
@ -8,7 +8,7 @@ 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 LockIndicator from '../../common/LockIndicator.jsx'
|
||||
import InfoCircleIcon from '../../../Icons/InfoCircleIcon.jsx'
|
||||
import NoteIcon from '../../../Icons/NoteIcon.jsx'
|
||||
import AuditLogIcon from '../../../Icons/AuditLogIcon.jsx'
|
||||
|
||||
@ -12,7 +12,7 @@ import NoteIcon from '../../../Icons/NoteIcon.jsx'
|
||||
import AuditLogIcon from '../../../Icons/AuditLogIcon.jsx'
|
||||
import EditObjectForm from '../../common/EditObjectForm'
|
||||
import EditButtons from '../../common/EditButtons'
|
||||
import LockIndicator from '../Filaments/LockIndicator'
|
||||
import LockIndicator from '../../common/LockIndicator.jsx'
|
||||
import ActionHandler from '../../common/ActionHandler'
|
||||
import ObjectActions from '../../common/ObjectActions.jsx'
|
||||
import ObjectTable from '../../common/ObjectTable.jsx'
|
||||
|
||||
@ -13,7 +13,7 @@ import NoteIcon from '../../../Icons/NoteIcon.jsx'
|
||||
import AuditLogIcon from '../../../Icons/AuditLogIcon.jsx'
|
||||
import EditObjectForm from '../../common/EditObjectForm'
|
||||
import EditButtons from '../../common/EditButtons'
|
||||
import LockIndicator from '../Filaments/LockIndicator'
|
||||
import LockIndicator from '../../common/LockIndicator.jsx'
|
||||
import ActionHandler from '../../common/ActionHandler.jsx'
|
||||
import ObjectActions from '../../common/ObjectActions.jsx'
|
||||
import ObjectTable from '../../common/ObjectTable.jsx'
|
||||
|
||||
@ -9,7 +9,7 @@ import ObjectInfo from '../../common/ObjectInfo'
|
||||
import ViewButton from '../../common/ViewButton'
|
||||
import EditObjectForm from '../../common/EditObjectForm'
|
||||
import EditButtons from '../../common/EditButtons'
|
||||
import LockIndicator from '../../Management/Filaments/LockIndicator'
|
||||
import LockIndicator from '../../common/LockIndicator.jsx'
|
||||
import ActionHandler from '../../common/ActionHandler'
|
||||
import InfoCircleIcon from '../../../Icons/InfoCircleIcon.jsx'
|
||||
import NoteIcon from '../../../Icons/NoteIcon.jsx'
|
||||
|
||||
@ -9,8 +9,7 @@ import ObjectInfo from '../../common/ObjectInfo'
|
||||
import ViewButton from '../../common/ViewButton'
|
||||
import EditObjectForm from '../../common/EditObjectForm'
|
||||
import EditButtons from '../../common/EditButtons'
|
||||
import LockIndicator from '../../Management/Filaments/LockIndicator'
|
||||
import SubJobsTree from '../../common/SubJobsTree'
|
||||
import LockIndicator from '../../common/LockIndicator.jsx'
|
||||
import ActionHandler from '../../common/ActionHandler'
|
||||
import InfoCircleIcon from '../../../Icons/InfoCircleIcon'
|
||||
import JobIcon from '../../../Icons/JobIcon'
|
||||
@ -128,7 +127,11 @@ const JobInfo = () => {
|
||||
}
|
||||
collapseKey='subJobs'
|
||||
>
|
||||
<SubJobsTree jobData={objectData} loading={loading} />
|
||||
<ObjectTable
|
||||
type='subJob'
|
||||
masterFilter={{ 'job._id': jobId }}
|
||||
visibleColumns={{ 'job._id': false }}
|
||||
/>
|
||||
</InfoCollapse>
|
||||
|
||||
<InfoCollapse
|
||||
|
||||
@ -17,7 +17,7 @@ const NewJob = ({ onOk }) => {
|
||||
<NewObjectForm
|
||||
type={'job'}
|
||||
defaultValues={{
|
||||
active: true
|
||||
state: { type: 'draft' }
|
||||
}}
|
||||
>
|
||||
{({ handleSubmit, submitLoading, objectData, formValid }) => {
|
||||
@ -36,20 +36,6 @@ const NewJob = ({ onOk }) => {
|
||||
/>
|
||||
)
|
||||
},
|
||||
{
|
||||
title: 'Optional',
|
||||
key: 'optional',
|
||||
content: (
|
||||
<ObjectInfo
|
||||
type='job'
|
||||
column={1}
|
||||
bordered={false}
|
||||
isEditing={true}
|
||||
required={false}
|
||||
objectData={objectData}
|
||||
/>
|
||||
)
|
||||
},
|
||||
{
|
||||
title: 'Summary',
|
||||
key: 'summary',
|
||||
@ -61,7 +47,8 @@ const NewJob = ({ onOk }) => {
|
||||
visibleProperties={{
|
||||
_id: false,
|
||||
createdAt: false,
|
||||
updatedAt: false
|
||||
updatedAt: false,
|
||||
startedAt: false
|
||||
}}
|
||||
isEditing={false}
|
||||
objectData={objectData}
|
||||
|
||||
@ -9,7 +9,7 @@ import ObjectInfo from '../../common/ObjectInfo'
|
||||
import ViewButton from '../../common/ViewButton'
|
||||
import EditObjectForm from '../../common/EditObjectForm'
|
||||
import EditButtons from '../../common/EditButtons'
|
||||
import LockIndicator from '../../Management/Filaments/LockIndicator'
|
||||
import LockIndicator from '../../common/LockIndicator.jsx'
|
||||
import PrinterJobsTree from '../../common/PrinterJobsTree'
|
||||
import InfoCircleIcon from '../../../Icons/InfoCircleIcon.jsx'
|
||||
import NoteIcon from '../../../Icons/NoteIcon.jsx'
|
||||
|
||||
@ -23,7 +23,9 @@ const CountryDisplay = ({ countryCode }) => {
|
||||
hasBorderRadius={true}
|
||||
gradient='real-circular'
|
||||
/>
|
||||
<div>
|
||||
<Text ellipsis>{country.name}</Text>
|
||||
</div>
|
||||
</Flex>
|
||||
)
|
||||
}
|
||||
|
||||
@ -6,70 +6,55 @@ import ArrowLeftIcon from '../../Icons/ArrowLeftIcon'
|
||||
import ArrowRightIcon from '../../Icons/ArrowRightIcon'
|
||||
|
||||
const breadcrumbNameMap = {
|
||||
'/dashboard/production': 'Production',
|
||||
'/dashboard/inventory': 'Inventory',
|
||||
'/dashboard/management': 'Management',
|
||||
'/dashboard/developer': 'Developer',
|
||||
'/dashboard/production/overview': 'Overview',
|
||||
'/dashboard/production/printers': 'Printers',
|
||||
'/dashboard/production/printers/control': 'Control',
|
||||
'/dashboard/production/printers/info': 'Info',
|
||||
'/dashboard/production/jobs': 'Jobs',
|
||||
'/dashboard/production/subjobs': 'Sub Jobs',
|
||||
'/dashboard/production/jobs/info': 'Info',
|
||||
'/dashboard/production/gcodefiles': 'G Code Files',
|
||||
'/dashboard/production/gcodefiles/info': 'Info',
|
||||
'/dashboard/management/filaments': 'Filaments',
|
||||
'/dashboard/management/filaments/info': 'Info',
|
||||
'/dashboard/management/parts': 'Parts',
|
||||
'/dashboard/management/parts/info': 'Info',
|
||||
'/dashboard/management/products': 'Products',
|
||||
'/dashboard/management/products/info': 'Info',
|
||||
'/dashboard/management/vendors': 'Vendors',
|
||||
'/dashboard/management/vendors/info': 'Info',
|
||||
'/dashboard/management/materials': 'Materials',
|
||||
'/dashboard/management/materials/info': 'Info',
|
||||
'/dashboard/management/notetypes': 'Note Types',
|
||||
'/dashboard/management/notetypes/info': 'Info',
|
||||
'/dashboard/management/users': 'Users',
|
||||
'/dashboard/management/users/info': 'Info',
|
||||
'/dashboard/management/settings': 'Settings',
|
||||
'/dashboard/management/auditlogs': 'Audit Logs',
|
||||
'/dashboard/inventory/filamentstocks': 'Filament Stocks',
|
||||
'/dashboard/inventory/filamentstocks/info': 'Info',
|
||||
'/dashboard/inventory/partstocks': 'Part Stocks',
|
||||
'/dashboard/inventory/partstocks/info': 'Info',
|
||||
'/dashboard/inventory/productstocks': 'Products',
|
||||
'/dashboard/inventory/productstocks/info': 'Info',
|
||||
'/dashboard/inventory/stockevents': 'Stock Events',
|
||||
'/dashboard/inventory/stockevents/info': 'Info',
|
||||
'/dashboard/inventory/stockaudits': 'Stock Audits',
|
||||
'/dashboard/inventory/stockaudits/info': 'Info',
|
||||
'/dashboard/developer/sessionstorage': 'Session Storage',
|
||||
'/dashboard/developer/authcontextdebug': 'Auth Context Debug',
|
||||
'/dashboard/developer/printservercontextdebug': 'Print Server Context Debug'
|
||||
production: 'Production',
|
||||
inventory: 'Inventory',
|
||||
management: 'Management',
|
||||
developer: 'Developer',
|
||||
overview: 'Overview',
|
||||
printers: 'Printers',
|
||||
hosts: 'Hosts',
|
||||
control: 'Control',
|
||||
info: 'Info',
|
||||
jobs: 'Jobs',
|
||||
subjobs: 'Sub Jobs',
|
||||
gcodefiles: 'G Code Files',
|
||||
filaments: 'Filaments',
|
||||
parts: 'Parts',
|
||||
products: 'Products',
|
||||
vendors: 'Vendors',
|
||||
materials: 'Materials',
|
||||
notetypes: 'Note Types',
|
||||
users: 'Users',
|
||||
settings: 'Settings',
|
||||
auditlogs: 'Audit Logs',
|
||||
filamentstocks: 'Filament Stocks',
|
||||
partstocks: 'Part Stocks',
|
||||
productstocks: 'Products',
|
||||
stockevents: 'Stock Events',
|
||||
stockaudits: 'Stock Audits',
|
||||
sessionstorage: 'Session Storage',
|
||||
authcontextdebug: 'Auth Context Debug',
|
||||
printservercontextdebug: 'Print Server Context Debug'
|
||||
}
|
||||
|
||||
const mainSections = ['production', 'inventory', 'management', 'developer']
|
||||
|
||||
const DashboardBreadcrumb = () => {
|
||||
const location = useLocation()
|
||||
const navigate = useNavigate()
|
||||
const pathSnippets = location.pathname.split('/').filter((i) => i)
|
||||
|
||||
const breadcrumbItems = pathSnippets.map((_, index) => {
|
||||
const breadcrumbItems = pathSnippets.map((segment, index) => {
|
||||
const url = `/${pathSnippets.slice(0, index + 1).join('/')}`
|
||||
if (url != '/dashboard') {
|
||||
// Check if this is a main section (Production, Inventory, or Management)
|
||||
const isMainSection =
|
||||
url === '/dashboard/production' ||
|
||||
url === '/dashboard/inventory' ||
|
||||
url === '/dashboard/management'
|
||||
|
||||
if (segment !== 'dashboard') {
|
||||
const isMainSection = mainSections.includes(segment)
|
||||
const name = breadcrumbNameMap[segment] || segment
|
||||
return {
|
||||
title: isMainSection ? (
|
||||
<span style={{ padding: '0 12px' }}>{breadcrumbNameMap[url]}</span>
|
||||
<span style={{ padding: '0 12px' }}>{name}</span>
|
||||
) : (
|
||||
<Link to={url} style={{ padding: '0 12px' }}>
|
||||
{breadcrumbNameMap[url]}
|
||||
{name}
|
||||
</Link>
|
||||
),
|
||||
key: url
|
||||
|
||||
@ -37,6 +37,8 @@ import SearchIcon from '../../Icons/SearchIcon'
|
||||
import SettingsIcon from '../../Icons/SettingsIcon'
|
||||
import DeveloperIcon from '../../Icons/DeveloperIcon'
|
||||
import PrinterIcon from '../../Icons/PrinterIcon'
|
||||
import { ElectronContext } from '../context/ElectronContext'
|
||||
import DashboardWindowButtons from './DashboardWindowButtons'
|
||||
|
||||
const DashboardNavigation = () => {
|
||||
const { logout, userProfile } = useContext(AuthContext)
|
||||
@ -50,6 +52,7 @@ const DashboardNavigation = () => {
|
||||
const location = useLocation()
|
||||
const [selectedKey, setSelectedKey] = useState('production')
|
||||
const isMobile = useMediaQuery({ maxWidth: 768 })
|
||||
const { platform, isElectron } = useContext(ElectronContext)
|
||||
|
||||
useEffect(() => {
|
||||
const pathParts = location.pathname.split('/').filter(Boolean)
|
||||
@ -135,34 +138,17 @@ const DashboardNavigation = () => {
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<Header
|
||||
style={{
|
||||
width: '100vw',
|
||||
padding: 0,
|
||||
marginBottom: '0.1px',
|
||||
background: 'unset'
|
||||
}}
|
||||
theme='light'
|
||||
className='ant-menu-horizontal'
|
||||
>
|
||||
<Flex
|
||||
gap={'large'}
|
||||
align='center'
|
||||
className='ant-menu-light'
|
||||
style={{
|
||||
padding: '0 26px',
|
||||
height: '100%',
|
||||
borderBottom: '1px solid rgba(5, 5, 5, 0.00)'
|
||||
}}
|
||||
>
|
||||
{!isMobile ? (
|
||||
const navigationContents = (
|
||||
<>
|
||||
{isElectron && platform == 'darwin' ? <DashboardWindowButtons /> : null}
|
||||
{!isElectron && !isMobile ? (
|
||||
<FarmControlLogo style={{ fontSize: '200px' }} />
|
||||
) : (
|
||||
) : !isElectron && isMobile ? (
|
||||
<FarmControlLogoSmall style={{ fontSize: '48px' }} />
|
||||
)}
|
||||
) : null}
|
||||
<Menu
|
||||
mode='horizontal'
|
||||
className={isElectron ? 'electron-navigation' : null}
|
||||
items={mainMenuItems}
|
||||
style={{
|
||||
flexWrap: 'wrap',
|
||||
@ -281,9 +267,47 @@ const DashboardNavigation = () => {
|
||||
</Space>
|
||||
) : null}
|
||||
</Flex>
|
||||
</>
|
||||
)
|
||||
|
||||
return (
|
||||
<>
|
||||
{isElectron ? (
|
||||
<Flex
|
||||
className='ant-menu-horizontal ant-menu-light electron-navigation-wrapper'
|
||||
style={{ lineHeight: '40px', padding: '0 8px 0 4px' }}
|
||||
>
|
||||
{navigationContents}
|
||||
</Flex>
|
||||
) : (
|
||||
<Flex vertical>
|
||||
<Header
|
||||
style={{
|
||||
width: '100vw',
|
||||
padding: 0,
|
||||
marginBottom: '0.1px',
|
||||
background: 'unset'
|
||||
}}
|
||||
theme='light'
|
||||
className='ant-menu-horizontal'
|
||||
>
|
||||
<Flex
|
||||
gap={'large'}
|
||||
align='center'
|
||||
className='ant-menu-light'
|
||||
style={{
|
||||
padding: '0 26px',
|
||||
height: '100%',
|
||||
borderBottom: '1px solid rgba(5, 5, 5, 0.00)'
|
||||
}}
|
||||
>
|
||||
{navigationContents}
|
||||
</Flex>
|
||||
<Divider style={{ margin: 0 }} />
|
||||
</Header>
|
||||
</Flex>
|
||||
)}
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
@ -1,4 +1,4 @@
|
||||
import React, { useState, useEffect } from 'react'
|
||||
import React, { useState, useEffect, useContext } from 'react'
|
||||
import { Layout, Menu, Flex, Button } from 'antd'
|
||||
import { CaretDownFilled } from '@ant-design/icons'
|
||||
import CollapseSidebarIcon from '../../Icons/CollapseSidebarIcon'
|
||||
@ -6,7 +6,7 @@ import ExpandSidebarIcon from '../../Icons/ExpandSidebarIcon'
|
||||
import { useMediaQuery } from 'react-responsive'
|
||||
import { useNavigate } from 'react-router-dom'
|
||||
import PropTypes from 'prop-types'
|
||||
|
||||
import { ElectronContext } from '../context/ElectronContext'
|
||||
const { Sider } = Layout
|
||||
|
||||
const DashboardSidebar = ({
|
||||
@ -24,6 +24,8 @@ const DashboardSidebar = ({
|
||||
const isMobile = useMediaQuery({ maxWidth: 768 })
|
||||
const navigate = useNavigate()
|
||||
|
||||
const { isElectron } = useContext(ElectronContext)
|
||||
|
||||
useEffect(() => {
|
||||
if (typeof collapsedProp === 'boolean') {
|
||||
setCollapsed(collapsedProp)
|
||||
@ -56,6 +58,7 @@ const DashboardSidebar = ({
|
||||
selectedKeys={[selectedKey]}
|
||||
items={_items}
|
||||
_internalDisableMenuItemTitleTooltip
|
||||
style={{ lineHeight: '40px' }}
|
||||
overflowedIndicator={<Button type='text' icon={<CaretDownFilled />} />}
|
||||
/>
|
||||
)
|
||||
@ -72,6 +75,7 @@ const DashboardSidebar = ({
|
||||
mode='inline'
|
||||
selectedKeys={[selectedKey]}
|
||||
items={_items}
|
||||
className={isElectron ? 'electron-sidebar' : null}
|
||||
style={{ flexGrow: 1, border: 'none' }}
|
||||
_internalDisableMenuItemTitleTooltip
|
||||
/>
|
||||
|
||||
50
src/components/Dashboard/common/DashboardWindowButtons.jsx
Normal file
50
src/components/Dashboard/common/DashboardWindowButtons.jsx
Normal file
@ -0,0 +1,50 @@
|
||||
import React, { useContext } from 'react'
|
||||
import { Flex, Button } from 'antd'
|
||||
import { ElectronContext } from '../context/ElectronContext'
|
||||
import XMarkIcon from '../../Icons/XMarkIcon'
|
||||
import MinusIcon from '../../Icons/MinusIcon'
|
||||
import ContractIcon from '../../Icons/ContractIcon'
|
||||
import ExpandIcon from '../../Icons/ExpandIcon'
|
||||
|
||||
const DashboardWindowButtons = () => {
|
||||
const { isMaximized, handleWindowControl, platform } =
|
||||
useContext(ElectronContext)
|
||||
|
||||
const closeButton = (
|
||||
<Button
|
||||
icon={<XMarkIcon />}
|
||||
type={'text'}
|
||||
onClick={() => handleWindowControl('close')}
|
||||
/>
|
||||
)
|
||||
const maximizeButton = (
|
||||
<Button
|
||||
icon={<MinusIcon />}
|
||||
type={'text'}
|
||||
onClick={() => handleWindowControl('minimize')}
|
||||
/>
|
||||
)
|
||||
const minimizeButton = (
|
||||
<Button
|
||||
icon={isMaximized ? <ContractIcon /> : <ExpandIcon />}
|
||||
type={'text'}
|
||||
onClick={() => handleWindowControl('maximize')}
|
||||
/>
|
||||
)
|
||||
|
||||
return (
|
||||
<Flex align='center'>
|
||||
{platform == 'darwin' ? (
|
||||
<>
|
||||
{closeButton}
|
||||
{minimizeButton}
|
||||
{maximizeButton}
|
||||
</>
|
||||
) : (
|
||||
<>{closeButton}</>
|
||||
)}
|
||||
</Flex>
|
||||
)
|
||||
}
|
||||
|
||||
export default DashboardWindowButtons
|
||||
@ -6,14 +6,7 @@ import BinIcon from '../../Icons/BinIcon'
|
||||
|
||||
const { Text } = Typography
|
||||
|
||||
const DeleteObjectModal = ({
|
||||
open,
|
||||
onOk,
|
||||
onCancel,
|
||||
loading,
|
||||
objectType,
|
||||
objectName
|
||||
}) => {
|
||||
const DeleteObjectModal = ({ open, onOk, onCancel, loading, objectType }) => {
|
||||
const model = getModelByName(objectType)
|
||||
return (
|
||||
<Modal
|
||||
@ -49,8 +42,7 @@ const DeleteObjectModal = ({
|
||||
]}
|
||||
>
|
||||
<Text>
|
||||
Are you sure you want to delete this {model.label.toLowerCase()}
|
||||
{objectName ? ` "${objectName}"` : ''}?
|
||||
Are you sure you want to delete this {model.label.toLowerCase()}?
|
||||
</Text>
|
||||
</Modal>
|
||||
)
|
||||
@ -61,8 +53,7 @@ DeleteObjectModal.propTypes = {
|
||||
onOk: PropTypes.func.isRequired,
|
||||
onCancel: PropTypes.func.isRequired,
|
||||
loading: PropTypes.bool,
|
||||
objectType: PropTypes.string.isRequired,
|
||||
objectName: PropTypes.string
|
||||
objectType: PropTypes.string.isRequired
|
||||
}
|
||||
|
||||
export default DeleteObjectModal
|
||||
|
||||
@ -4,6 +4,7 @@ import { ApiServerContext } from '../context/ApiServerContext'
|
||||
import { AuthContext } from '../context/AuthContext'
|
||||
import PropTypes from 'prop-types'
|
||||
import DeleteObjectModal from './DeleteObjectModal'
|
||||
import merge from 'lodash/merge'
|
||||
|
||||
/**
|
||||
* EditObjectForm is a reusable form component for editing any object type.
|
||||
@ -82,7 +83,7 @@ const EditObjectForm = ({ id, type, style, children }) => {
|
||||
|
||||
// Update event handler
|
||||
const updateObjectEventHandler = useCallback((value) => {
|
||||
setObjectData((prev) => ({ ...prev, ...value }))
|
||||
setObjectData((prev) => merge({}, prev, value))
|
||||
}, [])
|
||||
|
||||
// Update event handler
|
||||
|
||||
@ -13,7 +13,7 @@ const propertyOrder = [
|
||||
const GCodeFileSelect = ({ onChange, filter, useFilter = false, style }) => {
|
||||
return (
|
||||
<ObjectSelect
|
||||
endpoint={`${config.backendUrl}/gcodefiles`}
|
||||
endpoint={`${config.backendUrl}/gcodefiles/properties`}
|
||||
propertyOrder={propertyOrder}
|
||||
filter={filter}
|
||||
useFilter={useFilter}
|
||||
@ -21,7 +21,7 @@ const GCodeFileSelect = ({ onChange, filter, useFilter = false, style }) => {
|
||||
showSearch={true}
|
||||
style={style}
|
||||
placeholder='Select GCode File'
|
||||
type='gcodefile'
|
||||
type='gcodeFile'
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
@ -1,7 +1,7 @@
|
||||
import React from 'react'
|
||||
import { Flex, Tag } from 'antd'
|
||||
import IdDisplay from '../../common/IdDisplay'
|
||||
import LockIcon from '../../../Icons/LockIcon'
|
||||
import IdDisplay from './IdDisplay'
|
||||
import LockIcon from '../../Icons/LockIcon'
|
||||
import PropTypes from 'prop-types'
|
||||
|
||||
const LockIndicator = ({ lock }) => {
|
||||
@ -95,7 +95,7 @@ const NoteItem = ({
|
||||
}
|
||||
|
||||
// Reload child notes when a new child note is added
|
||||
const reloadChildNotes = async () => {
|
||||
const reloadChildNotes = useCallback(async () => {
|
||||
// Always fetch child notes when this function is called
|
||||
// This ensures child notes are loaded even if the parent wasn't expanded before
|
||||
setLoadingChildNotes(note._id)
|
||||
@ -110,14 +110,14 @@ const NoteItem = ({
|
||||
} finally {
|
||||
setLoadingChildNotes(null)
|
||||
}
|
||||
}
|
||||
}, [fetchData, note._id])
|
||||
|
||||
// Listen for child note additions
|
||||
useEffect(() => {
|
||||
if (onChildNoteAdded) {
|
||||
onChildNoteAdded(note._id, reloadChildNotes)
|
||||
}
|
||||
}, [note._id, onChildNoteAdded])
|
||||
}, [note._id, onChildNoteAdded, reloadChildNotes])
|
||||
|
||||
// Check if the current user can delete this note
|
||||
const canDeleteNote = userProfile && userProfile._id === note.user._id
|
||||
|
||||
27
src/components/Dashboard/common/ObjectDisplay.jsx
Normal file
27
src/components/Dashboard/common/ObjectDisplay.jsx
Normal file
@ -0,0 +1,27 @@
|
||||
import React from 'react'
|
||||
import PropTypes from 'prop-types'
|
||||
import { Tag, Typography } from 'antd'
|
||||
import { getModelByName } from '../../../database/ObjectModels'
|
||||
|
||||
const { Text } = Typography
|
||||
|
||||
const ObjectDisplay = ({ object, objectType }) => {
|
||||
if (!object) {
|
||||
return <Text type='secondary'>n/a</Text>
|
||||
}
|
||||
const model = getModelByName(objectType)
|
||||
const Icon = model.icon
|
||||
return (
|
||||
<Tag color='default' style={{ margin: 0 }} icon={<Icon />}>
|
||||
{object?.name ? object.name : null}
|
||||
</Tag>
|
||||
)
|
||||
}
|
||||
|
||||
ObjectDisplay.propTypes = {
|
||||
object: PropTypes.object,
|
||||
objectType: PropTypes.string,
|
||||
style: PropTypes.object
|
||||
}
|
||||
|
||||
export default ObjectDisplay
|
||||
@ -1,49 +1,28 @@
|
||||
import React from 'react'
|
||||
import PropTypes from 'prop-types'
|
||||
import { List, Typography, Flex } from 'antd'
|
||||
import { getModelByName } from '../../../database/ObjectModels'
|
||||
import IdDisplay from './IdDisplay'
|
||||
import ObjectDisplay from './ObjectDisplay'
|
||||
import { Space, Typography } from 'antd'
|
||||
|
||||
const { Text } = Typography
|
||||
|
||||
const ObjectList = ({ value, objectType, bordered = true }) => {
|
||||
const ObjectList = ({ value, objectType, style }) => {
|
||||
if (!value || !Array.isArray(value) || value.length === 0) {
|
||||
return <Text type='secondary'>n/a</Text>
|
||||
}
|
||||
|
||||
return (
|
||||
<List
|
||||
size='small'
|
||||
bordered={bordered}
|
||||
dataSource={value}
|
||||
renderItem={(item) => {
|
||||
const model = getModelByName(objectType)
|
||||
const Icon = model.icon
|
||||
return (
|
||||
<List.Item>
|
||||
<Flex gap={'small'} align='center'>
|
||||
<Icon />
|
||||
{item?.name ? <Text ellipsis>{item.name}</Text> : null}
|
||||
{item?._id ? (
|
||||
<IdDisplay
|
||||
id={item?._id}
|
||||
longId={false}
|
||||
type={objectType}
|
||||
showHyperlink={true}
|
||||
/>
|
||||
) : null}
|
||||
</Flex>
|
||||
</List.Item>
|
||||
)
|
||||
}}
|
||||
style={{ width: '100%' }}
|
||||
/>
|
||||
<Space size={'small'} wrap style={style}>
|
||||
{value.map((item) => (
|
||||
<ObjectDisplay object={item} objectType={objectType} key={item._id} />
|
||||
))}
|
||||
</Space>
|
||||
)
|
||||
}
|
||||
|
||||
ObjectList.propTypes = {
|
||||
value: PropTypes.array,
|
||||
bordered: PropTypes.bool,
|
||||
objectType: PropTypes.string
|
||||
objectType: PropTypes.string,
|
||||
style: PropTypes.object
|
||||
}
|
||||
|
||||
export default ObjectList
|
||||
|
||||
@ -10,14 +10,9 @@ import {
|
||||
DatePicker,
|
||||
Switch
|
||||
} from 'antd'
|
||||
import VendorSelect from './VendorSelect'
|
||||
import FilamentSelect from './FilamentSelect'
|
||||
import IdDisplay from './IdDisplay'
|
||||
import TimeDisplay from './TimeDisplay'
|
||||
import dayjs from 'dayjs'
|
||||
import PrinterSelect from './PrinterSelect'
|
||||
import GCodeFileSelect from './GCodeFileSelect'
|
||||
import PartSelect from './PartSelect'
|
||||
import EmailDisplay from './EmailDisplay'
|
||||
import UrlDisplay from './UrlDisplay'
|
||||
import CountryDisplay from './CountryDisplay'
|
||||
@ -41,6 +36,8 @@ import ObjectList from './ObjectList'
|
||||
import VarianceDisplay from './VarianceDisplay'
|
||||
import OperationDisplay from './OperationDisplay'
|
||||
import MarkdownDisplay from './MarkdownDisplay'
|
||||
import ObjectSelect from './ObjectSelect'
|
||||
import ObjectDisplay from './ObjectDisplay'
|
||||
|
||||
const { Text } = Typography
|
||||
|
||||
@ -291,7 +288,7 @@ const ObjectProperty = ({
|
||||
}
|
||||
case 'object': {
|
||||
if (value && value.name) {
|
||||
return <Text ellipsis>{value.name}</Text>
|
||||
return <ObjectDisplay object={value} objectType={objectType} />
|
||||
} else {
|
||||
return (
|
||||
<Text type='secondary' {...textParams}>
|
||||
@ -424,7 +421,7 @@ const ObjectProperty = ({
|
||||
}
|
||||
const hasRequiredRule = rules.some((rule) => rule && rule.required)
|
||||
if (!hasRequiredRule) {
|
||||
rules.push({ required: true, message: 'This field is required' })
|
||||
rules.push({ required: true, message: '' })
|
||||
}
|
||||
mergedFormItemProps.rules = rules
|
||||
}
|
||||
@ -591,45 +588,17 @@ const ObjectProperty = ({
|
||||
)
|
||||
}
|
||||
case 'object':
|
||||
switch (objectType) {
|
||||
case 'vendor':
|
||||
return (
|
||||
<Form.Item name={formItemName} {...mergedFormItemProps}>
|
||||
<VendorSelect placeholder={label} />
|
||||
<ObjectSelect type={objectType} />
|
||||
</Form.Item>
|
||||
)
|
||||
case 'printer':
|
||||
case 'objectList':
|
||||
return (
|
||||
<Form.Item name={formItemName} {...mergedFormItemProps}>
|
||||
<PrinterSelect placeholder={label} />
|
||||
<ObjectSelect type={objectType} multiple />
|
||||
</Form.Item>
|
||||
)
|
||||
case 'gcodeFile':
|
||||
return (
|
||||
<Form.Item name={formItemName} {...mergedFormItemProps}>
|
||||
<GCodeFileSelect placeholder={label} />
|
||||
</Form.Item>
|
||||
)
|
||||
case 'filament':
|
||||
return (
|
||||
<Form.Item name={formItemName} {...mergedFormItemProps}>
|
||||
<FilamentSelect placeholder={label} />
|
||||
</Form.Item>
|
||||
)
|
||||
case 'part':
|
||||
return (
|
||||
<Form.Item name={formItemName} {...mergedFormItemProps}>
|
||||
<PartSelect placeholder={label} />
|
||||
</Form.Item>
|
||||
)
|
||||
default:
|
||||
return (
|
||||
<Text type='secondary' {...textParams}>
|
||||
n/a
|
||||
</Text>
|
||||
)
|
||||
}
|
||||
|
||||
case 'tags':
|
||||
return (
|
||||
<Form.Item name={formItemName} {...mergedFormItemProps}>
|
||||
|
||||
@ -1,340 +1,215 @@
|
||||
import React, { useEffect, useState, useCallback } from 'react'
|
||||
import React, {
|
||||
useEffect,
|
||||
useState,
|
||||
useContext,
|
||||
useCallback,
|
||||
useMemo
|
||||
} from 'react'
|
||||
import PropTypes from 'prop-types'
|
||||
import { TreeSelect, Typography, Flex, Badge, Space, Button, Input } from 'antd'
|
||||
import axios from 'axios'
|
||||
import { getModelByName } from '../../../database/ObjectModels'
|
||||
import IdDisplay from './IdDisplay'
|
||||
import { TreeSelect, Space, Button, Input } from 'antd'
|
||||
import ReloadIcon from '../../Icons/ReloadIcon'
|
||||
import { ApiServerContext } from '../context/ApiServerContext'
|
||||
import ObjectProperty from './ObjectProperty'
|
||||
const { Text } = Typography
|
||||
import { getModelByName } from '../../../database/ObjectModels'
|
||||
import merge from 'lodash/merge'
|
||||
const { SHOW_CHILD } = TreeSelect
|
||||
|
||||
// --- Utility: Resolve nested property path (e.g., 'filament.diameter') ---
|
||||
function resolvePropertyPath(obj, path) {
|
||||
if (!obj || !path) return { value: undefined, finalProperty: undefined }
|
||||
const props = path.split('.')
|
||||
let value = obj
|
||||
for (const prop of props) {
|
||||
if (value && typeof value === 'object') {
|
||||
value = value[prop]
|
||||
} else {
|
||||
return { value: undefined, finalProperty: prop }
|
||||
}
|
||||
}
|
||||
return { value, finalProperty: props[props.length - 1] }
|
||||
}
|
||||
|
||||
// --- Utility: Build filter object for a node based on propertyOrder ---
|
||||
function buildFilterForNode(node, treeData, propertyOrder) {
|
||||
let filterObj = {}
|
||||
let currentId = node.id
|
||||
while (currentId !== 0) {
|
||||
const currentNode = treeData.find((d) => d.id === currentId)
|
||||
if (!currentNode) break
|
||||
filterObj[propertyOrder[currentNode.propertyId]] = currentNode.value
|
||||
currentId = currentNode.pId
|
||||
}
|
||||
return filterObj
|
||||
}
|
||||
|
||||
/**
|
||||
* ObjectSelect - a generic, reusable async TreeSelect for hierarchical object selection.
|
||||
*
|
||||
* Props:
|
||||
* - endpoint: API endpoint to fetch data from (required)
|
||||
* - propertyOrder: array of property names for category levels (required)
|
||||
* - filter: object for filtering (optional)
|
||||
* - useFilter: bool (optional)
|
||||
* - value: selected value (optional) - can be an object with _id, array of objects, or simple value/array
|
||||
* - onChange: function (optional)
|
||||
* - showSearch: bool (optional, default false)
|
||||
* - treeCheckable: bool (optional, default false) - enables multi-select mode with checkboxes
|
||||
* - treeSelectProps: any other TreeSelect props (optional)
|
||||
*/
|
||||
const ObjectSelect = ({
|
||||
endpoint,
|
||||
propertyOrder,
|
||||
filter = {},
|
||||
useFilter = false,
|
||||
value,
|
||||
onChange,
|
||||
showSearch = false,
|
||||
treeCheckable = false,
|
||||
treeSelectProps = {},
|
||||
type = 'unknown',
|
||||
showSearch = false,
|
||||
multiple = false,
|
||||
treeSelectProps = {},
|
||||
filter = {},
|
||||
value,
|
||||
...rest
|
||||
}) => {
|
||||
const { fetchObjectsByProperty } = useContext(ApiServerContext)
|
||||
// --- State ---
|
||||
const [treeData, setTreeData] = useState([])
|
||||
const [loading, setLoading] = useState(false)
|
||||
const [defaultValue, setDefaultValue] = useState(treeCheckable ? [] : value)
|
||||
const [searchValue, setSearchValue] = useState('')
|
||||
const [objectPropertiesTree, setObjectPropertiesTree] = useState({})
|
||||
const [initialized, setInitialized] = useState(false)
|
||||
const [error, setError] = useState(false)
|
||||
const properties = useMemo(() => getModelByName(type).group || [], [type])
|
||||
const [objectList, setObjectList] = useState([])
|
||||
const [treeSelectValue, setTreeSelectValue] = useState(null)
|
||||
const [initialLoading, setInitialLoading] = useState(true)
|
||||
|
||||
// --- API: Fetch data for a property level or leaf ---
|
||||
const fetchData = useCallback(
|
||||
async (property, filter, search) => {
|
||||
setLoading(true)
|
||||
setError(false)
|
||||
// Fetch the object properties tree from the API
|
||||
const handleFetchObjectsProperties = useCallback(
|
||||
async (customFilter = filter) => {
|
||||
try {
|
||||
const params = { ...filter, property }
|
||||
if (search) params.search = search
|
||||
const response = await axios.get(endpoint, {
|
||||
params,
|
||||
withCredentials: true
|
||||
const data = await fetchObjectsByProperty(type, {
|
||||
properties: properties,
|
||||
filter: customFilter
|
||||
})
|
||||
setLoading(false)
|
||||
return response.data
|
||||
} catch (err) {
|
||||
setLoading(false)
|
||||
setError(true)
|
||||
return []
|
||||
}
|
||||
},
|
||||
[endpoint]
|
||||
)
|
||||
|
||||
// --- API: Fetch a single object by ID ---
|
||||
const fetchObjectById = useCallback(
|
||||
async (objectId) => {
|
||||
setLoading(true)
|
||||
setObjectPropertiesTree((prev) => merge({}, prev, data))
|
||||
setInitialLoading(false)
|
||||
setError(false)
|
||||
try {
|
||||
const response = await axios.get(`${endpoint}/${objectId}`, {
|
||||
withCredentials: true
|
||||
})
|
||||
setLoading(false)
|
||||
return response.data
|
||||
return data
|
||||
} catch (err) {
|
||||
setLoading(false)
|
||||
setError(true)
|
||||
return null
|
||||
}
|
||||
},
|
||||
[endpoint]
|
||||
[type, fetchObjectsByProperty, properties, filter]
|
||||
)
|
||||
|
||||
// --- Render node title ---
|
||||
const renderTitle = useCallback(
|
||||
(item) => {
|
||||
if (item.propertyType) {
|
||||
return (
|
||||
// Convert the API response to AntD TreeSelect treeData
|
||||
const buildTreeData = useCallback(
|
||||
(data, pIdx = 0, parentKeys = [], filterPath = []) => {
|
||||
if (!data) return []
|
||||
if (Array.isArray(data)) {
|
||||
return data.map((object) => {
|
||||
setObjectList((prev) => {
|
||||
const filtered = prev.filter(
|
||||
(prevObject) => prevObject._id != object._id
|
||||
)
|
||||
return [...filtered, object]
|
||||
})
|
||||
return {
|
||||
title: (
|
||||
<ObjectProperty
|
||||
type={item.propertyType}
|
||||
value={item.value}
|
||||
key={object._id}
|
||||
type='object'
|
||||
value={object}
|
||||
objectType={type}
|
||||
objectData={object}
|
||||
isEditing={false}
|
||||
/>
|
||||
)
|
||||
} else {
|
||||
const model = getModelByName(type)
|
||||
const Icon = model.icon
|
||||
return (
|
||||
<Flex gap={'small'} align='center' style={{ width: '100%' }}>
|
||||
{Icon && <Icon />}
|
||||
{item?.color && <Badge color={item.color}></Badge>}
|
||||
<Text ellipsis>{item.name || type.title}</Text>
|
||||
<IdDisplay id={item._id} longId={false} type={type} />
|
||||
</Flex>
|
||||
)
|
||||
}
|
||||
},
|
||||
[type]
|
||||
)
|
||||
|
||||
// --- Build tree nodes for a property level ---
|
||||
const buildCategoryNodes = useCallback(
|
||||
(data, propertyName, propertyId, parentId) => {
|
||||
return data.map((item) => {
|
||||
let resolved = resolvePropertyPath(item, propertyName)
|
||||
let value = resolved.value
|
||||
let propertyType = resolved.finalProperty
|
||||
return {
|
||||
id: value,
|
||||
pId: parentId,
|
||||
value: value,
|
||||
key: value,
|
||||
propertyId: propertyId,
|
||||
title: renderTitle({ ...item, value, propertyType }),
|
||||
isLeaf: false,
|
||||
selectable: false,
|
||||
raw: item
|
||||
}
|
||||
})
|
||||
},
|
||||
[renderTitle]
|
||||
)
|
||||
|
||||
// --- Build tree nodes for leaf level ---
|
||||
const buildLeafNodes = useCallback(
|
||||
(data, parentId) => {
|
||||
return data.map((item) => {
|
||||
const value = item._id || item.id || item.value
|
||||
return {
|
||||
id: value,
|
||||
pId: parentId,
|
||||
value: value,
|
||||
key: value,
|
||||
title: renderTitle(item),
|
||||
),
|
||||
value: object._id,
|
||||
key: object._id,
|
||||
isLeaf: true,
|
||||
raw: item
|
||||
}
|
||||
})
|
||||
},
|
||||
[renderTitle]
|
||||
)
|
||||
|
||||
// --- Tree loader: load children for a node or root ---
|
||||
const handleTreeLoad = useCallback(
|
||||
async (node) => {
|
||||
if (!propertyOrder.length) return
|
||||
if (node) {
|
||||
// Not at leaf level yet
|
||||
if (node.propertyId !== propertyOrder.length - 1) {
|
||||
const nextPropertyId = node.propertyId + 1
|
||||
const propertyName = propertyOrder[nextPropertyId]
|
||||
const filterObj = buildFilterForNode(node, treeData, propertyOrder)
|
||||
const data = await fetchData(propertyName, filterObj, searchValue)
|
||||
setTreeData((prev) => [
|
||||
...prev,
|
||||
...buildCategoryNodes(data, propertyName, nextPropertyId, node.id)
|
||||
])
|
||||
} else {
|
||||
// At leaf level
|
||||
const filterObj = buildFilterForNode(node, treeData, propertyOrder)
|
||||
const data = await fetchData(null, filterObj, searchValue)
|
||||
setTreeData((prev) => [...prev, ...buildLeafNodes(data, node.id)])
|
||||
}
|
||||
} else {
|
||||
// Root load
|
||||
const propertyName = propertyOrder[0]
|
||||
const data = await fetchData(propertyName, {}, searchValue)
|
||||
setTreeData(buildCategoryNodes(data, propertyName, 0, 0))
|
||||
}
|
||||
},
|
||||
[
|
||||
propertyOrder,
|
||||
treeData,
|
||||
fetchData,
|
||||
buildCategoryNodes,
|
||||
buildLeafNodes,
|
||||
searchValue
|
||||
]
|
||||
)
|
||||
|
||||
// --- OnChange handler ---
|
||||
const handleOnChange = (val, selectedOptions) => {
|
||||
if (onChange) {
|
||||
if (treeCheckable) {
|
||||
// Multi-select
|
||||
let selectedObjects = []
|
||||
if (Array.isArray(val)) {
|
||||
selectedObjects = val.map((selectedValue) => {
|
||||
const node = treeData.find((n) => n.value === selectedValue)
|
||||
return node ? node.raw : selectedValue
|
||||
})
|
||||
}
|
||||
onChange(selectedObjects, selectedOptions)
|
||||
} else {
|
||||
// Single select
|
||||
const node = treeData.find((n) => n.value === val)
|
||||
onChange(node ? node.raw : val, selectedOptions)
|
||||
}
|
||||
}
|
||||
setDefaultValue(val)
|
||||
}
|
||||
|
||||
// --- Search handler ---
|
||||
const handleSearch = (val) => {
|
||||
setSearchValue(val)
|
||||
setTreeData([])
|
||||
}
|
||||
|
||||
// --- Sync defaultValue and load tree path for object values ---
|
||||
useEffect(() => {
|
||||
if (treeCheckable) {
|
||||
if (Array.isArray(value)) {
|
||||
const valueIds = value.map((v) => v._id || v.id || v)
|
||||
setDefaultValue(valueIds)
|
||||
value.forEach((item) => {
|
||||
if (item && typeof item === 'object' && item._id) {
|
||||
const existingNode = treeData.find(
|
||||
(node) => node.value === item._id
|
||||
)
|
||||
if (!existingNode) {
|
||||
fetchObjectById(item._id).then((object) => {
|
||||
if (object) {
|
||||
// For multi-select, just add the leaf node
|
||||
setTreeData((prev) => [
|
||||
...prev,
|
||||
...buildLeafNodes(
|
||||
[object],
|
||||
object[propertyOrder[propertyOrder.length - 2]] || 0
|
||||
)
|
||||
])
|
||||
property: properties[pIdx - 1], // previous property
|
||||
parentKeys,
|
||||
filterPath
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
})
|
||||
} else {
|
||||
setDefaultValue([])
|
||||
}
|
||||
} else {
|
||||
if (value?._id) {
|
||||
setDefaultValue(value._id)
|
||||
const existingNode = treeData.find((node) => node.value === value._id)
|
||||
if (!existingNode) {
|
||||
fetchObjectById(value._id).then((object) => {
|
||||
if (object) {
|
||||
setTreeData((prev) => [
|
||||
...prev,
|
||||
...buildLeafNodes(
|
||||
[object],
|
||||
object[propertyOrder[propertyOrder.length - 2]] || 0
|
||||
)
|
||||
])
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
}, [
|
||||
if (typeof data == 'object') {
|
||||
const property = properties[pIdx] || null
|
||||
return Object.entries(data)
|
||||
.map(([key, value]) => {
|
||||
if (property != null && typeof value === 'object') {
|
||||
const newFilterPath = filterPath.concat({ property, value: key })
|
||||
return {
|
||||
title: <ObjectProperty type={property} value={key} />,
|
||||
value: key,
|
||||
key: parentKeys.concat(key).join(':'),
|
||||
property,
|
||||
parentKeys: parentKeys.concat(key),
|
||||
filterPath: newFilterPath,
|
||||
selectable: false,
|
||||
children: buildTreeData(
|
||||
value,
|
||||
treeData,
|
||||
fetchObjectById,
|
||||
buildLeafNodes,
|
||||
propertyOrder,
|
||||
treeCheckable
|
||||
])
|
||||
pIdx + 1,
|
||||
parentKeys.concat(key),
|
||||
newFilterPath
|
||||
),
|
||||
isLeaf: false
|
||||
}
|
||||
}
|
||||
})
|
||||
.filter(Boolean)
|
||||
}
|
||||
},
|
||||
[properties, type]
|
||||
)
|
||||
|
||||
// --- Initial load ---
|
||||
// --- loadData for async loading on expand ---
|
||||
const loadData = async (node) => {
|
||||
// node.property is the property name, node.value is the value
|
||||
if (!node.property) return
|
||||
// Build filter for this node by merging all parent property-value pairs
|
||||
const customFilter = { ...filter }
|
||||
if (Array.isArray(node.filterPath)) {
|
||||
node.filterPath.forEach(({ property, value }) => {
|
||||
customFilter[property] = value
|
||||
})
|
||||
}
|
||||
customFilter[node.property] = node.value
|
||||
// Fetch children for this node
|
||||
const data = await handleFetchObjectsProperties(customFilter)
|
||||
if (!data) return
|
||||
// Build new children
|
||||
const children = buildTreeData(
|
||||
data,
|
||||
properties.indexOf(node.property) + 1,
|
||||
node.parentKeys || [],
|
||||
(node.filterPath || []).concat({
|
||||
property: node.property,
|
||||
value: node.value
|
||||
})
|
||||
)
|
||||
// Update treeData with new children for this node
|
||||
setTreeData((prevTreeData) => {
|
||||
// Helper to recursively update the correct node
|
||||
const updateNode = (nodes) =>
|
||||
nodes.map((n) => {
|
||||
if (n.key === node.key) {
|
||||
return { ...n, children, isLeaf: children.length === 0 }
|
||||
} else if (n.children) {
|
||||
return { ...n, children: updateNode(n.children) }
|
||||
}
|
||||
return n
|
||||
})
|
||||
return updateNode(prevTreeData)
|
||||
})
|
||||
}
|
||||
|
||||
const onTreeSelectChange = (value) => {
|
||||
// value can be a string (single) or array (multiple)
|
||||
if (multiple) {
|
||||
// Multiple selection
|
||||
let selectedObjects = []
|
||||
if (Array.isArray(value)) {
|
||||
selectedObjects = value
|
||||
.map((id) => objectList.find((obj) => obj._id === id))
|
||||
.filter(Boolean)
|
||||
}
|
||||
setTreeSelectValue(value)
|
||||
if (rest.onChange) rest.onChange(selectedObjects)
|
||||
} else {
|
||||
// Single selection
|
||||
const selectedObject = objectList.find((obj) => obj._id === value)
|
||||
setTreeSelectValue(value)
|
||||
if (rest.onChange) rest.onChange(selectedObject)
|
||||
}
|
||||
}
|
||||
|
||||
// Update treeData when objectPropertiesTree changes
|
||||
useEffect(() => {
|
||||
if (treeData.length === 0 && !error && !loading) {
|
||||
if (!treeCheckable && value && typeof value === 'object' && value._id) {
|
||||
if (objectPropertiesTree && Object.keys(objectPropertiesTree).length > 0) {
|
||||
const newTreeData = buildTreeData(objectPropertiesTree)
|
||||
setTreeData((prev) => {
|
||||
if (JSON.stringify(prev) !== JSON.stringify(newTreeData)) {
|
||||
return newTreeData
|
||||
}
|
||||
return prev
|
||||
})
|
||||
}
|
||||
}, [objectPropertiesTree, properties, type, buildTreeData])
|
||||
|
||||
useEffect(() => {
|
||||
if (value && typeof value === 'object' && value !== null && !initialized) {
|
||||
// Build a new filter from value's properties that are in the properties list
|
||||
const valueFilter = { ...filter }
|
||||
properties.forEach((prop) => {
|
||||
if (Object.prototype.hasOwnProperty.call(value, prop)) {
|
||||
valueFilter[prop] = value[prop]
|
||||
}
|
||||
})
|
||||
// Fetch with the new filter
|
||||
handleFetchObjectsProperties(valueFilter)
|
||||
setTreeSelectValue(value._id)
|
||||
setInitialized(true)
|
||||
return
|
||||
}
|
||||
if (useFilter || searchValue) {
|
||||
// Flat filter mode
|
||||
fetchData(null, filter, searchValue).then((data) => {
|
||||
setTreeData(buildLeafNodes(data, 0))
|
||||
})
|
||||
} else {
|
||||
handleTreeLoad(null)
|
||||
if (!initialized) {
|
||||
handleFetchObjectsProperties()
|
||||
setInitialized(true)
|
||||
}
|
||||
}
|
||||
}, [
|
||||
treeData,
|
||||
useFilter,
|
||||
filter,
|
||||
searchValue,
|
||||
buildLeafNodes,
|
||||
fetchData,
|
||||
handleTreeLoad,
|
||||
error,
|
||||
loading,
|
||||
value,
|
||||
treeCheckable
|
||||
])
|
||||
}, [value, filter, properties, handleFetchObjectsProperties, initialized])
|
||||
|
||||
// --- Error UI ---
|
||||
if (error) {
|
||||
@ -346,6 +221,7 @@ const ObjectSelect = ({
|
||||
onClick={() => {
|
||||
setError(false)
|
||||
setTreeData([])
|
||||
setInitialized(false)
|
||||
}}
|
||||
danger
|
||||
/>
|
||||
@ -353,35 +229,37 @@ const ObjectSelect = ({
|
||||
)
|
||||
}
|
||||
|
||||
if (initialLoading) {
|
||||
return <TreeSelect disabled loading placeholder='Loading...' />
|
||||
}
|
||||
|
||||
// --- Main TreeSelect UI ---
|
||||
return (
|
||||
<TreeSelect
|
||||
treeDataSimpleMode
|
||||
treeDataSimpleMode={false}
|
||||
treeDefaultExpandAll={true}
|
||||
loadData={handleTreeLoad}
|
||||
treeData={treeData}
|
||||
onChange={handleOnChange}
|
||||
loading={loading}
|
||||
value={loading ? 'Loading...' : defaultValue}
|
||||
showSearch={showSearch}
|
||||
onSearch={showSearch ? handleSearch : undefined}
|
||||
treeCheckable={treeCheckable}
|
||||
showCheckedStrategy={treeCheckable ? SHOW_CHILD : undefined}
|
||||
multiple={multiple}
|
||||
loadData={loadData}
|
||||
showCheckedStrategy={SHOW_CHILD}
|
||||
placeholder={`Select a ${getModelByName(type).label.toLowerCase()}...`}
|
||||
{...treeSelectProps}
|
||||
{...rest}
|
||||
value={treeSelectValue}
|
||||
onChange={onTreeSelectChange}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
ObjectSelect.propTypes = {
|
||||
endpoint: PropTypes.string.isRequired,
|
||||
propertyOrder: PropTypes.arrayOf(PropTypes.string).isRequired,
|
||||
properties: PropTypes.arrayOf(PropTypes.string).isRequired,
|
||||
filter: PropTypes.object,
|
||||
useFilter: PropTypes.bool,
|
||||
value: PropTypes.any,
|
||||
onChange: PropTypes.func,
|
||||
showSearch: PropTypes.bool,
|
||||
treeCheckable: PropTypes.bool,
|
||||
multiple: PropTypes.bool,
|
||||
treeSelectProps: PropTypes.object,
|
||||
type: PropTypes.string.isRequired
|
||||
}
|
||||
|
||||
@ -38,6 +38,7 @@ import CheckIcon from '../../Icons/CheckIcon'
|
||||
import { useNavigate } from 'react-router-dom'
|
||||
import QuestionCircleIcon from '../../Icons/QuestionCircleIcon'
|
||||
import { AuthContext } from '../context/AuthContext'
|
||||
import merge from 'lodash/merge'
|
||||
const logger = loglevel.getLogger('DasboardTable')
|
||||
logger.setLevel(config.logLevel)
|
||||
|
||||
@ -65,7 +66,7 @@ const ObjectTable = forwardRef(
|
||||
adjustedScrollHeight = 'calc(var(--unit-100vh) - 316px)'
|
||||
}
|
||||
if (cards) {
|
||||
adjustedScrollHeight = 'calc(var(--unit-100vh) - 210px)'
|
||||
adjustedScrollHeight = 'calc(var(--unit-100vh) - 280px)'
|
||||
}
|
||||
const [, contextHolder] = message.useMessage()
|
||||
const tableRef = useRef(null)
|
||||
@ -108,7 +109,6 @@ const ObjectTable = forwardRef(
|
||||
type={'text'}
|
||||
size={'small'}
|
||||
onClick={() => {
|
||||
console.log(objectData)
|
||||
if (action.url) {
|
||||
navigate(action.url(objectData._id))
|
||||
}
|
||||
@ -135,13 +135,6 @@ const ObjectTable = forwardRef(
|
||||
order: sorter.order
|
||||
})
|
||||
}
|
||||
console.log('Fetching Objects', {
|
||||
page: pageNum,
|
||||
limit: pageSize,
|
||||
filter: { ...filter, ...masterFilter },
|
||||
sorter,
|
||||
onDataChange
|
||||
})
|
||||
try {
|
||||
const result = await fetchObjects(type, {
|
||||
page: pageNum,
|
||||
@ -250,12 +243,6 @@ const ObjectTable = forwardRef(
|
||||
const lowestPage = Math.min(...pages.map((p) => p.pageNum))
|
||||
const prevPage = lowestPage - 1
|
||||
|
||||
logger.debug(
|
||||
'Down',
|
||||
scrollHeight - scrollTop - clientHeight < 100,
|
||||
lazyLoading
|
||||
)
|
||||
|
||||
// Load more data when scrolling down
|
||||
if (scrollHeight - scrollTop - clientHeight < 100 && hasMore) {
|
||||
setTimeout(() => {
|
||||
@ -276,13 +263,14 @@ const ObjectTable = forwardRef(
|
||||
loadPreviousPage()
|
||||
}
|
||||
},
|
||||
[lazyLoading, loadNextPage, loadPreviousPage]
|
||||
[lazyLoading, loadNextPage, loadPreviousPage, hasMore, pages]
|
||||
)
|
||||
|
||||
const reload = useCallback(() => {
|
||||
const reload = useCallback(async () => {
|
||||
setLazyLoading(true)
|
||||
for (let i = 0; i < pages.length; i++) {
|
||||
const page = pages[i]
|
||||
fetchData(page.pageNum)
|
||||
await fetchData(page.pageNum)
|
||||
}
|
||||
}, [fetchData, pages])
|
||||
|
||||
@ -292,7 +280,7 @@ const ObjectTable = forwardRef(
|
||||
prevPages.map((page) => ({
|
||||
...page,
|
||||
items: page.items.map((item) =>
|
||||
item._id === updatedData._id ? { ...item, ...updatedData } : item
|
||||
item._id === updatedData._id ? merge({}, item, updatedData) : item
|
||||
)
|
||||
}))
|
||||
)
|
||||
@ -453,7 +441,7 @@ const ObjectTable = forwardRef(
|
||||
// Table columns from model properties
|
||||
const columnsWithSkeleton = [
|
||||
{
|
||||
title: model.icon,
|
||||
title: lazyLoading ? <LoadingOutlined /> : cards ? model.icon : null,
|
||||
key: 'icon',
|
||||
width: 45,
|
||||
fixed: 'left',
|
||||
@ -688,16 +676,12 @@ const ObjectTable = forwardRef(
|
||||
return (
|
||||
<>
|
||||
{contextHolder}
|
||||
{cards ? (
|
||||
<Spin indicator={<LoadingOutlined />} spinning={loading}>
|
||||
{renderCards()}
|
||||
</Spin>
|
||||
) : (
|
||||
<Flex gap={'middle'} vertical>
|
||||
<Table
|
||||
ref={tableRef}
|
||||
dataSource={tableData}
|
||||
columns={columnsWithSkeleton}
|
||||
className={'dashboard-table'}
|
||||
className={cards ? 'dashboard-cards-header' : 'dashboard-table'}
|
||||
pagination={false}
|
||||
scroll={{ y: adjustedScrollHeight }}
|
||||
rowKey='_id'
|
||||
@ -707,7 +691,12 @@ const ObjectTable = forwardRef(
|
||||
showSorterTooltip={false}
|
||||
style={{ height: '100%' }}
|
||||
/>
|
||||
)}
|
||||
{cards ? (
|
||||
<Spin indicator={<LoadingOutlined />} spinning={loading}>
|
||||
{renderCards()}
|
||||
</Spin>
|
||||
) : null}
|
||||
</Flex>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
@ -41,7 +41,6 @@ const PrinterMovementPanel = ({ printerId }) => {
|
||||
|
||||
const handleHomeAxisClick = (axis) => {
|
||||
if (printServer) {
|
||||
logger.debug('Homeing Axis:', axis)
|
||||
printServer.emit('printer.gcode.script', {
|
||||
printerId,
|
||||
script: `G28 ${axis}`
|
||||
@ -52,7 +51,6 @@ const PrinterMovementPanel = ({ printerId }) => {
|
||||
const handleMoveAxisClick = (axis, minus) => {
|
||||
const distanceValue = !minus ? posValue * -1 : posValue
|
||||
if (printServer) {
|
||||
logger.debug('Moving Axis:', axis, distanceValue)
|
||||
printServer.emit('printer.gcode.script', {
|
||||
printerId,
|
||||
script: `_CLIENT_LINEAR_MOVE ${axis}=${distanceValue} F=${rateValue}`
|
||||
|
||||
7
src/components/Icons/ContractIcon.jsx
Normal file
7
src/components/Icons/ContractIcon.jsx
Normal file
@ -0,0 +1,7 @@
|
||||
import React from 'react'
|
||||
import Icon from '@ant-design/icons'
|
||||
import { ReactComponent as CustomIconSvg } from '../../assets/icons/contracticon.min.svg'
|
||||
|
||||
const ContractIcon = (props) => <Icon component={CustomIconSvg} {...props} />
|
||||
|
||||
export default ContractIcon
|
||||
7
src/components/Icons/ExpandIcon.jsx
Normal file
7
src/components/Icons/ExpandIcon.jsx
Normal file
@ -0,0 +1,7 @@
|
||||
import React from 'react'
|
||||
import Icon from '@ant-design/icons'
|
||||
import { ReactComponent as CustomIconSvg } from '../../assets/icons/expandicon.min.svg'
|
||||
|
||||
const ExpandIcon = (props) => <Icon component={CustomIconSvg} {...props} />
|
||||
|
||||
export default ExpandIcon
|
||||
7
src/components/Icons/HostIcon.jsx
Normal file
7
src/components/Icons/HostIcon.jsx
Normal file
@ -0,0 +1,7 @@
|
||||
import React from 'react'
|
||||
import Icon from '@ant-design/icons'
|
||||
import { ReactComponent as CustomIconSvg } from '../../assets/icons/hosticon.min.svg'
|
||||
|
||||
const HostIcon = (props) => <Icon component={CustomIconSvg} {...props} />
|
||||
|
||||
export default HostIcon
|
||||
7
src/components/Icons/MinusIcon.jsx
Normal file
7
src/components/Icons/MinusIcon.jsx
Normal file
@ -0,0 +1,7 @@
|
||||
import React from 'react'
|
||||
import Icon from '@ant-design/icons'
|
||||
import { ReactComponent as CustomIconSvg } from '../../assets/icons/minusicon.min.svg'
|
||||
|
||||
const MinusIcon = (props) => <Icon component={CustomIconSvg} {...props} />
|
||||
|
||||
export default MinusIcon
|
||||
@ -1,4 +1,5 @@
|
||||
import { Printer } from './models/Printer.js'
|
||||
import { Host } from './models/Host.js'
|
||||
import { Filament } from './models/Filament.js'
|
||||
import { Spool } from './models/Spool'
|
||||
import { GCodeFile } from './models/GCodeFile'
|
||||
@ -21,6 +22,7 @@ import QuestionCircleIcon from '../components/Icons/QuestionCircleIcon'
|
||||
|
||||
export const objectModels = [
|
||||
Printer,
|
||||
Host,
|
||||
Filament,
|
||||
Spool,
|
||||
GCodeFile,
|
||||
@ -44,6 +46,7 @@ export const objectModels = [
|
||||
// Re-export individual models for direct access
|
||||
export {
|
||||
Printer,
|
||||
Host,
|
||||
Filament,
|
||||
Spool,
|
||||
GCodeFile,
|
||||
|
||||
90
src/database/models/Host.js
Normal file
90
src/database/models/Host.js
Normal file
@ -0,0 +1,90 @@
|
||||
import HostIcon from '../../components/Icons/HostIcon'
|
||||
import InfoCircleIcon from '../../components/Icons/InfoCircleIcon'
|
||||
import ReloadIcon from '../../components/Icons/ReloadIcon'
|
||||
import EditIcon from '../../components/Icons/EditIcon'
|
||||
|
||||
export const Host = {
|
||||
name: 'host',
|
||||
label: 'Host',
|
||||
prefix: 'HST',
|
||||
icon: HostIcon,
|
||||
actions: [
|
||||
{
|
||||
name: 'info',
|
||||
label: 'Info',
|
||||
default: true,
|
||||
row: true,
|
||||
icon: InfoCircleIcon,
|
||||
url: (_id) => `/dashboard/production/hosts/info?hostId=${_id}`
|
||||
},
|
||||
|
||||
{
|
||||
name: 'reload',
|
||||
label: 'Reload',
|
||||
icon: ReloadIcon,
|
||||
url: (_id) =>
|
||||
`/dashboard/production/hosts/info?hostId=${_id}&action=reload`
|
||||
},
|
||||
{
|
||||
name: 'edit',
|
||||
label: 'Edit',
|
||||
row: true,
|
||||
icon: EditIcon,
|
||||
url: (_id) => `/dashboard/production/hosts/info?hostId=${_id}&action=edit`
|
||||
}
|
||||
],
|
||||
columns: ['name', '_id', 'state', 'tags', 'connectedAt'],
|
||||
filters: ['name', '_id', 'state', 'tags'],
|
||||
sorters: ['name', 'state', 'connectedAt'],
|
||||
group: ['tags'],
|
||||
properties: [
|
||||
{
|
||||
name: '_id',
|
||||
label: 'ID',
|
||||
type: 'id',
|
||||
objectType: 'host',
|
||||
showCopy: true
|
||||
},
|
||||
{
|
||||
name: 'connectedAt',
|
||||
label: 'Connected At',
|
||||
type: 'dateTime',
|
||||
readOnly: true
|
||||
},
|
||||
{
|
||||
name: 'name',
|
||||
label: 'Name',
|
||||
required: true,
|
||||
type: 'text',
|
||||
columnWidth: 200,
|
||||
columnFixed: 'left'
|
||||
},
|
||||
{
|
||||
name: 'state',
|
||||
label: 'Status',
|
||||
type: 'state',
|
||||
objectType: 'host',
|
||||
showName: false,
|
||||
readOnly: true
|
||||
},
|
||||
{
|
||||
name: 'host',
|
||||
label: 'Host',
|
||||
type: 'text',
|
||||
required: true
|
||||
},
|
||||
{
|
||||
name: 'tags',
|
||||
label: 'Tags',
|
||||
type: 'tags',
|
||||
required: false
|
||||
},
|
||||
{
|
||||
name: 'operatingSystem',
|
||||
label: 'Operating System',
|
||||
type: 'text',
|
||||
required: false,
|
||||
readOnly: true
|
||||
}
|
||||
]
|
||||
}
|
||||
@ -74,7 +74,7 @@ export const Job = {
|
||||
label: 'Quantity',
|
||||
type: 'number',
|
||||
columnWidth: 125,
|
||||
readOnly: true
|
||||
required: true
|
||||
},
|
||||
{
|
||||
name: 'createdAt',
|
||||
|
||||
@ -7,7 +7,7 @@ export const SubJob = {
|
||||
icon: SubJobIcon,
|
||||
actions: [],
|
||||
columns: ['_id', 'printer', 'printer._id', 'job._id', 'state', 'createdAt'],
|
||||
filters: ['state', '_id'],
|
||||
filters: ['state', '_id', 'job._id', 'printer._id'],
|
||||
sorters: ['createdAt', 'state'],
|
||||
properties: [
|
||||
{
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user