Refactor ObjectDisplay component to enhance ID validation and handling. Introduced utility functions for ID extraction and validation, ensuring proper object hydration and subscription management. Updated related logic to improve robustness against invalid IDs.

This commit is contained in:
Tom Butcher 2026-03-06 23:43:15 +00:00
parent 62f0e047e2
commit 545cc0c526

View File

@ -31,12 +31,33 @@ const ObjectDisplay = ({
setObjectData((prev) => merge({}, prev, value)) 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) => { const isMinimalObject = useCallback((obj) => {
if (!obj || typeof obj !== 'object' || Array.isArray(obj)) return false if (!obj || typeof obj !== 'object' || Array.isArray(obj)) return false
const keys = Object.keys(obj) 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 // If only an _id is provided, fetch the full object via spotlight
const fetchFullObjectIfNeeded = useCallback( const fetchFullObjectIfNeeded = useCallback(
@ -64,9 +85,10 @@ const ObjectDisplay = ({
// Subscribe to object updates when component mounts // Subscribe to object updates when component mounts
useEffect(() => { useEffect(() => {
if (object?._id && objectType && connected && token != null) { const id = getStringId(object)
if (isValidId(id) && objectType && connected && token != null) {
const objectUpdatesUnsubscribe = subscribeToObjectUpdates( const objectUpdatesUnsubscribe = subscribeToObjectUpdates(
object._id, id,
objectType, objectType,
updateObjectEventHandler updateObjectEventHandler
) )
@ -76,27 +98,31 @@ const ObjectDisplay = ({
} }
} }
}, [ }, [
object?._id, object,
objectType, objectType,
subscribeToObjectUpdates, subscribeToObjectUpdates,
connected, connected,
token, token,
updateObjectEventHandler updateObjectEventHandler,
isValidId,
getStringId
]) ])
// Update local state when object prop changes // Update local state when object prop changes
useEffect(() => { useEffect(() => {
if (token == null) return if (token == null) return
const id = getStringId(object)
if (!isValidId(id)) return
const isMinimal = isMinimalObject(object) const isMinimal = isMinimalObject(object)
// Only skip re-fetch when we have a minimal object and already hydrated this id // 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 let cancelled = false
if (isMinimal) setIsHydrating(true) if (isMinimal) setIsHydrating(true)
const hydrateObject = async () => { const hydrateObject = async () => {
const fullObject = await fetchFullObjectIfNeeded(object) const fullObject = await fetchFullObjectIfNeeded(object)
if (!cancelled) { if (!cancelled) {
setObjectData((prev) => merge({}, prev, fullObject)) setObjectData((prev) => merge({}, prev, fullObject))
if (isMinimal) idRef.current = object?._id if (isMinimal) idRef.current = id
setIsHydrating(false) setIsHydrating(false)
} }
} }
@ -105,7 +131,7 @@ const ObjectDisplay = ({
cancelled = true cancelled = true
setIsHydrating(false) setIsHydrating(false)
} }
}, [object, fetchFullObjectIfNeeded, isMinimalObject, token]) }, [object, fetchFullObjectIfNeeded, isMinimalObject, isValidId, getStringId, token])
if (!objectData) { if (!objectData) {
return <Text type='secondary'>n/a</Text> return <Text type='secondary'>n/a</Text>
} }
@ -137,7 +163,7 @@ const ObjectDisplay = ({
var hyperlink = null var hyperlink = null
const defaultModelActions = const defaultModelActions =
model.actions?.filter((action) => action.default == true) || [] model.actions?.filter((action) => action.default == true) || []
const objectId = objectData._id const objectId = getStringId(objectData)
if (defaultModelActions.length >= 1 && objectId) { if (defaultModelActions.length >= 1 && objectId) {
hyperlink = defaultModelActions[0].url(objectId) hyperlink = defaultModelActions[0].url(objectId)
@ -229,9 +255,9 @@ const ObjectDisplay = ({
<div style={{ minWidth: 0 }}> <div style={{ minWidth: 0 }}>
{renderNameDisplay()} {renderNameDisplay()}
{objectData?._id && !objectData?.name ? ( {objectId && !objectData?.name ? (
<IdDisplay <IdDisplay
id={objectData?._id} id={objectId}
reference={objectData?._reference || undefined} reference={objectData?._reference || undefined}
type={objectType} type={objectType}
longId={false} longId={false}