mirror of
https://github.com/t8y2/dbx.git
synced 2026-10-19 16:16:45 +00:00
feat(sidebar): add local table search
This commit is contained in:
@@ -6,8 +6,10 @@ import { useConnectionStore } from "@/stores/connectionStore";
|
||||
import { useQueryStore } from "@/stores/queryStore";
|
||||
import { useSettingsStore } from "@/stores/settingsStore";
|
||||
import { useToast } from "@/composables/useToast";
|
||||
import type { ObjectSourceKind, TableNameFilter, TreeNode, TreeNodeType } from "@/types/database";
|
||||
import type { ObjectSourceKind, TableInfo, TableNameFilter, TreeNode, TreeNodeType } from "@/types/database";
|
||||
import { filterSidebarSearchRootsByConnectionState, filterSidebarTree, filterSidebarTreeToConnectedConnections, resolveSidebarFilterGuards } from "@/lib/sidebar/sidebarSearchTree";
|
||||
import { matchSidebarLabel } from "@/lib/sidebar/sidebarSearch";
|
||||
import { buildTableTreeNodes } from "@/lib/table/tableTree";
|
||||
import { isCancelSearchShortcut, isCopySidebarSelectionShortcut, isEditSidebarConnectionShortcut, isPasteSidebarSelectionShortcut } from "@/lib/editor/keyboardShortcuts";
|
||||
import { copyNameForTreeNode, objectSourceKindForTreeNode } from "@/lib/sidebar/treeNodeClick";
|
||||
import { copyToClipboard } from "@/lib/common/clipboard";
|
||||
@@ -29,6 +31,8 @@ import InstallExtensionDialog from "@/components/objects/InstallExtensionDialog.
|
||||
import { RecycleScroller } from "vue-virtual-scroller";
|
||||
import "vue-virtual-scroller/dist/vue-virtual-scroller.css";
|
||||
import LightDropdown from "@/components/ui/LightDropdown.vue";
|
||||
import LightTooltip from "@/components/ui/LightTooltip.vue";
|
||||
import { Switch } from "@/components/ui/switch";
|
||||
import { cancelPendingSidebarDataOpen, runSidebarDataOpenImmediately, type SidebarDataOpenRequest } from "@/lib/sidebar/sidebarDataOpenCoordinator";
|
||||
import CustomContextMenu, { type ContextMenuItem } from "@/components/ui/CustomContextMenu.vue";
|
||||
import { Button } from "@/components/ui/button";
|
||||
@@ -111,6 +115,7 @@ const tableSearchTimers = new Map<string, number>();
|
||||
const tableSearchFocusRestoreTokens = new Map<string, number>();
|
||||
let tableSearchFocusRestoreTokenSeq = 0;
|
||||
let latestTableSearchInteractionParentId: string | null = null;
|
||||
let localTableSearchFocusPending = false;
|
||||
|
||||
watch(
|
||||
searchQuery,
|
||||
@@ -157,6 +162,10 @@ watch(
|
||||
|
||||
watch(deferredSearchQuery, (newQuery, oldQuery) => {
|
||||
store.sidebarSearchQuery = newQuery;
|
||||
if (settingsStore.editorSettings.sidebarGlobalSearchLocal) {
|
||||
if (!newQuery && oldQuery) searchRefreshedNodeIds.clear();
|
||||
return;
|
||||
}
|
||||
const tasks: Promise<void>[] = [];
|
||||
for (const root of store.treeNodes) {
|
||||
collectExpandedObjectSearchTargets(root, tasks, newQuery ? searchRefreshedNodeIds : undefined);
|
||||
@@ -357,6 +366,36 @@ function focusTableSearchInput(parentNodeId: string) {
|
||||
}
|
||||
|
||||
const displayedTreeNodes = computed(() => sortConnectionListForDisplay(store.treeNodes, settingsStore.editorSettings.sidebarConnectionSortMode));
|
||||
const localTableSearchResults = ref<Record<string, TableInfo[] | null>>({});
|
||||
|
||||
const localTableSearchParentTypes = new Set<TreeNodeType>(["database", "schema", "linked-server-schema", "group-tables"]);
|
||||
const localTableSearchChildTypes = new Set<TreeNodeType>(["table", "view", "materialized_view"]);
|
||||
|
||||
function filterLocallySearchedTables(nodes: TreeNode[]): TreeNode[] {
|
||||
return nodes.map((node) => {
|
||||
const children = node.children ? filterLocallySearchedTables(node.children) : undefined;
|
||||
const query = settingsStore.editorSettings.sidebarTableSearchLocal && localTableSearchParentTypes.has(node.type) ? store.sidebarTableSearchQueries[node.id]?.trim() : "";
|
||||
if (!query || !children) return children === node.children ? node : { ...node, children };
|
||||
|
||||
const indexed = localTableSearchResults.value[node.id];
|
||||
const matchingChildren =
|
||||
indexed === null
|
||||
? children.filter((child) => localTableSearchChildTypes.has(child.type) && !!matchSidebarLabel(child.label.toLowerCase(), query.toLowerCase()))
|
||||
: indexed
|
||||
? buildTableTreeNodes({ nodeId: node.id, connectionId: node.connectionId || "", database: node.database || "", schema: node.schema, catalog: node.catalog, tables: indexed.filter((entry) => !!matchSidebarLabel(entry.name.toLowerCase(), query.toLowerCase())) })
|
||||
: children.filter((child) => localTableSearchChildTypes.has(child.type) && !!matchSidebarLabel(child.label.toLowerCase(), query.toLowerCase()));
|
||||
return { ...node, children: matchingChildren, isExpanded: true };
|
||||
});
|
||||
}
|
||||
|
||||
async function loadLocalTableSearchResults(parentNodeId: string, refresh = false) {
|
||||
try {
|
||||
const entries = refresh ? await store.refreshSidebarTableSearchIndex(parentNodeId) : await store.loadSidebarTableSearchIndex(parentNodeId);
|
||||
localTableSearchResults.value = { ...localTableSearchResults.value, [parentNodeId]: entries };
|
||||
} finally {
|
||||
if (latestTableSearchInteractionParentId === parentNodeId) focusTableSearchInput(parentNodeId);
|
||||
}
|
||||
}
|
||||
|
||||
const filteredNodes = computed(() => {
|
||||
let nodes = displayedTreeNodes.value;
|
||||
@@ -364,6 +403,8 @@ const filteredNodes = computed(() => {
|
||||
nodes = filterSidebarTreeToConnectedConnections(nodes, store.connectedIds);
|
||||
}
|
||||
|
||||
nodes = filterLocallySearchedTables(nodes);
|
||||
|
||||
const q = deferredSearchQuery.value;
|
||||
nodes = filterSidebarTree(nodes, q, searchCollapsedIds.value, searchableNodeTypes.value);
|
||||
if (q) {
|
||||
@@ -626,7 +667,10 @@ watch(flatNodes, (nodes) => {
|
||||
}
|
||||
}
|
||||
stickyScrollTop.value = 0;
|
||||
void nextTick(scheduleSidebarScrollMetricsUpdate);
|
||||
void nextTick(() => {
|
||||
treeScrollerRef.value?.forceUpdate(true);
|
||||
scheduleSidebarScrollMetricsUpdate();
|
||||
});
|
||||
});
|
||||
|
||||
const sidebarTreeOverflowClass = computed(() => (settingsStore.editorSettings.sidebarAllowHorizontalScroll ? "overflow-x-auto sidebar-tree-horizontal-scroll" : "overflow-x-hidden"));
|
||||
@@ -797,11 +841,19 @@ const pasteHandlerRegistry = createSidebarPasteHandlerRegistry();
|
||||
provide(sidebarTreeContextKey, {
|
||||
getVisibleNodes: () => selectableVisibleNodes.value,
|
||||
getVisibleNodeIndex: (id: string) => selectableVisibleNodeIndexById.value.get(id) ?? -1,
|
||||
setTableSearchQuery: (parentNodeId, query) => {
|
||||
setTableSearchQuery: (parentNodeId, query, local) => {
|
||||
latestTableSearchInteractionParentId = parentNodeId;
|
||||
store.setSidebarTableSearchQuery(parentNodeId, query);
|
||||
scheduleSidebarTableSearchRefresh(parentNodeId, { restoreFocus: true });
|
||||
if (local) {
|
||||
localTableSearchFocusPending = true;
|
||||
void nextTick(() => {
|
||||
focusTableSearchInput(parentNodeId);
|
||||
localTableSearchFocusPending = false;
|
||||
});
|
||||
void loadLocalTableSearchResults(parentNodeId);
|
||||
} else scheduleSidebarTableSearchRefresh(parentNodeId, { restoreFocus: true });
|
||||
},
|
||||
refreshTableSearchIndex: (parentNodeId) => void loadLocalTableSearchResults(parentNodeId, true),
|
||||
registerPasteHandler: pasteHandlerRegistry.register,
|
||||
});
|
||||
provide(sidebarTreeRuntimeKey, sidebarTreeRuntime);
|
||||
@@ -1449,6 +1501,7 @@ function focusSearchAtEnd() {
|
||||
|
||||
function onWindowKeydown(event: KeyboardEvent) {
|
||||
if (event.defaultPrevented) return;
|
||||
if (localTableSearchFocusPending) return;
|
||||
if (sidebarShortcutTargetIsActive(event.target)) {
|
||||
if (sidebarShortcutTargetAllowsAppShortcut(event.target) && isEditConnectionShortcut(event)) {
|
||||
if (requestSelectedConnectionEdit()) {
|
||||
@@ -1641,6 +1694,9 @@ defineExpose({ focusSearch, createNewGroup, collapseAllTreeNodes });
|
||||
<X class="h-3 w-3" />
|
||||
</button>
|
||||
</div>
|
||||
<LightTooltip :text="t('sidebar.globalLocalSearchTooltip')" side="top" :delay="300">
|
||||
<Switch size="sm" :model-value="settingsStore.editorSettings.sidebarGlobalSearchLocal" :aria-label="t('sidebar.globalLocalSearch')" @update:model-value="settingsStore.updateEditorSettings({ sidebarGlobalSearchLocal: Boolean($event) })" />
|
||||
</LightTooltip>
|
||||
<button class="shrink-0 h-6 w-6 flex items-center justify-center rounded border border-border text-muted-foreground hover:bg-accent hover:text-foreground" :title="t('sidebar.locateActiveTab')" @click="locateActiveTabInSidebar">
|
||||
<Crosshair class="h-3.5 w-3.5" />
|
||||
</button>
|
||||
|
||||
@@ -33,6 +33,7 @@ import {
|
||||
Archive,
|
||||
Square,
|
||||
X,
|
||||
RefreshCw,
|
||||
} from "@lucide/vue";
|
||||
import { useConnectionStore } from "@/stores/connectionStore";
|
||||
import { useQueryStore } from "@/stores/queryStore";
|
||||
@@ -43,6 +44,7 @@ import ConnectionErrorIndicator from "@/components/connection/ConnectionErrorInd
|
||||
import ProductionContextBadge from "@/components/common/ProductionContextBadge.vue";
|
||||
import { Badge } from "@/components/ui/badge";
|
||||
import { Input } from "@/components/ui/input";
|
||||
import { Switch } from "@/components/ui/switch";
|
||||
import LightTooltip from "@/components/ui/LightTooltip.vue";
|
||||
import type { ColumnInfo, ConnectionConfig, DatabaseType, TreeNode } from "@/types/database";
|
||||
import { alignedCommentLeadingWidth, canTreeNodePin, canTreeNodeShowExpander, sidebarTreeNodeComment, trailingCommentAvailableWidth, trailingCommentGapPx, treeItemPaddingLeft, treeLabelWidthClass, usesFullWidthTreeLabel } from "@/lib/sidebar/sidebarTreeItemLayout";
|
||||
@@ -713,11 +715,21 @@ function updateTableSearchQuery(value: string | number) {
|
||||
if (!parentId) return;
|
||||
const query = String(value);
|
||||
if (sidebarTreeContext?.setTableSearchQuery) {
|
||||
sidebarTreeContext.setTableSearchQuery(parentId, query);
|
||||
sidebarTreeContext.setTableSearchQuery(parentId, query, settingsStore.editorSettings.sidebarTableSearchLocal);
|
||||
return;
|
||||
}
|
||||
connectionStore.setSidebarTableSearchQuery(parentId, query);
|
||||
void connectionStore.refreshSidebarTableSearch(parentId);
|
||||
if (!settingsStore.editorSettings.sidebarTableSearchLocal) void connectionStore.refreshSidebarTableSearch(parentId);
|
||||
}
|
||||
|
||||
function updateTableSearchLocal(value: boolean) {
|
||||
settingsStore.updateEditorSettings({ sidebarTableSearchLocal: value });
|
||||
updateTableSearchQuery(tableSearchValue.value);
|
||||
}
|
||||
|
||||
function refreshTableSearchIndex() {
|
||||
const parentId = tableSearchParentId.value;
|
||||
if (parentId) sidebarTreeContext?.refreshTableSearchIndex?.(parentId);
|
||||
}
|
||||
|
||||
function clearTableSearchQuery() {
|
||||
@@ -1061,8 +1073,8 @@ function onKeydown(event: KeyboardEvent) {
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div v-if="node.type === 'table-search-control'" class="tree-table-search-control flex h-7 items-center py-0.5 pr-2" :style="tableSearchStyle" @click.stop @dblclick.stop @mousedown.stop @keydown.stop>
|
||||
<div class="relative w-full min-w-0">
|
||||
<div v-if="node.type === 'table-search-control'" class="tree-table-search-control flex h-7 items-center gap-1.5 py-0.5 pr-2" :style="tableSearchStyle" @click.stop @dblclick.stop @mousedown.stop @keydown.stop>
|
||||
<div class="relative min-w-0 flex-1">
|
||||
<Search class="pointer-events-none absolute left-2 top-1/2 h-3 w-3 -translate-y-1/2 text-muted-foreground" />
|
||||
<Input
|
||||
:model-value="tableSearchValue"
|
||||
@@ -1080,6 +1092,14 @@ function onKeydown(event: KeyboardEvent) {
|
||||
<X class="h-3 w-3" />
|
||||
</button>
|
||||
</div>
|
||||
<LightTooltip :text="t('sidebar.localTableSearchTooltip')" side="top" :delay="300">
|
||||
<Switch size="sm" :model-value="settingsStore.editorSettings.sidebarTableSearchLocal" :aria-label="t('sidebar.localTableSearch')" @update:model-value="updateTableSearchLocal(Boolean($event))" />
|
||||
</LightTooltip>
|
||||
<LightTooltip v-if="settingsStore.editorSettings.sidebarTableSearchLocal" :text="t('sidebar.refreshLocalTableSearchIndex')" side="top" :delay="300">
|
||||
<button type="button" class="flex h-5 w-5 shrink-0 items-center justify-center rounded text-muted-foreground hover:bg-muted hover:text-foreground" :aria-label="t('sidebar.refreshLocalTableSearchIndex')" @click.stop="refreshTableSearchIndex">
|
||||
<RefreshCw class="h-3 w-3" />
|
||||
</button>
|
||||
</LightTooltip>
|
||||
</div>
|
||||
|
||||
<div v-else @contextmenu="onTreeItemContextMenu">
|
||||
|
||||
@@ -109,6 +109,11 @@ export default {
|
||||
searchScopeView: "View",
|
||||
searchTablesInCurrentScope: "Search tables in this database...",
|
||||
clearTableSearch: "Clear table search",
|
||||
localTableSearch: "Local search",
|
||||
localTableSearchTooltip: "Search the local table index without remote requests. Turn off to search remotely. Use refresh to build or update the index.",
|
||||
refreshLocalTableSearchIndex: "Refresh local table index",
|
||||
globalLocalSearch: "Local search",
|
||||
globalLocalSearchTooltip: "Search only currently loaded sidebar objects without remote requests.",
|
||||
clearFilter: "Clear filter",
|
||||
locateActiveTab: "Locate in sidebar",
|
||||
},
|
||||
|
||||
@@ -111,6 +111,11 @@ export default withEnglishFallback({
|
||||
searchScopeView: "Vista",
|
||||
searchTablesInCurrentScope: "Buscar tablas en esta base de datos...",
|
||||
clearTableSearch: "Limpiar búsqueda de tablas",
|
||||
localTableSearch: "Búsqueda local",
|
||||
localTableSearchTooltip: "Busca en el índice local de tablas sin solicitudes remotas. Desactívalo para buscar de forma remota. Usa actualizar para crear o actualizar el índice.",
|
||||
refreshLocalTableSearchIndex: "Actualizar índice local de tablas",
|
||||
globalLocalSearch: "Búsqueda local",
|
||||
globalLocalSearchTooltip: "Busca solo los objetos de la barra lateral cargados actualmente sin solicitudes remotas.",
|
||||
clearFilter: "Limpiar filtro",
|
||||
locateActiveTab: "Localizar en la barra lateral",
|
||||
},
|
||||
|
||||
@@ -110,6 +110,11 @@ export default withEnglishFallback({
|
||||
searchScopeView: "Vista",
|
||||
searchTablesInCurrentScope: "Cerca tabelle in questo database...",
|
||||
clearTableSearch: "Cancella ricerca tabelle",
|
||||
localTableSearch: "Ricerca locale",
|
||||
localTableSearchTooltip: "Cerca nell'indice locale delle tabelle senza richieste remote. Disattiva per cercare in remoto. Usa aggiorna per creare o aggiornare l'indice.",
|
||||
refreshLocalTableSearchIndex: "Aggiorna indice locale delle tabelle",
|
||||
globalLocalSearch: "Ricerca locale",
|
||||
globalLocalSearchTooltip: "Cerca solo gli oggetti della barra laterale attualmente caricati senza richieste remote.",
|
||||
clearFilter: "Cancella filtro",
|
||||
locateActiveTab: "Trova nella barra laterale",
|
||||
},
|
||||
|
||||
@@ -111,6 +111,11 @@ export default withEnglishFallback({
|
||||
searchScopeView: "ビュー",
|
||||
searchTablesInCurrentScope: "このデータベースのテーブルを検索...",
|
||||
clearTableSearch: "テーブル検索をクリア",
|
||||
localTableSearch: "ローカル検索",
|
||||
localTableSearchTooltip: "リモート要求を行わずにローカルのテーブルインデックスを検索します。リモート検索するにはオフにしてください。更新でインデックスを作成または更新します。",
|
||||
refreshLocalTableSearchIndex: "ローカルテーブルインデックスを更新",
|
||||
globalLocalSearch: "ローカル検索",
|
||||
globalLocalSearchTooltip: "リモート要求を行わず、現在読み込まれているサイドバーオブジェクトのみを検索します。",
|
||||
clearFilter: "フィルターをクリア",
|
||||
locateActiveTab: "サイドバーで表示",
|
||||
},
|
||||
|
||||
@@ -111,6 +111,11 @@ export default withEnglishFallback({
|
||||
searchScopeView: "Visão",
|
||||
searchTablesInCurrentScope: "Pesquisar tabelas neste banco...",
|
||||
clearTableSearch: "Limpar busca de tabelas",
|
||||
localTableSearch: "Busca local",
|
||||
localTableSearchTooltip: "Pesquisa o índice local de tabelas sem solicitações remotas. Desative para pesquisar remotamente. Use atualizar para criar ou atualizar o índice.",
|
||||
refreshLocalTableSearchIndex: "Atualizar índice local de tabelas",
|
||||
globalLocalSearch: "Busca local",
|
||||
globalLocalSearchTooltip: "Pesquisa apenas os objetos da barra lateral carregados no momento, sem solicitações remotas.",
|
||||
clearFilter: "Limpar filtro",
|
||||
locateActiveTab: "Localizar na barra lateral",
|
||||
},
|
||||
|
||||
@@ -111,6 +111,11 @@ export default withEnglishFallback({
|
||||
searchScopeView: "视图",
|
||||
searchTablesInCurrentScope: "搜索当前库的表...",
|
||||
clearTableSearch: "清除表搜索",
|
||||
localTableSearch: "本地搜索",
|
||||
localTableSearchTooltip: "开启后搜索本地表索引,不发送远程请求;关闭后使用远程搜索。请使用刷新按钮构建或更新索引。",
|
||||
refreshLocalTableSearchIndex: "刷新本地表索引",
|
||||
globalLocalSearch: "本地搜索",
|
||||
globalLocalSearchTooltip: "开启后仅搜索当前已加载的侧边栏对象,不发送远程请求。",
|
||||
clearFilter: "清除筛选",
|
||||
locateActiveTab: "在侧边栏中定位",
|
||||
},
|
||||
|
||||
@@ -111,6 +111,11 @@ export default withEnglishFallback({
|
||||
searchScopeView: "檢視",
|
||||
searchTablesInCurrentScope: "搜尋目前資料庫的資料表...",
|
||||
clearTableSearch: "清除資料表搜尋",
|
||||
localTableSearch: "本機搜尋",
|
||||
localTableSearchTooltip: "使用本機資料表索引搜尋,不會發出遠端請求。關閉後改用遠端搜尋;使用重新整理建立或更新索引。",
|
||||
refreshLocalTableSearchIndex: "重新整理本機資料表索引",
|
||||
globalLocalSearch: "本機搜尋",
|
||||
globalLocalSearchTooltip: "只搜尋目前已載入的側邊欄物件,不會發出遠端請求。",
|
||||
clearFilter: "清除篩選",
|
||||
locateActiveTab: "在側邊欄中定位",
|
||||
},
|
||||
|
||||
@@ -1,24 +1,46 @@
|
||||
export const SCHEMA_TREE_CACHE_TTL_MS = 15 * 60 * 1000;
|
||||
|
||||
export interface TableSearchIndexEntry {
|
||||
name: string;
|
||||
tableType: string;
|
||||
}
|
||||
|
||||
export interface TableSearchIndex {
|
||||
complete: true;
|
||||
indexedAt: string;
|
||||
entries: TableSearchIndexEntry[];
|
||||
}
|
||||
|
||||
export interface SchemaTreeCacheEnvelope<T> {
|
||||
version: 2;
|
||||
version: 3;
|
||||
cachedAt: string;
|
||||
children: T;
|
||||
tableSearchIndex?: TableSearchIndex;
|
||||
}
|
||||
|
||||
export interface DecodedSchemaTreeCache<T> {
|
||||
children: T;
|
||||
isStale: boolean;
|
||||
tableSearchIndex?: TableSearchIndex;
|
||||
}
|
||||
|
||||
export function encodeSchemaTreeCache<T>(children: T, nowMs = Date.now()): SchemaTreeCacheEnvelope<T> {
|
||||
export function encodeSchemaTreeCache<T>(children: T, nowMs = Date.now(), tableSearchIndex?: TableSearchIndex): SchemaTreeCacheEnvelope<T> {
|
||||
return {
|
||||
version: 2,
|
||||
version: 3,
|
||||
cachedAt: new Date(nowMs).toISOString(),
|
||||
children,
|
||||
...(tableSearchIndex ? { tableSearchIndex } : {}),
|
||||
};
|
||||
}
|
||||
|
||||
function decodeTableSearchIndex(value: unknown): TableSearchIndex | undefined {
|
||||
if (!value || typeof value !== "object") return undefined;
|
||||
const index = value as Partial<TableSearchIndex>;
|
||||
if (index.complete !== true || typeof index.indexedAt !== "string" || !Array.isArray(index.entries)) return undefined;
|
||||
const entries = index.entries.filter((entry): entry is TableSearchIndexEntry => !!entry && typeof entry.name === "string" && typeof entry.tableType === "string");
|
||||
return entries.length === index.entries.length ? { complete: true, indexedAt: index.indexedAt, entries } : undefined;
|
||||
}
|
||||
|
||||
export function decodeSchemaTreeCache<T>(payload: unknown, nowMs = Date.now(), ttlMs = SCHEMA_TREE_CACHE_TTL_MS): DecodedSchemaTreeCache<T> | null {
|
||||
if (Array.isArray(payload)) {
|
||||
return { children: payload as T, isStale: true };
|
||||
@@ -26,16 +48,18 @@ export function decodeSchemaTreeCache<T>(payload: unknown, nowMs = Date.now(), t
|
||||
|
||||
if (!payload || typeof payload !== "object") return null;
|
||||
|
||||
const envelope = payload as Partial<SchemaTreeCacheEnvelope<unknown>>;
|
||||
if (envelope.version !== 2 || !Array.isArray(envelope.children) || typeof envelope.cachedAt !== "string") {
|
||||
const envelope = payload as { version?: unknown; cachedAt?: unknown; children?: unknown; tableSearchIndex?: unknown };
|
||||
if ((envelope.version !== 2 && envelope.version !== 3) || !Array.isArray(envelope.children) || typeof envelope.cachedAt !== "string") {
|
||||
return null;
|
||||
}
|
||||
|
||||
const cachedAtMs = Date.parse(envelope.cachedAt);
|
||||
if (!Number.isFinite(cachedAtMs)) return null;
|
||||
|
||||
const tableSearchIndex = decodeTableSearchIndex(envelope.tableSearchIndex);
|
||||
return {
|
||||
children: envelope.children as T,
|
||||
isStale: nowMs - cachedAtMs >= ttlMs,
|
||||
...(tableSearchIndex ? { tableSearchIndex } : {}),
|
||||
};
|
||||
}
|
||||
|
||||
@@ -4,7 +4,8 @@ import type { TreeNode } from "@/types/database";
|
||||
export interface SidebarTreeContext {
|
||||
getVisibleNodes: () => TreeNode[];
|
||||
getVisibleNodeIndex: (id: string) => number;
|
||||
setTableSearchQuery?: (parentNodeId: string, query: string) => void;
|
||||
setTableSearchQuery?: (parentNodeId: string, query: string, local: boolean) => void;
|
||||
refreshTableSearchIndex?: (parentNodeId: string) => void;
|
||||
registerPasteHandler?: (nodeId: string, callback: () => void) => () => void;
|
||||
}
|
||||
|
||||
|
||||
@@ -312,6 +312,112 @@ describe("connectionStore metadata loading", () => {
|
||||
expect(schemaNode.children?.map((node) => node.label)).toEqual(["users"]);
|
||||
});
|
||||
|
||||
it("keeps concurrent table-tree and local-index refreshes in separate cache entries", async () => {
|
||||
const treeCacheKey = "pg-1:app:public:group-tables:objects-v6";
|
||||
const indexCacheKey = `${treeCacheKey}:table-search-index-v1`;
|
||||
const cachedPayloads = new Map<string, unknown>([
|
||||
[
|
||||
treeCacheKey,
|
||||
{
|
||||
version: 3,
|
||||
cachedAt: new Date().toISOString(),
|
||||
children: [{ id: "old", label: "old_table", type: "table", connectionId: "pg-1", database: "app", schema: "public", isExpanded: false }],
|
||||
},
|
||||
],
|
||||
]);
|
||||
let releaseTableLists!: () => void;
|
||||
let tableListCalls = 0;
|
||||
const bothTableListsStarted = new Promise<void>((resolve) => {
|
||||
releaseTableLists = resolve;
|
||||
});
|
||||
const listTables = vi.fn(async (_connectionId: string, _database: string, _schema: string, _filter?: string, limit?: number) => {
|
||||
tableListCalls += 1;
|
||||
if (tableListCalls === 2) releaseTableLists();
|
||||
await bothTableListsStarted;
|
||||
return limit === 2 ? ([{ name: "indexed_table", table_type: "TABLE", comment: null }] satisfies TableInfo[]) : ([{ name: "fresh_table", table_type: "TABLE", comment: null }] satisfies TableInfo[]);
|
||||
});
|
||||
const loadSchemaCache = vi.fn(async (key: string) => {
|
||||
const payload = cachedPayloads.get(key) ?? null;
|
||||
await Promise.resolve();
|
||||
return payload == null ? null : structuredClone(payload);
|
||||
});
|
||||
const saveSchemaCache = vi.fn(async (key: string, payload: unknown) => {
|
||||
cachedPayloads.set(key, structuredClone(payload));
|
||||
});
|
||||
|
||||
vi.doMock("@/lib/backend/tauriRuntime", () => ({ isTauriRuntime: () => false }));
|
||||
vi.doMock("@/lib/backend/api", () => ({
|
||||
checkConnectionHealth: vi.fn().mockResolvedValue(undefined),
|
||||
deleteSchemaCachePrefix: vi.fn().mockResolvedValue(undefined),
|
||||
listInstalledAgents: vi.fn().mockResolvedValue([]),
|
||||
listTables,
|
||||
loadSchemaCache,
|
||||
saveConnections: vi.fn().mockResolvedValue(undefined),
|
||||
saveSchemaCache,
|
||||
saveSidebarLayout: vi.fn().mockResolvedValue(undefined),
|
||||
}));
|
||||
|
||||
const { decodeSchemaTreeCache } = await import("@/lib/metadata/schemaTreeCache");
|
||||
const { useConnectionStore } = await import("@/stores/connectionStore");
|
||||
const { useSettingsStore } = await import("@/stores/settingsStore");
|
||||
const store = useConnectionStore();
|
||||
useSettingsStore().desktopSettings.sidebar_table_page_size = 2;
|
||||
|
||||
const connection = postgresConnection();
|
||||
const tablesGroup: TreeNode = {
|
||||
id: "pg-1:app:public:__tables",
|
||||
label: "tree.tables",
|
||||
type: "group-tables",
|
||||
connectionId: connection.id,
|
||||
database: "app",
|
||||
schema: "public",
|
||||
isExpanded: true,
|
||||
children: [],
|
||||
};
|
||||
store.connections = [connection];
|
||||
store.connectedIds.add(connection.id);
|
||||
store.treeNodes = [
|
||||
{
|
||||
id: connection.id,
|
||||
label: connection.name,
|
||||
type: "connection",
|
||||
connectionId: connection.id,
|
||||
isExpanded: true,
|
||||
children: [
|
||||
{
|
||||
id: "pg-1:app",
|
||||
label: "app",
|
||||
type: "database",
|
||||
connectionId: connection.id,
|
||||
database: "app",
|
||||
isExpanded: true,
|
||||
children: [
|
||||
{
|
||||
id: "pg-1:app:public",
|
||||
label: "public",
|
||||
type: "schema",
|
||||
connectionId: connection.id,
|
||||
database: "app",
|
||||
schema: "public",
|
||||
isExpanded: true,
|
||||
children: [tablesGroup],
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
];
|
||||
|
||||
await Promise.all([store.loadObjectGroupChildren(tablesGroup, { force: true }), store.refreshSidebarTableSearchIndex(tablesGroup.id)]);
|
||||
|
||||
const treeCache = decodeSchemaTreeCache<TreeNode[]>(cachedPayloads.get(treeCacheKey));
|
||||
const indexCache = decodeSchemaTreeCache<TreeNode[]>(cachedPayloads.get(indexCacheKey));
|
||||
expect(treeCache?.children.map((node) => node.label)).toEqual(["fresh_table"]);
|
||||
expect(indexCache?.tableSearchIndex?.entries).toEqual([{ name: "indexed_table", tableType: "TABLE" }]);
|
||||
await expect(store.loadSidebarTableSearchIndex(tablesGroup.id)).resolves.toEqual([{ name: "indexed_table", table_type: "TABLE" }]);
|
||||
expect(loadSchemaCache).toHaveBeenLastCalledWith(indexCacheKey);
|
||||
});
|
||||
|
||||
it("bypasses Oracle object-group caches created before DIP visibility was fixed", async () => {
|
||||
const listTables = vi.fn().mockResolvedValue([
|
||||
{ name: "V_ONE", table_type: "VIEW", comment: null },
|
||||
@@ -336,6 +442,7 @@ describe("connectionStore metadata loading", () => {
|
||||
vi.doMock("@/lib/backend/api", () => ({
|
||||
checkConnectionHealth: vi.fn().mockResolvedValue(undefined),
|
||||
deleteSchemaCachePrefix: vi.fn().mockResolvedValue(undefined),
|
||||
listInstalledAgents: vi.fn().mockResolvedValue([]),
|
||||
listTables,
|
||||
loadSchemaCache,
|
||||
saveSchemaCache: vi.fn().mockResolvedValue(undefined),
|
||||
@@ -555,6 +662,7 @@ describe("connectionStore metadata loading", () => {
|
||||
vi.doMock("@/lib/backend/api", () => ({
|
||||
checkConnectionHealth: vi.fn().mockResolvedValue(undefined),
|
||||
deleteSchemaCachePrefix: vi.fn().mockResolvedValue(undefined),
|
||||
listInstalledAgents: vi.fn().mockResolvedValue([]),
|
||||
listSchemaInfos,
|
||||
loadSchemaCache,
|
||||
saveConnections: vi.fn().mockResolvedValue(undefined),
|
||||
@@ -949,6 +1057,7 @@ describe("connectionStore metadata loading", () => {
|
||||
checkConnectionHealth: vi.fn().mockResolvedValue(undefined),
|
||||
deleteSchemaCachePrefix: vi.fn().mockResolvedValue(undefined),
|
||||
listDatabases,
|
||||
listInstalledAgents: vi.fn().mockResolvedValue([]),
|
||||
listTables,
|
||||
listObjects,
|
||||
loadSchemaCache: vi.fn().mockResolvedValue(null),
|
||||
@@ -1282,6 +1391,7 @@ describe("connectionStore metadata loading", () => {
|
||||
vi.doMock("@/lib/backend/api", () => ({
|
||||
checkConnectionHealth: vi.fn().mockResolvedValue(undefined),
|
||||
deleteSchemaCachePrefix: vi.fn().mockResolvedValue(undefined),
|
||||
listInstalledAgents: vi.fn().mockResolvedValue([]),
|
||||
listTables,
|
||||
loadSchemaCache,
|
||||
saveSchemaCache: vi.fn().mockResolvedValue(undefined),
|
||||
@@ -1351,6 +1461,7 @@ describe("connectionStore metadata loading", () => {
|
||||
checkConnectionHealth: vi.fn().mockResolvedValue(undefined),
|
||||
deleteSchemaCachePrefix: vi.fn().mockResolvedValue(undefined),
|
||||
listDatabases,
|
||||
listInstalledAgents: vi.fn().mockResolvedValue([]),
|
||||
listTables,
|
||||
loadSchemaCache: vi.fn().mockResolvedValue(null),
|
||||
saveSchemaCache: vi.fn().mockResolvedValue(undefined),
|
||||
|
||||
@@ -1885,12 +1885,81 @@ export const useConnectionStore = defineStore("connection", () => {
|
||||
await api.saveSchemaCache(cacheKey, encodeSchemaTreeCache(children)).catch(() => undefined);
|
||||
}
|
||||
|
||||
function sidebarTableSearchTreeCacheKey(parent: TreeNode): string | null {
|
||||
if (!parent.connectionId || !parent.database) return null;
|
||||
if (parent.type === "group-tables") return objectGroupCacheKey(parent);
|
||||
if (parent.type !== "database" && parent.type !== "schema" && parent.type !== "linked-server-schema") return null;
|
||||
const simpleObjectDisplay = useSettingsStore().editorSettings.sidebarObjectDisplay === "simple";
|
||||
return schemaCacheKey(parent.connectionId, parent.database, parent.schema || "", simpleObjectDisplay ? "objects-simple-v6" : "objects-grouped-v7");
|
||||
}
|
||||
|
||||
function sidebarTableSearchIndexCacheKey(parent: TreeNode): string | null {
|
||||
const treeCacheKey = sidebarTableSearchTreeCacheKey(parent);
|
||||
return treeCacheKey ? `${treeCacheKey}:table-search-index-v1` : null;
|
||||
}
|
||||
|
||||
async function loadSidebarTableSearchIndex(parentNodeId: string): Promise<TableInfo[] | null> {
|
||||
const parent = findNode(treeNodes.value, parentNodeId);
|
||||
if (!parent) return null;
|
||||
const cacheKey = sidebarTableSearchIndexCacheKey(parent);
|
||||
if (!cacheKey) return null;
|
||||
const decoded = decodeSchemaTreeCache<TreeNode[]>(await api.loadSchemaCache<unknown>(cacheKey).catch(() => null));
|
||||
const index = decoded?.tableSearchIndex;
|
||||
if (!index) return null;
|
||||
return index.entries.map((entry) => ({ name: entry.name, table_type: entry.tableType }));
|
||||
}
|
||||
|
||||
async function refreshSidebarTableSearchIndex(parentNodeId: string): Promise<TableInfo[]> {
|
||||
const parent = findNode(treeNodes.value, parentNodeId);
|
||||
if (!parent?.connectionId || !hasTreeNodeDatabaseContext(parent)) return [];
|
||||
const cacheKey = sidebarTableSearchIndexCacheKey(parent);
|
||||
if (!cacheKey) return [];
|
||||
await ensureConnected(parent.connectionId);
|
||||
const config = getConfig(parent.connectionId);
|
||||
const querySchema = connectionObjectTreeQuerySchema(config, parent.database, parent.schema);
|
||||
const objectTypes = parent.type === "group-tables" ? (objectTypesForGroupNode(parent.type) ?? undefined) : undefined;
|
||||
const pageSize = sidebarObjectGroupPageSize();
|
||||
const entries: TableInfo[] = [];
|
||||
for (let offset = 0; ; offset += pageSize) {
|
||||
const page = await listTablesWithOptionalTableNameFilter(parent.connectionId, parent.database, querySchema, undefined, pageSize, offset, objectTypes, parent.catalog);
|
||||
entries.push(...page);
|
||||
if (page.length < pageSize) break;
|
||||
}
|
||||
const deduped = [...new Map(entries.map((entry) => [`${entry.table_type}\0${entry.name}`, entry])).values()];
|
||||
const tableSearchIndex = { complete: true as const, indexedAt: new Date().toISOString(), entries: deduped.map((entry) => ({ name: entry.name, tableType: entry.table_type })) };
|
||||
await api.saveSchemaCache(cacheKey, encodeSchemaTreeCache<TreeNode[]>([], Date.now(), tableSearchIndex));
|
||||
return deduped;
|
||||
}
|
||||
|
||||
async function savePersistedConnectionTreeChildren(cacheKey: string, children: TreeNode[]) {
|
||||
const metadataChildren = connectionMetadataChildren(children);
|
||||
if (metadataChildren.length === 0) return;
|
||||
await savePersistedTreeChildren(cacheKey, metadataChildren);
|
||||
}
|
||||
|
||||
function connectionRootCacheKey(connectionId: string, config: ConnectionConfig | undefined): string | null {
|
||||
if (!config || connectionIsDorisFamilyCatalogCapable(config)) return null;
|
||||
if (config.db_type === "duckdb") return schemaCacheKey(connectionId, "duckdb-root");
|
||||
if (connectionUsesVisibleSchemaFilter(config)) {
|
||||
return schemaCacheKey(connectionId, config.database || "", config.db_type === "oracle" ? "schemas-v2" : "schemas", config.show_system_schemas === true ? "show-system" : "hide-system");
|
||||
}
|
||||
return schemaCacheKey(connectionId, "databases-v2");
|
||||
}
|
||||
|
||||
async function hydrateTreeNodeFromCache(node: TreeNode | null, cacheKey: string | null): Promise<boolean> {
|
||||
if (!node || !cacheKey || loadedTreeNodeChildrenIds.value.has(node.id)) return false;
|
||||
const load = beginTreeNodeLoad(node);
|
||||
try {
|
||||
return (await loadPersistedTreeChildren(node, cacheKey, load)).hit;
|
||||
} finally {
|
||||
finishTreeNodeLoad(load);
|
||||
}
|
||||
}
|
||||
|
||||
async function hydrateConnectionRootFromCache(connectionId: string, config: ConnectionConfig | undefined): Promise<boolean> {
|
||||
return hydrateTreeNodeFromCache(findConnectionNode(connectionId), connectionRootCacheKey(connectionId, config));
|
||||
}
|
||||
|
||||
function isTreeNodeLoadedChildrenUsable(node: TreeNode): boolean {
|
||||
const sidebarObjectDisplay = useSettingsStore().editorSettings.sidebarObjectDisplay;
|
||||
if (!treeNodeLoadedChildrenContentPresent(node, sidebarObjectDisplay)) return false;
|
||||
@@ -2023,7 +2092,7 @@ export const useConnectionStore = defineStore("connection", () => {
|
||||
return current;
|
||||
}
|
||||
|
||||
function clearLoadedChildrenCache(prefix: string) {
|
||||
function clearLoadedChildrenCache(prefix: string, options?: { deletePersisted?: boolean }) {
|
||||
for (const id of loadedTreeNodeChildrenIds.value) {
|
||||
if (id === prefix || id.startsWith(`${prefix}:`)) {
|
||||
loadedTreeNodeChildrenIds.value.delete(id);
|
||||
@@ -2035,6 +2104,7 @@ export const useConnectionStore = defineStore("connection", () => {
|
||||
}
|
||||
}
|
||||
invalidateMetadataCachesByTreePrefix(prefix);
|
||||
if (options?.deletePersisted === false) return;
|
||||
const rawPrefix = `${prefix}:`;
|
||||
const encodedPrefix = `${schemaCacheKey(prefix)}:`;
|
||||
if (rawPrefix === encodedPrefix) {
|
||||
@@ -2634,7 +2704,9 @@ export const useConnectionStore = defineStore("connection", () => {
|
||||
node.children = [];
|
||||
}
|
||||
clearConnectionRootMetadataLoad(connectionId);
|
||||
clearLoadedChildrenCache(connectionId);
|
||||
// Disconnecting only tears down the live session. Keep the schema snapshot so
|
||||
// reconnecting can render databases and table names before the remote refresh.
|
||||
clearLoadedChildrenCache(connectionId, { deletePersisted: false });
|
||||
if (activeConnectionId.value === connectionId) {
|
||||
activeConnectionId.value = null;
|
||||
}
|
||||
@@ -2868,6 +2940,16 @@ export const useConnectionStore = defineStore("connection", () => {
|
||||
|
||||
async function loadDatabases(connectionId: string, options?: LoadTreeOptions) {
|
||||
const configForScope = getConfig(connectionId);
|
||||
const searchFilter = activeTreeLoadSearchFilter(options);
|
||||
if (!options?.force && !options?.connectedOnly && !searchFilter) {
|
||||
const cacheHit = await hydrateConnectionRootFromCache(connectionId, configForScope);
|
||||
if (cacheHit) {
|
||||
// Render the last known metadata immediately; network validation and refresh
|
||||
// continue in the background so opening a connection never waits on them.
|
||||
void loadDatabases(connectionId, { ...options, force: true }).catch((error) => recordMetadataLoadError(connectionId, error));
|
||||
return;
|
||||
}
|
||||
}
|
||||
return runTreeMetadataLoad(
|
||||
{
|
||||
kind: "connection-databases",
|
||||
@@ -3897,6 +3979,23 @@ export const useConnectionStore = defineStore("connection", () => {
|
||||
const configForScope = getConfig(connectionId);
|
||||
const simpleObjectDisplayForScope = useSettingsStore().editorSettings.sidebarObjectDisplay === "simple";
|
||||
const objectTypesForScope = simpleObjectDisplayForScope ? supportedSidebarObjectTypes(configForScope) : undefined;
|
||||
const searchFilter = activeTreeLoadSearchFilter(options);
|
||||
const querySchemaForScope = connectionObjectTreeQuerySchema(configForScope, database, schema);
|
||||
const effectiveSchemaForScope = connectionObjectTreeNodeSchema(configForScope, database, schema);
|
||||
const tableNameFilterForScope = activeTableNameFilterForScope({
|
||||
connectionId,
|
||||
database,
|
||||
schema: effectiveSchemaForScope ?? querySchemaForScope,
|
||||
nodeKind: simpleObjectDisplayForScope ? "simple-tables" : "group-tables",
|
||||
});
|
||||
if (!options?.force && simpleObjectDisplayForScope && !searchFilter && !options?.sidebarTableSearchParentId && !tableNameFilterForScope) {
|
||||
const nodeId = schema ? `${connectionId}:${database}:${schema}` : `${connectionId}:${database}`;
|
||||
const cacheKey = schemaCacheKey(connectionId, database, schema || "", "objects-simple-v6");
|
||||
if (await hydrateTreeNodeFromCache(findNode(treeNodes.value, nodeId), cacheKey)) {
|
||||
void loadTables(connectionId, database, schema, { ...options, force: true }).catch((error) => recordMetadataLoadError(connectionId, error));
|
||||
return;
|
||||
}
|
||||
}
|
||||
return runTreeMetadataLoad(
|
||||
{
|
||||
kind: "schema-tables",
|
||||
@@ -4012,6 +4111,24 @@ export const useConnectionStore = defineStore("connection", () => {
|
||||
const configForScope = node.connectionId ? getConfig(node.connectionId) : undefined;
|
||||
const objectTypesForScope = objectTypesForGroupNode(node.type);
|
||||
const pageSizeForScope = sidebarObjectGroupPageSize();
|
||||
const searchFilter = activeTreeLoadSearchFilter(options);
|
||||
const querySchemaForScope = connectionObjectTreeQuerySchema(configForScope, node.database || "", node.schema);
|
||||
const effectiveSchemaForScope = connectionObjectTreeNodeSchema(configForScope, node.database || "", node.schema);
|
||||
const tableNameFilterForScope = activeTableNameFilterForScope({
|
||||
connectionId: node.connectionId,
|
||||
database: node.database,
|
||||
schema: effectiveSchemaForScope ?? querySchemaForScope,
|
||||
nodeKind: node.type,
|
||||
catalog: node.catalog,
|
||||
});
|
||||
if (!options?.force && !searchFilter && !options?.sidebarTableSearchParentId && !tableNameFilterForScope) {
|
||||
if (await hydrateTreeNodeFromCache(node, objectGroupCacheKey(node))) {
|
||||
void loadObjectGroupChildren(node, { ...options, force: true }).catch((error) => {
|
||||
if (node.connectionId) recordMetadataLoadError(node.connectionId, error);
|
||||
});
|
||||
return;
|
||||
}
|
||||
}
|
||||
return runTreeMetadataLoad(
|
||||
{
|
||||
kind: "object-group",
|
||||
@@ -6609,6 +6726,8 @@ export const useConnectionStore = defineStore("connection", () => {
|
||||
refreshTreeNodeForTableNameFilter,
|
||||
setSidebarTableSearchQuery,
|
||||
refreshSidebarTableSearch,
|
||||
loadSidebarTableSearchIndex,
|
||||
refreshSidebarTableSearchIndex,
|
||||
createConnectionGroup(name: string, parentGroupId?: string | null) {
|
||||
const result = createGroupOp(sidebarLayout.value, name, parentGroupId);
|
||||
updateLayoutAndRebuild(result.layout);
|
||||
|
||||
@@ -477,6 +477,8 @@ export interface EditorSettings {
|
||||
sidebarObjectDisplay: "grouped" | "simple";
|
||||
routineSourceOpenMode: "query-tab" | "dialog";
|
||||
sidebarTableSearchEnabled: boolean;
|
||||
sidebarTableSearchLocal: boolean;
|
||||
sidebarGlobalSearchLocal: boolean;
|
||||
autoSelectActiveSidebarNode: boolean;
|
||||
openTabsRestoreMode: OpenTabsRestoreMode;
|
||||
disconnectTabHandlingMode: DisconnectTabHandlingMode;
|
||||
@@ -650,6 +652,8 @@ export const DEFAULT_EDITOR_SETTINGS: EditorSettings = {
|
||||
sidebarObjectDisplay: "grouped",
|
||||
routineSourceOpenMode: "query-tab",
|
||||
sidebarTableSearchEnabled: false,
|
||||
sidebarTableSearchLocal: true,
|
||||
sidebarGlobalSearchLocal: false,
|
||||
autoSelectActiveSidebarNode: false,
|
||||
openTabsRestoreMode: "all",
|
||||
disconnectTabHandlingMode: "close-tabs",
|
||||
@@ -950,6 +954,8 @@ export function normalizeEditorSettings(settings: Partial<EditorSettings>, exist
|
||||
sidebarObjectDisplay: settings.sidebarObjectDisplay === "simple" || settings.sidebarObjectDisplay === "grouped" ? settings.sidebarObjectDisplay : DEFAULT_EDITOR_SETTINGS.sidebarObjectDisplay,
|
||||
routineSourceOpenMode: settings.routineSourceOpenMode === "query-tab" || settings.routineSourceOpenMode === "dialog" ? settings.routineSourceOpenMode : DEFAULT_EDITOR_SETTINGS.routineSourceOpenMode,
|
||||
sidebarTableSearchEnabled: typeof settings.sidebarTableSearchEnabled === "boolean" ? settings.sidebarTableSearchEnabled : DEFAULT_EDITOR_SETTINGS.sidebarTableSearchEnabled,
|
||||
sidebarTableSearchLocal: typeof settings.sidebarTableSearchLocal === "boolean" ? settings.sidebarTableSearchLocal : DEFAULT_EDITOR_SETTINGS.sidebarTableSearchLocal,
|
||||
sidebarGlobalSearchLocal: typeof settings.sidebarGlobalSearchLocal === "boolean" ? settings.sidebarGlobalSearchLocal : DEFAULT_EDITOR_SETTINGS.sidebarGlobalSearchLocal,
|
||||
autoSelectActiveSidebarNode: settings.autoSelectActiveSidebarNode ?? DEFAULT_EDITOR_SETTINGS.autoSelectActiveSidebarNode,
|
||||
openTabsRestoreMode: normalizeOpenTabsRestoreMode(
|
||||
(settings as Partial<EditorSettings>).openTabsRestoreMode,
|
||||
@@ -1429,6 +1435,8 @@ export const useSettingsStore = defineStore("settings", () => {
|
||||
if (partial.sidebarObjectDisplay !== undefined) editorSettings.value.sidebarObjectDisplay = partial.sidebarObjectDisplay;
|
||||
if (partial.routineSourceOpenMode !== undefined) editorSettings.value.routineSourceOpenMode = partial.routineSourceOpenMode;
|
||||
if (partial.sidebarTableSearchEnabled !== undefined) editorSettings.value.sidebarTableSearchEnabled = partial.sidebarTableSearchEnabled;
|
||||
if (partial.sidebarTableSearchLocal !== undefined) editorSettings.value.sidebarTableSearchLocal = partial.sidebarTableSearchLocal;
|
||||
if (partial.sidebarGlobalSearchLocal !== undefined) editorSettings.value.sidebarGlobalSearchLocal = partial.sidebarGlobalSearchLocal;
|
||||
if (partial.autoSelectActiveSidebarNode !== undefined) editorSettings.value.autoSelectActiveSidebarNode = partial.autoSelectActiveSidebarNode;
|
||||
if (partial.openTabsRestoreMode !== undefined) editorSettings.value.openTabsRestoreMode = normalizeOpenTabsRestoreMode(partial.openTabsRestoreMode);
|
||||
if (partial.disconnectTabHandlingMode !== undefined) editorSettings.value.disconnectTabHandlingMode = normalizeDisconnectTabHandlingMode(partial.disconnectTabHandlingMode);
|
||||
|
||||
+1
-1
@@ -7,7 +7,7 @@ const now = Date.parse("2026-05-17T10:00:00.000Z");
|
||||
|
||||
test("wraps tree children with a cache timestamp", () => {
|
||||
assert.deepEqual(encodeSchemaTreeCache(children, now), {
|
||||
version: 2,
|
||||
version: 3,
|
||||
cachedAt: "2026-05-17T10:00:00.000Z",
|
||||
children,
|
||||
});
|
||||
|
||||
+5
@@ -288,6 +288,11 @@ test("defaults sidebar table search to disabled and preserves saved booleans", (
|
||||
assert.equal(normalizeEditorSettings({ sidebarTableSearchEnabled: true }).sidebarTableSearchEnabled, true);
|
||||
assert.equal(normalizeEditorSettings({ sidebarTableSearchEnabled: false }).sidebarTableSearchEnabled, false);
|
||||
assert.equal(normalizeEditorSettings({ sidebarTableSearchEnabled: "yes" as any }).sidebarTableSearchEnabled, false);
|
||||
assert.equal(DEFAULT_EDITOR_SETTINGS.sidebarTableSearchLocal, true);
|
||||
assert.equal(normalizeEditorSettings({}).sidebarTableSearchLocal, true);
|
||||
assert.equal(normalizeEditorSettings({ sidebarTableSearchLocal: false }).sidebarTableSearchLocal, false);
|
||||
assert.equal(DEFAULT_EDITOR_SETTINGS.sidebarGlobalSearchLocal, false);
|
||||
assert.equal(normalizeEditorSettings({ sidebarGlobalSearchLocal: true }).sidebarGlobalSearchLocal, true);
|
||||
});
|
||||
|
||||
test("defaults shortcut settings", () => {
|
||||
|
||||
Reference in New Issue
Block a user