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:
@@ -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
|
||||
|
||||
@@ -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
|
||||
})
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
|
||||
41
test/admin/collections/ReorderTabs.ts
Normal file
41
test/admin/collections/ReorderTabs.ts
Normal 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,
|
||||
}
|
||||
@@ -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,
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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".
|
||||
|
||||
@@ -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'
|
||||
|
||||
Reference in New Issue
Block a user