Skip to content

Commit 1d6d70f

Browse files
committed
fix(user-modal): handle small values filtering
1 parent 3a6d4be commit 1d6d70f

File tree

2 files changed

+108
-37
lines changed

2 files changed

+108
-37
lines changed

dashboard/src/components/common/date-picker.tsx

Lines changed: 25 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22

33
import * as React from 'react'
44
import { addDays } from 'date-fns'
5-
import { Calendar as CalendarIcon } from 'lucide-react'
5+
import { Calendar as CalendarIcon, X } from 'lucide-react'
66
import { DateRange } from 'react-day-picker'
77
import { cn } from '@/lib/utils'
88
import { Button } from '@/components/ui/button'
@@ -291,6 +291,17 @@ export function DatePicker({
291291
? `${String(displayDate.getHours()).padStart(2, '0')}:${String(displayDate.getMinutes()).padStart(2, '0')}`
292292
: ''
293293

294+
const handleClear = React.useCallback(
295+
(e: React.MouseEvent) => {
296+
e.preventDefault()
297+
e.stopPropagation()
298+
setInternalDate(undefined)
299+
onDateChange(undefined)
300+
onFieldChange?.(fieldName, undefined)
301+
},
302+
[onDateChange, onFieldChange, fieldName],
303+
)
304+
294305
return (
295306
<div className={cn('grid gap-2', className)}>
296307
{label && <label className="text-sm font-medium">{label}</label>}
@@ -303,7 +314,19 @@ export function DatePicker({
303314
type="button"
304315
>
305316
{displayDate ? formatDate(displayDate) : <span>{placeholder || label || t('timeSelector.pickDate')}</span>}
306-
<CalendarIcon className="ml-auto h-4 w-4 opacity-50" />
317+
<div className="ml-auto flex items-center gap-1">
318+
{displayDate && (
319+
<button
320+
type="button"
321+
onClick={handleClear}
322+
className="rounded-sm opacity-50 hover:opacity-100 focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2"
323+
aria-label={t('clear', { defaultValue: 'Clear' })}
324+
>
325+
<X className="h-4 w-4" />
326+
</button>
327+
)}
328+
<CalendarIcon className="h-4 w-4 opacity-50" />
329+
</div>
307330
</Button>
308331
</PopoverTrigger>
309332
<PopoverContent

dashboard/src/components/dialogs/user-modal.tsx

Lines changed: 83 additions & 35 deletions
Original file line numberDiff line numberDiff line change
@@ -136,15 +136,14 @@ const ExpiryDateField = ({
136136

137137
return (
138138
<FormItem className="flex flex-1 flex-col">
139-
<FormLabel>{label}</FormLabel>
139+
<FormLabel className='mb-0.5'>{label}</FormLabel>
140140
<div className="relative">
141141
<DatePicker
142142
mode="single"
143143
date={displayDate}
144144
onDateChange={handleDateChange}
145145
showTime={true}
146146
useUtcTimestamp={useUtcTimestamp}
147-
label=""
148147
placeholder={t('userDialog.expireDate', { defaultValue: 'Expire date' })}
149148
minDate={now}
150149
maxDate={maxDate}
@@ -154,7 +153,7 @@ const ExpiryDateField = ({
154153
onFieldChange={handleFieldChange}
155154
/>
156155
{expireInfo && (
157-
<p className={cn('absolute top-full mt-1 whitespace-nowrap text-xs text-muted-foreground', !expireInfo.time && 'hidden', dir === 'rtl' ? 'right-0' : 'left-0')}>
156+
<p className={cn('absolute top-full text-end right-0 mt-1 whitespace-nowrap text-xs text-muted-foreground', !expireInfo.time && 'hidden', dir === 'rtl' ? 'right-0' : 'left-0')}>
158157
{expireInfo.time !== '0' && expireInfo.time !== '0s'
159158
? t('expires', { time: expireInfo.time, defaultValue: 'Expires in {{time}}' })
160159
: t('expired', { time: expireInfo.time, defaultValue: 'Expired in {{time}}' })}
@@ -282,6 +281,8 @@ export default function UserModal({ isDialogOpen, onOpenChange, form, editingUse
282281
}, [isDialogOpen])
283282
const [touchedFields, setTouchedFields] = useState<Record<string, boolean>>({})
284283
const [isFormValid, setIsFormValid] = useState(false)
284+
// Ref to store raw input value for data_limit to allow typing decimals
285+
const dataLimitInputRef = React.useRef<string>('')
285286

286287
const handleModalOpenChange = React.useCallback(
287288
(open: boolean) => {
@@ -291,6 +292,7 @@ export default function UserModal({ isDialogOpen, onOpenChange, form, editingUse
291292
setIsFormValid(false)
292293
setActiveTab('groups')
293294
setSelectedTemplateId(null)
295+
dataLimitInputRef.current = ''
294296
}
295297
onOpenChange(open)
296298
},
@@ -1267,35 +1269,84 @@ export default function UserModal({ isDialogOpen, onOpenChange, form, editingUse
12671269
<FormField
12681270
control={form.control}
12691271
name="data_limit"
1270-
render={({ field }) => (
1271-
<FormItem className="h-full flex-1">
1272-
<FormLabel>{t('userDialog.dataLimit', { defaultValue: 'Data Limit (GB)' })}</FormLabel>
1273-
<FormControl>
1274-
<Input
1275-
type="number"
1276-
step="any"
1277-
min="0"
1278-
placeholder={t('userDialog.dataLimit', { defaultValue: 'e.g. 1' })}
1279-
{...field}
1280-
value={field.value ? field.value : ''}
1281-
onChange={e => {
1282-
const value = e.target.value === '' ? 0 : parseFloat(e.target.value)
1283-
if (!isNaN(value) && value >= 0) {
1284-
field.onChange(value)
1285-
handleFieldChange('data_limit', value)
1286-
}
1287-
}}
1288-
onBlur={() => {
1289-
handleFieldChange('data_limit', field.value || 0)
1290-
}}
1291-
/>
1292-
</FormControl>
1293-
{field.value !== null && field.value !== undefined && field.value > 0 && field.value < 1 && (
1294-
<p className="mt-1 text-xs text-muted-foreground">{formatBytes(Math.round(field.value * 1024 * 1024 * 1024))}</p>
1295-
)}
1296-
<FormMessage />
1297-
</FormItem>
1298-
)}
1272+
render={({ field }) => {
1273+
if (dataLimitInputRef.current === '' && field.value !== null && field.value !== undefined && field.value > 0) {
1274+
dataLimitInputRef.current = String(field.value)
1275+
} else if ((field.value === null || field.value === undefined) && dataLimitInputRef.current !== '') {
1276+
dataLimitInputRef.current = ''
1277+
}
1278+
1279+
const displayValue = dataLimitInputRef.current !== ''
1280+
? dataLimitInputRef.current
1281+
: (field.value !== null && field.value !== undefined && field.value > 0 ? String(field.value) : '')
1282+
1283+
return (
1284+
<FormItem className="h-full flex-1 relative">
1285+
<FormLabel>{t('userDialog.dataLimit', { defaultValue: 'Data Limit (GB)' })}</FormLabel>
1286+
<FormControl>
1287+
<Input
1288+
type="text"
1289+
inputMode="decimal"
1290+
placeholder={t('userDialog.dataLimit', { defaultValue: 'e.g. 1' })}
1291+
value={displayValue}
1292+
onChange={e => {
1293+
const rawValue = e.target.value.trim()
1294+
1295+
dataLimitInputRef.current = rawValue
1296+
1297+
if (rawValue === '') {
1298+
field.onChange(0)
1299+
handleFieldChange('data_limit', 0)
1300+
return
1301+
}
1302+
1303+
const validNumberPattern = /^-?\d*\.?\d*$/
1304+
if (validNumberPattern.test(rawValue)) {
1305+
if (rawValue.endsWith('.') && rawValue.length > 1) {
1306+
const prevValue = field.value !== null && field.value !== undefined ? field.value : 0
1307+
field.onChange(prevValue)
1308+
handleFieldChange('data_limit', prevValue)
1309+
} else if (rawValue === '.') {
1310+
field.onChange(0)
1311+
handleFieldChange('data_limit', 0)
1312+
} else {
1313+
const numValue = parseFloat(rawValue)
1314+
if (!isNaN(numValue) && numValue >= 0) {
1315+
field.onChange(numValue)
1316+
handleFieldChange('data_limit', numValue)
1317+
}
1318+
}
1319+
}
1320+
}}
1321+
onBlur={() => {
1322+
const rawValue = dataLimitInputRef.current.trim()
1323+
if (rawValue === '' || rawValue === '.' || rawValue === '0') {
1324+
dataLimitInputRef.current = ''
1325+
field.onChange(0)
1326+
handleFieldChange('data_limit', 0)
1327+
} else {
1328+
const numValue = parseFloat(rawValue)
1329+
if (!isNaN(numValue) && numValue >= 0) {
1330+
const finalValue = numValue
1331+
dataLimitInputRef.current = finalValue > 0 ? String(finalValue) : ''
1332+
field.onChange(finalValue)
1333+
handleFieldChange('data_limit', finalValue)
1334+
} else {
1335+
dataLimitInputRef.current = ''
1336+
field.onChange(0)
1337+
handleFieldChange('data_limit', 0)
1338+
}
1339+
}
1340+
}}
1341+
/>
1342+
</FormControl>
1343+
{field.value !== null && field.value !== undefined && field.value > 0 && field.value < 1 && (
1344+
<p className="mt-1 text-end right-0 absolute top-full text-xs text-muted-foreground">{formatBytes(Math.round(field.value * 1024 * 1024 * 1024))}</p>
1345+
)}
1346+
<FormMessage />
1347+
</FormItem>
1348+
)
1349+
}}
12991350
/>
13001351
{form.watch('data_limit') !== undefined && form.watch('data_limit') !== null && Number(form.watch('data_limit')) > 0 && (
13011352
<FormField
@@ -1386,9 +1437,6 @@ export default function UserModal({ isDialogOpen, onOpenChange, form, editingUse
13861437
/>
13871438
</FormControl>
13881439
<FormMessage />
1389-
{isTouched && isZeroOrEmpty && !hasError && (
1390-
<p className="text-sm text-destructive">{t('validation.required', { field: t('userDialog.onHoldExpireDuration', { defaultValue: 'On Hold Expire Duration' }) })}</p>
1391-
)}
13921440
</FormItem>
13931441
)
13941442
}}

0 commit comments

Comments
 (0)