diff --git a/src/components/Dashboard/Management/AuditLogs.jsx b/src/components/Dashboard/Management/AuditLogs.jsx index 3df2571..96a92f2 100644 --- a/src/components/Dashboard/Management/AuditLogs.jsx +++ b/src/components/Dashboard/Management/AuditLogs.jsx @@ -24,6 +24,7 @@ import AuditLogIcon from '../../Icons/AuditLogIcon' import XMarkIcon from '../../Icons/XMarkIcon' import CheckIcon from '../../Icons/CheckIcon' import BoolDisplay from '../common/BoolDisplay' +import StateTag from '../common/StateTag' const { Text } = Typography @@ -48,9 +49,7 @@ const formatValue = (value, propertyName) => { } if (propertyName === 'state' && typeof value === 'object' && value.type) { - return ( - {value.type.charAt(0).toUpperCase() + value.type.slice(1)} - ) + return } // Check if the value is a timestamp (ISO date string) diff --git a/src/components/Dashboard/common/AuditLogTable.jsx b/src/components/Dashboard/common/AuditLogTable.jsx index 5a22f06..a486286 100644 --- a/src/components/Dashboard/common/AuditLogTable.jsx +++ b/src/components/Dashboard/common/AuditLogTable.jsx @@ -5,6 +5,7 @@ import IdText from './IdText' import { AuditOutlined, LoadingOutlined } from '@ant-design/icons' import TimeDisplay from '../common/TimeDisplay' import BoolDisplay from './BoolDisplay' +import StateTag from './StateTag' const { Text } = Typography @@ -33,9 +34,7 @@ const formatValue = (value, propertyName) => { } if (propertyName === 'state' && typeof value === 'object' && value.type) { - return ( - {value.type.charAt(0).toUpperCase() + value.type.slice(1)} - ) + return } // Check if the value is a timestamp (ISO date string) diff --git a/src/components/Dashboard/common/FilamentSelect.jsx b/src/components/Dashboard/common/FilamentSelect.jsx index 55d15f8..313593d 100644 --- a/src/components/Dashboard/common/FilamentSelect.jsx +++ b/src/components/Dashboard/common/FilamentSelect.jsx @@ -28,9 +28,4 @@ FilamentSelect.propTypes = { useFilter: PropTypes.bool } -FilamentSelect.defaultProps = { - filter: {}, - useFilter: false -} - export default FilamentSelect diff --git a/src/components/Dashboard/common/JobState.jsx b/src/components/Dashboard/common/JobState.jsx index 76f28c5..96afec5 100644 --- a/src/components/Dashboard/common/JobState.jsx +++ b/src/components/Dashboard/common/JobState.jsx @@ -1,8 +1,9 @@ import PropTypes from 'prop-types' -import { Badge, Progress, Flex, Typography, Tag, Space } from 'antd' +import { Progress, Flex, Typography, Space } from 'antd' import React, { useState, useContext, useEffect } from 'react' import { SocketContext } from '../context/SocketContext' import IdText from './IdText' +import StateTag from './StateTag' const JobState = ({ job, @@ -12,13 +13,12 @@ const JobState = ({ showQuantity = true }) => { const { socket } = useContext(SocketContext) - const [badgeStatus, setBadgeStatus] = useState('default') - const [badgeText, setBadgeText] = useState('Unknown') const [currentState, setCurrentState] = useState( job?.state || { type: 'unknown', progress: 0 } ) const [initialized, setInitialized] = useState(false) const { Text } = Typography + useEffect(() => { if (socket && !initialized && job?._id) { setInitialized(true) @@ -35,38 +35,6 @@ const JobState = ({ } }, [socket, initialized, job?._id]) - useEffect(() => { - switch (currentState?.type) { - case 'draft': - setBadgeStatus('default') - setBadgeText('Draft') - break - case 'printing': - setBadgeStatus('processing') - setBadgeText('Printing') - break - case 'complete': - setBadgeStatus('success') - setBadgeText('Complete') - break - case 'failed': - setBadgeStatus('error') - setBadgeText('Failed') - break - case 'queued': - setBadgeStatus('warning') - setBadgeText('Queued') - break - case 'paused': - setBadgeStatus('warning') - setBadgeText('Paused') - break - default: - setBadgeStatus('default') - setBadgeText('Unknown') - } - }, [currentState]) - return ( {showId && ( @@ -75,12 +43,7 @@ const JobState = ({ {showQuantity && ({job.quantity})} {showStatus && ( - - - - {badgeText} - - + )} {showProgress && diff --git a/src/components/Dashboard/common/ObjectSelect.jsx b/src/components/Dashboard/common/ObjectSelect.jsx index 37fa708..9c906c0 100644 --- a/src/components/Dashboard/common/ObjectSelect.jsx +++ b/src/components/Dashboard/common/ObjectSelect.jsx @@ -4,8 +4,9 @@ import { TreeSelect, Typography, Flex, Badge } from 'antd' import axios from 'axios' import { getTypeMeta } from '../utils/Utils' import IdText from './IdText' +import CountryDisplay from './CountryDisplay' const { Text } = Typography - +const { SHOW_CHILD } = TreeSelect /** * ObjectSelect - a generic, reusable async TreeSelect for hierarchical object selection. * @@ -14,9 +15,10 @@ const { Text } = Typography * - 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 or a simple value + * - 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 = ({ @@ -27,13 +29,14 @@ const ObjectSelect = ({ value, onChange, showSearch = false, + treeCheckable = false, treeSelectProps = {}, type = 'unknown', ...rest }) => { const [treeData, setTreeData] = useState([]) const [loading, setLoading] = useState(false) - const [defaultValue, setDefaultValue] = useState(value) + const [defaultValue, setDefaultValue] = useState(treeCheckable ? [] : value) const [searchValue, setSearchValue] = useState('') const [error, setError] = useState(false) @@ -102,7 +105,12 @@ const ObjectSelect = ({ const renderTitle = useCallback( (item, isLeaf) => { if (!isLeaf) { - // For category nodes, just show the value + // For category nodes, check if it's a country property + const currentProperty = propertyOrder[item.propertyId] + if (currentProperty === 'country' && item.value) { + return + } + // For other category nodes, just show the value return {item[propertyOrder[item.propertyId]] || item.value} } // For leaf nodes, show icon, name, and id @@ -129,8 +137,9 @@ const ObjectSelect = ({ let currentPId = 0 // Build category nodes for each property level and load all available options - for (let i = 0; i < propertyOrder.length - 1; i++) { + for (let i = 0; i < propertyOrder.length; i++) { const propertyName = propertyOrder[i] + console.log('propname', propertyName) let propertyValue // Handle nested property access (e.g., 'filament.diameter') @@ -372,9 +381,25 @@ const ObjectSelect = ({ // OnChange handler const handleOnChange = (val, selectedOptions) => { if (onChange) { - // Find the raw object for the selected value - const node = treeData.find((n) => n.value === val) - onChange(node ? node.raw : val, selectedOptions) + if (treeCheckable) { + // Handle multiple selections with checkboxes + const selectedObjects = [] + if (Array.isArray(val)) { + val.forEach((selectedValue) => { + const node = treeData.find((n) => n.value === selectedValue) + if (node) { + selectedObjects.push(node.raw) + } else { + selectedObjects.push(selectedValue) + } + }) + } + onChange(selectedObjects, selectedOptions) + } else { + // Handle single selection + const node = treeData.find((n) => n.value === val) + onChange(node ? node.raw : val, selectedOptions) + } } console.log('val', val) setDefaultValue(val) @@ -388,29 +413,56 @@ const ObjectSelect = ({ // Keep defaultValue in sync and handle object values useEffect(() => { - if (value?._id) { - setDefaultValue(value._id) - } + if (treeCheckable) { + // Handle array of values for multi-select + if (Array.isArray(value)) { + const valueIds = value.map((v) => v._id || v.id || v) + setDefaultValue(valueIds) - // Check if value is an object with _id (default object case) - if (value && typeof value === 'object' && value._id) { - // If we already have this object loaded, don't fetch again - const existingNode = treeData.find((node) => node.value === value._id) - if (!existingNode) { - fetchObjectById(value._id).then((object) => { - if (object) { - buildTreePathForObject(object) + // Load tree paths for any objects that aren't already loaded + 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) { + buildTreePathForObject(object) + } + }) + } } }) + } else { + setDefaultValue([]) + } + } else { + // Handle single value + if (value?._id) { + setDefaultValue(value._id) + } + + // Check if value is an object with _id (default object case) + if (value && typeof value === 'object' && value._id) { + // If we already have this object loaded, don't fetch again + const existingNode = treeData.find((node) => node.value === value._id) + if (!existingNode) { + fetchObjectById(value._id).then((object) => { + if (object) { + buildTreePathForObject(object) + } + }) + } } } - }, [value, treeData, fetchObjectById, buildTreePathForObject]) + }, [value, treeData, fetchObjectById, buildTreePathForObject, treeCheckable]) // Initial load useEffect(() => { if (treeData.length === 0 && !error && !loading) { // If we have a default object value, don't load the regular tree - if (value && typeof value === 'object' && value._id) { + if (!treeCheckable && value && typeof value === 'object' && value._id) { return } @@ -429,7 +481,8 @@ const ObjectSelect = ({ handleTreeLoad, error, loading, - value + value, + treeCheckable ]) return error ? ( @@ -455,6 +508,8 @@ const ObjectSelect = ({ value={defaultValue} showSearch={showSearch} onSearch={showSearch ? handleSearch : undefined} + treeCheckable={treeCheckable} + showCheckedStrategy={treeCheckable ? SHOW_CHILD : undefined} {...treeSelectProps} {...rest} /> @@ -469,6 +524,7 @@ ObjectSelect.propTypes = { value: PropTypes.any, onChange: PropTypes.func, showSearch: PropTypes.bool, + treeCheckable: PropTypes.bool, treeSelectProps: PropTypes.object, type: PropTypes.string.isRequired } diff --git a/src/components/Dashboard/common/PrinterSelect.jsx b/src/components/Dashboard/common/PrinterSelect.jsx index 11710f9..373cbee 100644 --- a/src/components/Dashboard/common/PrinterSelect.jsx +++ b/src/components/Dashboard/common/PrinterSelect.jsx @@ -1,36 +1,18 @@ // PrinterSelect.js import React from 'react' import PropTypes from 'prop-types' -import { Tag } from 'antd' import config from '../../../config' import ObjectSelect from './ObjectSelect' -import PrinterState from './PrinterState' const PrinterSelect = ({ onChange, disabled }) => { - // getTitle: if isLeaf, render PrinterState, else render Tag or 'Untagged' - const getTitle = (item, isLeaf) => - isLeaf ? ( - - ) : item === 'Untagged' ? ( - 'Untagged' - ) : ( - {item} - ) - - // getValue/getKey: for leaf, use _id; for tag, use tag string - const getValue = (item, isLeaf) => (isLeaf ? item._id : item) - const getKey = (item, isLeaf) => (isLeaf ? item._id : item) - return ( ) } diff --git a/src/components/Dashboard/common/PrinterState.jsx b/src/components/Dashboard/common/PrinterState.jsx index e4f05e9..df93d49 100644 --- a/src/components/Dashboard/common/PrinterState.jsx +++ b/src/components/Dashboard/common/PrinterState.jsx @@ -1,20 +1,12 @@ // PrinterSelect.js import PropTypes from 'prop-types' -import { - Badge, - Progress, - Flex, - Space, - Tag, - Typography, - Button, - Tooltip -} from 'antd' +import { Progress, Flex, Space, Typography, Button, Tooltip } from 'antd' import React, { useState, useContext, useEffect } from 'react' import { SocketContext } from '../context/SocketContext' import { CaretLeftOutlined } from '@ant-design/icons' import XMarkIcon from '../../Icons/XMarkIcon' import PauseIcon from '../../Icons/PauseIcon' +import StateTag from './StateTag' const PrinterState = ({ printer, @@ -24,8 +16,6 @@ const PrinterState = ({ showControls = true }) => { const { socket } = useContext(SocketContext) - const [badgeStatus, setBadgeStatus] = useState('unknown') - const [badgeText, setBadgeText] = useState('Unknown') const [currentState, setCurrentState] = useState( printer?.state || { type: 'unknown', @@ -51,81 +41,12 @@ const PrinterState = ({ } }, [socket, initialized, printer?.id]) - useEffect(() => { - switch (currentState.type) { - case 'online': - setBadgeStatus('success') - setBadgeText('Online') - break - case 'standby': - setBadgeStatus('success') - setBadgeText('Standby') - break - case 'complete': - setBadgeStatus('success') - setBadgeText('Complete') - break - case 'offline': - setBadgeStatus('default') - setBadgeText('Offline') - break - case 'shutdown': - setBadgeStatus('default') - setBadgeText('Shutdown') - break - case 'initializing': - setBadgeStatus('warning') - setBadgeText('Initializing') - break - case 'printing': - setBadgeStatus('processing') - setBadgeText('Printing') - break - case 'paused': - setBadgeStatus('warning') - setBadgeText('Paused') - break - case 'cancelled': - setBadgeStatus('warning') - setBadgeText('Cancelled') - break - case 'loading': - setBadgeStatus('processing') - setBadgeText('Uploading') - break - case 'processing': - setBadgeStatus('processing') - setBadgeText('Processing') - break - case 'ready': - setBadgeStatus('success') - setBadgeText('Ready') - break - case 'error': - setBadgeStatus('error') - setBadgeText('Error') - break - case 'startup': - setBadgeStatus('warning') - setBadgeText('Startup') - break - default: - setBadgeStatus('default') - setBadgeText(currentState.type) - } - }, [currentState]) - return ( {showPrinterName && {printer.name}} {showStatus && ( - - - - {badgeText} - - + )} {showProgress && diff --git a/src/components/Dashboard/common/StateTag.jsx b/src/components/Dashboard/common/StateTag.jsx new file mode 100644 index 0000000..e41c49a --- /dev/null +++ b/src/components/Dashboard/common/StateTag.jsx @@ -0,0 +1,103 @@ +import PropTypes from 'prop-types' +import { Badge, Flex, Tag } from 'antd' +import React, { useMemo } from 'react' + +const StateTag = ({ state, showBadge = true, style = {} }) => { + const { badgeStatus, badgeText } = useMemo(() => { + let status = 'default' + let text = 'Unknown' + + switch (state) { + case 'online': + status = 'success' + text = 'Online' + break + case 'standby': + status = 'success' + text = 'Standby' + break + case 'complete': + status = 'success' + text = 'Complete' + break + case 'offline': + status = 'default' + text = 'Offline' + break + case 'shutdown': + status = 'default' + text = 'Shutdown' + break + case 'initializing': + status = 'warning' + text = 'Initializing' + break + case 'printing': + status = 'processing' + text = 'Printing' + break + case 'paused': + status = 'warning' + text = 'Paused' + break + case 'cancelled': + status = 'error' + text = 'Cancelled' + break + case 'loading': + status = 'processing' + text = 'Uploading' + break + case 'processing': + status = 'processing' + text = 'Processing' + break + case 'ready': + status = 'success' + text = 'Ready' + break + case 'error': + status = 'error' + text = 'Error' + break + case 'startup': + status = 'warning' + text = 'Startup' + break + case 'draft': + status = 'default' + text = 'Draft' + break + case 'failed': + status = 'error' + text = 'Failed' + break + case 'queued': + status = 'warning' + text = 'Queued' + break + default: + status = 'default' + text = state || 'Unknown' + } + + return { badgeStatus: status, badgeText: text } + }, [state]) + + return ( + + + {showBadge && } + {badgeText} + + + ) +} + +StateTag.propTypes = { + state: PropTypes.string, + showBadge: PropTypes.bool, + style: PropTypes.object +} + +export default StateTag diff --git a/src/components/Dashboard/common/SubJobState.jsx b/src/components/Dashboard/common/SubJobState.jsx index ca0f0a6..ea90db0 100644 --- a/src/components/Dashboard/common/SubJobState.jsx +++ b/src/components/Dashboard/common/SubJobState.jsx @@ -1,9 +1,10 @@ import PropTypes from 'prop-types' -import { Badge, Progress, Flex, Button, Space, Tag, Tooltip } from 'antd' // eslint-disable-line +import { Progress, Flex, Button, Space, Tooltip } from 'antd' // eslint-disable-line import { CaretLeftOutlined } from '@ant-design/icons' // eslint-disable-line import React, { useState, useContext, useEffect } from 'react' import { SocketContext } from '../context/SocketContext' import IdText from './IdText' +import StateTag from './StateTag' import XMarkIcon from '../../Icons/XMarkIcon' import PauseIcon from '../../Icons/PauseIcon' import BinIcon from '../../Icons/BinIcon' @@ -16,8 +17,6 @@ const SubJobState = ({ showControls = true //eslint-disable-line }) => { const { socket } = useContext(SocketContext) - const [badgeStatus, setBadgeStatus] = useState('unknown') - const [badgeText, setBadgeText] = useState('Unknown') const [currentState, setCurrentState] = useState( subJob?.state || { type: 'unknown', @@ -45,42 +44,6 @@ const SubJobState = ({ } }, [socket, initialized, subJob?._id]) - useEffect(() => { - switch (currentState.type) { - case 'draft': - setBadgeStatus('default') - setBadgeText('Draft') - break - case 'printing': - setBadgeStatus('processing') - setBadgeText('Printing') - break - case 'complete': - setBadgeStatus('success') - setBadgeText('Complete') - break - case 'failed': - setBadgeStatus('error') - setBadgeText('Failed') - break - case 'queued': - setBadgeStatus('warning') - setBadgeText('Queued') - break - case 'paused': - setBadgeStatus('warning') - setBadgeText('Paused') - break - case 'cancelled': - setBadgeStatus('error') - setBadgeText('Cancelled') - break - default: - setBadgeStatus('default') - setBadgeText('Unknown') - } - }, [currentState]) - return ( {showId && ( @@ -88,12 +51,7 @@ const SubJobState = ({ )} {showStatus && ( - - - - {badgeText} - - + )} {showProgress && diff --git a/src/components/Dashboard/common/VendorSelect.jsx b/src/components/Dashboard/common/VendorSelect.jsx index 424b442..9e79388 100644 --- a/src/components/Dashboard/common/VendorSelect.jsx +++ b/src/components/Dashboard/common/VendorSelect.jsx @@ -1,188 +1,30 @@ -import { TreeSelect, Space } from 'antd' -import React, { useEffect, useState } from 'react' +import React from 'react' import PropTypes from 'prop-types' -import axios from 'axios' -import CountryDisplay from './CountryDisplay' -import VendorIcon from '../../Icons/VendorIcon' import config from '../../../config' +import ObjectSelect from './ObjectSelect' const propertyOrder = ['country'] const VendorSelect = ({ onChange, filter = {}, useFilter = false, value }) => { - const [vendorsTreeData, setVendorsTreeData] = useState([]) - const [loading, setLoading] = useState(true) - const [defaultValue, setDefaultValue] = useState(null) - - const fetchVendorsData = async (property, filter) => { - setLoading(true) - try { - const response = await axios.get(`${config.backendUrl}/vendors`, { - params: { - ...filter, - property - }, - headers: { - Accept: 'application/json' - }, - withCredentials: true - }) - setLoading(false) - return response.data - } catch (err) { - console.error(err) - } - } - - const getFilter = (node) => { - var filter = {} - var currentId = node.id - while (currentId != 0) { - const currentNode = vendorsTreeData.filter( - (treeData) => treeData['id'] === currentId - )[0] - filter[propertyOrder[currentNode.propertyId]] = - currentNode.value.split('-')[0] - currentId = currentNode.pId - } - return filter - } - - const generateVendorTreeNodes = async (node = null, filter = null) => { - if (!node) { - return - } - - if (filter === null) { - filter = getFilter(node) - } - - const vendorData = await fetchVendorsData(null, filter) - - let newNodeList = [] - - for (const vendor of vendorData) { - const random = Math.random().toString(36).substring(2, 6) - - const newNode = { - id: random, - pId: node.id, - value: vendor._id, - vendor: vendor, - key: vendor._id, - title: ( - - - {vendor.name} - - ), - isLeaf: true - } - - newNodeList.push(newNode) - } - - setVendorsTreeData(vendorsTreeData.concat(newNodeList)) - } - - const generateVendorCategoryTreeNodes = async (node = null) => { - var filter = {} - - var propertyId = 0 - - if (!node) { - node = {} - node.id = 0 - } else { - filter = getFilter(node) - propertyId = node.propertyId + 1 - } - - const propertyName = propertyOrder[propertyId] - - const propertyData = await fetchVendorsData(propertyName, filter) - - const newNodeList = [] - - for (const item of propertyData) { - const property = item[propertyName] - const random = Math.random().toString(36).substring(2, 6) - - const newNode = { - id: random, - pId: node.id, - value: property + '-' + random, - key: property + '-' + random, - propertyId: propertyId, - title: , - isLeaf: false, - selectable: false - } - - newNodeList.push(newNode) - } - - setVendorsTreeData(vendorsTreeData.concat(newNodeList)) - } - - const handleVendorsTreeLoad = async (node) => { - if (node) { - if (node.propertyId !== propertyOrder.length - 1) { - await generateVendorCategoryTreeNodes(node) - } else { - await generateVendorTreeNodes(node) // End of properties - } - } else { - await generateVendorCategoryTreeNodes(null) // First property - } - } - - const handleOnChange = (value, selectedOptions) => { - const vendorObject = vendorsTreeData.find( - (node) => node.value === value - )?.vendor - onChange(vendorObject, selectedOptions) - } - - useEffect(() => { - setVendorsTreeData([]) - }, []) - - useEffect(() => { - if (vendorsTreeData.length === 0) { - if (useFilter === true) { - generateVendorTreeNodes({ id: 0 }, filter) - } else { - handleVendorsTreeLoad(null) - } - } - }, [vendorsTreeData]) - - useEffect(() => { - console.log('value', value) - if (value?.name) { - setDefaultValue(value.name) - } - }, [value]) - return ( - ) } VendorSelect.propTypes = { onChange: PropTypes.func, + value: PropTypes.object, filter: PropTypes.object, - useFilter: PropTypes.bool, - value: PropTypes.object + useFilter: PropTypes.bool } export default VendorSelect