feat(editor): add sql editor split panes with shared results

This commit is contained in:
Lan-zk authored and GitHub committed 2026-09-04 14:44:03 +08:00
1 parent 24a274bd89
commit 5a3a6e2bb0
76 files changed
+7997 -3343

No files matched your search

+209 -182
View File
@@ -1,13 +1,13 @@
<script setup lang="ts">
import { ref, computed, watch, onMounted, onUnmounted, nextTick, defineAsyncComponent } from "vue";
import { ref, computed, watch, onMounted, onUnmounted, nextTick, defineAsyncComponent, provide } from "vue";
import { useI18n } from "vue-i18n";
import { ChevronsRight, FileText } from "@lucide/vue";
import { TooltipProvider } from "@/components/ui/tooltip";
import AppToolbar from "@/components/layout/AppToolbar.vue";
import AppTabBar from "@/components/layout/AppTabBar.vue";
import AppSidebar from "@/components/layout/AppSidebar.vue";
import EditorToolbar from "@/components/layout/EditorToolbar.vue";
import ContentArea from "@/components/layout/ContentArea.vue";
import SqlEditorWorkspace from "@/components/layout/SqlEditorWorkspace.vue";
import { EDITOR_TOOLBAR_ACTIONS } from "@/components/layout/editorToolbarActions";
import AppDialogs from "@/components/layout/AppDialogs.vue";
import DetachedTabHeader from "@/components/layout/DetachedTabHeader.vue";
import WelcomeScreen from "@/components/layout/WelcomeScreen.vue";
@@ -32,6 +32,7 @@ import MultiDbExecuteDialog from "@/components/editor/MultiDbExecuteDialog.vue";
import { useDialogSources } from "@/composables/useDialogSources";
import { useNavigationTargets } from "@/composables/useNavigationTargets";
import { useDataGridActions } from "@/composables/useDataGridActions";
import type { DataGridSortMode } from "@/lib/dataGrid/dataGridSort";
import { useTauriEvents } from "@/composables/useTauriEvents";
import { useCloseActionPrompt, type AppCloseAction, type AppCloseRequestOptions } from "@/composables/useCloseActionPrompt";
import { disposeAllSqlServerActivityTraces } from "@/lib/sqlserver/sqlServerActivityTraceRuntime";
@@ -57,7 +58,7 @@ import { buildEditableObjectSource, buildExecutableObjectSourceStatements, execu
import { loadEditableObjectSourceForEditor } from "@/lib/table/objectSourceLoad";
import { schemaAfterConnectionSwitch } from "@/lib/schema/connectionSchemaInitialization";
import { resolveHistorySqlRestoreTarget } from "@/lib/history/historyRestoreTarget";
import { resolveExecutableSql, resolveExecutableSqlWithBackend, type SqlExecutionSnapshot } from "@/lib/sql/sqlExecutionTarget";
import { resolveExecutableSql, resolveExecutableSqlWithBackend, type SqlExecutionOverride, type SqlExecutionSnapshot } from "@/lib/sql/sqlExecutionTarget";
import { uuid } from "@/lib/common/utils";
import { isMacOS, isWindows } from "@/lib/backend/platform";
import { isTauriRuntime } from "@/lib/backend/tauriRuntime";
@@ -96,7 +97,6 @@ import {
switchToTabIndexFromShortcut,
tabSwitcherDirectionFromShortcut,
} from "@/lib/editor/keyboardShortcuts";
import { isPreviewTab } from "@/lib/tabs/tabPresentation";
import { createTabNavigationHistory, moveInTabNavigationHistory, recordTabVisit } from "@/lib/tabs/tabNavigationHistory";
import { initialTabSwitcherSelection, moveTabSwitcherSelection, tabSwitcherOrder } from "@/lib/tabs/tabSwitcher";
import { createTabSwitcherKeyboardController } from "@/lib/tabs/tabSwitcherKeyboard";
@@ -286,7 +286,7 @@ const { sidebarWidth, aiPanelWidth, historyWidth, sqlLibraryWidth, sqlFilePanelW
const aiAssistantRef = ref<AiAssistantHandle | null>(null);
const appSidebarRef = ref<InstanceType<typeof AppSidebar> | null>(null);
const appTabBarRef = ref<InstanceType<typeof AppTabBar> | null>(null);
const contentAreaRef = ref<InstanceType<typeof ContentArea> | null>(null);
const contentAreaRef = ref<InstanceType<typeof SqlEditorWorkspace> | null>(null);
const selectedSql = ref("");
const cursorPos = ref(0);
@@ -307,6 +307,7 @@ const queryEditorObjectSourceTarget = ref<{
relationName?: string;
} | null>(null);
const showSaveSqlDialog = ref(false);
const saveSqlDialogTabId = ref<string | null>(null);
const sqlLibrarySaveFeedbackId = ref(0);
const saveSqlConfirmButtonRef = ref<HTMLElement | { $el?: HTMLElement } | null>(null);
const sqlLibraryFlyAnimation = ref<{
@@ -352,19 +353,23 @@ async function emitDetachedEvent(event: string, payload: unknown) {
async function isPointOverMainTabBar(position: { x: number; y: number }): Promise<boolean> {
if (isDetachedWindowContext) return false;
const tabBar = document.querySelector<HTMLElement>("[data-main-tab-bar]");
if (!tabBar) return false;
// In the split workspace every pane's strip (and the special-surfaces bar)
// carries the anchor; a point over any of them is a return-to-main drop.
const tabBars = Array.from(document.querySelectorAll<HTMLElement>("[data-main-tab-bar]"));
if (tabBars.length === 0) return false;
try {
const { getCurrentWindow } = await import("@tauri-apps/api/window");
const currentWindow = getCurrentWindow();
const [innerPosition, scaleFactor] = await Promise.all([currentWindow.innerPosition(), currentWindow.scaleFactor()]);
const rect = tabBar.getBoundingClientRect();
const safeScale = Number.isFinite(scaleFactor) && scaleFactor > 0 ? scaleFactor : 1;
const left = innerPosition.x + rect.left * safeScale;
const top = innerPosition.y + rect.top * safeScale;
const right = innerPosition.x + rect.right * safeScale;
const bottom = innerPosition.y + rect.bottom * safeScale;
return position.x >= left && position.x <= right && position.y >= top && position.y <= bottom;
return tabBars.some((tabBar) => {
const rect = tabBar.getBoundingClientRect();
const left = innerPosition.x + rect.left * safeScale;
const top = innerPosition.y + rect.top * safeScale;
const right = innerPosition.x + rect.right * safeScale;
const bottom = innerPosition.y + rect.bottom * safeScale;
return position.x >= left && position.x <= right && position.y >= top && position.y <= bottom;
});
} catch {
return false;
}
@@ -641,7 +646,6 @@ const activeConnection = computed(() => {
// Oracle connection uses the agent runtime but reports db_type "oracle"), so
// the toolbar's Commit/Rollback visibility can apply the Oracle dirty-state rule
// without inferring Oracle from the raw transport type.
const isOracleManualTransaction = computed(() => effectiveDatabaseTypeForConnection(activeConnection.value) === "oracle" && (activeTab.value?.autoCommit ?? true) === false);
function updateAgentDriverUpdateCount(count: number) {
if (!settingsStore.editorSettings.updateNotificationsEnabled) {
@@ -704,12 +708,12 @@ async function resolveActiveExecutableSql(snapshot?: SqlExecutionSnapshot) {
const blockDangerousRedisCommands = ref(true);
const databaseRequiredSignal = ref(0);
const databaseRequiredTabId = ref<string | null>(null);
const pendingToolbarExecutionSnapshot = ref<SqlExecutionSnapshot>();
const pendingToolbarExecutionSnapshot = ref<SqlExecutionSnapshot & { tabId?: string }>();
const sqlExecutionDangerStore = useSqlExecutionDangerStore();
const productionSafetyStore = useProductionSafetyStore();
function promptActiveDatabaseSelection() {
const tab = activeTab.value;
function promptActiveDatabaseSelection(tabId?: string) {
const tab = tabId ? queryStore.tabs.find((candidate) => candidate.id === tabId) : activeTab.value;
if (!tab) return;
databaseRequiredTabId.value = tab.id;
databaseRequiredSignal.value += 1;
@@ -718,13 +722,13 @@ function promptActiveDatabaseSelection() {
const {
dangerSql,
pendingDangerSql,
showDangerDialog,
suppressDangerConfirm,
tryExecute,
tryExecuteInNewResultTab,
doExecute,
cancelActiveExecution,
requestDangerConfirmation,
tryExplain,
onDangerConfirm,
showSqlParameterDialog,
@@ -750,7 +754,8 @@ const {
});
function captureActiveEditorExecutionSnapshot() {
pendingToolbarExecutionSnapshot.value = contentAreaRef.value?.captureQueryEditorExecutionSnapshot?.();
const snapshot = contentAreaRef.value?.captureQueryEditorExecutionSnapshot?.();
pendingToolbarExecutionSnapshot.value = snapshot ? { ...snapshot, tabId: activeTab.value?.id } : undefined;
}
function requestActiveEditorExecute(source?: "pointer" | "keyboard") {
@@ -758,7 +763,7 @@ function requestActiveEditorExecute(source?: "pointer" | "keyboard") {
pendingToolbarExecutionSnapshot.value = undefined;
if (source === "pointer") {
if (snapshot) {
void tryExecute(snapshot);
void tryExecute(snapshot, { tabId: snapshot.tabId ?? activeTab.value?.id });
return;
}
}
@@ -771,6 +776,36 @@ function requestActiveEditorExecuteInNewResultTab() {
void tryExecuteInNewResultTab();
}
// Per-group editor toolbars call back into this App-owned orchestration. The
// group focuses itself on pointerdown/focusin before any toolbar event, so the
// acting tab is passed explicitly instead of relying on the focused group.
provide(EDITOR_TOOLBAR_ACTIONS, {
explainMode,
blockDangerousRedisCommands,
databaseRequiredSignalFor: (tabId: string) => (databaseRequiredTabId.value === tabId ? databaseRequiredSignal.value : 0),
captureExecutionSnapshot: captureActiveEditorExecutionSnapshot,
toolbarExecute: requestActiveEditorExecute,
cancelExecution: (tabId: string) => cancelActiveExecution(tabId),
explain: (tabId: string) => tryExplain(undefined, { tabId }),
formatSql: formatActiveSql,
compressSql: compressActiveSql,
toggleSqlKeywordCase,
saveSql: (tabId: string) => void openSaveSqlDialog(tabId),
openSqlFile,
importResultArchive,
pasteSqlInCondition: pasteClipboardAsSqlInCondition,
multiExecute: requestMultiDbExecute,
previewChanges: requestActiveEditorPreviewChanges,
changeConnection: changeActiveConnection,
changeCatalog: changeActiveCatalog,
changeDatabase: changeActiveDatabase,
changeSchema: changeActiveSchema,
setDefaultDatabase: setActiveDatabaseAsDefault,
clearDefaultDatabase: clearActiveDefaultDatabase,
});
// Upstream "preview changes" entry: dormant until the group toolbar wires the
// preview-changes button into the workspace routing (see merge notes).
function requestActiveEditorPreviewChanges() {
void contentAreaRef.value?.requestQueryEditorPreviewChanges?.();
}
@@ -866,6 +901,7 @@ const tabWorkspaceLayoutClass = computed(() => {
return isVerticalTabPlacement.value ? "flex-row" : "flex-col";
});
// Every pane's vertical strip writes back to this shared width/collapse state.
function startTabBarResize(event: MouseEvent) {
if (settingsStore.editorSettings.tabPlacement === "right") {
startRightTabBarResize(event);
@@ -877,6 +913,7 @@ function startTabBarResize(event: MouseEvent) {
function toggleTabBarCollapsed() {
setTabBarCollapsed(!tabBarCollapsed.value);
}
const updateNotificationsEnabled = computed(() => settingsStore.editorSettings.updateNotificationsEnabled);
function openSettings(initialTab = "appearance", initialSection?: string) {
@@ -1338,8 +1375,12 @@ function analyzeHistoryWithAi(entry: HistoryEntry) {
invokeWhenAiReady((handle) => handle.triggerAction("explain", buildHistoryAiAnalysisPrompt(entry)));
}
function formatActiveSql() {
const tab = activeTab.value;
function resolveToolbarTab(tabId?: string) {
return tabId ? queryStore.tabs.find((candidate) => candidate.id === tabId) : activeTab.value;
}
function formatActiveSql(tabId?: string) {
const tab = resolveToolbarTab(tabId);
if (!tab || tab.mode !== "query" || !tab.sql.trim()) return;
formatSqlRequest.value = {
id: (formatSqlRequest.value?.id ?? 0) + 1,
@@ -1347,8 +1388,8 @@ function formatActiveSql() {
};
}
function compressActiveSql() {
const tab = activeTab.value;
function compressActiveSql(tabId?: string) {
const tab = resolveToolbarTab(tabId);
if (!tab || tab.mode !== "query" || !tab.sql.trim()) return;
compressSqlRequest.value = {
id: (compressSqlRequest.value?.id ?? 0) + 1,
@@ -1432,7 +1473,8 @@ function closePendingSavedTab() {
if (!pendingSaveAndCloseTabId.value) return;
const closeId = pendingSaveAndCloseTabId.value;
pendingSaveAndCloseTabId.value = null;
if (pendingPrevActiveTabId.value) queryStore.activeTabId = pendingPrevActiveTabId.value;
saveSqlDialogTabId.value = null;
if (pendingPrevActiveTabId.value) queryStore.activateTab(pendingPrevActiveTabId.value);
pendingPrevActiveTabId.value = null;
const shouldCloseTab = pendingSaveShouldCloseTab.value;
pendingSaveShouldCloseTab.value = true;
@@ -1442,6 +1484,7 @@ function closePendingSavedTab() {
function cancelPendingSaveAndClose() {
invalidateSaveSqlFolderSelection();
showSaveSqlDialog.value = false;
saveSqlDialogTabId.value = null;
pendingSaveAndCloseTabId.value = null;
pendingPrevActiveTabId.value = null;
pendingSaveShouldCloseTab.value = true;
@@ -1636,7 +1679,7 @@ async function formattedSqlForSave(tab: QueryTab): Promise<string> {
async function saveTabForCloseAll(tabId: string): Promise<boolean> {
const tab = queryStore.tabs.find((t) => t.id === tabId);
if (!tab) return true;
queryStore.activeTabId = tabId;
queryStore.activateTab(tabId);
if (tab.mode === "structure") {
await nextTick();
@@ -1693,7 +1736,7 @@ async function handleSaveTab(tabId: string) {
const tab = queryStore.tabs.find((t) => t.id === tabId);
if (!tab) return;
if (tab.mode === "structure") {
queryStore.activeTabId = tabId;
queryStore.activateTab(tabId);
await nextTick();
if (await contentAreaRef.value?.applyTableStructureChanges?.()) {
completePendingTabSave(tabId);
@@ -1741,7 +1784,8 @@ async function handleSaveTab(tabId: string) {
}
// No existing saved SQL — open save dialog, then close after save
const prevActive = queryStore.activeTabId;
queryStore.activeTabId = tabId;
queryStore.activateTab(tabId);
saveSqlDialogTabId.value = tabId;
saveSqlName.value = defaultSavedSqlName(tab.title);
resetSaveSqlFolderSelection(ROOT_SAVED_SQL_FOLDER);
pendingSaveAndCloseTabId.value = tabId;
@@ -1749,9 +1793,10 @@ async function handleSaveTab(tabId: string) {
showSaveSqlDialog.value = true;
}
async function openSaveSqlDialog() {
const tab = activeTab.value;
async function openSaveSqlDialog(tabId?: string) {
const tab = tabId ? queryStore.tabs.find((candidate) => candidate.id === tabId) : activeTab.value;
if (!tab || !canSaveSqlTab(tab)) return;
saveSqlDialogTabId.value = tab.id;
if (tab.objectSource) {
await saveActiveObjectSource(tab);
return;
@@ -1843,7 +1888,7 @@ async function handleSaveSqlFolderSelect(value: string) {
await selectSaveSqlFolder(value);
return;
}
const tab = activeTab.value;
const tab = saveSqlDialogTabId.value ? queryStore.tabs.find((candidate) => candidate.id === saveSqlDialogTabId.value) : activeTab.value;
if (!tab) return;
await selectSaveSqlFolder(
value,
@@ -1854,7 +1899,7 @@ async function handleSaveSqlFolderSelect(value: string) {
async function confirmSaveSqlToLibrary() {
if (saveSqlFolderCreationPending.value) return;
const tab = activeTab.value;
const tab = saveSqlDialogTabId.value ? queryStore.tabs.find((candidate) => candidate.id === saveSqlDialogTabId.value) : activeTab.value;
const name = saveSqlName.value.trim();
if (!tab || !tab.sql.trim() || !name) return;
const flyOrigin = elementCenter(saveSqlConfirmButtonElement());
@@ -1873,6 +1918,7 @@ async function confirmSaveSqlToLibrary() {
queryStore.linkSavedSql(tab.id, saved.id, saved.name);
queryStore.markTabClean(tab);
showSaveSqlDialog.value = false;
saveSqlDialogTabId.value = null;
closePendingSavedTab();
void notifyNewSqlLibrarySaved(flyOrigin);
} catch (e: any) {
@@ -1903,7 +1949,7 @@ async function saveExternalSqlTabAs(tab: QueryTab): Promise<boolean> {
}
async function saveActiveSqlAsLocalFile() {
const tab = activeTab.value;
const tab = saveSqlDialogTabId.value ? queryStore.tabs.find((candidate) => candidate.id === saveSqlDialogTabId.value) : activeTab.value;
if (tab) await saveExternalSqlTabAs(tab);
}
@@ -2439,8 +2485,8 @@ function onQueryEditorObjectSourceSaved() {
contentAreaRef.value?.refreshQueryEditorCompletionCache();
}
async function changeActiveConnection(connectionId: string) {
const tab = activeTab.value;
async function changeActiveConnection(connectionId: string, tabId?: string) {
const tab = resolveToolbarTab(tabId);
if (!tab) return;
const connection = connectionStore.getConfig(connectionId);
if (!connection) return;
@@ -2459,7 +2505,8 @@ async function changeActiveConnection(connectionId: string) {
// A configured default wins. Otherwise Oracle returns the session's current schema first.
const orderedSchemas = connection.default_schema ? [] : await api.listSchemas(connectionId, database);
const schema = schemaAfterConnectionSwitch(connection.db_type, orderedSchemas, connection.default_schema);
if (schema && activeTab.value?.id === tab.id && activeTab.value.connectionId === connectionId) {
const latestTab = queryStore.tabs.find((candidate) => candidate.id === tab.id);
if (schema && latestTab && latestTab.connectionId === connectionId) {
queryStore.updateSchema(tab.id, schema);
if (tab.externalSqlPath) rememberExternalSqlFileTarget(tab.externalSqlPath, { connectionId, database, catalog: undefined, schema });
}
@@ -2477,8 +2524,8 @@ async function changeActiveConnection(connectionId: string) {
}
}
function changeActiveDatabase(database: string) {
const tab = activeTab.value;
function changeActiveDatabase(database: string, tabId?: string) {
const tab = resolveToolbarTab(tabId);
if (tab) {
queryStore.updateDatabase(tab.id, database);
if (tab.externalSqlPath) rememberExternalSqlFileTarget(tab.externalSqlPath, { connectionId: tab.connectionId, database, catalog: tab.catalog, schema: tab.schema });
@@ -2488,28 +2535,28 @@ function changeActiveDatabase(database: string) {
}
}
function changeActiveCatalog(catalog: string | undefined, database: string) {
const tab = activeTab.value;
function changeActiveCatalog(catalog: string | undefined, database: string, tabId?: string) {
const tab = resolveToolbarTab(tabId);
if (tab) {
queryStore.updateCatalog(tab.id, catalog, database);
if (tab.externalSqlPath) rememberExternalSqlFileTarget(tab.externalSqlPath, { connectionId: tab.connectionId, database, catalog, schema: tab.schema });
}
}
async function setActiveDatabaseAsDefault() {
const tab = activeTab.value;
async function setActiveDatabaseAsDefault(tabId?: string) {
const tab = resolveToolbarTab(tabId);
if (!tab || !tab.connectionId || !tab.database || tab.catalog) return;
await connectionStore.setDefaultDatabase(tab.connectionId, tab.database);
}
async function clearActiveDefaultDatabase() {
const tab = activeTab.value;
async function clearActiveDefaultDatabase(tabId?: string) {
const tab = resolveToolbarTab(tabId);
if (!tab || !tab.connectionId) return;
await connectionStore.clearDefaultDatabase(tab.connectionId);
}
function changeActiveSchema(schema: string | undefined) {
const tab = activeTab.value;
function changeActiveSchema(schema: string | undefined, tabId?: string) {
const tab = resolveToolbarTab(tabId);
if (!tab) return;
queryStore.updateSchema(tab.id, schema);
if (tab.externalSqlPath) rememberExternalSqlFileTarget(tab.externalSqlPath, { connectionId: tab.connectionId, database: tab.database, catalog: tab.catalog, schema });
@@ -2554,7 +2601,7 @@ function routeAiRedisCommand(command: string, execute: boolean): boolean {
console.warn("[DBX] Redis AI command could not reach the active Redis console");
return true;
}
void routed.then((handled) => {
void routed.then((handled: any) => {
if (!handled) console.warn("[DBX] Redis AI command could not reach the active Redis console");
});
return true;
@@ -2568,9 +2615,9 @@ function onAiAppendSql(sql: string) {
if (appendedSql !== currentSql) queryStore.updateSql(tabId, appendedSql);
}
function runAiGeneratedSql(sql: string) {
function runAiGeneratedSql(sql: string, tabId = activeTab.value?.id) {
selectedSql.value = "";
nextTick(() => tryExecute(sql));
nextTick(() => tryExecute(sql, { tabId }));
}
function onAiExecuteSql(sql: string) {
@@ -2579,13 +2626,13 @@ function onAiExecuteSql(sql: string) {
const currentSql = queryStore.tabs.find((tab) => tab.id === tabId)?.sql ?? "";
const appendedSql = buildDeduplicatedAppendedEditorSql(currentSql, sql);
if (appendedSql !== currentSql) queryStore.updateSql(tabId, appendedSql);
runAiGeneratedSql(sql);
runAiGeneratedSql(sql, tabId);
}
function onAiTempRunSql(sql: string) {
if (routeAiRedisCommand(sql, true)) return;
ensureQueryTab();
runAiGeneratedSql(sql);
const tabId = ensureQueryTab();
runAiGeneratedSql(sql, tabId);
}
function onAiRequestAutoExecuteSql(sql: string) {
@@ -2608,12 +2655,10 @@ function onAiRequestAutoExecuteSql(sql: string) {
nextTick(() => {
if (decision.action === "auto_execute") {
void doExecute(sql);
void doExecute(sql, undefined, { tabId });
return;
}
dangerSql.value = sql;
pendingDangerSql.value = sql;
showDangerDialog.value = true;
requestDangerConfirmation(sql, tabId);
});
}
@@ -2622,7 +2667,7 @@ function onAiOpenExplainPlan(sql: string) {
queryStore.updateSql(tabId, buildAppendedEditorSql(activeTab.value?.sql || "", sql));
selectedSql.value = "";
nextTick(() => {
void tryExplain(sql);
void tryExplain(sql, { tabId });
});
}
@@ -2807,7 +2852,7 @@ function dispatchBeforeTabSwitch(tabId: string) {
function activateQueryTab(tabId: string): boolean {
if (!queryStore.tabs.some((tab) => tab.id === tabId)) return false;
dispatchBeforeTabSwitch(tabId);
queryStore.activeTabId = tabId;
if (!queryStore.activateTab(tabId)) return false;
activateQuerySurface();
return true;
}
@@ -2940,7 +2985,9 @@ async function handleKeydown(e: KeyboardEvent) {
return;
}
if (isFocusSearchShortcut(e, shortcuts)) {
const focused = contentAreaRef.value?.focusSearch() || appSidebarRef.value?.focusSearch();
// Route by the event target so focus inside the shared result grid (or a
// Teleported cell-detail dialog) lands on the result search, not the editor.
const focused = contentAreaRef.value?.focusSearch(e.target instanceof Element ? e.target : null) || appSidebarRef.value?.focusSearch();
if (focused) {
e.preventDefault();
e.stopPropagation();
@@ -3390,13 +3437,8 @@ onUnmounted(() => {
:agent-driver-update-count="toolbarAgentDriverUpdateCount"
:detached-drop-target="detachedDropTargetTabId !== null"
:can-detach-tabs="isDesktop"
:tab-bar-width="tabBarWidth"
:tab-bar-collapsed="tabBarCollapsed"
@toggle-zen-mode="toggleZenMode"
@activate-driver-store="openDriverStorePage"
@activate-settings-page="activateSettingsPage"
@locate-tab="locateTabInSidebar"
@activate-tab="activateQuerySurface"
@close-driver-store="closeDriverStorePage"
@close-settings-page="closeSettingsPage"
@save-tab="handleSaveTab"
@@ -3405,8 +3447,6 @@ onUnmounted(() => {
@discard-all-tab-close="handleDiscardAllPendingTabClose"
@cancel-tab-close="cancelPendingAppClose"
@detach-tab="detachTab"
@start-resize="startTabBarResize"
@toggle-collapse="toggleTabBarCollapsed"
/>
<DetachedTabHeader
v-else-if="activeTab"
@@ -3449,129 +3489,115 @@ onUnmounted(() => {
@update:open="(open: boolean) => (open ? activateSettingsPage() : closeSettingsPage())"
@check-updates="checkUpdates()"
/>
<div v-if="activeTab" v-show="!driverStoreActive && !settingsStore.settingsPageActive" class="flex flex-col flex-1 min-h-0">
<EditorToolbar
v-if="activeTab.mode === 'query' && !isPreviewTab(activeTab)"
:active-tab="activeTab"
<div v-if="queryStore.tabs.length > 0" v-show="!driverStoreActive && !settingsStore.settingsPageActive" class="flex flex-col flex-1 min-h-0">
<SqlEditorWorkspace
ref="contentAreaRef"
@locate-tab="locateTabInSidebar"
@toggle-zen-mode="toggleZenMode"
@start-resize="startTabBarResize"
@toggle-collapse="toggleTabBarCollapsed"
:active-tab="activeTab!"
:active-connection="activeConnection"
:tab-bar-width="tabBarWidth"
:tab-bar-collapsed="tabBarCollapsed"
:can-detach-tabs="isDesktop"
:detached-drop-target="detachedDropTargetTabId !== null"
@detach-tab="detachTab"
:executable-sql="executableSql"
:can-preview-changes="previewChangesAvailable"
:explain-mode="explainMode"
:active-output-view="activeOutputView"
:format-sql-request="formatSqlRequest"
:compress-sql-request="compressSqlRequest"
:selected-sql="selectedSql"
:cursor-pos="cursorPos"
:block-dangerous-redis-commands="blockDangerousRedisCommands"
:sql-keyword-case="settingsStore.editorSettings.sqlFormatter.keywordCase"
:database-required-signal="databaseRequiredTabId === activeTab.id ? databaseRequiredSignal : 0"
:auto-commit="activeTab.autoCommit ?? true"
:txn-session-id="activeTab?.txnSessionId"
:txn-auto-rolled-back="activeTab?.txnAutoRolledBack"
:oracle-txn-possibly-dirty="activeTab?.oracleTxnPossiblyDirty"
:is-oracle-manual-transaction="isOracleManualTransaction"
@update:explain-mode="(m: 'explain' | 'autotrace') => (explainMode = m)"
@update:block-dangerous-redis-commands="(v: boolean) => (blockDangerousRedisCommands = v)"
@update:auto-commit="
(v: boolean) => {
if (activeTab) queryStore.setAutoCommit(activeTab.id, v);
:zen-mode="isZenMode"
@update:active-output-view="
(tabId: string, view: 'result' | 'summary' | 'explain' | 'chart' | 'messages') => {
if (tabId === queryStore.activeTabId) activeOutputView = view;
}
"
@commit="activeTab && queryStore.commitTransaction(activeTab.id)"
@rollback="activeTab && queryStore.rollbackTransaction(activeTab.id)"
@dismiss-txn-rolled-back="activeTab && (activeTab.txnAutoRolledBack = false)"
@execute-pointer-down="captureActiveEditorExecutionSnapshot()"
@execute="requestActiveEditorExecute($event)"
@preview-changes="requestActiveEditorPreviewChanges()"
@multi-execute="requestMultiDbExecute()"
@cancel="cancelActiveExecution()"
@explain="tryExplain()"
@format-sql="formatActiveSql"
@compress-sql="compressActiveSql"
@toggle-sql-keyword-case="toggleSqlKeywordCase"
@save-sql="void openSaveSqlDialog()"
@open-sql="openSqlFile"
@import-result-archive="importResultArchive"
@paste-sql-in-condition="pasteClipboardAsSqlInCondition"
@change-connection="changeActiveConnection"
@change-database="changeActiveDatabase"
@change-catalog="changeActiveCatalog"
@change-schema="changeActiveSchema"
@set-default-database="setActiveDatabaseAsDefault"
@clear-default-database="clearActiveDefaultDatabase"
@preview-changes-available="
(tabId: string, value: boolean) => {
if (tabId === queryStore.activeTabId) previewChangesAvailable = value;
}
"
@fix-with-ai="(_tabId: string, message: string) => fixWithAi(message)"
@send-selection-to-ai="
(tabId: string, sql: string) => {
if (tabId === queryStore.activeTabId) sendSelectionToAi(sql);
}
"
@execute="(tabId: string, override?: SqlExecutionOverride) => tryExecute(override, { tabId })"
@execute-in-new-result-tab="(tabId: string, override?: SqlExecutionOverride) => tryExecuteInNewResultTab(override, { tabId })"
@cancel="(tabId: string) => cancelActiveExecution(tabId)"
@explain="(tabId: string) => tryExplain(undefined, { tabId })"
@editor-update="(tabId: string, v: string) => queryStore.updateSql(tabId, v)"
@editor-selection-change="
(tabId: string, v: string) => {
if (tabId === queryStore.activeTabId) selectedSql = v;
}
"
@editor-cursor-change="
(tabId: string, p: number) => {
if (tabId === queryStore.activeTabId) cursorPos = p;
}
"
@editor-viewport-change="(tabId: string, viewport: { scrollTop: number; scrollLeft: number }) => queryStore.updateEditorViewport(tabId, viewport)"
@editor-selection-state-change="(tabId: string, selection: { anchor: number; head: number }) => queryStore.updateEditorSelection(tabId, selection)"
@format-error="toast(t('toolbar.formatSqlFailed'))"
@save-sql="(tabId: string) => void openSaveSqlDialog(tabId)"
@reload="(tabId: string, sql: any, searchText: any, whereInput: any, orderBy: any, limit: any, offset: any, intent: any) => onReloadData(tabId, sql, searchText, whereInput, orderBy, limit, offset, intent)"
@paginate="(tabId: string, offset: number, limit: number, whereInput?: string, orderBy?: string) => onPaginate(tabId, offset, limit, whereInput, orderBy)"
@sort="(tabId: string, column: string, columnIndex: number, direction: 'asc' | 'desc' | null, whereInput?: string, mode?: DataGridSortMode) => onSort(tabId, column, columnIndex, direction, whereInput, mode)"
@execute-sql="(tabId: string, sql: string) => onExecuteSql(tabId, sql)"
@click-table="(_tabId: string, target: SqlObjectNavigationTarget) => onClickTable(target)"
@view-table-data="(_tabId: string, target: SqlObjectNavigationTarget) => onViewTableData(target)"
@edit-table-structure="(_tabId: string, target: SqlObjectNavigationTarget) => onEditTableStructure(target)"
@view-table-ddl="(_tabId: string, target: SqlObjectNavigationTarget) => onViewTableDdl(target)"
@open-object-source="(_tabId: string, target: SqlObjectNavigationTarget, initialEditing: boolean) => onOpenObjectSource(target, initialEditing)"
@open-object-table="
(tabId: string, target: { tableName: string; schema?: string; tableType?: string; catalog?: string }) => {
const tab = queryStore.tabs.find((candidate) => candidate.id === tabId) ?? activeTab;
if (!tab) return;
openObjectBrowserTableTarget({
connectionId: tab.connectionId,
database: tab.database,
schema: target.schema,
catalog: target.catalog,
tableName: target.tableName,
tableType: target.tableType,
});
}
"
@object-schema-change="(tabId: string, schema: string | undefined) => queryStore.updateSchema(tabId, schema)"
@object-browser-viewport-change="(tabId: string, viewport: any) => queryStore.updateObjectBrowserViewport(tabId, viewport)"
@structure-editor-saved="
(tabId: string, commentChanged: boolean) => {
const tab = queryStore.tabs.find((candidate) => candidate.id === tabId);
if (!tab) return;
onStructureEditorSaved(
async () => {
await onReloadData(tabId);
},
toast,
{
connectionId: tab.connectionId,
database: tab.database,
schema: tab.schema,
catalog: tab.catalog,
tableName: tab.structureTableName || '',
},
commentChanged,
);
}
"
@structure-editor-close="(tabId: string) => queryStore.closeTab(tabId)"
@open-settings="openSettings"
@open-connection-settings="openConnectionSettings"
/>
<KeepAlive :max="4">
<ContentArea
ref="contentAreaRef"
:key="activeTab.id"
:active-tab="activeTab"
:active-connection="activeConnection"
:executable-sql="executableSql"
:active-output-view="activeOutputView"
:format-sql-request="formatSqlRequest"
:compress-sql-request="compressSqlRequest"
:selected-sql="selectedSql"
:cursor-pos="cursorPos"
:block-dangerous-redis-commands="blockDangerousRedisCommands"
:zen-mode="isZenMode"
@update:active-output-view="activeOutputView = $event"
@fix-with-ai="fixWithAi"
@send-selection-to-ai="sendSelectionToAi"
@execute="tryExecute($event)"
@execute-in-new-result-tab="tryExecuteInNewResultTab($event)"
@cancel="cancelActiveExecution()"
@explain="tryExplain()"
@editor-update="(tabId: string, v: string) => queryStore.updateSql(tabId, v)"
@editor-selection-change="(v: string) => (selectedSql = v)"
@editor-cursor-change="(p: number) => (cursorPos = p)"
@preview-changes-available="(v: boolean) => (previewChangesAvailable = v)"
@editor-viewport-change="(tabId: string, viewport: { scrollTop: number; scrollLeft: number }) => queryStore.updateEditorViewport(tabId, viewport)"
@editor-selection-state-change="(tabId: string, selection: { anchor: number; head: number }) => queryStore.updateEditorSelection(tabId, selection)"
@format-error="toast(t('toolbar.formatSqlFailed'))"
@save-sql="void openSaveSqlDialog()"
@reload="(sql, searchText, whereInput, orderBy, limit, offset, intent) => onReloadData(sql, searchText, whereInput, orderBy, limit, offset, intent)"
@paginate="onPaginate"
@sort="onSort"
@execute-sql="onExecuteSql"
@click-table="onClickTable"
@view-table-data="onViewTableData"
@edit-table-structure="onEditTableStructure"
@view-table-ddl="onViewTableDdl"
@open-object-source="onOpenObjectSource"
@open-object-table="
(target) =>
activeTab &&
openObjectBrowserTableTarget({
connectionId: activeTab.connectionId,
database: activeTab.database,
schema: target.schema,
catalog: target.catalog,
tableName: target.tableName,
tableType: target.tableType,
})
"
@object-schema-change="(schema) => activeTab && queryStore.updateSchema(activeTab.id, schema)"
@object-browser-viewport-change="(tabId, viewport) => queryStore.updateObjectBrowserViewport(tabId, viewport)"
@structure-editor-saved="
(commentChanged) =>
activeTab &&
onStructureEditorSaved(
onReloadData,
toast,
{
connectionId: activeTab.connectionId,
database: activeTab.database,
schema: activeTab.schema,
catalog: activeTab.catalog,
tableName: activeTab.structureTableName || '',
},
commentChanged,
)
"
@structure-editor-close="activeTab && queryStore.closeTab(activeTab.id)"
@open-settings="openSettings"
@open-connection-settings="openConnectionSettings"
@toggle-zen-mode="toggleZenMode"
/>
</KeepAlive>
</div>
<WelcomeScreen
v-else-if="!driverStoreActive && !settingsStore.settingsPageActive"
v-else-if="queryStore.tabs.length === 0 && !driverStoreActive && !settingsStore.settingsPageActive"
:connection-stats="connectionStats"
:recent-connections="recentConnections"
:saved-sql-history-items="savedSqlHistoryItems"
@@ -3768,6 +3794,7 @@ onUnmounted(() => {
showSaveSqlDialog = open;
if (!open) {
invalidateSaveSqlFolderSelection();
saveSqlDialogTabId = null;
if (pendingSaveAndCloseTabId) cancelPendingSaveAndClose();
}
}