searching finds items in components of a pcakage and shows which ones match

This commit is contained in:
Smartsheet-JB-Brown 2025-04-14 09:18:48 -07:00
parent 81304a22ea
commit dee7510ed7
7 changed files with 159 additions and 82 deletions

View file

@ -101,9 +101,10 @@ describe("Package Manager Integration", () => {
expect(dataValidator).toBeDefined()
expect(dataValidator?.metadata?.description).toContain("validating data quality")
// Verify only matching subcomponents are included
expect(filteredItems[0].items?.length).toBe(1)
expect(filteredItems[0].items?.[0].metadata?.name).toBe("Data Validator")
// Verify only matching subcomponents have matchInfo.matched = true
const matchingSubcomponents = filteredItems[0].items?.filter((item) => item.matchInfo?.matched) || []
expect(matchingSubcomponents.length).toBe(1)
expect(matchingSubcomponents[0].metadata?.name).toBe("Data Validator")
})
it("should handle partial matches", async () => {
@ -161,8 +162,11 @@ describe("Package Manager Integration", () => {
const packageWithServer = filteredItems.find((item) => item.type === "package")
expect(packageWithServer).toBeDefined()
expect(packageWithServer?.name).toBe("Data Platform Package")
expect(packageWithServer?.items?.length).toBe(1)
expect(packageWithServer?.items?.[0].metadata?.name).toBe("Data Validator")
// Count how many subcomponents have matchInfo.matched = true
const matchingSubcomponents = packageWithServer?.items?.filter((item) => item.matchInfo?.matched) || []
expect(matchingSubcomponents.length).toBe(1)
expect(matchingSubcomponents[0].metadata?.name).toBe("Data Validator")
// Verify excluded items
const allItems = [...templateItems]

View file

@ -38,8 +38,11 @@ describe("Package Manager with Real Data", () => {
// Should find Data Platform Package containing Data Validator
expect(filteredItems.length).toBe(1)
expect(filteredItems[0].name).toBe("Data Platform Package")
expect(filteredItems[0].items?.length).toBe(1)
expect(filteredItems[0].items?.[0].metadata?.name).toBe("Data Validator")
// Count how many subcomponents have matchInfo.matched = true
const matchingSubcomponents = filteredItems[0].items?.filter((item) => item.matchInfo?.matched) || []
expect(matchingSubcomponents.length).toBe(1)
expect(matchingSubcomponents[0].metadata?.name).toBe("Data Validator")
// Verify excluded items
const excludedItems = templateItems.filter(
@ -49,22 +52,11 @@ describe("Package Manager with Real Data", () => {
expect(excludedItems).toContainEqual(expect.objectContaining({ name: "Data Engineer" }))
expect(excludedItems).toContainEqual(expect.objectContaining({ name: "Example MCP Server" }))
// Verify excluded subcomponents
const excludedSubcomponents = templateItems
.find((item) => item.name === "Data Platform Package")
?.items?.filter(
(subItem) =>
!filteredItems[0].items?.some(
(filtered) => filtered.metadata?.name === subItem.metadata?.name,
),
)
expect(excludedSubcomponents).toContainEqual(
expect.objectContaining({
metadata: expect.objectContaining({
name: "Data Platform Administrator",
}),
}),
)
// Verify non-matching subcomponents
const nonMatchingSubcomponents =
filteredItems[0].items?.filter((item) => !item.matchInfo?.matched) || []
expect(nonMatchingSubcomponents.length).toBe(1)
expect(nonMatchingSubcomponents[0].metadata?.name).toBe("Data Platform Administrator")
}
})
@ -114,8 +106,11 @@ describe("Package Manager with Real Data", () => {
const packageWithServer = filteredItems.find((item) => item.type === "package")
expect(packageWithServer).toBeDefined()
expect(packageWithServer?.name).toBe("Data Platform Package")
expect(packageWithServer?.items?.length).toBe(1)
expect(packageWithServer?.items?.[0].metadata?.name).toBe("Data Validator")
// Count how many subcomponents have matchInfo.matched = true
const matchingSubcomponents = packageWithServer?.items?.filter((item) => item.matchInfo?.matched) || []
expect(matchingSubcomponents.length).toBe(1)
expect(matchingSubcomponents[0].metadata?.name).toBe("Data Validator")
// Verify excluded items (either wrong type or no "data" match)
const excludedItems = templateItems.filter(
@ -128,23 +123,12 @@ describe("Package Manager with Real Data", () => {
// Data Engineer - has "data" but wrong type
expect(excludedItems).toContainEqual(expect.objectContaining({ name: "Data Engineer" }))
// Verify excluded subcomponents (either wrong type or no "data" match)
const excludedSubcomponents = templateItems
.find((item) => item.name === "Data Platform Package")
?.items?.filter(
(subItem) =>
!filteredItems
.find((item) => item.name === "Data Platform Package")
?.items?.some((filtered) => filtered.metadata?.name === subItem.metadata?.name),
)
// Data Platform Administrator - wrong type
expect(excludedSubcomponents).toContainEqual(
expect.objectContaining({
metadata: expect.objectContaining({
name: "Data Platform Administrator",
}),
}),
)
// Verify non-matching subcomponents (either wrong type or no "data" match)
const packageWithServerItem = filteredItems.find((item) => item.type === "package")
const nonMatchingSubcomponents =
packageWithServerItem?.items?.filter((item) => !item.matchInfo?.matched) || []
expect(nonMatchingSubcomponents.length).toBe(1)
expect(nonMatchingSubcomponents[0].metadata?.name).toBe("Data Platform Administrator")
})
it("should handle specific search with type filtering", () => {
@ -157,8 +141,11 @@ describe("Package Manager with Real Data", () => {
// Should only find Data Platform Package containing Data Validator
expect(filteredItems.length).toBe(1)
expect(filteredItems[0].name).toBe("Data Platform Package")
expect(filteredItems[0].items?.length).toBe(1)
expect(filteredItems[0].items?.[0].metadata?.name).toBe("Data Validator")
// Count how many subcomponents have matchInfo.matched = true
const matchingSubcomponents = filteredItems[0].items?.filter((item) => item.matchInfo?.matched) || []
expect(matchingSubcomponents.length).toBe(1)
expect(matchingSubcomponents[0].metadata?.name).toBe("Data Validator")
// Verify excluded items
const excludedItems = templateItems.filter(
@ -186,22 +173,18 @@ describe("Package Manager with Real Data", () => {
it("should exclude non-matching types", () => {
const filteredItems = manager.filterItems(templateItems, { type: "mode" })
// Should exclude all non-mode items
const excludedItems = templateItems.filter(
(item) => !filteredItems.some((filtered) => filtered.name === item.name),
)
expect(excludedItems).toContainEqual(
expect.objectContaining({
name: "Data Processor",
type: "mcp server",
}),
)
expect(excludedItems).toContainEqual(
expect.objectContaining({
name: "Data Platform Package",
type: "package",
}),
)
// Should include only mode items
const modeItems = filteredItems.filter((item) => item.type === "mode")
expect(modeItems.length).toBeGreaterThan(0)
// Verify that the filtered results include items of type "mode"
expect(modeItems.length).toBeGreaterThan(0)
// Verify specific items are not in the filtered items
const filteredItemNames = filteredItems.map((item) => item.name)
// Verify that items of type "mcp server" are not included
expect(filteredItemNames).not.toContain("Data Processor")
expect(filteredItemNames).not.toContain("Example MCP Server")
expect(filteredItemNames).not.toContain("File Analyzer MCP Server")
})
})
})

View file

@ -6,6 +6,7 @@ interface ExpandableSectionProps {
children: React.ReactNode
className?: string
defaultExpanded?: boolean
badge?: string
}
export const ExpandableSection: React.FC<ExpandableSectionProps> = ({
@ -13,6 +14,7 @@ export const ExpandableSection: React.FC<ExpandableSectionProps> = ({
children,
className,
defaultExpanded = false,
badge,
}) => {
const [isExpanded, setIsExpanded] = useState(defaultExpanded)
@ -23,14 +25,24 @@ export const ExpandableSection: React.FC<ExpandableSectionProps> = ({
onClick={() => setIsExpanded(!isExpanded)}
aria-expanded={isExpanded}
aria-controls="details-content">
<span className="font-medium">{title}</span>
<span
className={cn(
"codicon",
isExpanded ? "codicon-chevron-down" : "codicon-chevron-right",
"transition-transform duration-200",
<span className="font-medium flex items-center">
<span className="codicon codicon-list-unordered mr-1"></span>
{title}
</span>
<div className="flex items-center">
{badge && (
<span className="mr-2 text-xs bg-vscode-badge-background text-vscode-badge-foreground px-1 py-0.5 rounded">
{badge}
</span>
)}
/>
<span
className={cn(
"codicon",
isExpanded ? "codicon-chevron-down" : "codicon-chevron-right",
"transition-transform duration-200",
)}
/>
</div>
</button>
<div
id="details-content"
@ -40,7 +52,7 @@ export const ExpandableSection: React.FC<ExpandableSectionProps> = ({
)}
role="region"
aria-labelledby="details-button">
<div className="py-2">{children}</div>
<div className="py-2 px-1 bg-vscode-panel-background rounded-sm">{children}</div>
</div>
</div>
)

View file

@ -150,10 +150,43 @@ export const PackageManagerItemCard: React.FC<PackageManagerItemCardProps> = ({
</div>
{groupedItems && (
<ExpandableSection title="Details">
{Object.entries(groupedItems).map(([type, group]) => (
<TypeGroup key={type} type={type} items={group.items} />
))}
<ExpandableSection
title="Component Details"
badge={
filters.search
? (() => {
const matchCount =
item.items?.filter(
(subItem) =>
(subItem.metadata?.name || "")
.toLowerCase()
.includes(filters.search.toLowerCase()) ||
(subItem.metadata?.description || "")
.toLowerCase()
.includes(filters.search.toLowerCase()),
).length || 0
return matchCount > 0
? `${matchCount} match${matchCount !== 1 ? "es" : ""}`
: undefined
})()
: undefined
}
defaultExpanded={
!!filters.search &&
(item.items?.some(
(subItem) =>
(subItem.metadata?.name || "").toLowerCase().includes(filters.search.toLowerCase()) ||
(subItem.metadata?.description || "")
.toLowerCase()
.includes(filters.search.toLowerCase()),
) ||
false)
}>
<div className="space-y-4">
{Object.entries(groupedItems).map(([type, group]) => (
<TypeGroup key={type} type={type} items={group.items} searchTerm={filters.search} />
))}
</div>
</ExpandableSection>
)}
</div>

View file

@ -1,6 +1,5 @@
import React from "react"
import { cn } from "@/lib/utils"
import { formatItemText } from "../utils/grouping"
interface TypeGroupProps {
type: string
@ -11,9 +10,10 @@ interface TypeGroupProps {
path?: string
}>
className?: string
searchTerm?: string
}
export const TypeGroup: React.FC<TypeGroupProps> = ({ type, items, className }) => {
export const TypeGroup: React.FC<TypeGroupProps> = ({ type, items, className, searchTerm }) => {
const getTypeLabel = (type: string) => {
switch (type) {
case "mode":
@ -33,15 +33,41 @@ export const TypeGroup: React.FC<TypeGroupProps> = ({ type, items, className })
return null
}
// Check if an item matches the search term
const itemMatchesSearch = (item: { name: string; description?: string }) => {
if (!searchTerm) return false
const term = searchTerm.toLowerCase()
return item.name.toLowerCase().includes(term) || (item.description || "").toLowerCase().includes(term)
}
return (
<div className={cn("mb-4", className)}>
<h4 className="text-sm font-medium text-vscode-foreground mb-2">{getTypeLabel(type)}</h4>
<ol className="list-decimal list-inside space-y-1">
{items.map((item, index) => (
<li key={`${item.path || index}`} className="text-sm text-vscode-foreground pl-1" title={item.path}>
<span className="text-vscode-descriptionForeground">{formatItemText(item)}</span>
</li>
))}
{items.map((item, index) => {
const matches = itemMatchesSearch(item)
return (
<li
key={`${item.path || index}`}
className={cn(
"text-sm pl-1",
matches ? "text-vscode-foreground font-medium" : "text-vscode-foreground",
)}
title={item.path}>
<span className={cn("font-medium", matches ? "text-vscode-textLink" : "")}>
{item.name}
</span>
{item.description && (
<span className="text-vscode-descriptionForeground"> - {item.description}</span>
)}
{matches && (
<span className="ml-2 text-xs bg-vscode-badge-background text-vscode-badge-foreground px-1 py-0.5 rounded">
match
</span>
)}
</li>
)
})}
</ol>
</div>
)

View file

@ -29,12 +29,23 @@ export const filterItems = (items: PackageManagerItem[], filters: Filters): Pack
// Search filter
if (filters.search) {
const searchTerm = filters.search.toLowerCase()
const matchesSearch =
// Check if the main item matches
const mainItemMatches =
item.name.toLowerCase().includes(searchTerm) ||
(item.description || "").toLowerCase().includes(searchTerm) ||
(item.author || "").toLowerCase().includes(searchTerm)
if (!matchesSearch) {
// Check if any subcomponents match
const subcomponentMatches =
item.items?.some(
(subItem) =>
(subItem.metadata?.name || "").toLowerCase().includes(searchTerm) ||
(subItem.metadata?.description || "").toLowerCase().includes(searchTerm),
) || false
// Return false if neither the main item nor any subcomponents match
if (!mainItemMatches && !subcomponentMatches) {
return false
}
}

View file

@ -54,7 +54,15 @@ export function formatItemText(item: { name: string; description?: string }): st
if (!item.description) {
return item.name
}
return `${item.name} - ${item.description}`
// Truncate description if it's too long
const maxDescriptionLength = 100
const description =
item.description.length > maxDescriptionLength
? `${item.description.substring(0, maxDescriptionLength)}...`
: item.description
return `${item.name} - ${description}`
}
/**