Enhance ObjectSelect Component with Improved Value Handling and State Management
- Updated value comparison logic in areValuesEqual to handle null values and case insensitivity. - Introduced load generation tracking to manage asynchronous updates and prevent stale data handling. - Refactored onTreeSelectChange to improve selection handling for both single and multiple selections. - Added useEffect hooks to manage state changes based on type and masterFilter updates, enhancing component responsiveness. - Implemented findObjectById function for better object retrieval from the list, improving performance and clarity.
This commit is contained in:
parent
4d2ec19ff1
commit
5dca5ef42f
@ -22,7 +22,8 @@ const EMPTY_OBJECT = {}
|
|||||||
const areValuesEqual = (v1, v2) => {
|
const areValuesEqual = (v1, v2) => {
|
||||||
const id1 = v1 && typeof v1 === 'object' && v1._id ? v1._id : v1
|
const id1 = v1 && typeof v1 === 'object' && v1._id ? v1._id : v1
|
||||||
const id2 = v2 && typeof v2 === 'object' && v2._id ? v2._id : v2
|
const id2 = v2 && typeof v2 === 'object' && v2._id ? v2._id : v2
|
||||||
return String(id1) === String(id2)
|
if (id1 == null || id2 == null) return id1 == id2
|
||||||
|
return String(id1).toLowerCase() === String(id2).toLowerCase()
|
||||||
}
|
}
|
||||||
|
|
||||||
const toSelectValue = (id) => {
|
const toSelectValue = (id) => {
|
||||||
@ -249,6 +250,15 @@ 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 loadGenerationRef = useRef(0)
|
||||||
|
const getSelectKey = useCallback(
|
||||||
|
(selectType, selectMasterFilter) =>
|
||||||
|
`${selectType}::${JSON.stringify(selectMasterFilter ?? {})}`,
|
||||||
|
[]
|
||||||
|
)
|
||||||
|
const [committedSelectKey, setCommittedSelectKey] = useState(() =>
|
||||||
|
getSelectKey(type, masterFilter)
|
||||||
|
)
|
||||||
|
|
||||||
filterRef.current = filter
|
filterRef.current = filter
|
||||||
masterFilterRef.current = masterFilter
|
masterFilterRef.current = masterFilter
|
||||||
@ -366,12 +376,14 @@ 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, { replace = false } = {}) => {
|
||||||
|
const generation = loadGenerationRef.current
|
||||||
try {
|
try {
|
||||||
const data = await fetchObjectsByProperty(type, {
|
const data = await fetchObjectsByProperty(type, {
|
||||||
properties: properties,
|
properties: properties,
|
||||||
filter: customFilter,
|
filter: customFilter,
|
||||||
masterFilter
|
masterFilter
|
||||||
})
|
})
|
||||||
|
if (generation !== loadGenerationRef.current) return null
|
||||||
|
|
||||||
if (Array.isArray(data)) {
|
if (Array.isArray(data)) {
|
||||||
setObjectPropertiesTree((prev) =>
|
setObjectPropertiesTree((prev) =>
|
||||||
@ -551,6 +563,7 @@ const ObjectSelect = ({
|
|||||||
(data) => {
|
(data) => {
|
||||||
const { treeNodes, objects } = buildTreeData(data)
|
const { treeNodes, objects } = buildTreeData(data)
|
||||||
setObjectList(objects)
|
setObjectList(objects)
|
||||||
|
objectListRef.current = objects
|
||||||
setTreeData(treeNodes)
|
setTreeData(treeNodes)
|
||||||
treeDataRef.current = treeNodes
|
treeDataRef.current = treeNodes
|
||||||
|
|
||||||
@ -674,52 +687,64 @@ const ObjectSelect = ({
|
|||||||
updateEventHandlerRef.current = updateEventHandler
|
updateEventHandlerRef.current = updateEventHandler
|
||||||
newEventHandlerRef.current = newEventHandler
|
newEventHandlerRef.current = newEventHandler
|
||||||
|
|
||||||
|
const findObjectById = useCallback((id) => {
|
||||||
|
if (id == null || id === '') return null
|
||||||
|
return (
|
||||||
|
objectListRef.current.find((obj) => areValuesEqual(obj._id, id)) || null
|
||||||
|
)
|
||||||
|
}, [])
|
||||||
|
|
||||||
const onTreeSelectChange = useCallback(
|
const onTreeSelectChange = useCallback(
|
||||||
(value) => {
|
(nextValue) => {
|
||||||
const isEmptySelection = multiple
|
const isEmptySelection = multiple
|
||||||
? !Array.isArray(value) || value.length === 0
|
? !Array.isArray(nextValue) || nextValue.length === 0
|
||||||
: value == null || value === ''
|
: nextValue == null || nextValue === ''
|
||||||
if (
|
|
||||||
isEmptySelection &&
|
const pendingId = getValueId(valueRef.current)
|
||||||
treeDataRef.current.length === 0 &&
|
const pendingInTree =
|
||||||
valueRef.current != null
|
pendingId != null &&
|
||||||
) {
|
(isValueInTree(treeDataRef.current, pendingId) ||
|
||||||
|
findObjectById(pendingId) != null)
|
||||||
|
|
||||||
|
// TreeSelect emits empty/null when treeData is replaced or a controlled
|
||||||
|
// value is not in the tree yet. Don't wipe an external value we still own.
|
||||||
|
if (isEmptySelection && pendingId != null && !pendingInTree) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
setValueNotFound(false)
|
setValueNotFound(false)
|
||||||
clearedMissingValueRef.current = false
|
clearedMissingValueRef.current = false
|
||||||
// Mark this as an internal change
|
|
||||||
if (!multiple) setIsSearching(false)
|
if (!multiple) setIsSearching(false)
|
||||||
isInternalChangeRef.current = true
|
isInternalChangeRef.current = true
|
||||||
|
|
||||||
// value can be a string (single) or array (multiple)
|
|
||||||
if (multiple) {
|
if (multiple) {
|
||||||
// Multiple selection
|
|
||||||
let selectedObjects = []
|
let selectedObjects = []
|
||||||
if (Array.isArray(value)) {
|
if (Array.isArray(nextValue)) {
|
||||||
selectedObjects = value
|
selectedObjects = nextValue.map(findObjectById).filter(Boolean)
|
||||||
.map((id) => objectList.find((obj) => areValuesEqual(obj._id, id)))
|
|
||||||
.filter(Boolean)
|
|
||||||
}
|
}
|
||||||
setTreeSelectValue(value)
|
setTreeSelectValue(nextValue)
|
||||||
onChange?.(selectedObjects)
|
onChange?.(selectedObjects)
|
||||||
} else {
|
return
|
||||||
// Single selection: replace the previous object instead of emitting
|
}
|
||||||
// undefined (lodash merge skips undefined and would keep the old value).
|
|
||||||
if (value == null || value === '') {
|
if (nextValue == null || nextValue === '') {
|
||||||
setTreeSelectValue(null)
|
setTreeSelectValue(null)
|
||||||
onChange?.(null)
|
onChange?.(null)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
const selectedObject = objectList.find((obj) =>
|
|
||||||
areValuesEqual(obj._id, value)
|
const selectedObject = findObjectById(nextValue)
|
||||||
)
|
setTreeSelectValue(nextValue)
|
||||||
setTreeSelectValue(value)
|
if (selectedObject) {
|
||||||
onChange?.(selectedObject ?? null)
|
onChange?.(selectedObject)
|
||||||
|
return
|
||||||
}
|
}
|
||||||
|
if (pendingId != null && areValuesEqual(pendingId, nextValue)) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
onChange?.(null)
|
||||||
},
|
},
|
||||||
[multiple, objectList, onChange]
|
[multiple, onChange, findObjectById]
|
||||||
)
|
)
|
||||||
|
|
||||||
const onSearch = useCallback(
|
const onSearch = useCallback(
|
||||||
@ -831,8 +856,74 @@ const ObjectSelect = ({
|
|||||||
treeDataRef.current = treeData
|
treeDataRef.current = treeData
|
||||||
}, [treeData])
|
}, [treeData])
|
||||||
|
|
||||||
|
const prevValuesRef = useRef({ type, masterFilter })
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
|
const prevValues = prevValuesRef.current
|
||||||
|
|
||||||
|
// Deep comparison for objects, simple comparison for primitives
|
||||||
|
const hasChanged =
|
||||||
|
prevValues.type !== type ||
|
||||||
|
JSON.stringify(prevValues.masterFilter) !== JSON.stringify(masterFilter)
|
||||||
|
|
||||||
|
if (hasChanged) {
|
||||||
|
loadGenerationRef.current += 1
|
||||||
|
searchRequestIdRef.current += 1
|
||||||
|
setIsSearching(false)
|
||||||
|
setSearchValue('')
|
||||||
|
setObjectPropertiesTree({})
|
||||||
|
setObjectList([])
|
||||||
|
setTreeData([])
|
||||||
|
treeDataRef.current = []
|
||||||
|
setTreeVersion((v) => v + 1)
|
||||||
|
setExpandedKeys([])
|
||||||
|
setInitialized(false)
|
||||||
|
valueRef.current = null
|
||||||
|
setTreeSelectValue(null)
|
||||||
|
setInitialLoading(true)
|
||||||
|
setReloading(false)
|
||||||
|
setError(false)
|
||||||
|
setValueNotFound(false)
|
||||||
|
clearedMissingValueRef.current = false
|
||||||
|
prevValuesRef.current = { type, masterFilter }
|
||||||
|
setCommittedSelectKey(getSelectKey(type, masterFilter))
|
||||||
|
}
|
||||||
|
}, [type, masterFilter, getSelectKey])
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const currentValueIdentity = getValueIdentity(value)
|
||||||
|
const hasValueChanged =
|
||||||
|
prevValueIdentityRef.current !== currentValueIdentity
|
||||||
|
|
||||||
|
if (hasValueChanged) {
|
||||||
|
const changeSource = isInternalChangeRef.current ? 'internal' : 'external'
|
||||||
|
|
||||||
|
if (changeSource == 'external') {
|
||||||
|
loadGenerationRef.current += 1
|
||||||
|
setObjectPropertiesTree({})
|
||||||
|
setTreeData([])
|
||||||
|
treeDataRef.current = []
|
||||||
|
setInitialized(false)
|
||||||
|
setInitialLoading(true)
|
||||||
|
valueRef.current = null
|
||||||
|
clearedMissingValueRef.current = false
|
||||||
|
setValueNotFound(false)
|
||||||
|
}
|
||||||
|
|
||||||
|
isInternalChangeRef.current = false
|
||||||
|
prevValueRef.current = value
|
||||||
|
prevValueIdentityRef.current = currentValueIdentity
|
||||||
|
}
|
||||||
|
}, [value, getValueIdentity])
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (getSelectKey(type, masterFilter) !== committedSelectKey) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
const generation = loadGenerationRef.current
|
||||||
const handleValue = async () => {
|
const handleValue = async () => {
|
||||||
|
if (generation !== loadGenerationRef.current) return
|
||||||
if (
|
if (
|
||||||
multiple &&
|
multiple &&
|
||||||
Array.isArray(value) &&
|
Array.isArray(value) &&
|
||||||
@ -841,12 +932,15 @@ const ObjectSelect = ({
|
|||||||
) {
|
) {
|
||||||
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
|
||||||
const pathKeys = []
|
const pathKeys = []
|
||||||
|
|
||||||
if (fullValues.length === 0) {
|
if (fullValues.length === 0) {
|
||||||
handleFetchObjectsProperties()
|
const data = await handleFetchObjectsProperties()
|
||||||
|
if (generation !== loadGenerationRef.current) return
|
||||||
|
if (Array.isArray(data)) applyTreeFromData(data)
|
||||||
} else {
|
} else {
|
||||||
fullValues.forEach((fullValue) => {
|
for (const fullValue of fullValues) {
|
||||||
const valueFilter = { ...filter }
|
const valueFilter = { ...filter }
|
||||||
const parentKeys = []
|
const parentKeys = []
|
||||||
|
|
||||||
@ -874,8 +968,10 @@ const ObjectSelect = ({
|
|||||||
parentKeys.push(valueString)
|
parentKeys.push(valueString)
|
||||||
})
|
})
|
||||||
|
|
||||||
handleFetchObjectsProperties(valueFilter)
|
const data = await handleFetchObjectsProperties(valueFilter)
|
||||||
})
|
if (generation !== loadGenerationRef.current) return
|
||||||
|
if (Array.isArray(data)) applyTreeFromData(data)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
setExpandedKeys([...new Set(pathKeys)])
|
setExpandedKeys([...new Set(pathKeys)])
|
||||||
@ -895,9 +991,8 @@ const ObjectSelect = ({
|
|||||||
type != 'unknown'
|
type != 'unknown'
|
||||||
) {
|
) {
|
||||||
valueRef.current = value
|
valueRef.current = value
|
||||||
// Check if value is a minimal object and fetch full object if needed
|
|
||||||
const fullValue = await fetchFullObjectIfNeeded(value)
|
const fullValue = await fetchFullObjectIfNeeded(value)
|
||||||
// Build a new filter from value's properties that are in the properties list
|
if (generation !== loadGenerationRef.current) return
|
||||||
const valueFilter = { ...filter }
|
const valueFilter = { ...filter }
|
||||||
const pathKeys = []
|
const pathKeys = []
|
||||||
const parentKeys = []
|
const parentKeys = []
|
||||||
@ -922,7 +1017,6 @@ const ObjectSelect = ({
|
|||||||
valueFilter[prop] = filterValue
|
valueFilter[prop] = filterValue
|
||||||
valueString = filterValue
|
valueString = filterValue
|
||||||
}
|
}
|
||||||
// Build the path key for this property level
|
|
||||||
const nodeKey = parentKeys
|
const nodeKey = parentKeys
|
||||||
.concat(prop + ':' + valueString)
|
.concat(prop + ':' + valueString)
|
||||||
.join('-')
|
.join('-')
|
||||||
@ -930,10 +1024,10 @@ const ObjectSelect = ({
|
|||||||
parentKeys.push(valueString)
|
parentKeys.push(valueString)
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
// Expand the path to the object
|
|
||||||
setExpandedKeys(pathKeys)
|
setExpandedKeys(pathKeys)
|
||||||
// Fetch with the new filter
|
const data = await handleFetchObjectsProperties(valueFilter)
|
||||||
handleFetchObjectsProperties(valueFilter)
|
if (generation !== loadGenerationRef.current) return
|
||||||
|
if (Array.isArray(data)) applyTreeFromData(data)
|
||||||
setTreeSelectValue(toSelectValue(valueRef.current._id))
|
setTreeSelectValue(toSelectValue(valueRef.current._id))
|
||||||
setInitialized(true)
|
setInitialized(true)
|
||||||
return
|
return
|
||||||
@ -959,13 +1053,7 @@ const ObjectSelect = ({
|
|||||||
setInitialized(true)
|
setInitialized(true)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
const timeoutId = setTimeout(() => {
|
|
||||||
handleValue()
|
handleValue()
|
||||||
}, 10)
|
|
||||||
|
|
||||||
return () => {
|
|
||||||
clearTimeout(timeoutId)
|
|
||||||
}
|
|
||||||
}, [
|
}, [
|
||||||
value,
|
value,
|
||||||
filter,
|
filter,
|
||||||
@ -975,73 +1063,15 @@ const ObjectSelect = ({
|
|||||||
token,
|
token,
|
||||||
fetchFullObjectIfNeeded,
|
fetchFullObjectIfNeeded,
|
||||||
type,
|
type,
|
||||||
|
masterFilter,
|
||||||
|
committedSelectKey,
|
||||||
|
getSelectKey,
|
||||||
connected,
|
connected,
|
||||||
getValueIdentity,
|
getValueIdentity,
|
||||||
multiple
|
multiple,
|
||||||
|
applyTreeFromData
|
||||||
])
|
])
|
||||||
|
|
||||||
const prevValuesRef = useRef({ type, masterFilter })
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
const prevValues = prevValuesRef.current
|
|
||||||
|
|
||||||
// Deep comparison for objects, simple comparison for primitives
|
|
||||||
const hasChanged =
|
|
||||||
prevValues.type !== type ||
|
|
||||||
JSON.stringify(prevValues.masterFilter) !== JSON.stringify(masterFilter)
|
|
||||||
|
|
||||||
if (hasChanged) {
|
|
||||||
searchRequestIdRef.current += 1
|
|
||||||
setIsSearching(false)
|
|
||||||
setSearchValue('')
|
|
||||||
setObjectPropertiesTree({})
|
|
||||||
setObjectList([])
|
|
||||||
setTreeData([])
|
|
||||||
setTreeVersion((v) => v + 1)
|
|
||||||
setExpandedKeys([])
|
|
||||||
setInitialized(false)
|
|
||||||
valueRef.current = null
|
|
||||||
onTreeSelectChange(null)
|
|
||||||
setTreeSelectValue(null)
|
|
||||||
setInitialLoading(true)
|
|
||||||
setReloading(false)
|
|
||||||
setError(false)
|
|
||||||
setValueNotFound(false)
|
|
||||||
clearedMissingValueRef.current = false
|
|
||||||
prevValuesRef.current = { type, masterFilter }
|
|
||||||
}
|
|
||||||
}, [type, masterFilter, onTreeSelectChange])
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
// Check if value has actually changed
|
|
||||||
const currentValueIdentity = getValueIdentity(value)
|
|
||||||
const hasValueChanged =
|
|
||||||
prevValueIdentityRef.current !== currentValueIdentity
|
|
||||||
|
|
||||||
if (hasValueChanged) {
|
|
||||||
const changeSource = isInternalChangeRef.current ? 'internal' : 'external'
|
|
||||||
|
|
||||||
if (changeSource == 'external') {
|
|
||||||
setObjectPropertiesTree({})
|
|
||||||
setTreeData([])
|
|
||||||
treeDataRef.current = []
|
|
||||||
setInitialized(false)
|
|
||||||
setInitialLoading(true)
|
|
||||||
valueRef.current = null
|
|
||||||
clearedMissingValueRef.current = false
|
|
||||||
setValueNotFound(false)
|
|
||||||
prevValuesRef.current = { type, masterFilter }
|
|
||||||
}
|
|
||||||
|
|
||||||
// Reset the internal change flag
|
|
||||||
isInternalChangeRef.current = false
|
|
||||||
|
|
||||||
// Update the previous value reference
|
|
||||||
prevValueRef.current = value
|
|
||||||
prevValueIdentityRef.current = currentValueIdentity
|
|
||||||
}
|
|
||||||
}, [value, getValueIdentity, type, masterFilter])
|
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
objectListRef.current = objectList
|
objectListRef.current = objectList
|
||||||
}, [objectList])
|
}, [objectList])
|
||||||
@ -1137,16 +1167,6 @@ const ObjectSelect = ({
|
|||||||
|
|
||||||
if (missing) {
|
if (missing) {
|
||||||
setValueNotFound(true)
|
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
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -1154,7 +1174,7 @@ const ObjectSelect = ({
|
|||||||
if (value != null) {
|
if (value != null) {
|
||||||
setValueNotFound(false)
|
setValueNotFound(false)
|
||||||
}
|
}
|
||||||
}, [value, multiple, objectList, treeData, valueReady, onChange])
|
}, [value, multiple, objectList, treeData, valueReady])
|
||||||
|
|
||||||
const displayPlaceholder = valueNotFound
|
const displayPlaceholder = valueNotFound
|
||||||
? `${modelLabel.charAt(0).toUpperCase()}${modelLabel.slice(1).toLowerCase()} not found.`
|
? `${modelLabel.charAt(0).toUpperCase()}${modelLabel.slice(1).toLowerCase()} not found.`
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user