From 5a3a6e2bb0fcceddcfaf9dc6af094b767f380fd9 Mon Sep 17 00:00:00 2001 From: Lan-zk <68137514+Lan-zk@users.noreply.github.com> Date: Fri, 4 Sep 2026 14:44:03 +0800 Subject: [PATCH] feat(editor): add sql editor split panes with shared results --- ...ss_6c9f190b-00b1-48fe-93f5-27ff80467195.md | 48 + apps/desktop/src/App.vue | 391 ++-- .../DocumentBrowserFieldSearch.spec.ts | 14 +- .../__tests__/EditorSearchPanel.spec.ts | 3 +- .../src/components/layout/AppTabBar.vue | 2070 +---------------- .../src/components/layout/ContentArea.vue | 260 ++- .../src/components/layout/EditorGroup.vue | 205 ++ .../components/layout/EditorGroupTabBar.vue | 1309 +++++++++++ .../src/components/layout/EditorToolbar.vue | 196 +- .../components/layout/QueryEditorSurface.vue | 49 + .../components/layout/QueryResultSurface.vue | 43 + .../components/layout/SqlEditorWorkspace.vue | 256 ++ .../layout/__tests__/AppTabBar.spec.ts | 310 +-- .../__tests__/ContentArea.dataHeader.spec.ts | 8 +- .../__tests__/ContentArea.outputView.spec.ts | 6 +- .../ContentArea.unavailableDataReload.spec.ts | 3 +- .../__tests__/EditorGroup.mount.spec.ts | 185 ++ .../EditorGroupTabBar.behavior.spec.ts | 562 +++++ .../EditorGroupTabBar.groups.spec.ts | 421 ++++ .../__tests__/EditorGroupTabBar.spec.ts | 41 + .../__tests__/EditorToolbar.mount.spec.ts | 203 ++ .../QueryEditorSurface.mount.spec.ts | 101 + .../QueryResultSurface.mount.spec.ts | 100 + .../SqlEditorWorkspace.mount.spec.ts | 461 ++++ .../__tests__/SqlEditorWorkspace.spec.ts | 102 + .../appContentSurfaces.contract.spec.ts | 40 + .../toolbarOverflow.contract.spec.ts | 44 + .../src/components/layout/appTabBar.css | 531 +++++ .../components/layout/editorToolbarActions.ts | 74 + .../src/components/layout/querySurfaces.ts | 98 + .../components/layout/sqlEditorWorkspace.css | 87 + .../components/objects/DatabaseBrowser.vue | 51 +- .../src/components/objects/ObjectBrowser.vue | 74 +- .../src/components/ui/ToolbarOverflowMenu.vue | 40 + .../__tests__/useDataGridActions.spec.ts | 56 +- .../__tests__/useSqlExecution.spec.ts | 26 +- .../composables/__tests__/useTabDrag.spec.ts | 233 -- .../__tests__/useToolbarOverflow.spec.ts | 133 ++ .../src/composables/useDataGridActions.ts | 31 +- .../src/composables/useDataGridSelection.ts | 6 +- .../src/composables/useSqlExecution.ts | 270 ++- apps/desktop/src/composables/useTabDrag.ts | 245 -- .../src/composables/useToolbarOverflow.ts | 87 + apps/desktop/src/i18n/locales/en.ts | 6 + apps/desktop/src/i18n/locales/es.ts | 7 + apps/desktop/src/i18n/locales/it.ts | 7 + apps/desktop/src/i18n/locales/ja.ts | 7 + apps/desktop/src/i18n/locales/ko.ts | 7 + apps/desktop/src/i18n/locales/pt-BR.ts | 7 + apps/desktop/src/i18n/locales/tr.ts | 6 + apps/desktop/src/i18n/locales/zh-CN.ts | 6 + apps/desktop/src/i18n/locales/zh-TW.ts | 7 + .../editor/queryEditorExecution.spec.ts | 18 +- .../__tests__/editor/queryEditorFocus.spec.ts | 2 +- .../editor/toolbarExplainRouting.spec.ts | 19 + .../tabs/editorToolbarLayout.spec.ts | 40 + .../src/lib/__tests__/tabs/tabDrag.spec.ts | 23 + .../src/lib/__tests__/tabs/tabMenu.spec.ts | 135 ++ .../__tests__/tabs/tabPresentation.spec.ts | 39 + .../__tests__/aiRedisConsoleRouting.spec.ts | 2 +- .../src/lib/app/openTabsPersistence.ts | 17 + apps/desktop/src/lib/backend/http.ts | 10 +- apps/desktop/src/lib/backend/tauri.ts | 6 +- .../src/lib/tabs/contentSurfaceEvents.ts | 68 + .../src/lib/tabs/editorToolbarLayout.ts | 60 + apps/desktop/src/lib/tabs/tabDrag.ts | 28 + apps/desktop/src/lib/tabs/tabMenu.ts | 82 + apps/desktop/src/lib/tabs/tabPresentation.ts | 61 +- .../__tests__/queryStore.detached.spec.ts | 111 + .../__tests__/queryStore.groups.spec.ts | 175 ++ .../__tests__/queryStore.splitRight.spec.ts | 87 + apps/desktop/src/stores/queryStore.ts | 802 ++++++- .../app-tests/dataTabHeaderConnection.test.ts | 4 +- packages/app-tests/queryMessagesView.test.ts | 2 +- packages/app-tests/queryResultToolbar.test.ts | 2 +- packages/app-tests/useDataGridActions.test.ts | 14 +- 76 files changed, 7997 insertions(+), 3343 deletions(-) create mode 100644 .zcode/plans/plan-sess_6c9f190b-00b1-48fe-93f5-27ff80467195.md create mode 100644 apps/desktop/src/components/layout/EditorGroup.vue create mode 100644 apps/desktop/src/components/layout/EditorGroupTabBar.vue create mode 100644 apps/desktop/src/components/layout/QueryEditorSurface.vue create mode 100644 apps/desktop/src/components/layout/QueryResultSurface.vue create mode 100644 apps/desktop/src/components/layout/SqlEditorWorkspace.vue create mode 100644 apps/desktop/src/components/layout/__tests__/EditorGroup.mount.spec.ts create mode 100644 apps/desktop/src/components/layout/__tests__/EditorGroupTabBar.behavior.spec.ts create mode 100644 apps/desktop/src/components/layout/__tests__/EditorGroupTabBar.groups.spec.ts create mode 100644 apps/desktop/src/components/layout/__tests__/EditorGroupTabBar.spec.ts create mode 100644 apps/desktop/src/components/layout/__tests__/EditorToolbar.mount.spec.ts create mode 100644 apps/desktop/src/components/layout/__tests__/QueryEditorSurface.mount.spec.ts create mode 100644 apps/desktop/src/components/layout/__tests__/QueryResultSurface.mount.spec.ts create mode 100644 apps/desktop/src/components/layout/__tests__/SqlEditorWorkspace.mount.spec.ts create mode 100644 apps/desktop/src/components/layout/__tests__/SqlEditorWorkspace.spec.ts create mode 100644 apps/desktop/src/components/layout/__tests__/appContentSurfaces.contract.spec.ts create mode 100644 apps/desktop/src/components/layout/__tests__/toolbarOverflow.contract.spec.ts create mode 100644 apps/desktop/src/components/layout/appTabBar.css create mode 100644 apps/desktop/src/components/layout/editorToolbarActions.ts create mode 100644 apps/desktop/src/components/layout/querySurfaces.ts create mode 100644 apps/desktop/src/components/layout/sqlEditorWorkspace.css create mode 100644 apps/desktop/src/components/ui/ToolbarOverflowMenu.vue delete mode 100644 apps/desktop/src/composables/__tests__/useTabDrag.spec.ts create mode 100644 apps/desktop/src/composables/__tests__/useToolbarOverflow.spec.ts delete mode 100644 apps/desktop/src/composables/useTabDrag.ts create mode 100644 apps/desktop/src/composables/useToolbarOverflow.ts create mode 100644 apps/desktop/src/lib/__tests__/editor/toolbarExplainRouting.spec.ts create mode 100644 apps/desktop/src/lib/__tests__/tabs/editorToolbarLayout.spec.ts create mode 100644 apps/desktop/src/lib/__tests__/tabs/tabDrag.spec.ts create mode 100644 apps/desktop/src/lib/__tests__/tabs/tabMenu.spec.ts create mode 100644 apps/desktop/src/lib/tabs/contentSurfaceEvents.ts create mode 100644 apps/desktop/src/lib/tabs/editorToolbarLayout.ts create mode 100644 apps/desktop/src/lib/tabs/tabDrag.ts create mode 100644 apps/desktop/src/lib/tabs/tabMenu.ts create mode 100644 apps/desktop/src/stores/__tests__/queryStore.detached.spec.ts create mode 100644 apps/desktop/src/stores/__tests__/queryStore.groups.spec.ts create mode 100644 apps/desktop/src/stores/__tests__/queryStore.splitRight.spec.ts diff --git a/.zcode/plans/plan-sess_6c9f190b-00b1-48fe-93f5-27ff80467195.md b/.zcode/plans/plan-sess_6c9f190b-00b1-48fe-93f5-27ff80467195.md new file mode 100644 index 0000000000..09ab5e75b4 --- /dev/null +++ b/.zcode/plans/plan-sess_6c9f190b-00b1-48fe-93f5-27ff80467195.md @@ -0,0 +1,48 @@ +# feat/sql-editor-split 分支 PR 前完整 Review 与修复计划 + +## Review 结论(基于两路并行审查,所有发现均已在源码逐行核实) + +分支整体架构健康:两轮评审(第一轮 R1-R12、第二轮 SP/ST 编号)的修复痕迹全部可确认(registerOpenTab 原子化、事件契约 33 个事件全带 tabId、appTabBar.css 共享样式、恢复顺序修正、拖拽 owner 校验、四组 disabled、AppTabBar 单生产路径)。**发现 1 个高优先级功能 bug、4 个中优先级缺陷、若干低优先级问题,全部有明确修复方案。** + +## 审查发现(按严重度) + +### 🔴 P0-1 工具栏 EXPLAIN 按钮把 tab UUID 当 SQL 执行(已核实:App.vue:503) +`explain: (tabId: string) => tryExplain(tabId)` — `tryExplain` 第一参数是 `sqlOverride: string | snapshot`,tabId 类型合法地滑过了编译器,点击任何一组的"执行计划"按钮会把 UUID 当 SQL 去 EXPLAIN,必然后端报错。对照正确写法已存在于 App.vue:3140(`tryExplain(undefined, { tabId })`)。**修复:改为 `tryExplain(undefined, { tabId })`,并补一条 EditorGroup 挂载测试接真实 actions 断言 explain 调用形状(现有测试 mock 了 actions 只断言收到 tabId,是盲区)。** + +### 🟡 P1-1 showDirtyTabCloseConfirm 未同步群组(queryStore.ts:2937) +脏 tab 关闭确认直接写 `activeTabId.value = tab.id`,不切 focusedGroupId、不改组 active — 确认期间共享结果区跟随脏 tab 而组编辑器显示另一个 tab;确认关闭后焦点跳回原组而非脏 tab 所在组。**修复:改为 `activateTab(tab.id)`。** + +### 🟡 P1-2 closeTab 空组剪除不修 sizes(queryStore.ts:3188-3194) +对比 removeTabsFromGroups/moveTabToGroup 都走 pruneEmptyGroups→repairGroupSizes,closeTab 的内联剪除漏了 repairGroupSizes:2 组 [50,50] 关掉一组最后一个 tab 后 sizes 仍 [50,50],单 Pane 可能只占半宽直到手动拖动或重启。**修复:剪除后调 `repairGroupSizes()`(或将内联逻辑改调 pruneEmptyGroups)。** + +### 🟡 P1-3 他组卸载会杀死进行中的拖拽(EditorGroupTabBar.vue:507/408) +模块级共享拖拽会话,但每个实例的 `onUnmounted(cleanupTabDrag)` 无条件清空共享状态 — 拖拽进行中用 Ctrl+W 关掉另一组最后一个 tab,该组卸载会把源栏的 ghost/状态一起清掉。**修复:cleanupTabDrag 增加 `groupTabDrag.sourceGroupId === props.groupId` 守卫(仅拖拽源实例清共享态),监听移除仍无条件执行。** + +### 🟡 P1-4 AI 自动执行 danger 分支缺 pendingDangerTabId(App.vue:2333-2335) +dangerSql 直接置值打开对话框但未设 `pendingDangerTabId`(本分支新增机制),确认时 `onDangerConfirm` 回退到"确认时刻"的全局 active tab;模态期间 Ctrl+数字仍可切 tab,存在目标漂移窗口 — 与同函数 :2330 已正确传 tabId 的 auto_execute 分支不一致。**修复:补 `pendingDangerSql.value = sql` 同处置 `pendingDangerTabId` 的途径 — 由于该状态在 useSqlExecution 内部,方案为给 App 暴露的路径统一改走 `tryExecute(sql, { tabId })`(由它统一设置 pending 态),删除手写的 dangerSql/pendingDangerSql/showDangerDialog 三行。** + +### 🟢 低优先级(一并修) +- **L1** EditorToolbar.vue:89 残留 `[TIER-DBG]` console.log(全分支唯一新增调试日志,违反指南阶段6)→ 删除。 +- **L2** querySurfaces.ts:13/:15 `focusSearch()`/`refreshData()` 接口声明缺 target 参数(实现已是 `(target: Element | null)`,契约漂移)→ 补参数声明。 +- **L3** EditorGroup.vue:126 模板内联单行 if 花括号(指南 §2.6 边缘项)→ 加花括号。 +- **L4** 双指针并发拖拽会话覆盖(EditorGroupTabBar)→ pointerdown 增加"已有活跃会话则忽略"守卫,一行。 + +### 已确认无需修复(审查中排除的疑点) +- 收敛机制无无限循环(迟滞单调性可证);watch getter 依赖追踪正确;快捷键路由三处签名全一致;持久化 payload 类型与两 adapter 完全一致;legacy tabs-only 恢复、pinned 过滤后组修复、editorOnly 守卫全部 9 处 emit 点核查通过;跨栏拖拽 click 抑制依赖 DOM click 公共祖先语义,设计正确。 +- reorderTab 无生产调用方(仅 app-tests 使用):保留不改,PR 描述中注明。 + +## 执行步骤 + +1. **P0-1**:App.vue:503 改 `tryExplain(undefined, { tabId })`;在 EditorGroup.mount.spec.ts 增加接真实 editorToolbarActions 注入(非 vi.fn 空壳)断言 explain 链路调用形状的用例。 +2. **P1-1**:queryStore showDirtyTabCloseConfirm 改 `activateTab(tab.id)`。 +3. **P1-2**:closeTab 空组剪除后调 `repairGroupSizes()`。 +4. **P1-3**:cleanupTabDrag 加源实例守卫;handleTabPointerDown 加活跃会话守卫(兼修 L4)。 +5. **P1-4**:AI danger 分支改走 `tryExecute(sql, { tabId })` 统一 pending 态管理。 +6. **L1-L3**:删调试日志、补接口参数、模板花括号。 +7. **验证**:每步跑定向 vitest;全部完成后跑 `pnpm typecheck` + `pnpm lint` + `pnpm build` + `git diff --check` + 全量 `pnpm test -- --maxWorkers=4`(维持本分支 1224 文件/11980 用例全绿的基线)。 +8. **输出**:修复完成后给出对照本清单的逐项核销报告;PR 描述建议中注明遗留项(ST1 双 ContentArea 架构灰区、GUI 验收未执行,均已在评审文档记录)。 + +## 不做的事 +- 不做 ST1 ContentArea 物理拆分(1500+ 行搬移,属独立工作项,评审文档已记录为遗留)。 +- 不动 reorderTab 死代码(有测试引用,PR 注明即可)。 +- 不新增功能;纯修复本 review 清单。 \ No newline at end of file diff --git a/apps/desktop/src/App.vue b/apps/desktop/src/App.vue index 154797f94b..95cd999d1d 100644 --- a/apps/desktop/src/App.vue +++ b/apps/desktop/src/App.vue @@ -1,13 +1,13 @@ - - diff --git a/apps/desktop/src/components/layout/ContentArea.vue b/apps/desktop/src/components/layout/ContentArea.vue index d1cff8e280..242113bbf1 100644 --- a/apps/desktop/src/components/layout/ContentArea.vue +++ b/apps/desktop/src/components/layout/ContentArea.vue @@ -121,6 +121,7 @@ const ExplainPlanViewer = defineAsyncComponent(() => import("@/components/explai const QueryChart = defineAsyncComponent(() => import("@/components/chart/QueryChart.vue")); import { useQueryStore } from "@/stores/queryStore"; import { useConnectionStore } from "@/stores/connectionStore"; +import type { ContentAreaSurfaceEmits, ContentAreaSurfaceProps } from "@/components/layout/querySurfaces"; import { TABLE_FONT_SIZE_MAX, TABLE_FONT_SIZE_MIN, useSettingsStore, type DataGridSearchMode, type ResultRunDisplayMode } from "@/stores/settingsStore"; import { useToast } from "@/composables/useToast"; import { isTauriRuntime } from "@/lib/backend/tauriRuntime"; @@ -151,14 +152,15 @@ import { chartableColumnIndexes } from "@/lib/dataGrid/chartData"; import { elasticsearchJsonResponseForResult } from "@/lib/elasticsearch/elasticsearchJsonResponse"; import * as api from "@/lib/backend/api"; import { applyMongoGridChangesToDocument, applyMongoGridChangesToDocumentBaseline, buildMongoUpdateDocument, formatMongoShellLiteral, serializeMongoDocumentId, type MongoInputValue } from "@/lib/mongo/mongoDocumentValues"; -import type { SqlExecutionOverride } from "@/lib/sql/sqlExecutionTarget"; import type { DataGridSortMode } from "@/lib/dataGrid/dataGridSort"; import { isDataGridToolbarCompact, type DataGridReloadIntent } from "@/lib/dataGrid/dataGridToolbar"; import { useTabScroll } from "@/composables/useTabScroll"; +import { useToolbarOverflow } from "@/composables/useToolbarOverflow"; +import ToolbarOverflowMenu from "@/components/ui/ToolbarOverflowMenu.vue"; import { formatElapsedSeconds } from "@/lib/common/elapsedTime"; import { copyToClipboard } from "@/lib/common/clipboard"; import type { CustomSaveHandler } from "@/composables/useDataGridEditor"; -import type { QueryTab, ConnectionConfig, TableInfoTab, TreeNode, VectorCollectionMeta, ObjectBrowserViewport } from "@/types/database"; +import type { QueryTab, TableInfoTab, TreeNode, VectorCollectionMeta } from "@/types/database"; import type { SqlObjectNavigationTarget } from "@/lib/sql/sqlNavigation"; import { sqlFormatDialectForDbType, type SqlFormatDialect } from "@/lib/sql/sqlFormatter"; import { productionContextForDatabase } from "@/lib/database/productionSafety"; @@ -199,53 +201,18 @@ type ElasticsearchJsonResponsePanelHandle = { focusSearch: () => boolean; }; -const props = defineProps<{ - activeTab: QueryTab; - activeConnection?: ConnectionConfig; - executableSql: string; - activeOutputView: "result" | "summary" | "explain" | "chart" | "messages"; - formatSqlRequest: { id: number; tabId: string } | null; - compressSqlRequest: { id: number; tabId: string } | null; - selectedSql: string; - cursorPos: number; - blockDangerousRedisCommands: boolean; - zenMode?: boolean; -}>(); +const props = defineProps< + ContentAreaSurfaceProps & { + /** Render only the query editor pane (used by QueryEditorSurface). */ + editorOnly?: boolean; + /** Render only the shared query result pane (used by QueryResultSurface). */ + resultOnly?: boolean; + /** Whether the query editor should request focus on mount. */ + autoFocus?: boolean; + } +>(); -const emit = defineEmits<{ - "update:activeOutputView": [value: "result" | "summary" | "explain" | "chart" | "messages"]; - fixWithAi: [errorMessage: string]; - sendSelectionToAi: [sql: string]; - execute: [sqlOverride?: SqlExecutionOverride]; - executeInNewResultTab: [sqlOverride?: SqlExecutionOverride]; - saveSql: []; - cancel: []; - explain: []; - editorUpdate: [tabId: string, value: string]; - editorSelectionChange: [value: string]; - editorCursorChange: [pos: number]; - previewChangesAvailable: [value: boolean]; - editorViewportChange: [tabId: string, viewport: { scrollTop: number; scrollLeft: number }]; - editorSelectionStateChange: [tabId: string, selection: { anchor: number; head: number }]; - formatError: []; - reload: [sql?: string, searchText?: string, whereInput?: string, orderBy?: string, limit?: number, offset?: number, intent?: DataGridReloadIntent]; - paginate: [offset: number, limit: number, whereInput?: string, orderBy?: string]; - sort: [column: string, columnIndex: number, direction: "asc" | "desc" | null, whereInput?: string, mode?: DataGridSortMode]; - executeSql: [sql: string]; - clickTable: [target: SqlObjectNavigationTarget]; - viewTableData: [target: SqlObjectNavigationTarget]; - viewTableDdl: [target: SqlObjectNavigationTarget]; - editTableStructure: [target: SqlObjectNavigationTarget]; - openObjectSource: [target: SqlObjectNavigationTarget, initialEditing: boolean]; - openObjectTable: [target: { tableName: string; schema?: string; tableType?: string; catalog?: string }]; - objectSchemaChange: [schema: string | undefined]; - objectBrowserViewportChange: [tabId: string, viewport: ObjectBrowserViewport]; - structureEditorSaved: [commentChanged: boolean]; - structureEditorClose: []; - openSettings: [initialTab?: string, initialSection?: string]; - openConnectionSettings: [connectionId: string, initialTab: "advanced"]; - toggleZenMode: []; -}>(); +const emit = defineEmits(); const { t, locale } = useI18n(); const queryStore = useQueryStore(); @@ -300,6 +267,16 @@ const standaloneResultToolbarWidth = ref(0); const standaloneResultToolbarViewportWidth = ref(0); const resultTabsScrollerRef = ref(null); const dataGridViewOptionsOpen = ref(false); +// Data-mode header condensation (same measured-tier mechanism as the SQL +// editor toolbar). The chips keep a min-width floor so scrollWidth reports +// real overflow; tiers then drop button labels and move secondary actions +// into the overflow menu instead of crushing the chips away. +const dataToolbarRef = ref(null); +const { tier: dataToolbarTier } = useToolbarOverflow(dataToolbarRef, [() => props.activeTab.id, () => !!props.activeTab.result]); +const dataToolbarCompact = computed(() => dataToolbarTier.value >= 1); +const showDataToolbarOverflow = computed(() => dataToolbarTier.value >= 2); +const showDataTableInfoButton = computed(() => dataToolbarTier.value < 2); +const showDataColumnsChip = computed(() => dataToolbarTier.value < 2); const dataGridRenderMode = computed(() => settingsStore.editorSettings.dataGridRenderMode); const dataGridSearchMode = computed(() => settingsStore.editorSettings.dataGridSearchMode); const resultRunDisplayMode = computed(() => settingsStore.editorSettings.resultRunDisplayMode); @@ -728,10 +705,14 @@ watch( watch( () => [props.activeTab.id, props.activeTab.result, props.activeTab.results, props.activeTab.isExecuting, props.activeOutputView] as const, () => { + // The output view belongs to the shared result surface. An editor-only + // surface (a background group's editor) must never redirect the global + // view when its own tab finishes executing. + if (props.editorOnly) return; if (props.activeTab.isExecuting) return; if (hasExecutionSummary.value && !hasTabularResult.value && props.activeOutputView === "result") { const result = props.activeTab.result; - emit("update:activeOutputView", result ? defaultViewForResult(result) : "summary"); + emit("update:activeOutputView", props.activeTab.id, result ? defaultViewForResult(result) : "summary"); } }, { immediate: true }, @@ -869,23 +850,23 @@ function closeColumnInfo() { } function onHandleClickTable(target: SqlObjectNavigationTarget) { - emit("clickTable", target); + emit("clickTable", props.activeTab.id, target); } function onHandleViewTableData(target: SqlObjectNavigationTarget) { - emit("viewTableData", target); + emit("viewTableData", props.activeTab.id, target); } function onHandleViewTableDdl(target: SqlObjectNavigationTarget) { - emit("viewTableDdl", target); + emit("viewTableDdl", props.activeTab.id, target); } function onHandleEditTableStructure(target: SqlObjectNavigationTarget) { - emit("editTableStructure", target); + emit("editTableStructure", props.activeTab.id, target); } function onHandleOpenObjectSource(target: SqlObjectNavigationTarget, initialEditing: boolean) { - emit("openObjectSource", target, initialEditing); + emit("openObjectSource", props.activeTab.id, target, initialEditing); } function onHandleCloseColumnPanel() { @@ -903,7 +884,12 @@ function focusSearch(): boolean { if (props.activeTab.mode === "consul") return consulWorkspaceRef.value?.focusSearch() ?? false; if (props.activeTab.mode === "databases") return databaseBrowserRef.value?.focusSearch() ?? false; if (props.activeTab.mode === "objects") return objectBrowserRef.value?.focusSearch() ?? false; - if (props.activeTab.mode === "query") return queryEditorRef.value?.openSearch() ?? false; + if (props.activeTab.mode === "query") { + // The shared result surface (resultOnly) owns the grid, not the editor; + // route its search to the DataGrid instead of the missing QueryEditor. + if (props.resultOnly) return dataGridRef.value?.focusSearch() ?? false; + return queryEditorRef.value?.openSearch() ?? false; + } return dataGridRef.value?.focusSearch() ?? false; } @@ -920,7 +906,7 @@ function refreshQueryEditorCompletionCache(): boolean { function reloadUnavailableDataTab() { const { whereInput, orderBy } = restoredDataTabReloadFilters(props.activeTab); - emit("reload", undefined, undefined, whereInput, orderBy); + emit("reload", props.activeTab.id, undefined, whereInput, orderBy); } function refreshData(): boolean { @@ -940,7 +926,7 @@ function refreshData(): boolean { if (activeElasticsearchJsonResponse.value) { // Match DataGrid's toolbar refresh intent so multi-result runs are // refreshed as a group instead of replacing them with the active result. - emit("reload", activeResultSql.value, undefined, undefined, undefined, undefined, undefined, "refresh"); + emit("reload", props.activeTab.id, activeResultSql.value, undefined, undefined, undefined, undefined, undefined, "refresh"); return true; } if (!dataGridRef.value) return false; @@ -976,7 +962,7 @@ async function removeResultRun(runId: string) { const removedActiveRun = props.activeTab.activeResultRunId === runId; const removed = await queryStore.removeResultRun(props.activeTab.id, runId); if (!removed) return; - if (removedActiveRun) emit("update:activeOutputView", "result"); + if (removedActiveRun) emit("update:activeOutputView", props.activeTab.id, "result"); await nextTick(); const activeRunTab = resultTabsScrollerRef.value?.querySelector('[data-active-result-run="true"]'); activeRunTab?.focus({ preventScroll: true }); @@ -1040,7 +1026,7 @@ function resultRunContextMenuItems(run: (typeof resultRuns.value)[number]): Cont async function closeCurrentQueryResult() { if (!(await queryStore.closeQueryResult(props.activeTab.id))) return; - emit("update:activeOutputView", "result"); + emit("update:activeOutputView", props.activeTab.id, "result"); } async function selectResultRun(runId: string) { @@ -1048,7 +1034,7 @@ async function selectResultRun(runId: string) { toast(t("tabs.missingResultRun"), 4000); return false; } - emit("update:activeOutputView", "result"); + emit("update:activeOutputView", props.activeTab.id, "result"); return true; } @@ -1082,10 +1068,13 @@ function toggleResultAutoSave() { function selectResultItem(item: (typeof visibleResultItems.value)[number]) { queryStore.setActiveResultIndex(props.activeTab.id, item.index); - emit("update:activeOutputView", "result"); - nextTick(() => { - queryEditorRef.value?.previewStatementRange(resultSourceRange(props.activeTab.sql, item.result, item.index, activeEffectiveDatabaseType.value) ?? null); - }); + emit("update:activeOutputView", props.activeTab.id, "result"); + const range = resultSourceRange(props.activeTab.sql, item.result, item.index, activeEffectiveDatabaseType.value) ?? null; + if (queryEditorRef.value) { + nextTick(() => queryEditorRef.value?.previewStatementRange(range)); + } else { + emit("previewStatement", props.activeTab.id, range); + } } function executionSummaryItemRange(item: ExecutionSummaryItem) { @@ -1096,11 +1085,21 @@ function executionSummaryItemRange(item: ExecutionSummaryItem) { } function previewExecutionSummaryItem(item: ExecutionSummaryItem) { - queryEditorRef.value?.previewStatementRange(executionSummaryItemRange(item) ?? null); + const range = executionSummaryItemRange(item) ?? null; + if (queryEditorRef.value) { + queryEditorRef.value?.previewStatementRange(range); + } else { + emit("previewStatement", props.activeTab.id, range); + } } function focusExecutionSummaryItem(item: ExecutionSummaryItem) { - queryEditorRef.value?.focusStatementRange(executionSummaryItemRange(item) ?? null); + const range = executionSummaryItemRange(item) ?? null; + if (queryEditorRef.value) { + queryEditorRef.value?.focusStatementRange(range); + } else { + emit("focusStatement", props.activeTab.id, range); + } } async function copyExecutionSummaryError(error: string) { @@ -1188,6 +1187,18 @@ async function executeRedisCommand(command: string): Promise { return (await redisKeyBrowserRef.value?.executeCommand?.(command)) ?? false; } +function previewStatementRange(range: { from: number; to: number } | null): boolean { + if (!queryEditorRef.value) return false; + queryEditorRef.value.previewStatementRange(range); + return true; +} + +function focusStatementRange(range: { from: number; to: number } | null): boolean { + if (!queryEditorRef.value) return false; + queryEditorRef.value.focusStatementRange(range); + return true; +} + defineExpose({ focusSearch, openGoToColumn, @@ -1206,11 +1217,13 @@ defineExpose({ applyTableStructureChanges, insertRedisCommand, executeRedisCommand, + previewStatementRange, + focusStatementRange, });