From 5f81ed2e1183ab11668ad9048fbe6ca9e6d4da1b Mon Sep 17 00:00:00 2001 From: t8y2 <1156263951@qq.com> Date: Mon, 7 Sep 2026 14:10:16 +0800 Subject: [PATCH] feat(tabs): show table comments in tab tooltips Closes #8342 --- apps/desktop/src/App.vue | 3 ++- .../components/layout/EditorGroupTabBar.vue | 2 +- .../src/components/layout/querySurfaces.ts | 2 +- .../src/components/objects/ObjectBrowser.vue | 6 +++--- .../useSidebarDataOpenRuntime.spec.ts | 15 ++++++++++++--- .../src/composables/useNavigationTargets.ts | 7 ++++++- .../composables/useSidebarDataOpenRuntime.ts | 7 ++++++- apps/desktop/src/i18n/locales/en.ts | 1 + apps/desktop/src/i18n/locales/es.ts | 1 + apps/desktop/src/i18n/locales/it.ts | 1 + apps/desktop/src/i18n/locales/ja.ts | 1 + apps/desktop/src/i18n/locales/ko.ts | 1 + apps/desktop/src/i18n/locales/pt-BR.ts | 1 + apps/desktop/src/i18n/locales/tr.ts | 1 + apps/desktop/src/i18n/locales/zh-CN.ts | 1 + apps/desktop/src/i18n/locales/zh-TW.ts | 1 + .../lib/__tests__/tabs/tabPresentation.spec.ts | 18 ++++++++++++++++++ .../desktop/src/lib/app/openTabsPersistence.ts | 2 ++ apps/desktop/src/lib/tabs/tabPresentation.ts | 12 ++++++++++++ apps/desktop/src/types/database.ts | 1 + packages/app-tests/openTabsPersistence.test.ts | 2 ++ 21 files changed, 75 insertions(+), 11 deletions(-) diff --git a/apps/desktop/src/App.vue b/apps/desktop/src/App.vue index 6ff4eb920e..c8df98c56d 100644 --- a/apps/desktop/src/App.vue +++ b/apps/desktop/src/App.vue @@ -3579,7 +3579,7 @@ onUnmounted(() => { @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 }) => { + (tabId: string, target: { tableName: string; schema?: string; tableType?: string; catalog?: string; comment?: string | null }) => { const tab = queryStore.tabs.find((candidate) => candidate.id === tabId) ?? activeTab; if (!tab) return; openObjectBrowserTableTarget({ @@ -3589,6 +3589,7 @@ onUnmounted(() => { catalog: target.catalog, tableName: target.tableName, tableType: target.tableType, + comment: target.comment, }); } " diff --git a/apps/desktop/src/components/layout/EditorGroupTabBar.vue b/apps/desktop/src/components/layout/EditorGroupTabBar.vue index 61c6b9948a..7faef7ad94 100644 --- a/apps/desktop/src/components/layout/EditorGroupTabBar.vue +++ b/apps/desktop/src/components/layout/EditorGroupTabBar.vue @@ -1300,7 +1300,7 @@ watch([() => props.specialPageTabs?.settingsActive, () => props.specialPageTabs? diff --git a/apps/desktop/src/components/layout/querySurfaces.ts b/apps/desktop/src/components/layout/querySurfaces.ts index 70b3bf31cb..a1ce13ea2f 100644 --- a/apps/desktop/src/components/layout/querySurfaces.ts +++ b/apps/desktop/src/components/layout/querySurfaces.ts @@ -85,7 +85,7 @@ export interface ContentAreaSurfaceEmits { viewTableDdl: [tabId: string, target: SqlObjectNavigationTarget]; editTableStructure: [tabId: string, target: SqlObjectNavigationTarget]; openObjectSource: [tabId: string, target: SqlObjectNavigationTarget, initialEditing: boolean]; - openObjectTable: [tabId: string, target: { tableName: string; schema?: string; tableType?: string; catalog?: string }]; + openObjectTable: [tabId: string, target: { tableName: string; schema?: string; tableType?: string; catalog?: string; comment?: string | null }]; objectSchemaChange: [tabId: string, schema: string | undefined]; objectBrowserViewportChange: [tabId: string, viewport: ObjectBrowserViewport]; objectBrowserSearchChange: [tabId: string, query: string]; diff --git a/apps/desktop/src/components/objects/ObjectBrowser.vue b/apps/desktop/src/components/objects/ObjectBrowser.vue index 5877f08009..7a6121de2e 100644 --- a/apps/desktop/src/components/objects/ObjectBrowser.vue +++ b/apps/desktop/src/components/objects/ObjectBrowser.vue @@ -182,7 +182,7 @@ const props = defineProps<{ }>(); const emit = defineEmits<{ - openTable: [target: { tableName: string; schema?: string; tableType?: string; catalog?: string }]; + openTable: [target: { tableName: string; schema?: string; tableType?: string; catalog?: string; comment?: string | null }]; schemaChange: [schema: string | undefined]; viewportChange: [viewport: ObjectBrowserViewport]; searchChange: [query: string]; @@ -966,7 +966,7 @@ function executeRowAction(row: ObjectBrowserRow, action: ObjectBrowserRowAction) void openTypeInfo(row); break; case "open-table": - emit("openTable", { tableName: row.name, schema: row.schema, tableType: objectBrowserOpenTableType(row), catalog: props.catalog }); + emit("openTable", { tableName: row.name, schema: row.schema, tableType: objectBrowserOpenTableType(row), catalog: props.catalog, comment: row.comment }); break; case "open-source": void (row.type === "EVENT" ? openEventEditor(row) : openSource(row)); @@ -1736,7 +1736,7 @@ function objectBrowserOpenTableType(row: ObjectBrowserRow): string { } function openViewData(row: ObjectBrowserRow) { - emit("openTable", { tableName: row.name, schema: row.schema, tableType: objectBrowserOpenTableType(row), catalog: props.catalog }); + emit("openTable", { tableName: row.name, schema: row.schema, tableType: objectBrowserOpenTableType(row), catalog: props.catalog, comment: row.comment }); } function openStructureEditor(row: ObjectBrowserRow) { diff --git a/apps/desktop/src/composables/__tests__/useSidebarDataOpenRuntime.spec.ts b/apps/desktop/src/composables/__tests__/useSidebarDataOpenRuntime.spec.ts index a39f5889cf..81a80475b6 100644 --- a/apps/desktop/src/composables/__tests__/useSidebarDataOpenRuntime.spec.ts +++ b/apps/desktop/src/composables/__tests__/useSidebarDataOpenRuntime.spec.ts @@ -202,16 +202,24 @@ describe("useSidebarDataOpenRuntime", () => { it("reuses a sidebar tab for the same table in same-table mode", async () => { mocks.openDataTabsNextToActive = true; - await useSidebarDataOpenRuntime().openData(tableNode); - await useSidebarDataOpenRuntime().openData(tableNode); + await useSidebarDataOpenRuntime().openData({ ...tableNode, comment: "Old comment" }); + await useSidebarDataOpenRuntime().openData({ ...tableNode, comment: "Updated comment" }); expect(mocks.tabs).toHaveLength(1); + expect(mocks.tabs[0]?.tableComment).toBe("Updated comment"); + }); + + it("copies the existing sidebar table comment without another metadata request", async () => { + await useSidebarDataOpenRuntime().openData({ ...tableNode, comment: "Application users" }); + + expect(mocks.tabs[0]?.tableComment).toBe("Application users"); + expect(mocks.loadTableMetadata).toHaveBeenCalledTimes(1); }); it("keeps different sidebar tables independent in same-table mode", async () => { const ordersNode = { ...tableNode, id: "table-orders", label: "orders" }; - await useSidebarDataOpenRuntime().openData(tableNode); + await useSidebarDataOpenRuntime().openData({ ...tableNode, comment: "User table" }); await useSidebarDataOpenRuntime().openData(ordersNode); expect(mocks.tabs).toHaveLength(2); @@ -270,6 +278,7 @@ describe("useSidebarDataOpenRuntime", () => { expect(mocks.tabs).toHaveLength(1); expect(mocks.tabs[0]?.title).toBe("orders"); + expect(mocks.tabs[0]?.tableComment).toBeUndefined(); expect(mocks.tabs[0]?.tableMeta?.tableName).toBe("orders"); expect(mocks.tabs[0]?.resultLocalSortOriginalLargeValueCells).toBeUndefined(); }); diff --git a/apps/desktop/src/composables/useNavigationTargets.ts b/apps/desktop/src/composables/useNavigationTargets.ts index b799394ae2..9098a01967 100644 --- a/apps/desktop/src/composables/useNavigationTargets.ts +++ b/apps/desktop/src/composables/useNavigationTargets.ts @@ -22,6 +22,7 @@ export type NavigationTarget = { schema?: string; tableName: string; tableType?: string; + comment?: string | null; columnName?: string; whereInput?: string; }; @@ -72,7 +73,10 @@ async function openTableTarget(target: NavigationTarget, options: { tableInfoTab } const tabId = queryStore.createTab(target.connectionId, target.database, tabTitle, "data", tableSchema, undefined, undefined, { forceNew: true }); const targetTab = queryStore.tabs.find((tab) => tab.id === tabId); - if (targetTab) targetTab.tableInfoTab = options.tableInfoTab; + if (targetTab) { + targetTab.tableInfoTab = options.tableInfoTab; + targetTab.tableComment = target.comment; + } // Stamp the new table identity synchronously so SQL rebuilds (refresh, // filters, row count) never read a stale tableMeta from a reused tab or // fall back to parsing the schema-qualified tab title (issue #3613). @@ -339,6 +343,7 @@ export function useNavigationTargets(dialogs: { showFieldLineageDialog: { value: schema: target.schema, catalog: target.catalog, tableType: target.tableType, + comment: target.comment, }, undefined, "default", diff --git a/apps/desktop/src/composables/useSidebarDataOpenRuntime.ts b/apps/desktop/src/composables/useSidebarDataOpenRuntime.ts index 0c588e4bb3..4763a11993 100644 --- a/apps/desktop/src/composables/useSidebarDataOpenRuntime.ts +++ b/apps/desktop/src/composables/useSidebarDataOpenRuntime.ts @@ -173,6 +173,7 @@ export function useSidebarDataOpenRuntime() { }; if (existingSameTableTab && (existingSameTableTab.isExecuting || canActivateExistingDataTableTab(existingSameTableTab, { activateExecuting: false }))) { + if (node.comment !== undefined) existingSameTableTab.tableComment = node.comment; queryStore.switchTab(existingSameTableTab.id); logPhase("existing-tab-activated", { table: node.label }); // 代次失配视同冷缓存(即使位于 30s TTL 窗口内也要重建):disconnect / @@ -192,12 +193,16 @@ export function useSidebarDataOpenRuntime() { if (existingDataTabCandidate) { queryStore.switchTab(existingDataTabCandidate.tab.id); resetReusedDataTabState(existingDataTabCandidate.tab); + existingDataTabCandidate.tab.tableComment = node.comment; return existingDataTabCandidate.tab.id; } - return queryStore.createTab(node.connectionId, node.database, node.label, "data", tableSchema, undefined, node.catalog, { + const createdTabId = queryStore.createTab(node.connectionId, node.database, node.label, "data", tableSchema, undefined, node.catalog, { forceNew: true, insertAfterActive: settingsStore.editorSettings.openDataTabsNextToActive, }); + const createdTab = queryStore.tabs.find((tab) => tab.id === createdTabId); + if (createdTab) createdTab.tableComment = node.comment; + return createdTabId; })(); openDataLog("info", "tab-created", { traceId, tabId, elapsed: elapsed() }); logPhase("tab-created", { tabId }); diff --git a/apps/desktop/src/i18n/locales/en.ts b/apps/desktop/src/i18n/locales/en.ts index 726d97e9c7..c1a33a02fb 100644 --- a/apps/desktop/src/i18n/locales/en.ts +++ b/apps/desktop/src/i18n/locales/en.ts @@ -1456,6 +1456,7 @@ export default { tooltipGroup: "Group:", tooltipDatabase: "Database:", tooltipTable: "Table:", + tooltipTableComment: "Table Comment:", tooltipCollection: "Collection:", tooltipSchema: "Schema:", resultN: "Result {n}", diff --git a/apps/desktop/src/i18n/locales/es.ts b/apps/desktop/src/i18n/locales/es.ts index a3283e3e69..bc4ed27c07 100644 --- a/apps/desktop/src/i18n/locales/es.ts +++ b/apps/desktop/src/i18n/locales/es.ts @@ -1433,6 +1433,7 @@ export default withEnglishFallback({ tooltipGroup: "Grupo:", tooltipDatabase: "Base de datos:", tooltipTable: "Tabla:", + tooltipTableComment: "Comentario de tabla:", tooltipCollection: "Colección:", tooltipSchema: "Esquema:", resultN: "Resultado {n}", diff --git a/apps/desktop/src/i18n/locales/it.ts b/apps/desktop/src/i18n/locales/it.ts index 70156b3336..c4ec1f2bfa 100644 --- a/apps/desktop/src/i18n/locales/it.ts +++ b/apps/desktop/src/i18n/locales/it.ts @@ -1431,6 +1431,7 @@ export default withEnglishFallback({ tooltipGroup: "Gruppo:", tooltipDatabase: "Database:", tooltipTable: "Tabella:", + tooltipTableComment: "Commento tabella:", tooltipCollection: "Collezione:", tooltipSchema: "Schema:", resultN: "Risultato {n}", diff --git a/apps/desktop/src/i18n/locales/ja.ts b/apps/desktop/src/i18n/locales/ja.ts index 164559d3bc..70803f4aef 100644 --- a/apps/desktop/src/i18n/locales/ja.ts +++ b/apps/desktop/src/i18n/locales/ja.ts @@ -1450,6 +1450,7 @@ export default withEnglishFallback({ tooltipGroup: "グループ:", tooltipDatabase: "データベース:", tooltipTable: "テーブル:", + tooltipTableComment: "テーブルコメント:", tooltipCollection: "コレクション:", tooltipSchema: "スキーマ:", resultN: "結果 {n}", diff --git a/apps/desktop/src/i18n/locales/ko.ts b/apps/desktop/src/i18n/locales/ko.ts index 2ae03df8dd..8a336c2004 100644 --- a/apps/desktop/src/i18n/locales/ko.ts +++ b/apps/desktop/src/i18n/locales/ko.ts @@ -1337,6 +1337,7 @@ export default withEnglishFallback({ tooltipGroup: "그룹:", tooltipDatabase: "데이터베이스:", tooltipTable: "테이블:", + tooltipTableComment: "테이블 주석:", tooltipCollection: "컬렉션:", tooltipSchema: "스키마:", resultN: "결과 {n}", diff --git a/apps/desktop/src/i18n/locales/pt-BR.ts b/apps/desktop/src/i18n/locales/pt-BR.ts index 29c94b2713..469b706f98 100644 --- a/apps/desktop/src/i18n/locales/pt-BR.ts +++ b/apps/desktop/src/i18n/locales/pt-BR.ts @@ -1433,6 +1433,7 @@ export default withEnglishFallback({ tooltipGroup: "Grupo:", tooltipDatabase: "Banco de dados:", tooltipTable: "Tabela:", + tooltipTableComment: "Comentário da tabela:", tooltipCollection: "Coleção:", tooltipSchema: "Schema:", resultN: "Resultado {n}", diff --git a/apps/desktop/src/i18n/locales/tr.ts b/apps/desktop/src/i18n/locales/tr.ts index 8c305935cf..b129e2788f 100644 --- a/apps/desktop/src/i18n/locales/tr.ts +++ b/apps/desktop/src/i18n/locales/tr.ts @@ -1456,6 +1456,7 @@ export default withEnglishFallback({ tooltipGroup: "Grup:", tooltipDatabase: "Veritabanı:", tooltipTable: "Tablo:", + tooltipTableComment: "Tablo açıklaması:", tooltipCollection: "Koleksiyon:", tooltipSchema: "Şema:", resultN: "Sonuç {n}", diff --git a/apps/desktop/src/i18n/locales/zh-CN.ts b/apps/desktop/src/i18n/locales/zh-CN.ts index 1b3bfd511c..06f7c7c231 100644 --- a/apps/desktop/src/i18n/locales/zh-CN.ts +++ b/apps/desktop/src/i18n/locales/zh-CN.ts @@ -1380,6 +1380,7 @@ export default withEnglishFallback({ tooltipGroup: "分组:", tooltipDatabase: "数据库:", tooltipTable: "表:", + tooltipTableComment: "表注释:", tooltipCollection: "集合:", tooltipSchema: "Schema:", resultN: "结果 {n}", diff --git a/apps/desktop/src/i18n/locales/zh-TW.ts b/apps/desktop/src/i18n/locales/zh-TW.ts index 6e9910904c..d3faa1037f 100644 --- a/apps/desktop/src/i18n/locales/zh-TW.ts +++ b/apps/desktop/src/i18n/locales/zh-TW.ts @@ -1432,6 +1432,7 @@ export default withEnglishFallback({ tooltipGroup: "群組:", tooltipDatabase: "資料庫:", tooltipTable: "資料表:", + tooltipTableComment: "資料表註解:", tooltipCollection: "集合:", tooltipSchema: "Schema:", resultN: "結果 {n}", diff --git a/apps/desktop/src/lib/__tests__/tabs/tabPresentation.spec.ts b/apps/desktop/src/lib/__tests__/tabs/tabPresentation.spec.ts index 1f58f3a009..1b1734504d 100644 --- a/apps/desktop/src/lib/__tests__/tabs/tabPresentation.spec.ts +++ b/apps/desktop/src/lib/__tests__/tabs/tabPresentation.spec.ts @@ -27,6 +27,8 @@ const translations: Record = { "tabs.tooltipConnection": "Connection:", "tabs.tooltipGroup": "Group:", "tabs.tooltipDatabase": "Database:", + "tabs.tooltipTable": "Table:", + "tabs.tooltipTableComment": "Table Comment:", "connectionGroup.ungroupedLabel": "Ungrouped", "editor.noDatabase": "No database", }; @@ -209,6 +211,22 @@ describe("tab group presentation", () => { expect(connectionGroupDisplayName("conn-1", translate)).toBe("Ungrouped"); }); + + it("shows a bounded table comment only when it is non-empty", () => { + const lines = tabTooltipLines( + queryTab({ + mode: "data", + tableComment: ` ${"表".repeat(55)}\narchive `, + tableMeta: { schema: "public", tableName: "users", columns: [], primaryKeys: [] }, + }), + translate, + ); + const comment = lines.find((line) => line.label === "Table Comment:")?.value; + + expect(Array.from(comment || "")).toHaveLength(50); + expect(comment?.endsWith("…")).toBe(true); + expect(tabTooltipLines(queryTab({ mode: "data", tableComment: " ", tableMeta: { schema: "public", tableName: "users", columns: [], primaryKeys: [] } }), translate).some((line) => line.label === "Table Comment:")).toBe(false); + }); }); describe("query result source ranges", () => { diff --git a/apps/desktop/src/lib/app/openTabsPersistence.ts b/apps/desktop/src/lib/app/openTabsPersistence.ts index c03df61548..710941f1da 100644 --- a/apps/desktop/src/lib/app/openTabsPersistence.ts +++ b/apps/desktop/src/lib/app/openTabsPersistence.ts @@ -53,6 +53,7 @@ export interface SavedOpenTab { objectBrowser?: QueryTab["objectBrowser"]; objectSource?: QueryTab["objectSource"]; sourceView?: boolean; + tableComment?: QueryTab["tableComment"]; tableMeta?: QueryTab["tableMeta"]; mongoEditTarget?: QueryTab["mongoEditTarget"]; resultEvicted?: boolean; @@ -148,6 +149,7 @@ export function serializeOpenTabs(tabs: QueryTab[]): SavedOpenTab[] { objectBrowser: tab.objectBrowser, objectSource: tab.objectSource, ...(tab.sourceView ? { sourceView: true } : {}), + ...(tab.tableComment !== undefined ? { tableComment: tab.tableComment } : {}), tableMeta: tab.tableMeta, ...(tab.mongoEditTarget !== undefined ? { mongoEditTarget: tab.mongoEditTarget } : {}), ...(tab.mode !== "data" && tab.resultEvicted ? { resultEvicted: true } : {}), diff --git a/apps/desktop/src/lib/tabs/tabPresentation.ts b/apps/desktop/src/lib/tabs/tabPresentation.ts index b4f49b8854..5647000e1f 100644 --- a/apps/desktop/src/lib/tabs/tabPresentation.ts +++ b/apps/desktop/src/lib/tabs/tabPresentation.ts @@ -11,6 +11,14 @@ import type { BatchSqlExecution, ConnectionConfig, DatabaseType, QueryResult, Qu type Translate = (key: string, params?: Record) => string; export type OutputView = "result" | "summary" | "explain" | "chart"; +const TABLE_COMMENT_TOOLTIP_MAX_LENGTH = 50; + +function tableCommentTooltipValue(comment: string | null | undefined): string | undefined { + const normalized = comment?.trim().replace(/\s+/g, " "); + if (!normalized) return undefined; + const characters = Array.from(normalized); + return characters.length <= TABLE_COMMENT_TOOLTIP_MAX_LENGTH ? normalized : `${characters.slice(0, TABLE_COMMENT_TOOLTIP_MAX_LENGTH - 1).join("")}…`; +} export function connectionDisplayName(connectionId: string): string { const connectionStore = useConnectionStore(); @@ -170,6 +178,10 @@ export function tabTooltipLines(tab: QueryTab, t: Translate): { label: string; v } if (tab.mode === "data" && tab.tableMeta?.tableName) { lines.push({ label: t("tabs.tooltipTable"), value: tab.tableMeta.tableName }); + const comment = tableCommentTooltipValue(tab.tableComment); + if (comment) { + lines.push({ label: t("tabs.tooltipTableComment"), value: comment }); + } } if (tab.mode === "mongo" && tab.sql) { lines.push({ label: t("tabs.tooltipCollection"), value: tab.sql }); diff --git a/apps/desktop/src/types/database.ts b/apps/desktop/src/types/database.ts index 364028115d..271d566817 100644 --- a/apps/desktop/src/types/database.ts +++ b/apps/desktop/src/types/database.ts @@ -1304,6 +1304,7 @@ export interface QueryTab { objectType: ObjectSourceKind; signature?: string; }; + tableComment?: string | null; tableMeta?: { schema?: string; tableName: string; diff --git a/packages/app-tests/openTabsPersistence.test.ts b/packages/app-tests/openTabsPersistence.test.ts index e71676ba0f..dd0e18d47b 100644 --- a/packages/app-tests/openTabsPersistence.test.ts +++ b/packages/app-tests/openTabsPersistence.test.ts @@ -311,6 +311,7 @@ test("restores data and structure tabs with table state", () => { resultPageOffset: 50, whereInput: "id > 10", orderByInput: "id DESC", + tableComment: "Application users", tableMeta: { schema: "public", tableName: "users", @@ -351,6 +352,7 @@ test("restores data and structure tabs with table state", () => { assert.equal(restored.tabs[0]?.resultPageOffset, 50); assert.equal(restored.tabs[0]?.whereInput, "id > 10"); assert.equal(restored.tabs[0]?.orderByInput, "id DESC"); + assert.equal(restored.tabs[0]?.tableComment, "Application users"); assert.equal(restored.tabs[1]?.structureTableName, "users"); });