feat(next): reorder document view tabs (#12288)

Introduces the ability to customize the order of both default and custom
tabs. This way you can make custom tabs appear before default ones, or
change the order of tabs as you see fit.

To do this, use the new `tab.order` property in your edit view's config:

```ts
import type { CollectionConfig } from 'payload'

export const MyCollectionConfig: CollectionConfig = {
  // ...
  admin: {
    components: {
      views: {
        edit: {
          myCustomView: {
            path: '/my-custom-view',
            Component: '/path/to/component',
            tab: {
              href: '/my-custom-view',
              order: 100, // This will put this tab in the first position
            },
          }
        }
      }
    }
  }
}
```

---------

Co-authored-by: Jacob Fletcher <jacobsfletch@gmail.com>
This commit is contained in:
Jessica Rynkar
2025-06-13 15:28:29 +01:00
committed by GitHub
parent 245a2dee7e
commit 65309b1d21
8 changed files with 139 additions and 18 deletions

View File

@@ -123,6 +123,7 @@ export const MyCollection: CollectionConfig = {
tab: {
label: 'Another Custom View',
href: '/another-custom-view',
order: '100',
},
// highlight-end
},
@@ -143,6 +144,7 @@ The following options are available for tabs:
| ----------- | ------------------------------------------------------------------------------------------------------------- |
| `label` | The label to display in the tab. |
| `href` | The URL to navigate to when the tab is clicked. This is optional and defaults to the tab's `path`. |
| `order` | The order in which the tab appears in the navigation. Can be set on default and custom tabs. |
| `Component` | The component to render in the tab. This can be a Server or Client component. [More details](#tab-components) |
### Tab Components

View File

@@ -23,6 +23,7 @@ export const getTabs = ({
tab: {
href: '',
label: ({ t }) => t('general:edit'),
order: 100,
...(customViews?.['default']?.tab || {}),
},
viewPath: '/',
@@ -48,6 +49,7 @@ export const getTabs = ({
},
href: '/preview',
label: ({ t }) => t('general:livePreview'),
order: 200,
...(customViews?.['livePreview']?.tab || {}),
},
viewPath: '/preview',
@@ -62,6 +64,7 @@ export const getTabs = ({
),
href: '/versions',
label: ({ t }) => t('version:versions'),
order: 300,
Pill_Component: VersionsPill,
...(customViews?.['versions']?.tab || {}),
},
@@ -74,24 +77,37 @@ export const getTabs = ({
(globalConfig && !globalConfig?.admin?.hideAPIURL),
href: '/api',
label: 'API',
order: 400,
...(customViews?.['api']?.tab || {}),
},
viewPath: '/api',
},
].concat(
Object.entries(customViews).reduce((acc, [key, value]) => {
if (documentViewKeys.includes(key)) {
]
.concat(
Object.entries(customViews).reduce((acc, [key, value]) => {
if (documentViewKeys.includes(key)) {
return acc
}
if (value?.tab) {
acc.push({
tab: value.tab,
viewPath: 'path' in value ? value.path : '',
})
}
return acc
}, []),
)
?.sort(({ tab: a }, { tab: b }) => {
if (a.order === undefined && b.order === undefined) {
return 0
} else if (a.order === undefined) {
return 1
} else if (b.order === undefined) {
return -1
}
if (value?.tab) {
acc.push({
tab: value.tab,
viewPath: 'path' in value ? value.path : '',
})
}
return acc
}, []),
)
return a.order - b.order
})
}

View File

@@ -66,6 +66,11 @@ export type DocumentTabConfig = {
readonly isActive?: ((args: { href: string }) => boolean) | boolean
readonly label?: ((args: { t: (key: string) => string }) => string) | string
readonly newTab?: boolean
/**
* Sets the order to render the tab in the admin panel
* Recommended to use increments of 100 (e.g. 0, 100, 200)
*/
readonly order?: number
readonly Pill?: PayloadComponent
}

View File

@@ -0,0 +1,41 @@
import type { CollectionConfig } from 'payload'
import { reorderTabsSlug } from '../slugs.js'
export const ReorderTabs: CollectionConfig = {
slug: reorderTabsSlug,
admin: {
components: {
views: {
edit: {
default: {
tab: {
order: 100,
},
},
livePreview: {
tab: {
order: 0,
},
},
test: {
path: '/test',
Component: '/components/views/CustomEdit/index.js#CustomEditView',
tab: {
label: 'Test',
href: '/test',
order: 50,
},
},
},
},
},
},
fields: [
{
name: 'title',
type: 'text',
},
],
versions: true,
}

View File

@@ -1,3 +1,4 @@
/* eslint-disable no-restricted-exports */
import { fileURLToPath } from 'node:url'
import path from 'path'
@@ -21,6 +22,7 @@ import { CollectionNoApiView } from './collections/NoApiView.js'
import { CollectionNotInView } from './collections/NotInView.js'
import { Placeholder } from './collections/Placeholder.js'
import { Posts } from './collections/Posts.js'
import { ReorderTabs } from './collections/ReorderTabs.js'
import { UploadCollection } from './collections/Upload.js'
import { UploadTwoCollection } from './collections/UploadTwo.js'
import { UseAsTitleGroupField } from './collections/UseAsTitleGroupField.js'
@@ -45,18 +47,19 @@ import {
protectedCustomNestedViewPath,
publicCustomViewPath,
} from './shared.js'
import { editMenuItemsSlug } from './slugs.js'
import { editMenuItemsSlug, reorderTabsSlug } from './slugs.js'
const filename = fileURLToPath(import.meta.url)
const dirname = path.dirname(filename)
export default buildConfigWithDefaults({
admin: {
livePreview: {
collections: [reorderTabsSlug, editMenuItemsSlug],
url: 'http://localhost:3000',
},
importMap: {
baseDir: path.resolve(dirname),
},
livePreview: {
url: 'http://localhost:3000/',
collections: [editMenuItemsSlug],
},
components: {
actions: ['/components/actions/AdminButton/index.js#AdminButton'],
afterDashboard: [
@@ -161,6 +164,7 @@ export default buildConfigWithDefaults({
CollectionNoApiView,
CustomViews1,
CustomViews2,
ReorderTabs,
CustomFields,
CollectionGroup1A,
CollectionGroup1B,

View File

@@ -37,7 +37,9 @@ import {
group1GlobalSlug,
noApiViewCollectionSlug,
noApiViewGlobalSlug,
placeholderCollectionSlug,
postsCollectionSlug,
reorderTabsSlug,
} from '../../slugs.js'
const { beforeAll, beforeEach, describe } = test
@@ -67,7 +69,10 @@ describe('Document View', () => {
let serverURL: string
let customViewsURL: AdminUrlUtil
let customFieldsURL: AdminUrlUtil
let placeholderURL: AdminUrlUtil
let collectionCustomViewPathId: string
let editMenuItemsURL: AdminUrlUtil
let reorderTabsURL: AdminUrlUtil
beforeAll(async ({ browser }, testInfo) => {
const prebuild = false // Boolean(process.env.CI)
@@ -83,7 +88,9 @@ describe('Document View', () => {
globalURL = new AdminUrlUtil(serverURL, globalSlug)
customViewsURL = new AdminUrlUtil(serverURL, customViews2CollectionSlug)
customFieldsURL = new AdminUrlUtil(serverURL, customFieldsSlug)
placeholderURL = new AdminUrlUtil(serverURL, placeholderCollectionSlug)
editMenuItemsURL = new AdminUrlUtil(serverURL, editMenuItemsSlug)
reorderTabsURL = new AdminUrlUtil(serverURL, reorderTabsSlug)
const context = await browser.newContext()
page = await context.newPage()
@@ -646,6 +653,26 @@ describe('Document View', () => {
})
})
describe('reordering tabs', () => {
beforeEach(async () => {
await page.goto(reorderTabsURL.create)
await page.locator('#field-title').fill('Reorder Tabs')
await saveDocAndAssert(page)
})
test('collection — should show live preview as first tab', async () => {
const tabs = page.locator('.doc-tabs__tabs-container .doc-tab')
const firstTab = tabs.first()
await expect(firstTab).toContainText('Live Preview')
})
test('collection — should show edit as third tab', async () => {
const tabs = page.locator('.doc-tabs__tabs-container .doc-tab')
const secondTab = tabs.nth(2)
await expect(secondTab).toContainText('Edit')
})
})
describe('custom editMenuItem components', () => {
test('should render custom editMenuItems component', async () => {
await page.goto(editMenuItemsURL.create)

View File

@@ -76,6 +76,7 @@ export interface Config {
'collection-no-api-view': CollectionNoApiView;
'custom-views-one': CustomViewsOne;
'custom-views-two': CustomViewsTwo;
'reorder-tabs': ReorderTab;
'custom-fields': CustomField;
'group-one-collection-ones': GroupOneCollectionOne;
'group-one-collection-twos': GroupOneCollectionTwo;
@@ -106,6 +107,7 @@ export interface Config {
'collection-no-api-view': CollectionNoApiViewSelect<false> | CollectionNoApiViewSelect<true>;
'custom-views-one': CustomViewsOneSelect<false> | CustomViewsOneSelect<true>;
'custom-views-two': CustomViewsTwoSelect<false> | CustomViewsTwoSelect<true>;
'reorder-tabs': ReorderTabsSelect<false> | ReorderTabsSelect<true>;
'custom-fields': CustomFieldsSelect<false> | CustomFieldsSelect<true>;
'group-one-collection-ones': GroupOneCollectionOnesSelect<false> | GroupOneCollectionOnesSelect<true>;
'group-one-collection-twos': GroupOneCollectionTwosSelect<false> | GroupOneCollectionTwosSelect<true>;
@@ -340,6 +342,16 @@ export interface CustomViewsTwo {
updatedAt: string;
createdAt: string;
}
/**
* This interface was referenced by `Config`'s JSON-Schema
* via the `definition` "reorder-tabs".
*/
export interface ReorderTab {
id: string;
title?: string | null;
updatedAt: string;
createdAt: string;
}
/**
* This interface was referenced by `Config`'s JSON-Schema
* via the `definition` "custom-fields".
@@ -578,6 +590,10 @@ export interface PayloadLockedDocument {
relationTo: 'custom-views-two';
value: string | CustomViewsTwo;
} | null)
| ({
relationTo: 'reorder-tabs';
value: string | ReorderTab;
} | null)
| ({
relationTo: 'custom-fields';
value: string | CustomField;
@@ -834,6 +850,15 @@ export interface CustomViewsTwoSelect<T extends boolean = true> {
updatedAt?: T;
createdAt?: T;
}
/**
* This interface was referenced by `Config`'s JSON-Schema
* via the `definition` "reorder-tabs_select".
*/
export interface ReorderTabsSelect<T extends boolean = true> {
title?: T;
updatedAt?: T;
createdAt?: T;
}
/**
* This interface was referenced by `Config`'s JSON-Schema
* via the `definition` "custom-fields_select".

View File

@@ -1,6 +1,7 @@
export const usersCollectionSlug = 'users'
export const customViews1CollectionSlug = 'custom-views-one'
export const customViews2CollectionSlug = 'custom-views-two'
export const reorderTabsSlug = 'reorder-tabs'
export const geoCollectionSlug = 'geo'
export const arrayCollectionSlug = 'array'
export const postsCollectionSlug = 'posts'