1- import { useState } from "react"
2- import { useParams , useNavigate } from "react-router-dom"
1+ import { useState , useEffect , useCallback , useRef } from "react"
2+ import { useParams , useNavigate , useBlocker } from "react-router-dom"
33import { useTranslation } from "react-i18next"
44import { useProviderAgent , useProviderMutations } from "@/hooks/use-provider"
55import type { RegisterAgentData } from "@/hooks/use-provider"
@@ -27,6 +27,9 @@ export function AgentEditPage() {
2727 const [ capInput , setCapInput ] = useState ( "" )
2828 const [ tagInput , setTagInput ] = useState ( "" )
2929
30+ const [ isDirty , setIsDirty ] = useState ( false )
31+ const submittingRef = useRef ( false )
32+
3033 // Initialize form when agent data loads
3134 if ( agent && ! form ) {
3235 setForm ( {
@@ -43,6 +46,25 @@ export function AgentEditPage() {
4346 } )
4447 }
4548
49+ // Track dirty state
50+ const markDirty = useCallback ( ( ) => setIsDirty ( true ) , [ ] )
51+
52+ // Warn on browser close / tab close
53+ useEffect ( ( ) => {
54+ if ( ! isDirty ) return
55+ const handler = ( e : BeforeUnloadEvent ) => {
56+ e . preventDefault ( )
57+ }
58+ window . addEventListener ( "beforeunload" , handler )
59+ return ( ) => window . removeEventListener ( "beforeunload" , handler )
60+ } , [ isDirty ] )
61+
62+ // Block in-app navigation
63+ const blocker = useBlocker (
64+ ( { currentLocation, nextLocation } ) =>
65+ isDirty && ! submittingRef . current && currentLocation . pathname !== nextLocation . pathname
66+ )
67+
4668 if ( loading ) {
4769 return (
4870 < div className = "flex h-64 items-center justify-center" >
@@ -61,8 +83,10 @@ export function AgentEditPage() {
6183
6284 if ( ! agent || ! form ) return null
6385
64- const update = ( key : keyof RegisterAgentData , value : unknown ) =>
86+ const update = ( key : keyof RegisterAgentData , value : unknown ) => {
6587 setForm ( ( prev ) => prev ? { ...prev , [ key ] : value } : prev )
88+ markDirty ( )
89+ }
6690
6791 const toggleProtocol = ( proto : string ) => {
6892 const current = form . protocols ?? [ ]
@@ -92,11 +116,14 @@ export function AgentEditPage() {
92116 if ( ! id || ! form ) return
93117 setSubmitting ( true )
94118 setSubmitError ( null )
119+ submittingRef . current = true
95120 try {
96121 await updateAgent ( id , form as RegisterAgentData )
122+ setIsDirty ( false )
97123 navigate ( `/console/agents/${ id } ` )
98124 } catch ( e ) {
99125 setSubmitError ( e instanceof Error ? e . message : "Failed to save" )
126+ submittingRef . current = false
100127 } finally {
101128 setSubmitting ( false )
102129 }
@@ -323,6 +350,24 @@ export function AgentEditPage() {
323350 { submitting ? t ( 'wizard.saving' ) : t ( 'wizard.saveChanges' ) }
324351 </ Button >
325352 </ div >
353+
354+ { /* Unsaved changes navigation blocker */ }
355+ { blocker . state === "blocked" && (
356+ < div className = "fixed inset-0 z-50 flex items-center justify-center bg-black/50" >
357+ < div className = "rounded-lg border border-border bg-card p-6 shadow-lg max-w-sm" >
358+ < h3 className = "text-lg font-semibold" > { t ( 'wizard.unsavedChangesTitle' ) } </ h3 >
359+ < p className = "mt-2 text-sm text-muted-foreground" > { t ( 'wizard.unsavedChangesDesc' ) } </ p >
360+ < div className = "mt-4 flex justify-end gap-2" >
361+ < Button variant = "outline" size = "sm" onClick = { ( ) => blocker . reset ?.( ) } >
362+ { t ( 'common.cancel' ) }
363+ </ Button >
364+ < Button variant = "destructive" size = "sm" onClick = { ( ) => blocker . proceed ?.( ) } >
365+ { t ( 'wizard.discardChanges' ) }
366+ </ Button >
367+ </ div >
368+ </ div >
369+ </ div >
370+ ) }
326371 </ div >
327372 )
328373}
0 commit comments