Enhance ObjectSelect and ApiServerContext with Subscription Logic and Utility Functions
All checks were successful
farmcontrol/farmcontrol-ui/pipeline/head This commit looks good
All checks were successful
farmcontrol/farmcontrol-ui/pipeline/head This commit looks good
- Introduced new utility functions in ObjectSelect for tree node searching and value validation, improving data handling and user experience. - Enhanced ObjectSelect component to manage object updates and reloading logic more effectively, ensuring accurate state representation. - Added subscription capabilities for all object updates in ApiServerContext, allowing for real-time updates across components. - Refactored existing methods to improve clarity and maintainability, enhancing overall performance of the dashboard components.
This commit is contained in:
parent
dff0a02c12
commit
a5b888333d
@ -35,6 +35,141 @@ 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,
|
||||||
@ -48,8 +183,14 @@ const ObjectSelect = ({
|
|||||||
style = {},
|
style = {},
|
||||||
...rest
|
...rest
|
||||||
}) => {
|
}) => {
|
||||||
const { fetchObjectsByProperty, fetchObject, connected, searchObjects } =
|
const {
|
||||||
useContext(ApiServerContext)
|
fetchObjectsByProperty,
|
||||||
|
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([])
|
||||||
@ -60,13 +201,32 @@ 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)
|
||||||
@ -165,7 +325,12 @@ const ObjectSelect = ({
|
|||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
// It's a leaf object
|
// 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)
|
merged.push(item)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -175,7 +340,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) => {
|
async (customFilter = filter, { replace = false } = {}) => {
|
||||||
try {
|
try {
|
||||||
const data = await fetchObjectsByProperty(type, {
|
const data = await fetchObjectsByProperty(type, {
|
||||||
properties: properties,
|
properties: properties,
|
||||||
@ -184,7 +349,9 @@ const ObjectSelect = ({
|
|||||||
})
|
})
|
||||||
|
|
||||||
if (Array.isArray(data)) {
|
if (Array.isArray(data)) {
|
||||||
setObjectPropertiesTree((prev) => mergeGroups(prev, data))
|
setObjectPropertiesTree((prev) =>
|
||||||
|
replace ? data : mergeGroups(prev, data)
|
||||||
|
)
|
||||||
} else {
|
} else {
|
||||||
setObjectPropertiesTree(data)
|
setObjectPropertiesTree(data)
|
||||||
}
|
}
|
||||||
@ -194,7 +361,9 @@ const ObjectSelect = ({
|
|||||||
return data
|
return data
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.error(err)
|
console.error(err)
|
||||||
setError(true)
|
if (!replace) {
|
||||||
|
setError(true)
|
||||||
|
}
|
||||||
return null
|
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
|
// Convert the API response to AntD TreeSelect treeData
|
||||||
const buildTreeData = useCallback(
|
const buildTreeData = useCallback(
|
||||||
(data, pIdx = 0, parentKeys = [], filterPath = []) => {
|
(data, pIdx = 0, parentKeys = [], filterPath = [], objects = []) => {
|
||||||
if (!data || !Array.isArray(data)) return []
|
if (!data || !Array.isArray(data)) {
|
||||||
|
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) {
|
||||||
return data.map((object) => {
|
const treeNodes = data.map((object) => {
|
||||||
setObjectList((prev) => {
|
upsertObject(objects, object)
|
||||||
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 }}>
|
||||||
@ -248,10 +467,11 @@ const ObjectSelect = ({
|
|||||||
filterPath
|
filterPath
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
return { treeNodes, objects }
|
||||||
}
|
}
|
||||||
|
|
||||||
// Group Nodes
|
// Group Nodes
|
||||||
return data
|
const treeNodes = 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
|
||||||
@ -272,15 +492,15 @@ const ObjectSelect = ({
|
|||||||
value: valueString
|
value: valueString
|
||||||
})
|
})
|
||||||
|
|
||||||
var nodeChildren = buildTreeData(
|
const { treeNodes: nodeChildren } = buildTreeData(
|
||||||
children,
|
children,
|
||||||
pIdx + 1,
|
pIdx + 1,
|
||||||
parentKeys.concat(valueString),
|
parentKeys.concat(valueString),
|
||||||
newFilterPath
|
newFilterPath,
|
||||||
|
objects
|
||||||
)
|
)
|
||||||
if (nodeChildren.length == 0) {
|
const resolvedChildren =
|
||||||
nodeChildren = undefined
|
nodeChildren.length === 0 ? undefined : nodeChildren
|
||||||
}
|
|
||||||
const modelProperty = getModelProperty(type, property)
|
const modelProperty = getModelProperty(type, property)
|
||||||
return {
|
return {
|
||||||
title: <ObjectProperty {...modelProperty} value={value} />,
|
title: <ObjectProperty {...modelProperty} value={value} />,
|
||||||
@ -292,78 +512,129 @@ const ObjectSelect = ({
|
|||||||
filterPath: newFilterPath,
|
filterPath: newFilterPath,
|
||||||
selectable: false,
|
selectable: false,
|
||||||
isLeaf: false,
|
isLeaf: false,
|
||||||
children: nodeChildren
|
children: resolvedChildren
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
.filter(Boolean)
|
.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 ---
|
// --- loadData for async loading on expand ---
|
||||||
const loadData = async (node) => {
|
const loadData = useCallback(
|
||||||
// node.property is the property name, node.value is the value key
|
async (node) => {
|
||||||
if (!node.property) return
|
if (!node.property) return
|
||||||
if (type == 'unknown') return
|
if (type == 'unknown') return
|
||||||
// Build filter for this node by merging all parent property-value pairs
|
await handleFetchObjectsProperties(buildFilterFromNode(node))
|
||||||
const customFilter = { ...filter }
|
},
|
||||||
if (Array.isArray(node.filterPath)) {
|
[buildFilterFromNode, handleFetchObjectsProperties, type]
|
||||||
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
|
|
||||||
|
|
||||||
// Navigate to the specific node's children in the response
|
loadDataRef.current = loadData
|
||||||
let nodeSpecificChildren = data
|
|
||||||
|
|
||||||
if (node.filterPath && Array.isArray(node.filterPath)) {
|
const reloadTree = useCallback(
|
||||||
for (const pathItem of node.filterPath) {
|
async ({ silent = false } = {}) => {
|
||||||
if (!Array.isArray(nodeSpecificChildren)) break
|
if (isSearching) return
|
||||||
const match = nodeSpecificChildren.find(
|
if (!silent) {
|
||||||
(g) =>
|
setReloading(true)
|
||||||
g.property === pathItem.property &&
|
}
|
||||||
areValuesEqual(g.value, pathItem.value)
|
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) {
|
for (const key of keysToReload) {
|
||||||
nodeSpecificChildren = match.children
|
const node = findTreeNodeByKey(treeNodes, key)
|
||||||
} else {
|
if (!node?.property) continue
|
||||||
nodeSpecificChildren = []
|
|
||||||
break
|
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 reload = useCallback(() => reloadTree(), [reloadTree])
|
||||||
const children = buildTreeData(
|
const silentReload = useCallback(
|
||||||
nodeSpecificChildren,
|
() => reloadTree({ silent: true }),
|
||||||
properties.indexOf(node.property) + 1,
|
[reloadTree]
|
||||||
node.parentKeys || [],
|
)
|
||||||
node.filterPath
|
|
||||||
)
|
|
||||||
|
|
||||||
// Update treeData with new children for this node only
|
reloadRef.current = reload
|
||||||
setTreeData((prevTreeData) => {
|
silentReloadRef.current = silentReload
|
||||||
// Helper to recursively update the correct node
|
|
||||||
const updateNode = (nodes) =>
|
updateEventHandlerRef.current = updateEventHandler
|
||||||
nodes.map((n) => {
|
newEventHandlerRef.current = newEventHandler
|
||||||
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)
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
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
|
||||||
@ -404,7 +675,7 @@ const ObjectSelect = ({
|
|||||||
if (!trimmed) {
|
if (!trimmed) {
|
||||||
searchRequestIdRef.current += 1
|
searchRequestIdRef.current += 1
|
||||||
setIsSearching(false)
|
setIsSearching(false)
|
||||||
setTreeData(buildTreeData(objectPropertiesTree))
|
applyTreeFromData(objectPropertiesTree)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -429,7 +700,13 @@ const ObjectSelect = ({
|
|||||||
)
|
)
|
||||||
: data
|
: data
|
||||||
|
|
||||||
setTreeData(buildTreeData(searchData, properties.length))
|
const { treeNodes, objects } = buildTreeData(
|
||||||
|
searchData,
|
||||||
|
properties.length
|
||||||
|
)
|
||||||
|
setObjectList(objects)
|
||||||
|
setTreeData(treeNodes)
|
||||||
|
treeDataRef.current = treeNodes
|
||||||
},
|
},
|
||||||
[
|
[
|
||||||
searchObjects,
|
searchObjects,
|
||||||
@ -439,7 +716,8 @@ const ObjectSelect = ({
|
|||||||
properties.length,
|
properties.length,
|
||||||
multiple,
|
multiple,
|
||||||
treeSelectValue,
|
treeSelectValue,
|
||||||
objectList
|
objectList,
|
||||||
|
applyTreeFromData
|
||||||
]
|
]
|
||||||
)
|
)
|
||||||
|
|
||||||
@ -481,16 +759,23 @@ const ObjectSelect = ({
|
|||||||
// Update treeData when objectPropertiesTree changes
|
// Update treeData when objectPropertiesTree changes
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (isSearching) return
|
if (isSearching) return
|
||||||
if (objectPropertiesTree && Object.keys(objectPropertiesTree).length > 0) {
|
if (!Array.isArray(objectPropertiesTree)) return
|
||||||
const newTreeData = buildTreeData(objectPropertiesTree)
|
if (objectPropertiesTree.length > 0) {
|
||||||
setTreeData((prev) => {
|
applyTreeFromData(objectPropertiesTree)
|
||||||
if (JSON.stringify(prev) !== JSON.stringify(newTreeData)) {
|
} else {
|
||||||
return newTreeData
|
setObjectList([])
|
||||||
}
|
setTreeData([])
|
||||||
return prev
|
treeDataRef.current = []
|
||||||
})
|
|
||||||
}
|
}
|
||||||
}, [objectPropertiesTree, properties, buildTreeData, isSearching])
|
}, [objectPropertiesTree, applyTreeFromData, isSearching])
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
expandedKeysRef.current = expandedKeys
|
||||||
|
}, [expandedKeys])
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
treeDataRef.current = treeData
|
||||||
|
}, [treeData])
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const handleValue = async () => {
|
const handleValue = async () => {
|
||||||
@ -658,7 +943,10 @@ 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])
|
||||||
@ -688,6 +976,63 @@ 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(() => {
|
||||||
@ -698,14 +1043,59 @@ 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 ${getModelByName(type).label.toLowerCase()}...`,
|
: `Select a ${modelLabel.toLowerCase()}...`,
|
||||||
[type]
|
[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 ---
|
// --- Error UI ---
|
||||||
if (error) {
|
if (error) {
|
||||||
return (
|
return (
|
||||||
@ -739,15 +1129,17 @@ 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}
|
||||||
value={treeSelectValue}
|
placeholder={displayPlaceholder}
|
||||||
|
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,6 +100,8 @@ 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
|
||||||
@ -723,6 +725,7 @@ 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 &&
|
||||||
@ -747,6 +750,21 @@ 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) => {
|
||||||
@ -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(
|
const offObjectTypeUpdatesEvent = useCallback(
|
||||||
(objectType, filter, callback) => {
|
(objectType, filter, callback) => {
|
||||||
if (socketRef.current && socketRef.current.connected == true) {
|
if (socketRef.current && socketRef.current.connected == true) {
|
||||||
@ -957,6 +999,45 @@ 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) {
|
||||||
@ -2729,6 +2810,7 @@ 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