feat(sidebar): persist custom pinned item ordering
This commit is contained in:
1 parent
b635ea1466
commit
406b7acdbb
16 files changed
+1019
-82
No files matched your search
@@ -0,0 +1,134 @@
|
||||
import { createPinia, setActivePinia } from "pinia";
|
||||
import { beforeEach, describe, expect, it, vi } from "vitest";
|
||||
import { treeNodePinKey } from "@/lib/app/pinnedItems";
|
||||
import type { TreeNode } from "@/types/database";
|
||||
|
||||
function installLocalStorage() {
|
||||
const data = new Map<string, string>();
|
||||
vi.stubGlobal("localStorage", {
|
||||
getItem: vi.fn((key: string) => data.get(key) ?? null),
|
||||
setItem: vi.fn((key: string, value: string) => data.set(key, value)),
|
||||
removeItem: vi.fn((key: string) => data.delete(key)),
|
||||
});
|
||||
}
|
||||
|
||||
function tableNode(name = "users"): TreeNode {
|
||||
return {
|
||||
id: `conn:db:public:${name}`,
|
||||
label: name,
|
||||
type: "table",
|
||||
connectionId: "conn",
|
||||
database: "db",
|
||||
schema: "public",
|
||||
tableName: name,
|
||||
};
|
||||
}
|
||||
|
||||
describe("connectionStore pinned tree node removal", () => {
|
||||
beforeEach(() => {
|
||||
vi.resetModules();
|
||||
vi.unstubAllGlobals();
|
||||
installLocalStorage();
|
||||
setActivePinia(createPinia());
|
||||
});
|
||||
|
||||
it("does not pin a new table that reuses a deleted pinned table identity", async () => {
|
||||
vi.doMock("@/lib/backend/tauriRuntime", () => ({ isTauriRuntime: () => false }));
|
||||
|
||||
const { useConnectionStore } = await import("@/stores/connectionStore");
|
||||
const store = useConnectionStore();
|
||||
const deletedTable = tableNode();
|
||||
store.treeNodes = [
|
||||
{
|
||||
id: "conn",
|
||||
label: "Connection",
|
||||
type: "connection",
|
||||
connectionId: "conn",
|
||||
children: [deletedTable],
|
||||
},
|
||||
];
|
||||
|
||||
store.toggleTreeNodePin(deletedTable);
|
||||
expect(store.isTreeNodePinned(deletedTable)).toBe(true);
|
||||
|
||||
store.removeTreeNode(deletedTable.id);
|
||||
const replacement = tableNode();
|
||||
store.treeNodes[0].children = [replacement];
|
||||
|
||||
expect(store.isTreeNodePinned(replacement)).toBe(false);
|
||||
});
|
||||
|
||||
it("serializes desktop pin saves so an older reorder cannot overwrite the latest one", async () => {
|
||||
const savePinnedTreeNodeIds = vi.fn().mockResolvedValue(undefined);
|
||||
vi.doMock("@/lib/backend/tauriRuntime", () => ({ isTauriRuntime: () => true }));
|
||||
vi.doMock("@/lib/backend/api", () => ({ savePinnedTreeNodeIds }));
|
||||
|
||||
const { useConnectionStore } = await import("@/stores/connectionStore");
|
||||
const store = useConnectionStore();
|
||||
const users = tableNode("users");
|
||||
const orders = tableNode("orders");
|
||||
store.treeNodes = [{ id: "conn", label: "Connection", type: "connection", connectionId: "conn", children: [users, orders] }];
|
||||
store.toggleTreeNodePin(users);
|
||||
store.toggleTreeNodePin(orders);
|
||||
await vi.waitFor(() => expect(savePinnedTreeNodeIds).toHaveBeenCalledTimes(2));
|
||||
|
||||
savePinnedTreeNodeIds.mockClear();
|
||||
const snapshots: string[][] = [];
|
||||
const resolvers: Array<() => void> = [];
|
||||
savePinnedTreeNodeIds.mockImplementation((ids: string[]) => {
|
||||
snapshots.push([...ids]);
|
||||
return new Promise<void>((resolve) => resolvers.push(resolve));
|
||||
});
|
||||
|
||||
const usersKey = treeNodePinKey(users);
|
||||
const ordersKey = treeNodePinKey(orders);
|
||||
expect(store.reorderPinnedTreeNodes(usersKey, ordersKey, "after")).toBe(true);
|
||||
await vi.waitFor(() => expect(savePinnedTreeNodeIds).toHaveBeenCalledTimes(1));
|
||||
expect(store.reorderPinnedTreeNodes(ordersKey, usersKey, "after")).toBe(true);
|
||||
|
||||
expect(savePinnedTreeNodeIds).toHaveBeenCalledTimes(1);
|
||||
resolvers[0]!();
|
||||
await vi.waitFor(() => expect(savePinnedTreeNodeIds).toHaveBeenCalledTimes(2));
|
||||
resolvers[1]!();
|
||||
|
||||
expect(snapshots).toEqual([
|
||||
[ordersKey, usersKey],
|
||||
[usersKey, ordersKey],
|
||||
]);
|
||||
});
|
||||
|
||||
it("moves a renamed pinned object to its new identity so recreating the old name is unpinned", async () => {
|
||||
vi.doMock("@/lib/backend/tauriRuntime", () => ({ isTauriRuntime: () => false }));
|
||||
|
||||
const { useConnectionStore } = await import("@/stores/connectionStore");
|
||||
const store = useConnectionStore();
|
||||
const users = tableNode("users");
|
||||
const accounts = tableNode("accounts");
|
||||
store.treeNodes = [{ id: "conn", label: "Connection", type: "connection", connectionId: "conn", children: [users] }];
|
||||
store.toggleTreeNodePin(users);
|
||||
|
||||
store.treeNodes[0].children = [accounts];
|
||||
store.replacePinnedTreeNode(users, accounts);
|
||||
|
||||
const recreatedUsers = tableNode("users");
|
||||
store.treeNodes[0].children = [accounts, recreatedUsers];
|
||||
|
||||
expect(store.isTreeNodePinned(accounts)).toBe(true);
|
||||
expect(store.isTreeNodePinned(recreatedUsers)).toBe(false);
|
||||
});
|
||||
|
||||
it("removes the old pin when a renamed replacement is not loaded in the sidebar", async () => {
|
||||
vi.doMock("@/lib/backend/tauriRuntime", () => ({ isTauriRuntime: () => false }));
|
||||
|
||||
const { useConnectionStore } = await import("@/stores/connectionStore");
|
||||
const store = useConnectionStore();
|
||||
const users = tableNode("users");
|
||||
const accounts = tableNode("accounts");
|
||||
store.treeNodes = [{ id: "conn", label: "Connection", type: "connection", connectionId: "conn", children: [users] }];
|
||||
store.toggleTreeNodePin(users);
|
||||
|
||||
expect(store.replacePinnedTreeNode(users, accounts)).toBe(true);
|
||||
expect(store.isTreeNodePinned(users)).toBe(false);
|
||||
expect(store.isTreeNodePinned(accounts)).toBe(false);
|
||||
});
|
||||
});
|
||||
@@ -22,7 +22,21 @@ import type {
|
||||
TunnelProfile,
|
||||
VectorCollectionMeta,
|
||||
} from "@/types/database";
|
||||
import { inheritNaturalTreeNodeOrder, migrateLegacyPinnedTreeNodeIds, syncPinnedTreeNodeStateInPlace, treeNodePinKey } from "@/lib/app/pinnedItems";
|
||||
import {
|
||||
inheritNaturalTreeNodeOrder,
|
||||
migrateLegacyPinnedTreeNodeOrder,
|
||||
normalizePinnedTreeNodeOrder,
|
||||
orderItemsByPinnedTreeNodeOrder,
|
||||
pinnedTreeNodeIdentityMatches,
|
||||
removePinnedTreeNodesFromOrder,
|
||||
reorderPinnedTreeNodeOrder,
|
||||
replacePinnedTreeNodeInOrder,
|
||||
syncPinnedTreeNodeStateInPlace,
|
||||
treeNodePinIdentity,
|
||||
treeNodePinKey,
|
||||
type PinnedTreeNodeIdentity,
|
||||
type PinnedTreeNodeIdentityCanonicalizer,
|
||||
} from "@/lib/app/pinnedItems";
|
||||
import {
|
||||
reconcileLayout,
|
||||
buildTreeNodesFromLayout,
|
||||
@@ -78,7 +92,6 @@ import {
|
||||
import { hasTreeNodeDatabaseContext, normalizeCataloglessDatabaseNodes, treeNodeSchemaCachePrefix } from "@/lib/sidebar/treeNodeContext";
|
||||
import { decodeSchemaTreeCache, encodeSchemaTreeCache } from "@/lib/metadata/schemaTreeCache";
|
||||
import { sortSidebarTreeChildrenForParent } from "@/lib/sidebar/sidebarNodeOrdering";
|
||||
import { prunePinnedTreeNodeIdsForConnection } from "@/lib/app/pinnedTreeNodeIds";
|
||||
import { connectionSupportsDatabaseUserAdmin } from "@/lib/database/databaseUserAdmin";
|
||||
import { getTableMetadataCapabilities } from "@/lib/table/tableMetadataCapabilities";
|
||||
import { useSettingsStore } from "@/stores/settingsStore";
|
||||
@@ -304,7 +317,9 @@ export const useConnectionStore = defineStore("connection", () => {
|
||||
const sidebarDatabaseStorageInFlight = new Map<string, Promise<DatabaseStorageInfo[]>>();
|
||||
const sidebarTableStorageCache = new Map<string, { expiresAt: number; value: ObjectStatistics[] }>();
|
||||
const sidebarTableStorageInFlight = new Map<string, Promise<ObjectStatistics[]>>();
|
||||
const pinnedTreeNodeOrder = ref<string[]>([]);
|
||||
const pinnedTreeNodeIds = ref<Set<string>>(new Set());
|
||||
let pinnedTreeNodePersistQueue: Promise<void> = Promise.resolve();
|
||||
const connectedIds = ref<Set<string>>(new Set());
|
||||
const identifierQuotes = ref<Record<string, string>>({});
|
||||
const lastConnectionHealthCheckAt = ref<Record<string, number>>({});
|
||||
@@ -1002,27 +1017,27 @@ export const useConnectionStore = defineStore("connection", () => {
|
||||
};
|
||||
}
|
||||
|
||||
function loadPinnedTreeNodeIdsFromLocalStorage(): Set<string> {
|
||||
function loadPinnedTreeNodeOrderFromLocalStorage(): string[] {
|
||||
try {
|
||||
if (typeof localStorage === "undefined") return new Set();
|
||||
if (typeof localStorage === "undefined") return [];
|
||||
const saved = localStorage.getItem(PINNED_TREE_NODES_STORAGE_KEY);
|
||||
const ids = saved ? JSON.parse(saved) : [];
|
||||
return new Set(Array.isArray(ids) ? ids.filter((id) => typeof id === "string") : []);
|
||||
return normalizePinnedTreeNodeOrder(Array.isArray(ids) ? ids.filter((id): id is string => typeof id === "string") : []);
|
||||
} catch {
|
||||
return new Set();
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
async function loadPinnedTreeNodeIds(): Promise<Set<string>> {
|
||||
if (!isDesktop) return loadPinnedTreeNodeIdsFromLocalStorage();
|
||||
async function loadPinnedTreeNodeOrder(): Promise<string[]> {
|
||||
if (!isDesktop) return loadPinnedTreeNodeOrderFromLocalStorage();
|
||||
const ids = await api.loadPinnedTreeNodeIds().catch(() => []);
|
||||
const valid = ids.filter((id) => typeof id === "string");
|
||||
if (valid.length > 0) return new Set(valid);
|
||||
const valid = normalizePinnedTreeNodeOrder(ids.filter((id): id is string => typeof id === "string"));
|
||||
if (valid.length > 0) return valid;
|
||||
|
||||
// Migrate legacy localStorage values for existing desktop users.
|
||||
const legacy = loadPinnedTreeNodeIdsFromLocalStorage();
|
||||
if (legacy.size > 0) {
|
||||
await api.savePinnedTreeNodeIds([...legacy]).catch(() => undefined);
|
||||
const legacy = loadPinnedTreeNodeOrderFromLocalStorage();
|
||||
if (legacy.length > 0) {
|
||||
await api.savePinnedTreeNodeIds(legacy).catch(() => undefined);
|
||||
if (typeof localStorage !== "undefined") {
|
||||
localStorage.removeItem(PINNED_TREE_NODES_STORAGE_KEY);
|
||||
}
|
||||
@@ -1030,18 +1045,53 @@ export const useConnectionStore = defineStore("connection", () => {
|
||||
return legacy;
|
||||
}
|
||||
|
||||
function setPinnedTreeNodeOrder(order: readonly string[]) {
|
||||
const normalized = normalizePinnedTreeNodeOrder(order);
|
||||
pinnedTreeNodeOrder.value = normalized;
|
||||
pinnedTreeNodeIds.value = new Set(normalized);
|
||||
}
|
||||
|
||||
function persistPinnedTreeNodeIds() {
|
||||
const snapshot = [...pinnedTreeNodeOrder.value];
|
||||
if (isDesktop) {
|
||||
void api.savePinnedTreeNodeIds([...pinnedTreeNodeIds.value]).catch(() => undefined);
|
||||
// A later drag must never be persisted before an earlier request finishes:
|
||||
// otherwise a slow old request can overwrite the final ordering on disk.
|
||||
pinnedTreeNodePersistQueue = pinnedTreeNodePersistQueue.catch(() => undefined).then(() => api.savePinnedTreeNodeIds(snapshot).catch(() => undefined));
|
||||
return;
|
||||
}
|
||||
if (typeof localStorage === "undefined") return;
|
||||
localStorage.setItem(PINNED_TREE_NODES_STORAGE_KEY, JSON.stringify([...pinnedTreeNodeIds.value]));
|
||||
localStorage.setItem(PINNED_TREE_NODES_STORAGE_KEY, JSON.stringify(snapshot));
|
||||
}
|
||||
|
||||
function findLoadedTreeNodeById(nodes: readonly TreeNode[], id: string): TreeNode | null {
|
||||
for (const node of nodes) {
|
||||
if (node.id === id) return node;
|
||||
const child = node.children ? findLoadedTreeNodeById(node.children, id) : null;
|
||||
if (child) return child;
|
||||
const hiddenChild = node.hiddenChildren ? findLoadedTreeNodeById(node.hiddenChildren, id) : null;
|
||||
if (hiddenChild) return hiddenChild;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
function isTreeNodePinned(node: TreeNode | string): boolean {
|
||||
if (typeof node === "string") return pinnedTreeNodeIds.value.has(node);
|
||||
return pinnedTreeNodeIds.value.has(treeNodePinKey(node)) || pinnedTreeNodeIds.value.has(node.id);
|
||||
if (typeof node !== "string") return pinnedTreeNodeIds.value.has(treeNodePinKey(node)) || pinnedTreeNodeIds.value.has(node.id);
|
||||
if (pinnedTreeNodeIds.value.has(node)) return true;
|
||||
const loadedNode = findLoadedTreeNodeById(treeNodes.value, node);
|
||||
return !!loadedNode && pinnedTreeNodeIds.value.has(treeNodePinKey(loadedNode));
|
||||
}
|
||||
|
||||
function isFixedPriorityTreeNode(node: TreeNode): boolean {
|
||||
if (node.type !== "database" && node.type !== "redis-db" && node.type !== "mongo-db") return false;
|
||||
return !!node.connectionId && typeof node.database === "string" && isDefaultDatabase(node.connectionId, node.database);
|
||||
}
|
||||
|
||||
function orderByPinnedTreeNodes<T>(items: readonly T[], matches: (item: T, identity: PinnedTreeNodeIdentity) => boolean): T[] {
|
||||
return orderItemsByPinnedTreeNodeOrder(items, pinnedTreeNodeOrder.value, matches, treeNodes.value);
|
||||
}
|
||||
|
||||
function syncPinnedTreeState(nodes: TreeNode[]) {
|
||||
syncPinnedTreeNodeStateInPlace(nodes, pinnedTreeNodeIds.value, pinnedTreeNodeOrder.value, isFixedPriorityTreeNode);
|
||||
}
|
||||
|
||||
function isConnectionUtilityNode(node: TreeNode): boolean {
|
||||
@@ -1110,17 +1160,43 @@ export const useConnectionStore = defineStore("connection", () => {
|
||||
return child;
|
||||
});
|
||||
}
|
||||
const migratedPins = migrateLegacyPinnedTreeNodeIds(children, pinnedTreeNodeIds.value);
|
||||
const migratedPins = migrateLegacyPinnedTreeNodeOrder(children, pinnedTreeNodeOrder.value);
|
||||
if (migratedPins.changed) {
|
||||
pinnedTreeNodeIds.value = migratedPins.ids;
|
||||
setPinnedTreeNodeOrder(migratedPins.order);
|
||||
persistPinnedTreeNodeIds();
|
||||
}
|
||||
syncPinnedTreeNodeStateInPlace(children, migratedPins.ids);
|
||||
syncPinnedTreeState(children);
|
||||
parent.children = markRawLeafTreeNodes(children);
|
||||
loadedTreeNodeChildrenIds.value.add(parent.id);
|
||||
}
|
||||
|
||||
function removePinnedTreeNodes(nodes: readonly TreeNode[], canonicalize: PinnedTreeNodeIdentityCanonicalizer = (identity) => identity, legacyKeys: readonly string[] = []): boolean {
|
||||
const nextPinnedOrder = removePinnedTreeNodesFromOrder(pinnedTreeNodeOrder.value, nodes, canonicalize, legacyKeys);
|
||||
if (nextPinnedOrder.length === pinnedTreeNodeOrder.value.length && nextPinnedOrder.every((key, index) => key === pinnedTreeNodeOrder.value[index])) return false;
|
||||
setPinnedTreeNodeOrder(nextPinnedOrder);
|
||||
syncPinnedTreeState(treeNodes.value);
|
||||
persistPinnedTreeNodeIds();
|
||||
return true;
|
||||
}
|
||||
|
||||
function replacePinnedTreeNode(oldNode: TreeNode, newNode: TreeNode, canonicalize: PinnedTreeNodeIdentityCanonicalizer = (identity) => identity, legacyKeys: readonly string[] = []): boolean {
|
||||
// Use the freshly loaded sidebar node when available so the persisted key
|
||||
// carries its real id, not the id of the pre-rename object.
|
||||
const loadedReplacement = findTreeNodes(treeNodes.value, (node) => pinnedTreeNodeIdentityMatches(treeNodePinIdentity(node), treeNodePinIdentity(newNode), canonicalize))[0];
|
||||
// A caller may provide a virtual row while the sidebar object is unloaded;
|
||||
// persisting that row id would create a pin that the sidebar cannot restore.
|
||||
const nextPinnedOrder = loadedReplacement ? replacePinnedTreeNodeInOrder(pinnedTreeNodeOrder.value, oldNode, loadedReplacement, canonicalize, legacyKeys) : removePinnedTreeNodesFromOrder(pinnedTreeNodeOrder.value, [oldNode], canonicalize, legacyKeys);
|
||||
if (nextPinnedOrder.length === pinnedTreeNodeOrder.value.length && nextPinnedOrder.every((key, index) => key === pinnedTreeNodeOrder.value[index])) return false;
|
||||
setPinnedTreeNodeOrder(nextPinnedOrder);
|
||||
syncPinnedTreeState(treeNodes.value);
|
||||
persistPinnedTreeNodeIds();
|
||||
return true;
|
||||
}
|
||||
|
||||
function removeTreeNode(nodeId: string) {
|
||||
const node = findNode(treeNodes.value, nodeId);
|
||||
if (node) removePinnedTreeNodes([node]);
|
||||
|
||||
const parent = findParentNode(treeNodes.value, nodeId);
|
||||
if (parent?.children) {
|
||||
parent.children = parent.children.filter((c) => c.id !== nodeId);
|
||||
@@ -1816,19 +1892,53 @@ export const useConnectionStore = defineStore("connection", () => {
|
||||
|
||||
function toggleTreeNodePin(node: TreeNode) {
|
||||
const pinKey = treeNodePinKey(node);
|
||||
const next = new Set(pinnedTreeNodeIds.value);
|
||||
const wasPinned = next.has(pinKey) || next.has(node.id);
|
||||
const wasPinned = pinnedTreeNodeIds.value.has(pinKey) || pinnedTreeNodeIds.value.has(node.id);
|
||||
// Remove the legacy bare id as part of every toggle so old ambiguous pins
|
||||
// cannot continue matching objects in a different database.
|
||||
next.delete(node.id);
|
||||
if (wasPinned) next.delete(pinKey);
|
||||
else next.add(pinKey);
|
||||
pinnedTreeNodeIds.value = next;
|
||||
// cannot continue matching objects in a different database. Newly pinned
|
||||
// nodes append to the persisted order, placing them last in their sibling
|
||||
// pin section until the user explicitly reorders them.
|
||||
const next = pinnedTreeNodeOrder.value.filter((id) => id !== node.id && id !== pinKey);
|
||||
if (!wasPinned) next.push(pinKey);
|
||||
setPinnedTreeNodeOrder(next);
|
||||
persistPinnedTreeNodeIds();
|
||||
|
||||
// Pinning is infrequent; synchronizing the loaded tree here also clears any
|
||||
// stale flags created by legacy unscoped ids without rebuilding metadata.
|
||||
syncPinnedTreeNodeStateInPlace(treeNodes.value, next);
|
||||
syncPinnedTreeState(treeNodes.value);
|
||||
}
|
||||
|
||||
function findPinnedTreeNodeLocation(nodes: TreeNode[], pinKey: string): { node: TreeNode; siblings: TreeNode[] } | null {
|
||||
for (const node of nodes) {
|
||||
if (treeNodePinKey(node) === pinKey) return { node, siblings: nodes };
|
||||
if (node.children) {
|
||||
const found = findPinnedTreeNodeLocation(node.children, pinKey);
|
||||
if (found) return found;
|
||||
}
|
||||
if (node.hiddenChildren) {
|
||||
const found = findPinnedTreeNodeLocation(node.hiddenChildren, pinKey);
|
||||
if (found) return found;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
function canReorderPinnedTreeNodes(draggedKey: string, targetKey: string): boolean {
|
||||
if (!draggedKey || !targetKey || draggedKey === targetKey) return false;
|
||||
const dragged = findPinnedTreeNodeLocation(treeNodes.value, draggedKey);
|
||||
const target = findPinnedTreeNodeLocation(treeNodes.value, targetKey);
|
||||
if (!dragged || !target || dragged.siblings !== target.siblings) return false;
|
||||
if (!isTreeNodePinned(dragged.node) || !isTreeNodePinned(target.node)) return false;
|
||||
return !isFixedPriorityTreeNode(dragged.node) && !isFixedPriorityTreeNode(target.node);
|
||||
}
|
||||
|
||||
function reorderPinnedTreeNodes(draggedKey: string, targetKey: string, position: DropPosition): boolean {
|
||||
if (position === "inside" || !canReorderPinnedTreeNodes(draggedKey, targetKey)) return false;
|
||||
const next = reorderPinnedTreeNodeOrder(pinnedTreeNodeOrder.value, draggedKey, targetKey, position);
|
||||
if (next.length === pinnedTreeNodeOrder.value.length && next.every((key, index) => key === pinnedTreeNodeOrder.value[index])) return false;
|
||||
setPinnedTreeNodeOrder(next);
|
||||
syncPinnedTreeState(treeNodes.value);
|
||||
persistPinnedTreeNodeIds();
|
||||
return true;
|
||||
}
|
||||
|
||||
async function addConnection(config: ConnectionConfig, targetGroupId?: string | null) {
|
||||
@@ -1942,9 +2052,12 @@ export const useConnectionStore = defineStore("connection", () => {
|
||||
const nextConnections = connections.value.filter((c) => !removedIds.has(c.id));
|
||||
await persistConnections(nextConnections);
|
||||
connections.value = nextConnections;
|
||||
let nextPinnedOrder = pinnedTreeNodeOrder.value;
|
||||
for (const id of removedIds) {
|
||||
pinnedTreeNodeIds.value = prunePinnedTreeNodeIdsForConnection(pinnedTreeNodeIds.value, id);
|
||||
const prefix = `${id}:`;
|
||||
nextPinnedOrder = nextPinnedOrder.filter((pinId) => pinId !== id && !pinId.startsWith(prefix));
|
||||
}
|
||||
setPinnedTreeNodeOrder(nextPinnedOrder);
|
||||
persistPinnedTreeNodeIds();
|
||||
removeSidebarTableNameFiltersForConnections(removedIds);
|
||||
for (const id of removedIds) {
|
||||
@@ -5450,7 +5563,14 @@ export const useConnectionStore = defineStore("connection", () => {
|
||||
}
|
||||
return node;
|
||||
});
|
||||
treeNodes.value = mergeState(freshNodes);
|
||||
const mergedNodes = mergeState(freshNodes);
|
||||
const migratedPins = migrateLegacyPinnedTreeNodeOrder(mergedNodes, pinnedTreeNodeOrder.value);
|
||||
if (migratedPins.changed) {
|
||||
setPinnedTreeNodeOrder(migratedPins.order);
|
||||
persistPinnedTreeNodeIds();
|
||||
}
|
||||
syncPinnedTreeState(mergedNodes);
|
||||
treeNodes.value = mergedNodes;
|
||||
}
|
||||
|
||||
function updateLayoutAndRebuild(nextLayout: SidebarLayout) {
|
||||
@@ -5899,8 +6019,8 @@ export const useConnectionStore = defineStore("connection", () => {
|
||||
async function initFromDisk() {
|
||||
if (!initFromDiskPromise) {
|
||||
initFromDiskPromise = (async () => {
|
||||
const [pinnedIds, saved] = await Promise.all([loadPinnedTreeNodeIds(), api.loadConnections(), tunnelProfileStore.init()]);
|
||||
pinnedTreeNodeIds.value = pinnedIds;
|
||||
const [pinnedOrder, saved] = await Promise.all([loadPinnedTreeNodeOrder(), api.loadConnections(), tunnelProfileStore.init()]);
|
||||
setPinnedTreeNodeOrder(pinnedOrder);
|
||||
connections.value = saved.map(normalizeConnection);
|
||||
const savedLayout = await api.loadSidebarLayout();
|
||||
const currentLayout = sidebarLayout.value.groups.length || sidebarLayout.value.order.length ? sidebarLayout.value : null;
|
||||
@@ -5936,6 +6056,8 @@ export const useConnectionStore = defineStore("connection", () => {
|
||||
connectionMultiSelectActive,
|
||||
treeClipboard,
|
||||
treeNodes,
|
||||
removePinnedTreeNodes,
|
||||
replacePinnedTreeNode,
|
||||
removeTreeNode,
|
||||
refreshAllTree,
|
||||
collapseAllTreeNodes,
|
||||
@@ -5956,7 +6078,10 @@ export const useConnectionStore = defineStore("connection", () => {
|
||||
getConfig,
|
||||
connectionIdentifierQuote,
|
||||
isTreeNodePinned,
|
||||
orderByPinnedTreeNodes,
|
||||
toggleTreeNodePin,
|
||||
canReorderPinnedTreeNodes,
|
||||
reorderPinnedTreeNodes,
|
||||
addConnection,
|
||||
copyConnectionsToTreeClipboard,
|
||||
pasteConnectionClipboard,
|
||||
|
||||
Reference in new issue
Block a user