Refine overflow navigation review feedback

This commit is contained in:
vrustx
2026-05-31 11:47:44 +08:00
parent e94de447c9
commit 9d603b617d
14 changed files with 173 additions and 224 deletions
@@ -103,6 +103,7 @@ const sidebarObjectDisplayHelp = ref<"grouped" | "simple" | null>(null);
const editAutoSelectActiveSidebarNode = ref(settingsStore.editorSettings.autoSelectActiveSidebarNode);
const editSidebarHiddenTablePrefixes = ref(settingsStore.editorSettings.sidebarHiddenTablePrefixes.join("\n"));
const editSidebarHideTableComments = ref(settingsStore.editorSettings.sidebarHideTableComments);
const editSidebarAllowHorizontalScroll = ref(settingsStore.editorSettings.sidebarAllowHorizontalScroll);
const redisScanPageSizeOptions = [200, 1000, 5000, 10000];
const systemFonts = ref<string[]>([]);
const systemFontsLoading = ref(false);
@@ -237,6 +238,7 @@ watch(
editAutoSelectActiveSidebarNode.value = settingsStore.editorSettings.autoSelectActiveSidebarNode;
editSidebarHiddenTablePrefixes.value = settingsStore.editorSettings.sidebarHiddenTablePrefixes.join("\n");
editSidebarHideTableComments.value = settingsStore.editorSettings.sidebarHideTableComments;
editSidebarAllowHorizontalScroll.value = settingsStore.editorSettings.sidebarAllowHorizontalScroll;
editSnippets.value = settingsStore.editorSettings.snippets.map((s) => ({ ...s }));
void loadSystemFontOptions();
}
@@ -274,6 +276,7 @@ function hasChanges(): boolean {
editSidebarObjectDisplay.value !== settingsStore.editorSettings.sidebarObjectDisplay ||
editAutoSelectActiveSidebarNode.value !== settingsStore.editorSettings.autoSelectActiveSidebarNode ||
editSidebarHideTableComments.value !== settingsStore.editorSettings.sidebarHideTableComments ||
editSidebarAllowHorizontalScroll.value !== settingsStore.editorSettings.sidebarAllowHorizontalScroll ||
JSON.stringify(normalizeSidebarHiddenTablePrefixes(editSidebarHiddenTablePrefixes.value)) !==
JSON.stringify(settingsStore.editorSettings.sidebarHiddenTablePrefixes) ||
JSON.stringify(editSnippets.value) !== JSON.stringify(settingsStore.editorSettings.snippets)
@@ -300,6 +303,7 @@ async function persistSettings() {
sidebarObjectDisplay: editSidebarObjectDisplay.value,
autoSelectActiveSidebarNode: editAutoSelectActiveSidebarNode.value,
sidebarHideTableComments: editSidebarHideTableComments.value,
sidebarAllowHorizontalScroll: editSidebarAllowHorizontalScroll.value,
sidebarHiddenTablePrefixes: normalizeSidebarHiddenTablePrefixes(editSidebarHiddenTablePrefixes.value),
snippets: editSnippets.value,
});
@@ -337,6 +341,7 @@ function resetDefaults() {
editSidebarObjectDisplay.value = DEFAULT_EDITOR_SETTINGS.sidebarObjectDisplay;
editAutoSelectActiveSidebarNode.value = DEFAULT_EDITOR_SETTINGS.autoSelectActiveSidebarNode;
editSidebarHideTableComments.value = DEFAULT_EDITOR_SETTINGS.sidebarHideTableComments;
editSidebarAllowHorizontalScroll.value = DEFAULT_EDITOR_SETTINGS.sidebarAllowHorizontalScroll;
editSidebarHiddenTablePrefixes.value = DEFAULT_EDITOR_SETTINGS.sidebarHiddenTablePrefixes.join("\n");
editSnippets.value = DEFAULT_SQL_SNIPPETS.map((s) => ({ ...s }));
}
@@ -1339,6 +1344,22 @@ watch(
</div>
<Switch id="sidebar-hide-table-comments" v-model="editSidebarHideTableComments" />
</div>
<div class="flex items-center justify-between gap-4 rounded-md border bg-muted/20 px-3 py-2">
<div class="flex items-center gap-2">
<Label for="sidebar-allow-horizontal-scroll">
{{ t("settings.sidebarAllowHorizontalScroll") }}
</Label>
<Tooltip>
<TooltipTrigger as-child>
<CircleHelp class="h-3.5 w-3.5 cursor-help text-muted-foreground hover:text-foreground" />
</TooltipTrigger>
<TooltipContent class="max-w-[320px] text-xs leading-relaxed" side="top" align="start">
{{ t("settings.sidebarAllowHorizontalScrollDescription") }}
</TooltipContent>
</Tooltip>
</div>
<Switch id="sidebar-allow-horizontal-scroll" v-model="editSidebarAllowHorizontalScroll" />
</div>
<div class="space-y-2">
<Label for="sidebar-hidden-table-prefixes">{{ t("settings.sidebarHiddenTablePrefixes") }}</Label>
<textarea
+91 -146
View File
@@ -22,7 +22,7 @@ import { useConnectionStore } from "@/stores/connectionStore";
import { useQueryStore } from "@/stores/queryStore";
import { useSettingsStore } from "@/stores/settingsStore";
import { useTabScroll } from "@/composables/useTabScroll";
import { shouldShowTabOverflowControls, tabGroupColor, tabGroupKey } from "@/lib/tabPresentation";
import { connectionColor, shouldShowTabOverflowControls } from "@/lib/tabPresentation";
import { hexToRgba } from "@/lib/color";
import type { QueryTab } from "@/types/database";
@@ -128,14 +128,24 @@ watch(
);
function tabColorStyle(tab: QueryTab) {
const color = tabGroupColor(tab);
const color = connectionColor(tab.connectionId);
const isActive = tab.id === queryStore.activeTabId && !props.showDriverStore;
const isClassic = settingsStore.editorSettings.appLayout === "classic";
if (!color) {
if (isClassic) {
return isActive ? { boxShadow: "0 1px 0 0 var(--color-background)" } : undefined;
}
return isActive
? {
borderColor: "var(--ring)",
}
: undefined;
}
if (isClassic) {
return {
backgroundColor: hexToRgba(color, isActive ? 0.16 : 0.07),
boxShadow: isActive ? `inset 0 -2px 0 ${color}` : `inset 0 -1px 0 ${hexToRgba(color, 0.32)}`,
boxShadow: isActive ? `inset 0 -2px 0 ${color}` : undefined,
};
}
@@ -160,13 +170,6 @@ const showTabOverflowControls = computed(() =>
),
);
interface TabGroup {
key: string;
label: string;
color: string;
tabs: QueryTab[];
}
const connectionSummariesById = computed(
() =>
new Map(
@@ -239,45 +242,14 @@ function tabTooltipRows(tab: QueryTab): { label: string; value: string }[] {
return rows;
}
function groupLabelForTab(tab: Pick<QueryTab, "connectionId" | "database">): string {
return `${connectionNameForTab(tab)} / ${databaseNameForTab(tab)}`;
}
const tabGroups = computed<TabGroup[]>(() => {
const groups: TabGroup[] = [];
const byKey = new Map<string, TabGroup>();
for (const tab of queryStore.tabs) {
const key = tabGroupKey(tab);
let group = byKey.get(key);
if (!group) {
group = {
key,
label: groupLabelForTab(tab),
color: tabGroupColor(tab),
tabs: [],
};
byKey.set(key, group);
groups.push(group);
}
group.tabs.push(tab);
}
return groups;
});
const showTabGroupLabels = computed(() => tabGroups.value.length > 1);
const openTabMenuItems = computed(() =>
tabGroups.value.flatMap((group, groupIndex) =>
group.tabs.map((tab, tabIndex) => ({
value: tab.id,
label: tabTitle(tab),
title: tabTitle(tab),
sectionLabel: tabIndex === 0 ? group.label : undefined,
sectionColor: tabIndex === 0 ? group.color : undefined,
separatorBefore: groupIndex > 0 && tabIndex === 0,
icon: tabMenuIcon(tab),
iconClass: tabIconClass(tab),
})),
),
queryStore.tabs.map((tab) => ({
value: tab.id,
label: tabTitle(tab),
title: tabTitle(tab),
icon: tabMenuIcon(tab),
iconClass: tabIconClass(tab),
})),
);
function tabMenuIcon(tab: QueryTab) {
@@ -308,12 +280,6 @@ const tabOverflowControlClass = computed(() =>
? "h-full w-8 border-r border-border/80 dark:border-border/45 bg-background/80 text-foreground/75 hover:bg-accent hover:text-foreground disabled:cursor-default disabled:opacity-40"
: "h-7 w-7 rounded-md border border-border/60 bg-background text-foreground/70 hover:border-border hover:text-foreground",
);
const tabGroupLabelClass = computed(() =>
settingsStore.editorSettings.appLayout === "classic"
? "h-full border-r border-border/80 dark:border-border/45 bg-muted/55 px-2"
: "h-7 rounded-md border border-border/50 bg-muted/35 px-2",
);
</script>
<template>
@@ -346,99 +312,78 @@ const tabGroupLabelClass = computed(() =>
@scroll="updateScrollButtons"
@wheel="onTabsWheel"
>
<div
v-for="group in tabGroups"
:key="group.key"
class="flex h-full shrink-0 items-center"
:class="settingsStore.editorSettings.appLayout === 'classic' ? '' : 'gap-1.5'"
:data-tab-group="group.key"
<CustomContextMenu
v-for="tab in queryStore.tabs"
:key="tab.id"
:items="getTabMenuItems(tab)"
v-slot="{ onContextMenu }"
>
<div
v-if="showTabGroupLabels"
class="inline-flex shrink-0 items-center gap-1.5 text-[11px] font-medium text-muted-foreground"
:class="tabGroupLabelClass"
:title="group.label"
>
<span class="h-2 w-2 shrink-0 rounded-full" :style="{ backgroundColor: group.color }" />
<span class="max-w-32 truncate">{{ group.label }}</span>
</div>
<CustomContextMenu
v-for="tab in group.tabs"
:key="tab.id"
:items="getTabMenuItems(tab)"
v-slot="{ onContextMenu }"
>
<div
:class="settingsStore.editorSettings.appLayout === 'classic' ? 'h-full' : ''"
@contextmenu="onContextMenu"
>
<Tooltip>
<TooltipTrigger as-child>
<div
class="group flex items-center gap-1 px-2 text-xs cursor-pointer transition-colors whitespace-nowrap select-none"
:class="
settingsStore.editorSettings.appLayout === 'classic'
? [
compactTabTitle ? 'min-w-24' : 'min-w-38',
'h-full border-r border-border/80 dark:border-border/45',
tab.id === queryStore.activeTabId && !showDriverStore
? 'bg-background text-foreground font-medium'
: 'text-foreground/70 hover:text-foreground/90',
]
: [
compactTabTitle ? 'min-w-24' : 'min-w-38',
'h-7 rounded-md border',
tab.id === queryStore.activeTabId && !showDriverStore
? 'text-foreground font-medium'
: 'border-border/60 text-foreground/70 hover:border-border hover:text-foreground/90',
]
"
:style="tabColorStyle(tab)"
:data-active-tab="tab.id === queryStore.activeTabId && !showDriverStore"
@click="
queryStore.activeTabId = tab.id;
emit('close-driver-store');
"
@mousedown.middle.prevent="queryStore.closeTab(tab.id)"
<div :class="settingsStore.editorSettings.appLayout === 'classic' ? 'h-full' : ''" @contextmenu="onContextMenu">
<Tooltip>
<TooltipTrigger as-child>
<div
class="group flex items-center gap-1 px-2 text-xs cursor-pointer transition-colors whitespace-nowrap select-none"
:class="
settingsStore.editorSettings.appLayout === 'classic'
? [
compactTabTitle ? 'min-w-24' : 'min-w-38',
'h-full border-r border-border/80 dark:border-border/45',
tab.id === queryStore.activeTabId && !showDriverStore
? 'bg-background text-foreground font-medium'
: 'text-foreground/70 hover:text-foreground/90',
]
: [
compactTabTitle ? 'min-w-24' : 'min-w-38',
'h-7 rounded-md border',
tab.id === queryStore.activeTabId && !showDriverStore
? 'text-foreground font-medium'
: 'border-border/60 text-foreground/70 hover:border-border hover:text-foreground/90',
]
"
:style="tabColorStyle(tab)"
:data-active-tab="tab.id === queryStore.activeTabId && !showDriverStore"
@click="
queryStore.activeTabId = tab.id;
emit('close-driver-store');
"
@mousedown.middle.prevent="queryStore.closeTab(tab.id)"
>
<span class="shrink-0" :class="tabIconClass(tab)">
<Table2 v-if="tab.mode === 'data'" class="h-3.5 w-3.5" />
<TableProperties v-else-if="tab.mode === 'objects'" class="h-3.5 w-3.5" />
<PencilRuler v-else-if="tab.mode === 'structure'" class="h-3.5 w-3.5" />
<Code2 v-else class="h-3.5 w-3.5" />
</span>
<span class="min-w-0 truncate flex-1">{{ tabTitle(tab) }}</span>
<Tooltip>
<TooltipTrigger as-child>
<button
class="inline-flex rounded p-0.5 text-muted-foreground hover:bg-muted-foreground/20 hover:text-foreground focus:opacity-100"
:class="tab.pinned ? 'visible text-primary' : 'invisible group-hover:visible'"
@click.stop="queryStore.togglePinnedTab(tab.id)"
>
<Pin class="h-3 w-3" :class="{ 'fill-current': tab.pinned }" />
</button>
</TooltipTrigger>
<TooltipContent>{{ tab.pinned ? t("contextMenu.unpin") : t("contextMenu.pin") }}</TooltipContent>
</Tooltip>
<button
class="rounded hover:bg-muted-foreground/20 p-0.5 shrink-0"
@click.stop="queryStore.closeTab(tab.id)"
>
<span class="h-2 w-2 shrink-0 rounded-full" :style="{ backgroundColor: tabGroupColor(tab) }" />
<span class="shrink-0" :class="tabIconClass(tab)">
<Table2 v-if="tab.mode === 'data'" class="h-3.5 w-3.5" />
<TableProperties v-else-if="tab.mode === 'objects'" class="h-3.5 w-3.5" />
<PencilRuler v-else-if="tab.mode === 'structure'" class="h-3.5 w-3.5" />
<Code2 v-else class="h-3.5 w-3.5" />
</span>
<span class="min-w-0 truncate flex-1">{{ tabTitle(tab) }}</span>
<Tooltip>
<TooltipTrigger as-child>
<button
class="inline-flex rounded p-0.5 text-muted-foreground hover:bg-muted-foreground/20 hover:text-foreground focus:opacity-100"
:class="tab.pinned ? 'visible text-primary' : 'invisible group-hover:visible'"
@click.stop="queryStore.togglePinnedTab(tab.id)"
>
<Pin class="h-3 w-3" :class="{ 'fill-current': tab.pinned }" />
</button>
</TooltipTrigger>
<TooltipContent>{{ tab.pinned ? t("contextMenu.unpin") : t("contextMenu.pin") }}</TooltipContent>
</Tooltip>
<button
class="rounded hover:bg-muted-foreground/20 p-0.5 shrink-0"
@click.stop="queryStore.closeTab(tab.id)"
>
<X class="h-3 w-3" />
</button>
</div>
</TooltipTrigger>
<TooltipContent side="bottom" class="text-xs grid grid-cols-[auto_1fr] gap-x-2">
<template v-for="line in tabTooltipRows(tab)" :key="line.label">
<span class="text-muted-foreground">{{ line.label }}</span>
<span>{{ line.value }}</span>
</template>
</TooltipContent>
</Tooltip>
</div>
</CustomContextMenu>
</div>
<X class="h-3 w-3" />
</button>
</div>
</TooltipTrigger>
<TooltipContent side="bottom" class="text-xs grid grid-cols-[auto_1fr] gap-x-2">
<template v-for="line in tabTooltipRows(tab)" :key="line.label">
<span class="text-muted-foreground">{{ line.label }}</span>
<span>{{ line.value }}</span>
</template>
</TooltipContent>
</Tooltip>
</div>
</CustomContextMenu>
<!-- Driver Store Tab -->
<div
@@ -472,7 +417,7 @@ const tabGroupLabelClass = computed(() =>
</div>
<div :class="tabTailDragRegionClass" data-tauri-drag-region />
</div>
<div v-if="showTabOverflowControls" class="relative z-30 flex shrink-0 items-center gap-1">
<div v-if="showTabOverflowControls" class="relative z-30 flex shrink-0 items-center">
<button
v-if="showTabOverflowControls"
type="button"
@@ -163,6 +163,11 @@ const filteredNodes = computed(() => {
const flatNodes = computed<FlatTreeNode[]>(() => flattenTree(filteredNodes.value));
const useVirtualTree = computed(() => shouldVirtualizeFlatTree(flatNodes.value.length));
const activeTab = computed(() => queryStore.tabs.find((tab) => tab.id === queryStore.activeTabId));
const sidebarTreeOverflowClass = computed(() =>
settingsStore.editorSettings.sidebarAllowHorizontalScroll
? "overflow-x-auto sidebar-tree-horizontal-scroll"
: "overflow-x-hidden",
);
const pendingRenameGroupId = ref<string | null>(null);
const highlightedNodeId = ref<string | null>(null);
@@ -492,7 +497,8 @@ defineExpose({ focusSearch });
<RecycleScroller
v-if="flatNodes.length > 0 && useVirtualTree"
ref="treeScrollerRef"
class="sidebar-tree connection-tree-scroller min-h-0 flex-1 overflow-y-auto overflow-x-auto"
class="sidebar-tree connection-tree-scroller min-h-0 flex-1 overflow-y-auto"
:class="sidebarTreeOverflowClass"
:items="flatNodes"
:item-size="SIDEBAR_TREE_ROW_HEIGHT"
:buffer="SIDEBAR_TREE_SCROLL_BUFFER"
@@ -518,7 +524,8 @@ defineExpose({ focusSearch });
<div
v-else-if="flatNodes.length > 0"
ref="plainTreeScrollerRef"
class="sidebar-tree min-h-0 flex-1 overflow-y-auto overflow-x-auto"
class="sidebar-tree min-h-0 flex-1 overflow-y-auto"
:class="sidebarTreeOverflowClass"
>
<TreeItem
v-for="item in flatNodes"
@@ -546,8 +553,11 @@ defineExpose({ focusSearch });
}
.connection-tree-scroller :deep(.vue-recycle-scroller__item-view) {
width: max-content;
min-width: 100%;
contain: style;
}
.connection-tree-scroller.sidebar-tree-horizontal-scroll :deep(.vue-recycle-scroller__item-view) {
width: max-content;
}
</style>
@@ -157,7 +157,9 @@ const emit = defineEmits<{
"search-toggle": [node: TreeNode];
}>();
const usesFullWidthLabel = computed(() => usesFullWidthTreeLabel(props.node.type));
const usesFullWidthLabel = computed(() =>
usesFullWidthTreeLabel(props.node.type, settingsStore.editorSettings.sidebarAllowHorizontalScroll),
);
const rowWidthClass = computed(() => (usesFullWidthLabel.value ? "w-max min-w-full" : "w-full min-w-0"));
const labelWidthClass = computed(() =>
usesFullWidthLabel.value ? "shrink-0 whitespace-nowrap" : "min-w-0 flex-1 truncate",
@@ -1650,9 +1652,12 @@ const isActiveConnectionScope = computed(
const isSelected = computed(() => connectionStore.selectedTreeNodeId === props.node.id);
const rowStyle = computed(() => {
const color = connectionColor.value;
const backgroundColor = hexToRgba(color, isActiveConnectionScope.value ? 0.14 : 0.08);
return {
paddingLeft: paddingLeft.value,
backgroundColor: hexToRgba(color, isActiveConnectionScope.value ? 0.14 : 0.08),
"--tree-connection-active-bg": hexToRgba(color, 0.18),
"--tree-connection-active-focus-bg": hexToRgba(color, 0.22),
backgroundColor,
};
});
@@ -2662,18 +2667,18 @@ function treeItemMenuItems(): ContextMenuItem[] {
<style>
/* Unfocused: subtle gray */
.tree-item-active {
background-color: oklch(0.94 0 0) !important;
background-color: var(--tree-connection-active-bg, oklch(0.94 0 0)) !important;
}
:root.dark .tree-item-active {
background-color: oklch(0.26 0 0) !important;
background-color: var(--tree-connection-active-bg, oklch(0.26 0 0)) !important;
}
/* Focused: soft blue */
.tree-item-active:focus {
background-color: oklch(0.91 0.03 250) !important;
background-color: var(--tree-connection-active-focus-bg, oklch(0.91 0.03 250)) !important;
}
:root.dark .tree-item-active:focus {
background-color: oklch(0.35 0.06 250) !important;
background-color: var(--tree-connection-active-focus-bg, oklch(0.35 0.06 250)) !important;
}
/* Locate highlight: instant amber, then fade on removal */
@@ -9,8 +9,6 @@ export interface LightDropdownItem {
icon?: Component;
iconClass?: string;
leadingText?: string;
sectionLabel?: string;
sectionColor?: string;
disabled?: boolean;
separatorBefore?: boolean;
}
@@ -189,17 +187,6 @@ onBeforeUnmount(close);
<div v-if="label" class="bg-border -mx-1 my-1 h-px" />
<template v-for="item in items" :key="item.value">
<div v-if="item.separatorBefore" class="bg-border -mx-1 my-1 h-px" />
<div
v-if="item.sectionLabel"
class="flex items-center gap-1.5 px-1.5 pb-1 pt-1.5 text-[11px] text-muted-foreground"
>
<span
v-if="item.sectionColor"
class="h-2 w-2 shrink-0 rounded-full"
:style="{ backgroundColor: item.sectionColor }"
/>
<span class="min-w-0 truncate">{{ item.sectionLabel }}</span>
</div>
<button
type="button"
class="flex w-full items-center gap-1.5 rounded-md px-1.5 py-1 text-left text-sm outline-hidden hover:bg-accent hover:text-accent-foreground disabled:pointer-events-none disabled:opacity-50"
+3
View File
@@ -1488,6 +1488,9 @@ export default {
sidebarHideTableComments: "Hide table comments in sidebar",
sidebarHideTableCommentsDescription:
"Hide the inline table/view comments shown next to names in the sidebar tree to save horizontal space.",
sidebarAllowHorizontalScroll: "Allow sidebar horizontal scroll",
sidebarAllowHorizontalScrollDescription:
"Show long table, view, and collection names in full by allowing horizontal sidebar scrolling.",
snippetsDescription: "Customize SQL snippet templates triggered in the editor.",
snippetsAdd: "Add Snippet",
snippetsLabel: "Label",
+3
View File
@@ -1382,6 +1382,9 @@ export default {
sidebarHideTableComments: "Ocultar comentarios de tablas en la barra lateral",
sidebarHideTableCommentsDescription:
"Oculta los comentarios de tablas/vistas junto a los nombres en el árbol de la barra lateral para ahorrar espacio horizontal.",
sidebarAllowHorizontalScroll: "Permitir desplazamiento horizontal lateral",
sidebarAllowHorizontalScrollDescription:
"Muestra completos los nombres largos de tablas, vistas y colecciones permitiendo desplazamiento horizontal en la barra lateral.",
snippetsDescription: "Personaliza plantillas SQL activadas en el editor.",
snippetsAdd: "Agregar fragmento",
snippetsLabel: "Etiqueta",
+2
View File
@@ -1456,6 +1456,8 @@ export default {
sidebarHiddenTablePrefixesPlaceholder: "例如:\nODS_\nT8Y2_LONG_",
sidebarHideTableComments: "隐藏侧边栏表注释",
sidebarHideTableCommentsDescription: "隐藏侧边栏中表名旁边显示的表/视图注释,以节省横向空间。",
sidebarAllowHorizontalScroll: "允许侧边栏横向滚动",
sidebarAllowHorizontalScrollDescription: "完整显示较长的表、视图和集合名称;默认关闭以保留省略号截断。",
snippetsDescription: "自定义编辑器中触发的 SQL 代码片段模板。",
snippetsAdd: "添加片段",
snippetsLabel: "显示名",
@@ -19,8 +19,8 @@ export function treeItemPaddingLeft(depth: number): string {
return `${depth * 16 + 8}px`;
}
export function usesFullWidthTreeLabel(type: TreeNodeType): boolean {
return fullWidthLabelTypes.has(type);
export function usesFullWidthTreeLabel(type: TreeNodeType, allowHorizontalScroll: boolean): boolean {
return allowHorizontalScroll && fullWidthLabelTypes.has(type);
}
export function canTreeNodeExpand(type: TreeNodeType): boolean {
-29
View File
@@ -3,19 +3,6 @@ import { useConnectionStore } from "@/stores/connectionStore";
import { useSettingsStore } from "@/stores/settingsStore";
import type { QueryTab } from "@/types/database";
const tabGroupPalette = [
"#2563eb",
"#059669",
"#d97706",
"#dc2626",
"#7c3aed",
"#0891b2",
"#c2410c",
"#4f46e5",
"#be185d",
"#16a34a",
];
export function connectionDisplayName(connectionId: string): string {
const connectionStore = useConnectionStore();
return connectionStore.getConfig(connectionId)?.name || connectionId;
@@ -34,22 +21,6 @@ export function databaseDisplayNameForTab(connectionId: string, database: string
return database || t("editor.noDatabase");
}
export function tabGroupKey(tab: Pick<QueryTab, "connectionId" | "database">): string {
return `${tab.connectionId}::${tab.database || ""}`;
}
export function colorForTabGroupKey(groupKey: string): string {
let hash = 0;
for (let i = 0; i < groupKey.length; i += 1) {
hash = (hash * 31 + groupKey.charCodeAt(i)) | 0;
}
return tabGroupPalette[Math.abs(hash) % tabGroupPalette.length];
}
export function tabGroupColor(tab: Pick<QueryTab, "connectionId" | "database">): string {
return colorForTabGroupKey(tabGroupKey(tab));
}
export function tabGroupLabel(tab: Pick<QueryTab, "connectionId" | "database">): string {
const connection = connectionDisplayName(tab.connectionId);
const database = databaseDisplayNameForTab(tab.connectionId, tab.database);
+6
View File
@@ -195,6 +195,7 @@ export interface EditorSettings {
autoSelectActiveSidebarNode: boolean;
sidebarHiddenTablePrefixes: string[];
sidebarHideTableComments: boolean;
sidebarAllowHorizontalScroll: boolean;
columnFormatters: Record<string, ColumnFormatterConfig>;
customColumnFormatters: Record<string, CustomColumnFormatterConfig>;
snippets: SqlSnippet[];
@@ -248,6 +249,7 @@ export const DEFAULT_EDITOR_SETTINGS: EditorSettings = {
autoSelectActiveSidebarNode: false,
sidebarHiddenTablePrefixes: [],
sidebarHideTableComments: false,
sidebarAllowHorizontalScroll: false,
columnFormatters: {},
customColumnFormatters: {},
snippets: DEFAULT_SQL_SNIPPETS,
@@ -361,6 +363,8 @@ export function normalizeEditorSettings(settings: Partial<EditorSettings>, exist
settings.autoSelectActiveSidebarNode ?? DEFAULT_EDITOR_SETTINGS.autoSelectActiveSidebarNode,
sidebarHiddenTablePrefixes: normalizeSidebarHiddenTablePrefixes(settings.sidebarHiddenTablePrefixes),
sidebarHideTableComments: settings.sidebarHideTableComments ?? DEFAULT_EDITOR_SETTINGS.sidebarHideTableComments,
sidebarAllowHorizontalScroll:
settings.sidebarAllowHorizontalScroll ?? DEFAULT_EDITOR_SETTINGS.sidebarAllowHorizontalScroll,
columnFormatters: normalizeColumnFormatters(settings.columnFormatters),
customColumnFormatters: normalizeCustomColumnFormatters(settings.customColumnFormatters),
snippets: normalizeSqlSnippets(settings.snippets, existing?.snippets),
@@ -496,6 +500,8 @@ export const useSettingsStore = defineStore("settings", () => {
);
if (partial.sidebarHideTableComments !== undefined)
editorSettings.value.sidebarHideTableComments = partial.sidebarHideTableComments;
if (partial.sidebarAllowHorizontalScroll !== undefined)
editorSettings.value.sidebarAllowHorizontalScroll = partial.sidebarAllowHorizontalScroll;
if (partial.columnFormatters !== undefined) editorSettings.value.columnFormatters = partial.columnFormatters;
if (partial.customColumnFormatters !== undefined)
editorSettings.value.customColumnFormatters = partial.customColumnFormatters;
+12
View File
@@ -76,6 +76,11 @@ test("defaults active tab sidebar selection to off", () => {
assert.equal(normalizeEditorSettings({}).autoSelectActiveSidebarNode, false);
});
test("defaults sidebar horizontal scroll to off", () => {
assert.equal(DEFAULT_EDITOR_SETTINGS.sidebarAllowHorizontalScroll, false);
assert.equal(normalizeEditorSettings({}).sidebarAllowHorizontalScroll, false);
});
test("defaults data grid header display settings", () => {
assert.equal(DEFAULT_EDITOR_SETTINGS.showColumnCommentsInHeader, false);
assert.equal(DEFAULT_EDITOR_SETTINGS.compactColumnHeaderActions, true);
@@ -119,6 +124,13 @@ test("keeps saved active tab sidebar selection", () => {
assert.equal(normalizeEditorSettings({ autoSelectActiveSidebarNode: true } as any).autoSelectActiveSidebarNode, true);
});
test("keeps saved sidebar horizontal scroll preference", () => {
assert.equal(
normalizeEditorSettings({ sidebarAllowHorizontalScroll: true } as any).sidebarAllowHorizontalScroll,
true,
);
});
test("keeps saved sidebar activation", () => {
assert.equal(normalizeEditorSettings({ sidebarActivation: "double" } as any).sidebarActivation, "double");
assert.equal(normalizeEditorSettings({ sidebarActivation: "invalid" } as any).sidebarActivation, "single");
@@ -37,12 +37,13 @@ test("canTreeNodeShowExpander hides empty saved SQL containers", () => {
assert.equal(canTreeNodeShowExpander({ type: "saved-sql-folder", childCount: 1 }), true);
});
test("usesFullWidthTreeLabel only expands object names that users need to read horizontally", () => {
assert.equal(usesFullWidthTreeLabel("table"), true);
assert.equal(usesFullWidthTreeLabel("view"), true);
assert.equal(usesFullWidthTreeLabel("mongo-collection"), true);
test("usesFullWidthTreeLabel only expands object names when horizontal scroll is enabled", () => {
assert.equal(usesFullWidthTreeLabel("table", true), true);
assert.equal(usesFullWidthTreeLabel("view", true), true);
assert.equal(usesFullWidthTreeLabel("mongo-collection", true), true);
assert.equal(usesFullWidthTreeLabel("connection"), false);
assert.equal(usesFullWidthTreeLabel("schema"), false);
assert.equal(usesFullWidthTreeLabel("column"), false);
assert.equal(usesFullWidthTreeLabel("table", false), false);
assert.equal(usesFullWidthTreeLabel("connection", true), false);
assert.equal(usesFullWidthTreeLabel("schema", true), false);
assert.equal(usesFullWidthTreeLabel("column", true), false);
});
+1 -18
View File
@@ -1,11 +1,6 @@
import { strict as assert } from "node:assert";
import test from "node:test";
import {
colorForTabGroupKey,
shouldShowTabOverflowControls,
tabGroupColor,
tabGroupKey,
} from "../../apps/desktop/src/lib/tabPresentation.ts";
import { shouldShowTabOverflowControls } from "../../apps/desktop/src/lib/tabPresentation.ts";
test("tab overflow controls only show when there are hidden tabs to reach", () => {
assert.equal(shouldShowTabOverflowControls(0, true, true, true), false);
@@ -14,15 +9,3 @@ test("tab overflow controls only show when there are hidden tabs to reach", () =
assert.equal(shouldShowTabOverflowControls(3, false, true, false), true);
assert.equal(shouldShowTabOverflowControls(3, false, false, true), true);
});
test("tab group key and color are stable per connection database pair", () => {
const first = { connectionId: "local", database: "orders" };
const same = { connectionId: "local", database: "orders" };
const other = { connectionId: "local", database: "billing" };
assert.equal(tabGroupKey(first), "local::orders");
assert.equal(tabGroupKey(first), tabGroupKey(same));
assert.notEqual(tabGroupKey(first), tabGroupKey(other));
assert.equal(tabGroupColor(first), tabGroupColor(same));
assert.equal(colorForTabGroupKey(tabGroupKey(first)), tabGroupColor(first));
});