diff --git a/src/components/Dashboard/common/ObjectSelect.jsx b/src/components/Dashboard/common/ObjectSelect.jsx
index 396da1d4..393b158a 100644
--- a/src/components/Dashboard/common/ObjectSelect.jsx
+++ b/src/components/Dashboard/common/ObjectSelect.jsx
@@ -35,6 +35,141 @@ const getFirstSelectableLeaf = (nodes) => {
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 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,
@@ -48,8 +183,14 @@ const ObjectSelect = ({
style = {},
...rest
}) => {
- const { fetchObjectsByProperty, fetchObject, connected, searchObjects } =
- useContext(ApiServerContext)
+ const {
+ fetchObjectsByProperty,
+ fetchObject,
+ connected,
+ searchObjects,
+ subscribeToAllObjectUpdates,
+ subscribeToObjectTypeUpdates
+ } = useContext(ApiServerContext)
const { token } = useContext(AuthContext)
// --- State ---
const [treeData, setTreeData] = useState([])
@@ -60,13 +201,32 @@ const ObjectSelect = ({
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)
@@ -165,7 +325,12 @@ const ObjectSelect = ({
}
} else {
// It's a leaf object
- if (!merged.some((x) => String(x._id) === String(item._id))) {
+ const existingIdx = merged.findIndex(
+ (x) => String(x._id) === String(item._id)
+ )
+ if (existingIdx > -1) {
+ merged[existingIdx] = { ...merged[existingIdx], ...item }
+ } else {
merged.push(item)
}
}
@@ -175,7 +340,7 @@ const ObjectSelect = ({
// Fetch the object properties tree from the API
const handleFetchObjectsProperties = useCallback(
- async (customFilter = filter) => {
+ async (customFilter = filter, { replace = false } = {}) => {
try {
const data = await fetchObjectsByProperty(type, {
properties: properties,
@@ -184,7 +349,9 @@ const ObjectSelect = ({
})
if (Array.isArray(data)) {
- setObjectPropertiesTree((prev) => mergeGroups(prev, data))
+ setObjectPropertiesTree((prev) =>
+ replace ? data : mergeGroups(prev, data)
+ )
} else {
setObjectPropertiesTree(data)
}
@@ -194,7 +361,9 @@ const ObjectSelect = ({
return data
} catch (err) {
console.error(err)
- setError(true)
+ if (!replace) {
+ setError(true)
+ }
return null
}
},
@@ -208,24 +377,74 @@ const ObjectSelect = ({
]
)
+ 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 = []) => {
- if (!data || !Array.isArray(data)) return []
+ (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) {
- return data.map((object) => {
- setObjectList((prev) => {
- if (
- prev.some(
- (p) =>
- p._id.toString().toLowerCase() ===
- object._id.toString().toLowerCase()
- )
- )
- return prev
- return [...prev, object]
- })
+ const treeNodes = data.map((object) => {
+ upsertObject(objects, object)
return {
title: (
@@ -248,10 +467,11 @@ const ObjectSelect = ({
filterPath
}
})
+ return { treeNodes, objects }
}
// Group Nodes
- return data
+ const treeNodes = data
.map((group) => {
// Only process if it looks like a group
if (!group.property) return null
@@ -272,15 +492,15 @@ const ObjectSelect = ({
value: valueString
})
- var nodeChildren = buildTreeData(
+ const { treeNodes: nodeChildren } = buildTreeData(
children,
pIdx + 1,
parentKeys.concat(valueString),
- newFilterPath
+ newFilterPath,
+ objects
)
- if (nodeChildren.length == 0) {
- nodeChildren = undefined
- }
+ const resolvedChildren =
+ nodeChildren.length === 0 ? undefined : nodeChildren
const modelProperty = getModelProperty(type, property)
return {
title: ,
@@ -292,78 +512,129 @@ const ObjectSelect = ({
filterPath: newFilterPath,
selectable: false,
isLeaf: false,
- children: nodeChildren
+ children: resolvedChildren
}
})
.filter(Boolean)
+
+ return { treeNodes, objects }
},
- [properties, type]
+ [properties, type, upsertObject]
+ )
+
+ const applyTreeFromData = useCallback(
+ (data) => {
+ const { treeNodes, objects } = buildTreeData(data)
+ setObjectList(objects)
+ setTreeData(treeNodes)
+ treeDataRef.current = treeNodes
+ return { treeNodes, objects }
+ },
+ [buildTreeData]
+ )
+
+ 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 = async (node) => {
- // node.property is the property name, node.value is the value key
- if (!node.property) return
- if (type == 'unknown') 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
- })
- }
- // Ensure current node is in filter (should be covered by filterPath, but redundancy is safe)
- customFilter[node.property] = node.filterValue
- // Fetch children for this node
- const data = await handleFetchObjectsProperties(customFilter)
- if (!data) return
+ const loadData = useCallback(
+ async (node) => {
+ if (!node.property) return
+ if (type == 'unknown') return
+ await handleFetchObjectsProperties(buildFilterFromNode(node))
+ },
+ [buildFilterFromNode, handleFetchObjectsProperties, type]
+ )
- // Navigate to the specific node's children in the response
- let nodeSpecificChildren = data
+ loadDataRef.current = loadData
- if (node.filterPath && Array.isArray(node.filterPath)) {
- for (const pathItem of node.filterPath) {
- if (!Array.isArray(nodeSpecificChildren)) break
- const match = nodeSpecificChildren.find(
- (g) =>
- g.property === pathItem.property &&
- areValuesEqual(g.value, pathItem.value)
+ 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
)
- if (match) {
- nodeSpecificChildren = match.children
- } else {
- nodeSpecificChildren = []
- break
+ 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
+ ]
+ )
- // Build new tree children only for this specific node
- const children = buildTreeData(
- nodeSpecificChildren,
- properties.indexOf(node.property) + 1,
- node.parentKeys || [],
- node.filterPath
- )
+ const reload = useCallback(() => reloadTree(), [reloadTree])
+ const silentReload = useCallback(
+ () => reloadTree({ silent: true }),
+ [reloadTree]
+ )
- // 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)
- })
- }
+ reloadRef.current = reload
+ silentReloadRef.current = silentReload
+
+ updateEventHandlerRef.current = updateEventHandler
+ newEventHandlerRef.current = newEventHandler
const onTreeSelectChange = useCallback(
(value) => {
+ setValueNotFound(false)
+ clearedMissingValueRef.current = false
// Mark this as an internal change
if (!multiple) setIsSearching(false)
isInternalChangeRef.current = true
@@ -404,7 +675,7 @@ const ObjectSelect = ({
if (!trimmed) {
searchRequestIdRef.current += 1
setIsSearching(false)
- setTreeData(buildTreeData(objectPropertiesTree))
+ applyTreeFromData(objectPropertiesTree)
return
}
@@ -429,7 +700,13 @@ const ObjectSelect = ({
)
: data
- setTreeData(buildTreeData(searchData, properties.length))
+ const { treeNodes, objects } = buildTreeData(
+ searchData,
+ properties.length
+ )
+ setObjectList(objects)
+ setTreeData(treeNodes)
+ treeDataRef.current = treeNodes
},
[
searchObjects,
@@ -439,7 +716,8 @@ const ObjectSelect = ({
properties.length,
multiple,
treeSelectValue,
- objectList
+ objectList,
+ applyTreeFromData
]
)
@@ -481,16 +759,23 @@ const ObjectSelect = ({
// Update treeData when objectPropertiesTree changes
useEffect(() => {
if (isSearching) return
- if (objectPropertiesTree && Object.keys(objectPropertiesTree).length > 0) {
- const newTreeData = buildTreeData(objectPropertiesTree)
- setTreeData((prev) => {
- if (JSON.stringify(prev) !== JSON.stringify(newTreeData)) {
- return newTreeData
- }
- return prev
- })
+ if (!Array.isArray(objectPropertiesTree)) return
+ if (objectPropertiesTree.length > 0) {
+ applyTreeFromData(objectPropertiesTree)
+ } else {
+ setObjectList([])
+ setTreeData([])
+ treeDataRef.current = []
}
- }, [objectPropertiesTree, properties, buildTreeData, isSearching])
+ }, [objectPropertiesTree, applyTreeFromData, isSearching])
+
+ useEffect(() => {
+ expandedKeysRef.current = expandedKeys
+ }, [expandedKeys])
+
+ useEffect(() => {
+ treeDataRef.current = treeData
+ }, [treeData])
useEffect(() => {
const handleValue = async () => {
@@ -658,7 +943,10 @@ const ObjectSelect = ({
onTreeSelectChange(null)
setTreeSelectValue(null)
setInitialLoading(true)
+ setReloading(false)
setError(false)
+ setValueNotFound(false)
+ clearedMissingValueRef.current = false
prevValuesRef.current = { type, masterFilter }
}
}, [type, masterFilter, onTreeSelectChange])
@@ -688,6 +976,63 @@ const ObjectSelect = ({
}
}, [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(() => {
@@ -698,14 +1043,59 @@ const ObjectSelect = ({
}
}, [initialLoading])
+ const modelLabel = useMemo(() => getModelByName(type).label, [type])
+
const placeholder = useMemo(
() =>
type == 'unknown' || type == undefined
? 'n/a'
- : `Select a ${getModelByName(type).label.toLowerCase()}...`,
- [type]
+ : `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 (
@@ -739,15 +1129,17 @@ const ObjectSelect = ({
multiple={multiple}
loadData={isSearching ? undefined : loadData}
showCheckedStrategy={SHOW_CHILD}
- placeholder={placeholder}
{...treeSelectProps}
{...rest}
- value={treeSelectValue}
+ placeholder={displayPlaceholder}
+ status={valueNotFound ? 'warning' : treeSelectProps.status}
+ value={displayValue}
onChange={onTreeSelectChange}
onSearch={onSearch}
onInputKeyDown={onInputKeyDown}
searchValue={searchValue}
disabled={disabled || type == 'unknown' || type == undefined}
+ loading={reloading}
style={{ opacity: delayedInitialLoading ? 0 : 1 }}
className={multiple ? 'object-select-multiple' : 'object-select'}
/>
diff --git a/src/components/Dashboard/context/ApiServerContext.jsx b/src/components/Dashboard/context/ApiServerContext.jsx
index b310cc46..46429e1f 100644
--- a/src/components/Dashboard/context/ApiServerContext.jsx
+++ b/src/components/Dashboard/context/ApiServerContext.jsx
@@ -100,6 +100,8 @@ const stableStringify = (value) => {
const getObjectTypeSubscriptionKey = (objectType, filter = {}) =>
`${objectType}:${stableStringify(filter || {})}`
+const getAllObjectUpdatesSubscriptionKey = (objectType) => `${objectType}:*`
+
const mapObjectFiltersForQuery = (filter = {}, type) => {
const newFilter = { ...filter }
if (filter == null || Object.keys(filter).length === 0) return newFilter
@@ -723,6 +725,7 @@ const ApiServerProvider = ({ children }) => {
const objectType = data.objectType
const callbacksRefKey = `${objectType}:${id}`
+ const allCallbacksRefKey = getAllObjectUpdatesSubscriptionKey(objectType)
if (
id &&
@@ -747,6 +750,21 @@ const ApiServerProvider = ({ children }) => {
Array.from(subscribedCallbacksRef.current.keys())
)
}
+
+ if (objectType && subscribedCallbacksRef.current.has(allCallbacksRefKey)) {
+ const callbacks = subscribedCallbacksRef.current.get(allCallbacksRefKey)
+ logger.debug(
+ `Calling ${callbacks.length} callbacks for all object updates:`,
+ allCallbacksRefKey
+ )
+ callbacks.forEach((callback) => {
+ try {
+ callback(id, data.object)
+ } catch (error) {
+ logger.error('Error in all object update callback:', error)
+ }
+ })
+ }
}
const handleObjectEvent = async (data) => {
@@ -890,6 +908,30 @@ const ApiServerProvider = ({ children }) => {
}
}, [])
+ const offAllObjectUpdatesEvent = useCallback((objectType, callback) => {
+ if (socketRef.current && socketRef.current.connected == true) {
+ const callbacksRefKey = getAllObjectUpdatesSubscriptionKey(objectType)
+ if (subscribedCallbacksRef.current.has(callbacksRefKey)) {
+ const callbacks = subscribedCallbacksRef.current
+ .get(callbacksRefKey)
+ .filter((cb) => cb !== callback)
+ if (callbacks.length === 0) {
+ logger.debug(
+ 'No callbacks found for all object updates:',
+ callbacksRefKey,
+ 'unsubscribing...'
+ )
+ subscribedCallbacksRef.current.delete(callbacksRefKey)
+ socketRef.current.emit('unsubscribeAllObjectUpdates', {
+ objectType: objectType
+ })
+ } else {
+ subscribedCallbacksRef.current.set(callbacksRefKey, callbacks)
+ }
+ }
+ }
+ }, [])
+
const offObjectTypeUpdatesEvent = useCallback(
(objectType, filter, callback) => {
if (socketRef.current && socketRef.current.connected == true) {
@@ -957,6 +999,45 @@ const ApiServerProvider = ({ children }) => {
[offObjectUpdatesEvent]
)
+ const subscribeToAllObjectUpdates = useCallback(
+ (objectType, callback) => {
+ if (socketRef.current && socketRef.current.connected == true) {
+ const callbacksRefKey = getAllObjectUpdatesSubscriptionKey(objectType)
+ if (!subscribedCallbacksRef.current.has(callbacksRefKey)) {
+ subscribedCallbacksRef.current.set(callbacksRefKey, [])
+ }
+
+ const callbacksLength =
+ subscribedCallbacksRef.current.get(callbacksRefKey).length
+
+ if (callbacksLength <= 0) {
+ socketRef.current.emit(
+ 'subscribeToAllObjectUpdates',
+ { objectType: objectType },
+ (result) => {
+ if (result.success) {
+ logger.info(
+ 'Subscribed to all object updates for objectType:',
+ objectType
+ )
+ }
+ }
+ )
+ }
+ logger.info(
+ 'Adding all-object-updates callback for objectType:',
+ objectType,
+ 'callbacks length:',
+ callbacksLength + 1
+ )
+ subscribedCallbacksRef.current.get(callbacksRefKey).push(callback)
+
+ return () => offAllObjectUpdatesEvent(objectType, callback)
+ }
+ },
+ [offAllObjectUpdatesEvent]
+ )
+
// Subscribe to user profile updates when WebSocket is connected and userProfile._id exists
useEffect(() => {
if (connected && userProfile?._id) {
@@ -2729,6 +2810,7 @@ const ApiServerProvider = ({ children }) => {
sendObjectFunction,
deleteObject,
subscribeToObjectUpdates,
+ subscribeToAllObjectUpdates,
subscribeToObjectEvent,
subscribeToObjectTypeUpdates,
subscribeToObjectActivity,