diff --git a/src/components/Dashboard/common/ObjectDisplay.jsx b/src/components/Dashboard/common/ObjectDisplay.jsx index 56ac041..045a807 100644 --- a/src/components/Dashboard/common/ObjectDisplay.jsx +++ b/src/components/Dashboard/common/ObjectDisplay.jsx @@ -31,12 +31,33 @@ const ObjectDisplay = ({ setObjectData((prev) => merge({}, prev, value)) }, []) - // Detect minimal objects that only contain an _id + // Ensure ID is valid before hydrate/subscribe (non-empty, not null/undefined) + const isValidId = useCallback((id) => { + return id != null && String(id).trim() !== '' + }, []) + + // Extract string ID from object; handles both primitive _id and populated ref (_id as object) + const getStringId = useCallback((obj) => { + const id = obj?._id + if (id == null) return null + if (typeof id === 'string') return id + if (typeof id === 'object' && id !== null && typeof id._id === 'string') + return id._id + return null + }, []) + + // Detect minimal objects that only contain an _id (must be string, not populated object) 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 - }, []) + const id = obj._id + return ( + keys.length === 1 && + keys[0] === '_id' && + typeof id === 'string' && + isValidId(id) + ) + }, [isValidId]) // If only an _id is provided, fetch the full object via spotlight const fetchFullObjectIfNeeded = useCallback( @@ -64,9 +85,10 @@ const ObjectDisplay = ({ // Subscribe to object updates when component mounts useEffect(() => { - if (object?._id && objectType && connected && token != null) { + const id = getStringId(object) + if (isValidId(id) && objectType && connected && token != null) { const objectUpdatesUnsubscribe = subscribeToObjectUpdates( - object._id, + id, objectType, updateObjectEventHandler ) @@ -76,27 +98,31 @@ const ObjectDisplay = ({ } } }, [ - object?._id, + object, objectType, subscribeToObjectUpdates, connected, token, - updateObjectEventHandler + updateObjectEventHandler, + isValidId, + getStringId ]) // Update local state when object prop changes useEffect(() => { if (token == null) return + const id = getStringId(object) + if (!isValidId(id)) return const isMinimal = isMinimalObject(object) // Only skip re-fetch when we have a minimal object and already hydrated this id - if (isMinimal && idRef.current === object?._id) return + if (isMinimal && idRef.current === id) return let cancelled = false if (isMinimal) setIsHydrating(true) const hydrateObject = async () => { const fullObject = await fetchFullObjectIfNeeded(object) if (!cancelled) { setObjectData((prev) => merge({}, prev, fullObject)) - if (isMinimal) idRef.current = object?._id + if (isMinimal) idRef.current = id setIsHydrating(false) } } @@ -105,7 +131,7 @@ const ObjectDisplay = ({ cancelled = true setIsHydrating(false) } - }, [object, fetchFullObjectIfNeeded, isMinimalObject, token]) + }, [object, fetchFullObjectIfNeeded, isMinimalObject, isValidId, getStringId, token]) if (!objectData) { return n/a } @@ -137,7 +163,7 @@ const ObjectDisplay = ({ var hyperlink = null const defaultModelActions = model.actions?.filter((action) => action.default == true) || [] - const objectId = objectData._id + const objectId = getStringId(objectData) if (defaultModelActions.length >= 1 && objectId) { hyperlink = defaultModelActions[0].url(objectId) @@ -229,9 +255,9 @@ const ObjectDisplay = ({
{renderNameDisplay()} - {objectData?._id && !objectData?.name ? ( + {objectId && !objectData?.name ? (