import { useEffect, useState, useContext, useCallback, useMemo, useRef } 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, fetchObject } = 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) // 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) => { 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) => { // Mark this as an internal change isInternalChangeRef.current = true // 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, buildTreeData]) useEffect(() => { 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(fullValue._id) setInitialized(true) return } if (!initialized && token != null) { handleFetchObjectsProperties() setInitialized(true) } } handleValue() }, [ value, filter, properties, handleFetchObjectsProperties, initialized, token, fetchFullObjectIfNeeded ]) const prevValuesRef = useRef({ type, masterFilter }) 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) { setObjectPropertiesTree({}) setTreeData([]) setInitialized(false) prevValuesRef.current = { type, masterFilter } } }, [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 (