Skip to content

Commit 1d78fa9

Browse files
x0sinaImMohammad20000
authored andcommitted
refactor(user-modal, users-table): enhance state management and improve user editing experience
1 parent 1ddd10f commit 1d78fa9

File tree

2 files changed

+129
-94
lines changed

2 files changed

+129
-94
lines changed

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

Lines changed: 90 additions & 38 deletions
Original file line numberDiff line numberDiff line change
@@ -309,19 +309,43 @@ export default function UserModal({ isDialogOpen, onOpenChange, form, editingUse
309309
{ id: 'groups', label: 'groups', icon: Users },
310310
{ id: 'templates', label: 'templates.title', icon: Layers },
311311
]
312-
const [nextPlanEnabled, setNextPlanEnabled] = useState(() => {
313-
const nextPlan = form.watch('next_plan')
314-
return nextPlan !== undefined && nextPlan !== null && Object.keys(nextPlan).length > 0
315-
})
312+
const [nextPlanEnabled, setNextPlanEnabled] = useState(false)
313+
const [nextPlanManuallyDisabled, setNextPlanManuallyDisabled] = useState(false)
316314
const [selectedTemplateId, setSelectedTemplateId] = useState<number | null>(null)
317315
const [expireCalendarOpen, setExpireCalendarOpen] = useState(false)
318316
const [onHoldCalendarOpen, setOnHoldCalendarOpen] = useState(false)
319317

320-
// Reset calendar state when modal opens/closes
318+
const hasNextPlanValues = React.useCallback((nextPlan: any): boolean => {
319+
if (!nextPlan || typeof nextPlan !== 'object') return false
320+
321+
const hasAnyValue = !!(
322+
(nextPlan.user_template_id !== undefined && nextPlan.user_template_id !== null) ||
323+
(nextPlan.expire !== undefined && nextPlan.expire !== null) ||
324+
(nextPlan.data_limit !== undefined && nextPlan.data_limit !== null) ||
325+
nextPlan.add_remaining_traffic !== undefined
326+
)
327+
328+
return hasAnyValue
329+
}, [])
330+
331+
const nextPlanValue = React.useMemo(() => ({
332+
user_template_id: form.watch('next_plan.user_template_id'),
333+
expire: form.watch('next_plan.expire'),
334+
data_limit: form.watch('next_plan.data_limit'),
335+
add_remaining_traffic: form.watch('next_plan.add_remaining_traffic'),
336+
}), [
337+
form.watch('next_plan.user_template_id'),
338+
form.watch('next_plan.expire'),
339+
form.watch('next_plan.data_limit'),
340+
form.watch('next_plan.add_remaining_traffic'),
341+
])
342+
321343
useEffect(() => {
322344
if (!isDialogOpen) {
323345
setExpireCalendarOpen(false)
324346
setOnHoldCalendarOpen(false)
347+
setNextPlanEnabled(false)
348+
setNextPlanManuallyDisabled(false)
325349
}
326350
}, [isDialogOpen])
327351
const [touchedFields, setTouchedFields] = useState<Record<string, boolean>>({})
@@ -332,16 +356,21 @@ export default function UserModal({ isDialogOpen, onOpenChange, form, editingUse
332356
const handleModalOpenChange = React.useCallback(
333357
(open: boolean) => {
334358
if (!open) {
335-
form.reset()
359+
// Only reset form if not editing (for create mode)
360+
// When editing, parent component will repopulate the form
361+
if (!editingUser) {
362+
form.reset()
363+
}
336364
setTouchedFields({})
337365
setIsFormValid(false)
338366
setActiveTab('groups')
339367
setSelectedTemplateId(null)
368+
setNextPlanEnabled(false)
340369
dataLimitInputRef.current = ''
341370
}
342371
onOpenChange(open)
343372
},
344-
[form, onOpenChange],
373+
[form, onOpenChange, editingUser],
345374
)
346375

347376
const handleFieldChange = React.useCallback(
@@ -429,6 +458,7 @@ export default function UserModal({ isDialogOpen, onOpenChange, form, editingUse
429458
gcTime: 0,
430459
refetchOnMount: true,
431460
refetchOnReconnect: false,
461+
enabled: isDialogOpen,
432462
},
433463
})
434464

@@ -569,34 +599,32 @@ export default function UserModal({ isDialogOpen, onOpenChange, form, editingUse
569599
form.setValue('on_hold_timeout', undefined)
570600
form.clearErrors('on_hold_timeout')
571601
}
572-
}, [status, form, t, handleFieldChange])
602+
}, [status, form, t, handleFieldChange, touchedFields])
573603

574604
useEffect(() => {
575605
if (!nextPlanEnabled) {
576-
// Clear all next_plan data when disabled
577606
form.setValue('next_plan', undefined)
578607
handleFieldChange('next_plan', undefined)
579-
} else if (!form.watch('next_plan') || form.watch('next_plan') === null) {
580-
form.setValue('next_plan', {})
581-
handleFieldChange('next_plan', {})
608+
} else {
609+
setNextPlanManuallyDisabled(false)
610+
const isEmpty = !nextPlanValue.user_template_id && !nextPlanValue.expire && !nextPlanValue.data_limit && nextPlanValue.add_remaining_traffic === undefined
611+
if (isEmpty) {
612+
form.setValue('next_plan', {})
613+
handleFieldChange('next_plan', {})
614+
}
582615
}
583-
// eslint-disable-next-line
584-
}, [nextPlanEnabled])
616+
}, [nextPlanEnabled, nextPlanValue])
585617

586-
// Sync switch state when next_plan value changes (e.g., when editing a user)
587-
// Only sync when the form has data but the switch is off (initial load scenario)
588618
useEffect(() => {
589-
const nextPlan = form.watch('next_plan')
590-
const shouldBeEnabled = nextPlan !== undefined && nextPlan !== null && Object.keys(nextPlan).length > 0
619+
if (!isDialogOpen || !editingUser || nextPlanManuallyDisabled) return
620+
621+
const shouldBeEnabled = hasNextPlanValues(nextPlanValue)
591622

592-
// Only sync if:
593-
// 1. The form has data (shouldBeEnabled is true)
594-
// 2. The switch is currently off (nextPlanEnabled is false)
595-
// 3. We're not in the middle of a user manually disabling it
596623
if (shouldBeEnabled && !nextPlanEnabled) {
597624
setNextPlanEnabled(true)
598625
}
599-
}, [form.watch('next_plan'), nextPlanEnabled])
626+
// Don't automatically disable - let user control it via the toggle
627+
}, [nextPlanValue, nextPlanEnabled, isDialogOpen, editingUser, hasNextPlanValues, nextPlanManuallyDisabled])
600628

601629
// Helper to convert expire field to needed schema using the same logic as other components
602630
function normalizeExpire(expire: Date | string | number | null | undefined, useUtcTimestamp: boolean = false): string | number | undefined {
@@ -920,12 +948,6 @@ export default function UserModal({ isDialogOpen, onOpenChange, form, editingUse
920948
status: values.status,
921949
}
922950

923-
// Remove next_plan.data_limit and next_plan.expire if next_plan.user_template_id is set
924-
if (preparedValues.next_plan && preparedValues.next_plan.user_template_id) {
925-
delete preparedValues.next_plan.data_limit
926-
delete preparedValues.next_plan.expire
927-
}
928-
929951
// Check if proxy settings are filled
930952
const hasProxySettings = values.proxy_settings && Object.values(values.proxy_settings).some(settings => settings && Object.values(settings).some(value => value !== undefined && value !== ''))
931953

@@ -950,18 +972,37 @@ export default function UserModal({ isDialogOpen, onOpenChange, form, editingUse
950972
}
951973
: undefined
952974

953-
// Convert data_limit from GB to bytes
975+
let nextPlanData = undefined
976+
if (nextPlanEnabled) {
977+
const nextPlanFromValues = values.next_plan
978+
const hasValues = nextPlanFromValues && hasNextPlanValues(nextPlanFromValues)
979+
980+
if (hasValues) {
981+
nextPlanData = { ...nextPlanFromValues }
982+
983+
if (nextPlanData.user_template_id) {
984+
delete nextPlanData.data_limit
985+
delete nextPlanData.expire
986+
}
987+
} else {
988+
nextPlanData = {
989+
expire: 0,
990+
data_limit: 0,
991+
}
992+
}
993+
}
994+
954995
const sendValues = {
955996
...preparedValues,
956997
data_limit: gbToBytes(preparedValues.data_limit as any),
957998
expire: preparedValues.expire,
958-
// Only include proxy_settings if they are filled
959999
...(hasProxySettings ? { proxy_settings: cleanedProxySettings } : {}),
960-
// Force send undefined when Next Plan is disabled
961-
next_plan: nextPlanEnabled ? preparedValues.next_plan : undefined,
9621000
}
9631001

964-
// Remove proxy_settings from the payload if it's empty or undefined
1002+
if (nextPlanEnabled) {
1003+
sendValues.next_plan = nextPlanData
1004+
}
1005+
9651006
if (!hasProxySettings) {
9661007
delete sendValues.proxy_settings
9671008
}
@@ -1149,8 +1190,7 @@ export default function UserModal({ isDialogOpen, onOpenChange, form, editingUse
11491190
}
11501191
}
11511192
}
1152-
// eslint-disable-next-line
1153-
}, [isDialogOpen, editingUser, generalSettings])
1193+
}, [isDialogOpen, editingUser, generalSettings, form])
11541194

11551195

11561196
return (
@@ -1817,15 +1857,27 @@ export default function UserModal({ isDialogOpen, onOpenChange, form, editingUse
18171857
{activeTab === 'groups' && editingUser && (
18181858
<div className="rounded-[--radius] border border-border p-4">
18191859
<div className="flex items-center justify-between">
1820-
<div className="flex items-center gap-2">
1860+
<div className="flex items-center gap-2 cursor-pointer" onClick={() => {
1861+
const newValue = !nextPlanEnabled
1862+
setNextPlanEnabled(newValue)
1863+
if (!newValue) {
1864+
setNextPlanManuallyDisabled(true)
1865+
} else {
1866+
setNextPlanManuallyDisabled(false)
1867+
}
1868+
}}>
18211869
<ListStart className="h-4 w-4" />
18221870
<div>{t('userDialog.nextPlanTitle', { defaultValue: 'Next Plan' })}</div>
18231871
</div>
18241872
<Switch
18251873
checked={nextPlanEnabled}
18261874
onCheckedChange={value => {
18271875
setNextPlanEnabled(value)
1828-
// Trigger validation when Next Plan toggle changes
1876+
if (!value) {
1877+
setNextPlanManuallyDisabled(true)
1878+
} else {
1879+
setNextPlanManuallyDisabled(false)
1880+
}
18291881
const currentValues = form.getValues()
18301882
const isValid = validateAllFields(currentValues, touchedFields)
18311883
setIsFormValid(isValid)

0 commit comments

Comments
 (0)