Skip to content
Toggle navigation
P
Projects
G
Groups
S
Snippets
Help
phsl
/
new-api
This project
Loading...
Sign in
Toggle navigation
Go to a project
Project
Repository
Issues
0
Merge Requests
0
Pipelines
Wiki
Snippets
Members
Activity
Graph
Charts
Create a new issue
Jobs
Commits
Issue Boards
Files
Commits
Branches
Tags
Contributors
Graph
Compare
Charts
Unverified
Commit
30d3a3a5
authored
Jun 10, 2026
by
Q.A.zh
Committed by
GitHub
Jun 10, 2026
Browse files
Options
Browse Files
Download
Email Patches
Plain Diff
perf(web): add debounce channel search and skip during IME composition (#5393)
parent
d2576ddc
Hide whitespace changes
Inline
Side-by-side
Showing
2 changed files
with
162 additions
and
17 deletions
+162
-17
web/default/src/components/data-table/toolbar.tsx
+105
-11
web/default/src/features/channels/components/channels-table.tsx
+57
-6
No files found.
web/default/src/components/data-table/toolbar.tsx
View file @
30d3a3a5
...
@@ -21,6 +21,7 @@ import { useState, type ReactNode } from 'react'
...
@@ -21,6 +21,7 @@ import { useState, type ReactNode } from 'react'
import
{
type
Table
}
from
'@tanstack/react-table'
import
{
type
Table
}
from
'@tanstack/react-table'
import
{
ChevronDown
,
Loader2
,
X
as
Cross2Icon
}
from
'lucide-react'
import
{
ChevronDown
,
Loader2
,
X
as
Cross2Icon
}
from
'lucide-react'
import
{
useTranslation
}
from
'react-i18next'
import
{
useTranslation
}
from
'react-i18next'
import
{
useDebounce
}
from
'@/hooks'
import
{
cn
}
from
'@/lib/utils'
import
{
cn
}
from
'@/lib/utils'
import
{
Button
}
from
'@/components/ui/button'
import
{
Button
}
from
'@/components/ui/button'
import
{
Input
}
from
'@/components/ui/input'
import
{
Input
}
from
'@/components/ui/input'
...
@@ -47,6 +48,10 @@ export type DataTableToolbarProps<TData> = {
...
@@ -47,6 +48,10 @@ export type DataTableToolbarProps<TData> = {
*/
*/
searchPlaceholder
?:
string
searchPlaceholder
?:
string
/**
/**
* Delay committing the default search input. Defaults to immediate updates.
*/
searchDebounceMs
?:
number
/**
* Column id to filter on. When provided, the search input filters
* Column id to filter on. When provided, the search input filters
* a specific column. When omitted, the search input updates the
* a specific column. When omitted, the search input updates the
* table's `globalFilter`.
* table's `globalFilter`.
...
@@ -136,6 +141,8 @@ export type DataTableToolbarProps<TData> = {
...
@@ -136,6 +141,8 @@ export type DataTableToolbarProps<TData> = {
export
function
DataTableToolbar
<
TData
>
(
props
:
DataTableToolbarProps
<
TData
>
)
{
export
function
DataTableToolbar
<
TData
>
(
props
:
DataTableToolbarProps
<
TData
>
)
{
const
{
t
}
=
useTranslation
()
const
{
t
}
=
useTranslation
()
const
[
expanded
,
setExpanded
]
=
useState
(
false
)
const
[
expanded
,
setExpanded
]
=
useState
(
false
)
const
isSearchComposingRef
=
React
.
useRef
(
false
)
const
lastCommittedSearchValueRef
=
React
.
useRef
(
''
)
const
filters
=
props
.
filters
??
[]
const
filters
=
props
.
filters
??
[]
const
hasExpandable
=
props
.
expandable
!=
null
const
hasExpandable
=
props
.
expandable
!=
null
...
@@ -147,26 +154,109 @@ export function DataTableToolbar<TData>(props: DataTableToolbarProps<TData>) {
...
@@ -147,26 +154,109 @@ export function DataTableToolbar<TData>(props: DataTableToolbarProps<TData>) {
!!
props
.
hasAdditionalFilters
!!
props
.
hasAdditionalFilters
const
placeholder
=
props
.
searchPlaceholder
??
t
(
'Filter...'
)
const
placeholder
=
props
.
searchPlaceholder
??
t
(
'Filter...'
)
const
currentSearchValue
=
props
.
searchKey
?
((
props
.
table
.
getColumn
(
props
.
searchKey
)?.
getFilterValue
()
as
string
)
??
''
)
:
((
props
.
table
.
getState
().
globalFilter
as
string
|
undefined
)
??
''
)
const
[
searchValue
,
setSearchValue
]
=
useState
(
currentSearchValue
)
const
[
pendingSearchValue
,
setPendingSearchValue
]
=
useState
(
currentSearchValue
)
const
searchDebounceMs
=
Math
.
max
(
0
,
props
.
searchDebounceMs
??
0
)
const
debouncedSearchValue
=
useDebounce
(
pendingSearchValue
,
searchDebounceMs
)
React
.
useEffect
(()
=>
{
lastCommittedSearchValueRef
.
current
=
currentSearchValue
if
(
!
isSearchComposingRef
.
current
)
{
setSearchValue
(
currentSearchValue
)
}
setPendingSearchValue
(
currentSearchValue
)
},
[
currentSearchValue
])
const
commitSearchValue
=
React
.
useCallback
(
(
value
:
string
)
=>
{
if
(
value
===
lastCommittedSearchValueRef
.
current
)
{
return
}
lastCommittedSearchValueRef
.
current
=
value
if
(
props
.
searchKey
)
{
props
.
table
.
getColumn
(
props
.
searchKey
)?.
setFilterValue
(
value
)
return
}
props
.
table
.
setGlobalFilter
(
value
)
},
[
props
.
searchKey
,
props
.
table
]
)
React
.
useEffect
(()
=>
{
if
(
searchDebounceMs
<=
0
||
isSearchComposingRef
.
current
||
debouncedSearchValue
!==
pendingSearchValue
)
{
return
}
commitSearchValue
(
debouncedSearchValue
)
},
[
commitSearchValue
,
debouncedSearchValue
,
pendingSearchValue
,
searchDebounceMs
,
])
const
queueSearchValue
=
(
value
:
string
)
=>
{
setPendingSearchValue
(
value
)
if
(
searchDebounceMs
<=
0
)
{
commitSearchValue
(
value
)
}
}
const
handleSearchChange
=
(
event
:
React
.
ChangeEvent
<
HTMLInputElement
>
)
=>
{
const
value
=
event
.
target
.
value
setSearchValue
(
value
)
if
(
!
isSearchComposingRef
.
current
)
{
queueSearchValue
(
value
)
}
}
const
handleSearchCompositionStart
=
()
=>
{
isSearchComposingRef
.
current
=
true
}
const
handleSearchCompositionEnd
=
(
event
:
React
.
CompositionEvent
<
HTMLInputElement
>
)
=>
{
isSearchComposingRef
.
current
=
false
const
value
=
event
.
currentTarget
.
value
setSearchValue
(
value
)
queueSearchValue
(
value
)
}
const
searchInput
=
props
.
searchKey
?
(
const
searchInput
=
props
.
searchKey
?
(
<
Input
<
Input
placeholder=
{
placeholder
}
placeholder=
{
placeholder
}
value=
{
value=
{
searchValue
}
(
props
.
table
.
getColumn
(
props
.
searchKey
)?.
getFilterValue
()
as
string
)
??
onChange=
{
handleSearchChange
}
''
onCompositionStart=
{
handleSearchCompositionStart
}
}
onCompositionEnd=
{
handleSearchCompositionEnd
}
onChange=
{
(
event
)
=>
props
.
table
.
getColumn
(
props
.
searchKey
!
)
?.
setFilterValue
(
event
.
target
.
value
)
}
className=
'w-full sm:w-[200px] lg:w-[240px]'
className=
'w-full sm:w-[200px] lg:w-[240px]'
/>
/>
)
:
(
)
:
(
<
Input
<
Input
placeholder=
{
placeholder
}
placeholder=
{
placeholder
}
value=
{
props
.
table
.
getState
().
globalFilter
??
''
}
value=
{
searchValue
}
onChange=
{
(
event
)
=>
props
.
table
.
setGlobalFilter
(
event
.
target
.
value
)
}
onChange=
{
handleSearchChange
}
onCompositionStart=
{
handleSearchCompositionStart
}
onCompositionEnd=
{
handleSearchCompositionEnd
}
className=
'w-full sm:w-[200px] lg:w-[240px]'
className=
'w-full sm:w-[200px] lg:w-[240px]'
/>
/>
)
)
...
@@ -186,6 +276,10 @@ export function DataTableToolbar<TData>(props: DataTableToolbarProps<TData>) {
...
@@ -186,6 +276,10 @@ export function DataTableToolbar<TData>(props: DataTableToolbarProps<TData>) {
})
})
const
handleReset
=
()
=>
{
const
handleReset
=
()
=>
{
isSearchComposingRef
.
current
=
false
setSearchValue
(
''
)
setPendingSearchValue
(
''
)
lastCommittedSearchValueRef
.
current
=
''
props
.
table
.
resetColumnFilters
()
props
.
table
.
resetColumnFilters
()
props
.
table
.
setGlobalFilter
(
''
)
props
.
table
.
setGlobalFilter
(
''
)
props
.
onReset
?.()
props
.
onReset
?.()
...
...
web/default/src/features/channels/components/channels-table.tsx
View file @
30d3a3a5
...
@@ -16,7 +16,14 @@ along with this program. If not, see <https://www.gnu.org/licenses/>.
...
@@ -16,7 +16,14 @@ along with this program. If not, see <https://www.gnu.org/licenses/>.
For commercial licensing, please contact support@quantumnous.com
For commercial licensing, please contact support@quantumnous.com
*/
*/
import
{
useState
,
useMemo
,
useEffect
}
from
'react'
import
{
useState
,
useMemo
,
useEffect
,
useRef
,
type
ChangeEvent
,
type
CompositionEvent
,
}
from
'react'
import
{
useQuery
}
from
'@tanstack/react-query'
import
{
useQuery
}
from
'@tanstack/react-query'
import
{
getRouteApi
}
from
'@tanstack/react-router'
import
{
getRouteApi
}
from
'@tanstack/react-router'
import
{
import
{
...
@@ -124,17 +131,26 @@ export function ChannelsTable() {
...
@@ -124,17 +131,26 @@ export function ChannelsTable() {
(
columnFilters
.
find
((
f
)
=>
f
.
id
===
'model'
)?.
value
as
string
)
||
''
(
columnFilters
.
find
((
f
)
=>
f
.
id
===
'model'
)?.
value
as
string
)
||
''
// Local state for immediate input feedback
// Local state for immediate input feedback
const
isModelFilterComposingRef
=
useRef
(
false
)
const
[
modelFilterInput
,
setModelFilterInput
]
=
useState
(
modelFilterFromUrl
)
const
[
modelFilterInput
,
setModelFilterInput
]
=
useState
(
modelFilterFromUrl
)
const
debouncedModelFilter
=
useDebounce
(
modelFilterInput
,
500
)
const
[
modelFilterPendingValue
,
setModelFilterPendingValue
]
=
useState
(
modelFilterFromUrl
)
const
debouncedModelFilter
=
useDebounce
(
modelFilterPendingValue
,
500
)
// Sync local input with URL when URL changes (e.g., from back/forward navigation)
// Sync local input with URL when URL changes (e.g., from back/forward navigation)
useEffect
(()
=>
{
useEffect
(()
=>
{
setModelFilterInput
(
modelFilterFromUrl
)
if
(
!
isModelFilterComposingRef
.
current
)
{
setModelFilterInput
(
modelFilterFromUrl
)
}
setModelFilterPendingValue
(
modelFilterFromUrl
)
},
[
modelFilterFromUrl
])
},
[
modelFilterFromUrl
])
// Update URL when debounced value changes
// Update URL when debounced value changes
useEffect
(()
=>
{
useEffect
(()
=>
{
if
(
debouncedModelFilter
!==
modelFilterFromUrl
)
{
if
(
debouncedModelFilter
===
modelFilterPendingValue
&&
debouncedModelFilter
!==
modelFilterFromUrl
)
{
onColumnFiltersChange
((
prev
)
=>
{
onColumnFiltersChange
((
prev
)
=>
{
const
filtered
=
prev
.
filter
((
f
)
=>
f
.
id
!==
'model'
)
const
filtered
=
prev
.
filter
((
f
)
=>
f
.
id
!==
'model'
)
return
debouncedModelFilter
return
debouncedModelFilter
...
@@ -142,7 +158,34 @@ export function ChannelsTable() {
...
@@ -142,7 +158,34 @@ export function ChannelsTable() {
:
filtered
:
filtered
})
})
}
}
},
[
debouncedModelFilter
,
modelFilterFromUrl
,
onColumnFiltersChange
])
},
[
debouncedModelFilter
,
modelFilterFromUrl
,
modelFilterPendingValue
,
onColumnFiltersChange
,
])
const
handleModelFilterChange
=
(
event
:
ChangeEvent
<
HTMLInputElement
>
)
=>
{
const
value
=
event
.
target
.
value
setModelFilterInput
(
value
)
if
(
!
isModelFilterComposingRef
.
current
)
{
setModelFilterPendingValue
(
value
)
}
}
const
handleModelFilterCompositionStart
=
()
=>
{
isModelFilterComposingRef
.
current
=
true
}
const
handleModelFilterCompositionEnd
=
(
event
:
CompositionEvent
<
HTMLInputElement
>
)
=>
{
isModelFilterComposingRef
.
current
=
false
const
value
=
event
.
currentTarget
.
value
setModelFilterInput
(
value
)
setModelFilterPendingValue
(
value
)
}
const
modelFilter
=
modelFilterFromUrl
const
modelFilter
=
modelFilterFromUrl
...
@@ -385,11 +428,19 @@ export function ChannelsTable() {
...
@@ -385,11 +428,19 @@ export function ChannelsTable() {
applyHeaderSize
applyHeaderSize
toolbarProps=
{
{
toolbarProps=
{
{
searchPlaceholder
:
t
(
'Filter by name, ID, or key...'
),
searchPlaceholder
:
t
(
'Filter by name, ID, or key...'
),
searchDebounceMs
:
500
,
onReset
:
()
=>
{
isModelFilterComposingRef
.
current
=
false
setModelFilterInput
(
''
)
setModelFilterPendingValue
(
''
)
},
additionalSearch
:
(
additionalSearch
:
(
<
Input
<
Input
placeholder=
{
t
(
'Filter by model...'
)
}
placeholder=
{
t
(
'Filter by model...'
)
}
value=
{
modelFilterInput
}
value=
{
modelFilterInput
}
onChange=
{
(
e
)
=>
setModelFilterInput
(
e
.
target
.
value
)
}
onChange=
{
handleModelFilterChange
}
onCompositionStart=
{
handleModelFilterCompositionStart
}
onCompositionEnd=
{
handleModelFilterCompositionEnd
}
className=
'w-full sm:w-[150px] lg:w-[180px]'
className=
'w-full sm:w-[150px] lg:w-[180px]'
/>
/>
),
),
...
...
Write
Preview
Markdown
is supported
0%
Try again
or
attach a new file
Attach a file
Cancel
You are about to add
0
people
to the discussion. Proceed with caution.
Finish editing this message first!
Cancel
Please
register
or
sign in
to comment