Compare commits
No commits in common. "a5b888333d46139a31fbea51cf0e557adc1e39fa" and "589acab5925d145779737f6696303b3cd386be07" have entirely different histories.
a5b888333d
...
589acab592
@ -1,4 +1,4 @@
|
|||||||
import { useEffect, useState, useContext, useMemo, useCallback } from 'react'
|
import { useEffect, useState, useContext, useMemo } from 'react'
|
||||||
import {
|
import {
|
||||||
Card,
|
Card,
|
||||||
Segmented,
|
Segmented,
|
||||||
@ -21,7 +21,6 @@ import HistoryChartLegend from './HistoryChartLegend'
|
|||||||
import { useHistoryLegendOverlay } from '../hooks/useHistoryLegendOverlay'
|
import { useHistoryLegendOverlay } from '../hooks/useHistoryLegendOverlay'
|
||||||
import CheckIcon from '../../Icons/CheckIcon'
|
import CheckIcon from '../../Icons/CheckIcon'
|
||||||
import { LoadingOutlined } from '@ant-design/icons'
|
import { LoadingOutlined } from '@ant-design/icons'
|
||||||
import { round } from '../utils/Utils'
|
|
||||||
|
|
||||||
const legendMeasureStyle = {
|
const legendMeasureStyle = {
|
||||||
position: 'absolute',
|
position: 'absolute',
|
||||||
@ -198,26 +197,11 @@ const ModelHistoryDisplay = ({
|
|||||||
|
|
||||||
return {
|
return {
|
||||||
label: statDef.label || statDef.name,
|
label: statDef.label || statDef.name,
|
||||||
color,
|
color
|
||||||
prefix: statDef.prefix,
|
|
||||||
suffix: statDef.suffix,
|
|
||||||
roundNumber: statDef.roundNumber
|
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
}, [modelStats, themeColors])
|
}, [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 {
|
const {
|
||||||
slotRef,
|
slotRef,
|
||||||
measureRef,
|
measureRef,
|
||||||
@ -358,9 +342,7 @@ const ModelHistoryDisplay = ({
|
|||||||
date: point.date,
|
date: point.date,
|
||||||
dateFormatted: dayjs(point.date).format('DD/MM HH:mm'),
|
dateFormatted: dayjs(point.date).format('DD/MM HH:mm'),
|
||||||
category: label,
|
category: label,
|
||||||
value: statValue || 0,
|
value: statValue || 0
|
||||||
prefix: statDef.prefix || '',
|
|
||||||
suffix: statDef.suffix || ''
|
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
@ -543,7 +525,6 @@ const ModelHistoryDisplay = ({
|
|||||||
seriesLabels={seriesLabels}
|
seriesLabels={seriesLabels}
|
||||||
chartType={chartType}
|
chartType={chartType}
|
||||||
isDarkMode={isDarkMode}
|
isDarkMode={isDarkMode}
|
||||||
formatTooltipValue={formatTooltipValue}
|
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
{loading == true && chartRows.length == 0 && (
|
{loading == true && chartRows.length == 0 && (
|
||||||
|
|||||||
@ -35,141 +35,6 @@ const getFirstSelectableLeaf = (nodes) => {
|
|||||||
return null
|
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 = ({
|
const ObjectSelect = ({
|
||||||
type = 'unknown',
|
type = 'unknown',
|
||||||
showSearch = true,
|
showSearch = true,
|
||||||
@ -183,14 +48,8 @@ const ObjectSelect = ({
|
|||||||
style = {},
|
style = {},
|
||||||
...rest
|
...rest
|
||||||
}) => {
|
}) => {
|
||||||
const {
|
const { fetchObjectsByProperty, fetchObject, connected, searchObjects } =
|
||||||
fetchObjectsByProperty,
|
useContext(ApiServerContext)
|
||||||
fetchObject,
|
|
||||||
connected,
|
|
||||||
searchObjects,
|
|
||||||
subscribeToAllObjectUpdates,
|
|
||||||
subscribeToObjectTypeUpdates
|
|
||||||
} = useContext(ApiServerContext)
|
|
||||||
const { token } = useContext(AuthContext)
|
const { token } = useContext(AuthContext)
|
||||||
// --- State ---
|
// --- State ---
|
||||||
const [treeData, setTreeData] = useState([])
|
const [treeData, setTreeData] = useState([])
|
||||||
@ -201,32 +60,13 @@ const ObjectSelect = ({
|
|||||||
const [objectList, setObjectList] = useState([])
|
const [objectList, setObjectList] = useState([])
|
||||||
const [treeSelectValue, setTreeSelectValue] = useState(null)
|
const [treeSelectValue, setTreeSelectValue] = useState(null)
|
||||||
const [initialLoading, setInitialLoading] = useState(true)
|
const [initialLoading, setInitialLoading] = useState(true)
|
||||||
const [reloading, setReloading] = useState(false)
|
|
||||||
const [delayedInitialLoading, setDelayedInitalLoading] = useState(true)
|
const [delayedInitialLoading, setDelayedInitalLoading] = useState(true)
|
||||||
const [expandedKeys, setExpandedKeys] = useState([])
|
const [expandedKeys, setExpandedKeys] = useState([])
|
||||||
const [treeVersion, setTreeVersion] = useState(0)
|
const [treeVersion, setTreeVersion] = useState(0)
|
||||||
const [isSearching, setIsSearching] = useState(false)
|
const [isSearching, setIsSearching] = useState(false)
|
||||||
const [searchValue, setSearchValue] = useState('')
|
const [searchValue, setSearchValue] = useState('')
|
||||||
const [valueNotFound, setValueNotFound] = useState(false)
|
|
||||||
const searchRequestIdRef = useRef(0)
|
const searchRequestIdRef = useRef(0)
|
||||||
const valueRef = useRef(null)
|
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
|
// Refs to track value changes
|
||||||
const prevValueRef = useRef(value)
|
const prevValueRef = useRef(value)
|
||||||
@ -325,12 +165,7 @@ const ObjectSelect = ({
|
|||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
// It's a leaf object
|
// It's a leaf object
|
||||||
const existingIdx = merged.findIndex(
|
if (!merged.some((x) => String(x._id) === String(item._id))) {
|
||||||
(x) => String(x._id) === String(item._id)
|
|
||||||
)
|
|
||||||
if (existingIdx > -1) {
|
|
||||||
merged[existingIdx] = { ...merged[existingIdx], ...item }
|
|
||||||
} else {
|
|
||||||
merged.push(item)
|
merged.push(item)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -340,7 +175,7 @@ const ObjectSelect = ({
|
|||||||
|
|
||||||
// Fetch the object properties tree from the API
|
// Fetch the object properties tree from the API
|
||||||
const handleFetchObjectsProperties = useCallback(
|
const handleFetchObjectsProperties = useCallback(
|
||||||
async (customFilter = filter, { replace = false } = {}) => {
|
async (customFilter = filter) => {
|
||||||
try {
|
try {
|
||||||
const data = await fetchObjectsByProperty(type, {
|
const data = await fetchObjectsByProperty(type, {
|
||||||
properties: properties,
|
properties: properties,
|
||||||
@ -349,9 +184,7 @@ const ObjectSelect = ({
|
|||||||
})
|
})
|
||||||
|
|
||||||
if (Array.isArray(data)) {
|
if (Array.isArray(data)) {
|
||||||
setObjectPropertiesTree((prev) =>
|
setObjectPropertiesTree((prev) => mergeGroups(prev, data))
|
||||||
replace ? data : mergeGroups(prev, data)
|
|
||||||
)
|
|
||||||
} else {
|
} else {
|
||||||
setObjectPropertiesTree(data)
|
setObjectPropertiesTree(data)
|
||||||
}
|
}
|
||||||
@ -361,9 +194,7 @@ const ObjectSelect = ({
|
|||||||
return data
|
return data
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.error(err)
|
console.error(err)
|
||||||
if (!replace) {
|
|
||||||
setError(true)
|
setError(true)
|
||||||
}
|
|
||||||
return null
|
return null
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
@ -377,74 +208,24 @@ 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
|
// Convert the API response to AntD TreeSelect treeData
|
||||||
const buildTreeData = useCallback(
|
const buildTreeData = useCallback(
|
||||||
(data, pIdx = 0, parentKeys = [], filterPath = [], objects = []) => {
|
(data, pIdx = 0, parentKeys = [], filterPath = []) => {
|
||||||
if (!data || !Array.isArray(data)) {
|
if (!data || !Array.isArray(data)) return []
|
||||||
return { treeNodes: [], objects }
|
|
||||||
}
|
|
||||||
// If we are past the grouping properties, these are leaf objects
|
// If we are past the grouping properties, these are leaf objects
|
||||||
if (pIdx >= properties.length) {
|
if (pIdx >= properties.length) {
|
||||||
const treeNodes = data.map((object) => {
|
return data.map((object) => {
|
||||||
upsertObject(objects, object)
|
setObjectList((prev) => {
|
||||||
|
if (
|
||||||
|
prev.some(
|
||||||
|
(p) =>
|
||||||
|
p._id.toString().toLowerCase() ===
|
||||||
|
object._id.toString().toLowerCase()
|
||||||
|
)
|
||||||
|
)
|
||||||
|
return prev
|
||||||
|
return [...prev, object]
|
||||||
|
})
|
||||||
return {
|
return {
|
||||||
title: (
|
title: (
|
||||||
<div style={{ paddingTop: 0 }}>
|
<div style={{ paddingTop: 0 }}>
|
||||||
@ -467,11 +248,10 @@ const ObjectSelect = ({
|
|||||||
filterPath
|
filterPath
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
return { treeNodes, objects }
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Group Nodes
|
// Group Nodes
|
||||||
const treeNodes = data
|
return data
|
||||||
.map((group) => {
|
.map((group) => {
|
||||||
// Only process if it looks like a group
|
// Only process if it looks like a group
|
||||||
if (!group.property) return null
|
if (!group.property) return null
|
||||||
@ -492,15 +272,15 @@ const ObjectSelect = ({
|
|||||||
value: valueString
|
value: valueString
|
||||||
})
|
})
|
||||||
|
|
||||||
const { treeNodes: nodeChildren } = buildTreeData(
|
var nodeChildren = buildTreeData(
|
||||||
children,
|
children,
|
||||||
pIdx + 1,
|
pIdx + 1,
|
||||||
parentKeys.concat(valueString),
|
parentKeys.concat(valueString),
|
||||||
newFilterPath,
|
newFilterPath
|
||||||
objects
|
|
||||||
)
|
)
|
||||||
const resolvedChildren =
|
if (nodeChildren.length == 0) {
|
||||||
nodeChildren.length === 0 ? undefined : nodeChildren
|
nodeChildren = undefined
|
||||||
|
}
|
||||||
const modelProperty = getModelProperty(type, property)
|
const modelProperty = getModelProperty(type, property)
|
||||||
return {
|
return {
|
||||||
title: <ObjectProperty {...modelProperty} value={value} />,
|
title: <ObjectProperty {...modelProperty} value={value} />,
|
||||||
@ -512,129 +292,78 @@ const ObjectSelect = ({
|
|||||||
filterPath: newFilterPath,
|
filterPath: newFilterPath,
|
||||||
selectable: false,
|
selectable: false,
|
||||||
isLeaf: false,
|
isLeaf: false,
|
||||||
children: resolvedChildren
|
children: nodeChildren
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
.filter(Boolean)
|
.filter(Boolean)
|
||||||
|
|
||||||
return { treeNodes, objects }
|
|
||||||
},
|
},
|
||||||
[properties, type, upsertObject]
|
[properties, type]
|
||||||
)
|
)
|
||||||
|
|
||||||
const applyTreeFromData = useCallback(
|
// --- loadData for async loading on expand ---
|
||||||
(data) => {
|
const loadData = async (node) => {
|
||||||
const { treeNodes, objects } = buildTreeData(data)
|
// node.property is the property name, node.value is the value key
|
||||||
setObjectList(objects)
|
if (!node.property) return
|
||||||
setTreeData(treeNodes)
|
if (type == 'unknown') return
|
||||||
treeDataRef.current = treeNodes
|
// Build filter for this node by merging all parent property-value pairs
|
||||||
return { treeNodes, objects }
|
|
||||||
},
|
|
||||||
[buildTreeData]
|
|
||||||
)
|
|
||||||
|
|
||||||
const buildFilterFromNode = useCallback(
|
|
||||||
(node) => {
|
|
||||||
const customFilter = { ...filter }
|
const customFilter = { ...filter }
|
||||||
if (Array.isArray(node.filterPath)) {
|
if (Array.isArray(node.filterPath)) {
|
||||||
node.filterPath.forEach(({ property, value }) => {
|
node.filterPath.forEach(({ property, value }) => {
|
||||||
customFilter[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
|
customFilter[node.property] = node.filterValue
|
||||||
return customFilter
|
// Fetch children for this node
|
||||||
},
|
const data = await handleFetchObjectsProperties(customFilter)
|
||||||
[filter]
|
if (!data) return
|
||||||
|
|
||||||
|
// Navigate to the specific node's children in the response
|
||||||
|
let nodeSpecificChildren = data
|
||||||
|
|
||||||
|
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)
|
||||||
)
|
)
|
||||||
|
if (match) {
|
||||||
// --- loadData for async loading on expand ---
|
nodeSpecificChildren = match.children
|
||||||
const loadData = useCallback(
|
} else {
|
||||||
async (node) => {
|
nodeSpecificChildren = []
|
||||||
if (!node.property) return
|
break
|
||||||
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,
|
// Build new tree children only for this specific node
|
||||||
masterFilter
|
const children = buildTreeData(
|
||||||
|
nodeSpecificChildren,
|
||||||
|
properties.indexOf(node.property) + 1,
|
||||||
|
node.parentKeys || [],
|
||||||
|
node.filterPath
|
||||||
|
)
|
||||||
|
|
||||||
|
// 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
|
||||||
})
|
})
|
||||||
if (!Array.isArray(mergedData)) return
|
return updateNode(prevTreeData)
|
||||||
|
|
||||||
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(
|
const onTreeSelectChange = useCallback(
|
||||||
(value) => {
|
(value) => {
|
||||||
setValueNotFound(false)
|
|
||||||
clearedMissingValueRef.current = false
|
|
||||||
// Mark this as an internal change
|
// Mark this as an internal change
|
||||||
if (!multiple) setIsSearching(false)
|
if (!multiple) setIsSearching(false)
|
||||||
isInternalChangeRef.current = true
|
isInternalChangeRef.current = true
|
||||||
@ -675,7 +404,7 @@ const ObjectSelect = ({
|
|||||||
if (!trimmed) {
|
if (!trimmed) {
|
||||||
searchRequestIdRef.current += 1
|
searchRequestIdRef.current += 1
|
||||||
setIsSearching(false)
|
setIsSearching(false)
|
||||||
applyTreeFromData(objectPropertiesTree)
|
setTreeData(buildTreeData(objectPropertiesTree))
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -700,13 +429,7 @@ const ObjectSelect = ({
|
|||||||
)
|
)
|
||||||
: data
|
: data
|
||||||
|
|
||||||
const { treeNodes, objects } = buildTreeData(
|
setTreeData(buildTreeData(searchData, properties.length))
|
||||||
searchData,
|
|
||||||
properties.length
|
|
||||||
)
|
|
||||||
setObjectList(objects)
|
|
||||||
setTreeData(treeNodes)
|
|
||||||
treeDataRef.current = treeNodes
|
|
||||||
},
|
},
|
||||||
[
|
[
|
||||||
searchObjects,
|
searchObjects,
|
||||||
@ -716,8 +439,7 @@ const ObjectSelect = ({
|
|||||||
properties.length,
|
properties.length,
|
||||||
multiple,
|
multiple,
|
||||||
treeSelectValue,
|
treeSelectValue,
|
||||||
objectList,
|
objectList
|
||||||
applyTreeFromData
|
|
||||||
]
|
]
|
||||||
)
|
)
|
||||||
|
|
||||||
@ -759,23 +481,16 @@ const ObjectSelect = ({
|
|||||||
// Update treeData when objectPropertiesTree changes
|
// Update treeData when objectPropertiesTree changes
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (isSearching) return
|
if (isSearching) return
|
||||||
if (!Array.isArray(objectPropertiesTree)) return
|
if (objectPropertiesTree && Object.keys(objectPropertiesTree).length > 0) {
|
||||||
if (objectPropertiesTree.length > 0) {
|
const newTreeData = buildTreeData(objectPropertiesTree)
|
||||||
applyTreeFromData(objectPropertiesTree)
|
setTreeData((prev) => {
|
||||||
} else {
|
if (JSON.stringify(prev) !== JSON.stringify(newTreeData)) {
|
||||||
setObjectList([])
|
return newTreeData
|
||||||
setTreeData([])
|
|
||||||
treeDataRef.current = []
|
|
||||||
}
|
}
|
||||||
}, [objectPropertiesTree, applyTreeFromData, isSearching])
|
return prev
|
||||||
|
})
|
||||||
useEffect(() => {
|
}
|
||||||
expandedKeysRef.current = expandedKeys
|
}, [objectPropertiesTree, properties, buildTreeData, isSearching])
|
||||||
}, [expandedKeys])
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
treeDataRef.current = treeData
|
|
||||||
}, [treeData])
|
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const handleValue = async () => {
|
const handleValue = async () => {
|
||||||
@ -943,10 +658,7 @@ const ObjectSelect = ({
|
|||||||
onTreeSelectChange(null)
|
onTreeSelectChange(null)
|
||||||
setTreeSelectValue(null)
|
setTreeSelectValue(null)
|
||||||
setInitialLoading(true)
|
setInitialLoading(true)
|
||||||
setReloading(false)
|
|
||||||
setError(false)
|
setError(false)
|
||||||
setValueNotFound(false)
|
|
||||||
clearedMissingValueRef.current = false
|
|
||||||
prevValuesRef.current = { type, masterFilter }
|
prevValuesRef.current = { type, masterFilter }
|
||||||
}
|
}
|
||||||
}, [type, masterFilter, onTreeSelectChange])
|
}, [type, masterFilter, onTreeSelectChange])
|
||||||
@ -976,63 +688,6 @@ const ObjectSelect = ({
|
|||||||
}
|
}
|
||||||
}, [value, getValueIdentity, type, masterFilter])
|
}, [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(() => {
|
useEffect(() => {
|
||||||
if (initialLoading == false) {
|
if (initialLoading == false) {
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
@ -1043,59 +698,14 @@ const ObjectSelect = ({
|
|||||||
}
|
}
|
||||||
}, [initialLoading])
|
}, [initialLoading])
|
||||||
|
|
||||||
const modelLabel = useMemo(() => getModelByName(type).label, [type])
|
|
||||||
|
|
||||||
const placeholder = useMemo(
|
const placeholder = useMemo(
|
||||||
() =>
|
() =>
|
||||||
type == 'unknown' || type == undefined
|
type == 'unknown' || type == undefined
|
||||||
? 'n/a'
|
? 'n/a'
|
||||||
: `Select a ${modelLabel.toLowerCase()}...`,
|
: `Select a ${getModelByName(type).label.toLowerCase()}...`,
|
||||||
[type, modelLabel]
|
[type]
|
||||||
)
|
)
|
||||||
|
|
||||||
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 ---
|
// --- Error UI ---
|
||||||
if (error) {
|
if (error) {
|
||||||
return (
|
return (
|
||||||
@ -1129,17 +739,15 @@ const ObjectSelect = ({
|
|||||||
multiple={multiple}
|
multiple={multiple}
|
||||||
loadData={isSearching ? undefined : loadData}
|
loadData={isSearching ? undefined : loadData}
|
||||||
showCheckedStrategy={SHOW_CHILD}
|
showCheckedStrategy={SHOW_CHILD}
|
||||||
|
placeholder={placeholder}
|
||||||
{...treeSelectProps}
|
{...treeSelectProps}
|
||||||
{...rest}
|
{...rest}
|
||||||
placeholder={displayPlaceholder}
|
value={treeSelectValue}
|
||||||
status={valueNotFound ? 'warning' : treeSelectProps.status}
|
|
||||||
value={displayValue}
|
|
||||||
onChange={onTreeSelectChange}
|
onChange={onTreeSelectChange}
|
||||||
onSearch={onSearch}
|
onSearch={onSearch}
|
||||||
onInputKeyDown={onInputKeyDown}
|
onInputKeyDown={onInputKeyDown}
|
||||||
searchValue={searchValue}
|
searchValue={searchValue}
|
||||||
disabled={disabled || type == 'unknown' || type == undefined}
|
disabled={disabled || type == 'unknown' || type == undefined}
|
||||||
loading={reloading}
|
|
||||||
style={{ opacity: delayedInitialLoading ? 0 : 1 }}
|
style={{ opacity: delayedInitialLoading ? 0 : 1 }}
|
||||||
className={multiple ? 'object-select-multiple' : 'object-select'}
|
className={multiple ? 'object-select-multiple' : 'object-select'}
|
||||||
/>
|
/>
|
||||||
|
|||||||
@ -100,8 +100,6 @@ const stableStringify = (value) => {
|
|||||||
const getObjectTypeSubscriptionKey = (objectType, filter = {}) =>
|
const getObjectTypeSubscriptionKey = (objectType, filter = {}) =>
|
||||||
`${objectType}:${stableStringify(filter || {})}`
|
`${objectType}:${stableStringify(filter || {})}`
|
||||||
|
|
||||||
const getAllObjectUpdatesSubscriptionKey = (objectType) => `${objectType}:*`
|
|
||||||
|
|
||||||
const mapObjectFiltersForQuery = (filter = {}, type) => {
|
const mapObjectFiltersForQuery = (filter = {}, type) => {
|
||||||
const newFilter = { ...filter }
|
const newFilter = { ...filter }
|
||||||
if (filter == null || Object.keys(filter).length === 0) return newFilter
|
if (filter == null || Object.keys(filter).length === 0) return newFilter
|
||||||
@ -725,7 +723,6 @@ const ApiServerProvider = ({ children }) => {
|
|||||||
const objectType = data.objectType
|
const objectType = data.objectType
|
||||||
|
|
||||||
const callbacksRefKey = `${objectType}:${id}`
|
const callbacksRefKey = `${objectType}:${id}`
|
||||||
const allCallbacksRefKey = getAllObjectUpdatesSubscriptionKey(objectType)
|
|
||||||
|
|
||||||
if (
|
if (
|
||||||
id &&
|
id &&
|
||||||
@ -750,21 +747,6 @@ const ApiServerProvider = ({ children }) => {
|
|||||||
Array.from(subscribedCallbacksRef.current.keys())
|
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) => {
|
const handleObjectEvent = async (data) => {
|
||||||
@ -908,30 +890,6 @@ 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(
|
const offObjectTypeUpdatesEvent = useCallback(
|
||||||
(objectType, filter, callback) => {
|
(objectType, filter, callback) => {
|
||||||
if (socketRef.current && socketRef.current.connected == true) {
|
if (socketRef.current && socketRef.current.connected == true) {
|
||||||
@ -999,45 +957,6 @@ const ApiServerProvider = ({ children }) => {
|
|||||||
[offObjectUpdatesEvent]
|
[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
|
// Subscribe to user profile updates when WebSocket is connected and userProfile._id exists
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (connected && userProfile?._id) {
|
if (connected && userProfile?._id) {
|
||||||
@ -2810,7 +2729,6 @@ const ApiServerProvider = ({ children }) => {
|
|||||||
sendObjectFunction,
|
sendObjectFunction,
|
||||||
deleteObject,
|
deleteObject,
|
||||||
subscribeToObjectUpdates,
|
subscribeToObjectUpdates,
|
||||||
subscribeToAllObjectUpdates,
|
|
||||||
subscribeToObjectEvent,
|
subscribeToObjectEvent,
|
||||||
subscribeToObjectTypeUpdates,
|
subscribeToObjectTypeUpdates,
|
||||||
subscribeToObjectActivity,
|
subscribeToObjectActivity,
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user