mirror of
https://github.com/Syngnat/GoNavi.git
synced 2026-08-21 16:34:21 +08:00
🐛 fix(update): 修复 Windows 在线更新多实例确认流程
- 安装前获取维护锁并枚举同目录进程,仅检测到其他实例时触发关闭确认 - 改用 Windows 原生系统模态对话框并先收起下载进度窗口,避免确认提示被遮挡 - 单实例直接进入安装,取消确认或退出检查中止后恢复下载完成状态 - 多语言确认文案展示实际实例数量,补齐前后端与发布工作流回归测试
This commit is contained in:
@@ -2583,12 +2583,20 @@ function App() {
|
||||
}
|
||||
}, [t]);
|
||||
|
||||
const handleApplicationQuitRequest = useCallback(async (confirmedAction?: ApplicationQuitConfirmedAction) => {
|
||||
const handleApplicationQuitRequest = useCallback(async (
|
||||
confirmedAction?: ApplicationQuitConfirmedAction,
|
||||
cancelledAction?: () => void,
|
||||
) => {
|
||||
if (applicationQuitHandlingRef.current) {
|
||||
return;
|
||||
}
|
||||
applicationQuitHandlingRef.current = true;
|
||||
|
||||
const cancelRequest = () => {
|
||||
resetApplicationQuitRequest();
|
||||
cancelledAction?.();
|
||||
};
|
||||
|
||||
const runConfirmedAction = async (): Promise<boolean> => {
|
||||
let accepted = false;
|
||||
try {
|
||||
@@ -2601,14 +2609,14 @@ function App() {
|
||||
accepted = true;
|
||||
}
|
||||
} catch (error) {
|
||||
resetApplicationQuitRequest();
|
||||
cancelRequest();
|
||||
message.error(t('app.quit.message.quit_failed', {
|
||||
detail: error instanceof Error ? error.message : String(error),
|
||||
}));
|
||||
return false;
|
||||
}
|
||||
if (!accepted) {
|
||||
resetApplicationQuitRequest();
|
||||
cancelRequest();
|
||||
}
|
||||
return accepted;
|
||||
};
|
||||
@@ -2622,7 +2630,7 @@ function App() {
|
||||
latestState.savedQueries,
|
||||
);
|
||||
} catch (error) {
|
||||
resetApplicationQuitRequest();
|
||||
cancelRequest();
|
||||
message.error(t('app.quit.unsaved_sql.inspect_failed', {
|
||||
detail: error instanceof Error ? error.message : String(error),
|
||||
}));
|
||||
@@ -2663,14 +2671,14 @@ function App() {
|
||||
</>
|
||||
),
|
||||
onCancel: () => {
|
||||
resetApplicationQuitRequest();
|
||||
cancelRequest();
|
||||
},
|
||||
onOk: async () => {
|
||||
try {
|
||||
await saveApplicationQuitUnsavedSQLTargets(targets, saveQuery);
|
||||
message.success(t('app.quit.unsaved_sql.saved'));
|
||||
} catch (error) {
|
||||
resetApplicationQuitRequest();
|
||||
cancelRequest();
|
||||
message.error(t('app.quit.unsaved_sql.save_failed_cancel_exit', {
|
||||
detail: error instanceof Error ? error.message : String(error),
|
||||
}));
|
||||
@@ -2684,24 +2692,12 @@ function App() {
|
||||
}, [applicationQuitModalZIndex, ensureSavedQueriesLoaded, forceQuitApplication, resetApplicationQuitRequest, saveQuery, t]);
|
||||
|
||||
const handleInstallUpdateRequest = useCallback(async () => {
|
||||
if (installMode === 'portable' || installMode === 'msi') {
|
||||
Modal.confirm({
|
||||
title: t('app.about.update_install_confirm.close_instances_title'),
|
||||
content: t('app.about.update_install_confirm.close_instances_content'),
|
||||
okText: t('app.about.update_install_confirm.close_instances_ok'),
|
||||
cancelText: t('common.cancel'),
|
||||
closable: true,
|
||||
maskClosable: false,
|
||||
zIndex: applicationQuitModalZIndex,
|
||||
okButtonProps: { danger: true, type: 'primary' },
|
||||
onOk: async () => {
|
||||
await handleApplicationQuitRequest(() => handleInstallFromProgress(true));
|
||||
},
|
||||
});
|
||||
return;
|
||||
}
|
||||
await handleApplicationQuitRequest(() => handleInstallFromProgress(false));
|
||||
}, [applicationQuitModalZIndex, handleApplicationQuitRequest, handleInstallFromProgress, installMode, t]);
|
||||
hideUpdateDownloadProgress();
|
||||
await handleApplicationQuitRequest(
|
||||
() => handleInstallFromProgress(false),
|
||||
showUpdateDownloadProgress,
|
||||
);
|
||||
}, [handleApplicationQuitRequest, handleInstallFromProgress, hideUpdateDownloadProgress, showUpdateDownloadProgress]);
|
||||
|
||||
useEffect(() => {
|
||||
const offBeforeClose = EventsOn('app:before-close-request', () => {
|
||||
|
||||
@@ -10,15 +10,18 @@ const appSource = readFileSync(
|
||||
describe('restart-to-update unsaved SQL guard', () => {
|
||||
it('runs the confirmed update action through every application quit path', () => {
|
||||
expect(appSource).toContain('type ApplicationQuitConfirmedAction = () => Promise<boolean>;');
|
||||
expect(appSource).toContain('const handleApplicationQuitRequest = useCallback(async (confirmedAction?: ApplicationQuitConfirmedAction) => {');
|
||||
expect(appSource).toContain('const handleApplicationQuitRequest = useCallback(async (');
|
||||
expect(appSource).toContain('confirmedAction?: ApplicationQuitConfirmedAction,');
|
||||
expect(appSource).toContain('cancelledAction?: () => void,');
|
||||
expect(appSource).toContain('const runConfirmedAction = async (): Promise<boolean> => {');
|
||||
const runnerStart = appSource.indexOf('const runConfirmedAction = async (): Promise<boolean> => {');
|
||||
const runnerEnd = appSource.indexOf('\n let targets;', runnerStart);
|
||||
const runnerSource = appSource.slice(runnerStart, runnerEnd);
|
||||
expect(runnerSource).toContain('accepted = await confirmedAction();');
|
||||
expect(runnerSource).toContain('await forceQuitApplication();\n accepted = true;');
|
||||
expect(runnerSource).toContain('} catch (error) {\n resetApplicationQuitRequest();');
|
||||
expect(runnerSource).toContain('if (!accepted) {\n resetApplicationQuitRequest();');
|
||||
expect(runnerSource).toContain('} catch (error) {\n cancelRequest();');
|
||||
expect(runnerSource).toContain('if (!accepted) {\n cancelRequest();');
|
||||
expect(appSource).toContain('resetApplicationQuitRequest();\n cancelledAction?.();');
|
||||
expect(runnerSource).toContain('return accepted;');
|
||||
expect(appSource).toContain('if (targets.length === 0) {\n await runConfirmedAction();');
|
||||
expect(appSource).toContain('void runConfirmedAction();');
|
||||
@@ -57,15 +60,20 @@ describe('restart-to-update unsaved SQL guard', () => {
|
||||
expect(loaderSource).not.toContain('await reloadSavedQueryGroups();');
|
||||
});
|
||||
|
||||
it('confirms closing every Windows instance before entering the unsaved SQL guard', () => {
|
||||
it('lets the backend confirm only actually running Windows instances after the unsaved SQL guard', () => {
|
||||
expect(appSource).toContain('const handleInstallUpdateRequest = useCallback(async () => {');
|
||||
expect(appSource).toContain("if (installMode === 'portable' || installMode === 'msi') {");
|
||||
expect(appSource).toContain("title: t('app.about.update_install_confirm.close_instances_title')");
|
||||
expect(appSource).toContain("content: t('app.about.update_install_confirm.close_instances_content')");
|
||||
expect(appSource).toContain("okText: t('app.about.update_install_confirm.close_instances_ok')");
|
||||
expect(appSource).toContain("cancelText: t('common.cancel')");
|
||||
expect(appSource).toContain('await handleApplicationQuitRequest(() => handleInstallFromProgress(true));');
|
||||
expect(appSource).toContain('await handleApplicationQuitRequest(() => handleInstallFromProgress(false));');
|
||||
const installRequestStart = appSource.indexOf('const handleInstallUpdateRequest = useCallback(async () => {');
|
||||
const installRequestEnd = appSource.indexOf('\n\n useEffect(() => {', installRequestStart);
|
||||
const installRequestSource = appSource.slice(installRequestStart, installRequestEnd);
|
||||
expect(installRequestSource.indexOf('hideUpdateDownloadProgress();')).toBeGreaterThan(-1);
|
||||
expect(installRequestSource.indexOf('await handleApplicationQuitRequest(')).toBeGreaterThan(-1);
|
||||
expect(installRequestSource.indexOf('hideUpdateDownloadProgress();')).toBeLessThan(
|
||||
installRequestSource.indexOf('await handleApplicationQuitRequest('),
|
||||
);
|
||||
expect(installRequestSource).toContain('() => handleInstallFromProgress(false),');
|
||||
expect(installRequestSource).toContain('showUpdateDownloadProgress,');
|
||||
expect(appSource).not.toContain("title: t('app.about.update_install_confirm.close_instances_title')");
|
||||
expect(appSource).not.toContain('handleInstallFromProgress(true)');
|
||||
expect(appSource.match(/void handleInstallUpdateRequest\(\);/g)).toHaveLength(2);
|
||||
expect(appSource).not.toContain('onClick={handleInstallFromProgress}');
|
||||
expect(appSource).toContain("updateInstallAction === 'install-and-restart'");
|
||||
@@ -73,7 +81,7 @@ describe('restart-to-update unsaved SQL guard', () => {
|
||||
expect(appSource.match(/\{updateInstallActionLabel\}/g)).toHaveLength(2);
|
||||
});
|
||||
|
||||
it('keeps every update quit confirmation above active settings and update dialogs', () => {
|
||||
it('keeps the unsaved SQL quit confirmation above active settings and update dialogs', () => {
|
||||
const unsavedConfirmStart = appSource.indexOf('const confirmRef = Modal.confirm({');
|
||||
const installRequestStart = appSource.indexOf('const handleInstallUpdateRequest = useCallback', unsavedConfirmStart);
|
||||
const installRequestEnd = appSource.indexOf('\n\n useEffect(() => {', installRequestStart);
|
||||
@@ -87,6 +95,6 @@ describe('restart-to-update unsaved SQL guard', () => {
|
||||
expect(appSource).toContain('const applicationQuitModalZIndex = Math.max(');
|
||||
expect(appSource).toContain('settingsChildModalZIndex + 100,');
|
||||
expect(unsavedConfirmSource).toContain('zIndex: applicationQuitModalZIndex');
|
||||
expect(installRequestSource).toContain('zIndex: applicationQuitModalZIndex');
|
||||
expect(installRequestSource).not.toContain('Modal.confirm({');
|
||||
});
|
||||
});
|
||||
|
||||
@@ -568,6 +568,39 @@ describe('useAppUpdateManager', () => {
|
||||
);
|
||||
});
|
||||
|
||||
it('restores the ready state without an error toast when Windows instance confirmation is cancelled', async () => {
|
||||
backendApp.CheckForUpdates.mockResolvedValue({
|
||||
success: true,
|
||||
data: {
|
||||
hasUpdate: true,
|
||||
currentVersion: '0.8.1',
|
||||
latestVersion: '0.8.2',
|
||||
downloaded: true,
|
||||
assetSize: 2048,
|
||||
installMode: 'portable',
|
||||
packageType: 'portable',
|
||||
},
|
||||
});
|
||||
backendApp.InstallUpdateAndRestart.mockResolvedValue({
|
||||
success: false,
|
||||
data: { cancelled: true },
|
||||
});
|
||||
|
||||
renderHook();
|
||||
await act(async () => {
|
||||
await hook?.checkForUpdates(false);
|
||||
});
|
||||
|
||||
let accepted = true;
|
||||
await act(async () => {
|
||||
accepted = await hook!.handleInstallFromProgress();
|
||||
});
|
||||
|
||||
expect(accepted).toBe(false);
|
||||
expect(hook?.updateDownloadProgress.status).toBe('done');
|
||||
expect(messageApi.error).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('returns false without calling the backend when no update is ready', async () => {
|
||||
renderHook();
|
||||
|
||||
|
||||
@@ -414,6 +414,16 @@ export const useAppUpdateManager = ({
|
||||
res = { success: false, message: error?.message || t('common.unknown') };
|
||||
}
|
||||
if (!res?.success) {
|
||||
if (res?.data?.cancelled === true) {
|
||||
setUpdateDownloadProgress((prev) => ({
|
||||
...prev,
|
||||
open: true,
|
||||
status: 'done',
|
||||
percent: 100,
|
||||
message: '',
|
||||
}));
|
||||
return false;
|
||||
}
|
||||
setUpdateDownloadProgress((prev) => ({
|
||||
...prev,
|
||||
open: true,
|
||||
|
||||
Reference in New Issue
Block a user