[fix] fix Linux Alt+F4 exit handling (#2576)
This commit is contained in:
1 parent
3160c02b4e
commit
77cd835cda
4 files changed
+39
-31
No files matched your search
@@ -144,10 +144,11 @@ test("external SQL file paths persist with open query tabs", async () => {
|
|||||||
const tabId = store.createTab("conn-1", "db", "draft.sql");
|
const tabId = store.createTab("conn-1", "db", "draft.sql");
|
||||||
store.updateSql(tabId, "select 1;");
|
store.updateSql(tabId, "select 1;");
|
||||||
store.linkExternalSqlPath(tabId, "/tmp/draft.sql", "draft.sql");
|
store.linkExternalSqlPath(tabId, "/tmp/draft.sql", "draft.sql");
|
||||||
store.flushPendingPersist();
|
await store.flushPendingPersist();
|
||||||
|
|
||||||
setActivePinia(createPinia());
|
setActivePinia(createPinia());
|
||||||
store = useQueryStore();
|
store = useQueryStore();
|
||||||
|
await store.initOpenTabs();
|
||||||
const tab = store.tabs.find((item) => item.id === tabId);
|
const tab = store.tabs.find((item) => item.id === tabId);
|
||||||
|
|
||||||
assert.equal(tab?.externalSqlPath, "/tmp/draft.sql");
|
assert.equal(tab?.externalSqlPath, "/tmp/draft.sql");
|
||||||
@@ -177,13 +178,14 @@ test("clean saved SQL tabs persist without duplicating SQL text", async () => {
|
|||||||
createdAt: "2026-06-27T00:00:00.000Z",
|
createdAt: "2026-06-27T00:00:00.000Z",
|
||||||
updatedAt: "2026-06-27T00:00:00.000Z",
|
updatedAt: "2026-06-27T00:00:00.000Z",
|
||||||
});
|
});
|
||||||
store.flushPendingPersist();
|
await store.flushPendingPersist();
|
||||||
|
|
||||||
const rawTabs = localStorage.getItem("dbx-open-tabs") ?? "";
|
const rawTabs = localStorage.getItem("dbx-app-state:open_tabs") ?? "";
|
||||||
assert.equal(rawTabs.includes("large_table"), false);
|
assert.equal(rawTabs.includes("large_table"), false);
|
||||||
|
|
||||||
setActivePinia(createPinia());
|
setActivePinia(createPinia());
|
||||||
store = useQueryStore();
|
store = useQueryStore();
|
||||||
|
await store.initOpenTabs();
|
||||||
const tab = store.tabs.find((item) => item.savedSqlId === "saved-1");
|
const tab = store.tabs.find((item) => item.savedSqlId === "saved-1");
|
||||||
|
|
||||||
assert.equal(tab?.sql, "");
|
assert.equal(tab?.sql, "");
|
||||||
@@ -209,13 +211,14 @@ test("dirty saved SQL tabs keep unsaved edits in open tab persistence", async ()
|
|||||||
updatedAt: "2026-06-27T00:00:00.000Z",
|
updatedAt: "2026-06-27T00:00:00.000Z",
|
||||||
});
|
});
|
||||||
store.updateSql(tabId, "SELECT 2;");
|
store.updateSql(tabId, "SELECT 2;");
|
||||||
store.flushPendingPersist();
|
await store.flushPendingPersist();
|
||||||
|
|
||||||
const rawTabs = localStorage.getItem("dbx-open-tabs") ?? "";
|
const rawTabs = localStorage.getItem("dbx-app-state:open_tabs") ?? "";
|
||||||
assert.equal(rawTabs.includes("SELECT 2;"), true);
|
assert.equal(rawTabs.includes("SELECT 2;"), true);
|
||||||
|
|
||||||
setActivePinia(createPinia());
|
setActivePinia(createPinia());
|
||||||
store = useQueryStore();
|
store = useQueryStore();
|
||||||
|
await store.initOpenTabs();
|
||||||
const tab = store.tabs.find((item) => item.savedSqlId === "saved-1");
|
const tab = store.tabs.find((item) => item.savedSqlId === "saved-1");
|
||||||
|
|
||||||
assert.equal(tab?.sql, "SELECT 2;");
|
assert.equal(tab?.sql, "SELECT 2;");
|
||||||
@@ -452,7 +455,7 @@ test("close other fixed tabs does not close regular tabs", () => {
|
|||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
test("close other tabs pauses on restored unsaved query tabs", () => {
|
test("close other tabs pauses on restored unsaved query tabs", async () => {
|
||||||
const restoreStorage = installMemoryStorage();
|
const restoreStorage = installMemoryStorage();
|
||||||
try {
|
try {
|
||||||
localStorage.setItem(
|
localStorage.setItem(
|
||||||
@@ -479,6 +482,7 @@ test("close other tabs pauses on restored unsaved query tabs", () => {
|
|||||||
localStorage.setItem("dbx-active-tab", "b");
|
localStorage.setItem("dbx-active-tab", "b");
|
||||||
setActivePinia(createPinia());
|
setActivePinia(createPinia());
|
||||||
const store = useQueryStore();
|
const store = useQueryStore();
|
||||||
|
await store.initOpenTabs();
|
||||||
|
|
||||||
store.closeOtherTabs("b");
|
store.closeOtherTabs("b");
|
||||||
|
|
||||||
|
|||||||
@@ -9,7 +9,7 @@ import { AI_PROVIDER_PRESETS, DEFAULT_EDITOR_SETTINGS, normalizeAiConfig, normal
|
|||||||
|
|
||||||
const OLD_FONT_SIZE_KEY = "dbx-query-editor-font-size";
|
const OLD_FONT_SIZE_KEY = "dbx-query-editor-font-size";
|
||||||
|
|
||||||
function withMockLocalStorage(initial: Record<string, string>, run: () => void) {
|
async function withMockLocalStorage(initial: Record<string, string>, run: () => void | Promise<void>) {
|
||||||
const previousDescriptor = Object.getOwnPropertyDescriptor(globalThis, "localStorage");
|
const previousDescriptor = Object.getOwnPropertyDescriptor(globalThis, "localStorage");
|
||||||
const values = new Map(Object.entries(initial));
|
const values = new Map(Object.entries(initial));
|
||||||
const localStorageMock = {
|
const localStorageMock = {
|
||||||
@@ -31,7 +31,7 @@ function withMockLocalStorage(initial: Record<string, string>, run: () => void)
|
|||||||
});
|
});
|
||||||
|
|
||||||
try {
|
try {
|
||||||
run();
|
await run();
|
||||||
} finally {
|
} finally {
|
||||||
if (previousDescriptor) {
|
if (previousDescriptor) {
|
||||||
Object.defineProperty(globalThis, "localStorage", previousDescriptor);
|
Object.defineProperty(globalThis, "localStorage", previousDescriptor);
|
||||||
@@ -60,26 +60,28 @@ test("defaults export batch size to 2000 rows", () => {
|
|||||||
assert.equal(normalizeEditorSettings({ exportBatchSize: 2000 }).exportBatchSize, 2000);
|
assert.equal(normalizeEditorSettings({ exportBatchSize: 2000 }).exportBatchSize, 2000);
|
||||||
});
|
});
|
||||||
|
|
||||||
test("migrates the legacy saved export batch default to 2000 once", () => {
|
test("migrates the legacy saved export batch default to 2000 once", async () => {
|
||||||
withMockLocalStorage({ "dbx-editor-settings": JSON.stringify({ exportBatchSize: 10000 }) }, () => {
|
await withMockLocalStorage({ "dbx-editor-settings": JSON.stringify({ exportBatchSize: 10000 }) }, async () => {
|
||||||
setActivePinia(createPinia());
|
setActivePinia(createPinia());
|
||||||
const store = useSettingsStore();
|
const store = useSettingsStore();
|
||||||
|
await store.initEditorSettings();
|
||||||
|
|
||||||
assert.equal(store.editorSettings.exportBatchSize, 2000);
|
assert.equal(store.editorSettings.exportBatchSize, 2000);
|
||||||
assert.equal(localStorage.getItem("dbx-export-batch-size-default-migrated-v1"), "1");
|
assert.equal(localStorage.getItem("dbx-editor-settings"), null);
|
||||||
assert.equal(JSON.parse(localStorage.getItem("dbx-editor-settings") || "{}").exportBatchSize, 2000);
|
assert.equal(JSON.parse(localStorage.getItem("dbx-app-state:editor_settings") || "{}").exportBatchSize, 2000);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
test("keeps a manually saved 10000 export batch size after migration", () => {
|
test("keeps a manually saved 10000 export batch size after migration", async () => {
|
||||||
withMockLocalStorage(
|
await withMockLocalStorage(
|
||||||
{
|
{
|
||||||
"dbx-editor-settings": JSON.stringify({ exportBatchSize: 10000 }),
|
"dbx-editor-settings": JSON.stringify({ exportBatchSize: 10000 }),
|
||||||
"dbx-export-batch-size-default-migrated-v1": "1",
|
"dbx-export-batch-size-default-migrated-v1": "1",
|
||||||
},
|
},
|
||||||
() => {
|
async () => {
|
||||||
setActivePinia(createPinia());
|
setActivePinia(createPinia());
|
||||||
const store = useSettingsStore();
|
const store = useSettingsStore();
|
||||||
|
await store.initEditorSettings();
|
||||||
|
|
||||||
assert.equal(store.editorSettings.exportBatchSize, 10000);
|
assert.equal(store.editorSettings.exportBatchSize, 10000);
|
||||||
},
|
},
|
||||||
@@ -441,8 +443,8 @@ test("keeps SQL formatter default objects distinct", () => {
|
|||||||
assert.notEqual(normalized.sqlFormatter, DEFAULT_SQL_FORMATTER_SETTINGS);
|
assert.notEqual(normalized.sqlFormatter, DEFAULT_SQL_FORMATTER_SETTINGS);
|
||||||
});
|
});
|
||||||
|
|
||||||
test("does not leak default-loaded SQL formatter mutations into defaults", () => {
|
test("does not leak default-loaded SQL formatter mutations into defaults", async () => {
|
||||||
withMockLocalStorage({}, () => {
|
await withMockLocalStorage({}, async () => {
|
||||||
setActivePinia(createPinia());
|
setActivePinia(createPinia());
|
||||||
const store = useSettingsStore();
|
const store = useSettingsStore();
|
||||||
const editorDefaultKeywordCase = DEFAULT_EDITOR_SETTINGS.sqlFormatter.keywordCase;
|
const editorDefaultKeywordCase = DEFAULT_EDITOR_SETTINGS.sqlFormatter.keywordCase;
|
||||||
@@ -462,10 +464,11 @@ test("does not leak default-loaded SQL formatter mutations into defaults", () =>
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
test("does not leak migrated SQL formatter mutations into defaults", () => {
|
test("does not leak migrated SQL formatter mutations into defaults", async () => {
|
||||||
withMockLocalStorage({ [OLD_FONT_SIZE_KEY]: "18" }, () => {
|
await withMockLocalStorage({ [OLD_FONT_SIZE_KEY]: "18" }, async () => {
|
||||||
setActivePinia(createPinia());
|
setActivePinia(createPinia());
|
||||||
const store = useSettingsStore();
|
const store = useSettingsStore();
|
||||||
|
await store.initEditorSettings();
|
||||||
const editorDefaultKeywordCase = DEFAULT_EDITOR_SETTINGS.sqlFormatter.keywordCase;
|
const editorDefaultKeywordCase = DEFAULT_EDITOR_SETTINGS.sqlFormatter.keywordCase;
|
||||||
const formatterDefaultKeywordCase = DEFAULT_SQL_FORMATTER_SETTINGS.keywordCase;
|
const formatterDefaultKeywordCase = DEFAULT_SQL_FORMATTER_SETTINGS.keywordCase;
|
||||||
|
|
||||||
|
|||||||
@@ -9,6 +9,7 @@ const apiMock = vi.hoisted(() => ({
|
|||||||
cancelQueryResultExport: vi.fn(),
|
cancelQueryResultExport: vi.fn(),
|
||||||
startTableExport: vi.fn(),
|
startTableExport: vi.fn(),
|
||||||
cancelTableExport: vi.fn(),
|
cancelTableExport: vi.fn(),
|
||||||
|
saveEditorSettings: vi.fn(async () => {}),
|
||||||
exportQueryResultCsv: vi.fn(),
|
exportQueryResultCsv: vi.fn(),
|
||||||
exportQueryResultXlsx: vi.fn(),
|
exportQueryResultXlsx: vi.fn(),
|
||||||
exportQueryResultJson: vi.fn(),
|
exportQueryResultJson: vi.fn(),
|
||||||
|
|||||||
+12
-12
@@ -12,10 +12,12 @@ use std::sync::atomic::{AtomicBool, Ordering};
|
|||||||
use std::sync::Arc;
|
use std::sync::Arc;
|
||||||
use std::time::Instant;
|
use std::time::Instant;
|
||||||
#[cfg(target_os = "macos")]
|
#[cfg(target_os = "macos")]
|
||||||
|
use tauri::menu::Menu;
|
||||||
|
#[cfg(target_os = "macos")]
|
||||||
use tauri::menu::{AboutMetadata, MenuItem, PredefinedMenuItem, Submenu};
|
use tauri::menu::{AboutMetadata, MenuItem, PredefinedMenuItem, Submenu};
|
||||||
use tauri::RunEvent;
|
use tauri::RunEvent;
|
||||||
use tauri::{
|
use tauri::{
|
||||||
menu::{Menu, MenuBuilder},
|
menu::MenuBuilder,
|
||||||
tray::{MouseButton, MouseButtonState, TrayIconBuilder, TrayIconEvent},
|
tray::{MouseButton, MouseButtonState, TrayIconBuilder, TrayIconEvent},
|
||||||
};
|
};
|
||||||
use tauri::{Emitter, Manager};
|
use tauri::{Emitter, Manager};
|
||||||
@@ -24,6 +26,7 @@ use tauri_plugin_deep_link::DeepLinkExt;
|
|||||||
|
|
||||||
const DESKTOP_TRAY_ID: &str = "main-tray";
|
const DESKTOP_TRAY_ID: &str = "main-tray";
|
||||||
const APP_CLOSE_REQUESTED_EVENT: &str = "dbx-app-close-requested";
|
const APP_CLOSE_REQUESTED_EVENT: &str = "dbx-app-close-requested";
|
||||||
|
#[cfg(target_os = "macos")]
|
||||||
const APP_MENU_QUIT_ID: &str = "app-menu-quit";
|
const APP_MENU_QUIT_ID: &str = "app-menu-quit";
|
||||||
|
|
||||||
pub struct CloseBehaviorState {
|
pub struct CloseBehaviorState {
|
||||||
@@ -39,10 +42,6 @@ impl CloseBehaviorState {
|
|||||||
self.confirmed_exit.store(true, Ordering::Relaxed);
|
self.confirmed_exit.store(true, Ordering::Relaxed);
|
||||||
}
|
}
|
||||||
|
|
||||||
pub(crate) fn is_exit_confirmed(&self) -> bool {
|
|
||||||
self.confirmed_exit.load(Ordering::Relaxed)
|
|
||||||
}
|
|
||||||
|
|
||||||
fn take_confirmed_exit(&self) -> bool {
|
fn take_confirmed_exit(&self) -> bool {
|
||||||
self.confirmed_exit.swap(false, Ordering::Relaxed)
|
self.confirmed_exit.swap(false, Ordering::Relaxed)
|
||||||
}
|
}
|
||||||
@@ -67,8 +66,8 @@ fn should_show_main_window_after_setup() -> bool {
|
|||||||
true
|
true
|
||||||
}
|
}
|
||||||
|
|
||||||
fn should_confirm_app_exit_request(exit_code: Option<i32>, confirmed_exit: bool) -> bool {
|
fn should_confirm_app_exit_request(target_os: &str, exit_code: Option<i32>, confirmed_exit: bool) -> bool {
|
||||||
exit_code != Some(tauri::RESTART_EXIT_CODE) && !confirmed_exit
|
should_hide_window_on_close(target_os) && exit_code != Some(tauri::RESTART_EXIT_CODE) && !confirmed_exit
|
||||||
}
|
}
|
||||||
|
|
||||||
fn native_window_decorations_override(target_os: &str) -> Option<bool> {
|
fn native_window_decorations_override(target_os: &str) -> Option<bool> {
|
||||||
@@ -449,10 +448,11 @@ mod tests {
|
|||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn only_user_requested_app_exit_needs_frontend_confirmation() {
|
fn only_user_requested_app_exit_needs_frontend_confirmation() {
|
||||||
assert!(should_confirm_app_exit_request(None, false));
|
assert!(should_confirm_app_exit_request("windows", None, false));
|
||||||
assert!(should_confirm_app_exit_request(Some(0), false));
|
assert!(should_confirm_app_exit_request("macos", Some(0), false));
|
||||||
assert!(!should_confirm_app_exit_request(Some(0), true));
|
assert!(!should_confirm_app_exit_request("windows", Some(0), true));
|
||||||
assert!(!should_confirm_app_exit_request(Some(tauri::RESTART_EXIT_CODE), false));
|
assert!(!should_confirm_app_exit_request("windows", Some(tauri::RESTART_EXIT_CODE), false));
|
||||||
|
assert!(!should_confirm_app_exit_request("linux", Some(0), false));
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
@@ -1114,7 +1114,7 @@ pub fn run() {
|
|||||||
.try_state::<CloseBehaviorState>()
|
.try_state::<CloseBehaviorState>()
|
||||||
.map(|state| state.take_confirmed_exit())
|
.map(|state| state.take_confirmed_exit())
|
||||||
.unwrap_or(false);
|
.unwrap_or(false);
|
||||||
if should_confirm_app_exit_request(*code, confirmed_exit) {
|
if should_confirm_app_exit_request(std::env::consts::OS, *code, confirmed_exit) {
|
||||||
api.prevent_exit();
|
api.prevent_exit();
|
||||||
request_app_close(app_handle, "quit");
|
request_app_close(app_handle, "quit");
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in new issue
Block a user