{userProfile ? (
-
-
- {JSON.stringify(
- // eslint-disable-next-line
- { ...userProfile, access_token: '...' },
- null,
- 2
- )}
-
-
+
) : (
n/a
)}
diff --git a/src/components/Dashboard/Developer/SessionStorage.jsx b/src/components/Dashboard/Developer/SessionStorage.jsx
index fee632f..ec994a6 100644
--- a/src/components/Dashboard/Developer/SessionStorage.jsx
+++ b/src/components/Dashboard/Developer/SessionStorage.jsx
@@ -2,6 +2,7 @@ import { useState } from 'react'
import { Descriptions, Button, Typography, Flex, Space, Dropdown } from 'antd'
import ReloadIcon from '../../Icons/ReloadIcon'
import BoolDisplay from '../common/BoolDisplay'
+import DataTree from '../common/DataTree'
const { Text } = Typography
@@ -14,6 +15,16 @@ const getSessionStorageItems = () => {
return items
}
+const isJsonString = (str) => {
+ if (typeof str !== 'string') return false
+ try {
+ const parsed = JSON.parse(str)
+ return typeof parsed === 'object' && parsed !== null
+ } catch {
+ return false
+ }
+}
+
const SessionStorage = () => {
const [items, setItems] = useState(getSessionStorageItems())
@@ -64,14 +75,18 @@ const SessionStorage = () => {
isBool = true
boolValue = value === 'true'
}
+
+ // Check if value is JSON
+ const isJson = isJsonString(value)
+
return (
{isBool ? (
+ ) : isJson ? (
+
) : (
-
- {value}
-
+ {value}
)}
)
diff --git a/src/components/Dashboard/Inventory/PartStocks.jsx b/src/components/Dashboard/Inventory/PartStocks.jsx
index d1bd986..05ab315 100644
--- a/src/components/Dashboard/Inventory/PartStocks.jsx
+++ b/src/components/Dashboard/Inventory/PartStocks.jsx
@@ -1,148 +1,29 @@
// src/partStocks.js
-import { useState, useContext, useRef } from 'react'
-import { useNavigate } from 'react-router-dom'
-import { Button, Flex, Space, Modal, message, Dropdown, Typography } from 'antd'
+import { useState, useRef } from 'react'
-import { AuthContext } from '../context/AuthContext'
+import { Button, Flex, Space, Modal, Dropdown, message } from 'antd'
import NewPartStock from './PartStocks/NewPartStock'
-import IdDisplay from '../common/IdDisplay'
-import PartStockIcon from '../../Icons/PartStockIcon'
-import InfoCircleIcon from '../../Icons/InfoCircleIcon'
import PlusIcon from '../../Icons/PlusIcon'
import ReloadIcon from '../../Icons/ReloadIcon'
-import PartStockState from '../common/PartStockState'
-import TimeDisplay from '../common/TimeDisplay'
+import useColumnVisibility from '../hooks/useColumnVisibility'
import ObjectTable from '../common/ObjectTable'
-
-import config from '../../../config'
-
-const { Text } = Typography
+import ListIcon from '../../Icons/ListIcon'
+import GridIcon from '../../Icons/GridIcon'
+import useViewMode from '../hooks/useViewMode'
+import ColumnViewButton from '../common/ColumnViewButton'
const PartStocks = () => {
const [messageApi, contextHolder] = message.useMessage()
- const navigate = useNavigate()
const tableRef = useRef()
const [newPartStockOpen, setNewPartStockOpen] = useState(false)
- const { authenticated } = useContext(AuthContext)
+ const [viewMode, setViewMode] = useViewMode('partStocks')
- const getPartStockActionItems = (id) => {
- return {
- items: [
- {
- label: 'Info',
- key: 'info',
- icon:
- }
- ],
- onClick: ({ key }) => {
- if (key === 'info') {
- navigate(`/dashboard/inventory/partstocks/info?partStockId=${id}`)
- }
- }
- }
- }
-
- // Column definitions
- const columns = [
- {
- title: '',
- dataIndex: '',
- key: 'icon',
- width: 40,
- fixed: 'left',
- render: () =>
- },
- {
- title: 'Part Name',
- dataIndex: 'part',
- key: 'name',
- width: 200,
- fixed: 'left',
- render: (part) => {part.name}
- },
- {
- title: 'ID',
- dataIndex: '_id',
- key: 'id',
- width: 180,
- render: (text) => (
-
- )
- },
- {
- title: 'State',
- key: 'state',
- width: 350,
- render: (record) =>
- },
- {
- title: 'Current Quantity',
- dataIndex: 'currentQuantity',
- key: 'currentQuantity',
- width: 160,
- render: (currentQuantity) => {currentQuantity}
- },
- {
- title: 'Starting Quantity',
- dataIndex: 'startingQuantity',
- key: 'startingQuantity',
- width: 160,
- render: (startingQuantity) => {startingQuantity}
- },
- {
- title: 'Created At',
- dataIndex: 'createdAt',
- key: 'createdAt',
- width: 180,
- render: (createdAt) => {
- if (createdAt) {
- return
- } else {
- return 'n/a'
- }
- }
- },
- {
- title: 'Updated At',
- dataIndex: 'updatedAt',
- key: 'updatedAt',
- width: 180,
- render: (updatedAt) => {
- if (updatedAt) {
- return
- } else {
- return 'n/a'
- }
- }
- },
- {
- title: 'Actions',
- key: 'actions',
- fixed: 'right',
- width: 150,
- render: (text, record) => {
- return (
-
- }
- onClick={() =>
- navigate(
- `/dashboard/inventory/partstocks/info?partStockId=${record._id}`
- )
- }
- />
-
-
-
-
- )
- }
- }
- ]
+ const [columnVisibility, setColumnVisibility] =
+ useColumnVisibility('partStocks')
const actionItems = {
items: [
@@ -171,23 +52,40 @@ const PartStocks = () => {
<>
{contextHolder}
-
-
-
-
-
+
+
+
+
+
+
+
+
+ : }
+ onClick={() =>
+ setViewMode(viewMode === 'cards' ? 'list' : 'cards')
+ }
+ />
+
+
+
{
setNewPartStockOpen(false)
}}
diff --git a/src/components/Dashboard/Management/Files/FileInfo.jsx b/src/components/Dashboard/Management/Files/FileInfo.jsx
index eaa9ebd..e45eebc 100644
--- a/src/components/Dashboard/Management/Files/FileInfo.jsx
+++ b/src/components/Dashboard/Management/Files/FileInfo.jsx
@@ -1,4 +1,4 @@
-import { useRef, useState } from 'react'
+import { useContext, useRef, useState } from 'react'
import { useLocation } from 'react-router-dom'
import { Space, Flex, Card } from 'antd'
import loglevel from 'loglevel'
@@ -22,6 +22,7 @@ import DocumentPrintButton from '../../common/DocumentPrintButton.jsx'
import FileIcon from '../../../Icons/FileIcon.jsx'
import FilePreview from '../../common/FilePreview.jsx'
import MissingPlaceholder from '../../common/MissingPlaceholder.jsx'
+import { ApiServerContext } from '../../context/ApiServerContext.jsx'
const log = loglevel.getLogger('FileInfo')
log.setLevel(config.logLevel)
@@ -41,11 +42,12 @@ const FileInfo = () => {
editLoading: false,
formValid: false,
lock: null,
- loading: false,
+ loading: true,
objectData: {
_id: fileId
}
})
+ const { fetchFileContent } = useContext(ApiServerContext)
const actions = {
reload: () => {
@@ -64,6 +66,10 @@ const FileInfo = () => {
objectFormRef?.current?.handleUpdate?.()
return true
},
+ download: () => {
+ fetchFileContent(objectFormState?.objectData, true)
+ return true
+ },
delete: () => {
objectFormRef?.current?.handleDelete?.()
return true
diff --git a/src/components/Dashboard/Management/Parts.jsx b/src/components/Dashboard/Management/Parts.jsx
index 2e363ca..dc2e6fe 100644
--- a/src/components/Dashboard/Management/Parts.jsx
+++ b/src/components/Dashboard/Management/Parts.jsx
@@ -4,7 +4,7 @@ import { useState, useRef } from 'react'
import { Button, Flex, Space, Modal, Dropdown, message } from 'antd'
import ObjectTable from '../common/ObjectTable'
-import NewProduct from './Products/NewProduct'
+import NewPart from './Parts/NewPart'
import PlusIcon from '../../Icons/PlusIcon'
import ReloadIcon from '../../Icons/ReloadIcon'
@@ -20,7 +20,7 @@ import ColumnViewButton from '../common/ColumnViewButton'
const Parts = (filter) => {
const [messageApi, contextHolder] = message.useMessage()
- const [newProductOpen, setNewProductOpen] = useState(false)
+ const [newPartOpen, setNewPartOpen] = useState(false)
const tableRef = useRef()
const [viewMode, setViewMode] = useViewMode('part')
const [columnVisibility, setColumnVisibility] = useColumnVisibility('part')
@@ -28,8 +28,8 @@ const Parts = (filter) => {
const actionItems = {
items: [
{
- label: 'New Product',
- key: 'newProduct',
+ label: 'New Part',
+ key: 'newPart',
icon:
},
{ type: 'divider' },
@@ -42,8 +42,8 @@ const Parts = (filter) => {
onClick: ({ key }) => {
if (key === 'reloadList') {
tableRef.current?.reload()
- } else if (key === 'newProduct') {
- setNewProductOpen(true)
+ } else if (key === 'newPart') {
+ setNewPartOpen(true)
}
}
}
@@ -82,21 +82,21 @@ const Parts = (filter) => {
/>
{
- setNewProductOpen(false)
+ setNewPartOpen(false)
}}
destroyOnHidden={true}
>
- {
- setNewProductOpen(false)
- messageApi.success('Product created successfully!')
+ setNewPartOpen(false)
+ messageApi.success('Part created successfully!')
tableRef.current?.reload()
}}
- reset={newProductOpen}
+ reset={newPartOpen}
/>
>
diff --git a/src/components/Dashboard/Management/Parts/NewPart.jsx b/src/components/Dashboard/Management/Parts/NewPart.jsx
new file mode 100644
index 0000000..0fae248
--- /dev/null
+++ b/src/components/Dashboard/Management/Parts/NewPart.jsx
@@ -0,0 +1,110 @@
+import PropTypes from 'prop-types'
+import ObjectInfo from '../../common/ObjectInfo'
+import NewObjectForm from '../../common/NewObjectForm'
+import WizardView from '../../common/WizardView'
+
+const NewPart = ({ onOk }) => {
+ return (
+
+ {({ handleSubmit, submitLoading, objectData, formValid }) => {
+ const steps = [
+ {
+ title: 'Required',
+ key: 'required',
+ content: (
+
+ )
+ },
+ {
+ title: 'Pricing',
+ key: 'pricing',
+ content: (
+
+ )
+ },
+ {
+ title: 'Optional',
+ key: 'optional',
+ content: (
+
+ )
+ },
+ {
+ title: 'Summary',
+ key: 'summary',
+ content: (
+
+ )
+ }
+ ]
+ return (
+ {
+ handleSubmit()
+ onOk()
+ }}
+ />
+ )
+ }}
+
+ )
+}
+
+NewPart.propTypes = {
+ onOk: PropTypes.func.isRequired,
+ reset: PropTypes.bool
+}
+
+export default NewPart
diff --git a/src/components/Dashboard/Management/Parts/PartInfo.jsx b/src/components/Dashboard/Management/Parts/PartInfo.jsx
index bdcc330..4576449 100644
--- a/src/components/Dashboard/Management/Parts/PartInfo.jsx
+++ b/src/components/Dashboard/Management/Parts/PartInfo.jsx
@@ -1,4 +1,4 @@
-import { useRef, useState, useContext } from 'react'
+import { useRef, useState } from 'react'
import { useLocation } from 'react-router-dom'
import { Space, Flex, Card } from 'antd'
import useCollapseState from '../../hooks/useCollapseState'
@@ -17,14 +17,12 @@ import ObjectActions from '../../common/ObjectActions.jsx'
import ObjectTable from '../../common/ObjectTable.jsx'
import InfoCollapsePlaceholder from '../../common/InfoCollapsePlaceholder.jsx'
import DocumentPrintButton from '../../common/DocumentPrintButton.jsx'
-import { ApiServerContext } from '../../context/ApiServerContext'
const PartInfo = () => {
const location = useLocation()
const objectFormRef = useRef(null)
const actionHandlerRef = useRef(null)
const partId = new URLSearchParams(location.search).get('partId')
- const { fetchObjectContent } = useContext(ApiServerContext)
const [collapseState, updateCollapseState] = useCollapseState('PartInfo', {
info: true,
parts: true,
@@ -55,13 +53,6 @@ const PartInfo = () => {
finishEdit: () => {
objectFormRef?.current?.handleUpdate?.()
return true
- },
- download: () => {
- if (partId && objectFormRef?.current?.getObjectData) {
- const objectData = objectFormRef.current.getObjectData()
- fetchObjectContent(partId, 'part', `${objectData?.name || 'part'}.stl`)
- return true
- }
}
}
diff --git a/src/components/Dashboard/Management/Products/NewProduct.jsx b/src/components/Dashboard/Management/Products/NewProduct.jsx
index 29c1012..ed85e57 100644
--- a/src/components/Dashboard/Management/Products/NewProduct.jsx
+++ b/src/components/Dashboard/Management/Products/NewProduct.jsx
@@ -1,509 +1,104 @@
import PropTypes from 'prop-types'
-import { useState, useContext, useEffect, useRef } from 'react'
-import axios from 'axios'
-import { useMediaQuery } from 'react-responsive'
-import {
- Input,
- Button,
- message,
- Typography,
- Flex,
- Steps,
- Divider,
- Upload,
- Descriptions,
- Modal,
- Progress,
- Form,
- Checkbox,
- InputNumber
-} from 'antd'
-import { DeleteOutlined, EyeOutlined } from '@ant-design/icons'
-import { AuthContext } from '../../context/AuthContext'
-import PartIcon from '../../../Icons/PartIcon'
-import VendorSelect from '../../common/VendorSelect'
-
-import config from '../../../../config'
-
-const { Dragger } = Upload
-const { Title, Text } = Typography
-
-const initialNewProductForm = {
- name: '',
- parts: [],
- vendor: null,
- marginOrPrice: false,
- margin: 0,
- price: 0
-}
-
-const NewProduct = ({ onOk, reset }) => {
- // UI state
- const [messageApi, contextHolder] = message.useMessage()
- const [currentStep, setCurrentStep] = useState(0)
- const [newProductLoading, setNewProductLoading] = useState(false)
- const [nextEnabled, setNextEnabled] = useState(false)
-
- const [newProductForm] = Form.useForm()
- const [newProductFormValues, setNewProductFormValues] = useState(
- initialNewProductForm
- )
- const newProductFormUpdateValues = Form.useWatch([], newProductForm)
-
- // Combined parts and files state
- const [parts, setParts] = useState([])
- const [fileUrls, setFileUrls] = useState({})
- const [uploadProgress, setUploadProgress] = useState({})
-
- // Preview state
- const [previewVisible, setPreviewVisible] = useState(false)
- const [previewFile, setPreviewFile] = useState(null)
- const [isPreviewLoading, setIsPreviewLoading] = useState(false)
- const previewTimerRef = useRef(null)
-
- const [marginOrPrice, setMarginOrPrice] = useState(false)
-
- const { token, authenticated } = useContext(AuthContext)
-
- const isMobile = useMediaQuery({ maxWidth: 768 })
-
- useEffect(() => {
- newProductForm
- .validateFields({
- validateOnly: true
- })
- .then(() => setNextEnabled(true))
- .catch(() => setNextEnabled(false))
- }, [newProductForm, newProductFormUpdateValues])
-
- useEffect(() => {
- if (reset) {
- newProductForm.resetFields()
- }
- }, [reset, newProductForm])
-
- useEffect(() => {
- setMarginOrPrice(newProductFormValues.marginOrPrice)
- }, [newProductFormValues])
-
- // Effect: Cleanup file URLs on unmount
- useEffect(() => {
- return () => {
- Object.values(fileUrls).forEach(URL.revokeObjectURL)
- if (previewTimerRef.current) {
- clearTimeout(previewTimerRef.current)
- }
- }
- }, [fileUrls])
-
- useEffect(() => {
- setNewProductFormValues((prev) => ({ ...prev, parts: parts }))
- }, [parts, setNewProductFormValues])
-
- // File handlers
- const handleFileAdd = (file) => {
- const objectUrl = URL.createObjectURL(file)
- const defaultName = file.name.replace(/\.[^/.]+$/, '')
-
- setParts((prev) => [
- {
- name: defaultName,
- file,
- uid: file.uid
- },
- ...prev
- ])
-
- setFileUrls((prev) => ({ ...prev, [file.uid]: objectUrl }))
- setUploadProgress((prev) => ({ ...prev, [file.uid]: 0 }))
-
- return false // Prevent default upload
- }
-
- const handleFileRemove = (index) => {
- setParts((prev) => {
- const newParts = [...prev]
- const removedPart = newParts[index]
- newParts.splice(index, 1)
-
- // Cleanup URL and progress
- if (removedPart && fileUrls[removedPart.uid]) {
- URL.revokeObjectURL(fileUrls[removedPart.uid])
- setFileUrls((urls) => {
- const newUrls = { ...urls }
- delete newUrls[removedPart.uid]
- return newUrls
- })
- setUploadProgress((progress) => {
- const newProgress = { ...progress }
- delete newProgress[removedPart.uid]
- return newProgress
- })
- }
-
- return newParts
- })
- }
-
- const handleNameChange = (index, newName) => {
- setParts((prev) => {
- const newParts = [...prev]
- newParts[index] = { ...newParts[index], name: newName }
- return newParts
- })
- }
-
- const handlePreview = (file) => {
- setPreviewFile(file)
- setPreviewVisible(true)
- setIsPreviewLoading(true)
-
- if (previewTimerRef.current) {
- clearTimeout(previewTimerRef.current)
- }
- previewTimerRef.current = setTimeout(() => {
- setIsPreviewLoading(false)
- }, 300)
- }
-
- const handleNewProduct = async () => {
- setNewProductLoading(true)
- try {
- const result = await axios.post(
- `${config.backendUrl}/products`,
- newProductFormValues,
- {
- withCredentials: true // Important for including cookies
- }
- )
- await uploadParts(result.data.parts)
- onOk()
- } catch (error) {
- messageApi.error('Error creating new product: ' + error.message)
- } finally {
- setNewProductLoading(false)
- }
- }
-
- // Submit handler
- const uploadParts = async (partIds) => {
- if (!authenticated) return
-
- try {
- // Upload files sequentially for each part
- for (let i = 0; i < parts.length; i++) {
- const formData = new FormData()
- formData.append('partFile', parts[i].file)
-
- await axios.post(
- `${config.backendUrl}/parts/${partIds[i]}/content`,
- formData,
- {
- headers: {
- 'Content-Type': 'multipart/form-data',
- Authorization: `Bearer ${token}`
- },
- onUploadProgress: (progressEvent) => {
- const percentCompleted = Math.round(
- (progressEvent.loaded * 100) / progressEvent.total
- )
- setUploadProgress((prev) => ({
- ...prev,
- [parts[i].uid]: percentCompleted
- }))
- }
- }
- )
- }
- } catch (error) {
- messageApi.error('Error creating product: ' + error.message)
- }
- }
-
- // Step Contents
- const uploadStep = (
-
- {parts.length != 0 ? (
-
-
- {parts.map((part, index) => (
-
- handleNameChange(index, e.target.value)}
- style={{ flex: 1 }}
- />
- }
- onClick={() => handlePreview(part.file)}
- />
- }
- onClick={() => handleFileRemove(index)}
- />
-
- ))}
-
-
- ) : null}
-
- setTimeout(() => onSuccess('ok'), 0)}
- >
-
-
-
-
- Click or drag 3D Model files here
-
- Supported file extensions: .stl, .3mf
-
-
-
-
- )
-
- const detailsStep = (
- <>
-
-
-
-
-
-
-
- {marginOrPrice == false ? (
-
-
-
- ) : (
-
-
-
- )}
-
- Price
-
-
- >
- )
-
- const summaryStep = (
- {newProductFormValues?.name}
- },
- {
- key: 'vendor',
- label: 'Vendor',
- children: {newProductFormValues?.vendor?.name}
- },
- {
- key: 'marginPrice',
- label: !marginOrPrice ? 'Margin' : 'Price',
- children: !marginOrPrice ? (
- {newProductFormValues?.margin}%
- ) : (
- £{newProductFormValues?.price}
- )
- },
- ...parts.map((part, index) => ({
- key: part.uid,
- label: `Part ${index + 1}`,
- children: (
-
- {part.name}
-
-
- )
- }))
- ]}
- />
- )
-
- const steps = [
- { title: 'Upload Parts', content: uploadStep },
- { title: 'Details', content: detailsStep },
- { title: 'Summary', content: summaryStep }
- ]
+import ObjectInfo from '../../common/ObjectInfo'
+import NewObjectForm from '../../common/NewObjectForm'
+import WizardView from '../../common/WizardView'
+const NewProduct = ({ onOk }) => {
return (
-
- {contextHolder}
-
- {!isMobile && (
-
-
-
- )}
-
- {!isMobile && }
-
-
-
- New Product
-
-
-
-
-
-
-
- {currentStep < steps.length - 1 ? (
-
- ) : (
-
- )}
-
-
-
- {
- setPreviewVisible(false)
- setPreviewFile(null)
- if (previewTimerRef.current) {
- clearTimeout(previewTimerRef.current)
- }
- }}
- style={{ top: 30 }}
- width='90%'
- >
-
- {previewFile && !isPreviewLoading ? (
-
- ) : (
-
- Loading 3D model...
-
- )}
-
-
-
+ />
+ )
+ }}
+
)
}
NewProduct.propTypes = {
- reset: PropTypes.bool.isRequired,
- onOk: PropTypes.func.isRequired
+ onOk: PropTypes.func.isRequired,
+ reset: PropTypes.bool
}
export default NewProduct
diff --git a/src/components/Dashboard/Production/GCodeFiles/GCodeFileInfo.jsx b/src/components/Dashboard/Production/GCodeFiles/GCodeFileInfo.jsx
index c95ac04..e4b9c7b 100644
--- a/src/components/Dashboard/Production/GCodeFiles/GCodeFileInfo.jsx
+++ b/src/components/Dashboard/Production/GCodeFiles/GCodeFileInfo.jsx
@@ -1,6 +1,6 @@
import { useRef, useState } from 'react'
import { useLocation } from 'react-router-dom'
-import { Space, Flex, Card, Typography } from 'antd'
+import { Space, Flex, Card } from 'antd'
import { LoadingOutlined } from '@ant-design/icons'
import loglevel from 'loglevel'
import config from '../../../../config.js'
@@ -21,8 +21,8 @@ import ObjectTable from '../../common/ObjectTable.jsx'
import InfoCollapsePlaceholder from '../../common/InfoCollapsePlaceholder.jsx'
import EyeIcon from '../../../Icons/EyeIcon.jsx'
import DocumentPrintButton from '../../common/DocumentPrintButton.jsx'
-
-const { Text } = Typography
+import MissingPlaceholder from '../../common/MissingPlaceholder.jsx'
+import FilePreview from '../../common/FilePreview.jsx'
const log = loglevel.getLogger('GCodeFileInfo')
log.setLevel(config.logLevel)
@@ -175,17 +175,16 @@ const GCodeFileInfo = () => {
}
collapseKey='preview'
>
-
- {objectData?.gcodeFileInfo?.thumbnail ? (
-
+
- ) : (
- n/a
- )}
-
+
+ ) : (
+
+ )}
)
diff --git a/src/components/Dashboard/Production/Jobs/NewJob.jsx b/src/components/Dashboard/Production/Jobs/NewJob.jsx
index 77e0550..a15667a 100644
--- a/src/components/Dashboard/Production/Jobs/NewJob.jsx
+++ b/src/components/Dashboard/Production/Jobs/NewJob.jsx
@@ -1,18 +1,9 @@
-import { useState } from 'react'
import PropTypes from 'prop-types'
-import { useMediaQuery } from 'react-responsive'
-import { Typography, Flex, Steps, Divider } from 'antd'
-
-import NewObjectButtons from '../../common/NewObjectButtons'
import ObjectInfo from '../../common/ObjectInfo'
import NewObjectForm from '../../common/NewObjectForm'
-
-const { Title } = Typography
+import WizardView from '../../common/WizardView'
const NewJob = ({ onOk }) => {
- const [currentStep, setCurrentStep] = useState(0)
- const isMobile = useMediaQuery({ maxWidth: 768 })
-
return (
{
}
]
return (
-
- {!isMobile && (
-
-
-
- )}
-
- {!isMobile && (
-
- )}
-
-
-
- New Job
-
-
- {steps[currentStep].content}
-
- setCurrentStep((prev) => prev - 1)}
- onNext={() => setCurrentStep((prev) => prev + 1)}
- onSubmit={() => {
- handleSubmit()
- onOk()
- }}
- formValid={formValid}
- submitLoading={submitLoading}
- />
-
-
+ {
+ handleSubmit()
+ onOk()
+ }}
+ />
)
}}
diff --git a/src/components/Dashboard/Production/Printers/ControlPrinter.jsx b/src/components/Dashboard/Production/Printers/ControlPrinter.jsx
index 95f7c41..40b0846 100644
--- a/src/components/Dashboard/Production/Printers/ControlPrinter.jsx
+++ b/src/components/Dashboard/Production/Printers/ControlPrinter.jsx
@@ -276,7 +276,10 @@ const ControlPrinter = () => {
}}
) : (
-
+
)}
{
}}
) : (
-
+
)}
{
}}
) : (
-
+
)}
diff --git a/src/components/Dashboard/common/ActionHandler.jsx b/src/components/Dashboard/common/ActionHandler.jsx
index 8e0c65e..34686cc 100644
--- a/src/components/Dashboard/common/ActionHandler.jsx
+++ b/src/components/Dashboard/common/ActionHandler.jsx
@@ -42,7 +42,7 @@ const ActionHandler = forwardRef(
// Execute action and clear from URL
useEffect(() => {
if (
- !loading &&
+ loading == false &&
action &&
actions[action] &&
lastExecutedAction.current !== action
diff --git a/src/components/Dashboard/common/DashboardNavigation.jsx b/src/components/Dashboard/common/DashboardNavigation.jsx
index c8c0216..670fe75 100644
--- a/src/components/Dashboard/common/DashboardNavigation.jsx
+++ b/src/components/Dashboard/common/DashboardNavigation.jsx
@@ -123,7 +123,6 @@ const DashboardNavigation = () => {
} else {
setApiServerState('disconnected')
}
- console.log('Connecting/connected', connecting, connected)
}, [connecting, connected])
const handleMainMenuClick = ({ key }) => {
@@ -221,7 +220,11 @@ const DashboardNavigation = () => {
}
/>
{isMobile && }
-
+
{
- return (
-
- )
-}
-
-FilamentSelect.propTypes = {
- onChange: PropTypes.func,
- value: PropTypes.object,
- filter: PropTypes.object,
- useFilter: PropTypes.bool
-}
-
-export default FilamentSelect
diff --git a/src/components/Dashboard/common/FilamentStockDisplay.jsx b/src/components/Dashboard/common/FilamentStockDisplay.jsx
deleted file mode 100644
index 2c60df2..0000000
--- a/src/components/Dashboard/common/FilamentStockDisplay.jsx
+++ /dev/null
@@ -1,50 +0,0 @@
-import { Flex, Typography, Badge } from 'antd'
-import PropTypes from 'prop-types'
-
-import FilamentStockIcon from '../../Icons/FilamentStockIcon'
-import IdDisplay from './IdDisplay'
-
-const { Text } = Typography
-
-const FilamentStockDisplay = ({
- filamentStock,
- longId = false,
- showIcon = true,
- showColor = true,
- showId = true,
- showCopy = true
-}) => {
- FilamentStockDisplay.propTypes = {
- filamentStock: PropTypes.shape({
- _id: PropTypes.string.isRequired,
- filament: PropTypes.shape({
- name: PropTypes.string,
- color: PropTypes.string
- }),
- currentNetWeight: PropTypes.number
- }).isRequired,
- longId: PropTypes.bool,
- showIcon: PropTypes.bool,
- showColor: PropTypes.bool,
- showId: PropTypes.bool,
- showCopy: PropTypes.bool
- }
-
- return (
-
- {showIcon && }
- {showColor && }
- {`${filamentStock.filament?.name} (${filamentStock.currentNetWeight}g)`}
- {showId && (
-
- )}
-
- )
-}
-
-export default FilamentStockDisplay
diff --git a/src/components/Dashboard/common/FilamentStockSelect.jsx b/src/components/Dashboard/common/FilamentStockSelect.jsx
deleted file mode 100644
index 37d71a9..0000000
--- a/src/components/Dashboard/common/FilamentStockSelect.jsx
+++ /dev/null
@@ -1,35 +0,0 @@
-import PropTypes from 'prop-types'
-import config from '../../../config'
-import ObjectSelect from './ObjectSelect'
-
-const FilamentStockSelect = ({
- onChange,
- filter = {},
- useFilter = false,
- value,
- disabled = false
-}) => {
- return (
-
- )
-}
-
-FilamentStockSelect.propTypes = {
- onChange: PropTypes.func.isRequired,
- value: PropTypes.object,
- filter: PropTypes.object,
- useFilter: PropTypes.bool,
- disabled: PropTypes.bool
-}
-
-export default FilamentStockSelect
diff --git a/src/components/Dashboard/common/FilamentStockState.jsx b/src/components/Dashboard/common/FilamentStockState.jsx
deleted file mode 100644
index 440c88c..0000000
--- a/src/components/Dashboard/common/FilamentStockState.jsx
+++ /dev/null
@@ -1,49 +0,0 @@
-import PropTypes from 'prop-types'
-import { Progress, Flex, Space } from 'antd'
-import StateTag from './StateTag'
-
-const getProgressColor = (percent) => {
- if (percent <= 50) {
- return '#52c41a' // green[5]
- } else if (percent <= 80) {
- // Interpolate between green and yellow
- const ratio = (percent - 50) / 30
- return `rgb(${Math.round(255 * ratio)}, ${Math.round(255 * (1 - ratio))}, 0)`
- } else {
- // Interpolate between yellow and red
- const ratio = (percent - 80) / 20
- return `rgb(255, ${Math.round(255 * (1 - ratio))}, 0)`
- }
-}
-
-const FilamentStockState = ({
- state = { type: 'unknown' },
- showProgress = true,
- showState = true
-}) => {
- return (
-
- {showState && (
-
-
-
- )}
- {showProgress && state.type === 'partiallyconsumed' ? (
-
- ) : null}
-
- )
-}
-
-FilamentStockState.propTypes = {
- state: PropTypes.object,
- showProgress: PropTypes.bool,
- showState: PropTypes.bool
-}
-
-export default FilamentStockState
diff --git a/src/components/Dashboard/common/FileList.jsx b/src/components/Dashboard/common/FileList.jsx
index d843806..acce2b9 100644
--- a/src/components/Dashboard/common/FileList.jsx
+++ b/src/components/Dashboard/common/FileList.jsx
@@ -22,9 +22,10 @@ const FileList = ({
showPreview = true,
showInfo = true,
showDownload = true,
- defaultPreviewOpen = false
+ defaultPreviewOpen = false,
+ card = true
}) => {
- const { fetchFileContent } = useContext(ApiServerContext)
+ const { fetchFileContent, flushFile } = useContext(ApiServerContext)
const navigate = useNavigate()
const [previewOpen, setPreviewOpen] = useState(defaultPreviewOpen)
const infoAction = getModelByName('file').actions.filter(
@@ -40,6 +41,7 @@ const FileList = ({
}
const handleRemove = (fileToRemove) => {
+ flushFile(fileToRemove._id)
if (multiple) {
const currentFiles = Array.isArray(files) ? files : []
const updatedFiles = currentFiles.filter((file) => {
@@ -59,76 +61,103 @@ const FileList = ({
const filesToRender = multiple ? files : [files]
+ const renderFileContent = (file) => (
+
+
+
+
+
+
+ {file.name || file.filename || 'Unknown file'}
+
+ {file.extension}
+
+
+ {showDownload && (
+ }
+ size='small'
+ type='text'
+ onClick={() => handleDownload(file)}
+ />
+ )}
+ {showPreview && (
+ : }
+ size='small'
+ type='text'
+ onClick={() => {
+ if (previewOpen == true) {
+ setPreviewOpen(false)
+ } else {
+ setPreviewOpen(true)
+ }
+ }}
+ />
+ )}
+ {showInfo && (
+ }
+ size='small'
+ type='text'
+ onClick={() => {
+ navigate(infoAction.url(file._id))
+ }}
+ />
+ )}
+ {editing && (
+ }
+ size='small'
+ type='text'
+ onClick={() => handleRemove(file)}
+ />
+ )}
+
+
+ {previewOpen ? (
+ <>
+
+
+ >
+ ) : null}
+
+ )
+
return (
- {filesToRender.map((file, index) => (
-
0 && multiple ? '4px' : undefined
- }}
- >
-
-
-
-
+ {filesToRender.map((file, index) => {
+ const key = file._id || file.id || index
+ const style = {
+ marginTop: index > 0 && multiple ? '4px' : undefined
+ }
- {file.name || file.filename || 'Unknown file'}
- {file.extension}
-
-
- {showDownload && (
- }
- size='small'
- type='text'
- onClick={() => handleDownload(file)}
- />
- )}
- {showPreview && (
- : }
- size='small'
- type='text'
- onClick={() => {
- if (previewOpen == true) {
- setPreviewOpen(false)
- } else {
- setPreviewOpen(true)
- }
- }}
- />
- )}
- {showInfo && (
- }
- size='small'
- type='text'
- onClick={() => {
- navigate(infoAction.url(file._id))
- }}
- />
- )}
- {editing && (
- }
- size='small'
- type='text'
- onClick={() => handleRemove(file)}
- />
- )}
-
-
- {previewOpen ? (
- <>
-
-
- >
- ) : null}
-
-
- ))}
+ if (card) {
+ return (
+
+ {renderFileContent(file)}
+
+ )
+ } else {
+ return (
+
+ {index > 0 &&
}
+ {renderFileContent(file)}
+
+ )
+ }
+ })}
)
}
@@ -141,7 +170,8 @@ FileList.propTypes = {
showPreview: PropTypes.string,
showInfo: PropTypes.bool,
showDownload: PropTypes.bool,
- defaultPreviewOpen: PropTypes.bool
+ defaultPreviewOpen: PropTypes.bool,
+ card: PropTypes.bool
}
export default FileList
diff --git a/src/components/Dashboard/common/FilePreview.jsx b/src/components/Dashboard/common/FilePreview.jsx
index 2596f1b..4aab4e1 100644
--- a/src/components/Dashboard/common/FilePreview.jsx
+++ b/src/components/Dashboard/common/FilePreview.jsx
@@ -26,8 +26,6 @@ const FilePreview = ({ file, style = {} }) => {
}
}, [file._id, file?.type, fetchPreview, token])
- console.log('file', file)
-
if (loading == true || !file?.type) {
return
}
diff --git a/src/components/Dashboard/common/FileUpload.jsx b/src/components/Dashboard/common/FileUpload.jsx
index 4bce154..b4940ce 100644
--- a/src/components/Dashboard/common/FileUpload.jsx
+++ b/src/components/Dashboard/common/FileUpload.jsx
@@ -14,7 +14,8 @@ const FileUpload = ({
onChange,
multiple = true,
defaultPreviewOpen = false,
- showPreview = true
+ showPreview = true,
+ showInfo
}) => {
const { uploadFile } = useContext(ApiServerContext)
@@ -39,6 +40,9 @@ const FileUpload = ({
// Track if there are no items in the list
const [hasNoItems, setHasNoItems] = useState(false)
+ // Track the selected file from ObjectSelect
+ const [selectedFile, setSelectedFile] = useState(null)
+
// Update hasNoItems when currentFiles changes
useEffect(() => {
const noItems = multiple
@@ -71,13 +75,39 @@ const FileUpload = ({
return false // Prevent default upload behavior
}
+ // Handle adding selected file to the list
+ const handleAddSelectedFile = () => {
+ if (selectedFile) {
+ if (multiple) {
+ // For multiple files, add to existing array
+ const newFiles = [...currentFiles, selectedFile]
+ setCurrentFiles(newFiles)
+ onChange(newFiles)
+ } else {
+ // For single file, replace the value
+ setCurrentFiles(selectedFile)
+ onChange(selectedFile)
+ }
+ // Clear the selection
+ setSelectedFile(null)
+ }
+ }
+
return (
{hasNoItems ? (
-
- } />
+
+ }
+ onClick={handleAddSelectedFile}
+ disabled={!selectedFile}
+ />
or
@@ -97,7 +127,7 @@ const FileUpload = ({
files={currentFiles}
multiple={multiple}
editing={true}
- showInfo={false}
+ showInfo={showInfo || false}
showPreview={showPreview}
defaultPreviewOpen={defaultPreviewOpen}
onChange={(updatedFiles) => {
@@ -112,7 +142,8 @@ FileUpload.propTypes = {
value: PropTypes.oneOfType([PropTypes.object, PropTypes.array]),
onChange: PropTypes.func,
multiple: PropTypes.bool,
- showPreview: PropTypes.string,
+ showPreview: PropTypes.bool,
+ showInfo: PropTypes.bool,
defaultPreviewOpen: PropTypes.bool
}
diff --git a/src/components/Dashboard/common/GCodeFileSelect.jsx b/src/components/Dashboard/common/GCodeFileSelect.jsx
deleted file mode 100644
index ed7b38d..0000000
--- a/src/components/Dashboard/common/GCodeFileSelect.jsx
+++ /dev/null
@@ -1,35 +0,0 @@
-// GCodeFileSelect.js
-import PropTypes from 'prop-types'
-import config from '../../../config'
-import ObjectSelect from './ObjectSelect'
-
-const propertyOrder = [
- 'filament.diameter',
- 'filament.type',
- 'filament.vendor.name'
-]
-
-const GCodeFileSelect = ({ onChange, filter, useFilter = false, style }) => {
- return (
-
- )
-}
-
-GCodeFileSelect.propTypes = {
- onChange: PropTypes.func,
- filter: PropTypes.object,
- useFilter: PropTypes.bool,
- style: PropTypes.object
-}
-
-export default GCodeFileSelect
diff --git a/src/components/Dashboard/common/InventorySidebar.jsx b/src/components/Dashboard/common/InventorySidebar.jsx
deleted file mode 100644
index c165730..0000000
--- a/src/components/Dashboard/common/InventorySidebar.jsx
+++ /dev/null
@@ -1,121 +0,0 @@
-import { useState, useEffect } from 'react'
-import { Link, useLocation } from 'react-router-dom'
-import { Layout, Menu, Flex, Button } from 'antd'
-import { DashboardOutlined, CaretDownFilled } from '@ant-design/icons'
-import FilamentStockIcon from '../../Icons/FilamentStockIcon'
-import PartStockIcon from '../../Icons/PartStockIcon'
-import ProductStockIcon from '../../Icons/ProductStockIcon'
-import StockAuditIcon from '../../Icons/StockAuditIcon'
-import StockEventIcon from '../../Icons/StockEventIcon'
-import CollapseSidebarIcon from '../../Icons/CollapseSidebarIcon'
-import ExpandSidebarIcon from '../../Icons/ExpandSidebarIcon'
-import { useMediaQuery } from 'react-responsive'
-
-const { Sider } = Layout
-
-const SIDEBAR_COLLAPSED_KEY = 'sidebar_collapsed'
-
-const InventorySidebar = () => {
- const location = useLocation()
- const [selectedKey, setSelectedKey] = useState('inventory')
- const [collapsed, setCollapsed] = useState(() => {
- const savedState = sessionStorage.getItem(SIDEBAR_COLLAPSED_KEY)
- return savedState ? JSON.parse(savedState) : false
- })
- const isMobile = useMediaQuery({ maxWidth: 768 })
-
- useEffect(() => {
- const pathParts = location.pathname.split('/').filter(Boolean)
- if (pathParts.length > 2) {
- setSelectedKey(pathParts[2]) // Return the section (inventory/management)
- }
- }, [location.pathname])
-
- const handleCollapse = (newCollapsed) => {
- setCollapsed(newCollapsed)
- sessionStorage.setItem(SIDEBAR_COLLAPSED_KEY, JSON.stringify(newCollapsed))
- }
-
- const items = [
- {
- key: 'overview',
- label: Overview,
- icon:
- },
- { type: 'divider' },
- {
- key: 'filamentstocks',
- label: (
- Filament Stocks
- ),
- icon:
- },
- {
- key: 'partstocks',
- label: Part Stocks,
- icon:
- },
- {
- key: 'productstocks',
- label: (
- Product Stocks
- ),
- icon:
- },
- { type: 'divider' },
- {
- key: 'stockevents',
- label: Stock Events,
- icon:
- },
- {
- key: 'stockaudits',
- label: Stock Audits,
- icon:
- }
- ]
-
- if (isMobile) {
- return (
- } />}
- />
- )
- }
-
- return (
-
-
-
-
- : }
- style={{ flexGrow: 1 }}
- onClick={() => handleCollapse(!collapsed)}
- />
-
-
-
- )
-}
-
-export default InventorySidebar
diff --git a/src/components/Dashboard/common/JobState.jsx b/src/components/Dashboard/common/JobState.jsx
deleted file mode 100644
index c959b79..0000000
--- a/src/components/Dashboard/common/JobState.jsx
+++ /dev/null
@@ -1,43 +0,0 @@
-import PropTypes from 'prop-types'
-import { Progress, Flex, Space } from 'antd'
-import { useState, useEffect } from 'react'
-import StateTag from './StateTag'
-
-const JobState = ({ state, showProgress = true, showState = true }) => {
- const [currentState, setCurrentState] = useState(
- state || { type: 'unknown', progress: 0 }
- )
-
- useEffect(() => {
- if (state) {
- setCurrentState(state)
- }
- }, [state])
-
- return (
-
- {showState && (
-
-
-
- )}
- {showProgress &&
- (currentState.type === 'printing' ||
- currentState.type === 'processing') ? (
-
- ) : null}
-
- )
-}
-
-JobState.propTypes = {
- state: PropTypes.object,
- showProgress: PropTypes.bool,
- showState: PropTypes.bool
-}
-
-export default JobState
diff --git a/src/components/Dashboard/common/ManagementSidebar.jsx b/src/components/Dashboard/common/ManagementSidebar.jsx
deleted file mode 100644
index 2689b19..0000000
--- a/src/components/Dashboard/common/ManagementSidebar.jsx
+++ /dev/null
@@ -1,129 +0,0 @@
-import { useState, useEffect } from 'react'
-import { Link, useLocation } from 'react-router-dom'
-import { Layout, Menu, Flex, Button } from 'antd'
-import { CaretDownFilled } from '@ant-design/icons'
-import FilamentIcon from '../../Icons/FilamentIcon'
-import PartIcon from '../../Icons/PartIcon'
-import ProductIcon from '../../Icons/ProductIcon'
-import VendorIcon from '../../Icons/VendorIcon'
-import MaterialIcon from '../../Icons/MaterialIcon'
-import CollapseSidebarIcon from '../../Icons/CollapseSidebarIcon'
-import ExpandSidebarIcon from '../../Icons/ExpandSidebarIcon'
-import AuditLogIcon from '../../Icons/AuditLogIcon'
-import NoteTypeIcon from '../../Icons/NoteTypeIcon'
-import { useMediaQuery } from 'react-responsive'
-import SettingsIcon from '../../Icons/SettingsIcon'
-
-const { Sider } = Layout
-
-const SIDEBAR_COLLAPSED_KEY = 'sidebar_collapsed'
-
-const ManagementSidebar = () => {
- const location = useLocation()
- const [selectedKey, setSelectedKey] = useState('production')
- const [collapsed, setCollapsed] = useState(() => {
- const savedState = sessionStorage.getItem(SIDEBAR_COLLAPSED_KEY)
- return savedState ? JSON.parse(savedState) : false
- })
- const isMobile = useMediaQuery({ maxWidth: 768 })
-
- useEffect(() => {
- const pathParts = location.pathname.split('/').filter(Boolean)
- if (pathParts.length > 2) {
- setSelectedKey(pathParts[2]) // Return the section (production/management)
- }
- }, [location.pathname])
-
- const handleCollapse = (newCollapsed) => {
- setCollapsed(newCollapsed)
- sessionStorage.setItem(SIDEBAR_COLLAPSED_KEY, JSON.stringify(newCollapsed))
- }
-
- const items = [
- {
- key: 'filaments',
- label: Filaments,
- icon:
- },
- {
- key: 'parts',
- label: Parts,
- icon:
- },
- {
- key: 'products',
- label: Products,
- icon:
- },
- {
- key: 'vendors',
- label: Vendors,
- icon:
- },
- {
- key: 'materials',
- label: Materials,
- icon:
- },
- {
- key: 'notetypes',
- label: Note Types,
- icon:
- },
- { type: 'divider' },
- {
- key: 'settings',
- label: Settings,
- icon:
- },
- {
- key: 'auditlogs',
- label: Audit Log,
- icon:
- }
- ]
-
- if (isMobile) {
- return (
- } />}
- />
- )
- }
-
- return (
-
-
-
-
- : }
- style={{ flexGrow: 1 }}
- onClick={() => handleCollapse(!collapsed)}
- />
-
-
-
- )
-}
-
-export default ManagementSidebar
diff --git a/src/components/Dashboard/common/NewObjectForm.jsx b/src/components/Dashboard/common/NewObjectForm.jsx
index b02ddb7..c78a1b7 100644
--- a/src/components/Dashboard/common/NewObjectForm.jsx
+++ b/src/components/Dashboard/common/NewObjectForm.jsx
@@ -1,8 +1,9 @@
-import { useState, useEffect, useContext } from 'react'
+import { useState, useEffect, useContext, useCallback } from 'react'
import { Form, message } from 'antd'
import { ApiServerContext } from '../context/ApiServerContext'
import PropTypes from 'prop-types'
import merge from 'lodash/merge'
+import { getModelByName } from '../../../database/ObjectModels'
/**
* NewObjectForm is a reusable form component for creating new objects.
@@ -25,12 +26,46 @@ const NewObjectForm = ({ type, style, defaultValues = {}, children }) => {
const [messageApi, contextHolder] = message.useMessage()
const { createObject, showError } = useContext(ApiServerContext)
+ // Get the model definition for this object type
+ const model = getModelByName(type)
+
+ // Function to calculate computed values from model properties
+ const calculateComputedValues = useCallback((currentData, model) => {
+ if (!model || !model.properties) return {}
+
+ const computedValues = {}
+
+ model.properties.forEach((property) => {
+ // Check if this property has a computed value function
+ if (property.value && typeof property.value === 'function') {
+ try {
+ const computedValue = property.value(currentData)
+ if (computedValue !== undefined) {
+ computedValues[property.name] = computedValue
+ }
+ } catch (error) {
+ console.warn(
+ `Error calculating value for property ${property.name}:`,
+ error
+ )
+ }
+ }
+ })
+
+ return computedValues
+ }, [])
+
// Set initial form values when defaultValues change
useEffect(() => {
if (Object.keys(defaultValues).length > 0) {
- form.setFieldsValue(defaultValues)
+ // Calculate computed values for initial data
+ const computedValues = calculateComputedValues(defaultValues, model)
+ const initialFormData = { ...defaultValues, ...computedValues }
+
+ form.setFieldsValue(initialFormData)
+ setObjectData(initialFormData)
}
- }, [form, defaultValues])
+ }, [form, defaultValues, calculateComputedValues, model])
// Validate form on change
useEffect(() => {
@@ -67,8 +102,20 @@ const NewObjectForm = ({ type, style, defaultValues = {}, children }) => {
layout='vertical'
style={style}
onValuesChange={(values) => {
+ // Calculate computed values based on current form data
+ const currentFormData = { ...objectData, ...values }
+ const computedValues = calculateComputedValues(currentFormData, model)
+
+ // Update form with computed values if any were calculated
+ if (Object.keys(computedValues).length > 0) {
+ form.setFieldsValue(computedValues)
+ }
+
+ // Merge all values (user input + computed values)
+ const allValues = { ...values, ...computedValues }
+
setObjectData((prev) => {
- return merge({}, prev, values)
+ return merge({}, prev, allValues)
})
}}
>
diff --git a/src/components/Dashboard/common/ObjectForm.jsx b/src/components/Dashboard/common/ObjectForm.jsx
index 964164a..78fcae1 100644
--- a/src/components/Dashboard/common/ObjectForm.jsx
+++ b/src/components/Dashboard/common/ObjectForm.jsx
@@ -4,7 +4,8 @@ import {
useContext,
useCallback,
forwardRef,
- useImperativeHandle
+ useImperativeHandle,
+ useRef
} from 'react'
import { Form, message } from 'antd'
import { ApiServerContext } from '../context/ApiServerContext'
@@ -12,6 +13,7 @@ import { AuthContext } from '../context/AuthContext'
import PropTypes from 'prop-types'
import DeleteObjectModal from './DeleteObjectModal'
import merge from 'lodash/merge'
+import { getModelByName } from '../../../database/ObjectModels'
/**
* ObjectForm is a reusable form component for editing any object type.
@@ -50,30 +52,130 @@ const ObjectForm = forwardRef(
showError,
connected,
subscribeToObjectUpdates,
- subscribeToObjectLock
+ subscribeToObjectLock,
+ flushFile
} = useContext(ApiServerContext)
const { token } = useContext(AuthContext)
+
+ // Get the model definition for this object type
+ const model = getModelByName(type)
+
+ // Check if the model has properties with type 'file' or 'fileList'
+ const hasFileProperties = useCallback(() => {
+ if (!model || !model.properties) return false
+ return model.properties.some(
+ (property) => property.type === 'file' || property.type === 'fileList'
+ )
+ }, [model])
+
+ const flushOrphanFiles = useCallback(() => {
+ if (!model || !model.properties || !objectData) return
+
+ model.properties.forEach((property) => {
+ if (property.type === 'file') {
+ // Handle single file property
+ const fileId =
+ objectData[property.name]?._id || objectData[property.name]
+ if (fileId) {
+ flushFile(fileId)
+ }
+ } else if (property.type === 'fileList') {
+ // Handle fileList property
+ const fileList = objectData[property.name]
+ if (Array.isArray(fileList)) {
+ fileList.forEach((file) => {
+ const fileId = file?._id || file
+ if (fileId) {
+ flushFile(fileId)
+ }
+ })
+ }
+ }
+ })
+ }, [model, objectData, flushFile])
+
+ // Refs to store current values for cleanup
+ const currentIdRef = useRef(id)
+ const currentTypeRef = useRef(type)
+ const currentIsEditingRef = useRef(isEditing)
+ const currentUnlockObjectRef = useRef(unlockObject)
+ const currentHasFilePropertiesRef = useRef(hasFileProperties)
+ const currentFlushOrphanFilesRef = useRef(flushOrphanFiles)
+
+ // Update refs when values change
+ useEffect(() => {
+ currentIdRef.current = id
+ currentTypeRef.current = type
+ currentIsEditingRef.current = isEditing
+ currentUnlockObjectRef.current = unlockObject
+ currentHasFilePropertiesRef.current = hasFileProperties
+ currentFlushOrphanFilesRef.current = flushOrphanFiles
+ })
+
+ // Function to calculate computed values from model properties
+ const calculateComputedValues = useCallback((currentData, model) => {
+ if (!model || !model.properties) return {}
+
+ const computedValues = {}
+
+ model.properties.forEach((property) => {
+ // Check if this property has a computed value function
+ if (property.value && typeof property.value === 'function') {
+ try {
+ const computedValue = property.value(currentData)
+ if (computedValue !== undefined) {
+ computedValues[property.name] = computedValue
+ }
+ } catch (error) {
+ console.warn(
+ `Error calculating value for property ${property.name}:`,
+ error
+ )
+ }
+ }
+ })
+
+ return computedValues
+ }, [])
+
// Validate form on change
useEffect(() => {
form
.validateFields({ validateOnly: true })
.then(() => {
setFormValid(true)
- onStateChange({ formValid: true, objectData: form.getFieldsValue() })
+ onStateChange({
+ formValid: true,
+ objectData: { ...serverObjectData, ...form.getFieldsValue() }
+ })
})
.catch(() => {
- onStateChange({ formValid: true, objectData: form.getFieldsValue() })
+ onStateChange({
+ formValid: true,
+ objectData: { ...serverObjectData, ...form.getFieldsValue() }
+ })
})
}, [form, formUpdateValues])
// Cleanup on unmount
useEffect(() => {
return () => {
- if (id) {
- unlockObject(id, type)
+ if (currentIdRef.current) {
+ currentUnlockObjectRef.current(
+ currentIdRef.current,
+ currentTypeRef.current
+ )
+ }
+
+ // Call flushOrphanFiles if component was editing and model has file properties
+ if (
+ currentIsEditingRef.current &&
+ currentHasFilePropertiesRef.current()
+ ) {
+ currentFlushOrphanFilesRef.current()
}
}
- }, [id, type, unlockObject])
+ }, []) // Empty dependency array - only run on mount/unmount
const handleFetchObject = useCallback(async () => {
try {
@@ -85,7 +187,12 @@ const ObjectForm = forwardRef(
onStateChange({ lock: lockEvent })
setObjectData(data)
setServerObjectData(data)
- form.setFieldsValue(data)
+
+ // Calculate and set computed values on initial load
+ const computedValues = calculateComputedValues(data, model)
+ const initialFormData = { ...data, ...computedValues }
+
+ form.setFieldsValue(initialFormData)
setFetchLoading(false)
onStateChange({ loading: false })
} catch (err) {
@@ -157,8 +264,12 @@ const ObjectForm = forwardRef(
const cancelEditing = () => {
if (serverObjectData) {
- form.setFieldsValue(serverObjectData)
- setObjectData(serverObjectData)
+ // Recalculate computed values when canceling
+ const computedValues = calculateComputedValues(serverObjectData, model)
+ const resetFormData = { ...serverObjectData, ...computedValues }
+
+ form.setFieldsValue(resetFormData)
+ setObjectData(resetFormData)
}
setIsEditing(false)
onStateChange({ isEditing: false })
@@ -247,8 +358,24 @@ const ObjectForm = forwardRef(
if (onEdit != undefined) {
onEdit(values)
}
+
+ // Calculate computed values based on current form data
+ const currentFormData = { ...objectData, ...values }
+ const computedValues = calculateComputedValues(
+ currentFormData,
+ model
+ )
+
+ // Update form with computed values if any were calculated
+ if (Object.keys(computedValues).length > 0) {
+ form.setFieldsValue(computedValues)
+ }
+
+ // Merge all values (user input + computed values)
+ const allValues = { ...values, ...computedValues }
+
setObjectData((prev) => {
- return { ...prev, ...values }
+ return { ...prev, ...allValues }
})
}}
>
diff --git a/src/components/Dashboard/common/ObjectInfo.jsx b/src/components/Dashboard/common/ObjectInfo.jsx
index 79d3000..d664cef 100644
--- a/src/components/Dashboard/common/ObjectInfo.jsx
+++ b/src/components/Dashboard/common/ObjectInfo.jsx
@@ -46,13 +46,24 @@ const ObjectInfo = ({
objectPropertyProps = { ...objectPropertyProps, showHyperlink }
}
// Filter items based on visibleProperties
- // If a property key exists in visibleProperties and is false, hide it
+ // If all values in visibleProperties are true, use whitelist mode
+ // Otherwise, use blacklist mode (hide properties set to false)
+ const visibleValues = Object.values(visibleProperties)
+ const isWhitelistMode =
+ visibleValues.length > 0 && visibleValues.every((value) => value === true)
+
items = items.filter((item) => {
const propertyName = item.name
- return !(
- propertyName in visibleProperties &&
- visibleProperties[propertyName] === false
- )
+ if (isWhitelistMode) {
+ // Whitelist mode: only show properties that are explicitly set to true
+ return visibleProperties[propertyName] === true
+ } else {
+ // Blacklist mode: hide properties that are explicitly set to false
+ return !(
+ propertyName in visibleProperties &&
+ visibleProperties[propertyName] === false
+ )
+ }
})
// Map items to Descriptions 'items' prop format
diff --git a/src/components/Dashboard/common/ObjectProperty.jsx b/src/components/Dashboard/common/ObjectProperty.jsx
index 87f410f..a490b80 100644
--- a/src/components/Dashboard/common/ObjectProperty.jsx
+++ b/src/components/Dashboard/common/ObjectProperty.jsx
@@ -39,6 +39,9 @@ import ObjectTypeDisplay from './ObjectTypeDisplay'
import CodeBlockEditor from './CodeBlockEditor'
import StateDisplay from './StateDisplay'
import AlertsDisplay from './AlertsDisplay'
+import FileUpload from './FileUpload'
+import DataTree from './DataTree'
+import FileList from './FileList'
const { Text } = Typography
@@ -76,6 +79,9 @@ const ObjectProperty = ({
initial = false,
height = 'auto',
minimal = false,
+ previewOpen = false,
+ showPreview = true,
+ showHyperlink,
...rest
}) => {
if (value && typeof value == 'function' && objectData) {
@@ -370,7 +376,14 @@ const ObjectProperty = ({
}
case 'id': {
if (value) {
- return
+ return (
+
+ )
} else {
return (
@@ -426,6 +439,40 @@ const ObjectProperty = ({
case 'propertyChanges': {
return
}
+ case 'data': {
+ return
+ }
+ case 'file': {
+ if (value == null || value?.length == 0 || value == undefined) {
+ return (
+
+ n/a
+
+ )
+ } else {
+ return (
+
+ )
+ }
+ }
+ case 'fileList': {
+ return (
+
+ )
+ }
default: {
if (value) {
return {value}
@@ -443,7 +490,7 @@ const ObjectProperty = ({
// Editable mode: wrap in Form.Item
// Merge required rule if needed
let mergedFormItemProps = { ...formItemProps, style: { flexGrow: 1 } }
- if (required) {
+ if (required && disabled == false) {
let rules
if (mergedFormItemProps.rules) {
rules = [...mergedFormItemProps.rules]
@@ -664,6 +711,30 @@ const ObjectProperty = ({
)
+ case 'file':
+ return (
+
+
+
+ )
+ case 'fileList':
+ return (
+
+
+
+ )
default:
return (
@@ -699,7 +770,10 @@ ObjectProperty.propTypes = {
empty: PropTypes.oneOfType([PropTypes.bool, PropTypes.func]),
difference: PropTypes.oneOfType([PropTypes.any, PropTypes.func]),
objectData: PropTypes.object,
- height: PropTypes.string
+ height: PropTypes.string,
+ previewOpen: PropTypes.bool,
+ showPreview: PropTypes.bool,
+ showHyperlink: PropTypes.bool
}
export default ObjectProperty
diff --git a/src/components/Dashboard/common/ObjectSelect.jsx b/src/components/Dashboard/common/ObjectSelect.jsx
index 1999dea..d1b1b6b 100644
--- a/src/components/Dashboard/common/ObjectSelect.jsx
+++ b/src/components/Dashboard/common/ObjectSelect.jsx
@@ -27,7 +27,7 @@ const ObjectSelect = ({
disabled = false,
...rest
}) => {
- const { fetchObjectsByProperty } = useContext(ApiServerContext)
+ const { fetchObjectsByProperty, fetchObject } = useContext(ApiServerContext)
const { token } = useContext(AuthContext)
// --- State ---
const [treeData, setTreeData] = useState([])
@@ -39,6 +39,36 @@ const ObjectSelect = ({
const [treeSelectValue, setTreeSelectValue] = useState(null)
const [initialLoading, setInitialLoading] = useState(true)
+ // Refs to track value changes
+ const prevValueRef = useRef(value)
+ const isInternalChangeRef = useRef(false)
+
+ // Utility function to check if object only contains _id
+ const isMinimalObject = useCallback((obj) => {
+ if (!obj || typeof obj !== 'object' || Array.isArray(obj)) {
+ return false
+ }
+ const keys = Object.keys(obj)
+ return keys.length === 1 && keys[0] === '_id' && obj._id
+ }, [])
+
+ // Function to fetch full object if only _id is present
+ const fetchFullObjectIfNeeded = useCallback(
+ async (obj) => {
+ if (isMinimalObject(obj)) {
+ try {
+ const fullObject = await fetchObject(obj._id, type)
+ return fullObject
+ } catch (err) {
+ console.error('Failed to fetch full object:', err)
+ return obj // Return original object if fetch fails
+ }
+ }
+ return obj
+ },
+ [isMinimalObject, fetchObject, type]
+ )
+
// Fetch the object properties tree from the API
const handleFetchObjectsProperties = useCallback(
async (customFilter = filter) => {
@@ -79,7 +109,7 @@ const ObjectSelect = ({
})
return {
title: (
-
+
{
+ // Mark this as an internal change
+ isInternalChangeRef.current = true
+
// value can be a string (single) or array (multiple)
if (multiple) {
// Multiple selection
@@ -211,38 +244,51 @@ const ObjectSelect = ({
}, [objectPropertiesTree, properties, 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)) {
- const filterValue = value[prop]
- if (filterValue?.name) {
- valueFilter[prop] = filterValue.name
- } else if (Array.isArray(filterValue)) {
- valueFilter[prop] = filterValue.join(',')
- } else {
- valueFilter[prop] = filterValue
+ const handleValue = async () => {
+ if (
+ value &&
+ typeof value === 'object' &&
+ value !== null &&
+ !initialized
+ ) {
+ // Check if value is a minimal object and fetch full object if needed
+ const fullValue = await fetchFullObjectIfNeeded(value)
+
+ // 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(fullValue, prop)) {
+ const filterValue = fullValue[prop]
+ if (filterValue?.name) {
+ valueFilter[prop] = filterValue.name
+ } else if (Array.isArray(filterValue)) {
+ valueFilter[prop] = filterValue.join(',')
+ } else {
+ valueFilter[prop] = filterValue
+ }
}
- }
- })
- // Fetch with the new filter
- handleFetchObjectsProperties(valueFilter)
- setTreeSelectValue(value._id)
- setInitialized(true)
- return
- }
- if (!initialized && token != null) {
- handleFetchObjectsProperties()
- setInitialized(true)
+ })
+ // Fetch with the new filter
+ handleFetchObjectsProperties(valueFilter)
+ setTreeSelectValue(fullValue._id)
+ setInitialized(true)
+ return
+ }
+ if (!initialized && token != null) {
+ handleFetchObjectsProperties()
+ setInitialized(true)
+ }
}
+
+ handleValue()
}, [
value,
filter,
properties,
handleFetchObjectsProperties,
initialized,
- token
+ token,
+ fetchFullObjectIfNeeded
])
const prevValuesRef = useRef({ type, masterFilter })
@@ -263,6 +309,29 @@ const ObjectSelect = ({
}
}, [type, masterFilter])
+ useEffect(() => {
+ // Check if value has actually changed
+ const hasValueChanged =
+ JSON.stringify(prevValueRef.current) !== JSON.stringify(value)
+
+ if (hasValueChanged) {
+ const changeSource = isInternalChangeRef.current ? 'internal' : 'external'
+
+ if (changeSource == 'external') {
+ setObjectPropertiesTree({})
+ setTreeData([])
+ setInitialized(false)
+ prevValuesRef.current = { type, masterFilter }
+ }
+
+ // Reset the internal change flag
+ isInternalChangeRef.current = false
+
+ // Update the previous value reference
+ prevValueRef.current = value
+ }
+ }, [value])
+
// --- Error UI ---
if (error) {
return (
diff --git a/src/components/Dashboard/common/ObjectTable.jsx b/src/components/Dashboard/common/ObjectTable.jsx
index 5934ce5..64ee3c5 100644
--- a/src/components/Dashboard/common/ObjectTable.jsx
+++ b/src/components/Dashboard/common/ObjectTable.jsx
@@ -49,12 +49,13 @@ const ObjectTable = forwardRef(
{
type,
pageSize = 25,
- scrollHeight = 'calc(var(--unit-100vh) - 270px)',
+ scrollHeight = 'calc(var(--unit-100vh) - 260px)',
onDataChange,
initialPage = 1,
cards = false,
visibleColumns = {},
- masterFilter = {}
+ masterFilter = {},
+ size = 'middle'
},
ref
) => {
@@ -103,6 +104,7 @@ const ObjectTable = forwardRef(
const unsubscribesRef = useRef([])
const updateEventHandlerRef = useRef()
const subscribeToObjectTypeUpdatesRef = useRef(null)
+ const prevValuesRef = useRef({ type, masterFilter })
const rowActions =
model.actions?.filter((action) => action.row == true) || []
@@ -462,6 +464,27 @@ const ObjectTable = forwardRef(
}
}, [token, loadInitialPage, initialPage, pages, initialized])
+ // Watch for changes in type and masterFilter, reset component state when they change
+ useEffect(() => {
+ const prevValues = prevValuesRef.current
+
+ // Deep comparison for objects, simple comparison for primitives
+ const hasChanged =
+ prevValues.type !== type ||
+ JSON.stringify(prevValues.masterFilter) !== JSON.stringify(masterFilter)
+
+ if (hasChanged) {
+ setPages([])
+ setTableFilter({})
+ setTableSorter({})
+ setInitialized(false)
+ setLoading(true)
+ setLazyLoading(false)
+ setHasMore(true)
+ prevValuesRef.current = { type, masterFilter }
+ }
+ }, [type, masterFilter])
+
const getFilterDropdown = ({
setSelectedKeys,
selectedKeys,
@@ -523,7 +546,9 @@ const ObjectTable = forwardRef(
key: 'icon',
width: 45,
fixed: 'left',
- render: () => createElement(model.icon)
+ render: () => {
+ return {createElement(model.icon)}
+ }
}
]
@@ -771,7 +796,7 @@ const ObjectTable = forwardRef(
onChange={handleTableChange}
showSorterTooltip={false}
style={{ height: '100%' }}
- size={isElectron ? 'small' : 'middle'}
+ size={size}
/>
{cards ? (
} spinning={loading}>
@@ -795,7 +820,8 @@ ObjectTable.propTypes = {
cards: PropTypes.bool,
cardRenderer: PropTypes.func,
visibleColumns: PropTypes.object,
- masterFilter: PropTypes.object
+ masterFilter: PropTypes.object,
+ size: PropTypes.string
}
export default ObjectTable
diff --git a/src/components/Dashboard/common/ObjectTypeSelect.jsx b/src/components/Dashboard/common/ObjectTypeSelect.jsx
index ed25cc3..58c96ca 100644
--- a/src/components/Dashboard/common/ObjectTypeSelect.jsx
+++ b/src/components/Dashboard/common/ObjectTypeSelect.jsx
@@ -20,8 +20,6 @@ const ObjectTypeSelect = ({
label:
}))
- console.log('VALUE', value)
-
return (