Implement search functionality in ObjectSelect component, allowing users to filter selectable tree nodes. Update ApiServerContext to include searchObjects method for backend integration. Adjust state management for search operations and enhance user interaction with keyboard navigation support.
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
This commit is contained in:
parent
612b250529
commit
62805b0d6a
@ -26,9 +26,19 @@ const areValuesEqual = (v1, v2) => {
|
|||||||
return String(id1) === String(id2)
|
return String(id1) === String(id2)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const getFirstSelectableLeaf = (nodes) => {
|
||||||
|
if (!Array.isArray(nodes)) return null
|
||||||
|
for (const node of nodes) {
|
||||||
|
if (node.isLeaf && node.selectable !== false) return node
|
||||||
|
const found = getFirstSelectableLeaf(node.children)
|
||||||
|
if (found) return found
|
||||||
|
}
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
|
||||||
const ObjectSelect = ({
|
const ObjectSelect = ({
|
||||||
type = 'unknown',
|
type = 'unknown',
|
||||||
showSearch = false,
|
showSearch = true,
|
||||||
multiple = false,
|
multiple = false,
|
||||||
treeSelectProps = EMPTY_OBJECT,
|
treeSelectProps = EMPTY_OBJECT,
|
||||||
filter = EMPTY_OBJECT,
|
filter = EMPTY_OBJECT,
|
||||||
@ -38,7 +48,7 @@ const ObjectSelect = ({
|
|||||||
disabled = false,
|
disabled = false,
|
||||||
...rest
|
...rest
|
||||||
}) => {
|
}) => {
|
||||||
const { fetchObjectsByProperty, fetchObject, connected } =
|
const { fetchObjectsByProperty, fetchObject, connected, searchObjects } =
|
||||||
useContext(ApiServerContext)
|
useContext(ApiServerContext)
|
||||||
const { token } = useContext(AuthContext)
|
const { token } = useContext(AuthContext)
|
||||||
// --- State ---
|
// --- State ---
|
||||||
@ -52,6 +62,9 @@ const ObjectSelect = ({
|
|||||||
const [initialLoading, setInitialLoading] = useState(true)
|
const [initialLoading, setInitialLoading] = 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 [searchValue, setSearchValue] = useState('')
|
||||||
|
const searchRequestIdRef = useRef(0)
|
||||||
const valueRef = useRef(null)
|
const valueRef = useRef(null)
|
||||||
|
|
||||||
// Refs to track value changes
|
// Refs to track value changes
|
||||||
@ -345,6 +358,7 @@ const ObjectSelect = ({
|
|||||||
const onTreeSelectChange = useCallback(
|
const onTreeSelectChange = useCallback(
|
||||||
(value) => {
|
(value) => {
|
||||||
// Mark this as an internal change
|
// Mark this as an internal change
|
||||||
|
setIsSearching(false)
|
||||||
isInternalChangeRef.current = true
|
isInternalChangeRef.current = true
|
||||||
|
|
||||||
// value can be a string (single) or array (multiple)
|
// value can be a string (single) or array (multiple)
|
||||||
@ -368,8 +382,55 @@ const ObjectSelect = ({
|
|||||||
[multiple, objectList, onChange]
|
[multiple, objectList, onChange]
|
||||||
)
|
)
|
||||||
|
|
||||||
|
const onSearch = useCallback(
|
||||||
|
async (search) => {
|
||||||
|
setSearchValue(search)
|
||||||
|
const trimmed = search?.trim() ?? ''
|
||||||
|
if (!trimmed) {
|
||||||
|
searchRequestIdRef.current += 1
|
||||||
|
setIsSearching(false)
|
||||||
|
setTreeData(buildTreeData(objectPropertiesTree))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
const requestId = ++searchRequestIdRef.current
|
||||||
|
setIsSearching(true)
|
||||||
|
const data = await searchObjects(type, trimmed)
|
||||||
|
if (requestId !== searchRequestIdRef.current) return
|
||||||
|
if (!Array.isArray(data)) return
|
||||||
|
|
||||||
|
setTreeData(buildTreeData(data, properties.length))
|
||||||
|
},
|
||||||
|
[
|
||||||
|
searchObjects,
|
||||||
|
type,
|
||||||
|
buildTreeData,
|
||||||
|
objectPropertiesTree,
|
||||||
|
properties.length
|
||||||
|
]
|
||||||
|
)
|
||||||
|
|
||||||
|
const onInputKeyDown = useCallback(
|
||||||
|
(e) => {
|
||||||
|
treeSelectProps.onInputKeyDown?.(e)
|
||||||
|
if (e.defaultPrevented || e.key !== 'Enter' || !isSearching) return
|
||||||
|
|
||||||
|
const firstLeaf = getFirstSelectableLeaf(treeData)
|
||||||
|
if (!firstLeaf?.value) return
|
||||||
|
|
||||||
|
e.preventDefault()
|
||||||
|
e.stopPropagation()
|
||||||
|
onTreeSelectChange(
|
||||||
|
multiple ? [firstLeaf.value] : firstLeaf.value
|
||||||
|
)
|
||||||
|
setSearchValue('')
|
||||||
|
onSearch('')
|
||||||
|
},
|
||||||
|
[treeSelectProps, isSearching, treeData, multiple, onTreeSelectChange, onSearch]
|
||||||
|
)
|
||||||
// Update treeData when objectPropertiesTree changes
|
// Update treeData when objectPropertiesTree changes
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
|
if (isSearching) return
|
||||||
if (objectPropertiesTree && Object.keys(objectPropertiesTree).length > 0) {
|
if (objectPropertiesTree && Object.keys(objectPropertiesTree).length > 0) {
|
||||||
const newTreeData = buildTreeData(objectPropertiesTree)
|
const newTreeData = buildTreeData(objectPropertiesTree)
|
||||||
setTreeData((prev) => {
|
setTreeData((prev) => {
|
||||||
@ -379,7 +440,7 @@ const ObjectSelect = ({
|
|||||||
return prev
|
return prev
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
}, [objectPropertiesTree, properties, buildTreeData])
|
}, [objectPropertiesTree, properties, buildTreeData, isSearching])
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const handleValue = async () => {
|
const handleValue = async () => {
|
||||||
@ -480,6 +541,9 @@ const ObjectSelect = ({
|
|||||||
JSON.stringify(prevValues.masterFilter) !== JSON.stringify(masterFilter)
|
JSON.stringify(prevValues.masterFilter) !== JSON.stringify(masterFilter)
|
||||||
|
|
||||||
if (hasChanged) {
|
if (hasChanged) {
|
||||||
|
searchRequestIdRef.current += 1
|
||||||
|
setIsSearching(false)
|
||||||
|
setSearchValue('')
|
||||||
setObjectPropertiesTree({})
|
setObjectPropertiesTree({})
|
||||||
setObjectList([])
|
setObjectList([])
|
||||||
setTreeData([])
|
setTreeData([])
|
||||||
@ -559,14 +623,18 @@ const ObjectSelect = ({
|
|||||||
onTreeExpand={setExpandedKeys}
|
onTreeExpand={setExpandedKeys}
|
||||||
treeData={treeData}
|
treeData={treeData}
|
||||||
showSearch={showSearch}
|
showSearch={showSearch}
|
||||||
|
filterTreeNode={() => true}
|
||||||
multiple={multiple}
|
multiple={multiple}
|
||||||
loadData={loadData}
|
loadData={isSearching ? undefined : loadData}
|
||||||
showCheckedStrategy={SHOW_CHILD}
|
showCheckedStrategy={SHOW_CHILD}
|
||||||
placeholder={placeholder}
|
placeholder={placeholder}
|
||||||
{...treeSelectProps}
|
{...treeSelectProps}
|
||||||
{...rest}
|
{...rest}
|
||||||
value={treeSelectValue}
|
value={treeSelectValue}
|
||||||
onChange={onTreeSelectChange}
|
onChange={onTreeSelectChange}
|
||||||
|
onSearch={onSearch}
|
||||||
|
onInputKeyDown={onInputKeyDown}
|
||||||
|
searchValue={searchValue}
|
||||||
disabled={disabled || type == 'unknown' || type == undefined}
|
disabled={disabled || type == 'unknown' || type == undefined}
|
||||||
/>
|
/>
|
||||||
)
|
)
|
||||||
|
|||||||
@ -744,20 +744,23 @@ const ApiServerProvider = ({ children }) => {
|
|||||||
[offLockEvent]
|
[offLockEvent]
|
||||||
)
|
)
|
||||||
|
|
||||||
const showError = useCallback((error, callback = null) => {
|
const showError = useCallback(
|
||||||
const code = error.response.data.code || 'UNKNOWN'
|
(error, callback = null) => {
|
||||||
if (code == 'UNAUTHORIZED') {
|
const code = error.response.data.code || 'UNKNOWN'
|
||||||
setUnauthenticated()
|
if (code == 'UNAUTHORIZED') {
|
||||||
return
|
setUnauthenticated()
|
||||||
}
|
return
|
||||||
var content = `Error ${error.code} (${error.status}): ${error.message}`
|
}
|
||||||
if (error.response?.data?.error) {
|
var content = `Error ${error.code} (${error.status}): ${error.message}`
|
||||||
content = `${error.response?.data?.error} (${error.status})`
|
if (error.response?.data?.error) {
|
||||||
}
|
content = `${error.response?.data?.error} (${error.status})`
|
||||||
setErrorModalContent(content)
|
}
|
||||||
setRetryCallback(() => callback)
|
setErrorModalContent(content)
|
||||||
setShowErrorModal(true)
|
setRetryCallback(() => callback)
|
||||||
}, [setUnauthenticated])
|
setShowErrorModal(true)
|
||||||
|
},
|
||||||
|
[setUnauthenticated]
|
||||||
|
)
|
||||||
|
|
||||||
const handleRetry = () => {
|
const handleRetry = () => {
|
||||||
setShowErrorModal(false)
|
setShowErrorModal(false)
|
||||||
@ -911,6 +914,27 @@ const ApiServerProvider = ({ children }) => {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const searchObjects = async (type, search) => {
|
||||||
|
const searchUrl = `${config.backendUrl}/${getObjectEndpoint(type)}/search`
|
||||||
|
logger.debug('Searching objects for ' + type, { search })
|
||||||
|
try {
|
||||||
|
const response = await axios.get(searchUrl, {
|
||||||
|
params: { search },
|
||||||
|
headers: {
|
||||||
|
Accept: 'application/json',
|
||||||
|
Authorization: `Bearer ${token}`
|
||||||
|
}
|
||||||
|
})
|
||||||
|
return response.data
|
||||||
|
} catch (err) {
|
||||||
|
console.error(err)
|
||||||
|
showError(err, () => {
|
||||||
|
searchObjects(type, search)
|
||||||
|
})
|
||||||
|
return []
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Update filament information
|
// Update filament information
|
||||||
const updateObject = async (id, type, value) => {
|
const updateObject = async (id, type, value) => {
|
||||||
const updateUrl = `${config.backendUrl}/${getObjectEndpoint(type)}/${id}`
|
const updateUrl = `${config.backendUrl}/${getObjectEndpoint(type)}/${id}`
|
||||||
@ -1824,6 +1848,7 @@ const ApiServerProvider = ({ children }) => {
|
|||||||
fetchObject,
|
fetchObject,
|
||||||
fetchObjects,
|
fetchObjects,
|
||||||
fetchObjectsByProperty,
|
fetchObjectsByProperty,
|
||||||
|
searchObjects,
|
||||||
fetchSpotlightData,
|
fetchSpotlightData,
|
||||||
getModelStats,
|
getModelStats,
|
||||||
getModelHistory,
|
getModelHistory,
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user