From 01ecb6020cc2d08079a951872cbc03d9ddfa4497 Mon Sep 17 00:00:00 2001 From: zhangsan Date: Sun, 19 Jul 2026 14:55:42 +0800 Subject: [PATCH] fix(sidebar): improve tree item comment layout --- .../src/components/sidebar/TreeItem.vue | 159 +++++++++++++----- .../sidebar/sidebarTreeItemLayout.spec.ts | 20 +++ .../src/lib/sidebar/sidebarTreeItemLayout.ts | 10 +- 3 files changed, 147 insertions(+), 42 deletions(-) create mode 100644 apps/desktop/src/lib/__tests__/sidebar/sidebarTreeItemLayout.spec.ts diff --git a/apps/desktop/src/components/sidebar/TreeItem.vue b/apps/desktop/src/components/sidebar/TreeItem.vue index 61017e6dc..2f5b1a49a 100644 --- a/apps/desktop/src/components/sidebar/TreeItem.vue +++ b/apps/desktop/src/components/sidebar/TreeItem.vue @@ -44,7 +44,7 @@ import { Badge } from "@/components/ui/badge"; import { Input } from "@/components/ui/input"; import LightTooltip from "@/components/ui/LightTooltip.vue"; import type { ColumnInfo, ConnectionConfig, DatabaseType, TreeNode, TreeNodeType } from "@/types/database"; -import { canTreeNodeShowExpander, treeItemPaddingLeft, treeLabelWidthClass, usesFullWidthTreeLabel } from "@/lib/sidebar/sidebarTreeItemLayout"; +import { canTreeNodeShowExpander, trailingCommentAvailableWidth, trailingCommentGapPx, treeItemPaddingLeft, treeLabelWidthClass, usesFullWidthTreeLabel } from "@/lib/sidebar/sidebarTreeItemLayout"; import { clearActiveTableReferencePayload, createTableReferencePayload, createTableReferenceDropEvent, setActiveTableReferencePayload, type QueryEditorTableReferencePayload } from "@/lib/editor/queryEditorTableDrop"; import { dataTabOpenModeFromTreeClick } from "@/lib/sidebar/dataTabOpenPolicy"; import { effectiveDatabaseTypeForConnection } from "@/lib/database/jdbcDialect"; @@ -68,12 +68,22 @@ const labelRef = ref(); const rowRef = ref(); +const trailingCommentLayoutRef = ref(); + +const trailingCommentLeadingRef = ref(); + +const trailingCommentMaxWidth = ref(0); + const labelOverflowing = ref(false); let labelResizeObserver: ResizeObserver | null = null; +let trailingCommentResizeObserver: ResizeObserver | null = null; + let labelMeasureFrame = 0; +let trailingCommentMeasureFrame = 0; + function cancelLabelOverflowMeasure() { if (!labelMeasureFrame) return; window.cancelAnimationFrame(labelMeasureFrame); @@ -164,10 +174,6 @@ const stopPasteHandlerRegistration = watch( const activeNode = shallowRef(props.node); -const usesFullWidthLabel = computed(() => usesFullWidthTreeLabel(activeNode.value.type, settingsStore.editorSettings.sidebarAllowHorizontalScroll)); - -const rowWidthClass = computed(() => (usesFullWidthLabel.value ? "w-max min-w-full" : "w-full min-w-0")); - const showProductionBadge = computed(() => { const connectionId = activeNode.value.connectionId; const context = productionContextForDatabase(connectionId ? connectionStore.getConfig(connectionId) : undefined, activeNode.value.database); @@ -542,17 +548,72 @@ const isNodeDefaultDatabase = computed( () => (activeNode.value.type === "database" || activeNode.value.type === "redis-db" || activeNode.value.type === "mongo-db") && !!activeNode.value.connectionId && !!activeNode.value.database && connectionStore.isDefaultDatabase(activeNode.value.connectionId, activeNode.value.database), ); -const columnComment = computed(() => (!settingsStore.editorSettings.sidebarHideTableComments && activeNode.value.type === "column" && activeNode.value.meta && "comment" in activeNode.value.meta ? (activeNode.value.meta as any).comment : null)); +const trailingComment = computed(() => { + if (settingsStore.editorSettings.sidebarHideTableComments) return null; + if (activeNode.value.type === "column" && activeNode.value.meta && "comment" in activeNode.value.meta) return (activeNode.value.meta as any).comment || null; + if ((activeNode.value.type === "schema" || activeNode.value.type === "table" || activeNode.value.type === "view" || activeNode.value.type === "mongo-collection" || activeNode.value.type === "vector-collection" || activeNode.value.type === "elasticsearch-index") && activeNode.value.comment) { + return activeNode.value.comment; + } + return null; +}); -const tableComment = computed(() => - !settingsStore.editorSettings.sidebarHideTableComments && - (activeNode.value.type === "schema" || activeNode.value.type === "table" || activeNode.value.type === "view" || activeNode.value.type === "mongo-collection" || activeNode.value.type === "vector-collection" || activeNode.value.type === "elasticsearch-index") && - activeNode.value.comment - ? activeNode.value.comment - : null, -); +function cancelTrailingCommentMeasure() { + if (!trailingCommentMeasureFrame) return; + window.cancelAnimationFrame(trailingCommentMeasureFrame); + trailingCommentMeasureFrame = 0; +} -const labelWidthClass = computed(() => treeLabelWidthClass({ fullWidth: usesFullWidthLabel.value, hasTrailingComment: !!columnComment.value || !!tableComment.value })); +function measureTrailingCommentLayout() { + const container = trailingCommentLayoutRef.value; + const leading = trailingCommentLeadingRef.value; + if (!trailingComment.value || !container || !leading) { + trailingCommentMaxWidth.value = 0; + return; + } + + // The leading group keeps the complete table name ahead of the comment. + // Only the width remaining after that name and the fixed gap may be used + // by the comment; once it reaches zero, the comment is hidden. + trailingCommentMaxWidth.value = trailingCommentAvailableWidth(container.clientWidth, leading.scrollWidth); +} + +function scheduleTrailingCommentMeasure() { + if (typeof window === "undefined") { + measureTrailingCommentLayout(); + return; + } + cancelTrailingCommentMeasure(); + trailingCommentMeasureFrame = window.requestAnimationFrame(() => { + trailingCommentMeasureFrame = 0; + measureTrailingCommentLayout(); + }); +} + +function refreshTrailingCommentMeasurement() { + trailingCommentResizeObserver?.disconnect(); + trailingCommentResizeObserver = null; + + if (!trailingComment.value || !trailingCommentLayoutRef.value || !trailingCommentLeadingRef.value) { + trailingCommentMaxWidth.value = 0; + return; + } + + scheduleTrailingCommentMeasure(); + if (typeof ResizeObserver !== "undefined") { + trailingCommentResizeObserver = new ResizeObserver(scheduleTrailingCommentMeasure); + trailingCommentResizeObserver.observe(trailingCommentLayoutRef.value); + } +} + +// Keep comment rows constrained to the sidebar. When space is tight, the +// comment truncates before the table name instead of creating a large gap. +const usesFullWidthLabel = computed(() => usesFullWidthTreeLabel(activeNode.value.type, settingsStore.editorSettings.sidebarAllowHorizontalScroll, !!trailingComment.value)); + +const rowWidthClass = computed(() => (usesFullWidthLabel.value ? "w-max min-w-full" : "w-full min-w-0")); + +const labelWidthClass = computed(() => treeLabelWidthClass({ fullWidth: usesFullWidthLabel.value, hasTrailingComment: !!trailingComment.value })); + +watch(() => [trailingComment.value, visibleLabel(activeNode.value), trailingCommentLayoutRef.value, trailingCommentLeadingRef.value], refreshTrailingCommentMeasurement, { flush: "post", immediate: true }); const paddingLeft = computed(() => treeItemPaddingLeft(props.depth)); @@ -605,6 +666,7 @@ const rowStyle = computed(() => { const backgroundColor = hexToRgba(color, isActiveConnectionScope.value ? 0.14 : 0.08); return { paddingLeft: paddingLeft.value, + paddingRight: trailingComment.value ? "12px" : undefined, "--tree-connection-row-bg": backgroundColor, "--tree-connection-row-hover-bg": hexToRgba(color, isActiveConnectionScope.value ? 0.18 : 0.12), "--tree-connection-active-bg": hexToRgba(color, 0.18), @@ -859,6 +921,8 @@ watch( onBeforeUnmount(() => { stopPasteHandlerRegistration(); handleMouseLeave(); + trailingCommentResizeObserver?.disconnect(); + cancelTrailingCommentMeasure(); finishTableReferenceDrag(); }); @@ -951,7 +1015,7 @@ function onKeydown(event: KeyboardEvent) {
- - {{ visibleLabel(node) }} - - {{ node.objectCount }} - - {{ t("editor.defaultDatabase") }} - - {{ columnComment }} - {{ tableComment }} +
+
+ + {{ visibleLabel(node) }} + + {{ node.objectCount }} + + {{ t("editor.defaultDatabase") }} + +
+
{{ t("connection.readOnlyBadge") }} @@ -1071,6 +1145,11 @@ function onKeydown(event: KeyboardEvent) { font-size: 10px; line-height: 1rem; opacity: 0.6; + /* Use the comment's natural width whenever the row has room for it. */ + width: max-content; + max-width: 100%; + /* Preserve table names when a narrow row needs to truncate its comment. */ + flex-shrink: 999; /* Sidebar rows repaint on hover; avoid heavier font shaping and fallback here. */ text-rendering: auto; } diff --git a/apps/desktop/src/lib/__tests__/sidebar/sidebarTreeItemLayout.spec.ts b/apps/desktop/src/lib/__tests__/sidebar/sidebarTreeItemLayout.spec.ts new file mode 100644 index 000000000..d9abd9f94 --- /dev/null +++ b/apps/desktop/src/lib/__tests__/sidebar/sidebarTreeItemLayout.spec.ts @@ -0,0 +1,20 @@ +import { describe, expect, it } from "vitest"; +import { trailingCommentAvailableWidth, treeLabelWidthClass, usesFullWidthTreeLabel } from "@/lib/sidebar/sidebarTreeItemLayout"; + +describe("sidebar tree item layout", () => { + it("keeps a table row constrained when it displays a comment", () => { + expect(usesFullWidthTreeLabel("table", true)).toBe(true); + expect(usesFullWidthTreeLabel("table", true, true)).toBe(false); + }); + + it("lets a table name consume the available row width before truncating", () => { + expect(treeLabelWidthClass({ fullWidth: false, hasTrailingComment: true })).toBe("min-w-0 flex-1 truncate"); + }); + + it("gives the comment only the width left after the full table name and gap", () => { + expect(trailingCommentAvailableWidth(260, 100)).toBe(152); + expect(trailingCommentAvailableWidth(108, 100)).toBe(0); + expect(trailingCommentAvailableWidth(100, 100)).toBe(0); + expect(trailingCommentAvailableWidth(99, 100)).toBe(0); + }); +}); diff --git a/apps/desktop/src/lib/sidebar/sidebarTreeItemLayout.ts b/apps/desktop/src/lib/sidebar/sidebarTreeItemLayout.ts index 3c47ced40..7fd17a5e0 100644 --- a/apps/desktop/src/lib/sidebar/sidebarTreeItemLayout.ts +++ b/apps/desktop/src/lib/sidebar/sidebarTreeItemLayout.ts @@ -54,8 +54,14 @@ export function treeItemPaddingLeft(depth: number): string { return `${depth * 16 + 8}px`; } -export function usesFullWidthTreeLabel(type: TreeNodeType, allowHorizontalScroll: boolean): boolean { - return allowHorizontalScroll && fullWidthLabelTypes.has(type); +export const trailingCommentGapPx = 8; + +export function trailingCommentAvailableWidth(containerWidth: number, leadingWidth: number): number { + return Math.max(0, Math.floor(containerWidth - leadingWidth - trailingCommentGapPx)); +} + +export function usesFullWidthTreeLabel(type: TreeNodeType, allowHorizontalScroll: boolean, hasTrailingComment = false): boolean { + return allowHorizontalScroll && !hasTrailingComment && fullWidthLabelTypes.has(type); } export function treeLabelWidthClass({ fullWidth, hasTrailingComment }: { fullWidth: boolean; hasTrailingComment: boolean }): string {