fix(common): normalize search tree adapter for team collection shape [skip ci]

The search tree adapter was typed `Ref<HoppCollection[]>` but the teams
provider forces a `Ref<TeamCollection[]>` into it via a double-cast
(`as unknown as Ref<HoppCollection[]>` in teams.workspace.ts:1472).
`HoppCollection` uses `folders` while `TeamCollection` uses `children`.

Root-level rendering worked because both types expose a top-level
`requests`, but expanding a nested team search hit would throw
`TypeError: Cannot read properties of undefined (reading 'map')` on
`item.folders.map`.

Normalize access inside the adapter via a union-safe shape that reads
either `folders` or `children`, and replace the personal-only
`navigateToFolderWithIndexPath` walk with a local walker using the same
accessor. Personal-workspace behavior is unchanged; team-workspace
expansion now walks the nested tree correctly.
This commit is contained in:
James George 2026-04-14 13:47:40 +05:30
parent e49e0a0013
commit 609dfe84e7

View File

@ -1,9 +1,39 @@
import { HoppCollection } from "@hoppscotch/data"
import { ChildrenResult, SmartTreeAdapter } from "@hoppscotch/ui"
import { Ref, computed } from "vue"
import { navigateToFolderWithIndexPath } from "~/newstore/collections"
import { RESTCollectionViewItem } from "~/services/new-workspace/view"
// Shape covers both `HoppCollection` (personal — uses `folders`) and
// `TeamCollection` (teams — uses `children`). Normalized at access time
// so a single adapter handles both workspaces.
type SearchTreeNode = {
name?: string
title?: string
folders?: SearchTreeNode[] | null
children?: SearchTreeNode[] | null
requests?: Array<{ name?: string; title?: string; [key: string]: unknown }> | null
}
const getChildCollections = (node: SearchTreeNode): SearchTreeNode[] =>
node.folders ?? node.children ?? []
const getNodeName = (node: SearchTreeNode): string =>
node.name ?? node.title ?? ""
// Local tree walk — does not depend on `folders` shape unlike the
// personal-workspace `navigateToFolderWithIndexPath` helper.
const navigateToNode = (
roots: SearchTreeNode[],
indexPath: number[]
): SearchTreeNode | null => {
if (indexPath.length === 0) return null
let current: SearchTreeNode | undefined = roots[indexPath[0]]
for (let i = 1; i < indexPath.length && current; i++) {
current = getChildCollections(current)[indexPath[i]]
}
return current ?? null
}
export class WorkspaceRESTSearchCollectionTreeAdapter
implements SmartTreeAdapter<RESTCollectionViewItem>
{
@ -13,18 +43,20 @@ export class WorkspaceRESTSearchCollectionTreeAdapter
nodeID: string | null
): Ref<ChildrenResult<RESTCollectionViewItem>> {
return computed(() => {
const roots = this.data.value as unknown as SearchTreeNode[]
if (nodeID === null) {
return {
status: "loaded" as const,
data: this.data.value.map((item, index) => ({
data: roots.map((item, index) => ({
id: index.toString(),
data: <RESTCollectionViewItem>{
type: "collection",
value: {
collectionID: index.toString(),
isLastItem: index === this.data.value.length - 1,
name: item.name,
isLastItem: index === roots.length - 1,
name: getNodeName(item),
parentCollectionID: null,
},
},
@ -33,11 +65,11 @@ export class WorkspaceRESTSearchCollectionTreeAdapter
}
const indexPath = nodeID.split("/").map((x) => parseInt(x, 10))
const item = navigateToFolderWithIndexPath(this.data.value, indexPath)
const item = navigateToNode(roots, indexPath)
if (item) {
const collections = item.folders.map(
const childCollections = getChildCollections(item)
const collections = childCollections.map(
(childCollection, childCollectionID) => {
return {
id: `${nodeID}/${childCollectionID}`,
@ -45,9 +77,9 @@ export class WorkspaceRESTSearchCollectionTreeAdapter
type: "collection",
value: {
isLastItem:
childCollectionID === item.folders.length - 1,
childCollectionID === childCollections.length - 1,
collectionID: `${nodeID}/${childCollectionID}`,
name: childCollection.name,
name: getNodeName(childCollection),
parentCollectionID: nodeID,
},
},
@ -55,14 +87,14 @@ export class WorkspaceRESTSearchCollectionTreeAdapter
}
)
const requests = item.requests.map((request, requestID) => {
const requestsList = item.requests ?? []
const requests = requestsList.map((request, requestID) => {
return {
id: `${nodeID}/${requestID}`,
data: <RESTCollectionViewItem>{
type: "request",
value: {
isLastItem:
requestID === item.requests.length - 1,
isLastItem: requestID === requestsList.length - 1,
parentCollectionID: nodeID,
collectionID: nodeID,
requestID: `${nodeID}/${requestID}`,