fix(settings): confirm before discarding unsaved settings

This commit is contained in:
Melody Ruch authored and GitHub committed 2026-08-17 14:52:00 +08:00
1 parent 9dbb2ef72f
commit 6a25bd1909
12 files changed
+160 -3

No files matched your search

@@ -1,5 +1,5 @@
import { describe, expect, it } from "vitest";
import { EDITOR_SETTINGS_DRAFT_KEYS, editorSettingsDraftFromSettings, editorSettingsDraftChanged, editorSettingsPatchFromDraft, normalizeQueryResultMaxRowsDraft, normalizeTableOpenPageSizeDraft } from "../editorSettingsDraft";
import { EDITOR_SETTINGS_DRAFT_KEYS, editorSettingsDraftFromSettings, editorSettingsDraftChanged, editorSettingsPatchFromDraft, normalizeQueryResultMaxRowsDraft, normalizeTableOpenPageSizeDraft, shouldConfirmEditorSettingsDialogClose } from "../editorSettingsDraft";
import type { EditorSettings } from "@/stores/settingsStore";
function makeSettings(overrides: Partial<EditorSettings> = {}): EditorSettings {
@@ -345,6 +345,33 @@ describe("editorSettingsDraftChanged - tabLayout", () => {
});
});
describe("shouldConfirmEditorSettingsDialogClose", () => {
// Regression for https://github.com/t8y2/dbx/issues/5905: customizing a
// shortcut or the sidebar activation mode and then dismissing the dialog
// via Escape/outside-click/the "Close" button (anything other than Apply)
// must not silently drop the draft.
it("requests confirmation when the dialog is closing with an unsaved shortcut/sidebarActivation edit", () => {
const settings = makeSettings({ sidebarActivation: "single" } as Partial<EditorSettings>);
const base = editorSettingsDraftFromSettings(settings);
const draft = editorSettingsDraftFromSettings(settings);
draft.sidebarActivation = "double";
expect(shouldConfirmEditorSettingsDialogClose(false, editorSettingsDraftChanged(draft, base))).toBe(true);
});
it("does not block closing when there is no unsaved draft", () => {
const settings = makeSettings();
const base = editorSettingsDraftFromSettings(settings);
const draft = editorSettingsDraftFromSettings(settings);
expect(shouldConfirmEditorSettingsDialogClose(false, editorSettingsDraftChanged(draft, base))).toBe(false);
});
it("never blocks opening the dialog, even with a stale dirty flag", () => {
expect(shouldConfirmEditorSettingsDialogClose(true, true)).toBe(false);
});
});
describe("editorSettingsPatchFromDraft - tabLayout", () => {
it("includes tabLayout in patch when changed", () => {
const settings = makeSettings({ tabLayout: "scroll" });
@@ -134,3 +134,12 @@ export function editorSettingsPatchFromDraft(draft: EditorSettingsDraft, base: E
export function editorSettingsDraftChanged(draft: EditorSettingsDraft, base: EditorSettingsDraft): boolean {
return EDITOR_SETTINGS_DRAFT_KEYS.some((key) => draftValueChanged(key, draft[key], base[key]));
}
// Closing the settings dialog (Escape, clicking outside, the X button, or the
// "Close" footer button) must never silently drop an unapplied draft — the
// dialog only persists shortcuts/sidebarActivation/etc. to the store on an
// explicit Apply. Route every close attempt through this check so an unsaved
// draft always surfaces a confirmation instead of vanishing.
export function shouldConfirmEditorSettingsDialogClose(nextOpen: boolean, hasUnsavedChanges: boolean): boolean {
return nextOpen === false && hasUnsavedChanges;
}