mirror of
https://github.com/t8y2/dbx.git
synced 2026-10-19 16:16:45 +00:00
Refine overflow navigation review feedback
This commit is contained in:
@@ -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
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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,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));
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user