fix: Search in side bar shows overlapping hosts when grouped by tags (#1303)

https://github.com/Termix-SSH/Support/issues/1303
This commit is contained in:
LukeGus committed 2026-09-20 19:06:07 -05:00
1 parent afae2889cb
commit e9fd092ebd
3 files changed
+68 -10

No files matched your search

+5 -1
View File
@@ -820,9 +820,13 @@ export function SidebarTree({
getItemKey: (index) => {
const row = visibleRows[index];
if (!row) return index;
// Group-by views (e.g. by tags) can list the same host under several
// group folders, so parentPath is folded in to keep each occurrence's
// key unique -- a shared key across rows made the virtualizer collide
// their measured positions and render them stacked on top of each other.
return isFolder(row.item)
? `folder:${row.item.path ?? row.item.name}`
: `host:${row.item.id}`;
: `${row.parentPath}>host:${row.item.id}`;
},
});
+35 -9
View File
@@ -34,7 +34,18 @@ export function folderHasMatch(folder: HostFolder, query: string): boolean {
return false;
}
export type VirtualRow = { item: Host | HostFolder; depth: number };
/** Sentinel parent for rows sitting at the tree root (no folder). */
export const ROOT_PARENT = "__root__";
// parentPath identifies which folder/group a row was collected under. Group-by
// views (e.g. group by tags) can list the same host under several synthetic
// group folders, so the host id alone isn't a unique row identity -- the
// virtualizer needs parentPath+id to tell those rows apart.
export type VirtualRow = {
item: Host | HostFolder;
depth: number;
parentPath: string;
};
function collectVisibleHostRows(
host: Host,
@@ -42,9 +53,10 @@ function collectVisibleHostRows(
closedHostParents: Set<string>,
out: VirtualRow[],
depth: number,
parentPath: string,
): void {
if (!query || hostMatchesQuery(host, query) || hostHasMatch(host, query)) {
out.push({ item: host, depth });
out.push({ item: host, depth, parentPath });
} else {
return;
}
@@ -57,7 +69,14 @@ function collectVisibleHostRows(
const isOpen = query ? true : !closedHostParents.has(hostExpandKey(host));
if (!isOpen) return;
for (const child of childHosts) {
collectVisibleHostRows(child, query, closedHostParents, out, depth + 1);
collectVisibleHostRows(
child,
query,
closedHostParents,
out,
depth + 1,
`${parentPath}>host:${host.id}`,
);
}
}
@@ -68,13 +87,15 @@ export function collectVisibleRows(
out: VirtualRow[] = [],
depth = 0,
closedHostParents: Set<string> = new Set(),
parentPath: string = ROOT_PARENT,
): VirtualRow[] {
for (const child of children) {
if (isFolder(child)) {
const visible = query ? folderHasMatch(child, query) : true;
if (!visible) continue;
out.push({ item: child, depth });
const childOpen = query ? true : openSet.has(child.path ?? child.name);
const path = child.path ?? child.name;
out.push({ item: child, depth, parentPath });
const childOpen = query ? true : openSet.has(path);
if (childOpen)
collectVisibleRows(
child.children,
@@ -83,9 +104,17 @@ export function collectVisibleRows(
out,
depth + 1,
closedHostParents,
path,
);
} else {
collectVisibleHostRows(child, query, closedHostParents, out, depth);
collectVisibleHostRows(
child,
query,
closedHostParents,
out,
depth,
parentPath,
);
}
}
return out;
@@ -123,9 +152,6 @@ export function collectAllFolderPaths(
return Array.from(paths).sort((a, b) => a.localeCompare(b));
}
/** Sentinel parent for rows sitting at the tree root (no folder). */
export const ROOT_PARENT = "__root__";
export function rowKey(item: Host | HostFolder): string {
return isFolder(item)
? `folder:${item.path ?? item.name}`
@@ -133,6 +133,34 @@ describe("collectVisibleRows with sub-host nesting", () => {
});
});
describe("collectVisibleRows with group-by views", () => {
it("gives each occurrence of a multi-tag host a distinct parentPath", () => {
// Group-by-tags puts the same host under one synthetic folder per tag,
// so a host with two tags shows up twice in the tree with the same id.
const multiTagHost = host("9", "shared-box");
const tree: (Host | HostFolder)[] = [
{
name: "prod",
path: "__group__:tag:prod",
children: [multiTagHost],
},
{
name: "web",
path: "__group__:tag:web",
children: [multiTagHost],
},
];
const rows = collectVisibleRows(
tree,
"",
new Set(["__group__:tag:prod", "__group__:tag:web"]),
);
const hostRows = rows.filter((r) => !isFolder(r.item));
expect(hostRows).toHaveLength(2);
expect(hostRows[0].parentPath).not.toBe(hostRows[1].parentPath);
});
});
describe("buildReorderRows", () => {
it("keys folders by path and hosts by id", () => {
const folder: HostFolder = {