Repository: OneKeyHQ/app-monorepo
Pull request #7315: feat: implement form instance management with Jotai in Form component
Author: login: limichange
Created: 2025-05-13T01:03:17Z | Merged: 2025-05-14T01:44:53Z



<!-- This is an auto-generated comment: release notes by coderabbit.ai -->
## Summary by CodeRabbit

- **New Features**
	- Added lifecycle management for dialog and form instances, enabling more robust tracking of open dialogs and forms.
	- Improved navigation logic to prevent leaving pages with open dialogs or unsaved form changes, including confirmation prompts when necessary.
	- Introduced a new translation key for close confirmation descriptions.
- **Bug Fixes**
	- Enhanced modal and dialog handling to reduce the risk of data loss and ensure smoother user experience during navigation.
<!-- end of auto-generated comment: release notes by coderabbit.ai -->


Diff:
diff --git a/packages/components/src/composite/Dialog/dialogInstances.ts b/packages/components/src/composite/Dialog/dialogInstances.ts
new file mode 100644
index 000000000000..2c1ebf25c966
--- /dev/null
+++ b/packages/components/src/composite/Dialog/dialogInstances.ts
@@ -0,0 +1,20 @@
+import type { IDialogInstance } from './type';
+
+const dialogInstances: IDialogInstance[] = [];
+
+export function getDialogInstances(): IDialogInstance[] {
+  return dialogInstances;
+}
+
+export function addDialogInstance(instance: IDialogInstance): void {
+  if (!dialogInstances.includes(instance)) {
+    dialogInstances.push(instance);
+  }
+}
+
+export function removeDialogInstance(instance: IDialogInstance): void {
+  const idx = dialogInstances.indexOf(instance);
+  if (idx !== -1) {
+    dialogInstances.splice(idx, 1);
+  }
+}
diff --git a/packages/components/src/composite/Dialog/index.tsx b/packages/components/src/composite/Dialog/index.tsx
index 550669303b61..0b5782d5fd70 100644
--- a/packages/components/src/composite/Dialog/index.tsx
+++ b/packages/components/src/composite/Dialog/index.tsx
@@ -45,6 +45,7 @@ import { Spinner, Stack } from '../../primitives';
 import { Content } from './Content';
 import { DialogContext } from './context';
 import { DialogForm } from './DialogForm';
+import { addDialogInstance, removeDialogInstance } from './dialogInstances';
 import { Footer, FooterAction } from './Footer';
 import {
   DialogDescription,
@@ -79,6 +80,7 @@ export type {
   IDialogInstance,
   IDialogShowProps,
 } from './type';
+export * from './dialogInstances';
 
 export const FIX_SHEET_PROPS: IStackProps = {
   display: 'block',
@@ -479,6 +481,8 @@ function dialogShow({
       }
     | undefined;
 
+  let dialogInstance: IDialogInstance | undefined;
+
   const buildForwardOnClose =
     (options: {
       onClose?: (extra?: { flag?: string }) => void | Promise<void>;
@@ -494,6 +498,10 @@ function dialogShow({
             portalRef.current.destroy();
             portalRef = undefined;
           }
+          if (dialogInstance) {
+            removeDialogInstance(dialogInstance);
+            dialogInstance = undefined;
+          }
           void options.onClose?.(extra);
           resolve();
         }, 300);
@@ -539,11 +547,13 @@ function dialogShow({
     }
     return instanceRef?.current?.close(extra);
   };
-  return {
+  dialogInstance = {
     close,
     getForm: () => instanceRef?.current?.getForm(),
     isExist,
   };
+  addDialogInstance(dialogInstance);
+  return dialogInstance;
 }
 
 const dialogConfirm = (props: IDialogConfirmProps) =>
diff --git a/packages/components/src/forms/Form/formInstances.ts b/packages/components/src/forms/Form/formInstances.ts
new file mode 100644
index 000000000000..88cc185b3c34
--- /dev/null
+++ b/packages/components/src/forms/Form/formInstances.ts
@@ -0,0 +1,20 @@
+import type { UseFormReturn } from 'react-hook-form';
+
+const formInstances: UseFormReturn<any>[] = [];
+
+export function getFormInstances(): UseFormReturn<any>[] {
+  return formInstances;
+}
+
+export function addFormInstance(instance: UseFormReturn<any>): void {
+  if (!formInstances.includes(instance)) {
+    formInstances.push(instance);
+  }
+}
+
+export function removeFormInstance(instance: UseFormReturn<any>): void {
+  const index = formInstances.indexOf(instance);
+  if (index !== -1) {
+    formInstances.splice(index, 1);
+  }
+}
diff --git a/packages/components/src/forms/Form/index.tsx b/packages/components/src/forms/Form/index.tsx
index 61681c3e2333..809f9bfef8c3 100644
--- a/packages/components/src/forms/Form/index.tsx
+++ b/packages/components/src/forms/Form/index.tsx
@@ -4,6 +4,7 @@ import {
   cloneElement,
   isValidElement,
   useCallback,
+  useEffect,
   useMemo,
 } from 'react';
 
@@ -26,6 +27,8 @@ import {
 import { Input } from '../Input';
 import { TextArea, TextAreaInput } from '../TextArea';
 
+import { addFormInstance, removeFormInstance } from './formInstances';
+
 import type { ISizableTextProps } from '../../primitives';
 import type { IPropsWithTestId } from '../../types';
 import type { ControllerRenderProps, UseFormReturn } from 'react-hook-form';
@@ -53,6 +56,14 @@ function HiddenSubmit() {
 }
 
 export function FormWrapper({ form: formContext, children }: IFormProps) {
+  useEffect(() => {
+    addFormInstance(formContext);
+
+    return () => {
+      removeFormInstance(formContext);
+    };
+  }, [formContext]);
+
   return (
     <FormProvider {...formContext}>
       <TMForm onSubmit={formContext.submit} position="relative">
@@ -270,3 +281,5 @@ export const Form = withStaticProperties(FormWrapper, {
   Field,
   FieldDescription,
 });
+
+export * from './formInstances';
diff --git a/packages/kit/src/provider/Bootstrap.tsx b/packages/kit/src/provider/Bootstrap.tsx
index ba9b64972886..510043b6d7f3 100644
--- a/packages/kit/src/provider/Bootstrap.tsx
+++ b/packages/kit/src/provider/Bootstrap.tsx
@@ -1,6 +1,6 @@
 import { useCallback, useEffect, useRef } from 'react';
 
-import { debounce, noop } from 'lodash';
+import { debounce, isEqual, noop } from 'lodash';
 import { useIntl } from 'react-intl';
 
 import {
@@ -8,6 +8,8 @@ import {
   Image,
   SizableText,
   YStack,
+  getDialogInstances,
+  getFormInstances,
   rootNavigationRef,
   useShortcuts,
 } from '@onekeyhq/components';
@@ -56,6 +58,8 @@ const useAppUpdateInfoCallback = platformEnv.isDesktop
 
 const useDesktopEvents = platformEnv.isDesktop
   ? () => {
+      const formInstances = getFormInstances();
+      const dialogInstances = getDialogInstances();
       const intl = useIntl();
       const navigation = useAppNavigation();
       const onLock = useOnLockCallback();
@@ -181,20 +185,26 @@ const useDesktopEvents = platformEnv.isDesktop
 
           const allModalRoutes = getAllModalRoutes();
 
-          if (!allModalRoutes) {
+          if (!allModalRoutes || dialogInstances.length !== 0) {
             return;
           }
 
-          if (allModalRoutes.length === 1 || allModalRoutes.length === 2) {
-            closeAllModalRoutes();
-            return;
-          }
+          const formInstance = formInstances[formInstances.length - 1];
+          const isFormChanged =
+            formInstance &&
+            !isEqual(
+              formInstance.formState.defaultValues,
+              formInstance.getValues(),
+            );
 
-          if (allModalRoutes.length > 2) {
+          if (allModalRoutes.length > 0 && isFormChanged) {
             Dialog.show({
               title: intl.formatMessage({
                 id: ETranslations.global_close,
               }),
+              description: intl.formatMessage({
+                id: ETranslations.global_close_confirm_description,
+              }),
               showCancelButton: true,
               showFooter: true,
               showConfirmButton: true,
@@ -205,11 +215,13 @@ const useDesktopEvents = platformEnv.isDesktop
             return;
           }
 
+          closeAllModalRoutes();
+
           setTimeout(() => {
             navigateAction?.();
           }, 100);
         },
-        [intl],
+        [intl, formInstances, dialogInstances],
       );
 
       useEffect(() => {
diff --git a/packages/shared/src/locale/enum/translations.ts b/packages/shared/src/locale/enum/translations.ts
index 30d951c78083..c52b3b71403b 100644
--- a/packages/shared/src/locale/enum/translations.ts
+++ b/packages/shared/src/locale/enum/translations.ts
@@ -1064,6 +1064,7 @@
   global_circulating_supply = 'global.circulating_supply',
   global_clear = 'global.clear',
   global_close = 'global.close',
+  global_close_confirm_description = 'global.close_confirm_description',
   global_collect_to_device = 'global.collect_to_device',
   global_collect_to_device_canceled = 'global.collect_to_device_canceled',
   global_collect_to_device_failed = 'global.collect_to_device_failed',
diff --git a/packages/shared/src/locale/json/bn.json b/packages/shared/src/locale/json/bn.json
index e19bc8873ad5..3b6e8fa2fb2f 100644
--- a/packages/shared/src/locale/json/bn.json
+++ b/packages/shared/src/locale/json/bn.json
@@ -1059,6 +1059,7 @@
   "global.circulating_supply": "পরিসঞ্চারিত সরবরাহ",
   "global.clear": "পরিষ্কার",
   "global.close": "বন্ধ করুন",
+  "global.close_confirm_description": "আপনার সেভ না করা পরিবর্তন আছে। আপনি যদি এখন বন্ধ করেন, এই তথ্য হারিয়ে যাবে।",
   "global.collect_to_device": "ডিভাইসে সংগ্রহ করুন",
   "global.collect_to_device_canceled": "ডিভাইসে বাতিল করা হয়েছে",
   "global.collect_to_device_failed": "সংগ্রহ করতে ব্যর্থ হয়েছে, দয়া করে আবার চেষ্টা করুন",
diff --git a/packages/shared/src/locale/json/de.json b/packages/shared/src/locale/json/de.json
index e343512f8cda..fb9524d676fb 100644
--- a/packages/shared/src/locale/json/de.json
+++ b/packages/shared/src/locale/json/de.json
@@ -1059,6 +1059,7 @@
   "global.circulating_supply": "Umlaufende Versorgung",
   "global.clear": "Klar",
   "global.close": "Schließen",
+  "global.close_confirm_description": "Sie haben nicht gespeicherte Änderungen. Wenn Sie jetzt schließen, gehen diese Informationen verloren.",
   "global.collect_to_device": "Auf Gerät sammeln",
   "global.collect_to_device_canceled": "Auf dem Gerät abgebrochen",
   "global.collect_to_device_failed": "Fehler beim Sammeln, bitte versuchen Sie es erneut",
diff --git a/packages/shared/src/locale/json/en.json b/packages/shared/src/locale/json/en.json
index 2b1c0c7c999d..07a31e20fa9c 100644
--- a/packages/shared/src/locale/json/en.json
+++ b/packages/shared/src/locale/json/en.json
@@ -1059,6 +1059,7 @@
   "global.circulating_supply": "Circulating supply",
   "global.clear": "Clear",
   "global.close": "Close",
+  "global.close_confirm_description": "You have unsaved changes. If you close now, this information will be lost.",
   "global.collect_to_device": "Collect to device",
   "global.collect_to_device_canceled": "Canceled on device",
   "global.collect_to_device_failed": "Failed to collect, please try again",
diff --git a/packages/shared/src/locale/json/en_US.json b/packages/shared/src/locale/json/en_US.json
index 7fc1aad07a67..08dd22eb592b 100644
--- a/packages/shared/src/locale/json/en_US.json
+++ b/packages/shared/src/locale/json/en_US.json
@@ -1059,6 +1059,7 @@
   "global.circulating_supply": "Circulating supply",
   "global.clear": "Clear",
   "global.close": "Close",
+  "global.close_confirm_description": "You have unsaved changes. If you close now, this information will be lost.",
   "global.collect_to_device": "Collect to device",
   "global.collect_to_device_canceled": "Canceled on device",
   "global.collect_to_device_failed": "Failed to collect, please try again",
diff --git a/packages/shared/src/locale/json/es.json b/packages/shared/src/locale/json/es.json
index f40b890f2bc9..313a36d98841 100644
--- a/packages/shared/src/locale/json/es.json
+++ b/packages/shared/src/locale/json/es.json
@@ -1059,6 +1059,7 @@
   "global.circulating_supply": "Suministro en circulación",
   "global.clear": "Claro",
   "global.close": "Cerrar",
+  "global.close_confirm_description": "Tiene cambios sin guardar. Si cierra ahora, esta información se perderá.",
   "global.collect_to_device": "Recoger en el dispositivo",
   "global.collect_to_device_canceled": "Cancelado en el dispositivo",
   "global.collect_to_device_failed": "Error al recoger, por favor intenta de nuevo",
diff --git a/packages/shared/src/locale/json/fr_FR.json b/packages/shared/src/locale/json/fr_FR.json
index ee40b7bc5cd4..6c20de6f8245 100644
--- a/packages/shared/src/locale/json/fr_FR.json
+++ b/packages/shared/src/locale/json/fr_FR.json
@@ -1059,6 +1059,7 @@
   "global.circulating_supply": "Offre en circulation",
   "global.clear": "Clair",
   "global.close": "Fermer",
+  "global.close_confirm_description": "Vous avez des modifications non enregistrées. Si vous fermez maintenant, ces informations seront perdues.",
   "global.collect_to_device": "Collecter sur l'appareil",
   "global.collect_to_device_canceled": "Annulé sur l'appareil",
   "global.collect_to_device_failed": "Échec de la collecte, veuillez réessayer",
diff --git a/packages/shared/src/locale/json/hi_IN.json b/packages/shared/src/locale/json/hi_IN.json
index 487e00be7b79..ba6e440be87f 100644
--- a/packages/shared/src/locale/json/hi_IN.json
+++ b/packages/shared/src/locale/json/hi_IN.json
@@ -1059,6 +1059,7 @@
   "global.circulating_supply": "संचारण सप्लाई",
   "global.clear": "स्पष्ट",
   "global.close": "बंद करें",
+  "global.close_confirm_description": "आपके पास बिना सहेजे गए परिवर्तन हैं। यदि आप अभी बंद करते हैं, तो यह जानकारी खो जाएगी।",
   "global.collect_to_device": "उपकरण में संग्रहित करें",
   "global.collect_to_device_canceled": "उपकरण पर रद्द किया गया",
   "global.collect_to_device_failed": "संग्रह करने में विफल, कृपया पुनः प्रयास करें",
diff --git a/packages/shared/src/locale/json/id.json b/packages/shared/src/locale/json/id.json
index e2420a4dd138..1fc4b8173165 100644
--- a/packages/shared/src/locale/json/id.json
+++ b/packages/shared/src/locale/json/id.json
@@ -1059,6 +1059,7 @@
   "global.circulating_supply": "Pasokan beredar",
   "global.clear": "Bersih",
   "global.close": "Tutup",
+  "global.close_confirm_description": "Anda memiliki perubahan yang belum disimpan. Jika Anda menutup sekarang, informasi ini akan hilang.",
   "global.collect_to_device": "Kumpulkan ke perangkat",
   "global.collect_to_device_canceled": "Dibatalkan di perangkat",
   "global.collect_to_device_failed": "Gagal mengumpulkan, silakan coba lagi",
diff --git a/packages/shared/src/locale/json/it_IT.json b/packages/shared/src/locale/json/it_IT.json
index 60603e0edc24..c9137723e524 100644
--- a/packages/shared/src/locale/json/it_IT.json
+++ b/packages/shared/src/locale/json/it_IT.json
@@ -1059,6 +1059,7 @@
   "global.circulating_supply": "Offerta circolante",
   "global.clear": "Chiaro",
   "global.close": "Chiudi",
+  "global.close_confirm_description": "Hai modifiche non salvate. Se chiudi ora, queste informazioni andranno perse.",
   "global.collect_to_device": "Raccogli sul dispositivo",
   "global.collect_to_device_canceled": "Annullato sul dispositivo",
   "global.collect_to_device_failed": "Impossibile raccogliere, si prega di riprovare",
diff --git a/packages/shared/src/locale/json/ja_JP.json b/packages/shared/src/locale/json/ja_JP.json
index f2ea0c0310a3..125b45f80f5f 100644
--- a/packages/shared/src/locale/json/ja_JP.json
+++ b/packages/shared/src/locale/json/ja_JP.json
@@ -1059,6 +1059,7 @@
   "global.circulating_supply": "流通供給量",
   "global.clear": "クリア",
   "global.close": "閉じる",
+  "global.close_confirm_description": "未保存の変更があります。今閉じると、この情報は失われます。",
   "global.collect_to_device": "デバイスに収集",
   "global.collect_to_device_canceled": "デバイスでキャンセルされました",
   "global.collect_to_device_failed": "収集に失敗しました、もう一度お試しください",
diff --git a/packages/shared/src/locale/json/ko_KR.json b/packages/shared/src/locale/json/ko_KR.json
index 09da0de09e76..73e15cc55cbc 100644
--- a/packages/shared/src/locale/json/ko_KR.json
+++ b/packages/shared/src/locale/json/ko_KR.json
@@ -1059,6 +1059,7 @@
   "global.circulating_supply": "순환 공급량",
   "global.clear": "지우기",
   "global.close": "닫기",
+  "global.close_confirm_description": "저장되지 않은 변경 사항이 있습니다. 지금 닫으면 이 정보가 손실됩니다.",
   "global.collect_to_device": "장치에 수집",
   "global.collect_to_device_canceled": "기기에서 취소됨",
   "global.collect_to_device_failed": "수집에 실패했습니다, 다시 시도해 주세요",
diff --git a/packages/shared/src/locale/json/pt.json b/packages/shared/src/locale/json/pt.json
index fcd840843432..4cbfac286762 100644
--- a/packages/shared/src/locale/json/pt.json
+++ b/packages/shared/src/locale/json/pt.json
@@ -1059,6 +1059,7 @@
   "global.circulating_supply": "Oferta circulante",
   "global.clear": "Claro",
   "global.close": "Fechar",
+  "global.close_confirm_description": "Você tem alterações não guardadas. Se fechar agora, estas informações serão perdidas.",
   "global.collect_to_device": "Coletar no dispositivo",
   "global.collect_to_device_canceled": "Cancelado no dispositivo",
   "global.collect_to_device_failed": "Falha ao coletar, por favor tente novamente",
diff --git a/packages/shared/src/locale/json/pt_BR.json b/packages/shared/src/locale/json/pt_BR.json
index facc2d2847a1..5e65697067e4 100644
--- a/packages/shared/src/locale/json/pt_BR.json
+++ b/packages/shared/src/locale/json/pt_BR.json
@@ -1059,6 +1059,7 @@
   "global.circulating_supply": "Oferta circulante",
   "global.clear": "Limpar",
   "global.close": "Fechar",
+  "global.close_confirm_description": "Você tem alterações não salvas. Se fechar agora, estas informações serão perdidas.",
   "global.collect_to_device": "Coletar no dispositivo",
   "global.collect_to_device_canceled": "Cancelado no dispositivo",
   "global.collect_to_device_failed": "Falha ao coletar, por favor tente novamente",
diff --git a/packages/shared/src/locale/json/ru.json b/packages/shared/src/locale/json/ru.json
index 806c8b680541..c0e958416279 100644
--- a/packages/shared/src/locale/json/ru.json
+++ b/packages/shared/src/locale/json/ru.json
@@ -1059,6 +1059,7 @@
   "global.circulating_supply": "Обращающийся запас",
   "global.clear": "Очистить",
   "global.close": "Закрыть",
+  "global.close_confirm_description": "У вас есть несохраненные изменения. Если вы закроете сейчас, эта информация будет потеряна.",
   "global.collect_to_device": "Собрать на устройство",
   "global.collect_to_device_canceled": "Отменено на устройстве",
   "global.collect_to_device_failed": "Не удалось собрать, пожалуйста, попробуйте еще раз",
diff --git a/packages/shared/src/locale/json/th_TH.json b/packages/shared/src/locale/json/th_TH.json
index 541b901ca7ea..958c7086f75c 100644
--- a/packages/shared/src/locale/json/th_TH.json
+++ b/packages/shared/src/locale/json/th_TH.json
@@ -1059,6 +1059,7 @@
   "global.circulating_supply": "จำนวนหุ้นที่หมุนเวียน",
   "global.clear": "ชัดเจน",
   "global.close": "ปิด",
+  "global.close_confirm_description": "คุณมีการเปลี่ยนแปลงที่ยังไม่ได้บันทึก หากคุณปิดตอนนี้ ข้อมูลนี้จะสูญหาย",
   "global.collect_to_device": "รวบรวมไปยังอุปกรณ์",
   "global.collect_to_device_canceled": "ยกเลิกบนอุปกรณ์",
   "global.collect_to_device_failed": "การเก็บรวบรวมล้มเหลว โปรดลองอีกครั้ง",
diff --git a/packages/shared/src/locale/json/uk_UA.json b/packages/shared/src/locale/json/uk_UA.json
index d91472184709..8a8c942ca5e1 100644
--- a/packages/shared/src/locale/json/uk_UA.json
+++ b/packages/shared/src/locale/json/uk_UA.json
@@ -1059,6 +1059,7 @@
   "global.circulating_supply": "Обіговий запас",
   "global.clear": "Очистити",
   "global.close": "Закрити",
+  "global.close_confirm_description": "У вас є незбережені зміни. Якщо ви закриєте зараз, ця інформація буде втрачена.",
   "global.collect_to_device": "Зберігати на пристрої",
   "global.collect_to_device_canceled": "Скасовано на пристрої",
   "global.collect_to_device_failed": "Не вдалося зібрати, будь ласка, спробуйте ще раз",
diff --git a/packages/shared/src/locale/json/vi.json b/packages/shared/src/locale/json/vi.json
index 9a888e1e1364..3b3907304a1a 100644
--- a/packages/shared/src/locale/json/vi.json
+++ b/packages/shared/src/locale/json/vi.json
@@ -1059,6 +1059,7 @@
   "global.circulating_supply": "Lượng cung ứng lưu hành",
   "global.clear": "Xóa",
   "global.close": "Đóng",
+  "global.close_confirm_description": "Bạn có các thay đổi chưa được lưu. Nếu bạn đóng bây giờ, thông tin này sẽ bị mất.",
   "global.collect_to_device": "Thu thập vào thiết bị",
   "global.collect_to_device_canceled": "Đã hủy trên thiết bị",
   "global.collect_to_device_failed": "Không thể thu thập, vui lòng thử lại",
diff --git a/packages/shared/src/locale/json/zh_CN.json b/packages/shared/src/locale/json/zh_CN.json
index 52f428c7e65a..57178db02092 100644
--- a/packages/shared/src/locale/json/zh_CN.json
+++ b/packages/shared/src/locale/json/zh_CN.json
@@ -1059,6 +1059,7 @@
   "global.circulating_supply": "流通供应量",
   "global.clear": "清除",
   "global.close": "关闭",
+  "global.close_confirm_description": "您有未保存的更改。如果现在关闭，这些信息将会丢失。",
   "global.collect_to_device": "收藏到设备",
   "global.collect_to_device_canceled": "已在设备上取消",
   "global.collect_to_device_failed": "收藏失败，请再试一次",
diff --git a/packages/shared/src/locale/json/zh_HK.json b/packages/shared/src/locale/json/zh_HK.json
index 2733cd01218c..31c5c2d9ea37 100644
--- a/packages/shared/src/locale/json/zh_HK.json
+++ b/packages/shared/src/locale/json/zh_HK.json
@@ -1059,6 +1059,7 @@
   "global.circulating_supply": "流通供應量",
   "global.clear": "清除",
   "global.close": "關閉",
+  "global.close_confirm_description": "你有未儲存嘅更改。如果而家閂咗佢，呢啲資料就會冇咗。",
   "global.collect_to_device": "收藏到設備",
   "global.collect_to_device_canceled": "已在裝置上取消",
   "global.collect_to_device_failed": "收藏失敗，請再試一次",
diff --git a/packages/shared/src/locale/json/zh_TW.json b/packages/shared/src/locale/json/zh_TW.json
index 83a9c0603ff8..313bc01b372d 100644
--- a/packages/shared/src/locale/json/zh_TW.json
+++ b/packages/shared/src/locale/json/zh_TW.json
@@ -1059,6 +1059,7 @@
   "global.circulating_supply": "流通供應量",
   "global.clear": "清除",
   "global.close": "關閉",
+  "global.close_confirm_description": "您有未儲存的變更。如果現在關閉，這些資訊將會遺失。",
   "global.collect_to_device": "收藏到裝置",
   "global.collect_to_device_canceled": "已在裝置上取消",
   "global.collect_to_device_failed": "收藏失敗，請再試一次",


Reviews:
user: login: coderabbitai[bot]
body: **Actionable comments posted: 2**

<details>
<summary>🔭 Outside diff range comments (1)</summary><blockquote>

<details>
<summary>packages/kit/src/provider/Bootstrap.tsx (1)</summary><blockquote>

`160-183`: _⚠️ Potential issue_

**Duplicate navigation triggers – `navigateAction` fires twice**

`closeAllModalRoutes()` already schedules `navigateAction`.  
Lines 220-223 schedule it again, so callers may navigate twice whenever at least one modal is closed. Users can land on the same tab twice and waste render cycles.

```diff
-          closeAllModalRoutes();
-
-          setTimeout(() => {
-            navigateAction?.();
-          }, 100);
+          // Let closeAllModalRoutes handle navigation timing.
+          if (allModalRoutes.length === 0) {
+            // No modals to close, navigate immediately.
+            navigateAction?.();
+            return;
+          }
+          closeAllModalRoutes();
```



Also applies to: 218-223

</blockquote></details>

</blockquote></details>

<details>
<summary>📜 Review details</summary>

**Configuration used: CodeRabbit UI**
**Review profile: ASSERTIVE**
**Plan: Pro**

<details>
<summary>📥 Commits</summary>

Reviewing files that changed from the base of the PR and between 77a90fe98acf64b01dfb31b3603b7cee2a6c93e8 and 20acef2aecadbb57a46dac76a6eda58f0eadea89.

</details>

<details>
<summary>⛔ Files ignored due to path filters (20)</summary>

* `packages/shared/src/locale/json/bn.json` is excluded by `!packages/shared/src/locale/json/*.json`
* `packages/shared/src/locale/json/de.json` is excluded by `!packages/shared/src/locale/json/*.json`
* `packages/shared/src/locale/json/en.json` is excluded by `!packages/shared/src/locale/json/*.json`
* `packages/shared/src/locale/json/en_US.json` is excluded by `!packages/shared/src/locale/json/*.json`
* `packages/shared/src/locale/json/es.json` is excluded by `!packages/shared/src/locale/json/*.json`
* `packages/shared/src/locale/json/fr_FR.json` is excluded by `!packages/shared/src/locale/json/*.json`
* `packages/shared/src/locale/json/hi_IN.json` is excluded by `!packages/shared/src/locale/json/*.json`
* `packages/shared/src/locale/json/id.json` is excluded by `!packages/shared/src/locale/json/*.json`
* `packages/shared/src/locale/json/it_IT.json` is excluded by `!packages/shared/src/locale/json/*.json`
* `packages/shared/src/locale/json/ja_JP.json` is excluded by `!packages/shared/src/locale/json/*.json`
* `packages/shared/src/locale/json/ko_KR.json` is excluded by `!packages/shared/src/locale/json/*.json`
* `packages/shared/src/locale/json/pt.json` is excluded by `!packages/shared/src/locale/json/*.json`
* `packages/shared/src/locale/json/pt_BR.json` is excluded by `!packages/shared/src/locale/json/*.json`
* `packages/shared/src/locale/json/ru.json` is excluded by `!packages/shared/src/locale/json/*.json`
* `packages/shared/src/locale/json/th_TH.json` is excluded by `!packages/shared/src/locale/json/*.json`
* `packages/shared/src/locale/json/uk_UA.json` is excluded by `!packages/shared/src/locale/json/*.json`
* `packages/shared/src/locale/json/vi.json` is excluded by `!packages/shared/src/locale/json/*.json`
* `packages/shared/src/locale/json/zh_CN.json` is excluded by `!packages/shared/src/locale/json/*.json`
* `packages/shared/src/locale/json/zh_HK.json` is excluded by `!packages/shared/src/locale/json/*.json`
* `packages/shared/src/locale/json/zh_TW.json` is excluded by `!packages/shared/src/locale/json/*.json`

</details>

<details>
<summary>📒 Files selected for processing (6)</summary>

* `packages/components/src/composite/Dialog/index.tsx` (5 hunks)
* `packages/components/src/composite/Dialog/jotai.ts` (1 hunks)
* `packages/components/src/forms/Form/index.tsx` (4 hunks)
* `packages/components/src/forms/Form/jotai.ts` (1 hunks)
* `packages/kit/src/provider/Bootstrap.tsx` (5 hunks)
* `packages/shared/src/locale/enum/translations.ts` (1 hunks)

</details>

<details>
<summary>🧰 Additional context used</summary>

<details>
<summary>🧬 Code Graph Analysis (3)</summary>

<details>
<summary>packages/components/src/forms/Form/index.tsx (2)</summary><blockquote>

<details>
<summary>packages/components/src/forms/Form/jotai.ts (1)</summary>

* `formInstancesAtom` (5-5)

</details>
<details>
<summary>apps/desktop/app/libs/store.ts (1)</summary>

* `instance` (19-19)

</details>

</blockquote></details>
<details>
<summary>packages/kit/src/provider/Bootstrap.tsx (3)</summary><blockquote>

<details>
<summary>packages/components/src/forms/Form/jotai.ts (1)</summary>

* `useFormInstances` (7-9)

</details>
<details>
<summary>packages/components/src/composite/Dialog/jotai.ts (1)</summary>

* `useDialogInstances` (7-9)

</details>
<details>
<summary>packages/components/src/composite/Dialog/index.tsx (1)</summary>

* `Dialog` (645-661)

</details>

</blockquote></details>
<details>
<summary>packages/components/src/composite/Dialog/index.tsx (2)</summary><blockquote>

<details>
<summary>packages/components/src/composite/Dialog/type.ts (1)</summary>

* `IDialogInstance` (144-148)

</details>
<details>
<summary>packages/components/src/composite/Dialog/jotai.ts (2)</summary>

* `removeDialogInstance` (16-19)
* `addDialogInstance` (11-14)

</details>

</blockquote></details>

</details><details>
<summary>🪛 GitHub Check: lint (20.x)</summary>

<details>
<summary>packages/components/src/forms/Form/index.tsx</summary>

[warning] 12-12: 
'isEqual' is defined but never used. Allowed unused vars must match /^_/u

</details>

</details>

</details>

<details>
<summary>🔇 Additional comments (9)</summary><blockquote>

<details>
<summary>packages/shared/src/locale/enum/translations.ts (1)</summary>

`1066-1068`: **Add translation key for close dialog confirmation**  
The new enum entry fits the pattern. Make sure you run `yarn fetch:locale` (or your locale script) to update all JSON files so this key shows up in every language pack.

</details>
<details>
<summary>packages/components/src/forms/Form/index.tsx (2)</summary>

`61-71`: **Good implementation of form instance tracking.**

Tracking form instances through Jotai atoms enables global state management. This helps with form lifecycle management across the application.

---

`291-291`: **Proper export of Jotai utilities.**

Re-exporting from './jotai' makes the atom and hook accessible to consumers of the Form component.

</details>
<details>
<summary>packages/components/src/forms/Form/jotai.ts (1)</summary>

`1-9`: **Well-structured Jotai atom setup.**

The implementation creates a clean state management pattern with:
1. An atom for storing form instances
2. A custom hook for easier consumption

This follows React best practices by encapsulating state logic in a custom hook.

</details>
<details>
<summary>packages/components/src/composite/Dialog/index.tsx (3)</summary>

`59-59`: **Good import of dialog instance management utilities.**

The import brings in functions for tracking dialog instances globally.

---

`83-83`: **Proper re-export of Jotai utilities.**

Re-exporting from './jotai' makes the dialog tracking functionality available to consumers.

---

`484-485`: **Robust dialog instance lifecycle management.**

The code properly:
1. Creates a dialog instance
2. Registers it with the global atom
3. Cleans it up when the dialog closes

This implementation ensures consistent tracking of active dialogs throughout the app.



Also applies to: 501-504, 550-556

</details>
<details>
<summary>packages/components/src/composite/Dialog/jotai.ts (1)</summary>

`1-19`: **Well-designed dialog instance state management.**

This implementation:
1. Creates an atom for dialog instances
2. Provides a hook for reactive access
3. Adds utility functions for imperative updates

Using `getDefaultStore()` allows these functions to work outside React components, which is perfect for this scenario.

</details>
<details>
<summary>packages/kit/src/provider/Bootstrap.tsx (1)</summary>

`188-191`: **Early-exit may lock the UI when any dialog is open**

The guard stops everything if `dialogInstances.length !== 0`.  
If a non-blocking dialog (e.g. toast-style) sits in the atom, keyboard shortcuts no longer work and users cannot switch tabs.

Consider:

1. Checking only for blocking dialogs, or  
2. Allowing `navigateAction` after closing those dialogs, similar to the form flow.

</details>

</blockquote></details>

</details>

<!-- This is an auto-generated comment by CodeRabbit for review status -->
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-05-13T01:14:11Z
user: login: huhuanming
state: DISMISSED
author_association: CONTRIBUTOR
submitted_at: 2025-05-13T07:43:54Z
user: login: sidmorizon
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-05-13T09:39:05Z
user: login: limichange
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-05-13T09:56:05Z
user: login: coderabbitai[bot]
body: **Actionable comments posted: 5**

<details>
<summary>♻️ Duplicate comments (1)</summary><blockquote>

<details>
<summary>packages/kit/src/provider/Bootstrap.tsx (1)</summary><blockquote>

`188-198`: _⚠️ Potential issue_

**Change detection still only looks at the last form**  

You already got this feedback earlier, but the issue remains.  
If an older form is dirty but the newest isn’t, navigation slips through.

```diff
-const formInstance = formInstances[formInstances.length - 1];
-const isFormChanged =
-  formInstance &&
-  !isEqual(
-    formInstance.formState.defaultValues,
-    formInstance.getValues(),
-  );
+const isFormChanged = formInstances.some(
+  (fi) => !isEqual(fi.formState.defaultValues, fi.getValues()),
+);
```

This iterates all tracked forms and blocks navigation if **any** are dirty.

</blockquote></details>

</blockquote></details>

<details>
<summary>📜 Review details</summary>

**Configuration used: CodeRabbit UI**
**Review profile: ASSERTIVE**
**Plan: Pro (Legacy)**

<details>
<summary>📥 Commits</summary>

Reviewing files that changed from the base of the PR and between 20acef2aecadbb57a46dac76a6eda58f0eadea89 and 1fe841c0d6bc87111f19adce95106fbd5bce2ab5.

</details>

<details>
<summary>📒 Files selected for processing (5)</summary>

* `packages/components/src/composite/Dialog/dialogInstances.ts` (1 hunks)
* `packages/components/src/composite/Dialog/index.tsx` (5 hunks)
* `packages/components/src/forms/Form/formInstances.ts` (1 hunks)
* `packages/components/src/forms/Form/index.tsx` (4 hunks)
* `packages/kit/src/provider/Bootstrap.tsx` (4 hunks)

</details>

<details>
<summary>🧰 Additional context used</summary>

<details>
<summary>🧬 Code Graph Analysis (2)</summary>

<details>
<summary>packages/components/src/forms/Form/index.tsx (1)</summary><blockquote>

<details>
<summary>packages/components/src/forms/Form/formInstances.ts (2)</summary>

* `addFormInstance` (9-13)
* `removeFormInstance` (15-20)

</details>

</blockquote></details>
<details>
<summary>packages/components/src/composite/Dialog/index.tsx (2)</summary><blockquote>

<details>
<summary>packages/components/src/composite/Dialog/type.ts (1)</summary>

* `IDialogInstance` (144-148)

</details>
<details>
<summary>packages/components/src/composite/Dialog/dialogInstances.ts (2)</summary>

* `removeDialogInstance` (15-20)
* `addDialogInstance` (9-13)

</details>

</blockquote></details>

</details>

</details>

<details>
<summary>⏰ Context from checks skipped due to timeout of 90000ms (3)</summary>

* GitHub Check: unittest (20.x)
* GitHub Check: lint (20.x)
* GitHub Check: Analyze (javascript-typescript)

</details>

<details>
<summary>🔇 Additional comments (7)</summary><blockquote>

<details>
<summary>packages/components/src/forms/Form/index.tsx (2)</summary>

`58-66`: **Potential duplicate registration when `form` prop identity changes**

The `useEffect` uses `[formContext]` as its dependency.  
If the parent recreates the form object on every render, the cleanup runs after the new instance is already added, briefly giving you two entries in the global list.

Usually `useForm()` returns a stable reference, but if that ever changes, you’ll get duplicates.

Guard against this by storing the previous ref in a `useRef` or by checking `isMounted` inside `addFormInstance`.

---

`285-286`: **Nice touch re-exporting the utilities**

Re-exporting `formInstances` helpers here lets consumers grab everything from the same barrel file. Keeps the public API neat. 👍

</details>
<details>
<summary>packages/components/src/composite/Dialog/index.tsx (5)</summary>

`48-48`: **Good import addition**

Clean import of the dialog instance management functions. These are needed for the new tracking features.

---

`83-83`: **Good API exposure**

Re-exporting from dialogInstances makes these functions available to consumers. Perfect for API consistency.

---

`484-485`: **Dialog instance variable added correctly**

Added a variable to track the current dialog instance. This enables proper lifecycle management.

---

`501-504`: **Good cleanup implementation**

The cleanup code properly deregisters the dialog instance when it's closed. This prevents memory leaks.

---

`550-556`: **Complete lifecycle management**

The dialog instance creation and registration logic is well-implemented. The instance includes all required methods (close, getForm, isExist) and is properly registered with addDialogInstance.

</details>

</blockquote></details>

</details>

<!-- This is an auto-generated comment by CodeRabbit for review status -->
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-05-13T10:26:48Z
user: login: originalix
state: APPROVED
author_association: COLLABORATOR
submitted_at: 2025-05-14T01:38:44Z
user: login: sidmorizon
state: APPROVED
author_association: CONTRIBUTOR
submitted_at: 2025-05-14T01:44:51Z

Review comments:
diff_hunk: @@ -4,9 +4,12 @@
   cloneElement,
   isValidElement,
   useCallback,
+  useEffect,
   useMemo,
 } from 'react';
 
+import { useAtom } from 'jotai';
+import isEqual from 'lodash/isEqual';
path: packages/components/src/forms/Form/index.tsx
user: login: coderabbitai[bot]
body: _🧹 Nitpick (assertive)_

**Fix unused import warning.**

The `isEqual` import from lodash is defined but never used in this file. Remove it to fix the linting warning.

```diff
-import isEqual from 'lodash/isEqual';
```


<!-- suggestion_start -->

<details>
<summary>📝 Committable suggestion</summary>

> ‼️ **IMPORTANT**
> Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

`````suggestion
import { useAtom } from 'jotai';
`````

</details>

<!-- suggestion_end -->

<details>
<summary>🧰 Tools</summary>

<details>
<summary>🪛 GitHub Check: lint (20.x)</summary>

[warning] 12-12: 
'isEqual' is defined but never used. Allowed unused vars must match /^_/u

</details>

</details>

<details>
<summary>🤖 Prompt for AI Agents (early access)</summary>

```
In packages/components/src/forms/Form/index.tsx around lines 11 to 12, the
import statement includes `isEqual` from lodash which is not used anywhere in
the file. Remove the `isEqual` import to resolve the unused import linting
warning.
```

</details>

<!-- This is an auto-generated comment by CodeRabbit -->
created_at: 2025-05-13T01:14:10Z
author_association: CONTRIBUTOR
diff_hunk: @@ -181,20 +185,26 @@ const useDesktopEvents = platformEnv.isDesktop
 
           const allModalRoutes = getAllModalRoutes();
 
-          if (!allModalRoutes) {
+          if (!allModalRoutes || dialogInstances.length !== 0) {
             return;
           }
 
-          if (allModalRoutes.length === 1 || allModalRoutes.length === 2) {
-            closeAllModalRoutes();
-            return;
-          }
+          const formInstance = formInstances[formInstances.length - 1];
+          const isFormChanged =
+            formInstance &&
+            !isEqual(
+              formInstance.formState.defaultValues,
+              formInstance.getValues(),
+            );
path: packages/kit/src/provider/Bootstrap.tsx
user: login: coderabbitai[bot]
body: _🧹 Nitpick (assertive)_

**Change detection only inspects the last form**

`formInstances[formInstances.length - 1]` ignores earlier dirty forms.  
If multiple forms are open, unsaved changes on any but the last slip through.

Iterate over the list:

```diff
-const formInstance = formInstances[formInstances.length - 1];
-const isFormChanged =
-  formInstance && !isEqual(formInstance.formState.defaultValues, formInstance.getValues());
+const isFormChanged = formInstances.some((fi) =>
+  !isEqual(fi.formState.defaultValues, fi.getValues()),
+);
```



<!-- suggestion_start -->

<details>
<summary>📝 Committable suggestion</summary>

> ‼️ **IMPORTANT**
> Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

`````suggestion
          const isFormChanged = formInstances.some((fi) =>
            !isEqual(fi.formState.defaultValues, fi.getValues()),
          );
`````

</details>

<!-- suggestion_end -->

<details>
<summary>🤖 Prompt for AI Agents (early access)</summary>

```
In packages/kit/src/provider/Bootstrap.tsx around lines 192 to 198, the current
change detection only checks the last form instance for unsaved changes,
ignoring any earlier forms. To fix this, iterate over all formInstances and
check if any form's current values differ from its defaultValues using isEqual.
Set isFormChanged to true if any form is dirty, ensuring changes in any open
form are detected.
```

</details>

<!-- This is an auto-generated comment by CodeRabbit -->
created_at: 2025-05-13T01:14:11Z
author_association: CONTRIBUTOR
diff_hunk: @@ -56,6 +56,7 @@ import {
   DialogTitle,
   SetDialogHeader,
 } from './Header';
+import { addDialogInstance, removeDialogInstance } from './jotai';
path: packages/components/src/composite/Dialog/index.tsx
user: login: sidmorizon
body: 为啥要用状态容器，这里没有涉及到 UI 层响应式更新，而是在函数里需要访问，全局变量就行了。 

状态容器会引起 UI 刷新
created_at: 2025-05-13T09:38:58Z
author_association: CONTRIBUTOR
diff_hunk: @@ -56,6 +56,7 @@ import {
   DialogTitle,
   SetDialogHeader,
 } from './Header';
+import { addDialogInstance, removeDialogInstance } from './jotai';
path: packages/components/src/composite/Dialog/index.tsx
user: login: limichange
body: 好的。
created_at: 2025-05-13T09:56:05Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,20 @@
+import type { UseFormReturn } from 'react-hook-form';
+
+const formInstances: UseFormReturn<any>[] = [];
+
+export function getFormInstances(): UseFormReturn<any>[] {
+  return formInstances;
+}
path: packages/components/src/forms/Form/formInstances.ts
user: login: coderabbitai[bot]
body: _⚠️ Potential issue_

**Return a defensive copy to avoid accidental mutation**

`getFormInstances()` exposes the live backing array.  
Any caller can mutate it directly (`push`, `splice`, etc.) and break your bookkeeping.  
Return a shallow clone or a read-only wrapper instead.

```diff
-export function getFormInstances(): UseFormReturn<any>[] {
-  return formInstances;
-}
+export function getFormInstances(): readonly UseFormReturn<any>[] {
+  return [...formInstances];
+}
```



<!-- suggestion_start -->

<details>
<summary>📝 Committable suggestion</summary>

> ‼️ **IMPORTANT**
> Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

`````suggestion
export function getFormInstances(): readonly UseFormReturn<any>[] {
  return [...formInstances];
}
`````

</details>

<!-- suggestion_end -->

<details>
<summary>🤖 Prompt for AI Agents (early access)</summary>

```
In packages/components/src/forms/Form/formInstances.ts around lines 5 to 7, the
function getFormInstances returns the live formInstances array, allowing
external code to mutate it directly. To fix this, modify getFormInstances to
return a shallow copy of the formInstances array instead of the original,
preventing accidental external mutations while preserving the original data
integrity.
```

</details>

<!-- This is an auto-generated comment by CodeRabbit -->
created_at: 2025-05-13T10:26:47Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,20 @@
+import type { UseFormReturn } from 'react-hook-form';
+
+const formInstances: UseFormReturn<any>[] = [];
+
+export function getFormInstances(): UseFormReturn<any>[] {
+  return formInstances;
+}
+
+export function addFormInstance(instance: UseFormReturn<any>): void {
+  if (!formInstances.includes(instance)) {
+    formInstances.push(instance);
+  }
+}
+
+export function removeFormInstance(instance: UseFormReturn<any>): void {
+  const index = formInstances.indexOf(instance);
+  if (index !== -1) {
+    formInstances.splice(index, 1);
+  }
+}
path: packages/components/src/forms/Form/formInstances.ts
user: login: coderabbitai[bot]
body: _🛠️ Refactor suggestion_

**Consider `WeakSet` for leak-free tracking**

Using a plain array keeps strong references forever.  
If a form component forgets to call `removeFormInstance`, the instance—and every object it closes over—stays in memory.

A `WeakSet<UseFormReturn<any>>` would drop entries automatically once the component unmounts, giving you a safety net with zero API changes for callers.

```diff
-const formInstances: UseFormReturn<any>[] = [];
+const formInstances = new WeakSet<UseFormReturn<any>>();
 ...
-if (!formInstances.includes(instance)) {
-  formInstances.push(instance);
-}
+formInstances.add(instance);
 ...
-const index = formInstances.indexOf(instance);
-if (index !== -1) {
-  formInstances.splice(index, 1);
-}
+formInstances.delete(instance);
```

<details>
<summary>🤖 Prompt for AI Agents (early access)</summary>

```
In packages/components/src/forms/Form/formInstances.ts around lines 9 to 20, the
current implementation uses an array to track form instances, which holds strong
references and can cause memory leaks if removeFormInstance is not called.
Replace the array with a WeakSet to store form instances, allowing automatic
garbage collection when instances are no longer referenced elsewhere. Update
addFormInstance to use WeakSet.add and removeFormInstance to use WeakSet.delete,
maintaining the same API without requiring changes from callers.
```

</details>

<!-- This is an auto-generated comment by CodeRabbit -->
created_at: 2025-05-13T10:26:47Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,20 @@
+import type { IDialogInstance } from './type';
+
+const dialogInstances: IDialogInstance[] = [];
+
+export function getDialogInstances(): IDialogInstance[] {
+  return dialogInstances;
+}
path: packages/components/src/composite/Dialog/dialogInstances.ts
user: login: coderabbitai[bot]
body: _🛠️ Refactor suggestion_

**Expose read-only data**

For the same reasons as with `formInstances`, return a cloned array (or `readonly` type) to stop external code from mutating your source of truth.

<details>
<summary>🤖 Prompt for AI Agents (early access)</summary>

```
In packages/components/src/composite/Dialog/dialogInstances.ts around lines 5 to
7, the function getDialogInstances returns the original dialogInstances array,
allowing external code to mutate it. To fix this, modify the function to return
a cloned copy of the array or a readonly version to prevent external mutation of
the source data.
```

</details>

<!-- This is an auto-generated comment by CodeRabbit -->
created_at: 2025-05-13T10:26:47Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,20 @@
+import type { IDialogInstance } from './type';
+
+const dialogInstances: IDialogInstance[] = [];
+
+export function getDialogInstances(): IDialogInstance[] {
+  return dialogInstances;
+}
+
+export function addDialogInstance(instance: IDialogInstance): void {
+  if (!dialogInstances.includes(instance)) {
+    dialogInstances.push(instance);
+  }
+}
+
+export function removeDialogInstance(instance: IDialogInstance): void {
+  const idx = dialogInstances.indexOf(instance);
+  if (idx !== -1) {
+    dialogInstances.splice(idx, 1);
+  }
+}
path: packages/components/src/composite/Dialog/dialogInstances.ts
user: login: coderabbitai[bot]
body: _🧹 Nitpick (assertive)_

**Use `WeakSet` to avoid dangling dialog references**

Dialogs are often short-lived. A `WeakSet<IDialogInstance>` frees memory automatically if a dialog is GC’d but `removeDialogInstance` wasn’t called (e.g., during an error boundary).

See the diff suggestion in the `formInstances` review for a drop-in replacement.

<details>
<summary>🤖 Prompt for AI Agents (early access)</summary>

```
In packages/components/src/composite/Dialog/dialogInstances.ts around lines 9 to
20, replace the current array-based storage of dialogInstances with a
WeakSet<IDialogInstance> to automatically handle memory cleanup of dialog
instances when they are garbage collected. Change dialogInstances to a WeakSet,
update addDialogInstance to use the WeakSet's add method, and
removeDialogInstance to use the delete method, eliminating the need for manual
index checks and splicing.
```

</details>

<!-- This is an auto-generated comment by CodeRabbit -->
created_at: 2025-05-13T10:26:47Z
author_association: CONTRIBUTOR
diff_hunk: @@ -181,20 +185,26 @@ const useDesktopEvents = platformEnv.isDesktop
 
           const allModalRoutes = getAllModalRoutes();
 
-          if (!allModalRoutes) {
+          if (!allModalRoutes || dialogInstances.length !== 0) {
             return;
           }
path: packages/kit/src/provider/Bootstrap.tsx
user: login: coderabbitai[bot]
body: _🧹 Nitpick (assertive)_

**Early-exit blocks confirmation when no dialogs are open**

`if (!allModalRoutes || dialogInstances.length !== 0) { return; }`  
When `allModalRoutes` is empty you exit, even if `isFormChanged` is true. That means unsaved changes on a **normal** page get ignored.

Consider moving the `dialogInstances` check below the dirty-form logic or expanding the confirmation scope beyond modals.

<details>
<summary>🤖 Prompt for AI Agents (early access)</summary>

```
In packages/kit/src/provider/Bootstrap.tsx around lines 188 to 190, the early
return condition prevents the confirmation dialog from showing when there are
unsaved changes on normal pages because it exits if allModalRoutes is empty. To
fix this, adjust the logic to first check if the form is dirty and prompt
confirmation regardless of modal presence, then check dialogInstances length to
decide on early exit. This ensures unsaved changes on non-modal pages trigger
confirmation properly.
```

</details>

<!-- This is an auto-generated comment by CodeRabbit -->
created_at: 2025-05-13T10:26:47Z
author_association: CONTRIBUTOR

PR comments:
user: login: codesandbox[bot]
created_at: 2025-05-13T01:03:19Z
body: #### Review or Edit in CodeSandbox

Open the branch in <a href="https://codesandbox.io/p/github/OneKeyHQ/app-monorepo/fix-dialog-form-2?mode=review&utm_source=gh_app">Web Editor</a> &bull; <a href="https://codesandbox.io/p/vscode?owner=OneKeyHQ&repo=app-monorepo&branch=fix-dialog-form-2&utm_source=gh_app">VS Code</a> &bull; <a href="https://codesandbox.io/p/vscode?owner=OneKeyHQ&repo=app-monorepo&branch=fix-dialog-form-2&insiders=true&utm_source=gh_app">Insiders</a><br>
Open <a href="https://codesandbox.io/p/devtool/preview/OneKeyHQ/app-monorepo/fix-dialog-form-2?task=preview&port=51423&redirect=true&utm_source=gh_app">Preview</a>

<!-- open-in-codesandbox:complete -->

author_association: NONE
user: login: coderabbitai[bot]
created_at: 2025-05-13T01:03:23Z
body: <!-- This is an auto-generated comment: summarize by coderabbit.ai -->
<!-- walkthrough_start -->

## Walkthrough

The updates add lifecycle tracking for dialog and form instances. New modules manage collections of these instances. Dialog and form components now register and deregister themselves on mount and unmount. Navigation logic checks for open dialogs and unsaved forms before closing modals or navigating away. A new translation key supports confirmation dialogs.

## Changes

| File(s)                                                                                      | Change Summary                                                                                                 |
|---------------------------------------------------------------------------------------------|---------------------------------------------------------------------------------------------------------------|
| `packages/components/src/composite/Dialog/index.tsx`<br>`.../dialogInstances.ts`             | Added dialog instance tracking. Dialogs register on creation and deregister on close. Exports dialog instance utilities. |
| `packages/components/src/forms/Form/index.tsx`<br>`.../formInstances.ts`                     | Added form instance tracking. Forms register on mount and deregister on unmount. Exports form instance utilities.         |
| `packages/kit/src/provider/Bootstrap.tsx`                                                    | Updated navigation logic to check for open dialogs and unsaved forms before closing modals or navigating.     |
| `packages/shared/src/locale/enum/translations.ts`                                            | Added a new translation key: `global_close_confirm_description`.                                              |

## Sequence Diagram(s)

```mermaid
sequenceDiagram
    participant User
    participant Dialog
    participant DialogInstances

    User->>Dialog: Open Dialog
    Dialog->>DialogInstances: addDialogInstance(instance)
    Note over DialogInstances: Instance registered

    User->>Dialog: Close Dialog
    Dialog->>DialogInstances: removeDialogInstance(instance)
    Note over DialogInstances: Instance deregistered
```

```mermaid
sequenceDiagram
    participant User
    participant Form
    participant FormInstances

    User->>Form: Mount Form
    Form->>FormInstances: addFormInstance(instance)
    Note over FormInstances: Instance registered

    User->>Form: Unmount Form
    Form->>FormInstances: removeFormInstance(instance)
    Note over FormInstances: Instance deregistered
```

```mermaid
sequenceDiagram
    participant User
    participant Bootstrap
    participant DialogInstances
    participant FormInstances

    User->>Bootstrap: Trigger Navigation
    Bootstrap->>DialogInstances: getDialogInstances()
    alt Dialogs Open
        Bootstrap-->>User: Block Navigation
    else No Dialogs
        Bootstrap->>FormInstances: getFormInstances()
        alt Unsaved Form Exists
            Bootstrap-->>User: Show Confirm Dialog
            User->>Bootstrap: Confirm
            Bootstrap-->>User: Close Modals and Navigate
        else No Unsaved Forms
            Bootstrap-->>User: Close Modals and Navigate
        end
    end
```

<!-- walkthrough_end -->

<!-- announcements_start -->

> [!TIP]
> <details>
> <summary>⚡️ Faster reviews with caching</summary>
> 
> - CodeRabbit now supports caching for code and dependencies, helping speed up reviews. This means quicker feedback, reduced wait times, and a smoother review experience overall. Cached data is encrypted and stored securely. This feature will be automatically enabled for all accounts on May 16th. To opt out, configure `Review - Disable Cache` at either the organization or repository level. If you prefer to disable all data retention across your organization, simply turn off the `Data Retention` setting under your Organization Settings.
> 
> Enjoy the performance boost—your workflow just got faster.
> 
> </details>

<!-- announcements_end -->
<!-- internal state start -->


<!-- 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= -->

<!-- internal state end -->
<!-- tips_start -->

---



<details>
<summary>🪧 Tips</summary>

### Chat

There are 3 ways to chat with [CodeRabbit](https://coderabbit.ai?utm_source=oss&utm_medium=github&utm_campaign=OneKeyHQ/app-monorepo&utm_content=7315):

- Review comments: Directly reply to a review comment made by CodeRabbit. Example:
  - `I pushed a fix in commit <commit_id>, please review it.`
  - `Explain this complex logic.`
  - `Open a follow-up GitHub issue for this discussion.`
- Files and specific lines of code (under the "Files changed" tab): Tag `@coderabbitai` in a new review comment at the desired location with your query. Examples:
  - `@coderabbitai explain this code block.`
  -	`@coderabbitai modularize this function.`
- PR comments: Tag `@coderabbitai` in a new PR comment to ask questions about the PR branch. For the best results, please provide a very specific query, as very limited context is provided in this mode. Examples:
  - `@coderabbitai gather interesting stats about this repository and render them as a table. Additionally, render a pie chart showing the language distribution in the codebase.`
  - `@coderabbitai read src/utils.ts and explain its main purpose.`
  - `@coderabbitai read the files in the src/scheduler package and generate a class diagram using mermaid and a README in the markdown format.`
  - `@coderabbitai help me debug CodeRabbit configuration file.`

### Support

Need help? Create a ticket on our [support page](https://www.coderabbit.ai/contact-us/support) for assistance with any issues or questions.

Note: Be mindful of the bot's finite context window. It's strongly recommended to break down tasks such as reading entire modules into smaller chunks. For a focused discussion, use review comments to chat about specific files and their changes, instead of using the PR comments.

### CodeRabbit Commands (Invoked using PR comments)

- `@coderabbitai pause` to pause the reviews on a PR.
- `@coderabbitai resume` to resume the paused reviews.
- `@coderabbitai review` to trigger an incremental review. This is useful when automatic reviews are disabled for the repository.
- `@coderabbitai full review` to do a full review from scratch and review all the files again.
- `@coderabbitai summary` to regenerate the summary of the PR.
- `@coderabbitai generate sequence diagram` to generate a sequence diagram of the changes in this PR.
- `@coderabbitai resolve` resolve all the CodeRabbit review comments.
- `@coderabbitai configuration` to show the current CodeRabbit configuration for the repository.
- `@coderabbitai help` to get help.

### Other keywords and placeholders

- Add `@coderabbitai ignore` anywhere in the PR description to prevent this PR from being reviewed.
- Add `@coderabbitai summary` or `Summary` to generate the high-level summary at a specific location in the PR description.
- Add `@coderabbitai` anywhere in the PR title to generate the title automatically.

### CodeRabbit Configuration File (`.coderabbit.yaml`)

- You can programmatically configure CodeRabbit by adding a `.coderabbit.yaml` file to the root of your repository.
- Please see the [configuration documentation](https://docs.coderabbit.ai/guides/configure-coderabbit) for more information.
- If your editor has YAML language server enabled, you can add the path at the top of this file to enable auto-completion and validation: `# yaml-language-server: $schema=https://coderabbit.ai/integrations/schema.v2.json`

### Documentation and Community

- Visit our [Documentation](https://docs.coderabbit.ai) for detailed information on how to use CodeRabbit.
- Join our [Discord Community](http://discord.gg/coderabbit) to get help, request features, and share feedback.
- Follow us on [X/Twitter](https://twitter.com/coderabbitai) for updates and announcements.

</details>

<!-- tips_end -->
author_association: CONTRIBUTOR