@@ -8,13 +8,14 @@ import { DropdownMenu, DropdownMenuContent, DropdownMenuItem, DropdownMenuTrigge
88import useDirDetection from '@/hooks/use-dir-detection'
99import { cn } from '@/lib/utils'
1010import { debounce } from 'es-toolkit'
11- import { RefreshCw , SearchIcon , Filter , X , ArrowUpDown , User , Calendar , ChartPie , ChevronDown } from 'lucide-react'
12- import { useState , useRef , useEffect } from 'react'
11+ import { RefreshCw , SearchIcon , Filter , X , ArrowUpDown , User , Calendar , ChartPie , ChevronDown , Check } from 'lucide-react'
12+ import { useState , useRef , useEffect , useCallback } from 'react'
1313import { useTranslation } from 'react-i18next'
1414import { useGetUsers , UserStatus } from '@/service/api'
1515import { RefetchOptions } from '@tanstack/react-query'
1616import { LoaderCircle } from 'lucide-react'
1717import { UseFormReturn } from 'react-hook-form'
18+ import { getUsersAutoRefreshIntervalSeconds , setUsersAutoRefreshIntervalSeconds } from '@/utils/userPreferenceStorage'
1819
1920// Sort configuration to eliminate duplication
2021const sortSections = [
@@ -47,6 +48,14 @@ const sortSections = [
4748 } ,
4849] as const
4950
51+ const autoRefreshOptions = [
52+ { value : 0 , labelKey : 'autoRefresh.off' } ,
53+ { value : 15 , labelKey : 'autoRefresh.15Seconds' } ,
54+ { value : 30 , labelKey : 'autoRefresh.30Seconds' } ,
55+ { value : 60 , labelKey : 'autoRefresh.1Minute' } ,
56+ { value : 300 , labelKey : 'autoRefresh.5Minutes' } ,
57+ ] as const
58+
5059interface FiltersProps {
5160 filters : {
5261 search ?: string
@@ -69,8 +78,47 @@ export const Filters = ({ filters, onFilterChange, refetch, advanceSearchOnOpen,
6978 const dir = useDirDetection ( )
7079 const [ search , setSearch ] = useState ( filters . search || '' )
7180 const [ isRefreshing , setIsRefreshing ] = useState ( false )
72- const userQuery = useGetUsers ( filters )
73- const handleRefetch = refetch || userQuery . refetch
81+ const [ autoRefreshInterval , setAutoRefreshInterval ] = useState < number > ( ( ) => getUsersAutoRefreshIntervalSeconds ( ) )
82+ const { refetch : queryRefetch } = useGetUsers ( filters , {
83+ query : {
84+ refetchOnWindowFocus : false ,
85+ } ,
86+ } )
87+ const refetchUsers = useCallback ( ( ) => {
88+ const refetchFn = refetch ?? queryRefetch
89+ return refetchFn ( )
90+ } , [ refetch , queryRefetch ] )
91+ useEffect ( ( ) => {
92+ const persistedValue = getUsersAutoRefreshIntervalSeconds ( )
93+ setAutoRefreshInterval ( prev => ( prev === persistedValue ? prev : persistedValue ) )
94+ } , [ ] )
95+ useEffect ( ( ) => {
96+ if ( ! autoRefreshInterval ) return
97+ const intervalId = setInterval ( ( ) => {
98+ refetchUsers ( )
99+ } , autoRefreshInterval * 1000 )
100+ return ( ) => clearInterval ( intervalId )
101+ } , [ autoRefreshInterval , refetchUsers ] )
102+ useEffect ( ( ) => {
103+ if ( typeof document === 'undefined' ) return
104+ const handleVisibilityChange = ( ) => {
105+ if ( document . visibilityState === 'visible' && autoRefreshInterval > 0 ) {
106+ refetchUsers ( )
107+ }
108+ }
109+ document . addEventListener ( 'visibilitychange' , handleVisibilityChange )
110+ return ( ) => {
111+ document . removeEventListener ( 'visibilitychange' , handleVisibilityChange )
112+ }
113+ } , [ autoRefreshInterval , refetchUsers ] )
114+ const currentAutoRefreshOption = autoRefreshOptions . find ( option => option . value === autoRefreshInterval ) ?? autoRefreshOptions [ 0 ]
115+ const autoRefreshShortLabel =
116+ autoRefreshInterval === 0
117+ ? t ( 'autoRefresh.offShort' )
118+ : autoRefreshInterval < 60
119+ ? t ( 'autoRefresh.shortSeconds' , { count : autoRefreshInterval } )
120+ : t ( 'autoRefresh.shortMinutes' , { count : Math . round ( autoRefreshInterval / 60 ) } )
121+ const currentAutoRefreshDescription = t ( currentAutoRefreshOption . labelKey )
74122 const onFilterChangeRef = useRef ( onFilterChange )
75123
76124 // Keep the ref in sync with the prop
@@ -114,13 +162,18 @@ export const Filters = ({ filters, onFilterChange, refetch, advanceSearchOnOpen,
114162 const handleRefreshClick = async ( ) => {
115163 setIsRefreshing ( true )
116164 try {
117- await handleRefetch ( )
165+ await refetchUsers ( )
118166 } finally {
119167 // Instant response - no delay
120168 setIsRefreshing ( false )
121169 }
122170 }
123171
172+ const handleAutoRefreshChange = ( seconds : number ) => {
173+ setUsersAutoRefreshIntervalSeconds ( seconds )
174+ setAutoRefreshInterval ( seconds )
175+ }
176+
124177 const handleOpenAdvanceSearch = ( ) => {
125178 advanceSearchOnOpen ( true )
126179 }
@@ -224,10 +277,62 @@ export const Filters = ({ filters, onFilterChange, refetch, advanceSearchOnOpen,
224277 </ div >
225278 ) }
226279 { /* Refresh Button */ }
227- < div className = "flex h-full items-center gap-2" >
228- < Button size = "icon-md" onClick = { handleRefreshClick } variant = "ghost" className = "flex items-center gap-2 border" disabled = { isRefreshing } >
280+ < div className = "flex h-full items-center gap-0" >
281+ < Button
282+ size = "icon-md"
283+ onClick = { handleRefreshClick }
284+ variant = "ghost"
285+ className = { cn ( 'relative flex items-center gap-2 border' , dir === 'rtl' ? 'rounded-l-none border-l-0' : 'rounded-r-none' ) }
286+ aria-label = { t ( 'autoRefresh.refreshNow' ) }
287+ title = { t ( 'autoRefresh.refreshNow' ) }
288+ disabled = { isRefreshing }
289+ >
229290 < RefreshCw className = { cn ( 'h-4 w-4' , isRefreshing && 'animate-spin' ) } />
291+ { autoRefreshInterval > 0 && < div className = "absolute -right-1 -top-1 h-2 w-2 rounded-full bg-primary" /> }
230292 </ Button >
293+ < DropdownMenu >
294+ < DropdownMenuTrigger asChild >
295+ < Button
296+ size = "icon-md"
297+ variant = "ghost"
298+ className = { cn ( 'relative flex items-center gap-2 border' , dir === 'rtl' ? 'rounded-r-none' : 'rounded-l-none border-l-0' ) }
299+ aria-label = { t ( 'autoRefresh.label' ) }
300+ title = { `${ t ( 'autoRefresh.label' ) } (${ autoRefreshShortLabel } )` }
301+ >
302+ < ChevronDown className = "h-3 w-3" />
303+ </ Button >
304+ </ DropdownMenuTrigger >
305+ < DropdownMenuContent align = "end" className = "w-52 md:w-56" >
306+ < DropdownMenuLabel className = "flex flex-col gap-0.5 px-2 py-1.5 text-xs text-muted-foreground md:px-3 md:py-2" >
307+ < span > { t ( 'autoRefresh.label' ) } </ span >
308+ < span className = "text-[11px] md:text-xs" > { t ( 'autoRefresh.currentSelection' , { value : currentAutoRefreshDescription } ) } </ span >
309+ </ DropdownMenuLabel >
310+ < DropdownMenuSeparator />
311+ < DropdownMenuItem
312+ onSelect = { ( ) => void handleRefreshClick ( ) }
313+ disabled = { isRefreshing }
314+ className = "flex items-center gap-2 px-2 py-1.5 text-xs md:px-3 md:py-2"
315+ >
316+ < RefreshCw className = { cn ( 'h-3.5 w-3.5 flex-shrink-0' , isRefreshing && 'animate-spin' ) } />
317+ < span className = "truncate" > { t ( 'autoRefresh.refreshNow' ) } </ span >
318+ { isRefreshing && < LoaderCircle className = "ml-auto h-3.5 w-3.5 animate-spin" /> }
319+ </ DropdownMenuItem >
320+ < DropdownMenuSeparator />
321+ { autoRefreshOptions . map ( option => {
322+ const isActive = option . value === autoRefreshInterval
323+ return (
324+ < DropdownMenuItem
325+ key = { option . value }
326+ onSelect = { ( ) => handleAutoRefreshChange ( option . value ) }
327+ className = { cn ( 'flex items-center gap-2 whitespace-nowrap px-2 py-1.5 text-xs md:px-3 md:py-2' , isActive && 'bg-accent' ) }
328+ >
329+ < span > { t ( option . labelKey ) } </ span >
330+ { isActive && < Check className = "ml-auto h-3 w-3 flex-shrink-0" /> }
331+ </ DropdownMenuItem >
332+ )
333+ } ) }
334+ </ DropdownMenuContent >
335+ </ DropdownMenu >
231336 </ div >
232337 </ div >
233338 )
0 commit comments