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 { getModelProperty } from '../../../database/ObjectModels' const { SHOW_CHILD } = TreeSelect const EMPTY_OBJECT = {} // Helper to check if two values are equal (handling objects/ids) const areValuesEqual = (v1, v2) => { const id1 = v1 && typeof v1 === 'object' && v1._id ? v1._id : v1 const id2 = v2 && typeof v2 === 'object' && v2._id ? v2._id : v2 return String(id1) === String(id2) } const toSelectValue = (id) => { if (id == null || id === '') return null return String(id).toLowerCase() } const getValueId = (item) => item && typeof item === 'object' ? item._id : item const getFirstSelectableLeaf = (nodes) => { if (!Array.isArray(nodes)) return null for (const node of nodes) { if (node.isLeaf && node.selectable !== false) return node const found = getFirstSelectableLeaf(node.children) if (found) return found } return null } const findTreeNodeByKey = (nodes, key) => { if (!Array.isArray(nodes)) return null for (const node of nodes) { if (node.key === key) return node if (node.children) { const found = findTreeNodeByKey(node.children, key) if (found) return found } } return null } const findTreeNodeByValue = (nodes, value) => { if (!Array.isArray(nodes) || value == null || value === '') return null const match = String(value).toLowerCase() for (const node of nodes) { if ( node?.value != null && String(node.value).toLowerCase() === match && node.isLeaf ) { return node } const found = findTreeNodeByValue(node?.children, value) if (found) return found } return null } const isValueInTree = (nodes, id) => { if (id == null || id === '') return false return findTreeNodeByValue(nodes, id) != null } const getSelectValueFromExternal = (externalValue, multiple, nodes) => { if (externalValue == null || !Array.isArray(nodes) || nodes.length === 0) { return undefined } if (multiple) { const values = Array.isArray(externalValue) ? externalValue : [] const ids = values .map((item) => toSelectValue(getValueId(item))) .filter((id) => id != null && isValueInTree(nodes, id)) return ids } const node = findTreeNodeByValue(nodes, getValueId(externalValue)) return node?.value } const isExternalValueMissing = ( externalValue, multiple, objectList, treeData, ready ) => { if (!ready || externalValue == null) return false const isMissingId = (id) => id != null && id !== '' && !objectList.some((obj) => areValuesEqual(obj._id, id)) && !isValueInTree(treeData, id) if (multiple) { const values = Array.isArray(externalValue) ? externalValue : [] if (values.length === 0) return false return values.some((item) => isMissingId(item && typeof item === 'object' ? item._id : item) ) } const id = externalValue && typeof externalValue === 'object' ? externalValue._id : externalValue return isMissingId(id) } const getBranchChildren = (data, filterPath) => { let nodeSpecificChildren = data if (!Array.isArray(filterPath)) return nodeSpecificChildren for (const pathItem of filterPath) { if (!Array.isArray(nodeSpecificChildren)) break const match = nodeSpecificChildren.find( (group) => group.property === pathItem.property && areValuesEqual(group.value, pathItem.value) ) if (match) { nodeSpecificChildren = match.children } else { return [] } } return nodeSpecificChildren } const replaceBranchChildren = (nodes, filterPath, newChildren, depth = 0) => { if (!Array.isArray(nodes) || !Array.isArray(filterPath)) return nodes if (depth >= filterPath.length) return newChildren const { property, value } = filterPath[depth] return nodes.map((node) => { if (node.property !== property || !areValuesEqual(node.value, value)) { return node } const nextDepth = depth + 1 if (nextDepth >= filterPath.length) { return { ...node, children: newChildren } } return { ...node, children: replaceBranchChildren( node.children || [], filterPath, newChildren, nextDepth ) } }) } const mergeObjectInTree = (nodes, id, updatedData) => { if (!Array.isArray(nodes)) return nodes return nodes.map((node) => { if (node._id && String(node._id) === String(id)) { return { ...node, ...updatedData } } if (node.property && node.children) { return { ...node, children: mergeObjectInTree(node.children, id, updatedData) } } return node }) } const updateAffectsFilter = (updatedData, filter, masterFilter) => { if (!updatedData || typeof updatedData !== 'object') return false const filterKeys = new Set([ ...Object.keys(filter || {}), ...Object.keys(masterFilter || {}) ]) if (filterKeys.size === 0) return false return Object.keys(updatedData).some((key) => filterKeys.has(key)) } const ObjectSelect = ({ type = 'unknown', showSearch = true, multiple = false, treeSelectProps = EMPTY_OBJECT, filter = EMPTY_OBJECT, masterFilter = EMPTY_OBJECT, value, onChange, disabled = false, style = {}, ...rest }) => { const { fetchObjectsByProperty, fetchObject, connected, searchObjects, subscribeToAllObjectUpdates, subscribeToObjectTypeUpdates } = 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) const [reloading, setReloading] = useState(false) const [delayedInitialLoading, setDelayedInitalLoading] = useState(true) const [expandedKeys, setExpandedKeys] = useState([]) const [treeVersion, setTreeVersion] = useState(0) const [isSearching, setIsSearching] = useState(false) const [searchValue, setSearchValue] = useState('') const [valueNotFound, setValueNotFound] = useState(false) const searchRequestIdRef = useRef(0) const valueRef = useRef(null) const objectListRef = useRef([]) const updateEventHandlerRef = useRef() const newEventHandlerRef = useRef() const subscriptionFilterRef = useRef() const subscribeToObjectTypeUpdatesFnRef = useRef(subscribeToObjectTypeUpdates) const subscribeToObjectTypeUpdatesRef = useRef(null) const subscribedTypeRef = useRef(null) const subscribeToAllObjectUpdatesRef = useRef(null) const treeDataRef = useRef([]) const loadDataRef = useRef(null) const expandedKeysRef = useRef([]) const filterRef = useRef(filter) const masterFilterRef = useRef(masterFilter) const clearedMissingValueRef = useRef(false) filterRef.current = filter masterFilterRef.current = masterFilter // Refs to track value changes const prevValueRef = useRef(value) const isInternalChangeRef = useRef(false) // Normalize a value to an identity string so we can detect in-place _id updates const getValueIdentity = useCallback((val) => { if (val && typeof val === 'object') { // Handle arrays if (Array.isArray(val)) { const ids = val .map((item) => { if (item && typeof item === 'object') { if (item._id) return String(item._id) if ( item.value && typeof item.value === 'object' && item.value._id ) return String(item.value._id) } return null }) .filter(Boolean) .sort() return ids.length > 0 ? ids.join(',') : JSON.stringify(val) } // Handle single objects if (val._id) return String(val._id) if (val.value && typeof val.value === 'object' && val.value._id) return String(val.value._id) } return JSON.stringify(val) }, []) const prevValueIdentityRef = useRef(getValueIdentity(value)) // 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] ) const mergeGroups = useCallback((current, incoming) => { if (!current) return incoming if (!incoming) return current if (!Array.isArray(current) || !Array.isArray(incoming)) return incoming const merged = [...current] // Helper to generate a unique key for a group node const getGroupKey = (item) => { const val = item.value const valPart = val && typeof val === 'object' && val._id ? val._id : JSON.stringify(val) return `${item.property}:${valPart}` } for (const item of incoming) { if (item.property && item.value !== undefined) { // It's a group node const itemKey = getGroupKey(item) const existingIdx = merged.findIndex( (x) => x.property && x.value !== undefined && getGroupKey(x) === itemKey ) if (existingIdx > -1) { merged[existingIdx] = { ...merged[existingIdx], children: mergeGroups(merged[existingIdx].children, item.children) } } else { merged.push(item) } } else { // It's a leaf object const existingIdx = merged.findIndex( (x) => String(x._id) === String(item._id) ) if (existingIdx > -1) { merged[existingIdx] = { ...merged[existingIdx], ...item } } else { merged.push(item) } } } return merged }, []) // Fetch the object properties tree from the API const handleFetchObjectsProperties = useCallback( async (customFilter = filter, { replace = false } = {}) => { try { const data = await fetchObjectsByProperty(type, { properties: properties, filter: customFilter, masterFilter }) if (Array.isArray(data)) { setObjectPropertiesTree((prev) => replace ? data : mergeGroups(prev, data) ) } else { setObjectPropertiesTree(data) } setInitialLoading(false) setError(false) return data } catch (err) { console.error(err) if (!replace) { setError(true) } return null } }, [ type, fetchObjectsByProperty, properties, filter, masterFilter, mergeGroups ] ) const updateEventHandler = useCallback((id, updatedData) => { const itemExists = objectListRef.current.some( (obj) => String(obj._id) === String(id) ) if ( updateAffectsFilter( updatedData, filterRef.current, masterFilterRef.current ) ) { if (itemExists) { reloadRef.current?.() } else { silentReloadRef.current?.() } return } if (!itemExists) return setObjectList((prev) => prev.map((obj) => String(obj._id) === String(id) ? { ...obj, ...updatedData } : obj ) ) setObjectPropertiesTree((prev) => mergeObjectInTree(prev, id, updatedData)) }, []) const newEventHandler = useCallback(() => { reloadRef.current?.() }, []) const reloadRef = useRef(null) const silentReloadRef = useRef(null) subscribeToObjectTypeUpdatesFnRef.current = subscribeToObjectTypeUpdates const subscriptionFilter = useMemo( () => ({ ...filter, ...masterFilter }), [filter, masterFilter] ) subscriptionFilterRef.current = subscriptionFilter const upsertObject = useCallback((objects, object) => { const id = object._id.toString().toLowerCase() const existingIdx = objects.findIndex( (item) => item._id.toString().toLowerCase() === id ) if (existingIdx > -1) { objects[existingIdx] = { ...objects[existingIdx], ...object } } else { objects.push(object) } }, []) // Convert the API response to AntD TreeSelect treeData const buildTreeData = useCallback( (data, pIdx = 0, parentKeys = [], filterPath = [], objects = []) => { if (!data || !Array.isArray(data)) { return { treeNodes: [], objects } } // If we are past the grouping properties, these are leaf objects if (pIdx >= properties.length) { const treeNodes = data.map((object) => { upsertObject(objects, object) return { title: (
), value: object._id.toString().toLowerCase(), key: object._id.toString().toLowerCase(), isLeaf: true, parentKeys, filterPath } }) return { treeNodes, objects } } // Group Nodes const treeNodes = data .map((group) => { // Only process if it looks like a group if (!group.property) return null const { property, value, children } = group var valueString = value if (value && typeof value === 'object' && value._id) { valueString = value._id } if (Array.isArray(valueString)) { valueString = valueString.join(',') } const nodeKey = parentKeys .concat(property + ':' + valueString) .join('-') const newFilterPath = filterPath.concat({ property, value: valueString }) const { treeNodes: nodeChildren } = buildTreeData( children, pIdx + 1, parentKeys.concat(valueString), newFilterPath, objects ) const resolvedChildren = nodeChildren.length === 0 ? undefined : nodeChildren const modelProperty = getModelProperty(type, property) return { title: , value: nodeKey, key: nodeKey, property, filterValue: valueString, parentKeys: parentKeys.concat(valueString), filterPath: newFilterPath, selectable: false, isLeaf: false, children: resolvedChildren } }) .filter(Boolean) return { treeNodes, objects } }, [properties, type, upsertObject] ) const applyTreeFromData = useCallback( (data) => { const { treeNodes, objects } = buildTreeData(data) setObjectList(objects) setTreeData(treeNodes) treeDataRef.current = treeNodes const syncedValue = getSelectValueFromExternal( valueRef.current, multiple, treeNodes ) if (multiple) { if (Array.isArray(syncedValue) && syncedValue.length > 0) { setTreeSelectValue(syncedValue) setValueNotFound(false) clearedMissingValueRef.current = false } } else if (syncedValue != null) { setTreeSelectValue(syncedValue) setValueNotFound(false) clearedMissingValueRef.current = false } return { treeNodes, objects } }, [buildTreeData, multiple] ) const buildFilterFromNode = useCallback( (node) => { const customFilter = { ...filter } if (Array.isArray(node.filterPath)) { node.filterPath.forEach(({ property, value }) => { customFilter[property] = value }) } customFilter[node.property] = node.filterValue return customFilter }, [filter] ) // --- loadData for async loading on expand --- const loadData = useCallback( async (node) => { if (!node.property) return if (type == 'unknown') return await handleFetchObjectsProperties(buildFilterFromNode(node)) }, [buildFilterFromNode, handleFetchObjectsProperties, type] ) loadDataRef.current = loadData const reloadTree = useCallback( async ({ silent = false } = {}) => { if (isSearching) return if (!silent) { setReloading(true) } try { let mergedData = await fetchObjectsByProperty(type, { properties: properties, filter: filter, masterFilter }) if (!Array.isArray(mergedData)) return let { treeNodes } = buildTreeData(mergedData) const keysToReload = [...expandedKeysRef.current].sort( (a, b) => a.length - b.length ) for (const key of keysToReload) { const node = findTreeNodeByKey(treeNodes, key) if (!node?.property) continue const branchData = await fetchObjectsByProperty(type, { properties: properties, filter: buildFilterFromNode(node), masterFilter }) if (!Array.isArray(branchData)) continue const branchChildren = getBranchChildren(branchData, node.filterPath) mergedData = replaceBranchChildren( mergedData, node.filterPath, branchChildren ) ;({ treeNodes } = buildTreeData(mergedData)) } setObjectPropertiesTree(mergedData) } catch (err) { console.error(err) } finally { if (!silent) { setReloading(false) } } }, [ isSearching, fetchObjectsByProperty, type, properties, filter, masterFilter, buildTreeData, buildFilterFromNode ] ) const reload = useCallback(() => reloadTree(), [reloadTree]) const silentReload = useCallback( () => reloadTree({ silent: true }), [reloadTree] ) reloadRef.current = reload silentReloadRef.current = silentReload updateEventHandlerRef.current = updateEventHandler newEventHandlerRef.current = newEventHandler const onTreeSelectChange = useCallback( (value) => { const isEmptySelection = multiple ? !Array.isArray(value) || value.length === 0 : value == null || value === '' if ( isEmptySelection && treeDataRef.current.length === 0 && valueRef.current != null ) { return } setValueNotFound(false) clearedMissingValueRef.current = false // Mark this as an internal change if (!multiple) setIsSearching(false) 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) => areValuesEqual(obj._id, id))) .filter(Boolean) } setTreeSelectValue(value) onChange?.(selectedObjects) } else { // Single selection: replace the previous object instead of emitting // undefined (lodash merge skips undefined and would keep the old value). if (value == null || value === '') { setTreeSelectValue(null) onChange?.(null) return } const selectedObject = objectList.find((obj) => areValuesEqual(obj._id, value) ) setTreeSelectValue(value) onChange?.(selectedObject ?? null) } }, [multiple, objectList, onChange] ) const onSearch = useCallback( async (search) => { setSearchValue(search) const trimmed = search?.trim() ?? '' if (!trimmed) { searchRequestIdRef.current += 1 setIsSearching(false) applyTreeFromData(objectPropertiesTree) return } const requestId = ++searchRequestIdRef.current setIsSearching(true) const data = await searchObjects(type, trimmed) if (requestId !== searchRequestIdRef.current) return if (!Array.isArray(data)) return const searchData = multiple && Array.isArray(treeSelectValue) ? [ ...data, ...objectList.filter((object) => treeSelectValue.some((id) => areValuesEqual(object._id, id)) ) ].filter( (object, index, objects) => objects.findIndex((item) => areValuesEqual(item._id, object._id) ) === index ) : data const { treeNodes, objects } = buildTreeData( searchData, properties.length ) setObjectList(objects) setTreeData(treeNodes) treeDataRef.current = treeNodes }, [ searchObjects, type, buildTreeData, objectPropertiesTree, properties.length, multiple, treeSelectValue, objectList, applyTreeFromData ] ) const onInputKeyDown = useCallback( (e) => { treeSelectProps.onInputKeyDown?.(e) if (e.defaultPrevented || e.key !== 'Enter' || !isSearching) return const firstLeaf = getFirstSelectableLeaf(treeData) if (!firstLeaf?.value) return e.preventDefault() e.stopPropagation() onTreeSelectChange( multiple ? [ ...(Array.isArray(treeSelectValue) ? treeSelectValue : []), firstLeaf.value ].filter( (item, index, values) => values.findIndex((value) => areValuesEqual(value, item)) === index ) : firstLeaf.value ) setSearchValue('') onSearch('') }, [ treeSelectProps, isSearching, treeData, treeSelectValue, multiple, onTreeSelectChange, onSearch ] ) // Update treeData when objectPropertiesTree changes useEffect(() => { if (isSearching) return if (!Array.isArray(objectPropertiesTree)) return if (objectPropertiesTree.length > 0) { applyTreeFromData(objectPropertiesTree) } else { setObjectList([]) setTreeData([]) treeDataRef.current = [] } }, [objectPropertiesTree, applyTreeFromData, isSearching]) useEffect(() => { expandedKeysRef.current = expandedKeys }, [expandedKeys]) useEffect(() => { treeDataRef.current = treeData }, [treeData]) useEffect(() => { const handleValue = async () => { if ( multiple && Array.isArray(value) && getValueIdentity(valueRef.current) !== getValueIdentity(value) && type != 'unknown' ) { valueRef.current = value const fullValues = await Promise.all(value.map(fetchFullObjectIfNeeded)) const pathKeys = [] if (fullValues.length === 0) { handleFetchObjectsProperties() } else { fullValues.forEach((fullValue) => { const valueFilter = { ...filter } const parentKeys = [] properties.forEach((prop) => { if (!Object.prototype.hasOwnProperty.call(fullValue, prop)) return const filterValue = fullValue[prop] let valueString = filterValue if ( filterValue && typeof filterValue === 'object' && filterValue._id ) { valueString = filterValue._id } else if (filterValue?.name) { valueString = filterValue.name } else if (Array.isArray(filterValue)) { valueString = filterValue.join(',') } valueFilter[prop] = valueString pathKeys.push( parentKeys.concat(prop + ':' + valueString).join('-') ) parentKeys.push(valueString) }) handleFetchObjectsProperties(valueFilter) }) } setExpandedKeys([...new Set(pathKeys)]) setTreeSelectValue( value .map((item) => toSelectValue(getValueId(item))) .filter((id) => id != null) ) setInitialized(true) return } if ( value && typeof value === 'object' && value !== null && getValueIdentity(valueRef.current) !== getValueIdentity(value) && type != 'unknown' ) { valueRef.current = value // 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 } const pathKeys = [] const parentKeys = [] properties.forEach((prop) => { if (Object.prototype.hasOwnProperty.call(fullValue, prop)) { const filterValue = fullValue[prop] let valueString = filterValue if ( filterValue && typeof filterValue === 'object' && filterValue._id ) { valueFilter[prop] = filterValue._id valueString = filterValue._id } else if (filterValue?.name) { valueFilter[prop] = filterValue.name valueString = filterValue.name } else if (Array.isArray(filterValue)) { valueFilter[prop] = filterValue.join(',') valueString = filterValue.join(',') } else { valueFilter[prop] = filterValue valueString = filterValue } // Build the path key for this property level const nodeKey = parentKeys .concat(prop + ':' + valueString) .join('-') pathKeys.push(nodeKey) parentKeys.push(valueString) } }) // Expand the path to the object setExpandedKeys(pathKeys) // Fetch with the new filter handleFetchObjectsProperties(valueFilter) setTreeSelectValue(toSelectValue(valueRef.current._id)) setInitialized(true) return } if ( !initialized && token != null && type != 'unknown' && type != undefined && connected == true ) { handleFetchObjectsProperties() setInitialized(true) } if ( value == null || type == 'unknown' || type == undefined || connected == false ) { setTreeSelectValue(null) setInitialLoading(false) setInitialized(true) } } const timeoutId = setTimeout(() => { handleValue() }, 10) return () => { clearTimeout(timeoutId) } }, [ value, filter, properties, handleFetchObjectsProperties, initialized, token, fetchFullObjectIfNeeded, type, connected, getValueIdentity, multiple ]) 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) { searchRequestIdRef.current += 1 setIsSearching(false) setSearchValue('') setObjectPropertiesTree({}) setObjectList([]) setTreeData([]) setTreeVersion((v) => v + 1) setExpandedKeys([]) setInitialized(false) valueRef.current = null onTreeSelectChange(null) setTreeSelectValue(null) setInitialLoading(true) setReloading(false) setError(false) setValueNotFound(false) clearedMissingValueRef.current = false prevValuesRef.current = { type, masterFilter } } }, [type, masterFilter, onTreeSelectChange]) useEffect(() => { // Check if value has actually changed const currentValueIdentity = getValueIdentity(value) const hasValueChanged = prevValueIdentityRef.current !== currentValueIdentity if (hasValueChanged) { const changeSource = isInternalChangeRef.current ? 'internal' : 'external' if (changeSource == 'external') { setObjectPropertiesTree({}) setTreeData([]) treeDataRef.current = [] setInitialized(false) setInitialLoading(true) valueRef.current = null clearedMissingValueRef.current = false setValueNotFound(false) prevValuesRef.current = { type, masterFilter } } // Reset the internal change flag isInternalChangeRef.current = false // Update the previous value reference prevValueRef.current = value prevValueIdentityRef.current = currentValueIdentity } }, [value, getValueIdentity, type, masterFilter]) useEffect(() => { objectListRef.current = objectList }, [objectList]) // Cleanup subscriptions on unmount useEffect(() => { return () => { if (connected === true && subscribeToObjectTypeUpdatesRef.current) { subscribeToObjectTypeUpdatesRef.current() subscribeToObjectTypeUpdatesRef.current = null } if (connected === true && subscribeToAllObjectUpdatesRef.current) { subscribeToAllObjectUpdatesRef.current() subscribeToAllObjectUpdatesRef.current = null } } }, [connected]) // Subscribe to all object updates for this type useEffect(() => { if (connected !== true || type === 'unknown' || type == null) return const unsubscribe = subscribeToAllObjectUpdates(type, (id, updateData) => { updateEventHandlerRef.current(id, updateData) }) subscribeToAllObjectUpdatesRef.current = unsubscribe return () => { if (unsubscribe) unsubscribe() if (subscribeToAllObjectUpdatesRef.current === unsubscribe) { subscribeToAllObjectUpdatesRef.current = null } } }, [type, connected, subscribeToAllObjectUpdates]) // Subscribe to type-level updates (new/changed objects matching filter) useEffect(() => { if (connected !== true) return if (subscribedTypeRef.current === type) return const unsubscribe = subscribeToObjectTypeUpdatesFnRef.current( type, subscriptionFilterRef.current, () => newEventHandlerRef.current() ) subscribeToObjectTypeUpdatesRef.current = unsubscribe subscribedTypeRef.current = type return () => { if (unsubscribe) unsubscribe() if (subscribeToObjectTypeUpdatesRef.current === unsubscribe) { subscribeToObjectTypeUpdatesRef.current = null } if (subscribedTypeRef.current === type) { subscribedTypeRef.current = null } } }, [type, connected]) useEffect(() => { if (initialLoading == false) { setTimeout(() => { setDelayedInitalLoading(false) }, 100) } else { setDelayedInitalLoading(true) } }, [initialLoading]) const modelLabel = useMemo(() => getModelByName(type).label, [type]) const placeholder = useMemo( () => type == 'unknown' || type == undefined ? 'n/a' : `Select a ${modelLabel.toLowerCase()}...`, [type, modelLabel] ) const valueReady = initialized && !initialLoading && !delayedInitialLoading && !reloading && !isSearching useEffect(() => { const missing = isExternalValueMissing( value, multiple, objectList, treeData, valueReady ) if (missing) { setValueNotFound(true) if ( value != null && !(Array.isArray(value) && value.length === 0) && !clearedMissingValueRef.current ) { clearedMissingValueRef.current = true isInternalChangeRef.current = true setTreeSelectValue(multiple ? [] : null) onChange?.(multiple ? [] : null) } return } clearedMissingValueRef.current = false if (value != null) { setValueNotFound(false) } }, [value, multiple, objectList, treeData, valueReady, onChange]) const displayPlaceholder = valueNotFound ? `${modelLabel.charAt(0).toUpperCase()}${modelLabel.slice(1).toLowerCase()} not found.` : placeholder const displayValue = valueNotFound ? (multiple ? [] : null) : treeSelectValue // --- Error UI --- if (error) { return (