import { useEffect, useState, useContext, useCallback, useMemo } from 'react' import PropTypes from 'prop-types' import { TreeSelect, Space, Button, Input } from 'antd' import ReloadIcon from '../../Icons/ReloadIcon' import { ApiServerContext } from '../context/ApiServerContext' import { AuthContext } from '../context/AuthContext' import ObjectProperty from './ObjectProperty' import { getModelByName } from '../../../database/ObjectModels' import merge from 'lodash/merge' const { SHOW_CHILD } = TreeSelect const ObjectSelect = ({ type = 'unknown', showSearch = false, multiple = false, treeSelectProps = {}, filter = {}, masterFilter = {}, value, disabled = false, ...rest }) => { const { fetchObjectsByProperty } = useContext(ApiServerContext) const { token } = useContext(AuthContext) // --- State --- const [treeData, setTreeData] = useState([]) const [objectPropertiesTree, setObjectPropertiesTree] = useState({}) const [initialized, setInitialized] = useState(false) const [error, setError] = useState(false) const properties = useMemo(() => getModelByName(type).group || [], [type]) const [objectList, setObjectList] = useState([]) const [treeSelectValue, setTreeSelectValue] = useState(null) const [initialLoading, setInitialLoading] = useState(true) // Fetch the object properties tree from the API const handleFetchObjectsProperties = useCallback( async (customFilter = filter) => { try { const data = await fetchObjectsByProperty(type, { properties: properties, filter: customFilter, masterFilter }) if (Array.isArray(data)) { setObjectPropertiesTree((prev) => merge([], prev, data)) } else { setObjectPropertiesTree((prev) => merge({}, prev, data)) } setInitialLoading(false) setError(false) return data } catch (err) { console.error(err) setError(true) return null } }, [type, fetchObjectsByProperty, properties, filter, masterFilter] ) // Convert the API response to AntD TreeSelect treeData const buildTreeData = useCallback( (data, pIdx = 0, parentKeys = [], filterPath = []) => { if (!data) return [] if (Array.isArray(data)) { return data.map((object) => { setObjectList((prev) => { const filtered = prev.filter( (prevObject) => prevObject._id != object._id ) return [...filtered, object] }) return { title: ( ), value: object._id, key: object._id, isLeaf: true, property: properties[pIdx - 1], // previous property parentKeys, filterPath } }) } if (typeof data == 'object') { const property = properties[pIdx] || null return Object.entries(data) .map(([key, value]) => { if (property != null && typeof value === 'object') { const newFilterPath = filterPath.concat({ property, value: key }) return { title: , value: parentKeys.concat(key).join('-'), filterValue: key, key: parentKeys.concat(key).join('-'), property, parentKeys: parentKeys.concat(key || '-'), filterPath: newFilterPath, selectable: false, children: buildTreeData( value, pIdx + 1, parentKeys.concat(key), newFilterPath ), isLeaf: false } } }) .filter(Boolean) } }, [properties, type] ) // --- loadData for async loading on expand --- const loadData = async (node) => { // node.property is the property name, node.value is the value if (!node.property) return // Build filter for this node by merging all parent property-value pairs const customFilter = { ...filter } if (Array.isArray(node.filterPath)) { node.filterPath.forEach(({ property, value }) => { customFilter[property] = value }) } customFilter[node.property] = node.filterValue // Fetch children for this node const data = await handleFetchObjectsProperties(customFilter) if (!data) return // Extract only the children for the specific node that was expanded let nodeSpecificData = data if (typeof data === 'object' && !Array.isArray(data)) { // If the API returns an object with multiple keys, get only the data for this node nodeSpecificData = data[node.value] || {} } // Build new tree children only for this specific node const children = buildTreeData( nodeSpecificData, properties.indexOf(node.property) + 1, node.parentKeys || [], (node.filterPath || []).concat({ property: node.property, value: node.filterValue }) ) // Update treeData with new children for this node only setTreeData((prevTreeData) => { // Helper to recursively update the correct node const updateNode = (nodes) => nodes.map((n) => { if (n.key === node.key) { return { ...n, children, isLeaf: children.length === 0 } } else if (n.children) { return { ...n, children: updateNode(n.children) } } return n }) return updateNode(prevTreeData) }) } const onTreeSelectChange = (value) => { // value can be a string (single) or array (multiple) if (multiple) { // Multiple selection let selectedObjects = [] if (Array.isArray(value)) { selectedObjects = value .map((id) => objectList.find((obj) => obj._id === id)) .filter(Boolean) } setTreeSelectValue(value) if (rest.onChange) rest.onChange(selectedObjects) } else { // Single selection const selectedObject = objectList.find((obj) => obj._id === value) setTreeSelectValue(value) if (rest.onChange) rest.onChange(selectedObject) } } // Update treeData when objectPropertiesTree changes useEffect(() => { if (objectPropertiesTree && Object.keys(objectPropertiesTree).length > 0) { const newTreeData = buildTreeData(objectPropertiesTree) setTreeData((prev) => { if (JSON.stringify(prev) !== JSON.stringify(newTreeData)) { return newTreeData } return prev }) } }, [objectPropertiesTree, properties, type, buildTreeData]) useEffect(() => { if (value && typeof value === 'object' && value !== null && !initialized) { // Build a new filter from value's properties that are in the properties list const valueFilter = { ...filter } properties.forEach((prop) => { if (Object.prototype.hasOwnProperty.call(value, prop)) { const filterValue = value[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) } }, [ value, filter, properties, handleFetchObjectsProperties, initialized, token ]) // --- Error UI --- if (error) { return (