Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 5 additions & 1 deletion dashboard/public/statics/locales/en.json
Original file line number Diff line number Diff line change
Expand Up @@ -1873,7 +1873,11 @@
"shadowsocksPasswordGenerationFailed": "Failed to generate password",
"realityTab": "Reality",
"vlessTab": "VLESS",
"shadowsocksTab": "ShadowSocks"
"shadowsocksTab": "ShadowSocks",
"unsavedChanges": "Unsaved Changes",
"unsavedChangesMessage": "You have unsaved changes. Are you sure you want to exit without saving?",
"keepEditing": "Keep Editing",
"discardChanges": "Discard Changes"
},
"settings.cores.title": "Cores",
"settings.cores.description": "Manage Your Cores",
Expand Down
6 changes: 5 additions & 1 deletion dashboard/public/statics/locales/fa.json
Original file line number Diff line number Diff line change
Expand Up @@ -1788,7 +1788,11 @@
"shadowsocksPasswordGenerationFailed": "تولید رمز عبور ناموفق بود",
"realityTab": "Reality",
"vlessTab": "VLESS",
"shadowsocksTab": "ShadowSocks"
"shadowsocksTab": "ShadowSocks",
"unsavedChanges": "تغییرات ذخیره نشده",
"unsavedChangesMessage": "شما تغییرات ذخیره نشده‌ای دارید. آیا مطمئن هستید که می‌خواهید بدون ذخیره خارج شوید؟",
"keepEditing": "ادامه ویرایش",
"discardChanges": "لغو تغییرات"
},
"settings.cores.title": "هسته‌ها",
"settings.cores.description": "مدیریت هسته‌های شما",
Expand Down
6 changes: 5 additions & 1 deletion dashboard/public/statics/locales/ru.json
Original file line number Diff line number Diff line change
Expand Up @@ -1762,7 +1762,11 @@
"shadowsocksPasswordGenerationFailed": "Не удалось сгенерировать пароль",
"realityTab": "Reality",
"vlessTab": "VLESS",
"shadowsocksTab": "ShadowSocks"
"shadowsocksTab": "ShadowSocks",
"unsavedChanges": "Несохраненные изменения",
"unsavedChangesMessage": "У вас есть несохраненные изменения. Вы уверены, что хотите выйти без сохранения?",
"keepEditing": "Продолжить редактирование",
"discardChanges": "Отменить изменения"
},
"settings.cores.title": "Ядра",
"settings.cores.description": "Управление вашими ядрами",
Expand Down
6 changes: 5 additions & 1 deletion dashboard/public/statics/locales/zh.json
Original file line number Diff line number Diff line change
Expand Up @@ -1835,7 +1835,11 @@
"shadowsocksPasswordGenerationFailed": "密码生成失败",
"realityTab": "Reality",
"vlessTab": "VLESS",
"shadowsocksTab": "ShadowSocks"
"shadowsocksTab": "ShadowSocks",
"unsavedChanges": "未保存的更改",
"unsavedChangesMessage": "您有未保存的更改。您确定要在不保存的情况下退出吗?",
"keepEditing": "继续编辑",
"discardChanges": "放弃更改"
},
"settings.cores.title": "核心",
"settings.cores.description": "管理您的核心",
Expand Down
61 changes: 58 additions & 3 deletions dashboard/src/components/dialogs/core-config-modal.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import { CopyButton } from '@/components/common/copy-button'
import { Button } from '@/components/ui/button'
import { Checkbox } from '@/components/ui/checkbox'
import { Dialog, DialogContent, DialogFooter, DialogHeader, DialogTitle } from '@/components/ui/dialog'
import { AlertDialog, AlertDialogCancel, AlertDialogContent, AlertDialogDescription, AlertDialogFooter, AlertDialogHeader, AlertDialogTitle } from '@/components/ui/alert-dialog'
import { Form, FormControl, FormField, FormItem, FormLabel, FormMessage } from '@/components/ui/form'
import { Input } from '@/components/ui/input'
import { Label } from '@/components/ui/label'
Expand All @@ -20,7 +21,7 @@ import { generateWireGuardKeyPair } from '@/utils/wireguard'
import { encodeURLSafe } from '@stablelib/base64'
import { generateKeyPair } from '@stablelib/x25519'
import { debounce } from 'es-toolkit'
import { Info, Key, Maximize2, Minimize2, Sparkles, Shield, Pencil, Cpu } from 'lucide-react'
import { Info, Key, Maximize2, Minimize2, Sparkles, Shield, Pencil, Cpu, AlertTriangle } from 'lucide-react'
import { MlKem768 } from 'mlkem'
import { Suspense, lazy, useCallback, useEffect, useState } from 'react'
import { UseFormReturn } from 'react-hook-form'
Expand Down Expand Up @@ -185,6 +186,10 @@ export default function CoreConfigModal({ isDialogOpen, onOpenChange, form, edit
const [generatedShadowsocksPassword, setGeneratedShadowsocksPassword] = useState<{ password: string; encryptionMethod: string } | null>(null)
const [generatedMldsa65, setGeneratedMldsa65] = useState<{ seed: string; verify: string } | null>(null)
const [generatedVLESS, setGeneratedVLESS] = useState<any>(null)

// Unsaved changes confirmation dialog state
const [isConfirmExitDialogOpen, setIsConfirmExitDialogOpen] = useState(false)

const handleVlessVariantChange = useCallback(
(value: string) => {
if (value === 'x25519' || value === 'mlkem768') {
Expand All @@ -201,6 +206,30 @@ export default function CoreConfigModal({ isDialogOpen, onOpenChange, form, edit
setIsResultsDialogOpen(true)
}, [])

// Handle modal close with unsaved changes check
const handleModalCloseWithCheck = useCallback((open: boolean) => {
if (!open && form.formState.isDirty) {
setIsConfirmExitDialogOpen(true)
} else {
onOpenChange(open)
if (!open) {
form.reset()
}
}
}, [form, onOpenChange])

// Confirm exit without saving
const handleConfirmExit = useCallback(() => {
setIsConfirmExitDialogOpen(false)
form.reset()
onOpenChange(false)
}, [form, onOpenChange])

// Cancel exit
const handleCancelExit = useCallback(() => {
setIsConfirmExitDialogOpen(false)
}, [])

const relayoutEditor = useCallback(
(editor = editorInstance) => {
if (!editor) return
Expand Down Expand Up @@ -736,6 +765,7 @@ export default function CoreConfigModal({ isDialogOpen, onOpenChange, form, edit
setValidation({ isValid: true })
setEditorInstance(null)
setIsEditorReady(false)
setIsConfirmExitDialogOpen(false)
// Don't clear generated values - keep them for reuse
}
}, [isDialogOpen])
Expand Down Expand Up @@ -1325,7 +1355,7 @@ export default function CoreConfigModal({ isDialogOpen, onOpenChange, form, edit
<>
{renderVlessAdvancedModal()}
{renderResultDialog()}
<Dialog open={isDialogOpen} onOpenChange={onOpenChange}>
<Dialog open={isDialogOpen} onOpenChange={handleModalCloseWithCheck}>
<DialogContent className="h-full w-full max-w-5xl md:h-auto">
<DialogHeader>
<DialogTitle className="flex items-center gap-2">
Expand Down Expand Up @@ -1809,7 +1839,7 @@ export default function CoreConfigModal({ isDialogOpen, onOpenChange, form, edit
)}

<div className="flex justify-end gap-2">
<Button type="button" variant="outline" onClick={() => onOpenChange(false)} disabled={createCoreMutation.isPending || modifyCoreMutation.isPending}>
<Button type="button" variant="outline" onClick={() => handleModalCloseWithCheck(false)} disabled={createCoreMutation.isPending || modifyCoreMutation.isPending}>
{t('cancel')}
</Button>
<LoaderButton
Expand All @@ -1827,6 +1857,31 @@ export default function CoreConfigModal({ isDialogOpen, onOpenChange, form, edit
</Form>
</DialogContent>
</Dialog>

{/* Unsaved Changes Confirmation Dialog */}
<AlertDialog open={isConfirmExitDialogOpen} onOpenChange={setIsConfirmExitDialogOpen}>
<AlertDialogContent>
<AlertDialogHeader>
<AlertDialogTitle className="flex items-center gap-2">
<AlertTriangle className="h-5 w-5 text-destructive" />
{t('coreConfigModal.unsavedChanges', { defaultValue: 'Unsaved Changes' })}
</AlertDialogTitle>
<AlertDialogDescription>
{t('coreConfigModal.unsavedChangesMessage', {
defaultValue: 'You have unsaved changes. Are you sure you want to exit without saving?'
})}
</AlertDialogDescription>
</AlertDialogHeader>
<AlertDialogFooter>
<AlertDialogCancel onClick={handleCancelExit}>
{t('coreConfigModal.keepEditing', { defaultValue: 'Keep Editing' })}
</AlertDialogCancel>
<Button variant="destructive" onClick={handleConfirmExit}>
{t('coreConfigModal.discardChanges', { defaultValue: 'Discard Changes' })}
</Button>
</AlertDialogFooter>
</AlertDialogContent>
</AlertDialog>
</>
)
}