From 9b86114d998ed6b573c4f9e5f6883a9462d23a7c Mon Sep 17 00:00:00 2001 From: Tom Butcher Date: Sun, 8 Nov 2026 23:55:05 +0000 Subject: [PATCH] Enhance ObjectSelect Component with Group Value Handling and Selection Path Logic - Introduced utility functions for managing group value tokens and matching logic, improving the handling of null and omitted references. - Refactored selection path building to streamline the process of generating value filters and path keys based on object properties. - Updated comparison logic in tree structure manipulation to utilize new group value matching, enhancing accuracy in node handling. - Added support for expanded keys management during value updates, ensuring consistent state across selections. --- .../Dashboard/common/ObjectSelect.jsx | 178 ++++++++++-------- 1 file changed, 96 insertions(+), 82 deletions(-) diff --git a/src/components/Dashboard/common/ObjectSelect.jsx b/src/components/Dashboard/common/ObjectSelect.jsx index 316273ab..9024ab86 100644 --- a/src/components/Dashboard/common/ObjectSelect.jsx +++ b/src/components/Dashboard/common/ObjectSelect.jsx @@ -17,6 +17,56 @@ import { getModelProperty } from '../../../database/ObjectModels' const { SHOW_CHILD } = TreeSelect const EMPTY_OBJECT = {} +const EMPTY_GROUP_TOKEN = '__null__' + +// Null and omitted references share one token. JSON drops undefined, so the +// empty group has to use the same key the select expands with. +const getGroupValueToken = (value) => { + if (value == null || value === '' || value === EMPTY_GROUP_TOKEN) { + return EMPTY_GROUP_TOKEN + } + if (Array.isArray(value)) { + return value.map((item) => getGroupValueToken(item)).join(',') + } + if (typeof value === 'object') { + if (value._id != null && value._id !== '') return String(value._id) + if (value.name != null && value.name !== '') return String(value.name) + return EMPTY_GROUP_TOKEN + } + return String(value) +} + +const groupValuesMatch = (left, right) => + getGroupValueToken(left) === getGroupValueToken(right) + +// Property groups always have `property` and no top-level `_id`. An empty +// reference omits `value`, and must not be treated as a leaf. +const isPropertyGroup = (item) => + Boolean(item?.property) && (item._id == null || item._id === '') + +const buildSelectionPath = (fullValue, properties) => { + const valueFilter = {} + const pathKeys = [] + const parentKeys = [] + const looksComplete = + fullValue && + typeof fullValue === 'object' && + Object.keys(fullValue).some((key) => key !== '_id') + + properties.forEach((prop) => { + const hasProp = + fullValue && Object.prototype.hasOwnProperty.call(fullValue, prop) + if (!hasProp && !looksComplete) return + + const filterValue = hasProp ? fullValue[prop] : null + const valueString = getGroupValueToken(filterValue) + valueFilter[prop] = valueString + pathKeys.push(parentKeys.concat(`${prop}:${valueString}`).join('-')) + parentKeys.push(valueString) + }) + + return { valueFilter, pathKeys } +} // Helper to check if two values are equal (handling objects/ids) const areValuesEqual = (v1, v2) => { @@ -134,7 +184,7 @@ const getBranchChildren = (data, filterPath) => { const match = nodeSpecificChildren.find( (group) => group.property === pathItem.property && - areValuesEqual(group.value, pathItem.value) + groupValuesMatch(group.value, pathItem.value) ) if (match) { nodeSpecificChildren = match.children @@ -151,7 +201,7 @@ const replaceBranchChildren = (nodes, filterPath, newChildren, depth = 0) => { const { property, value } = filterPath[depth] return nodes.map((node) => { - if (node.property !== property || !areValuesEqual(node.value, value)) { + if (node.property !== property || !groupValuesMatch(node.value, value)) { return node } const nextDepth = depth + 1 @@ -250,6 +300,7 @@ const ObjectSelect = ({ const filterRef = useRef(filter) const masterFilterRef = useRef(masterFilter) const clearedMissingValueRef = useRef(false) + const valueExpandRef = useRef(null) const loadGenerationRef = useRef(0) const loadedKeysRef = useRef(new Set()) const treeSelectValueRef = useRef(null) @@ -328,22 +379,15 @@ const ObjectSelect = ({ const merged = [...current] // Helper to generate a unique key for a group node - const getGroupKey = (item) => { - const val = item.value - const valPart = - val && typeof val === 'object' && val._id - ? val._id - : JSON.stringify(val) - return `${item.property}:${valPart}` - } + const getGroupKey = (item) => + `${item.property}:${getGroupValueToken(item.value)}` for (const item of incoming) { - if (item.property && item.value !== undefined) { - // It's a group node + if (isPropertyGroup(item)) { + // It's a group node, including an empty reference with no value const itemKey = getGroupKey(item) const existingIdx = merged.findIndex( - (x) => - x.property && x.value !== undefined && getGroupKey(x) === itemKey + (x) => isPropertyGroup(x) && getGroupKey(x) === itemKey ) if (existingIdx > -1) { @@ -506,13 +550,7 @@ const ObjectSelect = ({ if (!group.property) return null const { property, value, children } = group - var valueString = value - if (value && typeof value === 'object' && value._id) { - valueString = value._id - } - if (Array.isArray(valueString)) { - valueString = valueString.join(',') - } + const valueString = getGroupValueToken(value) const nodeKey = parentKeys .concat(property + ':' + valueString) .join('-') @@ -885,6 +923,7 @@ const ObjectSelect = ({ loadedKeysRef.current = new Set() setInitialized(false) valueRef.current = null + valueExpandRef.current = null setTreeSelectValue(null) setInitialLoading(true) setReloading(false) @@ -957,6 +996,18 @@ const ObjectSelect = ({ getValueIdentity(valueRef.current) !== valueIdentity ) { valueRef.current = value + const loadedValues = multiple + ? Array.isArray(value) + ? value + : [] + : [value] + const pathKeys = loadedValues.flatMap( + (item) => buildSelectionPath(item, properties).pathKeys + ) + if (pathKeys.length > 0) { + setExpandedKeys([...new Set(pathKeys)]) + pathKeys.forEach((key) => loadedKeysRef.current.add(key)) + } setTreeSelectValue( multiple ? ids.map((id) => toSelectValue(id)).filter((id) => id != null) @@ -973,6 +1024,7 @@ const ObjectSelect = ({ getValueIdentity(valueRef.current) !== getValueIdentity(value) && type != 'unknown' ) { + valueExpandRef.current = getValueIdentity(value) valueRef.current = value const fullValues = await Promise.all(value.map(fetchFullObjectIfNeeded)) if (generation !== loadGenerationRef.current) return @@ -984,41 +1036,24 @@ const ObjectSelect = ({ if (Array.isArray(data)) applyTreeFromData(data) } else { for (const fullValue of fullValues) { - const valueFilter = { ...filter } - const parentKeys = [] + const { valueFilter, pathKeys: valuePathKeys } = buildSelectionPath( + fullValue, + properties + ) + pathKeys.push(...valuePathKeys) - properties.forEach((prop) => { - if (!Object.prototype.hasOwnProperty.call(fullValue, prop)) return - - const filterValue = fullValue[prop] - let valueString = filterValue - if ( - filterValue && - typeof filterValue === 'object' && - filterValue._id - ) { - valueString = filterValue._id - } else if (filterValue?.name) { - valueString = filterValue.name - } else if (Array.isArray(filterValue)) { - valueString = filterValue.join(',') - } - - valueFilter[prop] = valueString - pathKeys.push( - parentKeys.concat(prop + ':' + valueString).join('-') - ) - parentKeys.push(valueString) + const data = await handleFetchObjectsProperties({ + ...filter, + ...valueFilter }) - - const data = await handleFetchObjectsProperties(valueFilter) if (generation !== loadGenerationRef.current) return if (Array.isArray(data)) applyTreeFromData(data) } } - setExpandedKeys([...new Set(pathKeys)]) - pathKeys.forEach((key) => loadedKeysRef.current.add(key)) + const expandedPathKeys = [...new Set(pathKeys)] + expandedPathKeys.forEach((key) => loadedKeysRef.current.add(key)) + setExpandedKeys(expandedPathKeys) setTreeSelectValue( value .map((item) => toSelectValue(getValueId(item))) @@ -1034,51 +1069,29 @@ const ObjectSelect = ({ getValueIdentity(valueRef.current) !== getValueIdentity(value) && type != 'unknown' ) { + valueExpandRef.current = getValueIdentity(value) valueRef.current = value const fullValue = await fetchFullObjectIfNeeded(value) if (generation !== loadGenerationRef.current) return - const valueFilter = { ...filter } - const pathKeys = [] - const parentKeys = [] - properties.forEach((prop) => { - if (Object.prototype.hasOwnProperty.call(fullValue, prop)) { - const filterValue = fullValue[prop] - let valueString = filterValue - if ( - filterValue && - typeof filterValue === 'object' && - filterValue._id - ) { - valueFilter[prop] = filterValue._id - valueString = filterValue._id - } else if (filterValue?.name) { - valueFilter[prop] = filterValue.name - valueString = filterValue.name - } else if (Array.isArray(filterValue)) { - valueFilter[prop] = filterValue.join(',') - valueString = filterValue.join(',') - } else { - valueFilter[prop] = filterValue - valueString = filterValue - } - const nodeKey = parentKeys - .concat(prop + ':' + valueString) - .join('-') - pathKeys.push(nodeKey) - parentKeys.push(valueString) - } - }) - setExpandedKeys(pathKeys) + const { valueFilter, pathKeys } = buildSelectionPath( + fullValue, + properties + ) pathKeys.forEach((key) => loadedKeysRef.current.add(key)) - const data = await handleFetchObjectsProperties(valueFilter) + const data = await handleFetchObjectsProperties({ + ...filter, + ...valueFilter + }) if (generation !== loadGenerationRef.current) return if (Array.isArray(data)) applyTreeFromData(data) + setExpandedKeys(pathKeys) setTreeSelectValue(toSelectValue(valueRef.current._id)) setInitialized(true) return } if ( !initialized && + valueExpandRef.current == null && token != null && type != 'unknown' && type != undefined && @@ -1093,6 +1106,7 @@ const ObjectSelect = ({ type == undefined || connected == false ) { + valueExpandRef.current = null setTreeSelectValue(null) setInitialLoading(false) setInitialized(true)