Skip to content

Commit 2f549cf

Browse files
committed
fix: status select mobile issues, remove table caching, fix disabled status
1 parent 2b73dc8 commit 2f549cf

File tree

9 files changed

+206
-206
lines changed

9 files changed

+206
-206
lines changed

dashboard/src/components/ActionButtons.tsx

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -66,7 +66,7 @@ const ActionButtons: FC<ActionButtonsProps> = ({ user }) => {
6666
const userForm = useForm<UseEditFormValues>({
6767
defaultValues: {
6868
username: user.username,
69-
status: user.status === 'active' || user.status === 'on_hold' || user.status === 'disabled' ? user.status : 'active',
69+
status: user.status === "expired" || user.status === "limited" ? "active" : user.status,
7070
data_limit: user.data_limit ? Math.round((Number(user.data_limit) / (1024 * 1024 * 1024)) * 100) / 100 : undefined, // Convert bytes to GB
7171
expire: user.expire,
7272
note: user.note || '',
@@ -88,7 +88,7 @@ const ActionButtons: FC<ActionButtonsProps> = ({ user }) => {
8888
useEffect(() => {
8989
const values: UseFormValues = {
9090
username: user.username,
91-
status: user.status === 'active' || user.status === 'on_hold' ? user.status : 'active',
91+
status: user.status === 'active' || user.status === 'on_hold' || user.status === 'disabled' ? user.status as any : 'active',
9292
data_limit: user.data_limit ? Math.round((Number(user.data_limit) / (1024 * 1024 * 1024)) * 100) / 100 : 0,
9393
expire: user.expire, // Pass raw expire value (timestamp)
9494
note: user.note || '',

dashboard/src/components/admins/AdminsTable.tsx

Lines changed: 6 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -192,14 +192,13 @@ export default function AdminsTable({ onEdit, onDelete, onToggleStatus, onResetU
192192
setCurrentPage(newPage)
193193

194194
try {
195-
// Wait for state to update before refetching
196-
await new Promise(resolve => setTimeout(resolve, 0))
195+
// Immediate refetch without delay
197196
await refetch()
198197
} finally {
199-
// Add a small delay to prevent flickering
198+
// Minimal delay for instant response
200199
setTimeout(() => {
201200
setIsChangingPage(false)
202-
}, 300)
201+
}, 50)
203202
}
204203
}
205204

@@ -209,14 +208,13 @@ export default function AdminsTable({ onEdit, onDelete, onToggleStatus, onResetU
209208
setCurrentPage(0) // Reset to first page when items per page changes
210209

211210
try {
212-
// Wait for state to update before refetching
213-
await new Promise(resolve => setTimeout(resolve, 0))
211+
// Immediate refetch without delay
214212
await refetch()
215213
} finally {
216-
// Add a small delay to prevent flickering
214+
// Minimal delay for instant response
217215
setTimeout(() => {
218216
setIsChangingPage(false)
219-
}, 300)
217+
}, 50)
220218
}
221219
}
222220

dashboard/src/components/admins/data-table.tsx

Lines changed: 11 additions & 44 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@ import { ColumnDef, flexRender, getCoreRowModel, useReactTable } from '@tanstack
22
import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from '@/components/ui/table'
33
import { cn } from '@/lib/utils'
44
import useDirDetection from '@/hooks/use-dir-detection'
5-
import React, { useState, useMemo, useEffect } from 'react'
5+
import React, { useState, useMemo, useEffect, memo, useCallback } from 'react'
66
import { ChevronDown, Edit2, Power, PowerOff, RefreshCw, Trash2, User, UserRound, LoaderCircle } from 'lucide-react'
77
import { Button } from '@/components/ui/button'
88
import { AdminDetails } from '@/service/api'
@@ -23,7 +23,7 @@ interface DataTableProps<TData extends AdminDetails> {
2323
isFetching?: boolean
2424
}
2525

26-
const ExpandedRowContent = ({
26+
const ExpandedRowContent = memo(({
2727
row,
2828
onEdit,
2929
onDelete,
@@ -76,12 +76,11 @@ const ExpandedRowContent = ({
7676
</div>
7777
</div>
7878
)
79-
}
79+
})
8080

8181
export function DataTable<TData extends AdminDetails>({ columns, data, onEdit, onDelete, onToggleStatus, onResetUsage, isLoading = false, isFetching = false }: DataTableProps<TData>) {
8282
const [expandedRow, setExpandedRow] = useState<string | null>(null)
8383
const { t } = useTranslation()
84-
const [visibleRows, setVisibleRows] = useState<number>(0)
8584
const table = useReactTable({
8685
data,
8786
columns,
@@ -91,26 +90,6 @@ export function DataTable<TData extends AdminDetails>({ columns, data, onEdit, o
9190
const isRTL = dir === 'rtl'
9291
const isLoadingData = isLoading || isFetching
9392

94-
useEffect(() => {
95-
if (isLoading || isFetching) {
96-
setVisibleRows(0)
97-
return
98-
}
99-
100-
const totalRows = table.getRowModel().rows.length
101-
let currentRow = 0
102-
103-
const loadNextRow = () => {
104-
if (currentRow < totalRows) {
105-
setVisibleRows(prev => prev + 1)
106-
currentRow++
107-
setTimeout(loadNextRow, 50)
108-
}
109-
}
110-
111-
loadNextRow()
112-
}, [isLoading, isFetching, table.getRowModel().rows.length])
113-
11493
const LoadingState = useMemo(() => (
11594
<TableRow>
11695
<TableCell colSpan={columns.length} className="h-24">
@@ -130,17 +109,17 @@ export function DataTable<TData extends AdminDetails>({ columns, data, onEdit, o
130109
</TableRow>
131110
), [columns.length, t])
132111

133-
const handleRowToggle = (rowId: string) => {
112+
const handleRowToggle = useCallback((rowId: string) => {
134113
setExpandedRow(expandedRow === rowId ? null : rowId)
135-
}
114+
}, [expandedRow])
136115

137-
const handleEditModal = (cellId: string, rowData: AdminDetails) => {
116+
const handleEditModal = useCallback((cellId: string, rowData: AdminDetails) => {
138117
const isChevron = cellId === 'chevron'
139118
const isSmallScreen = window.innerWidth < 768
140119
if (!isSmallScreen && !isChevron) {
141120
onEdit(rowData)
142121
}
143-
}
122+
}, [onEdit])
144123

145124
return (
146125
<div className="rounded-md border">
@@ -171,18 +150,13 @@ export function DataTable<TData extends AdminDetails>({ columns, data, onEdit, o
171150
</TableHeader>
172151
<TableBody>
173152
{isLoadingData ? LoadingState : table.getRowModel().rows?.length ? (
174-
table.getRowModel().rows.map((row, index) => (
153+
table.getRowModel().rows.map((row) => (
175154
<React.Fragment key={row.id}>
176155
<TableRow
177156
className={cn(
178157
'cursor-pointer md:cursor-default border-b hover:!bg-inherit md:hover:!bg-muted/50',
179-
expandedRow === row.id && 'border-transparent',
180-
index >= visibleRows && 'opacity-0',
181-
'transition-all duration-300 ease-in-out'
158+
expandedRow === row.id && 'border-transparent'
182159
)}
183-
style={{
184-
transform: index >= visibleRows ? 'translateY(10px)' : 'translateY(0)',
185-
}}
186160
onClick={() => window.innerWidth < 768 && handleRowToggle(row.id)}
187161
data-state={row.getIsSelected() && 'selected'}
188162
>
@@ -204,7 +178,7 @@ export function DataTable<TData extends AdminDetails>({ columns, data, onEdit, o
204178
>
205179
{cell.column.id === 'chevron' ? (
206180
<div className="flex items-center justify-center cursor-pointer" onClick={() => handleRowToggle(row.id)}>
207-
<ChevronDown className={cn('h-4 w-4 transition-transform duration-300', expandedRow === row.id && 'rotate-180')} />
181+
<ChevronDown className={cn('h-4 w-4', expandedRow === row.id && 'rotate-180')} />
208182
</div>
209183
) : (
210184
flexRender(cell.column.columnDef.cell, cell.getContext())
@@ -214,14 +188,7 @@ export function DataTable<TData extends AdminDetails>({ columns, data, onEdit, o
214188
</TableRow>
215189
{expandedRow === row.id && (
216190
<TableRow
217-
className={cn(
218-
"md:hidden border-b hover:!bg-inherit",
219-
index >= visibleRows && 'opacity-0',
220-
'transition-all duration-300 ease-in-out'
221-
)}
222-
style={{
223-
transform: index >= visibleRows ? 'translateY(10px)' : 'translateY(0)',
224-
}}
191+
className="md:hidden border-b hover:!bg-inherit"
225192
>
226193
<TableCell colSpan={columns.length} className="p-0 text-sm">
227194
<ExpandedRowContent

dashboard/src/components/admins/filters.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -38,7 +38,7 @@ export function Filters<T extends BaseFilters>({ filters, onFilterChange }: Filt
3838
username: value ? value : null,
3939
offset: 0, // Reset to first page when search is updated
4040
} as Partial<T>)
41-
}, 300),
41+
}, 50),
4242
[onFilterChange],
4343
)
4444

dashboard/src/components/dialogs/UserModal.tsx

Lines changed: 109 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -8,8 +8,8 @@ import { Form, FormControl, FormField, FormItem, FormLabel, FormMessage } from '
88
import { Input } from '@/components/ui/input'
99
import { LoaderButton } from '@/components/ui/loader-button'
1010
import { Calendar as PersianCalendar } from '@/components/ui/persian-calendar'
11-
import { Popover, PopoverContent, PopoverTrigger } from '@/components/ui/popover'
1211
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@/components/ui/select'
12+
import { Popover, PopoverContent, PopoverTrigger } from '@/components/ui/popover'
1313
import { Switch } from '@/components/ui/switch'
1414
import { Textarea } from '@/components/ui/textarea'
1515
import useDirDetection from '@/hooks/use-dir-detection'
@@ -30,7 +30,7 @@ import {
3030
import { dateUtils, useRelativeExpiryDate } from '@/utils/dateFormatter'
3131
import { formatBytes } from '@/utils/formatByte'
3232
import { useQuery, useQueryClient } from '@tanstack/react-query'
33-
import { CalendarIcon, Layers, ListStart, Lock, RefreshCcw, Users, X } from 'lucide-react'
33+
import { CalendarIcon, ChevronDown, Layers, ListStart, Lock, RefreshCcw, Users, X } from 'lucide-react'
3434
import React, { useEffect, useState, useTransition } from 'react'
3535
import { UseFormReturn } from 'react-hook-form'
3636
import { useTranslation } from 'react-i18next'
@@ -275,7 +275,7 @@ const ExpiryDateField = ({
275275
<PopoverContent
276276
className="w-auto p-0"
277277
align="start"
278-
onInteractOutside={e => {
278+
onInteractOutside={(e: Event) => {
279279
e.preventDefault()
280280
setCalendarOpen(false)
281281
}}
@@ -373,6 +373,98 @@ const ExpiryDateField = ({
373373

374374
export { ExpiryDateField }
375375

376+
// Custom Select component that works reliably on mobile
377+
const StatusSelect = ({
378+
value,
379+
onValueChange,
380+
placeholder,
381+
children,
382+
disabled
383+
}: {
384+
value?: string
385+
onValueChange?: (value: string) => void
386+
placeholder?: string
387+
children: React.ReactNode
388+
disabled?: boolean
389+
}) => {
390+
const [open, setOpen] = useState(false)
391+
const { t } = useTranslation()
392+
393+
const handleSelect = (selectedValue: string) => {
394+
onValueChange?.(selectedValue)
395+
setOpen(false)
396+
}
397+
398+
const getStatusText = (statusValue?: string) => {
399+
if (!statusValue) return placeholder || t('userDialog.selectStatus', { defaultValue: 'Select status' })
400+
401+
switch (statusValue) {
402+
case 'active': return t('status.active', { defaultValue: 'Active' })
403+
case 'disabled': return t('status.disabled', { defaultValue: 'Disabled' })
404+
case 'on_hold': return t('status.on_hold', { defaultValue: 'On Hold' })
405+
default: return placeholder || t('userDialog.selectStatus', { defaultValue: 'Select status' })
406+
}
407+
}
408+
409+
return (
410+
<Popover open={open} onOpenChange={setOpen}>
411+
<PopoverTrigger asChild>
412+
<Button
413+
variant="outline"
414+
role="combobox"
415+
aria-expanded={open}
416+
className="w-full justify-between h-9 px-3 py-2 text-sm"
417+
disabled={disabled}
418+
>
419+
<span className="truncate">{getStatusText(value)}</span>
420+
<ChevronDown className="h-4 w-4 shrink-0 opacity-50" />
421+
</Button>
422+
</PopoverTrigger>
423+
<PopoverContent className="w-[--radix-popover-trigger-width] p-1" align="start">
424+
{React.Children.map(children, (child) => {
425+
if (React.isValidElement(child) && child.props.value) {
426+
return React.cloneElement(child, {
427+
onSelect: handleSelect
428+
})
429+
}
430+
return child
431+
})}
432+
</PopoverContent>
433+
</Popover>
434+
)
435+
}
436+
437+
const StatusSelectItem = ({
438+
value,
439+
children,
440+
onSelect
441+
}: {
442+
value: string
443+
children: React.ReactNode
444+
onSelect?: (value: string) => void
445+
}) => {
446+
const getDotColor = () => {
447+
switch (value) {
448+
case 'active': return 'bg-green-500'
449+
case 'disabled': return 'bg-zinc-500'
450+
case 'on_hold': return 'bg-violet-500'
451+
default: return 'bg-gray-500'
452+
}
453+
}
454+
455+
return (
456+
<div
457+
className="relative flex w-full min-w-0 cursor-pointer select-none items-center rounded-sm py-2 px-2 text-sm outline-none hover:bg-accent hover:text-accent-foreground transition-colors"
458+
onClick={() => onSelect?.(value)}
459+
>
460+
<span className="truncate min-w-0 flex-1 pr-2">{children}</span>
461+
<span className="flex h-3.5 w-3.5 shrink-0 items-center justify-center">
462+
<div className={`h-2 w-2 rounded-full ${getDotColor()}`} />
463+
</span>
464+
</div>
465+
)
466+
}
467+
376468
export default function UserModal({ isDialogOpen, onOpenChange, form, editingUser, editingUserId, onSuccessCallback }: UserModalProps) {
377469
const { t } = useTranslation()
378470
const dir = useDirDetection()
@@ -494,14 +586,14 @@ export default function UserModal({ isDialogOpen, onOpenChange, form, editingUse
494586
},
495587
)
496588

497-
// Fetch data for tabs with proper caching and refetch on page view
589+
// Fetch data for tabs without caching
498590
const { data: templatesData, isLoading: templatesLoading } = useGetUserTemplates(undefined, {
499591
query: {
500-
staleTime: 5 * 60 * 1000, // 5 minutes
501-
gcTime: 10 * 60 * 1000, // 10 minutes
502-
refetchOnWindowFocus: true,
592+
staleTime: 0, // No stale time - always fetch fresh data
593+
gcTime: 0, // No garbage collection time - no caching
594+
refetchOnWindowFocus: false,
503595
refetchOnMount: true,
504-
refetchOnReconnect: true,
596+
refetchOnReconnect: false,
505597
},
506598
})
507599

@@ -1081,6 +1173,7 @@ export default function UserModal({ isDialogOpen, onOpenChange, form, editingUse
10811173
setUsePersianCalendar(i18n.language === 'fa')
10821174
}, [i18n.language])
10831175

1176+
10841177
return (
10851178
<Dialog open={isDialogOpen} onOpenChange={handleModalOpenChange}>
10861179
<DialogContent className={`lg:min-w-[900px] ${editingUser ? 'h-full sm:h-auto' : 'h-auto'}`}>
@@ -1154,23 +1247,18 @@ export default function UserModal({ isDialogOpen, onOpenChange, form, editingUse
11541247
<FormItem className="w-1/3">
11551248
<FormLabel>{t('status', { defaultValue: 'Status' })}</FormLabel>
11561249
<FormControl>
1157-
<Select
1250+
<StatusSelect
1251+
value={field.value || ''}
11581252
onValueChange={value => {
11591253
field.onChange(value)
11601254
handleFieldChange('status', value)
1161-
handleFieldBlur('status')
11621255
}}
1163-
value={field.value || ''}
1256+
placeholder={t('userDialog.selectStatus', { defaultValue: 'Select status' })}
11641257
>
1165-
<SelectTrigger>
1166-
<SelectValue placeholder={t('userDialog.selectStatus', { defaultValue: 'Select status' })} />
1167-
</SelectTrigger>
1168-
<SelectContent>
1169-
<SelectItem value="active">{t('status.active', { defaultValue: 'Active' })}</SelectItem>
1170-
{editingUser && <SelectItem value="disabled">{t('status.disabled', { defaultValue: 'Disabled' })}</SelectItem>}
1171-
<SelectItem value="on_hold">{t('status.on_hold', { defaultValue: 'On Hold' })}</SelectItem>
1172-
</SelectContent>
1173-
</Select>
1258+
<StatusSelectItem value="active">{t('status.active', { defaultValue: 'Active' })}</StatusSelectItem>
1259+
{editingUser && <StatusSelectItem value="disabled">{t('status.disabled', { defaultValue: 'Disabled' })}</StatusSelectItem>}
1260+
<StatusSelectItem value="on_hold">{t('status.on_hold', { defaultValue: 'On Hold' })}</StatusSelectItem>
1261+
</StatusSelect>
11741262
</FormControl>
11751263
<FormMessage />
11761264
</FormItem>
@@ -1916,7 +2004,7 @@ export default function UserModal({ isDialogOpen, onOpenChange, form, editingUse
19162004
>
19172005
{t('cancel', { defaultValue: 'Cancel' })}
19182006
</Button>
1919-
<LoaderButton type="submit" isLoading={loading} disabled={false} loadingText={editingUser ? t('modifying') : t('creating')}>
2007+
<LoaderButton type="submit" isLoading={loading} disabled={!isFormValid} loadingText={editingUser ? t('modifying') : t('creating')}>
19202008
{editingUser ? t('modify', { defaultValue: 'Modify' }) : t('create', { defaultValue: 'Create' })}
19212009
</LoaderButton>
19222010
</div>

dashboard/src/components/nodes/Nodes.tsx

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -30,8 +30,9 @@ export default function Nodes() {
3030

3131
const { data: nodesData, isLoading } = useGetNodes(undefined, {
3232
query: {
33-
refetchInterval: 5000,
34-
refetchIntervalInBackground: true,
33+
refetchInterval: 5000, // 5s
34+
staleTime: 0, // No stale time - always fetch fresh data
35+
gcTime: 0, // No garbage collection time - no caching
3536
},
3637
})
3738

0 commit comments

Comments
 (0)