Compare commits
2 Commits
589acab592
...
a5b888333d
| Author | SHA1 | Date | |
|---|---|---|---|
| a5b888333d | |||
| dff0a02c12 |
@ -1,4 +1,4 @@
|
||||
import { useEffect, useState, useContext, useMemo } from 'react'
|
||||
import { useEffect, useState, useContext, useMemo, useCallback } from 'react'
|
||||
import {
|
||||
Card,
|
||||
Segmented,
|
||||
@ -21,6 +21,7 @@ import HistoryChartLegend from './HistoryChartLegend'
|
||||
import { useHistoryLegendOverlay } from '../hooks/useHistoryLegendOverlay'
|
||||
import CheckIcon from '../../Icons/CheckIcon'
|
||||
import { LoadingOutlined } from '@ant-design/icons'
|
||||
import { round } from '../utils/Utils'
|
||||
|
||||
const legendMeasureStyle = {
|
||||
position: 'absolute',
|
||||
@ -197,11 +198,26 @@ const ModelHistoryDisplay = ({
|
||||
|
||||
return {
|
||||
label: statDef.label || statDef.name,
|
||||
color
|
||||
color,
|
||||
prefix: statDef.prefix,
|
||||
suffix: statDef.suffix,
|
||||
roundNumber: statDef.roundNumber
|
||||
}
|
||||
})
|
||||
}, [modelStats, themeColors])
|
||||
|
||||
const formatTooltipValue = useCallback(
|
||||
(value, seriesLabel) => {
|
||||
const seriesDef = seriesLabels.find((item) => item.label === seriesLabel)
|
||||
let numericValue = Number(value ?? 0)
|
||||
if (seriesDef?.roundNumber) {
|
||||
numericValue = round(numericValue, seriesDef.roundNumber)
|
||||
}
|
||||
return `${seriesDef?.prefix || ''}${numericValue}${seriesDef?.suffix || ''}`
|
||||
},
|
||||
[seriesLabels]
|
||||
)
|
||||
|
||||
const {
|
||||
slotRef,
|
||||
measureRef,
|
||||
@ -342,7 +358,9 @@ const ModelHistoryDisplay = ({
|
||||
date: point.date,
|
||||
dateFormatted: dayjs(point.date).format('DD/MM HH:mm'),
|
||||
category: label,
|
||||
value: statValue || 0
|
||||
value: statValue || 0,
|
||||
prefix: statDef.prefix || '',
|
||||
suffix: statDef.suffix || ''
|
||||
}
|
||||
})
|
||||
})
|
||||
@ -525,6 +543,7 @@ const ModelHistoryDisplay = ({
|
||||
seriesLabels={seriesLabels}
|
||||
chartType={chartType}
|
||||
isDarkMode={isDarkMode}
|
||||
formatTooltipValue={formatTooltipValue}
|
||||
/>
|
||||
)}
|
||||
{loading == true && chartRows.length == 0 && (
|
||||
|
||||
@ -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: (
|
||||
<div style={{ paddingTop: 0 }}>
|
||||
@ -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: <ObjectProperty {...modelProperty} value={value} />,
|
||||
@ -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'}
|
||||
/>
|
||||
|
||||
@ -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,
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user