fix(settings): normalize shortcut edits to the persisted form

This commit is contained in:
zipg authored and GitHub committed 2026-09-21 19:11:56 +08:00
1 parent 0c6fc610cd
commit 776f44c1cb
14 files changed
+174 -4

No files matched your search

@@ -174,7 +174,7 @@ import {
type WebDavConfig,
} from "@/lib/backend/api";
import { eventToModifierOnlyShortcut, eventToShortcut } from "@/lib/editor/keyboardShortcuts";
import { SHORTCUT_DEFINITIONS, countShortcutConflictPairs, findCrossScopeShortcutConflicts, findShortcutConflict, isReservedShortcut, normalizeShortcutSettings, type ShortcutActionId, type ShortcutDefinition, type ShortcutScope } from "@/lib/editor/shortcutRegistry";
import { SHORTCUT_DEFINITIONS, countShortcutConflictPairs, findCrossScopeShortcutConflicts, findShortcutConflict, isReservedShortcut, normalizeShortcutSettings, resolveCapturedShortcutEdit, type ShortcutActionId, type ShortcutDefinition, type ShortcutScope } from "@/lib/editor/shortcutRegistry";
import LightTooltip from "@/components/ui/LightTooltip.vue";
import { formatShortcutDisplay } from "@/lib/editor/shortcutDisplay";
import { COLUMN_NAME_COPY_SEPARATOR_LABELS, COLUMN_NAME_COPY_SEPARATOR_OPTIONS, isColumnNameCopySeparator, type ColumnNameCopySeparator } from "@/lib/dataGrid/dataGridColumnNameCopy";
@@ -2065,6 +2065,9 @@ async function persistSettings() {
// result back into the input so an out-of-range draft doesn't keep
// reporting unsaved changes after a successful apply.
editRedisDatabaseDisplayLimit.value = settingsStore.editorSettings.redisDatabaseDisplayLimit;
// 同理:落盘口径可能改写键位(跨平台默认键、保留键回退),草稿要跟着回到
// 落盘值,避免面板卡在“未保存”无法应用(#9881)。
editShortcuts.value = normalizeShortcutSettings(settingsStore.editorSettings.shortcuts);
}
if (pendingBackgroundImageCleanup) {
const cleanupPath = pendingBackgroundImageCleanup;
@@ -2688,7 +2691,19 @@ function onShortcutKeydown(actionId: ShortcutActionId, event: KeyboardEvent) {
editingShortcutId.value = null;
return;
}
onShortcutChange(actionId, shortcut);
// 草稿必须正好等于落盘值:否则“未保存”状态永远消不掉,#9881 里
// 「应用」点了没反应、「应用并关闭」每次都弹未保存确认。
const captured = resolveCapturedShortcutEdit(actionId, shortcut, editShortcuts.value);
if (captured.rejectedByPlatformDefault) {
toast(t("settings.shortcutPlatformDefault"), 3000);
editingShortcutId.value = null;
return;
}
if (captured.changed) {
editShortcuts.value = captured.shortcuts;
} else {
onShortcutChange(actionId, shortcut);
}
editingShortcutId.value = null;
}
@@ -2729,10 +2744,14 @@ function cancelShortcutEdit() {
function resetShortcut(actionId: ShortcutActionId) {
const definition = SHORTCUT_DEFINITIONS.find((item) => item.id === actionId);
if (!definition) return;
editShortcuts.value = {
// The static definition default is the macOS literal for a few actions
// (selection-occurrence), so the reset must go through the same persisted
// normalization as capture — otherwise Windows drafts a key that can never
// save and silently flips to the platform default on apply.
editShortcuts.value = normalizeShortcutSettings({
...editShortcuts.value,
[actionId]: definition.defaultShortcut,
};
});
}
function clearShortcut(actionId: ShortcutActionId) {
@@ -0,0 +1,46 @@
import { readFileSync } from "node:fs";
import { describe, expect, it } from "vitest";
const dialogSource = readFileSync(new URL("../EditorSettingsDialog.vue", import.meta.url), "utf8");
// Regression for https://github.com/t8y2/dbx/issues/9881: the shortcut draft was
// written straight from the captured key event, while persistence re-normalizes it
// and swaps "another platform's default" literals for the local platform default.
// The draft then never equaled the stored value again, so `hasChanges()` stayed true
// forever: "Apply" looked like it did nothing and "Apply and Close" always asked
// about unsaved changes — the shortcut (and every other pending edit) could never be
// saved. The capture path must therefore write the same value the store will keep.
describe("EditorSettingsDialog shortcut capture normalization", () => {
it("round-trips a captured combination through the persistence normalizer", () => {
expect(dialogSource).toContain("resolveCapturedShortcutEdit,");
expect(dialogSource).toContain("const captured = resolveCapturedShortcutEdit(actionId, shortcut, editShortcuts.value);");
});
it("refuses combinations the store would rewrite, keeping the previous shortcut", () => {
expect(dialogSource).toContain('toast(t("settings.shortcutPlatformDefault"), 3000);');
const guardStart = dialogSource.indexOf("if (captured.rejectedByPlatformDefault) {");
const guardEnd = dialogSource.indexOf("if (captured.changed) {", guardStart);
expect(guardStart).toBeGreaterThan(-1);
expect(guardEnd).toBeGreaterThan(guardStart);
// 拒绝分支必须先结束编辑、且不得写入草稿(保留原值 → 草稿仍是落盘值的不动点)
const guardBody = dialogSource.slice(guardStart, guardEnd);
expect(guardBody).toContain("editingShortcutId.value = null;");
expect(guardBody).toContain("return;");
expect(guardBody).not.toContain("editShortcuts.value =");
});
it("adopts the normalized result so the draft can never diverge from the stored value", () => {
expect(dialogSource).toContain("editShortcuts.value = captured.shortcuts;");
});
it("re-syncs the shortcut draft from the store after a successful apply", () => {
const bodyStart = dialogSource.indexOf("async function persistSettings() {");
const bodyEnd = dialogSource.indexOf("function applySettingsErrorToast(", bodyStart);
expect(bodyStart).toBeGreaterThan(-1);
expect(bodyEnd).toBeGreaterThan(bodyStart);
expect(dialogSource.slice(bodyStart, bodyEnd)).toContain("editShortcuts.value = normalizeShortcutSettings(settingsStore.editorSettings.shortcuts);");
});
});