Skip to content

Commit b1d92c2

Browse files
authored
feat: allows excluding entities from the nav sidebar / dashboard without disabling its routes (#9897)
### What? Previously, the `admin.group` property on `collection` / `global` configs allowed for a custom group and the `admin.hidden` property would not only hide the entity from the nav sidebar / dashboard but also disable its routes. ### Why? There was not a simple way to hide an entity from the nav sidebar / dashboard but still keep the entities routes. ### How? Now - we've added the `false` type to the `admin.group` field to account for this. Passing `false` to `admin.group` will hide the entity from the sidebar nav and dashboard but keep the routes available to navigate. I.e ``` admin: { group: false, }, ```
1 parent 5c2f72d commit b1d92c2

File tree

10 files changed

+99
-25
lines changed

10 files changed

+99
-25
lines changed

docs/admin/collections.mdx

Lines changed: 16 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -25,24 +25,24 @@ export const MyCollection: CollectionConfig = {
2525

2626
The following options are available:
2727

28-
| Option | Description |
29-
| -------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
30-
| **`group`** | Text used as a label for grouping Collection and Global links together in the navigation. |
31-
| **`hidden`** | Set to true or a function, called with the current user, returning true to exclude this Collection from navigation and admin routing. |
32-
| **`hooks`** | Admin-specific hooks for this Collection. [More details](../hooks/collections). |
33-
| **`useAsTitle`** | Specify a top-level field to use for a document title throughout the Admin Panel. If no field is defined, the ID of the document is used as the title. A field with `virtual: true` cannot be used as the title. |
28+
| Option | Description |
29+
| -------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
30+
| **`group`** | Text or localization object used to group Collection and Global links in the admin navigation. Set to `false` to hide the link from the navigation while keeping its routes accessible. |
31+
| **`hidden`** | Set to true or a function, called with the current user, returning true to exclude this Collection from navigation and admin routing. |
32+
| **`hooks`** | Admin-specific hooks for this Collection. [More details](../hooks/collections). |
33+
| **`useAsTitle`** | Specify a top-level field to use for a document title throughout the Admin Panel. If no field is defined, the ID of the document is used as the title. A field with `virtual: true` cannot be used as the title. |
3434
| **`description`** | Text to display below the Collection label in the List View to give editors more information. Alternatively, you can use the `admin.components.Description` to render a React component. [More details](#custom-components). |
35-
| **`defaultColumns`** | Array of field names that correspond to which columns to show by default in this Collection's List View. |
36-
| **`hideAPIURL`** | Hides the "API URL" meta field while editing documents within this Collection. |
37-
| **`enableRichTextLink`** | The [Rich Text](../fields/rich-text) field features a `Link` element which allows for users to automatically reference related documents within their rich text. Set to `true` by default. |
38-
| **`enableRichTextRelationship`** | The [Rich Text](../fields/rich-text) field features a `Relationship` element which allows for users to automatically reference related documents within their rich text. Set to `true` by default. |
39-
| **`meta`** | Page metadata overrides to apply to this Collection within the Admin Panel. [More details](./metadata). |
40-
| **`preview`** | Function to generate preview URLs within the Admin Panel that can point to your app. [More details](#preview). |
41-
| **`livePreview`** | Enable real-time editing for instant visual feedback of your front-end application. [More details](../live-preview/overview). |
35+
| **`defaultColumns`** | Array of field names that correspond to which columns to show by default in this Collection's List View. |
36+
| **`hideAPIURL`** | Hides the "API URL" meta field while editing documents within this Collection. |
37+
| **`enableRichTextLink`** | The [Rich Text](../fields/rich-text) field features a `Link` element which allows for users to automatically reference related documents within their rich text. Set to `true` by default. |
38+
| **`enableRichTextRelationship`** | The [Rich Text](../fields/rich-text) field features a `Relationship` element which allows for users to automatically reference related documents within their rich text. Set to `true` by default. |
39+
| **`meta`** | Page metadata overrides to apply to this Collection within the Admin Panel. [More details](./metadata). |
40+
| **`preview`** | Function to generate preview URLs within the Admin Panel that can point to your app. [More details](#preview). |
41+
| **`livePreview`** | Enable real-time editing for instant visual feedback of your front-end application. [More details](../live-preview/overview). |
4242
| **`components`** | Swap in your own React components to be used within this Collection. [More details](#custom-components). |
43-
| **`listSearchableFields`** | Specify which fields should be searched in the List search view. [More details](#list-searchable-fields). |
44-
| **`pagination`** | Set pagination-specific options for this Collection. [More details](#pagination). |
45-
| **`baseListFilter`** | You can define a default base filter for this collection's List view, which will be merged into any filters that the user performs. |
43+
| **`listSearchableFields`** | Specify which fields should be searched in the List search view. [More details](#list-searchable-fields). |
44+
| **`pagination`** | Set pagination-specific options for this Collection. [More details](#pagination). |
45+
| **`baseListFilter`** | You can define a default base filter for this collection's List view, which will be merged into any filters that the user performs. |
4646

4747
### Custom Components
4848

docs/admin/globals.mdx

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -25,9 +25,9 @@ export const MyGlobal: GlobalConfig = {
2525

2626
The following options are available:
2727

28-
| Option | Description |
29-
| ------------- | --------------------------------------------------------------------------------------------------------------------------------- |
30-
| **`group`** | Text used as a label for grouping Collection and Global links together in the navigation. |
28+
| Option | Description |
29+
| ----------------- | --------------------------------------------------------------------------------------------------------------------------------- |
30+
| **`group`** | Text or localization object used to group Collection and Global links in the admin navigation. Set to `false` to hide the link from the navigation while keeping its routes accessible. |
3131
| **`hidden`** | Set to true or a function, called with the current user, returning true to exclude this Global from navigation and admin routing. |
3232
| **`components`** | Swap in your own React components to be used within this Global. [More details](#custom-components). |
3333
| **`preview`** | Function to generate a preview URL within the Admin Panel for this Global that can point to your app. [More details](#preview). |

packages/payload/src/collections/config/types.ts

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -335,9 +335,12 @@ export type CollectionAdminOptions = {
335335
enableRichTextLink?: boolean
336336
enableRichTextRelationship?: boolean
337337
/**
338-
* Place collections into a navigational group
339-
* */
340-
group?: Record<string, string> | string
338+
* Specify a navigational group for collections in the admin sidebar.
339+
* - Provide a string to place the entity in a custom group.
340+
* - Provide a record to define localized group names.
341+
* - Set to `false` to exclude the entity from the sidebar / dashboard without disabling its routes.
342+
*/
343+
group?: false | Record<string, string> | string
341344
/**
342345
* Exclude the collection from the admin nav and routes
343346
*/

packages/payload/src/globals/config/types.ts

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -117,9 +117,12 @@ export type GlobalAdminOptions = {
117117
*/
118118
description?: EntityDescription
119119
/**
120-
* Place globals into a navigational group
121-
* */
122-
group?: Record<string, string> | string
120+
* Specify a navigational group for globals in the admin sidebar.
121+
* - Provide a string to place the entity in a custom group.
122+
* - Provide a record to define localized group names.
123+
* - Set to `false` to exclude the entity from the sidebar / dashboard without disabling its routes.
124+
*/
125+
group?: false | Record<string, string> | string
123126
/**
124127
* Exclude the global from the admin nav and routes
125128
*/

packages/ui/src/utilities/groupNavItems.ts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,11 @@ export function groupNavItems(
3939
): NavGroupType[] {
4040
const result = entities.reduce(
4141
(groups, entityToGroup) => {
42+
// Skip entities where admin.group is explicitly false
43+
if (entityToGroup.entity?.admin?.group === false) {
44+
return groups
45+
}
46+
4247
if (permissions?.[entityToGroup.type.toLowerCase()]?.[entityToGroup.entity.slug]?.read) {
4348
const translatedGroup = getTranslation(entityToGroup.entity.admin.group, i18n)
4449

test/admin/collections/NotInView.ts

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
import type { CollectionConfig } from 'payload'
2+
3+
import { notInViewCollectionSlug } from '../slugs.js'
4+
5+
export const CollectionNotInView: CollectionConfig = {
6+
slug: notInViewCollectionSlug,
7+
admin: {
8+
group: false,
9+
},
10+
fields: [
11+
{
12+
name: 'title',
13+
type: 'text',
14+
},
15+
],
16+
}

test/admin/config.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@ import { CollectionGroup2A } from './collections/Group2A.js'
1717
import { CollectionGroup2B } from './collections/Group2B.js'
1818
import { CollectionHidden } from './collections/Hidden.js'
1919
import { CollectionNoApiView } from './collections/NoApiView.js'
20+
import { CollectionNotInView } from './collections/NotInView.js'
2021
import { Posts } from './collections/Posts.js'
2122
import { UploadCollection } from './collections/Upload.js'
2223
import { Users } from './collections/Users.js'
@@ -27,6 +28,7 @@ import { GlobalGroup1A } from './globals/Group1A.js'
2728
import { GlobalGroup1B } from './globals/Group1B.js'
2829
import { GlobalHidden } from './globals/Hidden.js'
2930
import { GlobalNoApiView } from './globals/NoApiView.js'
31+
import { GlobalNotInView } from './globals/NotInView.js'
3032
import { Settings } from './globals/Settings.js'
3133
import { seed } from './seed.js'
3234
import {
@@ -143,6 +145,7 @@ export default buildConfigWithDefaults({
143145
Posts,
144146
Users,
145147
CollectionHidden,
148+
CollectionNotInView,
146149
CollectionNoApiView,
147150
CustomViews1,
148151
CustomViews2,
@@ -159,6 +162,7 @@ export default buildConfigWithDefaults({
159162
],
160163
globals: [
161164
GlobalHidden,
165+
GlobalNotInView,
162166
GlobalNoApiView,
163167
Global,
164168
CustomGlobalViews1,

test/admin/e2e/1/e2e.spec.ts

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,7 @@ import {
4040
customGlobalViews2GlobalSlug,
4141
customViews2CollectionSlug,
4242
globalSlug,
43+
notInViewCollectionSlug,
4344
postsCollectionSlug,
4445
settingsGlobalSlug,
4546
} from '../../slugs.js'
@@ -66,6 +67,7 @@ const dirname = path.resolve(currentFolder, '../../')
6667
describe('admin1', () => {
6768
let page: Page
6869
let postsUrl: AdminUrlUtil
70+
let notInViewUrl: AdminUrlUtil
6971
let globalURL: AdminUrlUtil
7072
let customViewsURL: AdminUrlUtil
7173
let customFieldsURL: AdminUrlUtil
@@ -83,6 +85,7 @@ describe('admin1', () => {
8385
prebuild,
8486
}))
8587
postsUrl = new AdminUrlUtil(serverURL, postsCollectionSlug)
88+
notInViewUrl = new AdminUrlUtil(serverURL, notInViewCollectionSlug)
8689
globalURL = new AdminUrlUtil(serverURL, globalSlug)
8790
customViewsURL = new AdminUrlUtil(serverURL, customViews2CollectionSlug)
8891
customFieldsURL = new AdminUrlUtil(serverURL, customFieldsSlug)
@@ -450,6 +453,28 @@ describe('admin1', () => {
450453
await page.goto(postsUrl.global('hidden-global'))
451454
await expect(page.locator('.not-found')).toContainText('Nothing found')
452455
})
456+
457+
test('nav — should not show group: false collections and globals', async () => {
458+
await page.goto(notInViewUrl.admin)
459+
// nav menu
460+
await expect(page.locator('#nav-not-in-view-collection')).toBeHidden()
461+
await expect(page.locator('#nav-global-not-in-view-global')).toBeHidden()
462+
})
463+
464+
test('dashboard — should not show group: false collections and globals', async () => {
465+
await page.goto(notInViewUrl.admin)
466+
// dashboard
467+
await expect(page.locator('#card-not-in-view-collection')).toBeHidden()
468+
await expect(page.locator('#card-not-in-view-global')).toBeHidden()
469+
})
470+
471+
test('routing — should not 404 on group: false collections and globals', async () => {
472+
// routing
473+
await page.goto(notInViewUrl.collection('not-in-view-collection'))
474+
await expect(page.locator('.list-header h1')).toContainText('Not In View Collections')
475+
await page.goto(notInViewUrl.global('not-in-view-global'))
476+
await expect(page.locator('.render-title')).toContainText('Not In View Global')
477+
})
453478
})
454479

455480
describe('custom providers', () => {

test/admin/globals/NotInView.ts

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
import type { GlobalConfig } from 'payload'
2+
3+
import { notInViewGlobalSlug } from '../slugs.js'
4+
5+
export const GlobalNotInView: GlobalConfig = {
6+
slug: notInViewGlobalSlug,
7+
admin: {
8+
group: false,
9+
},
10+
fields: [
11+
{
12+
name: 'title',
13+
type: 'text',
14+
},
15+
],
16+
}

test/admin/slugs.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ export const group1Collection2Slug = 'group-one-collection-twos'
88
export const group2Collection1Slug = 'group-two-collection-ones'
99
export const group2Collection2Slug = 'group-two-collection-twos'
1010
export const hiddenCollectionSlug = 'hidden-collection'
11+
export const notInViewCollectionSlug = 'not-in-view-collection'
1112
export const noApiViewCollectionSlug = 'collection-no-api-view'
1213
export const disableDuplicateSlug = 'disable-duplicate'
1314
export const uploadCollectionSlug = 'uploads'
@@ -35,6 +36,7 @@ export const group1GlobalSlug = 'group-globals-one'
3536
export const group2GlobalSlug = 'group-globals-two'
3637
export const hiddenGlobalSlug = 'hidden-global'
3738

39+
export const notInViewGlobalSlug = 'not-in-view-global'
3840
export const settingsGlobalSlug = 'settings'
3941
export const noApiViewGlobalSlug = 'global-no-api-view'
4042
export const globalSlugs = [

0 commit comments

Comments
 (0)