fix(next): incorrect active state for partial matches of collection names in sidebar (#11511)

Previously, collections with similar names (e.g., `uploads` and
`uploads-poly`) both appeared active when viewing either collection.

This was due to `pathname.startsWith(href)`, which caused partial
matches.

This update refines the `isActive` logic to prevent partial matches.
This commit is contained in:
Patrik
2025-03-03 16:46:47 -05:00
committed by GitHub
parent c417e3a627
commit 7d2480aef9
6 changed files with 88 additions and 1 deletions

View File

@@ -44,7 +44,8 @@ export const DefaultNavClient: React.FC<{
id = `nav-global-${slug}` id = `nav-global-${slug}`
} }
const isActive = pathname.startsWith(href) const isActive =
pathname.startsWith(href) && ['/', undefined].includes(pathname[href.length])
const Label = ( const Label = (
<> <>

View File

@@ -0,0 +1,14 @@
import type { CollectionConfig } from 'payload'
import { uploadTwoCollectionSlug } from '../slugs.js'
export const UploadTwoCollection: CollectionConfig = {
slug: uploadTwoCollectionSlug,
fields: [
{
name: 'title',
type: 'text',
},
],
upload: true,
}

View File

@@ -19,6 +19,7 @@ import { CollectionNoApiView } from './collections/NoApiView.js'
import { CollectionNotInView } from './collections/NotInView.js' import { CollectionNotInView } from './collections/NotInView.js'
import { Posts } from './collections/Posts.js' import { Posts } from './collections/Posts.js'
import { UploadCollection } from './collections/Upload.js' import { UploadCollection } from './collections/Upload.js'
import { UploadTwoCollection } from './collections/UploadTwo.js'
import { Users } from './collections/Users.js' import { Users } from './collections/Users.js'
import { with300Documents } from './collections/With300Documents.js' import { with300Documents } from './collections/With300Documents.js'
import { CustomGlobalViews1 } from './globals/CustomViews1.js' import { CustomGlobalViews1 } from './globals/CustomViews1.js'
@@ -40,6 +41,7 @@ import {
protectedCustomNestedViewPath, protectedCustomNestedViewPath,
publicCustomViewPath, publicCustomViewPath,
} from './shared.js' } from './shared.js'
export default buildConfigWithDefaults({ export default buildConfigWithDefaults({
admin: { admin: {
importMap: { importMap: {
@@ -141,6 +143,7 @@ export default buildConfigWithDefaults({
}, },
collections: [ collections: [
UploadCollection, UploadCollection,
UploadTwoCollection,
Posts, Posts,
Users, Users,
CollectionHidden, CollectionHidden,

View File

@@ -39,6 +39,7 @@ import {
notInViewCollectionSlug, notInViewCollectionSlug,
postsCollectionSlug, postsCollectionSlug,
settingsGlobalSlug, settingsGlobalSlug,
uploadTwoCollectionSlug,
} from '../../slugs.js' } from '../../slugs.js'
const { beforeAll, beforeEach, describe } = test const { beforeAll, beforeEach, describe } = test
@@ -73,6 +74,7 @@ describe('General', () => {
let disableDuplicateURL: AdminUrlUtil let disableDuplicateURL: AdminUrlUtil
let serverURL: string let serverURL: string
let adminRoutes: ReturnType<typeof getRoutes> let adminRoutes: ReturnType<typeof getRoutes>
let uploadsTwo: AdminUrlUtil
beforeAll(async ({ browser }, testInfo) => { beforeAll(async ({ browser }, testInfo) => {
const prebuild = false // Boolean(process.env.CI) const prebuild = false // Boolean(process.env.CI)
@@ -90,6 +92,7 @@ describe('General', () => {
globalURL = new AdminUrlUtil(serverURL, globalSlug) globalURL = new AdminUrlUtil(serverURL, globalSlug)
customViewsURL = new AdminUrlUtil(serverURL, customViews2CollectionSlug) customViewsURL = new AdminUrlUtil(serverURL, customViews2CollectionSlug)
disableDuplicateURL = new AdminUrlUtil(serverURL, disableDuplicateSlug) disableDuplicateURL = new AdminUrlUtil(serverURL, disableDuplicateSlug)
uploadsTwo = new AdminUrlUtil(serverURL, uploadTwoCollectionSlug)
context = await browser.newContext() context = await browser.newContext()
page = await context.newPage() page = await context.newPage()
@@ -424,6 +427,27 @@ describe('General', () => {
expect(tagName).toBe('a') expect(tagName).toBe('a')
}) })
test('should only have one nav item active at a time', async () => {
await page.goto(uploadsTwo.list)
await openNav(page)
// Locate "uploads" and "uploads-two" nav items
const uploadsNavItem = page.locator('.nav-group__content #nav-uploads')
const uploadsTwoNavItem = page.locator('.nav-group__content #nav-uploads-two')
// Ensure both exist before continuing
await expect(uploadsNavItem).toBeVisible()
await expect(uploadsTwoNavItem).toBeVisible()
// Locate all nav items containing the nav__link-indicator
const activeNavItems = page.locator(
'.nav-group__content .nav__link:has(.nav__link-indicator), .nav-group__content div.nav__link:has(.nav__link-indicator)',
)
// Expect exactly one nav item to have the indicator
await expect(activeNavItems).toHaveCount(1)
})
test('breadcrumbs — should navigate from list to dashboard', async () => { test('breadcrumbs — should navigate from list to dashboard', async () => {
await page.goto(postsUrl.list) await page.goto(postsUrl.list)
await page.locator(`.step-nav a[href="${adminRoutes.routes.admin}"]`).click() await page.locator(`.step-nav a[href="${adminRoutes.routes.admin}"]`).click()

View File

@@ -66,6 +66,7 @@ export interface Config {
}; };
collections: { collections: {
uploads: Upload; uploads: Upload;
'uploads-two': UploadsTwo;
posts: Post; posts: Post;
users: User; users: User;
'hidden-collection': HiddenCollection; 'hidden-collection': HiddenCollection;
@@ -90,6 +91,7 @@ export interface Config {
collectionsJoins: {}; collectionsJoins: {};
collectionsSelect: { collectionsSelect: {
uploads: UploadsSelect<false> | UploadsSelect<true>; uploads: UploadsSelect<false> | UploadsSelect<true>;
'uploads-two': UploadsTwoSelect<false> | UploadsTwoSelect<true>;
posts: PostsSelect<false> | PostsSelect<true>; posts: PostsSelect<false> | PostsSelect<true>;
users: UsersSelect<false> | UsersSelect<true>; users: UsersSelect<false> | UsersSelect<true>;
'hidden-collection': HiddenCollectionSelect<false> | HiddenCollectionSelect<true>; 'hidden-collection': HiddenCollectionSelect<false> | HiddenCollectionSelect<true>;
@@ -192,6 +194,25 @@ export interface Upload {
}; };
}; };
} }
/**
* This interface was referenced by `Config`'s JSON-Schema
* via the `definition` "uploads-two".
*/
export interface UploadsTwo {
id: string;
title?: string | null;
updatedAt: string;
createdAt: string;
url?: string | null;
thumbnailURL?: string | null;
filename?: string | null;
mimeType?: string | null;
filesize?: number | null;
width?: number | null;
height?: number | null;
focalX?: number | null;
focalY?: number | null;
}
/** /**
* This is a custom collection description. * This is a custom collection description.
* *
@@ -469,6 +490,10 @@ export interface PayloadLockedDocument {
relationTo: 'uploads'; relationTo: 'uploads';
value: string | Upload; value: string | Upload;
} | null) } | null)
| ({
relationTo: 'uploads-two';
value: string | UploadsTwo;
} | null)
| ({ | ({
relationTo: 'posts'; relationTo: 'posts';
value: string | Post; value: string | Post;
@@ -611,6 +636,24 @@ export interface UploadsSelect<T extends boolean = true> {
}; };
}; };
} }
/**
* This interface was referenced by `Config`'s JSON-Schema
* via the `definition` "uploads-two_select".
*/
export interface UploadsTwoSelect<T extends boolean = true> {
title?: T;
updatedAt?: T;
createdAt?: T;
url?: T;
thumbnailURL?: T;
filename?: T;
mimeType?: T;
filesize?: T;
width?: T;
height?: T;
focalX?: T;
focalY?: T;
}
/** /**
* This interface was referenced by `Config`'s JSON-Schema * This interface was referenced by `Config`'s JSON-Schema
* via the `definition` "posts_select". * via the `definition` "posts_select".

View File

@@ -12,6 +12,8 @@ export const notInViewCollectionSlug = 'not-in-view-collection'
export const noApiViewCollectionSlug = 'collection-no-api-view' export const noApiViewCollectionSlug = 'collection-no-api-view'
export const disableDuplicateSlug = 'disable-duplicate' export const disableDuplicateSlug = 'disable-duplicate'
export const uploadCollectionSlug = 'uploads' export const uploadCollectionSlug = 'uploads'
export const uploadTwoCollectionSlug = 'uploads-two'
export const customFieldsSlug = 'custom-fields' export const customFieldsSlug = 'custom-fields'
export const listDrawerSlug = 'with-list-drawer' export const listDrawerSlug = 'with-list-drawer'