diff --git a/docs/custom-components/document-views.mdx b/docs/custom-components/document-views.mdx index 1b930acd1..36b63b110 100644 --- a/docs/custom-components/document-views.mdx +++ b/docs/custom-components/document-views.mdx @@ -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 diff --git a/packages/next/src/elements/DocumentHeader/Tabs/tabs/index.tsx b/packages/next/src/elements/DocumentHeader/Tabs/tabs/index.tsx index 9c9638204..6c35864aa 100644 --- a/packages/next/src/elements/DocumentHeader/Tabs/tabs/index.tsx +++ b/packages/next/src/elements/DocumentHeader/Tabs/tabs/index.tsx @@ -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 + }) } diff --git a/packages/payload/src/admin/views/document.ts b/packages/payload/src/admin/views/document.ts index 7e8d14f14..3b83357f4 100644 --- a/packages/payload/src/admin/views/document.ts +++ b/packages/payload/src/admin/views/document.ts @@ -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 } diff --git a/test/admin/collections/ReorderTabs.ts b/test/admin/collections/ReorderTabs.ts new file mode 100644 index 000000000..949d16d4d --- /dev/null +++ b/test/admin/collections/ReorderTabs.ts @@ -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, +} diff --git a/test/admin/config.ts b/test/admin/config.ts index 15702890e..1873d20ee 100644 --- a/test/admin/config.ts +++ b/test/admin/config.ts @@ -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, diff --git a/test/admin/e2e/document-view/e2e.spec.ts b/test/admin/e2e/document-view/e2e.spec.ts index e6c181ab2..f84cf0ea8 100644 --- a/test/admin/e2e/document-view/e2e.spec.ts +++ b/test/admin/e2e/document-view/e2e.spec.ts @@ -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) diff --git a/test/admin/payload-types.ts b/test/admin/payload-types.ts index b42e50ee4..f7e71a197 100644 --- a/test/admin/payload-types.ts +++ b/test/admin/payload-types.ts @@ -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 | CollectionNoApiViewSelect; 'custom-views-one': CustomViewsOneSelect | CustomViewsOneSelect; 'custom-views-two': CustomViewsTwoSelect | CustomViewsTwoSelect; + 'reorder-tabs': ReorderTabsSelect | ReorderTabsSelect; 'custom-fields': CustomFieldsSelect | CustomFieldsSelect; 'group-one-collection-ones': GroupOneCollectionOnesSelect | GroupOneCollectionOnesSelect; 'group-one-collection-twos': GroupOneCollectionTwosSelect | GroupOneCollectionTwosSelect; @@ -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 { updatedAt?: T; createdAt?: T; } +/** + * This interface was referenced by `Config`'s JSON-Schema + * via the `definition` "reorder-tabs_select". + */ +export interface ReorderTabsSelect { + title?: T; + updatedAt?: T; + createdAt?: T; +} /** * This interface was referenced by `Config`'s JSON-Schema * via the `definition` "custom-fields_select". diff --git a/test/admin/slugs.ts b/test/admin/slugs.ts index e987283ff..9b47ca79f 100644 --- a/test/admin/slugs.ts +++ b/test/admin/slugs.ts @@ -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'