@@ -13,7 +13,7 @@ import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@
1313import { Textarea } from '@/components/ui/textarea'
1414import { queryClient } from '@/utils/query-client'
1515import useDirDetection from '@/hooks/use-dir-detection'
16- import React , { useState , useEffect } from 'react'
16+ import React , { useState , useEffect , useRef } from 'react'
1717import { Loader2 , Settings , RefreshCw } from 'lucide-react'
1818import { v4 as uuidv4 , v5 as uuidv5 , v6 as uuidv6 , v7 as uuidv7 } from 'uuid'
1919import { LoaderButton } from '../ui/loader-button'
@@ -82,15 +82,75 @@ export default function NodeModal({ isDialogOpen, onOpenChange, form, editingNod
8282 )
8383
8484 const currentNode = node || initialNodeData
85+ const lastSyncedNodeRef = useRef < NodeResponse | null > ( null )
86+
8587 useEffect ( ( ) => {
8688 if ( isDialogOpen ) {
8789 setErrorDetails ( null )
8890 setAutoCheck ( true )
8991 dataLimitInputRef . current = ''
9092 setIsFetchingNodeData ( false )
93+ lastSyncedNodeRef . current = null
9194 }
9295 } , [ isDialogOpen ] )
9396
97+ // Update form when node data changes (from auto-refresh or external updates)
98+ useEffect ( ( ) => {
99+ if ( ! isDialogOpen || ! editingNode || ! editingNodeId || ! node ) return
100+
101+ // Skip if form is dirty (user has made changes)
102+ if ( form . formState . isDirty ) return
103+
104+ // Skip if this is the same node data we already synced
105+ // Compare key fields that change externally (status, message, versions, usage)
106+ const lastSynced = lastSyncedNodeRef . current
107+ if ( lastSynced &&
108+ lastSynced . id === node . id &&
109+ lastSynced . status === node . status &&
110+ lastSynced . message === node . message &&
111+ lastSynced . xray_version === node . xray_version &&
112+ lastSynced . node_version === node . node_version &&
113+ lastSynced . uplink === node . uplink &&
114+ lastSynced . downlink === node . downlink &&
115+ lastSynced . name === node . name &&
116+ lastSynced . address === node . address &&
117+ lastSynced . port === node . port ) {
118+ return
119+ }
120+
121+ // Update form with new node data
122+ const dataLimitBytes = node . data_limit ?? null
123+ const dataLimitGB = dataLimitBytes !== null && dataLimitBytes !== undefined && dataLimitBytes > 0
124+ ? dataLimitBytes / ( 1024 * 1024 * 1024 )
125+ : 0
126+
127+ if ( dataLimitGB > 0 ) {
128+ const formatted = parseFloat ( dataLimitGB . toFixed ( 9 ) )
129+ dataLimitInputRef . current = String ( formatted )
130+ } else {
131+ dataLimitInputRef . current = ''
132+ }
133+
134+ form . reset ( {
135+ name : node . name ,
136+ address : node . address ,
137+ port : node . port ,
138+ usage_coefficient : node . usage_coefficient ,
139+ connection_type : node . connection_type ,
140+ server_ca : node . server_ca ,
141+ keep_alive : node . keep_alive ,
142+ api_key : ( node . api_key as string ) || '' ,
143+ core_config_id : node . core_config_id ?? cores ?. cores ?. [ 0 ] ?. id ,
144+ data_limit : dataLimitGB ,
145+ data_limit_reset_strategy : node . data_limit_reset_strategy ?? DataLimitResetStrategy . no_reset ,
146+ reset_time : node . reset_time ?? null ,
147+ default_timeout : node . default_timeout ?? 10 ,
148+ internal_timeout : node . internal_timeout ?? 15 ,
149+ } , { keepDirty : false , keepValues : false } )
150+
151+ lastSyncedNodeRef . current = node
152+ } , [ node , isDialogOpen , editingNode , editingNodeId , form , cores ] )
153+
94154 useEffect ( ( ) => {
95155 const values = form . getValues ( )
96156 const timer = setTimeout ( ( ) => {
@@ -145,6 +205,7 @@ export default function NodeModal({ isDialogOpen, onOpenChange, form, editingNod
145205 default_timeout : nodeData . default_timeout ?? 10 ,
146206 internal_timeout : nodeData . internal_timeout ?? 15 ,
147207 } )
208+ lastSyncedNodeRef . current = nodeData
148209 setIsFetchingNodeData ( false )
149210 } else {
150211 const fetchNodeData = async ( ) => {
@@ -178,6 +239,7 @@ export default function NodeModal({ isDialogOpen, onOpenChange, form, editingNod
178239 default_timeout : nodeData . default_timeout ?? 10 ,
179240 internal_timeout : nodeData . internal_timeout ?? 15 ,
180241 } )
242+ lastSyncedNodeRef . current = nodeData
181243 } catch ( error ) {
182244 console . error ( 'Error fetching node data:' , error )
183245 toast . error ( t ( 'nodes.fetchFailed' ) )
@@ -308,6 +370,7 @@ export default function NodeModal({ isDialogOpen, onOpenChange, form, editingNod
308370
309371 if ( nodeId && editingNode ) {
310372 queryClient . invalidateQueries ( { queryKey : [ `/api/node/${ nodeId } ` ] } )
373+ lastSyncedNodeRef . current = null
311374 }
312375 queryClient . invalidateQueries ( { queryKey : [ '/api/nodes' ] } )
313376 onOpenChange ( false )
@@ -528,7 +591,7 @@ export default function NodeModal({ isDialogOpen, onOpenChange, form, editingNod
528591 } }
529592 />
530593
531- < Accordion type = "single" collapsible className = "mb-4 mt-0 w-full pb-4" >
594+ < Accordion type = "single" collapsible className = "mb-4 ! mt-0 w-full pb-4" >
532595 < AccordionItem className = "rounded-sm border px-4 [&_[data-state=closed]]:no-underline [&_[data-state=open]]:no-underline" value = "advanced-settings" >
533596 < AccordionTrigger >
534597 < div className = "flex items-center gap-2" >
0 commit comments