yangdx commited on
Commit
467ab64
·
1 Parent(s): 9f83e85

Fix reslectiton problem by efactor graph search input box handling logic

Browse files
lightrag_webui/src/components/ui/AsyncSearch.tsx CHANGED
@@ -1,4 +1,4 @@
1
- import React, { useState, useEffect, useCallback } from 'react'
2
  import { Loader2 } from 'lucide-react'
3
  import { useDebounce } from '@/hooks/useDebounce'
4
 
@@ -81,100 +81,97 @@ export function AsyncSearch<T>({
81
  const [options, setOptions] = useState<T[]>([])
82
  const [loading, setLoading] = useState(false)
83
  const [error, setError] = useState<string | null>(null)
84
- const [selectedValue, setSelectedValue] = useState(value)
85
- const [focusedValue, setFocusedValue] = useState<string | null>(null)
86
  const [searchTerm, setSearchTerm] = useState('')
87
  const debouncedSearchTerm = useDebounce(searchTerm, preload ? 0 : 150)
88
- const [originalOptions, setOriginalOptions] = useState<T[]>([])
89
 
90
  useEffect(() => {
91
  setMounted(true)
92
- setSelectedValue(value)
93
- }, [value])
94
 
95
- // Effect for initial fetch
96
  useEffect(() => {
97
- const initializeOptions = async () => {
98
- try {
99
- setLoading(true)
100
- setError(null)
101
- // If we have a value, use it for the initial search
102
- const data = value !== null ? await fetcher(value) : []
103
- setOriginalOptions(data)
104
- setOptions(data)
105
- } catch (err) {
106
- setError(err instanceof Error ? err.message : 'Failed to fetch options')
107
- } finally {
108
- setLoading(false)
109
  }
110
  }
111
 
112
- if (!mounted) {
113
- initializeOptions()
 
114
  }
115
- }, [mounted, fetcher, value])
116
 
117
- useEffect(() => {
118
- const fetchOptions = async () => {
119
- try {
120
- setLoading(true)
121
- setError(null)
122
- const data = await fetcher(debouncedSearchTerm)
123
- setOriginalOptions(data)
124
- setOptions(data)
125
- } catch (err) {
126
- setError(err instanceof Error ? err.message : 'Failed to fetch options')
127
- } finally {
128
- setLoading(false)
129
- }
130
  }
 
 
 
 
 
131
 
132
- if (!mounted) {
133
- fetchOptions()
134
- } else if (!preload) {
135
- fetchOptions()
136
- } else if (preload) {
137
  if (debouncedSearchTerm) {
138
- setOptions(
139
- originalOptions.filter((option) =>
140
  filterFn ? filterFn(option, debouncedSearchTerm) : true
141
  )
142
  )
143
- } else {
144
- setOptions(originalOptions)
145
  }
 
 
146
  }
147
- // eslint-disable-next-line react-hooks/exhaustive-deps
148
- }, [fetcher, debouncedSearchTerm, mounted, preload, filterFn])
149
-
150
- const handleSelect = useCallback(
151
- (currentValue: string) => {
152
- if (currentValue !== selectedValue) {
153
- setSelectedValue(currentValue)
154
- onChange(currentValue)
155
- }
 
 
 
 
 
 
156
  setOpen(false)
157
- },
158
- [selectedValue, setSelectedValue, setOpen, onChange]
159
- )
160
 
161
- const handleFocus = useCallback(
162
- (currentValue: string) => {
163
- if (currentValue !== focusedValue) {
164
- setFocusedValue(currentValue)
165
- onFocus(currentValue)
166
- }
167
- },
168
- [focusedValue, setFocusedValue, onFocus]
169
- )
 
 
 
170
 
171
  return (
172
  <div
 
173
  className={cn(disabled && 'cursor-not-allowed opacity-50', className)}
174
- onFocus={() => {
175
- setOpen(true)
176
- }}
177
- onBlur={() => setOpen(false)}
178
  >
179
  <Command shouldFilter={false} className="bg-transparent">
180
  <div>
@@ -182,12 +179,13 @@ export function AsyncSearch<T>({
182
  placeholder={placeholder}
183
  value={searchTerm}
184
  className="max-h-8"
 
185
  onValueChange={(value) => {
186
  setSearchTerm(value)
187
- if (value && !open) setOpen(true)
188
  }}
189
  />
190
- {loading && options.length > 0 && (
191
  <div className="absolute top-1/2 right-2 flex -translate-y-1/2 transform items-center">
192
  <Loader2 className="h-4 w-4 animate-spin" />
193
  </div>
@@ -209,8 +207,8 @@ export function AsyncSearch<T>({
209
  key={getOptionValue(option) + `${idx}`}
210
  value={getOptionValue(option)}
211
  onSelect={handleSelect}
212
- onMouseEnter={() => handleFocus(getOptionValue(option))}
213
- className="truncate"
214
  >
215
  {renderOption(option)}
216
  </CommandItem>
 
1
+ import React, { useState, useEffect, useCallback, useRef } from 'react'
2
  import { Loader2 } from 'lucide-react'
3
  import { useDebounce } from '@/hooks/useDebounce'
4
 
 
81
  const [options, setOptions] = useState<T[]>([])
82
  const [loading, setLoading] = useState(false)
83
  const [error, setError] = useState<string | null>(null)
 
 
84
  const [searchTerm, setSearchTerm] = useState('')
85
  const debouncedSearchTerm = useDebounce(searchTerm, preload ? 0 : 150)
86
+ const containerRef = useRef<HTMLDivElement>(null)
87
 
88
  useEffect(() => {
89
  setMounted(true)
90
+ }, [])
 
91
 
92
+ // Handle clicks outside of the component
93
  useEffect(() => {
94
+ const handleClickOutside = (event: MouseEvent) => {
95
+ if (
96
+ containerRef.current &&
97
+ !containerRef.current.contains(event.target as Node) &&
98
+ open
99
+ ) {
100
+ setOpen(false)
 
 
 
 
 
101
  }
102
  }
103
 
104
+ document.addEventListener('mousedown', handleClickOutside)
105
+ return () => {
106
+ document.removeEventListener('mousedown', handleClickOutside)
107
  }
108
+ }, [open])
109
 
110
+ const fetchOptions = useCallback(async (query: string) => {
111
+ try {
112
+ setLoading(true)
113
+ setError(null)
114
+ const data = await fetcher(query)
115
+ setOptions(data)
116
+ } catch (err) {
117
+ setError(err instanceof Error ? err.message : 'Failed to fetch options')
118
+ } finally {
119
+ setLoading(false)
 
 
 
120
  }
121
+ }, [fetcher])
122
+
123
+ // Load options when search term changes
124
+ useEffect(() => {
125
+ if (!mounted) return
126
 
127
+ if (preload) {
 
 
 
 
128
  if (debouncedSearchTerm) {
129
+ setOptions((prev) =>
130
+ prev.filter((option) =>
131
  filterFn ? filterFn(option, debouncedSearchTerm) : true
132
  )
133
  )
 
 
134
  }
135
+ } else {
136
+ fetchOptions(debouncedSearchTerm)
137
  }
138
+ }, [mounted, debouncedSearchTerm, preload, filterFn, fetchOptions])
139
+
140
+ // Load initial value
141
+ useEffect(() => {
142
+ if (!mounted || !value) return
143
+ fetchOptions(value)
144
+ }, [mounted, value, fetchOptions])
145
+
146
+ const handleSelect = useCallback((currentValue: string) => {
147
+ onChange(currentValue)
148
+ requestAnimationFrame(() => {
149
+ // Blur the input to ensure focus event triggers on next click
150
+ const input = document.activeElement as HTMLElement
151
+ input?.blur()
152
+ // Close the dropdown
153
  setOpen(false)
154
+ })
155
+ }, [onChange])
 
156
 
157
+ const handleFocus = useCallback(() => {
158
+ setOpen(true)
159
+ // Use current search term to fetch options
160
+ fetchOptions(searchTerm)
161
+ }, [searchTerm, fetchOptions])
162
+
163
+ const handleMouseDown = useCallback((e: React.MouseEvent) => {
164
+ const target = e.target as HTMLElement
165
+ if (target.closest('.cmd-item')) {
166
+ e.preventDefault()
167
+ }
168
+ }, [])
169
 
170
  return (
171
  <div
172
+ ref={containerRef}
173
  className={cn(disabled && 'cursor-not-allowed opacity-50', className)}
174
+ onMouseDown={handleMouseDown}
 
 
 
175
  >
176
  <Command shouldFilter={false} className="bg-transparent">
177
  <div>
 
179
  placeholder={placeholder}
180
  value={searchTerm}
181
  className="max-h-8"
182
+ onFocus={handleFocus}
183
  onValueChange={(value) => {
184
  setSearchTerm(value)
185
+ if (!open) setOpen(true)
186
  }}
187
  />
188
+ {loading && (
189
  <div className="absolute top-1/2 right-2 flex -translate-y-1/2 transform items-center">
190
  <Loader2 className="h-4 w-4 animate-spin" />
191
  </div>
 
207
  key={getOptionValue(option) + `${idx}`}
208
  value={getOptionValue(option)}
209
  onSelect={handleSelect}
210
+ onMouseMove={() => onFocus(getOptionValue(option))}
211
+ className="truncate cmd-item"
212
  >
213
  {renderOption(option)}
214
  </CommandItem>