feat(sidebar): select active tab node
This commit is contained in:
parent
a159dcfc83
commit
3dc4f40e7f
|
|
@ -58,6 +58,7 @@ const editAppLayout = ref(settingsStore.editorSettings.appLayout);
|
|||
const editRedisScanPageSize = ref(settingsStore.editorSettings.redisScanPageSize);
|
||||
const editShortcuts = ref(normalizeShortcutSettings(settingsStore.editorSettings.shortcuts));
|
||||
const editSidebarActivation = ref(settingsStore.editorSettings.sidebarActivation);
|
||||
const editAutoSelectActiveSidebarNode = ref(settingsStore.editorSettings.autoSelectActiveSidebarNode);
|
||||
const editSidebarHiddenTablePrefixes = ref(settingsStore.editorSettings.sidebarHiddenTablePrefixes.join("\n"));
|
||||
const redisScanPageSizeOptions = [200, 1000, 5000, 10000];
|
||||
const systemFonts = ref<string[]>([]);
|
||||
|
|
@ -120,6 +121,7 @@ watch(
|
|||
editRedisScanPageSize.value = settingsStore.editorSettings.redisScanPageSize;
|
||||
editShortcuts.value = normalizeShortcutSettings(settingsStore.editorSettings.shortcuts);
|
||||
editSidebarActivation.value = settingsStore.editorSettings.sidebarActivation;
|
||||
editAutoSelectActiveSidebarNode.value = settingsStore.editorSettings.autoSelectActiveSidebarNode;
|
||||
editSidebarHiddenTablePrefixes.value = settingsStore.editorSettings.sidebarHiddenTablePrefixes.join("\n");
|
||||
void loadSystemFontOptions();
|
||||
}
|
||||
|
|
@ -133,6 +135,10 @@ const shortcutConflicts = computed(() =>
|
|||
}),
|
||||
);
|
||||
const hasShortcutConflicts = computed(() => shortcutConflicts.value.length > 0);
|
||||
const shortcutsChanged = computed(
|
||||
() => JSON.stringify(editShortcuts.value) !== JSON.stringify(settingsStore.editorSettings.shortcuts),
|
||||
);
|
||||
const hasBlockingShortcutConflicts = computed(() => shortcutsChanged.value && hasShortcutConflicts.value);
|
||||
|
||||
function hasChanges(): boolean {
|
||||
return (
|
||||
|
|
@ -145,13 +151,14 @@ function hasChanges(): boolean {
|
|||
editRedisScanPageSize.value !== settingsStore.editorSettings.redisScanPageSize ||
|
||||
JSON.stringify(editShortcuts.value) !== JSON.stringify(settingsStore.editorSettings.shortcuts) ||
|
||||
editSidebarActivation.value !== settingsStore.editorSettings.sidebarActivation ||
|
||||
editAutoSelectActiveSidebarNode.value !== settingsStore.editorSettings.autoSelectActiveSidebarNode ||
|
||||
JSON.stringify(normalizeSidebarHiddenTablePrefixes(editSidebarHiddenTablePrefixes.value)) !==
|
||||
JSON.stringify(settingsStore.editorSettings.sidebarHiddenTablePrefixes)
|
||||
);
|
||||
}
|
||||
|
||||
function applySettings() {
|
||||
if (hasShortcutConflicts.value) return;
|
||||
if (hasBlockingShortcutConflicts.value) return;
|
||||
settingsStore.updateEditorSettings({
|
||||
fontFamily: editFontFamily.value,
|
||||
fontSize: editFontSize.value,
|
||||
|
|
@ -162,6 +169,7 @@ function applySettings() {
|
|||
redisScanPageSize: editRedisScanPageSize.value,
|
||||
shortcuts: editShortcuts.value,
|
||||
sidebarActivation: editSidebarActivation.value,
|
||||
autoSelectActiveSidebarNode: editAutoSelectActiveSidebarNode.value,
|
||||
sidebarHiddenTablePrefixes: normalizeSidebarHiddenTablePrefixes(editSidebarHiddenTablePrefixes.value),
|
||||
});
|
||||
emit("update:open", false);
|
||||
|
|
@ -177,6 +185,7 @@ function resetDefaults() {
|
|||
editRedisScanPageSize.value = DEFAULT_EDITOR_SETTINGS.redisScanPageSize;
|
||||
editShortcuts.value = normalizeShortcutSettings(DEFAULT_EDITOR_SETTINGS.shortcuts);
|
||||
editSidebarActivation.value = DEFAULT_EDITOR_SETTINGS.sidebarActivation;
|
||||
editAutoSelectActiveSidebarNode.value = DEFAULT_EDITOR_SETTINGS.autoSelectActiveSidebarNode;
|
||||
editSidebarHiddenTablePrefixes.value = DEFAULT_EDITOR_SETTINGS.sidebarHiddenTablePrefixes.join("\n");
|
||||
}
|
||||
|
||||
|
|
@ -754,7 +763,7 @@ watch(
|
|||
<Label for="editor-word-wrap">{{ t("settings.wordWrap") }}</Label>
|
||||
<p class="text-xs text-muted-foreground">{{ t("settings.wordWrapDescription") }}</p>
|
||||
</div>
|
||||
<Switch id="editor-word-wrap" v-model:checked="editWordWrap" class="mt-0.5" />
|
||||
<Switch id="editor-word-wrap" v-model="editWordWrap" class="mt-0.5" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
|
@ -842,6 +851,15 @@ watch(
|
|||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex items-center justify-between gap-4 rounded-md border bg-muted/20 px-3 py-2">
|
||||
<div class="space-y-1">
|
||||
<Label for="auto-select-active-sidebar-node">{{ t("settings.autoSelectActiveSidebarNode") }}</Label>
|
||||
<p class="text-xs text-muted-foreground">
|
||||
{{ t("settings.autoSelectActiveSidebarNodeDescription") }}
|
||||
</p>
|
||||
</div>
|
||||
<Switch id="auto-select-active-sidebar-node" v-model="editAutoSelectActiveSidebarNode" />
|
||||
</div>
|
||||
<div class="space-y-2">
|
||||
<Label for="sidebar-hidden-table-prefixes">{{ t("settings.sidebarHiddenTablePrefixes") }}</Label>
|
||||
<textarea
|
||||
|
|
@ -1224,7 +1242,7 @@ watch(
|
|||
<Button variant="outline" @click="emit('update:open', false)">
|
||||
{{ t("common.close") }}
|
||||
</Button>
|
||||
<Button :disabled="!hasChanges() || hasShortcutConflicts" @click="applySettings">
|
||||
<Button :disabled="!hasChanges() || hasBlockingShortcutConflicts" @click="applySettings">
|
||||
{{ t("settings.apply") }}
|
||||
</Button>
|
||||
</DialogFooter>
|
||||
|
|
|
|||
|
|
@ -3,9 +3,12 @@ import { ref, computed, nextTick, watch } from "vue";
|
|||
import { useI18n } from "vue-i18n";
|
||||
import { Search, X, ListFilter, Check, FolderPlus } from "lucide-vue-next";
|
||||
import { useConnectionStore } from "@/stores/connectionStore";
|
||||
import { useQueryStore } from "@/stores/queryStore";
|
||||
import { useSettingsStore } from "@/stores/settingsStore";
|
||||
import type { TreeNode, TreeNodeType } from "@/types/database";
|
||||
import { filterSidebarTree } from "@/lib/sidebarSearchTree";
|
||||
import { isCancelSearchShortcut } from "@/lib/keyboardShortcuts";
|
||||
import { findSidebarNodeForActiveTab, scrollTopForSidebarNode } from "@/lib/sidebarActiveTabTarget";
|
||||
import {
|
||||
SIDEBAR_TREE_ROW_HEIGHT,
|
||||
SIDEBAR_TREE_PRERENDER_COUNT,
|
||||
|
|
@ -29,10 +32,13 @@ import {
|
|||
|
||||
const { t } = useI18n();
|
||||
const store = useConnectionStore();
|
||||
const queryStore = useQueryStore();
|
||||
const settingsStore = useSettingsStore();
|
||||
const searchQuery = ref("");
|
||||
const deferredSearchQuery = ref("");
|
||||
const searchInputRef = ref<HTMLInputElement>();
|
||||
const treeScrollerRef = ref<InstanceType<typeof RecycleScroller> | null>(null);
|
||||
const plainTreeScrollerRef = ref<HTMLElement | null>(null);
|
||||
type SearchScope = "connection" | "database" | "schema" | "table" | "view";
|
||||
const selectedSearchScopes = ref<SearchScope[]>([]);
|
||||
const searchCollapsedIds = ref<Set<string>>(new Set());
|
||||
|
|
@ -119,6 +125,7 @@ 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 pendingRenameGroupId = ref<string | null>(null);
|
||||
|
||||
|
|
@ -157,6 +164,43 @@ async function onNodeToggled(node: TreeNode, wasExpanded: boolean) {
|
|||
}
|
||||
}
|
||||
|
||||
function currentTreeScroller(): HTMLElement | null {
|
||||
return (
|
||||
((useVirtualTree.value ? treeScrollerRef.value?.$el : plainTreeScrollerRef.value) as HTMLElement | undefined) ??
|
||||
null
|
||||
);
|
||||
}
|
||||
|
||||
async function selectActiveTabSidebarNode() {
|
||||
if (!settingsStore.editorSettings.autoSelectActiveSidebarNode) return;
|
||||
const match = findSidebarNodeForActiveTab(activeTab.value, flatNodes.value);
|
||||
if (!match) return;
|
||||
|
||||
store.selectedTreeNodeId = match.id;
|
||||
await nextTick();
|
||||
|
||||
const index = flatNodes.value.findIndex((item) => item.id === match.id);
|
||||
const scroller = currentTreeScroller();
|
||||
if (!scroller || index < 0) return;
|
||||
|
||||
const nextScrollTop = scrollTopForSidebarNode({
|
||||
index,
|
||||
currentScrollTop: scroller.scrollTop,
|
||||
viewportHeight: scroller.clientHeight,
|
||||
});
|
||||
if (nextScrollTop !== scroller.scrollTop) {
|
||||
scroller.scrollTop = nextScrollTop;
|
||||
}
|
||||
}
|
||||
|
||||
watch(
|
||||
[activeTab, flatNodes, () => settingsStore.editorSettings.autoSelectActiveSidebarNode],
|
||||
() => {
|
||||
void selectActiveTabSidebarNode();
|
||||
},
|
||||
{ flush: "post" },
|
||||
);
|
||||
|
||||
function focusSearch(): boolean {
|
||||
const input = searchInputRef.value;
|
||||
if (!input) return false;
|
||||
|
|
@ -264,7 +308,11 @@ defineExpose({ focusSearch });
|
|||
/>
|
||||
</template>
|
||||
</RecycleScroller>
|
||||
<div v-else-if="flatNodes.length > 0" class="sidebar-tree min-h-0 flex-1 overflow-y-auto overflow-x-auto">
|
||||
<div
|
||||
v-else-if="flatNodes.length > 0"
|
||||
ref="plainTreeScrollerRef"
|
||||
class="sidebar-tree min-h-0 flex-1 overflow-y-auto overflow-x-auto"
|
||||
>
|
||||
<TreeItem
|
||||
v-for="item in flatNodes"
|
||||
:key="item.id"
|
||||
|
|
|
|||
|
|
@ -1198,6 +1198,9 @@ export default {
|
|||
sidebarActivationSingleDescription: "Open actionable sidebar items with one click.",
|
||||
sidebarActivationDouble: "Double click",
|
||||
sidebarActivationDoubleDescription: "Single click selects rows; double click opens items.",
|
||||
autoSelectActiveSidebarNode: "Always select opened item",
|
||||
autoSelectActiveSidebarNodeDescription:
|
||||
"When switching tabs, select the matching visible table, collection, or SQL file in the sidebar.",
|
||||
sidebarHiddenTablePrefixes: "Hidden table name prefixes",
|
||||
sidebarHiddenTablePrefixesDescription:
|
||||
"One prefix per line. Only sidebar table, view, and collection labels are shortened; tooltips and actions still use the full name.",
|
||||
|
|
|
|||
|
|
@ -1096,6 +1096,9 @@ export default {
|
|||
sidebarActivationSingleDescription: "Abrir elementos accionables de la barra lateral con un clic.",
|
||||
sidebarActivationDouble: "Doble clic",
|
||||
sidebarActivationDoubleDescription: "Un clic selecciona la fila; doble clic abre elementos.",
|
||||
autoSelectActiveSidebarNode: "Seleccionar siempre el elemento abierto",
|
||||
autoSelectActiveSidebarNodeDescription:
|
||||
"Al cambiar de pestaña, selecciona la tabla, colección o archivo SQL visible correspondiente en la barra lateral.",
|
||||
sidebarHiddenTablePrefixes: "Prefijos ocultos de tablas",
|
||||
sidebarHiddenTablePrefixesDescription:
|
||||
"Un prefijo por linea. Solo acorta etiquetas de tablas, vistas y colecciones en la barra lateral; las acciones y ayudas usan el nombre completo.",
|
||||
|
|
|
|||
|
|
@ -1176,6 +1176,8 @@ export default {
|
|||
sidebarActivationSingleDescription: "单击即可打开侧边栏中的可操作项目。",
|
||||
sidebarActivationDouble: "双击打开",
|
||||
sidebarActivationDoubleDescription: "单击只选中高亮,双击打开项目。",
|
||||
autoSelectActiveSidebarNode: "始终选中已打开项目",
|
||||
autoSelectActiveSidebarNodeDescription: "切换标签页时,在侧边栏选中匹配的可见表、集合或 SQL 文件。",
|
||||
sidebarHiddenTablePrefixes: "隐藏表名前缀",
|
||||
sidebarHiddenTablePrefixesDescription:
|
||||
"每行一个前缀,仅影响侧边栏表、视图和集合的显示名称,悬浮提示和实际操作仍使用完整名称。",
|
||||
|
|
|
|||
|
|
@ -0,0 +1,110 @@
|
|||
import { SIDEBAR_TREE_ROW_HEIGHT, type FlatTreeNode } from "@/composables/useFlatTree";
|
||||
import type { QueryTab, TreeNode } from "@/types/database";
|
||||
|
||||
export type ActiveTabSidebarTarget =
|
||||
| {
|
||||
type: "table";
|
||||
connectionId: string;
|
||||
database: string;
|
||||
schema?: string;
|
||||
tableName: string;
|
||||
}
|
||||
| {
|
||||
type: "mongo-collection";
|
||||
connectionId: string;
|
||||
database: string;
|
||||
collectionName: string;
|
||||
}
|
||||
| {
|
||||
type: "saved-sql-file";
|
||||
savedSqlId: string;
|
||||
};
|
||||
|
||||
export function activeTabSidebarTarget(tab: QueryTab | undefined | null): ActiveTabSidebarTarget | null {
|
||||
if (!tab) return null;
|
||||
|
||||
if (tab.savedSqlId) {
|
||||
return { type: "saved-sql-file", savedSqlId: tab.savedSqlId };
|
||||
}
|
||||
|
||||
if (tab.mode === "data") {
|
||||
const tableName = tab.tableMeta?.tableName || tab.title;
|
||||
if (!tableName) return null;
|
||||
return {
|
||||
type: "table",
|
||||
connectionId: tab.connectionId,
|
||||
database: tab.database,
|
||||
schema: tab.tableMeta?.schema ?? tab.schema,
|
||||
tableName,
|
||||
};
|
||||
}
|
||||
|
||||
if (tab.mode === "mongo") {
|
||||
const collectionName = tab.sql || tab.title.split(".").pop() || tab.title;
|
||||
if (!collectionName) return null;
|
||||
return {
|
||||
type: "mongo-collection",
|
||||
connectionId: tab.connectionId,
|
||||
database: tab.database,
|
||||
collectionName,
|
||||
};
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
function schemaMatches(node: TreeNode, schema: string | undefined): boolean {
|
||||
if (!schema) return true;
|
||||
return (node.schema || "") === schema;
|
||||
}
|
||||
|
||||
function matchesTarget(node: TreeNode, target: ActiveTabSidebarTarget): boolean {
|
||||
if (target.type === "saved-sql-file") {
|
||||
return node.type === "saved-sql-file" && node.savedSqlId === target.savedSqlId;
|
||||
}
|
||||
|
||||
if (target.type === "mongo-collection") {
|
||||
return (
|
||||
node.type === "mongo-collection" &&
|
||||
node.connectionId === target.connectionId &&
|
||||
node.database === target.database &&
|
||||
node.label === target.collectionName
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
(node.type === "table" || node.type === "view") &&
|
||||
node.connectionId === target.connectionId &&
|
||||
node.database === target.database &&
|
||||
schemaMatches(node, target.schema) &&
|
||||
node.label === target.tableName
|
||||
);
|
||||
}
|
||||
|
||||
export function findSidebarNodeForActiveTab(
|
||||
tab: QueryTab | undefined | null,
|
||||
flatNodes: readonly FlatTreeNode[],
|
||||
): FlatTreeNode | null {
|
||||
const target = activeTabSidebarTarget(tab);
|
||||
if (!target) return null;
|
||||
return flatNodes.find((item) => matchesTarget(item.node, target)) ?? null;
|
||||
}
|
||||
|
||||
export function scrollTopForSidebarNode(options: {
|
||||
index: number;
|
||||
currentScrollTop: number;
|
||||
viewportHeight: number;
|
||||
rowHeight?: number;
|
||||
}): number {
|
||||
const rowHeight = options.rowHeight ?? SIDEBAR_TREE_ROW_HEIGHT;
|
||||
if (options.index < 0 || options.viewportHeight <= 0) return options.currentScrollTop;
|
||||
|
||||
const rowTop = options.index * rowHeight;
|
||||
const rowBottom = rowTop + rowHeight;
|
||||
const viewportTop = options.currentScrollTop;
|
||||
const viewportBottom = options.currentScrollTop + options.viewportHeight;
|
||||
|
||||
if (rowTop < viewportTop) return rowTop;
|
||||
if (rowBottom > viewportBottom) return Math.max(0, rowBottom - options.viewportHeight);
|
||||
return options.currentScrollTop;
|
||||
}
|
||||
|
|
@ -171,6 +171,7 @@ export interface EditorSettings {
|
|||
mongoViewMode: "document" | "table";
|
||||
shortcuts: ShortcutSettings;
|
||||
sidebarActivation: SidebarActivation;
|
||||
autoSelectActiveSidebarNode: boolean;
|
||||
sidebarHiddenTablePrefixes: string[];
|
||||
columnFormatters: Record<string, ColumnFormatterConfig>;
|
||||
customColumnFormatters: Record<string, CustomColumnFormatterConfig>;
|
||||
|
|
@ -211,6 +212,7 @@ export const DEFAULT_EDITOR_SETTINGS: EditorSettings = {
|
|||
mongoViewMode: "document",
|
||||
shortcuts: normalizeShortcutSettings(),
|
||||
sidebarActivation: "single",
|
||||
autoSelectActiveSidebarNode: false,
|
||||
sidebarHiddenTablePrefixes: [],
|
||||
columnFormatters: {},
|
||||
customColumnFormatters: {},
|
||||
|
|
@ -256,6 +258,8 @@ export function normalizeEditorSettings(settings: Partial<EditorSettings>): Edit
|
|||
settings.sidebarActivation === "single" || settings.sidebarActivation === "double"
|
||||
? settings.sidebarActivation
|
||||
: DEFAULT_EDITOR_SETTINGS.sidebarActivation,
|
||||
autoSelectActiveSidebarNode:
|
||||
settings.autoSelectActiveSidebarNode ?? DEFAULT_EDITOR_SETTINGS.autoSelectActiveSidebarNode,
|
||||
sidebarHiddenTablePrefixes: normalizeSidebarHiddenTablePrefixes(settings.sidebarHiddenTablePrefixes),
|
||||
columnFormatters: normalizeColumnFormatters(settings.columnFormatters),
|
||||
customColumnFormatters: normalizeCustomColumnFormatters(settings.customColumnFormatters),
|
||||
|
|
|
|||
|
|
@ -44,10 +44,23 @@ test("shortcut settings capture custom keydown input instead of fixed select opt
|
|||
assert.doesNotMatch(source, /definition\.options/);
|
||||
});
|
||||
|
||||
test("shortcut conflicts only block applying changed shortcut settings", () => {
|
||||
assert.match(source, /const shortcutsChanged = computed/);
|
||||
assert.match(source, /const hasBlockingShortcutConflicts = computed/);
|
||||
assert.match(source, /shortcutsChanged\.value && hasShortcutConflicts\.value/);
|
||||
assert.match(source, /if \(hasBlockingShortcutConflicts\.value\) return/);
|
||||
assert.match(source, /:disabled="!hasChanges\(\) \|\| hasBlockingShortcutConflicts"/);
|
||||
});
|
||||
|
||||
test("settings dialog exposes sidebar activation in navigation settings", () => {
|
||||
assert.match(source, /value: "navigation"/);
|
||||
assert.match(source, /activeSettingsTab === ['"]navigation['"]/);
|
||||
assert.match(source, /settings\.sidebarActivation/);
|
||||
assert.match(source, /settings\.autoSelectActiveSidebarNode/);
|
||||
assert.match(source, /editAutoSelectActiveSidebarNode/);
|
||||
assert.match(source, /<Switch id="auto-select-active-sidebar-node" v-model="editAutoSelectActiveSidebarNode"/);
|
||||
assert.match(source, /<Switch id="editor-word-wrap" v-model="editWordWrap"/);
|
||||
assert.doesNotMatch(source, /v-model:checked/);
|
||||
assert.match(source, /settings\.sidebarHiddenTablePrefixes/);
|
||||
assert.match(source, /editSidebarHiddenTablePrefixes/);
|
||||
assert.match(source, /focus-visible:ring-inset/);
|
||||
|
|
|
|||
|
|
@ -35,7 +35,9 @@ test("defaults shortcut settings", () => {
|
|||
});
|
||||
|
||||
test("keeps saved shortcut overrides", () => {
|
||||
const settings = normalizeEditorSettings({ shortcuts: { executeSql: "Shift+Mod+Enter", newQuery: "Shift+Mod+N" } as any });
|
||||
const settings = normalizeEditorSettings({
|
||||
shortcuts: { executeSql: "Shift+Mod+Enter", newQuery: "Shift+Mod+N" } as any,
|
||||
});
|
||||
|
||||
assert.equal(settings.shortcuts.executeSql, "Shift+Mod+Enter");
|
||||
assert.equal(settings.shortcuts.newQuery, "Shift+Mod+N");
|
||||
|
|
@ -47,6 +49,15 @@ test("defaults sidebar activation to single click", () => {
|
|||
assert.equal(normalizeEditorSettings({}).sidebarActivation, "single");
|
||||
});
|
||||
|
||||
test("defaults active tab sidebar selection to off", () => {
|
||||
assert.equal(DEFAULT_EDITOR_SETTINGS.autoSelectActiveSidebarNode, false);
|
||||
assert.equal(normalizeEditorSettings({}).autoSelectActiveSidebarNode, false);
|
||||
});
|
||||
|
||||
test("keeps saved active tab sidebar selection", () => {
|
||||
assert.equal(normalizeEditorSettings({ autoSelectActiveSidebarNode: true } as any).autoSelectActiveSidebarNode, true);
|
||||
});
|
||||
|
||||
test("keeps saved sidebar activation", () => {
|
||||
assert.equal(normalizeEditorSettings({ sidebarActivation: "double" } as any).sidebarActivation, "double");
|
||||
assert.equal(normalizeEditorSettings({ sidebarActivation: "invalid" } as any).sidebarActivation, "single");
|
||||
|
|
|
|||
|
|
@ -0,0 +1,124 @@
|
|||
import { strict as assert } from "node:assert";
|
||||
import test from "node:test";
|
||||
import {
|
||||
activeTabSidebarTarget,
|
||||
findSidebarNodeForActiveTab,
|
||||
scrollTopForSidebarNode,
|
||||
} from "../../apps/desktop/src/lib/sidebarActiveTabTarget.ts";
|
||||
import type { FlatTreeNode } from "../../apps/desktop/src/composables/useFlatTree.ts";
|
||||
import type { QueryTab, TreeNode } from "../../apps/desktop/src/types/database.ts";
|
||||
|
||||
function flat(node: TreeNode, depth = 0): FlatTreeNode {
|
||||
return { id: node.id, node, depth, type: node.type };
|
||||
}
|
||||
|
||||
test("data tabs target the matching visible table or view node", () => {
|
||||
const tab: QueryTab = {
|
||||
id: "tab-1",
|
||||
title: "users",
|
||||
connectionId: "conn-1",
|
||||
database: "app",
|
||||
sql: "",
|
||||
isExecuting: false,
|
||||
mode: "data",
|
||||
tableMeta: { schema: "public", tableName: "users", columns: [], primaryKeys: [] },
|
||||
};
|
||||
const users: TreeNode = {
|
||||
id: "users-node",
|
||||
label: "users",
|
||||
type: "table",
|
||||
connectionId: "conn-1",
|
||||
database: "app",
|
||||
schema: "public",
|
||||
};
|
||||
|
||||
assert.deepEqual(activeTabSidebarTarget(tab), {
|
||||
type: "table",
|
||||
connectionId: "conn-1",
|
||||
database: "app",
|
||||
schema: "public",
|
||||
tableName: "users",
|
||||
});
|
||||
assert.equal(findSidebarNodeForActiveTab(tab, [flat(users)])?.id, "users-node");
|
||||
});
|
||||
|
||||
test("mongo tabs target the matching visible collection node", () => {
|
||||
const tab: QueryTab = {
|
||||
id: "tab-1",
|
||||
title: "app.events",
|
||||
connectionId: "conn-1",
|
||||
database: "app",
|
||||
sql: "events",
|
||||
isExecuting: false,
|
||||
mode: "mongo",
|
||||
};
|
||||
const collection: TreeNode = {
|
||||
id: "events-node",
|
||||
label: "events",
|
||||
type: "mongo-collection",
|
||||
connectionId: "conn-1",
|
||||
database: "app",
|
||||
};
|
||||
|
||||
assert.equal(findSidebarNodeForActiveTab(tab, [flat(collection)])?.id, "events-node");
|
||||
});
|
||||
|
||||
test("saved SQL tabs target the matching visible saved SQL file node", () => {
|
||||
const tab: QueryTab = {
|
||||
id: "tab-1",
|
||||
title: "report.sql",
|
||||
connectionId: "conn-1",
|
||||
database: "app",
|
||||
sql: "select 1",
|
||||
savedSqlId: "sql-1",
|
||||
isExecuting: false,
|
||||
mode: "query",
|
||||
};
|
||||
const file: TreeNode = { id: "file-node", label: "report.sql", type: "saved-sql-file", savedSqlId: "sql-1" };
|
||||
|
||||
assert.deepEqual(activeTabSidebarTarget(tab), { type: "saved-sql-file", savedSqlId: "sql-1" });
|
||||
assert.equal(findSidebarNodeForActiveTab(tab, [flat(file)])?.id, "file-node");
|
||||
});
|
||||
|
||||
test("query tabs without a saved SQL file have no sidebar target", () => {
|
||||
const tab: QueryTab = {
|
||||
id: "tab-1",
|
||||
title: "Query 1",
|
||||
connectionId: "conn-1",
|
||||
database: "app",
|
||||
sql: "select 1",
|
||||
isExecuting: false,
|
||||
mode: "query",
|
||||
};
|
||||
|
||||
assert.equal(activeTabSidebarTarget(tab), null);
|
||||
assert.equal(findSidebarNodeForActiveTab(tab, []), null);
|
||||
});
|
||||
|
||||
test("sidebar target lookup only uses the current flat visible tree", () => {
|
||||
const tab: QueryTab = {
|
||||
id: "tab-1",
|
||||
title: "users",
|
||||
connectionId: "conn-1",
|
||||
database: "app",
|
||||
sql: "",
|
||||
isExecuting: false,
|
||||
mode: "data",
|
||||
tableMeta: { tableName: "users", columns: [], primaryKeys: [] },
|
||||
};
|
||||
const collapsedParentOnly: TreeNode = {
|
||||
id: "db-node",
|
||||
label: "app",
|
||||
type: "database",
|
||||
connectionId: "conn-1",
|
||||
database: "app",
|
||||
};
|
||||
|
||||
assert.equal(findSidebarNodeForActiveTab(tab, [flat(collapsedParentOnly)]), null);
|
||||
});
|
||||
|
||||
test("sidebar node scrolling keeps visible rows in place and reveals hidden rows", () => {
|
||||
assert.equal(scrollTopForSidebarNode({ index: 2, currentScrollTop: 0, viewportHeight: 140 }), 0);
|
||||
assert.equal(scrollTopForSidebarNode({ index: 20, currentScrollTop: 0, viewportHeight: 140 }), 448);
|
||||
assert.equal(scrollTopForSidebarNode({ index: 1, currentScrollTop: 280, viewportHeight: 140 }), 28);
|
||||
});
|
||||
|
|
@ -15,3 +15,10 @@ test("connection tree filter menu uses sidebar search scope i18n labels", () =>
|
|||
assert.match(source, /t\("sidebar\.searchScopeDatabase"\)/);
|
||||
assert.match(source, /t\("sidebar\.searchScopeTable"\)/);
|
||||
});
|
||||
|
||||
test("connection tree can select visible sidebar nodes for the active tab when enabled", () => {
|
||||
assert.match(source, /autoSelectActiveSidebarNode/);
|
||||
assert.match(source, /findSidebarNodeForActiveTab\(activeTab\.value, flatNodes\.value\)/);
|
||||
assert.match(source, /store\.selectedTreeNodeId = match\.id/);
|
||||
assert.match(source, /scrollTopForSidebarNode/);
|
||||
});
|
||||
|
|
|
|||
Loading…
Reference in New Issue