Enhance ObjectSelect Component with Group Value Handling and Selection Path Logic
Some checks reported errors
farmcontrol/farmcontrol-ui/pipeline/head Something is wrong with the build of this commit

- 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.
This commit is contained in:
Tom Butcher 2026-11-08 23:55:05 +00:00
parent 82d76db106
commit 9b86114d99

View File

@ -17,6 +17,56 @@ import { getModelProperty } from '../../../database/ObjectModels'
const { SHOW_CHILD } = TreeSelect const { SHOW_CHILD } = TreeSelect
const EMPTY_OBJECT = {} 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) // Helper to check if two values are equal (handling objects/ids)
const areValuesEqual = (v1, v2) => { const areValuesEqual = (v1, v2) => {
@ -134,7 +184,7 @@ const getBranchChildren = (data, filterPath) => {
const match = nodeSpecificChildren.find( const match = nodeSpecificChildren.find(
(group) => (group) =>
group.property === pathItem.property && group.property === pathItem.property &&
areValuesEqual(group.value, pathItem.value) groupValuesMatch(group.value, pathItem.value)
) )
if (match) { if (match) {
nodeSpecificChildren = match.children nodeSpecificChildren = match.children
@ -151,7 +201,7 @@ const replaceBranchChildren = (nodes, filterPath, newChildren, depth = 0) => {
const { property, value } = filterPath[depth] const { property, value } = filterPath[depth]
return nodes.map((node) => { return nodes.map((node) => {
if (node.property !== property || !areValuesEqual(node.value, value)) { if (node.property !== property || !groupValuesMatch(node.value, value)) {
return node return node
} }
const nextDepth = depth + 1 const nextDepth = depth + 1
@ -250,6 +300,7 @@ const ObjectSelect = ({
const filterRef = useRef(filter) const filterRef = useRef(filter)
const masterFilterRef = useRef(masterFilter) const masterFilterRef = useRef(masterFilter)
const clearedMissingValueRef = useRef(false) const clearedMissingValueRef = useRef(false)
const valueExpandRef = useRef(null)
const loadGenerationRef = useRef(0) const loadGenerationRef = useRef(0)
const loadedKeysRef = useRef(new Set()) const loadedKeysRef = useRef(new Set())
const treeSelectValueRef = useRef(null) const treeSelectValueRef = useRef(null)
@ -328,22 +379,15 @@ const ObjectSelect = ({
const merged = [...current] const merged = [...current]
// Helper to generate a unique key for a group node // Helper to generate a unique key for a group node
const getGroupKey = (item) => { const getGroupKey = (item) =>
const val = item.value `${item.property}:${getGroupValueToken(item.value)}`
const valPart =
val && typeof val === 'object' && val._id
? val._id
: JSON.stringify(val)
return `${item.property}:${valPart}`
}
for (const item of incoming) { for (const item of incoming) {
if (item.property && item.value !== undefined) { if (isPropertyGroup(item)) {
// It's a group node // It's a group node, including an empty reference with no value
const itemKey = getGroupKey(item) const itemKey = getGroupKey(item)
const existingIdx = merged.findIndex( const existingIdx = merged.findIndex(
(x) => (x) => isPropertyGroup(x) && getGroupKey(x) === itemKey
x.property && x.value !== undefined && getGroupKey(x) === itemKey
) )
if (existingIdx > -1) { if (existingIdx > -1) {
@ -506,13 +550,7 @@ const ObjectSelect = ({
if (!group.property) return null if (!group.property) return null
const { property, value, children } = group const { property, value, children } = group
var valueString = value const valueString = getGroupValueToken(value)
if (value && typeof value === 'object' && value._id) {
valueString = value._id
}
if (Array.isArray(valueString)) {
valueString = valueString.join(',')
}
const nodeKey = parentKeys const nodeKey = parentKeys
.concat(property + ':' + valueString) .concat(property + ':' + valueString)
.join('-') .join('-')
@ -885,6 +923,7 @@ const ObjectSelect = ({
loadedKeysRef.current = new Set() loadedKeysRef.current = new Set()
setInitialized(false) setInitialized(false)
valueRef.current = null valueRef.current = null
valueExpandRef.current = null
setTreeSelectValue(null) setTreeSelectValue(null)
setInitialLoading(true) setInitialLoading(true)
setReloading(false) setReloading(false)
@ -957,6 +996,18 @@ const ObjectSelect = ({
getValueIdentity(valueRef.current) !== valueIdentity getValueIdentity(valueRef.current) !== valueIdentity
) { ) {
valueRef.current = value 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( setTreeSelectValue(
multiple multiple
? ids.map((id) => toSelectValue(id)).filter((id) => id != null) ? ids.map((id) => toSelectValue(id)).filter((id) => id != null)
@ -973,6 +1024,7 @@ const ObjectSelect = ({
getValueIdentity(valueRef.current) !== getValueIdentity(value) && getValueIdentity(valueRef.current) !== getValueIdentity(value) &&
type != 'unknown' type != 'unknown'
) { ) {
valueExpandRef.current = getValueIdentity(value)
valueRef.current = value valueRef.current = value
const fullValues = await Promise.all(value.map(fetchFullObjectIfNeeded)) const fullValues = await Promise.all(value.map(fetchFullObjectIfNeeded))
if (generation !== loadGenerationRef.current) return if (generation !== loadGenerationRef.current) return
@ -984,41 +1036,24 @@ const ObjectSelect = ({
if (Array.isArray(data)) applyTreeFromData(data) if (Array.isArray(data)) applyTreeFromData(data)
} else { } else {
for (const fullValue of fullValues) { for (const fullValue of fullValues) {
const valueFilter = { ...filter } const { valueFilter, pathKeys: valuePathKeys } = buildSelectionPath(
const parentKeys = [] fullValue,
properties
)
pathKeys.push(...valuePathKeys)
properties.forEach((prop) => { const data = await handleFetchObjectsProperties({
if (!Object.prototype.hasOwnProperty.call(fullValue, prop)) return ...filter,
...valueFilter
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(valueFilter)
if (generation !== loadGenerationRef.current) return if (generation !== loadGenerationRef.current) return
if (Array.isArray(data)) applyTreeFromData(data) if (Array.isArray(data)) applyTreeFromData(data)
} }
} }
setExpandedKeys([...new Set(pathKeys)]) const expandedPathKeys = [...new Set(pathKeys)]
pathKeys.forEach((key) => loadedKeysRef.current.add(key)) expandedPathKeys.forEach((key) => loadedKeysRef.current.add(key))
setExpandedKeys(expandedPathKeys)
setTreeSelectValue( setTreeSelectValue(
value value
.map((item) => toSelectValue(getValueId(item))) .map((item) => toSelectValue(getValueId(item)))
@ -1034,51 +1069,29 @@ const ObjectSelect = ({
getValueIdentity(valueRef.current) !== getValueIdentity(value) && getValueIdentity(valueRef.current) !== getValueIdentity(value) &&
type != 'unknown' type != 'unknown'
) { ) {
valueExpandRef.current = getValueIdentity(value)
valueRef.current = value valueRef.current = value
const fullValue = await fetchFullObjectIfNeeded(value) const fullValue = await fetchFullObjectIfNeeded(value)
if (generation !== loadGenerationRef.current) return if (generation !== loadGenerationRef.current) return
const valueFilter = { ...filter } const { valueFilter, pathKeys } = buildSelectionPath(
const pathKeys = [] fullValue,
const parentKeys = [] properties
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)
pathKeys.forEach((key) => loadedKeysRef.current.add(key)) pathKeys.forEach((key) => loadedKeysRef.current.add(key))
const data = await handleFetchObjectsProperties(valueFilter) const data = await handleFetchObjectsProperties({
...filter,
...valueFilter
})
if (generation !== loadGenerationRef.current) return if (generation !== loadGenerationRef.current) return
if (Array.isArray(data)) applyTreeFromData(data) if (Array.isArray(data)) applyTreeFromData(data)
setExpandedKeys(pathKeys)
setTreeSelectValue(toSelectValue(valueRef.current._id)) setTreeSelectValue(toSelectValue(valueRef.current._id))
setInitialized(true) setInitialized(true)
return return
} }
if ( if (
!initialized && !initialized &&
valueExpandRef.current == null &&
token != null && token != null &&
type != 'unknown' && type != 'unknown' &&
type != undefined && type != undefined &&
@ -1093,6 +1106,7 @@ const ObjectSelect = ({
type == undefined || type == undefined ||
connected == false connected == false
) { ) {
valueExpandRef.current = null
setTreeSelectValue(null) setTreeSelectValue(null)
setInitialLoading(false) setInitialLoading(false)
setInitialized(true) setInitialized(true)