Skip to content

Commit f08d1c4

Browse files
committed
feat(core-config-modal): add unsaved changes confirmation dialog
1 parent 9cab918 commit f08d1c4

5 files changed

Lines changed: 60 additions & 8 deletions

File tree

dashboard/public/statics/locales/en.json

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1938,7 +1938,9 @@
19381938
"keyPairGenerationFailed": "Failed to generate key pair",
19391939
"shortIdGenerationFailed": "Failed to generate short ID",
19401940
"shadowsocksPasswordGenerated": "Password generated successfully",
1941-
"shadowsocksPasswordGenerationFailed": "Failed to generate password"
1941+
"shadowsocksPasswordGenerationFailed": "Failed to generate password",
1942+
"discardChangesTitle": "Discard changes?",
1943+
"discardChangesDescription": "Your unsaved kernel configuration changes will be lost if you close this editor."
19421944
},
19431945
"coreEditor": {
19441946
"nameRequired": "Name is required",

dashboard/public/statics/locales/fa.json

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1853,7 +1853,9 @@
18531853
"keyPairGenerationFailed": "تولید جفت کلید ناموفق بود",
18541854
"shortIdGenerationFailed": "تولید شناسه کوتاه ناموفق بود",
18551855
"shadowsocksPasswordGenerated": "رمز عبور با موفقیت تولید شد",
1856-
"shadowsocksPasswordGenerationFailed": "تولید رمز عبور ناموفق بود"
1856+
"shadowsocksPasswordGenerationFailed": "تولید رمز عبور ناموفق بود",
1857+
"discardChangesTitle": "لغو تغییرات؟",
1858+
"discardChangesDescription": "اگر این ویرایشگر را ببندید، تغییرات ذخیره‌نشدهٔ پیکربندی هسته از بین خواهد رفت."
18571859
},
18581860
"coreEditor": {
18591861
"nameRequired": "نام الزامی است",

dashboard/public/statics/locales/ru.json

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1827,7 +1827,9 @@
18271827
"keyPairGenerationFailed": "Не удалось сгенерировать пару ключей",
18281828
"shortIdGenerationFailed": "Не удалось сгенерировать короткий ID",
18291829
"shadowsocksPasswordGenerated": "Пароль успешно сгенерирован",
1830-
"shadowsocksPasswordGenerationFailed": "Не удалось сгенерировать пароль"
1830+
"shadowsocksPasswordGenerationFailed": "Не удалось сгенерировать пароль",
1831+
"discardChangesTitle": "Отменить изменения?",
1832+
"discardChangesDescription": "Несохранённые изменения конфигурации ядра будут потеряны, если закрыть редактор."
18311833
},
18321834
"coreEditor": {
18331835
"nameRequired": "Укажите имя",

dashboard/public/statics/locales/zh.json

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1900,7 +1900,9 @@
19001900
"keyPairGenerationFailed": "密钥对生成失败",
19011901
"shortIdGenerationFailed": "短 ID 生成失败",
19021902
"shadowsocksPasswordGenerated": "密码生成成功",
1903-
"shadowsocksPasswordGenerationFailed": "密码生成失败"
1903+
"shadowsocksPasswordGenerationFailed": "密码生成失败",
1904+
"discardChangesTitle": "放弃更改?",
1905+
"discardChangesDescription": "如果关闭此编辑器,未保存的内核配置更改将会丢失。"
19041906
},
19051907
"coreEditor": {
19061908
"nameRequired": "名称为必填项",

dashboard/src/features/nodes/dialogs/core-config-modal.tsx

Lines changed: 48 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import { CodeEditorPanel } from '@/components/common/code-editor-panel'
22
import { CopyButton } from '@/components/common/copy-button'
3+
import { AlertDialog, AlertDialogAction, AlertDialogCancel, AlertDialogContent, AlertDialogDescription, AlertDialogFooter, AlertDialogHeader, AlertDialogTitle } from '@/components/ui/alert-dialog'
34
import { Button } from '@/components/ui/button'
45
import { Checkbox } from '@/components/ui/checkbox'
56
import { Dialog, DialogContent, DialogFooter, DialogHeader, DialogTitle } from '@/components/ui/dialog'
@@ -79,6 +80,7 @@ export default function CoreConfigModal({ isDialogOpen, onOpenChange, form, edit
7980
const [selectedVlessVariant, setSelectedVlessVariant] = useState<VlessKeyVariant>('x25519')
8081
const [vlessAdvancedSeed, setVlessAdvancedSeed] = useState<VlessBuilderOptions | undefined>(undefined)
8182
const [isVlessAdvancedModalOpen, setIsVlessAdvancedModalOpen] = useState(false)
83+
const [discardChangesOpen, setDiscardChangesOpen] = useState(false)
8284

8385
// Results dialog state
8486
const [isResultsDialogOpen, setIsResultsDialogOpen] = useState(false)
@@ -278,6 +280,32 @@ export default function CoreConfigModal({ isDialogOpen, onOpenChange, form, edit
278280
generateWireGuardKeys()
279281
}, [generateWireGuardKeys, generatedWireGuardKeyPair, showResultDialog])
280282

283+
const closeModal = useCallback(() => {
284+
setDiscardChangesOpen(false)
285+
onOpenChange(false)
286+
}, [onOpenChange])
287+
288+
const handleDialogOpenChange = useCallback(
289+
(open: boolean) => {
290+
if (open) {
291+
onOpenChange(true)
292+
return
293+
}
294+
if (createCoreMutation.isPending || modifyCoreMutation.isPending || form.formState.isSubmitting) return
295+
if (form.formState.isDirty) {
296+
setDiscardChangesOpen(true)
297+
return
298+
}
299+
closeModal()
300+
},
301+
[closeModal, createCoreMutation.isPending, form.formState.isDirty, form.formState.isSubmitting, modifyCoreMutation.isPending, onOpenChange],
302+
)
303+
304+
const confirmDiscardChanges = useCallback(() => {
305+
form.reset()
306+
closeModal()
307+
}, [closeModal, form])
308+
281309
const onSubmit = async (values: CoreConfigFormValues) => {
282310
try {
283311
// Validate JSON first
@@ -335,8 +363,8 @@ export default function CoreConfigModal({ isDialogOpen, onOpenChange, form, edit
335363
// Invalidate core config queries after successful action
336364
queryClient.invalidateQueries({ queryKey: ['/api/cores'] })
337365
queryClient.invalidateQueries({ queryKey: ['/api/cores/simple'] })
338-
onOpenChange(false)
339-
form.reset()
366+
form.reset(values)
367+
closeModal()
340368
} catch (error: any) {
341369
console.error('Core config operation failed:', error)
342370
console.error('Error response:', error?.response)
@@ -782,7 +810,7 @@ export default function CoreConfigModal({ isDialogOpen, onOpenChange, form, edit
782810
}}
783811
/>
784812
{renderResultDialog()}
785-
<Dialog open={isDialogOpen} onOpenChange={onOpenChange}>
813+
<Dialog open={isDialogOpen} onOpenChange={handleDialogOpenChange}>
786814
<DialogContent className="h-full w-full max-w-5xl md:h-auto">
787815
<DialogHeader>
788816
<DialogTitle className="flex items-center gap-2">
@@ -1072,7 +1100,7 @@ export default function CoreConfigModal({ isDialogOpen, onOpenChange, form, edit
10721100
)}
10731101

10741102
<div className="flex justify-end gap-2">
1075-
<Button type="button" variant="outline" onClick={() => onOpenChange(false)} disabled={createCoreMutation.isPending || modifyCoreMutation.isPending}>
1103+
<Button type="button" variant="outline" onClick={() => handleDialogOpenChange(false)} disabled={createCoreMutation.isPending || modifyCoreMutation.isPending}>
10761104
{t('cancel')}
10771105
</Button>
10781106
<LoaderButton
@@ -1090,6 +1118,22 @@ export default function CoreConfigModal({ isDialogOpen, onOpenChange, form, edit
10901118
</Form>
10911119
</DialogContent>
10921120
</Dialog>
1121+
<AlertDialog open={discardChangesOpen} onOpenChange={setDiscardChangesOpen}>
1122+
<AlertDialogContent dir={dir}>
1123+
<AlertDialogHeader>
1124+
<AlertDialogTitle>{t('coreConfigModal.discardChangesTitle', { defaultValue: 'Discard changes?' })}</AlertDialogTitle>
1125+
<AlertDialogDescription>
1126+
{t('coreConfigModal.discardChangesDescription', {
1127+
defaultValue: 'Your unsaved kernel configuration changes will be lost if you close this editor.',
1128+
})}
1129+
</AlertDialogDescription>
1130+
</AlertDialogHeader>
1131+
<AlertDialogFooter>
1132+
<AlertDialogCancel>{t('cancel')}</AlertDialogCancel>
1133+
<AlertDialogAction onClick={confirmDiscardChanges}>{t('coreEditor.leave', { defaultValue: 'Leave' })}</AlertDialogAction>
1134+
</AlertDialogFooter>
1135+
</AlertDialogContent>
1136+
</AlertDialog>
10931137
</>
10941138
)
10951139
}

0 commit comments

Comments
 (0)