feat(editor): add sql editor split panes with shared results
This commit is contained in:
1 parent
24a274bd89
commit
5a3a6e2bb0
76 files changed
+7997
-3343
No files matched your search
+209
-182
@@ -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();
|
||||
}
|
||||
}
|
||||
|
||||
Reference in new issue
Block a user