mirror of
https://github.com/t8y2/dbx.git
synced 2026-10-19 16:16:45 +00:00
fix(settings): confirm unsaved changes before closing
This commit is contained in:
@@ -494,6 +494,7 @@ provide(OPEN_PLUGIN_AI_CONVERSATION, (request) => {
|
||||
});
|
||||
const appSidebarRef = ref<InstanceType<typeof AppSidebar> | null>(null);
|
||||
const appTabBarRef = ref<InstanceType<typeof AppTabBar> | null>(null);
|
||||
const settingsPageRef = ref<InstanceType<typeof EditorSettingsPage> | null>(null);
|
||||
const contentAreaRef = ref<InstanceType<typeof SqlEditorWorkspace> | null>(null);
|
||||
const lastFocusedAuxiliarySurface = ref<AuxiliarySearchSurface>(null);
|
||||
const lastFocusedSidebarSurface = ref(false);
|
||||
@@ -1379,6 +1380,16 @@ function activateOpenSpecialPageFallback() {
|
||||
}
|
||||
|
||||
function closeSettingsPage() {
|
||||
if (settingsPageRef.value) {
|
||||
activateSettingsPage();
|
||||
settingsPageRef.value.requestClose();
|
||||
return;
|
||||
}
|
||||
// An async settings page that has not mounted cannot have an edited draft.
|
||||
finishCloseSettingsPage();
|
||||
}
|
||||
|
||||
function finishCloseSettingsPage() {
|
||||
settingsPageTabOpen.value = false;
|
||||
if (settingsReturnSurface.value === "driverStore" && driverStoreTabOpen.value) {
|
||||
activateMainContentSurface("driverStore");
|
||||
@@ -1473,7 +1484,7 @@ async function checkAllUpdates() {
|
||||
}
|
||||
|
||||
function openDriverStoreFromUpdate(target?: DriverStoreTab) {
|
||||
closeSettingsPage();
|
||||
finishCloseSettingsPage();
|
||||
openDriverStorePage(target);
|
||||
}
|
||||
|
||||
@@ -5250,6 +5261,7 @@ onUnmounted(() => {
|
||||
<EditorSettingsPage
|
||||
v-if="settingsPageTabOpen"
|
||||
v-show="settingsStore.settingsPageActive"
|
||||
ref="settingsPageRef"
|
||||
variant="page"
|
||||
:open="settingsPageTabOpen"
|
||||
:initial-tab="settingsInitialTab"
|
||||
@@ -5267,13 +5279,13 @@ onUnmounted(() => {
|
||||
:mcp-update-available="toolbarMcpUpdateAvailable"
|
||||
:plugin-update-count="componentUpdates.pluginUpdateCount.value"
|
||||
class="flex-1 min-h-0"
|
||||
@update:open="(open: boolean) => (open ? activateSettingsPage() : closeSettingsPage())"
|
||||
@update:open="(open: boolean) => (open ? activateSettingsPage() : finishCloseSettingsPage())"
|
||||
@check-updates="checkAllUpdates"
|
||||
@update-all="updateAllAvailable"
|
||||
@open-update-center="handleToolbarUpdateClick"
|
||||
@open-driver-store="openDriverStoreFromUpdate"
|
||||
@open-plugin-center="
|
||||
closeSettingsPage();
|
||||
finishCloseSettingsPage();
|
||||
openPluginCenterPage();
|
||||
"
|
||||
@open-cc-switch-plugin-center="
|
||||
|
||||
@@ -620,6 +620,8 @@ function closeSettings() {
|
||||
requestCloseSettings(false);
|
||||
}
|
||||
|
||||
defineExpose({ requestClose: closeSettings });
|
||||
|
||||
function cancelUnsavedSettingsClose() {
|
||||
showUnsavedSettingsCloseConfirm.value = false;
|
||||
}
|
||||
|
||||
Vendored
+144
@@ -0,0 +1,144 @@
|
||||
// @vitest-environment happy-dom
|
||||
|
||||
import { createApp, defineComponent, h, nextTick, ref, type App } from "vue";
|
||||
import { createPinia, setActivePinia } from "pinia";
|
||||
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
|
||||
import i18n from "@/i18n";
|
||||
import { useSettingsStore } from "@/stores/settingsStore";
|
||||
import EditorSettingsDialog from "../EditorSettingsDialog.vue";
|
||||
|
||||
const backend = vi.hoisted(() => ({ saved: null as unknown, saves: vi.fn() }));
|
||||
|
||||
vi.mock("@/lib/backend/api", async (importOriginal) => {
|
||||
const actual = await importOriginal<typeof import("@/lib/backend/api")>();
|
||||
return {
|
||||
...actual,
|
||||
loadEditorSettings: async () => backend.saved,
|
||||
saveEditorSettings: async (value: unknown) => {
|
||||
backend.saved = structuredClone(value);
|
||||
backend.saves(value);
|
||||
},
|
||||
loadHistoryRetentionLimit: async () => 5000,
|
||||
loadMcpHistoryRetentionLimit: async () => 5000,
|
||||
loadMcpGlobalPolicy: async () => null,
|
||||
loadDesktopSettings: async () => null,
|
||||
loadAiConfigs: async () => [],
|
||||
loadAiConfig: async () => null,
|
||||
loadAiProviderConfigs: async () => ({}),
|
||||
loadAiChatSelection: async () => null,
|
||||
webdavSyncSecretsStatus: async () => ({}),
|
||||
snippetSavedTokenStatus: async () => ({}),
|
||||
snippetSyncSettings: async () => ({}),
|
||||
};
|
||||
});
|
||||
|
||||
let app: App | undefined;
|
||||
let host: HTMLElement;
|
||||
|
||||
async function settle() {
|
||||
for (let i = 0; i < 10; i++) {
|
||||
await Promise.resolve();
|
||||
await nextTick();
|
||||
}
|
||||
}
|
||||
|
||||
beforeEach(() => {
|
||||
backend.saved = null;
|
||||
backend.saves.mockClear();
|
||||
localStorage.clear();
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
app?.unmount();
|
||||
host?.remove();
|
||||
document.body.innerHTML = "";
|
||||
});
|
||||
|
||||
async function mountPage() {
|
||||
const pinia = createPinia();
|
||||
setActivePinia(pinia);
|
||||
const store = useSettingsStore();
|
||||
await store.initEditorSettings();
|
||||
await store.initDesktopSettings();
|
||||
backend.saves.mockClear();
|
||||
const page = ref<{ requestClose(): void } | null>(null);
|
||||
const open = ref(true);
|
||||
host = document.createElement("div");
|
||||
document.body.append(host);
|
||||
app = createApp(
|
||||
defineComponent({
|
||||
setup: () => () =>
|
||||
open.value
|
||||
? h(EditorSettingsDialog, {
|
||||
ref: page,
|
||||
variant: "page",
|
||||
open: true,
|
||||
initialTab: "editor",
|
||||
"onUpdate:open": (value: boolean) => {
|
||||
open.value = value;
|
||||
},
|
||||
})
|
||||
: null,
|
||||
}),
|
||||
);
|
||||
app.use(pinia);
|
||||
app.use(i18n);
|
||||
app.mount(host);
|
||||
await settle();
|
||||
const changeFontSize = async () => {
|
||||
const input = host.querySelector<HTMLInputElement>('input[type="range"][min="10"][max="24"]')!;
|
||||
input.value = "19";
|
||||
input.dispatchEvent(new Event("input", { bubbles: true }));
|
||||
await settle();
|
||||
};
|
||||
return { page, open, store, changeFontSize };
|
||||
}
|
||||
|
||||
function clickText(key: string) {
|
||||
const text = i18n.global.t(key);
|
||||
const button = Array.from(document.querySelectorAll("button")).find((item) => item.textContent?.trim() === text);
|
||||
expect(button, key).toBeTruthy();
|
||||
button!.click();
|
||||
}
|
||||
|
||||
describe("settings page external close request", () => {
|
||||
it("keeps a dirty draft when continuing, and closes without saving only after discarding", async () => {
|
||||
const { page, open, store, changeFontSize } = await mountPage();
|
||||
const originalFontSize = store.editorSettings.fontSize;
|
||||
await changeFontSize();
|
||||
page.value!.requestClose();
|
||||
await settle();
|
||||
expect(open.value).toBe(true);
|
||||
clickText("settings.unsavedChangesCloseCancel");
|
||||
await settle();
|
||||
expect(host.querySelector<HTMLInputElement>('input[type="range"][min="10"][max="24"]')?.value).toBe("19");
|
||||
expect(backend.saves).not.toHaveBeenCalled();
|
||||
page.value!.requestClose();
|
||||
await settle();
|
||||
clickText("settings.unsavedChangesCloseDiscard");
|
||||
await settle();
|
||||
expect(open.value).toBe(false);
|
||||
expect(store.editorSettings.fontSize).toBe(originalFontSize);
|
||||
expect(backend.saves).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("closes an unchanged page directly", async () => {
|
||||
const { page, open } = await mountPage();
|
||||
page.value!.requestClose();
|
||||
await settle();
|
||||
expect(open.value).toBe(false);
|
||||
expect(backend.saves).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("closes directly after applying the draft", async () => {
|
||||
const { page, open, store, changeFontSize } = await mountPage();
|
||||
await changeFontSize();
|
||||
clickText("settings.apply");
|
||||
await settle();
|
||||
expect(store.editorSettings.fontSize).toBe(19);
|
||||
expect(backend.saves).toHaveBeenCalled();
|
||||
page.value!.requestClose();
|
||||
await settle();
|
||||
expect(open.value).toBe(false);
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user