fix(editor): save object source tabs contextually
This commit is contained in:
parent
72dd4d728b
commit
f91ccf89c4
33
src/App.vue
33
src/App.vue
|
|
@ -33,12 +33,14 @@ import "@/i18n";
|
|||
import { translateBackendError } from "@/i18n/backend-errors";
|
||||
import * as api from "@/lib/api";
|
||||
import { resolveDefaultDatabase } from "@/lib/defaultDatabase";
|
||||
import { buildExecutableObjectSourceSql, objectSourceSaveExecutionMode } from "@/lib/objectSourceEditor";
|
||||
import { resolveExecutableSql } from "@/lib/sqlExecutionTarget";
|
||||
import { isTauriRuntime } from "@/lib/tauriRuntime";
|
||||
import {
|
||||
isCloseTabShortcut,
|
||||
isExecuteSqlShortcut,
|
||||
isFocusSearchShortcut,
|
||||
isObjectSourceSaveShortcutTarget,
|
||||
isSaveShortcut,
|
||||
} from "@/lib/keyboardShortcuts";
|
||||
import { isPreviewTab } from "@/lib/tabPresentation";
|
||||
|
|
@ -203,6 +205,10 @@ function defaultSavedSqlName(title: string) {
|
|||
async function openSaveSqlDialog() {
|
||||
const tab = activeTab.value;
|
||||
if (!tab || !tab.sql.trim()) return;
|
||||
if (tab.objectSource) {
|
||||
await saveActiveObjectSource(tab);
|
||||
return;
|
||||
}
|
||||
const existing = tab.savedSqlId ? savedSqlStore.getFile(tab.savedSqlId) : undefined;
|
||||
if (existing) {
|
||||
const updated = await savedSqlStore.saveFile({
|
||||
|
|
@ -225,6 +231,30 @@ async function openSaveSqlDialog() {
|
|||
showSaveSqlDialog.value = true;
|
||||
}
|
||||
|
||||
async function saveActiveObjectSource(tab: NonNullable<typeof activeTab.value>) {
|
||||
const connection = connectionStore.getConfig(tab.connectionId);
|
||||
const source = tab.objectSource;
|
||||
if (!connection || !source) return;
|
||||
|
||||
try {
|
||||
const sql = buildExecutableObjectSourceSql({
|
||||
databaseType: connection.db_type,
|
||||
objectType: source.objectType,
|
||||
schema: source.schema || tab.schema || tab.database,
|
||||
name: source.name,
|
||||
source: tab.sql,
|
||||
});
|
||||
if (objectSourceSaveExecutionMode(connection.db_type) === "single") {
|
||||
await api.executeQuery(tab.connectionId, tab.database, sql, source.schema || tab.schema);
|
||||
} else {
|
||||
await api.executeScript(tab.connectionId, tab.database, sql, source.schema || tab.schema);
|
||||
}
|
||||
toast(t("objects.sourceSaved"), 2000);
|
||||
} catch (e: any) {
|
||||
toast(t("objects.sourceSaveFailed", { message: e?.message || String(e) }), 5000);
|
||||
}
|
||||
}
|
||||
|
||||
async function confirmSaveSqlToLibrary() {
|
||||
const tab = activeTab.value;
|
||||
const name = saveSqlName.value.trim();
|
||||
|
|
@ -437,6 +467,9 @@ function handleKeydown(e: KeyboardEvent) {
|
|||
}
|
||||
return;
|
||||
}
|
||||
if (isSaveShortcut(e) && e.target instanceof Element && isObjectSourceSaveShortcutTarget(e.target)) {
|
||||
return;
|
||||
}
|
||||
if (activeTab.value?.mode === "query" && !showSaveSqlDialog.value && isSaveShortcut(e)) {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
|
|
|
|||
|
|
@ -63,6 +63,7 @@ const activeConnectionValue = computed(() => props.activeConnection?.id || "");
|
|||
const activeSchemaValue = computed(() => props.activeTab.schema || "");
|
||||
const isSingleDb = computed(() => isSingleDatabase(props.activeConnection?.db_type));
|
||||
const schemaDatabaseKey = computed(() => props.activeTab.database || (isSingleDb.value ? "_" : ""));
|
||||
const saveTooltip = computed(() => (props.activeTab.objectSource ? t("objects.saveSource") : t("toolbar.saveSql")));
|
||||
|
||||
const showSchemaSelector = computed(() => {
|
||||
const connection = props.activeConnection;
|
||||
|
|
@ -178,7 +179,7 @@ function databaseDisplayName(database: string): string {
|
|||
<Save class="h-3.5 w-3.5" />
|
||||
</Button>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent>{{ t("toolbar.saveSql") }}</TooltipContent>
|
||||
<TooltipContent>{{ saveTooltip }}</TooltipContent>
|
||||
</Tooltip>
|
||||
<Tooltip>
|
||||
<TooltipTrigger as-child>
|
||||
|
|
|
|||
|
|
@ -590,6 +590,11 @@ function viewObjectSource() {
|
|||
.then((result) => {
|
||||
const tabId = queryStore.createTab(node.connectionId!, node.database!, node.label);
|
||||
queryStore.updateSql(tabId, result.source);
|
||||
queryStore.setObjectSource(tabId, {
|
||||
schema,
|
||||
name: node.label,
|
||||
objectType: objectType as "PROCEDURE" | "FUNCTION",
|
||||
});
|
||||
})
|
||||
.catch((e: any) => {
|
||||
toast(e?.message || String(e), 5000);
|
||||
|
|
|
|||
|
|
@ -634,6 +634,7 @@ export default {
|
|||
saveSource: "Save",
|
||||
cancelEdit: "Cancel",
|
||||
sourceSaved: "Source saved",
|
||||
sourceSaveFailed: "Failed to save source: {message}",
|
||||
schemaColumn: "Schema",
|
||||
comment: "Comment",
|
||||
loadingSchemas: "Loading schemas...",
|
||||
|
|
|
|||
|
|
@ -635,6 +635,7 @@ export default {
|
|||
saveSource: "Guardar",
|
||||
cancelEdit: "Cancelar",
|
||||
sourceSaved: "Código fuente guardado",
|
||||
sourceSaveFailed: "Error al guardar el código fuente: {message}",
|
||||
schemaColumn: "Esquema",
|
||||
comment: "Comentario",
|
||||
loadingSchemas: "Cargando esquemas...",
|
||||
|
|
|
|||
|
|
@ -619,6 +619,7 @@ export default {
|
|||
saveSource: "保存",
|
||||
cancelEdit: "取消",
|
||||
sourceSaved: "源码已保存",
|
||||
sourceSaveFailed: "保存源码失败:{message}",
|
||||
schemaColumn: "Schema",
|
||||
comment: "注释",
|
||||
loadingSchemas: "加载 Schema...",
|
||||
|
|
|
|||
|
|
@ -33,6 +33,12 @@ export function isSaveShortcut(event: ShortcutLikeEvent): boolean {
|
|||
return event.key.toLowerCase() === "s";
|
||||
}
|
||||
|
||||
export function isObjectSourceSaveShortcutTarget(
|
||||
target: { closest(selector: string): unknown } | null | undefined,
|
||||
): boolean {
|
||||
return !!target?.closest("[data-object-source-editor], [data-object-source-preview]");
|
||||
}
|
||||
|
||||
export function isCancelSearchShortcut(event: ShortcutLikeEvent): boolean {
|
||||
if (event.isComposing) return false;
|
||||
return event.key === "Escape";
|
||||
|
|
|
|||
|
|
@ -39,6 +39,6 @@ export function buildExecutableObjectSourceSql(input: BuildEditableObjectSourceS
|
|||
return ensureSemicolon(source);
|
||||
}
|
||||
|
||||
export function objectSourceSaveExecutionMode(databaseType: DatabaseType): ObjectSourceSaveExecutionMode {
|
||||
return databaseType === "sqlserver" ? "single" : "script";
|
||||
export function objectSourceSaveExecutionMode(_databaseType: DatabaseType): ObjectSourceSaveExecutionMode {
|
||||
return "single";
|
||||
}
|
||||
|
|
|
|||
|
|
@ -11,6 +11,7 @@ export interface SavedOpenTab {
|
|||
pinned?: boolean;
|
||||
mode?: QueryTab["mode"];
|
||||
objectBrowser?: QueryTab["objectBrowser"];
|
||||
objectSource?: QueryTab["objectSource"];
|
||||
tableMeta?: QueryTab["tableMeta"];
|
||||
}
|
||||
|
||||
|
|
@ -31,6 +32,7 @@ export function serializeOpenTabs(tabs: QueryTab[]): SavedOpenTab[] {
|
|||
pinned: tab.pinned,
|
||||
mode: tab.mode,
|
||||
objectBrowser: tab.objectBrowser,
|
||||
objectSource: tab.objectSource,
|
||||
tableMeta: tab.tableMeta,
|
||||
}));
|
||||
}
|
||||
|
|
|
|||
|
|
@ -52,6 +52,7 @@ export const useQueryStore = defineStore("query", () => {
|
|||
pinned: t.pinned,
|
||||
mode: t.mode,
|
||||
objectBrowser: t.objectBrowser,
|
||||
objectSource: t.objectSource,
|
||||
tableMeta: t.tableMeta,
|
||||
})),
|
||||
);
|
||||
|
|
@ -253,6 +254,11 @@ export const useQueryStore = defineStore("query", () => {
|
|||
if (tab) tab.tableMeta = meta;
|
||||
}
|
||||
|
||||
function setObjectSource(id: string, objectSource: NonNullable<QueryTab["objectSource"]>) {
|
||||
const tab = tabs.value.find((t) => t.id === id);
|
||||
if (tab) tab.objectSource = objectSource;
|
||||
}
|
||||
|
||||
function setExecuting(id: string, isExecuting: boolean) {
|
||||
const tab = tabs.value.find((t) => t.id === id);
|
||||
if (!tab) return;
|
||||
|
|
@ -600,6 +606,7 @@ export const useQueryStore = defineStore("query", () => {
|
|||
updateSchema,
|
||||
updateConnection,
|
||||
setTableMeta,
|
||||
setObjectSource,
|
||||
setExecuting,
|
||||
setErrorResult,
|
||||
setActiveResultIndex,
|
||||
|
|
|
|||
|
|
@ -268,6 +268,11 @@ export interface QueryTab {
|
|||
schema?: string;
|
||||
objectType?: "tables";
|
||||
};
|
||||
objectSource?: {
|
||||
schema?: string;
|
||||
name: string;
|
||||
objectType: ObjectSourceKind;
|
||||
};
|
||||
tableMeta?: {
|
||||
schema?: string;
|
||||
tableName: string;
|
||||
|
|
|
|||
|
|
@ -5,6 +5,7 @@ import {
|
|||
isCloseTabShortcut,
|
||||
isExecuteSqlShortcut,
|
||||
isFocusSearchShortcut,
|
||||
isObjectSourceSaveShortcutTarget,
|
||||
isSaveShortcut,
|
||||
} from "../src/lib/keyboardShortcuts.ts";
|
||||
|
||||
|
|
@ -64,6 +65,23 @@ test("ignores Alt+S for saving", () => {
|
|||
assert.equal(isSaveShortcut({ key: "s", altKey: true }), false);
|
||||
});
|
||||
|
||||
test("detects object source editor targets for contextual save", () => {
|
||||
const target = {
|
||||
closest: (selector: string) =>
|
||||
selector === "[data-object-source-editor], [data-object-source-preview]" ? {} : null,
|
||||
};
|
||||
|
||||
assert.equal(isObjectSourceSaveShortcutTarget(target), true);
|
||||
});
|
||||
|
||||
test("ignores regular editor targets for contextual object source save", () => {
|
||||
const target = {
|
||||
closest: () => null,
|
||||
};
|
||||
|
||||
assert.equal(isObjectSourceSaveShortcutTarget(target), false);
|
||||
});
|
||||
|
||||
test("matches Escape for cancelling search", () => {
|
||||
assert.equal(isCancelSearchShortcut({ key: "Escape" }), true);
|
||||
});
|
||||
|
|
|
|||
|
|
@ -30,6 +30,19 @@ test("SQL Server object source saves as a single batch", () => {
|
|||
assert.equal(objectSourceSaveExecutionMode("sqlserver"), "single");
|
||||
});
|
||||
|
||||
test("Kingbase object source saves as a single statement", () => {
|
||||
assert.equal(objectSourceSaveExecutionMode("kingbase"), "single");
|
||||
});
|
||||
|
||||
test("Postgres-family object source saves as a single statement", () => {
|
||||
assert.equal(objectSourceSaveExecutionMode("postgres"), "single");
|
||||
assert.equal(objectSourceSaveExecutionMode("gaussdb"), "single");
|
||||
});
|
||||
|
||||
test("MySQL object source saves as a single statement", () => {
|
||||
assert.equal(objectSourceSaveExecutionMode("mysql"), "single");
|
||||
});
|
||||
|
||||
test("Postgres view body opens as CREATE OR REPLACE VIEW", () => {
|
||||
const sql = buildExecutableObjectSourceSql({
|
||||
databaseType: "postgres",
|
||||
|
|
|
|||
|
|
@ -35,11 +35,31 @@ test("serializes unsaved query tabs with editor context", () => {
|
|||
pinned: true,
|
||||
mode: "query",
|
||||
objectBrowser: undefined,
|
||||
objectSource: undefined,
|
||||
tableMeta: undefined,
|
||||
},
|
||||
]);
|
||||
});
|
||||
|
||||
test("serializes object source query tabs with save context", () => {
|
||||
const saved = serializeOpenTabs([
|
||||
queryTab({
|
||||
title: "fn_add",
|
||||
objectSource: {
|
||||
schema: "public",
|
||||
name: "fn_add",
|
||||
objectType: "FUNCTION",
|
||||
},
|
||||
}),
|
||||
]);
|
||||
|
||||
assert.deepEqual(saved[0]?.objectSource, {
|
||||
schema: "public",
|
||||
name: "fn_add",
|
||||
objectType: "FUNCTION",
|
||||
});
|
||||
});
|
||||
|
||||
test("restores unsaved query tabs and active tab after restart", () => {
|
||||
const raw = JSON.stringify([
|
||||
queryTab({ id: "tab-1", sql: "select 1" }),
|
||||
|
|
@ -58,6 +78,26 @@ test("restores unsaved query tabs and active tab after restart", () => {
|
|||
assert.equal(restored.activeTabId, "tab-2");
|
||||
});
|
||||
|
||||
test("restores object source save context", () => {
|
||||
const raw = JSON.stringify([
|
||||
queryTab({
|
||||
objectSource: {
|
||||
schema: "public",
|
||||
name: "fn_add",
|
||||
objectType: "FUNCTION",
|
||||
},
|
||||
}),
|
||||
]);
|
||||
|
||||
const restored = restoreOpenTabsState(raw, "tab-1");
|
||||
|
||||
assert.deepEqual(restored.tabs[0]?.objectSource, {
|
||||
schema: "public",
|
||||
name: "fn_add",
|
||||
objectType: "FUNCTION",
|
||||
});
|
||||
});
|
||||
|
||||
test("desktop restore keeps legacy query tabs without a mode", () => {
|
||||
const raw = JSON.stringify([
|
||||
{
|
||||
|
|
|
|||
Loading…
Reference in New Issue