mirror of
https://github.com/t8y2/dbx.git
synced 2026-10-19 16:16:45 +00:00
fix(tabs): allow saving a saved SQL tab after clearing content
This commit is contained in:
@@ -105,6 +105,7 @@ import {
|
||||
tabSwitcherDirectionFromShortcut,
|
||||
} from "@/lib/editor/keyboardShortcuts";
|
||||
import { createTabNavigationHistory, moveInTabNavigationHistory, recordTabVisit } from "@/lib/tabs/tabNavigationHistory";
|
||||
import { canSaveSqlTab } from "@/lib/tabs/sqlTabSaveTarget";
|
||||
import { initialTabSwitcherSelection, moveTabSwitcherSelection, tabSwitcherOrder } from "@/lib/tabs/tabSwitcher";
|
||||
import { createTabSwitcherKeyboardController } from "@/lib/tabs/tabSwitcherKeyboard";
|
||||
import { formatShortcutDisplay } from "@/lib/editor/shortcutDisplay";
|
||||
@@ -1669,10 +1670,6 @@ function finishSqlLibraryFlyAnimation(animationId: number) {
|
||||
sqlLibrarySaveFeedbackId.value += 1;
|
||||
}
|
||||
|
||||
function canSaveSqlTab(tab: QueryTab): boolean {
|
||||
return !!tab.externalSqlPath || !!tab.sql.trim();
|
||||
}
|
||||
|
||||
function closePendingSavedTab() {
|
||||
if (!pendingSaveAndCloseTabId.value) return;
|
||||
const closeId = pendingSaveAndCloseTabId.value;
|
||||
|
||||
@@ -24,6 +24,7 @@ import { hexToRgba } from "@/lib/common/color";
|
||||
import { productionContextForDatabase } from "@/lib/database/productionSafety";
|
||||
import { formatShortcutDisplay } from "@/lib/editor/shortcutDisplay";
|
||||
import { resolveNextEditorToolbarTier, type EditorToolbarTier } from "@/lib/tabs/editorToolbarLayout";
|
||||
import { canSaveSqlTab } from "@/lib/tabs/sqlTabSaveTarget";
|
||||
import { looksLikeDmlStatement } from "@/lib/sql/dmlChangePreview";
|
||||
import { canFormatSqlForDatabaseType } from "@/lib/sql/sqlFormatter";
|
||||
import type { QueryTab, ConnectionConfig } from "@/types/database";
|
||||
@@ -249,7 +250,7 @@ const explainAnalyzeTooltip = computed(() => {
|
||||
if (dbType === "sqlserver") return t("toolbar.actualPlan");
|
||||
return t("toolbar.autotrace");
|
||||
});
|
||||
const canSaveSql = computed(() => !!props.activeTab.externalSqlPath || !!props.activeTab.sql.trim());
|
||||
const canSaveSql = computed(() => canSaveSqlTab(props.activeTab));
|
||||
const keywordCaseIsLower = computed(() => props.sqlKeywordCase === "lower");
|
||||
const keywordCaseToggleTooltip = computed(() => (keywordCaseIsLower.value ? t("toolbar.keywordCaseUpper") : t("toolbar.keywordCaseLower")));
|
||||
const sqlSemanticDiagnosticsEnabled = computed(() => settingsStore.editorSettings.sqlSemanticDiagnosticsEnabled);
|
||||
|
||||
@@ -0,0 +1,41 @@
|
||||
import { describe, expect, it } from "vitest";
|
||||
import { canSaveSqlTab, type SqlTabSaveTarget } from "@/lib/tabs/sqlTabSaveTarget";
|
||||
|
||||
const PROCEDURE_SOURCE = { schema: "dbo", name: "p", objectType: "PROCEDURE" } as const;
|
||||
|
||||
function saveTarget(overrides: Partial<SqlTabSaveTarget>): SqlTabSaveTarget {
|
||||
return { externalSqlPath: undefined, savedSqlId: undefined, sql: "", objectSource: undefined, ...overrides };
|
||||
}
|
||||
|
||||
describe("canSaveSqlTab", () => {
|
||||
it("keeps a saved SQL library tab saveable after its content is deleted", () => {
|
||||
// Issue #9247: emptying a query file is a legitimate edit that must reach the file.
|
||||
expect(canSaveSqlTab(saveTarget({ savedSqlId: "file-1", sql: "" }))).toBe(true);
|
||||
});
|
||||
|
||||
it("treats whitespace-only content in a saved tab as the emptied state, not as content", () => {
|
||||
expect(canSaveSqlTab(saveTarget({ savedSqlId: "file-1", sql: "\n\t " }))).toBe(true);
|
||||
});
|
||||
|
||||
it("keeps an external .sql file tab saveable after its content is deleted", () => {
|
||||
expect(canSaveSqlTab(saveTarget({ externalSqlPath: "/tmp/query.sql", sql: "" }))).toBe(true);
|
||||
});
|
||||
|
||||
it("still allows a brand-new tab with content", () => {
|
||||
expect(canSaveSqlTab(saveTarget({ sql: "SELECT 1;" }))).toBe(true);
|
||||
});
|
||||
|
||||
it("still blocks a brand-new tab with nothing to persist", () => {
|
||||
expect(canSaveSqlTab(saveTarget({ sql: " \n" }))).toBe(false);
|
||||
});
|
||||
|
||||
it("still blocks an emptied object-source tab", () => {
|
||||
// Saving an empty routine body would send a body-less CREATE to the server.
|
||||
expect(canSaveSqlTab(saveTarget({ objectSource: PROCEDURE_SOURCE, sql: "" }))).toBe(false);
|
||||
expect(canSaveSqlTab(saveTarget({ objectSource: PROCEDURE_SOURCE, savedSqlId: "file-1", sql: "" }))).toBe(false);
|
||||
});
|
||||
|
||||
it("allows an object-source tab with content", () => {
|
||||
expect(canSaveSqlTab(saveTarget({ objectSource: PROCEDURE_SOURCE, sql: "CREATE PROCEDURE dbo.p AS SELECT 1;" }))).toBe(true);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,22 @@
|
||||
import type { QueryTab } from "@/types/database";
|
||||
|
||||
/** Tab fields that decide whether its SQL can be persisted. */
|
||||
export type SqlTabSaveTarget = Pick<QueryTab, "externalSqlPath" | "savedSqlId" | "sql" | "objectSource">;
|
||||
|
||||
/**
|
||||
* Whether the tab's SQL can be saved at all.
|
||||
*
|
||||
* A tab that already has a persistence target — an external `.sql` file or a
|
||||
* saved-SQL library entry — stays saveable after its content is emptied.
|
||||
* Deleting everything is a legitimate edit that must reach the target, and the
|
||||
* emptied state is also the only state in which the user can observe the guard.
|
||||
*
|
||||
* A brand-new tab has nothing to persist, and an emptied object-source tab would
|
||||
* push a body-less `CREATE` for a routine or view to the server, so both keep the
|
||||
* content requirement.
|
||||
*/
|
||||
export function canSaveSqlTab(tab: SqlTabSaveTarget): boolean {
|
||||
if (tab.externalSqlPath || tab.sql.trim()) return true;
|
||||
if (tab.objectSource) return false;
|
||||
return !!tab.savedSqlId;
|
||||
}
|
||||
Reference in New Issue
Block a user