diff --git a/catalog/CHANGELOG.md b/catalog/CHANGELOG.md
index 285ea607f3a..6246ada7be3 100644
--- a/catalog/CHANGELOG.md
+++ b/catalog/CHANGELOG.md
@@ -18,6 +18,7 @@ where verb is one of
## Changes
+- [Fixed] Package page: clicking a package-name prefix filters the package list to that prefix again — regressed by the unified-search migration ([#4413](https://github.com/quiltdata/quilt/pull/4413)), which stopped reading the link's legacy `filter` param ([#5035](https://github.com/quiltdata/quilt/pull/5035))
- [Fixed] Test suite: register `afterEach(cleanup)` so `@testing-library/react` components are unmounted between tests. With `globals: false` ([#4660](https://github.com/quiltdata/quilt/pull/4660)) RTL's auto-cleanup never registered, leaving components mounted; their deferred passive effects could flush after the jsdom environment was torn down, intermittently failing the run with an unhandled "`document` global … not defined anymore" error even when every test passed ([#5026](https://github.com/quiltdata/quilt/pull/5026))
- [Fixed] Qurator chat: drop `
`-in-`
` DOM-nesting warning from the connector status helper text shown while connectors are still connecting ([#5003](https://github.com/quiltdata/quilt/pull/5003))
- [Added] Bucket Overview v2: a denser redesign gated behind the catalog `beta` features toggle (Admin Settings) — navigational header stats, inline Qurator, and a config-driven Tabulator tables section linking into Athena Queries ([#4995](https://github.com/quiltdata/quilt/pull/4995))
diff --git a/catalog/app/constants/routes.ts b/catalog/app/constants/routes.ts
index d0858b4f978..fb6bcb0da05 100644
--- a/catalog/app/constants/routes.ts
+++ b/catalog/app/constants/routes.ts
@@ -124,15 +124,15 @@ export const bucketDir = route(
export type BucketDirArgs = Parameters
interface BucketPackageListOpts {
- filter?: string
- sort?: string
- p?: string
+ // KeywordWildcard value for the search model's `name` filter (e.g. `foo/` →
+ // matches the `foo/*` prefix). Must stay in sync with PackagesSearchFilterIO.
+ name?: string
}
export const bucketPackageList = route(
'/b/:bucket/packages/',
- (bucket: string, { filter, sort, p }: BucketPackageListOpts = {}) =>
- `/b/${bucket}/packages/${mkSearch({ filter, sort, p })}`,
+ (bucket: string, { name }: BucketPackageListOpts = {}) =>
+ `/b/${bucket}/packages/${mkSearch({ name })}`,
)
export type BucketPackageListArgs = Parameters
diff --git a/catalog/app/containers/Bucket/PackageTree/PackageLink.spec.tsx b/catalog/app/containers/Bucket/PackageTree/PackageLink.spec.tsx
new file mode 100644
index 00000000000..78485f5ccf9
--- /dev/null
+++ b/catalog/app/containers/Bucket/PackageTree/PackageLink.spec.tsx
@@ -0,0 +1,56 @@
+import * as React from 'react'
+import { describe, it, expect, vi } from 'vitest'
+import { MemoryRouter } from 'react-router-dom'
+import { render } from '@testing-library/react'
+
+import { bucketPackageList, bucketPackageDetail } from 'constants/routes'
+import * as NamedRoutes from 'utils/NamedRoutes'
+import {
+ PackagesSearchFilterIO,
+ ResultType,
+ parseSearchParams,
+} from 'containers/Search/model'
+
+import PackageLink from './PackageLink'
+
+// The Search model's import graph pulls in `constants/config`, which throws
+// unless a catalog config is present on `window`.
+vi.mock('constants/config', () => ({ default: {} }))
+
+describe('containers/Bucket/PackageTree/PackageLink', () => {
+ // Regression guard (#4413): the package list reads filters by predicate key
+ // and ignores unrecognized params, so the prefix link must emit a param that
+ // round-trips through `parseSearchParams` into the `name` filter.
+ it('prefix link round-trips to the package list `name` filter', () => {
+ const { getByRole } = render(
+
+
+
+
+ ,
+ )
+
+ const href = getByRole('link', { name: 'team/' }).getAttribute('href')
+ expect(href).toBeTruthy()
+
+ const state = parseSearchParams(new URL(href!, 'http://localhost').search)
+
+ expect(state.resultType).toBe(ResultType.QuiltPackage)
+ if (state.resultType !== ResultType.QuiltPackage) throw new Error('unreachable')
+
+ expect(state.filter.predicates.name).toMatchObject({
+ wildcard: 'team/',
+ strict: false,
+ })
+
+ // ...and the prefix becomes a `team/*` wildcard at the GraphQL layer.
+ expect(PackagesSearchFilterIO.toGQL(state.filter)?.name?.wildcard).toBe('team/*')
+ })
+
+ // The flip side: the pre-fix `filter=` param must stay inert.
+ it('drops the unrecognized legacy `filter` param', () => {
+ const state = parseSearchParams('?filter=team/')
+ if (state.resultType !== ResultType.QuiltPackage) throw new Error('unreachable')
+ expect(state.filter.predicates.name).toBeNull()
+ })
+})
diff --git a/catalog/app/containers/Bucket/PackageTree/PackageLink.tsx b/catalog/app/containers/Bucket/PackageTree/PackageLink.tsx
index 472b4babc79..000f45753eb 100644
--- a/catalog/app/containers/Bucket/PackageTree/PackageLink.tsx
+++ b/catalog/app/containers/Bucket/PackageTree/PackageLink.tsx
@@ -21,7 +21,7 @@ export default function PackageLink({ bucket, name }: PackageLinkProps) {
const [prefix, suffix] = name.split('/')
return (
- {prefix}/
+ {prefix}/
{suffix}
)