diff --git a/public/favicon.ico b/public/favicon.ico index a11777c..bfc29ee 100644 Binary files a/public/favicon.ico and b/public/favicon.ico differ diff --git a/public/logo512.png b/public/logo512.png index a4e47a6..c1e5f1e 100644 Binary files a/public/logo512.png and b/public/logo512.png differ diff --git a/src/assets/logos/farmcontrolicon.afdesign b/src/assets/logos/farmcontrolicon.afdesign new file mode 100644 index 0000000..8e868d4 Binary files /dev/null and b/src/assets/logos/farmcontrolicon.afdesign differ diff --git a/src/assets/logos/farmcontrolicon.png b/src/assets/logos/farmcontrolicon.png new file mode 100644 index 0000000..afff5c9 Binary files /dev/null and b/src/assets/logos/farmcontrolicon.png differ diff --git a/src/assets/logos/farmcontroliconfill.afdesign b/src/assets/logos/farmcontroliconfill.afdesign new file mode 100644 index 0000000..cfb47f1 Binary files /dev/null and b/src/assets/logos/farmcontroliconfill.afdesign differ diff --git a/src/assets/logos/farmcontroliconfill.png b/src/assets/logos/farmcontroliconfill.png new file mode 100644 index 0000000..5f46ff5 Binary files /dev/null and b/src/assets/logos/farmcontroliconfill.png differ diff --git a/src/components/Dashboard/Inventory/FilamentStocks.jsx b/src/components/Dashboard/Inventory/FilamentStocks.jsx index de83cc2..9517b56 100644 --- a/src/components/Dashboard/Inventory/FilamentStocks.jsx +++ b/src/components/Dashboard/Inventory/FilamentStocks.jsx @@ -29,7 +29,7 @@ import TimeDisplay from '../common/TimeDisplay' import XMarkIcon from '../../Icons/XMarkIcon' import CheckIcon from '../../Icons/CheckIcon' import useColumnVisibility from '../hooks/useColumnVisibility' -import DashboardTable from '../common/DashboardTable' +import ObjectTable from '../common/ObjectTable' import ListIcon from '../../Icons/ListIcon' import GridIcon from '../../Icons/GridIcon' import useViewMode from '../hooks/useViewMode' @@ -326,7 +326,7 @@ const FilamentStocks = () => { - { key='notes' > - + diff --git a/src/components/Dashboard/Inventory/PartStocks.jsx b/src/components/Dashboard/Inventory/PartStocks.jsx index fafe6b4..368633b 100644 --- a/src/components/Dashboard/Inventory/PartStocks.jsx +++ b/src/components/Dashboard/Inventory/PartStocks.jsx @@ -14,7 +14,7 @@ import PlusIcon from '../../Icons/PlusIcon' import ReloadIcon from '../../Icons/ReloadIcon' import PartStockState from '../common/PartStockState' import TimeDisplay from '../common/TimeDisplay' -import DashboardTable from '../common/DashboardTable' +import ObjectTable from '../common/ObjectTable' import config from '../../../config' @@ -176,7 +176,7 @@ const PartStocks = () => { - { - { fixed: 'left', sorter: true, render: (type) => { - return {getTypeMeta(type?.toLowerCase()).title} + return {getModelByName(type).title} }, filterDropdown: ({ setSelectedKeys, @@ -128,7 +128,7 @@ const StockEvents = () => { ) : null} {record.subJob?.number ? ( @@ -310,7 +310,7 @@ const StockEvents = () => { - { - { loading={loading} indicator={} isEditing={isEditing} - items={[ - { - name: 'id', - label: 'ID', - value: objectData?._id, - type: 'id', - objectType: 'filament', - showCopy: true - }, - { - name: 'createdAt', - label: 'Created At', - value: objectData?.createdAt, - type: 'dateTime', - readOnly: true - }, - { - name: 'name', - label: 'Name', - value: objectData?.name, - required: true, - type: 'text' - }, - { - name: 'updatedAt', - label: 'Updated At', - value: objectData?.updatedAt, - type: 'dateTime', - readOnly: true - }, - { - name: 'vendor', - label: 'Vendor', - value: objectData?.vendor, - required: true, - type: 'object', - objectType: 'vendor' - }, - { - name: 'vendorId', - label: 'Vendor ID', - value: objectData?.vendor?.id, - type: 'id', - objectType: 'vendor', - showCopy: true, - showHyperlink: true - }, - { - name: 'type', - label: 'Material', - value: objectData?.type, - required: true, - type: 'material' - }, - { - name: 'cost', - label: 'Cost', - value: objectData?.cost, - required: true, - type: 'currency' - }, - { - name: 'color', - label: 'Color', - value: objectData?.color, - required: true, - type: 'color' - }, - { - name: 'diameter', - label: 'Diameter', - value: objectData?.diameter, - required: true, - type: 'mm' - }, - { - name: 'density', - label: 'Density', - value: objectData?.density, - required: true, - type: 'density' - }, - { - name: 'url', - label: 'URL', - value: objectData?.url, - type: 'text' - }, - { - name: 'barcode', - label: 'Barcode', - value: objectData?.barcode, - type: 'text' - } - ]} + items={getModelProperties('filament').map((prop) => ({ + ...prop, + value: getPropertyValue(objectData, prop.name) + }))} /> @@ -221,7 +133,7 @@ const FilamentInfo = () => { key='notes' > - + diff --git a/src/components/Dashboard/Management/NoteTypes.jsx b/src/components/Dashboard/Management/NoteTypes.jsx index 6c8802a..eee9ab3 100644 --- a/src/components/Dashboard/Management/NoteTypes.jsx +++ b/src/components/Dashboard/Management/NoteTypes.jsx @@ -17,7 +17,7 @@ import { AuthContext } from '../context/AuthContext' import IdDisplay from '../common/IdDisplay' import NewNoteType from './NoteTypes/NewNoteType' import TimeDisplay from '../common/TimeDisplay' -import DashboardTable from '../common/DashboardTable' +import ObjectTable from '../common/ObjectTable' import PlusIcon from '../../Icons/PlusIcon' import ReloadIcon from '../../Icons/ReloadIcon' import XMarkIcon from '../../Icons/XMarkIcon' @@ -304,7 +304,7 @@ const NoteTypes = () => { /> - { /> - { key='notes' > - + diff --git a/src/components/Dashboard/Management/Products.jsx b/src/components/Dashboard/Management/Products.jsx index f529f74..56efbed 100644 --- a/src/components/Dashboard/Management/Products.jsx +++ b/src/components/Dashboard/Management/Products.jsx @@ -19,7 +19,7 @@ import { DownloadOutlined } from '@ant-design/icons' import { AuthContext } from '../context/AuthContext' import IdDisplay from '../common/IdDisplay' import TimeDisplay from '../common/TimeDisplay' -import DashboardTable from '../common/DashboardTable' +import ObjectTable from '../common/ObjectTable' import NewProduct from './Products/NewProduct' import ProductIcon from '../../Icons/ProductIcon' import InfoCircleIcon from '../../Icons/InfoCircleIcon' @@ -353,7 +353,7 @@ const Products = () => { /> - { key='notes' > - + diff --git a/src/components/Dashboard/Management/Users.jsx b/src/components/Dashboard/Management/Users.jsx index 5c83815..1a42c56 100644 --- a/src/components/Dashboard/Management/Users.jsx +++ b/src/components/Dashboard/Management/Users.jsx @@ -14,7 +14,7 @@ import { ExportOutlined } from '@ant-design/icons' import { AuthContext } from '../context/AuthContext' import IdDisplay from '../common/IdDisplay' import TimeDisplay from '../common/TimeDisplay' -import DashboardTable from '../common/DashboardTable' +import ObjectTable from '../common/ObjectTable' import PersonIcon from '../../Icons/PersonIcon' import ReloadIcon from '../../Icons/ReloadIcon' import XMarkIcon from '../../Icons/XMarkIcon' @@ -367,7 +367,7 @@ const Users = () => { /> - { key='notes' > - + diff --git a/src/components/Dashboard/Management/Vendors.jsx b/src/components/Dashboard/Management/Vendors.jsx index 4c99876..cb9db33 100644 --- a/src/components/Dashboard/Management/Vendors.jsx +++ b/src/components/Dashboard/Management/Vendors.jsx @@ -18,7 +18,7 @@ import IdDisplay from '../common/IdDisplay' import NewVendor from './Vendors/NewVendor' import CountryDisplay from '../common/CountryDisplay' import TimeDisplay from '../common/TimeDisplay' -import DashboardTable from '../common/DashboardTable' +import ObjectTable from '../common/ObjectTable' import VendorIcon from '../../Icons/VendorIcon' import PlusIcon from '../../Icons/PlusIcon' import ReloadIcon from '../../Icons/ReloadIcon' @@ -363,7 +363,7 @@ const Vendors = () => { /> - { loading={loading} indicator={} isEditing={isEditing} - items={[ - { - name: 'id', - label: 'ID', - value: objectData?._id, - type: 'id', - objectType: 'vendor', - showCopy: true - }, - { - name: 'createdAt', - label: 'Created At', - value: objectData?.createdAt, - type: 'dateTime', - readOnly: true - }, - { - name: 'name', - label: 'Name', - value: objectData?.name, - required: true, - type: 'text' - }, - { - name: 'updatedAt', - label: 'Updated At', - value: objectData?.updatedAt, - type: 'dateTime', - readOnly: true - }, - { - name: 'website', - label: 'Website', - value: objectData?.website, - type: 'url' - }, - { - name: 'country', - label: 'Country', - value: objectData?.country, - type: 'country' - }, - { - name: 'contact', - label: 'Contact', - value: objectData?.contact, - type: 'text' - }, - { - name: 'phone', - label: 'Phone', - value: objectData?.phone, - type: 'text' - }, - { - name: 'email', - label: 'Email', - value: objectData?.email, - type: 'email' - } - ]} + items={getModelProperties('vendor').map((prop) => ({ + ...prop, + value: getPropertyValue(objectData, prop.name) + }))} /> @@ -187,7 +133,7 @@ const VendorInfo = () => { key='notes' > - + diff --git a/src/components/Dashboard/Production/GCodeFiles.jsx b/src/components/Dashboard/Production/GCodeFiles.jsx index 090a542..fed5181 100644 --- a/src/components/Dashboard/Production/GCodeFiles.jsx +++ b/src/components/Dashboard/Production/GCodeFiles.jsx @@ -30,7 +30,7 @@ import ReloadIcon from '../../Icons/ReloadIcon' import XMarkIcon from '../../Icons/XMarkIcon' import CheckIcon from '../../Icons/CheckIcon' import TimeDisplay from '../common/TimeDisplay' -import DashboardTable from '../common/DashboardTable' +import ObjectTable from '../common/ObjectTable' import ListIcon from '../../Icons/ListIcon' import GridIcon from '../../Icons/GridIcon' import useViewMode from '../hooks/useViewMode' @@ -381,7 +381,7 @@ const GCodeFiles = () => { /> - { loading={loading} indicator={} isEditing={isEditing} - items={[ - { - name: '_id', - label: 'ID', - type: 'id', - objectType: 'gcodefile', - value: objectData?._id, - showCopy: true - }, - { - name: 'createdAt', - label: 'Created At', - type: 'dateTime', - value: objectData?.createdAt, - readOnly: true - }, - { - name: 'name', - label: 'Name', - type: 'text', - value: objectData?.name, - required: true - }, - { - name: 'updatedAt', - label: 'Updated At', - type: 'dateTime', - value: objectData?.updatedAt, - readOnly: true - }, - { - name: 'filament', - label: 'Filament', - type: 'object', - value: objectData?.filament, - objectType: 'filament', - required: true - }, - { - name: 'cost', - label: 'Cost', - type: 'currency', - value: objectData?.cost, - readOnly: true - }, - { - name: [ - 'gcodeFileInfo', - 'estimatedPrintingTimeNormalMode' - ], - label: 'Est Print Time', - value: - objectData?.gcodeFileInfo - ?.estimatedPrintingTimeNormalMode, - type: 'text', - readOnly: true - }, - { - name: ['gcodeFileInfo', 'sparseInfillDensity'], - label: 'Infill Density', - value: objectData?.gcodeFileInfo?.sparseInfillDensity, - type: 'number', - readOnly: true - }, - { - name: ['gcodeFileInfo', 'sparseInfillPattern'], - label: 'Infill Pattern', - value: objectData?.gcodeFileInfo?.sparseInfillPattern, - type: 'text', - readOnly: true - }, - { - name: ['gcodeFileInfo', 'filamentUsedMm'], - label: 'Filament Used (mm)', - value: objectData?.gcodeFileInfo?.filamentUsedMm, - type: 'mm', - readOnly: true - }, - { - name: ['gcodeFileInfo', 'filamentUsedG'], - label: 'Filament Used (g)', - value: objectData?.gcodeFileInfo?.filamentUsedG, - type: 'weight', - readOnly: true - }, - { - name: ['gcodeFileInfo', 'nozzleTemperature'], - label: 'Hotend Temperature', - value: objectData?.gcodeFileInfo?.nozzleTemperature, - type: 'number', - readOnly: true - }, - { - name: ['gcodeFileInfo', 'hotPlateTemp'], - label: 'Bed Temperature', - value: objectData?.gcodeFileInfo?.hotPlateTemp, - type: 'number', - readOnly: true - }, - { - name: ['gcodeFileInfo', 'filamentSettingsId'], - label: 'Filament Profile', - value: objectData?.gcodeFileInfo?.filamentSettingsId, - type: 'text', - readOnly: true - }, - { - name: ['gcodeFileInfo', 'printSettingsId'], - label: 'Print Profile', - value: objectData?.gcodeFileInfo?.printSettingsId, - type: 'text', - readOnly: true - } - ]} + items={getModelProperties('gcodeFile').map((prop) => ({ + ...prop, + value: getPropertyValue(objectData, prop.name) + }))} objectData={objectData} type='gcodefile' /> @@ -266,7 +160,7 @@ const GCodeFileInfo = () => { key='notes' > - + diff --git a/src/components/Dashboard/Production/Jobs.jsx b/src/components/Dashboard/Production/Jobs.jsx index 5254c3e..18c3506 100644 --- a/src/components/Dashboard/Production/Jobs.jsx +++ b/src/components/Dashboard/Production/Jobs.jsx @@ -36,7 +36,7 @@ import CheckCircleIcon from '../../Icons/CheckCircleIcon.jsx' import PauseCircleIcon from '../../Icons/PauseCircleIcon.jsx' import XMarkCircleIcon from '../../Icons/XMarkCircleIcon.jsx' import QuestionCircleIcon from '../../Icons/QuestionCircleIcon.jsx' -import DashboardTable from '../common/DashboardTable' +import ObjectTable from '../common/ObjectTable.jsx' import ListIcon from '../../Icons/ListIcon.jsx' import GridIcon from '../../Icons/GridIcon.jsx' import useViewMode from '../hooks/useViewMode.js' @@ -146,9 +146,10 @@ const Jobs = () => { { title: 'State', key: 'state', + dataIndex: 'state', width: 240, - render: (record) => { - return + render: (state) => { + return }, filterDropdown: ({ setSelectedKeys, @@ -393,7 +394,7 @@ const Jobs = () => { - { const location = useLocation() @@ -114,72 +118,10 @@ const JobInfo = () => { indicator={} isEditing={isEditing} type='job' - items={[ - { - name: '_id', - label: 'ID', - value: objectData?._id, - type: 'id', - objectType: 'job', - showCopy: true - }, - { - name: 'state', - label: 'Status', - value: objectData, - type: 'state', - objectType: 'job', - showStatus: true, - showProgress: true, - showId: false, - showQuantity: false, - readOnly: true - }, - { - name: 'gcodeFile', - label: 'GCode File', - value: objectData?.gcodeFile, - type: 'object', - objectType: 'gcodeFile', - readOnly: true - }, - { - name: 'gcodeFileId', - label: 'GCode File ID', - value: objectData?.gcodeFile?._id, - type: 'id', - objectType: 'gcodefile', - showHyperlink: true - }, - { - name: 'quantity', - label: 'Quantity', - value: objectData?.quantity, - type: 'number', - readOnly: true - }, - { - name: 'createdAt', - label: 'Created At', - value: objectData?.createdAt, - type: 'dateTime', - readOnly: true - }, - { - name: 'startedAt', - label: 'Started At', - value: objectData?.startedAt, - type: 'dateTime', - readOnly: true - }, - { - name: 'assignedPrinters', - label: 'Assigned Printers', - value: objectData?.printers?.length, - type: 'number', - readOnly: true - } - ]} + items={getModelProperties('job').map((prop) => ({ + ...prop, + value: getPropertyValue(objectData, prop.name) + }))} /> @@ -203,7 +145,7 @@ const JobInfo = () => { key='notes' > - + diff --git a/src/components/Dashboard/Production/Printers.jsx b/src/components/Dashboard/Production/Printers.jsx index 674197a..f88c1d7 100644 --- a/src/components/Dashboard/Production/Printers.jsx +++ b/src/components/Dashboard/Production/Printers.jsx @@ -26,7 +26,7 @@ import PlusIcon from '../../Icons/PlusIcon' import ReloadIcon from '../../Icons/ReloadIcon' import XMarkIcon from '../../Icons/XMarkIcon' import CheckIcon from '../../Icons/CheckIcon' -import DashboardTable from '../common/DashboardTable' +import ObjectTable from '../common/ObjectTable' import config from '../../../config' import GridIcon from '../../Icons/GridIcon' @@ -83,15 +83,12 @@ const Printers = () => { }, { title: 'State', + dataIndex: 'state', key: 'state', width: 240, - render: (record) => { + render: (state) => { return ( - + ) } }, @@ -312,7 +309,7 @@ const Printers = () => { - { const location = useLocation() @@ -113,102 +117,10 @@ const PrinterInfo = () => { indicator={} isEditing={isEditing} type='printer' - items={[ - { - name: '_id', - label: 'ID', - value: objectData?._id, - type: 'id', - objectType: 'printer', - showCopy: true - }, - { - name: 'connectedAt', - label: 'Connected At', - value: objectData?.connectedAt, - type: 'dateTime', - readOnly: true - }, - { - name: 'name', - label: 'Name', - value: objectData?.name, - required: true, - type: 'text' - }, - { - name: 'state', - label: 'Status', - value: objectData, - type: 'state', - objectType: 'printer', - showName: false, - readOnly: true - }, - { - name: 'vendor', - label: 'Vendor', - value: objectData?.vendor, - type: 'object', - objectType: 'vendor', - required: true - }, - { - name: ['moonraker', 'host'], - label: 'Host', - value: objectData?.moonraker?.host, - type: 'text', - required: true - }, - { - name: 'vendorId', - label: 'Vendor ID', - value: objectData?.vendor?.id, - type: 'id', - objectType: 'vendor', - showHyperlink: true, - readOnly: true - }, - - { - name: ['moonraker', 'port'], - label: 'Port', - value: objectData?.moonraker?.port, - type: 'number', - required: true - }, - { - name: ['moonraker', 'apiKey'], - label: 'API Key', - value: objectData?.moonraker?.apiKey, - type: 'secret', - reveal: true, - required: false - }, - { - name: ['moonraker', 'protocol'], - label: 'Protocol', - value: objectData?.moonraker?.protocol, - type: 'wsprotocol', - required: true - }, - - { - name: 'tags', - label: 'Tags', - value: objectData?.tags, - type: 'tags', - required: false - }, - { - name: 'firmware', - label: 'Firmware Version', - value: objectData?.firmware, - type: 'text', - required: false, - readOnly: true - } - ]} + items={getModelProperties('printer').map((prop) => ({ + ...prop, + value: getPropertyValue(objectData, prop.name) + }))} /> @@ -233,7 +145,7 @@ const PrinterInfo = () => { key='notes' > - + diff --git a/src/components/Dashboard/common/AuditLogTable.jsx b/src/components/Dashboard/common/AuditLogTable.jsx index d8f670e..e8e9d2d 100644 --- a/src/components/Dashboard/common/AuditLogTable.jsx +++ b/src/components/Dashboard/common/AuditLogTable.jsx @@ -68,7 +68,10 @@ const formatValue = (value, propertyName) => { } const AuditLogTable = forwardRef( - ({ items, loading, showTargetColumn, showOwnerColumn }, ref) => { + ( + { items, loading = false, showTargetColumn = true, showOwnerColumn = true }, + ref + ) => { const [sortedInfo, setSortedInfo] = useState({ columnKey: 'createdAt', order: 'descend' @@ -207,10 +210,4 @@ AuditLogTable.propTypes = { showOwnerColumn: PropTypes.bool } -AuditLogTable.defaultProps = { - loading: false, - showTargetColumn: true, - showOwnerColumn: true -} - export default AuditLogTable diff --git a/src/components/Dashboard/common/ColorSelector.jsx b/src/components/Dashboard/common/ColorSelector.jsx index 1a70638..13f44e4 100644 --- a/src/components/Dashboard/common/ColorSelector.jsx +++ b/src/components/Dashboard/common/ColorSelector.jsx @@ -47,7 +47,7 @@ const ColorSelector = ({ value, onChange, disabled, required = false }) => { ColorSelector.propTypes = { value: PropTypes.string, - onChange: PropTypes.func.isRequired, + onChange: PropTypes.func, disabled: PropTypes.bool, required: PropTypes.bool } diff --git a/src/components/Dashboard/common/FilamentStockSelect.jsx b/src/components/Dashboard/common/FilamentStockSelect.jsx index 8790055..9e056e0 100644 --- a/src/components/Dashboard/common/FilamentStockSelect.jsx +++ b/src/components/Dashboard/common/FilamentStockSelect.jsx @@ -1,126 +1,26 @@ -import { TreeSelect } from 'antd' -import React, { useEffect, useState, useCallback } from 'react' +import React from 'react' import PropTypes from 'prop-types' -import axios from 'axios' import config from '../../../config' +import ObjectSelect from './ObjectSelect' -import FilamentStockDisplay from './FilamentStockDisplay' - -const FilamentStockSelect = ({ onChange, filter, useFilter, value }) => { - const [filamentStocksTreeData, setFilamentStocksTreeData] = useState([]) - const [filamentStocksData, setFilamentStocksData] = useState([]) - const [loading, setLoading] = useState(false) - const [defaultValue, setDefaultValue] = useState(value) - - const getFilamentStockTitle = (filamentStock) => { - return ( - - ) - } - - const fetchFilamentStocksData = async (property, filter) => { - setLoading(true) - try { - const response = await axios.get(`${config.backendUrl}/filamentstocks`, { - params: { - ...filter, - property - }, - headers: { - Accept: 'application/json' - }, - withCredentials: true - }) - setLoading(false) - return response.data - } catch (err) { - console.error(err) - } - } - - const generateFilamentStockTreeNodes = useCallback( - async (node = null, filter = null) => { - if (!node) { - return - } - - const filamentStockData = await fetchFilamentStocksData(null, filter) - setFilamentStocksData(filamentStockData) - - for (const filamentStock of filamentStockData) { - const newNode = { - id: filamentStock._id, - pId: node.id, - value: filamentStock._id, - key: filamentStock._id, - title: getFilamentStockTitle(filamentStock), - isLeaf: true - } - - setFilamentStocksTreeData((prev) => { - const filtered = prev.filter((node) => node.id !== newNode.id) - return [...filtered, newNode] - }) - } - }, - [] - ) - - const handleFilamentStocksTreeLoad = useCallback( - async (node) => { - if (node) { - await generateFilamentStockTreeNodes(node) - } else { - await generateFilamentStockTreeNodes({ id: 0 }) - } - }, - [generateFilamentStockTreeNodes] - ) - - const handleOnChange = (value, selectedOptions) => { - const filamentStockObject = filamentStocksData.filter( - (filamentStock) => filamentStock._id === value - )[0] - - onChange(filamentStockObject, selectedOptions) - } - - useEffect(() => { - if (value?._id != null) { - setDefaultValue(value) - } - }, [value]) - - useEffect(() => { - if (defaultValue != undefined) { - const newNode = { - id: defaultValue._id, - pId: 0, - value: defaultValue._id, - key: defaultValue._id, - title: getFilamentStockTitle(defaultValue), - isLeaf: true - } - setFilamentStocksTreeData([newNode]) - } else { - setFilamentStocksTreeData([]) - } - if (useFilter === true) { - generateFilamentStockTreeNodes({ id: 0 }, filter) - } else { - handleFilamentStocksTreeLoad(null) - } - }, [useFilter, defaultValue, filter]) - +const FilamentStockSelect = ({ + onChange, + filter = {}, + useFilter = false, + value, + disabled = false +}) => { return ( - ) } @@ -129,12 +29,8 @@ FilamentStockSelect.propTypes = { onChange: PropTypes.func.isRequired, value: PropTypes.object, filter: PropTypes.object, - useFilter: PropTypes.bool -} - -FilamentStockSelect.defaultProps = { - filter: {}, - useFilter: false + useFilter: PropTypes.bool, + disabled: PropTypes.bool } export default FilamentStockSelect diff --git a/src/components/Dashboard/common/IdDisplay.jsx b/src/components/Dashboard/common/IdDisplay.jsx index 2882473..689281f 100644 --- a/src/components/Dashboard/common/IdDisplay.jsx +++ b/src/components/Dashboard/common/IdDisplay.jsx @@ -6,7 +6,7 @@ import { useNavigate } from 'react-router-dom' import { useMediaQuery } from 'react-responsive' import CopyButton from './CopyButton' import SpotlightTooltip from './SpotlightTooltip' -import { getTypeMeta } from '../utils/Utils' +import { getModelByName } from '../../../database/ObjectModels' const { Text, Link } = Typography @@ -21,10 +21,10 @@ const IdDisplay = ({ const navigate = useNavigate() const isMobile = useMediaQuery({ maxWidth: 768 }) - const meta = getTypeMeta(type) - const prefix = meta.prefix - const hyperlink = meta.url(id) - const IconComponent = meta.icon + const model = getModelByName(type) + const prefix = model.prefix + const hyperlink = model.url(id) + const IconComponent = model.icon const icon = if (!id) { diff --git a/src/components/Dashboard/common/JobState.jsx b/src/components/Dashboard/common/JobState.jsx index 4b75b93..0efc244 100644 --- a/src/components/Dashboard/common/JobState.jsx +++ b/src/components/Dashboard/common/JobState.jsx @@ -1,47 +1,22 @@ import PropTypes from 'prop-types' -import { Progress, Flex, Typography, Space } from 'antd' -import React, { useState, useContext, useEffect } from 'react' -import { PrintServerContext } from '../context/PrintServerContext' -import IdDisplay from './IdDisplay' +import { Progress, Flex, Space } from 'antd' +import React, { useState, useEffect } from 'react' import StateTag from './StateTag' -const JobState = ({ - job, - showProgress = true, - showStatus = true, - showId = true, - showQuantity = true -}) => { - const { printServer } = useContext(PrintServerContext) +const JobState = ({ state, showProgress = true, showState = true }) => { const [currentState, setCurrentState] = useState( - job?.state || { type: 'unknown', progress: 0 } + state || { type: 'unknown', progress: 0 } ) - const [initialized, setInitialized] = useState(false) - const { Text } = Typography useEffect(() => { - if (printServer && !initialized && job?._id) { - setInitialized(true) - printServer.on('notify_job_update', (statusUpdate) => { - if (statusUpdate?._id === job._id && statusUpdate?.state) { - setCurrentState(statusUpdate.state) - } - }) + if (state) { + setCurrentState(state) } - return () => { - if (printServer && initialized) { - printServer.off('notify_job_update') - } - } - }, [printServer, initialized, job?._id]) + }, [state]) return ( - {showId && ( - - )} - {showQuantity && ({job.quantity})} - {showStatus && ( + {showState && ( @@ -60,18 +35,9 @@ const JobState = ({ } JobState.propTypes = { - job: PropTypes.shape({ - _id: PropTypes.string, - quantity: PropTypes.number, - state: PropTypes.shape({ - type: PropTypes.string, - progress: PropTypes.number - }) - }), + state: PropTypes.object, showProgress: PropTypes.bool, - showQuantity: PropTypes.bool, - showId: PropTypes.bool, - showStatus: PropTypes.bool + showState: PropTypes.bool } export default JobState diff --git a/src/components/Dashboard/common/DashboardNotes.jsx b/src/components/Dashboard/common/NotesPanel.jsx similarity index 99% rename from src/components/Dashboard/common/DashboardNotes.jsx rename to src/components/Dashboard/common/NotesPanel.jsx index d480826..08e2e8b 100644 --- a/src/components/Dashboard/common/DashboardNotes.jsx +++ b/src/components/Dashboard/common/NotesPanel.jsx @@ -259,7 +259,7 @@ NoteItem.propTypes = { onChildNoteAdded: PropTypes.func } -const DashboardNotes = ({ _id, onNewNote }) => { +const NotesPanel = ({ _id, onNewNote }) => { const [newNoteOpen, setNewNoteOpen] = useState(false) const [showMarkdown, setShowMarkdown] = useState(false) const [loading, setLoading] = useState(true) @@ -686,9 +686,9 @@ const DashboardNotes = ({ _id, onNewNote }) => { ) } -DashboardNotes.propTypes = { +NotesPanel.propTypes = { _id: PropTypes.string.isRequired, onNewNote: PropTypes.func } -export default DashboardNotes +export default NotesPanel diff --git a/src/components/Dashboard/common/ObjectProperty.jsx b/src/components/Dashboard/common/ObjectProperty.jsx index 786f40b..28daa79 100644 --- a/src/components/Dashboard/common/ObjectProperty.jsx +++ b/src/components/Dashboard/common/ObjectProperty.jsx @@ -32,6 +32,7 @@ import ColorSelector from './ColorSelector' import SecretDisplay from './SecretDisplay' import EyeIcon from '../../Icons/EyeIcon' import EyeSlashIcon from '../../Icons/EyeSlashIcon' +import FilamentStockState from './FilamentStockState' const { Text } = Typography @@ -57,8 +58,14 @@ const ObjectProperty = ({ readOnly = false, ...rest }) => { + // Split the name by "." to handle nested object properties + var formItemName = name + + if (name?.includes('.')) { + formItemName = name ? name.split('.') : undefined + } + const renderProperty = () => { - console.log('Rendering') if (!isEditing || readOnly) { switch (type) { case 'secret': @@ -120,7 +127,11 @@ const ObjectProperty = ({ } case 'number': { if (value != null) { - return {value} + if (Array.isArray(value)) { + return {value.length} + } else { + return {value} + } } else { return n/a } @@ -151,17 +162,18 @@ const ObjectProperty = ({ } } case 'state': { - if (value && value?.state) { + if (value && value?.type) { switch (objectType) { case 'printer': - return + return case 'job': - return - case 'subjob': - return - + return + case 'subJob': + return + case 'filamentStock': + return default: - return n/a + return No Object Type Specified } } else { return n/a @@ -251,7 +263,7 @@ const ObjectProperty = ({ switch (type) { case 'secret': return ( - + + ) case 'material': return ( - + ) @@ -437,18 +449,7 @@ const ObjectProperty = ({ } ObjectProperty.propTypes = { - type: PropTypes.oneOf([ - 'text', - 'number', - 'currency', - 'color', - 'weight', - 'vendor', - 'material', - 'id', - 'density', - 'mm' - ]), + type: PropTypes.string.isRequired, value: PropTypes.any, isEditing: PropTypes.bool, formItemProps: PropTypes.object, @@ -456,10 +457,8 @@ ObjectProperty.propTypes = { name: PropTypes.string, label: PropTypes.string, showLabel: PropTypes.bool, - objectType: PropTypes.string.isRequired, + objectType: PropTypes.string, readOnly: PropTypes.bool } -ObjectProperty.defaultProps = {} - export default ObjectProperty diff --git a/src/components/Dashboard/common/ObjectSelect.jsx b/src/components/Dashboard/common/ObjectSelect.jsx index 39465bf..9dbf836 100644 --- a/src/components/Dashboard/common/ObjectSelect.jsx +++ b/src/components/Dashboard/common/ObjectSelect.jsx @@ -2,12 +2,41 @@ import React, { useEffect, useState, useCallback } from 'react' import PropTypes from 'prop-types' import { TreeSelect, Typography, Flex, Badge, Space, Button, Input } from 'antd' import axios from 'axios' -import { getTypeMeta } from '../utils/Utils' +import { getModelByName } from '../../../database/ObjectModels' import IdDisplay from './IdDisplay' -import CountryDisplay from './CountryDisplay' import ReloadIcon from '../../Icons/ReloadIcon' +import ObjectProperty from './ObjectProperty' const { Text } = Typography 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. * @@ -35,29 +64,14 @@ const ObjectSelect = ({ type = 'unknown', ...rest }) => { + // --- State --- const [treeData, setTreeData] = useState([]) const [loading, setLoading] = useState(false) const [defaultValue, setDefaultValue] = useState(treeCheckable ? [] : value) const [searchValue, setSearchValue] = useState('') const [error, setError] = useState(false) - // Helper to get filter object for a node - const getFilter = useCallback( - (node) => { - 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 - }, - [treeData, propertyOrder] - ) - - // Fetch data from API + // --- API: Fetch data for a property level or leaf --- const fetchData = useCallback( async (property, filter, search) => { setLoading(true) @@ -74,14 +88,13 @@ const ObjectSelect = ({ } catch (err) { setLoading(false) setError(true) - // Optionally handle error return [] } }, [endpoint] ) - // Fetch single object by ID + // --- API: Fetch a single object by ID --- const fetchObjectById = useCallback( async (objectId) => { setLoading(true) @@ -95,305 +108,134 @@ const ObjectSelect = ({ } catch (err) { setLoading(false) setError(true) - console.error('Failed to fetch object by ID:', err) return null } }, [endpoint] ) - // Helper to render the title for a node + // --- Render node title --- const renderTitle = useCallback( - (item, isLeaf) => { - if (!isLeaf) { - // 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 - const meta = getTypeMeta(type) - const Icon = meta.icon - return ( - - {Icon && } - {item?.color && } - {item.name || type.title} - - - ) - }, - [propertyOrder, type] - ) - - // Build tree path for a default object - const buildTreePathForObject = useCallback( - async (object) => { - if (!object || !propertyOrder || propertyOrder.length === 0) return - - const newNodes = [] - let currentPId = 0 - - // Build category nodes for each property level and load all available options - for (let i = 0; i < propertyOrder.length; i++) { - const propertyName = propertyOrder[i] - let propertyValue - - // Handle nested property access (e.g., 'filament.diameter') - if (propertyName.includes('.')) { - const propertyPath = propertyName.split('.') - let currentValue = object - for (const prop of propertyPath) { - if (currentValue && typeof currentValue === 'object') { - currentValue = currentValue[prop] - } else { - currentValue = undefined - break - } - } - propertyValue = currentValue - } else { - propertyValue = object[propertyName] - } - - // Build filter for this level - let filterObj = {} - for (let j = 0; j < i; j++) { - const prevPropertyName = propertyOrder[j] - let prevPropertyValue - - if (prevPropertyName.includes('.')) { - const propertyPath = prevPropertyName.split('.') - let currentValue = object - for (const prop of propertyPath) { - if (currentValue && typeof currentValue === 'object') { - currentValue = currentValue[prop] - } else { - currentValue = undefined - break - } - } - prevPropertyValue = currentValue - } else { - prevPropertyValue = object[prevPropertyName] - } - - if (prevPropertyValue !== undefined && prevPropertyValue !== null) { - filterObj[prevPropertyName] = prevPropertyValue - } - } - - // Fetch all available options for this property level - const data = await fetchData(propertyName, filterObj, '') - - // Create nodes for all available options at this level - const levelNodes = data.map((item) => { - let value - if (typeof item === 'object' && item !== null) { - if (propertyName.includes('.')) { - const propertyPath = propertyName.split('.') - let currentValue = item - for (const prop of propertyPath) { - if (currentValue && typeof currentValue === 'object') { - currentValue = currentValue[prop] - } else { - currentValue = undefined - break - } - } - value = currentValue - } else { - value = item[propertyName] - } - } else { - value = item - } - - return { - id: value, - pId: currentPId, - value: value, - key: value, - propertyId: i, - title: renderTitle({ ...item, value }, false), - isLeaf: false, - selectable: false, - raw: item - } - }) - - newNodes.push(...levelNodes) - - // Update currentPId to the object's property value for the next level - if (propertyValue !== undefined && propertyValue !== null) { - currentPId = propertyValue - } - } - - // Load all leaf nodes at the final level - let finalFilterObj = {} - for (let j = 0; j < propertyOrder.length - 1; j++) { - const prevPropertyName = propertyOrder[j] - let prevPropertyValue - - if (prevPropertyName.includes('.')) { - const propertyPath = prevPropertyName.split('.') - let currentValue = object - for (const prop of propertyPath) { - if (currentValue && typeof currentValue === 'object') { - currentValue = currentValue[prop] - } else { - currentValue = undefined - break - } - } - prevPropertyValue = currentValue - } else { - prevPropertyValue = object[prevPropertyName] - } - - if (prevPropertyValue !== undefined && prevPropertyValue !== null) { - finalFilterObj[prevPropertyName] = prevPropertyValue - } - } - - const leafData = await fetchData(null, finalFilterObj, '') - const leafNodes = leafData.map((item) => ({ - id: item._id || item.id || item.value, - pId: currentPId, - value: item._id || item.id || item.value, - key: item._id || item.id || item.value, - title: renderTitle(item, true), - isLeaf: true, - raw: item - })) - - newNodes.push(...leafNodes) - - setTreeData(newNodes) - setDefaultValue(object._id || object.id) - }, - [propertyOrder, renderTitle, fetchData] - ) - - // Generate leaf nodes - const generateLeafNodes = useCallback( - async (node = null, filterArg = null, search = '') => { - if (!node) return - const actualFilter = filterArg === null ? getFilter(node) : filterArg - const data = await fetchData(null, actualFilter, search) - const newNodes = data.map((item) => { - const isLeaf = true - return { - id: item._id || item.id || item.value, - pId: node.id, - value: item._id || item.id || item.value, - key: item._id || item.id || item.value, - title: renderTitle(item, isLeaf), - isLeaf: true, - raw: item - } - }) - setTreeData((prev) => [...prev, ...newNodes]) - }, - [fetchData, getFilter, renderTitle] - ) - - // Generate category nodes - const generateCategoryNodes = useCallback( - async (node = null, search = '') => { - let filterObj = {} - let propertyId = 0 - if (!node) { - node = { id: 0 } + (item) => { + if (item.propertyType) { + return ( + + ) } else { - filterObj = getFilter(node) - propertyId = node.propertyId + 1 + const model = getModelByName(type) + const Icon = model.icon + return ( + + {Icon && } + {item?.color && } + {item.name || type.title} + + + ) } - const propertyName = propertyOrder[propertyId] - const data = await fetchData(propertyName, filterObj, search) - const newNodes = data.map((item) => { - const isLeaf = false - // Handle both cases: when item is a simple value or when it's an object - let value - if (typeof item === 'object' && item !== null) { - // Handle nested property access (e.g., 'filament.diameter') - if (propertyName.includes('.')) { - const propertyPath = propertyName.split('.') - let currentValue = item - for (const prop of propertyPath) { - if (currentValue && typeof currentValue === 'object') { - currentValue = currentValue[prop] - } else { - currentValue = undefined - break - } - } - value = currentValue - } else { - // If item is an object, try to get the property value - value = item[propertyName] - } - } else { - // If item is a simple value (string, number, etc.), use it directly - value = item - } - const title = renderTitle({ ...item, value }, isLeaf) + }, + [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: node.id, + pId: parentId, value: value, key: value, propertyId: propertyId, - title: title, + title: renderTitle({ ...item, value, propertyType }), isLeaf: false, selectable: false, raw: item } }) - setTreeData((prev) => [...prev, ...newNodes]) }, - [fetchData, getFilter, propertyOrder, renderTitle] + [renderTitle] ) - // Tree loader + // --- 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), + 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) { - await generateCategoryNodes(node, searchValue) + 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 { - await generateLeafNodes(node, null, searchValue) + // At leaf level + const filterObj = buildFilterForNode(node, treeData, propertyOrder) + const data = await fetchData(null, filterObj, searchValue) + setTreeData((prev) => [...prev, ...buildLeafNodes(data, node.id)]) } } else { - await generateCategoryNodes(null, searchValue) + // Root load + const propertyName = propertyOrder[0] + const data = await fetchData(propertyName, {}, searchValue) + setTreeData(buildCategoryNodes(data, propertyName, 0, 0)) } }, - [propertyOrder, generateCategoryNodes, generateLeafNodes, searchValue] + [ + propertyOrder, + treeData, + fetchData, + buildCategoryNodes, + buildLeafNodes, + searchValue + ] ) - // OnChange handler + // --- OnChange handler --- const handleOnChange = (val, selectedOptions) => { if (onChange) { if (treeCheckable) { - // Handle multiple selections with checkboxes - const selectedObjects = [] + // Multi-select + let selectedObjects = [] if (Array.isArray(val)) { - val.forEach((selectedValue) => { + selectedObjects = val.map((selectedValue) => { const node = treeData.find((n) => n.value === selectedValue) - if (node) { - selectedObjects.push(node.raw) - } else { - selectedObjects.push(selectedValue) - } + return node ? node.raw : selectedValue }) } onChange(selectedObjects, selectedOptions) } else { - // Handle single selection + // Single select const node = treeData.find((n) => n.value === val) onChange(node ? node.raw : val, selectedOptions) } @@ -401,21 +243,18 @@ const ObjectSelect = ({ setDefaultValue(val) } - // Search handler + // --- Search handler --- const handleSearch = (val) => { setSearchValue(val) setTreeData([]) } - // Keep defaultValue in sync and handle object values + // --- Sync defaultValue and load tree path for object values --- useEffect(() => { 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) - - // 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( @@ -424,7 +263,14 @@ const ObjectSelect = ({ if (!existingNode) { fetchObjectById(item._id).then((object) => { if (object) { - buildTreePathForObject(object) + // For multi-select, just add the leaf node + setTreeData((prev) => [ + ...prev, + ...buildLeafNodes( + [object], + object[propertyOrder[propertyOrder.length - 2]] || 0 + ) + ]) } }) } @@ -434,36 +280,44 @@ const ObjectSelect = ({ 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) + setTreeData((prev) => [ + ...prev, + ...buildLeafNodes( + [object], + object[propertyOrder[propertyOrder.length - 2]] || 0 + ) + ]) } }) } } } - }, [value, treeData, fetchObjectById, buildTreePathForObject, treeCheckable]) + }, [ + value, + treeData, + fetchObjectById, + buildLeafNodes, + propertyOrder, + treeCheckable + ]) - // Initial load + // --- Initial load --- useEffect(() => { if (treeData.length === 0 && !error && !loading) { - // If we have a default object value, don't load the regular tree if (!treeCheckable && value && typeof value === 'object' && value._id) { return } - if (useFilter || searchValue) { - generateLeafNodes({ id: 0 }, filter, searchValue) + // Flat filter mode + fetchData(null, filter, searchValue).then((data) => { + setTreeData(buildLeafNodes(data, 0)) + }) } else { handleTreeLoad(null) } @@ -473,7 +327,8 @@ const ObjectSelect = ({ useFilter, filter, searchValue, - generateLeafNodes, + buildLeafNodes, + fetchData, handleTreeLoad, error, loading, @@ -481,20 +336,25 @@ const ObjectSelect = ({ treeCheckable ]) - return error ? ( - - + // --- Error UI --- + if (error) { + return ( + + + - - -