diff --git a/packages/next/src/views/Dashboard/Default/index.tsx b/packages/next/src/views/Dashboard/Default/index.tsx index e8111bdb121..c2118828b39 100644 --- a/packages/next/src/views/Dashboard/Default/index.tsx +++ b/packages/next/src/views/Dashboard/Default/index.tsx @@ -44,7 +44,7 @@ export function DefaultDashboard(props: DashboardViewServerProps) { payload: { config: { admin: { - components: { afterDashboard, beforeDashboard }, + components: { afterDashboard, beforeDashboard, emptyDashboard }, }, routes: { admin: adminRoute }, }, @@ -75,7 +75,23 @@ export function DefaultDashboard(props: DashboardViewServerProps) { {!navGroups || navGroups?.length === 0 ? ( -

no nav groups....

+ emptyDashboard ? ( + RenderServerComponent({ + Component: emptyDashboard, + importMap: payload.importMap, + serverProps: { + i18n, + locale, + params, + payload, + permissions, + searchParams, + user, + } satisfies ServerProps, + }) + ) : ( +

no nav groups....

+ ) ) : ( navGroups.map(({ entities, label }, groupIndex) => { return ( diff --git a/packages/payload/src/bin/generateImportMap/iterateConfig.ts b/packages/payload/src/bin/generateImportMap/iterateConfig.ts index 9c945e21b57..e6e906165dc 100644 --- a/packages/payload/src/bin/generateImportMap/iterateConfig.ts +++ b/packages/payload/src/bin/generateImportMap/iterateConfig.ts @@ -68,6 +68,8 @@ export function iterateConfig({ addToImportMap(config.admin?.components?.beforeLogin) addToImportMap(config.admin?.components?.beforeNavLinks) + addToImportMap(config.admin?.components?.emptyDashboard) + addToImportMap(config.admin?.components?.providers) if (config.admin?.components?.views) { diff --git a/packages/payload/src/config/types.ts b/packages/payload/src/config/types.ts index a76832e8038..65fb8f5d916 100644 --- a/packages/payload/src/config/types.ts +++ b/packages/payload/src/config/types.ts @@ -805,6 +805,10 @@ export type Config = { * Add custom components before the navigation links */ beforeNavLinks?: CustomComponent[] + /** + * Add custom components when dashboard has any nav groups to show + */ + emptyDashboard?: CustomComponent[] /** Replace graphical components */ graphics?: { /** Replace the icon in the navigation */ diff --git a/test/empty-dashboard/components/EmptyDashboard/index.tsx b/test/empty-dashboard/components/EmptyDashboard/index.tsx new file mode 100644 index 00000000000..323572c701f --- /dev/null +++ b/test/empty-dashboard/components/EmptyDashboard/index.tsx @@ -0,0 +1,16 @@ +import type { PayloadServerReactComponent, SanitizedConfig } from 'payload' + +import React from 'react' + +const baseClass = 'empty-dashboard' + +export const EmptyDashboard: PayloadServerReactComponent< + SanitizedConfig['admin']['components']['emptyDashboard'][0] +> = () => { + return ( +
+

Empty Dashboard

+

This is a custom empty dashboard component.

+
+ ) +} diff --git a/test/empty-dashboard/config.ts b/test/empty-dashboard/config.ts new file mode 100644 index 00000000000..1a2a8834e0c --- /dev/null +++ b/test/empty-dashboard/config.ts @@ -0,0 +1,45 @@ +import { fileURLToPath } from 'node:url' +import path from 'path' + +import { buildConfigWithDefaults } from '../buildConfigWithDefaults.js' +import { devUser } from '../credentials.js' + +const filename = fileURLToPath(import.meta.url) +const dirname = path.dirname(filename) + +export default buildConfigWithDefaults({ + admin: { + importMap: { + baseDir: path.resolve(dirname), + }, + components: { + emptyDashboard: ['/components/EmptyDashboard/index.js#EmptyDashboard'], + }, + autoLogin: { + email: devUser.email, + password: devUser.password, + }, + }, + collections: [ + { + slug: 'users', + auth: true, + fields: [], + admin: { + group: false, + }, + }, + ], + onInit: async (payload) => { + await payload.create({ + collection: 'users', + data: { + email: devUser.email, + password: devUser.password, + }, + }) + }, + typescript: { + outputFile: path.resolve(dirname, 'payload-types.ts'), + }, +}) diff --git a/test/empty-dashboard/e2e.spec.ts b/test/empty-dashboard/e2e.spec.ts new file mode 100644 index 00000000000..b0b1410df6a --- /dev/null +++ b/test/empty-dashboard/e2e.spec.ts @@ -0,0 +1,49 @@ +import type { Page } from '@playwright/test' + +import { expect, test } from '@playwright/test' +import path from 'path' +import { fileURLToPath } from 'url' + +import { ensureCompilationIsDone, initPageConsoleErrorCatch } from '../helpers.js' +import { initPayloadE2ENoConfig } from '../helpers/initPayloadE2ENoConfig.js' +import { TEST_TIMEOUT_LONG } from '../playwright.config.js' + +const { beforeAll, describe } = test +const filename = fileURLToPath(import.meta.url) +const dirname = path.dirname(filename) + +describe('Empty Dashboard', () => { + let serverURL: string + let page: Page + + beforeAll(async ({ browser }, testInfo) => { + testInfo.setTimeout(TEST_TIMEOUT_LONG) + + // Initialize Payload + ;({ serverURL } = await initPayloadE2ENoConfig({ dirname })) + + // Set up browser context + const context = await browser.newContext() + page = await context.newPage() + initPageConsoleErrorCatch(page) + + // Wait for compilation to complete + await ensureCompilationIsDone({ page, serverURL }) + }) + + test('should display empty dashboard component', async () => { + // Navigate to admin dashboard + await page.goto(`${serverURL}/admin`) + + // Wait for the empty dashboard to be visible + const emptyDashboard = page.locator('.empty-dashboard') + await expect(emptyDashboard).toBeVisible() + + // Verify the content + const title = emptyDashboard.locator('h4') + await expect(title).toHaveText('Empty Dashboard') + + const description = emptyDashboard.locator('p') + await expect(description).toContainText('This is a custom empty dashboard component.') + }) +}) diff --git a/test/empty-dashboard/payload-types.ts b/test/empty-dashboard/payload-types.ts new file mode 100644 index 00000000000..91197ba3cee --- /dev/null +++ b/test/empty-dashboard/payload-types.ts @@ -0,0 +1,256 @@ +/* tslint:disable */ +/* eslint-disable */ +/** + * This file was automatically generated by Payload. + * DO NOT MODIFY IT BY HAND. Instead, modify your source Payload config, + * and re-run `payload generate:types` to regenerate this file. + */ + +/** + * Supported timezones in IANA format. + * + * This interface was referenced by `Config`'s JSON-Schema + * via the `definition` "supportedTimezones". + */ +export type SupportedTimezones = + | 'Pacific/Midway' + | 'Pacific/Niue' + | 'Pacific/Honolulu' + | 'Pacific/Rarotonga' + | 'America/Anchorage' + | 'Pacific/Gambier' + | 'America/Los_Angeles' + | 'America/Tijuana' + | 'America/Denver' + | 'America/Phoenix' + | 'America/Chicago' + | 'America/Guatemala' + | 'America/New_York' + | 'America/Bogota' + | 'America/Caracas' + | 'America/Santiago' + | 'America/Buenos_Aires' + | 'America/Sao_Paulo' + | 'Atlantic/South_Georgia' + | 'Atlantic/Azores' + | 'Atlantic/Cape_Verde' + | 'Europe/London' + | 'Europe/Berlin' + | 'Africa/Lagos' + | 'Europe/Athens' + | 'Africa/Cairo' + | 'Europe/Moscow' + | 'Asia/Riyadh' + | 'Asia/Dubai' + | 'Asia/Baku' + | 'Asia/Karachi' + | 'Asia/Tashkent' + | 'Asia/Calcutta' + | 'Asia/Dhaka' + | 'Asia/Almaty' + | 'Asia/Jakarta' + | 'Asia/Bangkok' + | 'Asia/Shanghai' + | 'Asia/Singapore' + | 'Asia/Tokyo' + | 'Asia/Seoul' + | 'Australia/Brisbane' + | 'Australia/Sydney' + | 'Pacific/Guam' + | 'Pacific/Noumea' + | 'Pacific/Auckland' + | 'Pacific/Fiji'; + +export interface Config { + auth: { + users: UserAuthOperations; + }; + blocks: {}; + collections: { + users: User; + 'payload-locked-documents': PayloadLockedDocument; + 'payload-preferences': PayloadPreference; + 'payload-migrations': PayloadMigration; + }; + collectionsJoins: {}; + collectionsSelect: { + users: UsersSelect | UsersSelect; + 'payload-locked-documents': PayloadLockedDocumentsSelect | PayloadLockedDocumentsSelect; + 'payload-preferences': PayloadPreferencesSelect | PayloadPreferencesSelect; + 'payload-migrations': PayloadMigrationsSelect | PayloadMigrationsSelect; + }; + db: { + defaultIDType: string; + }; + globals: {}; + globalsSelect: {}; + locale: null; + user: User & { + collection: 'users'; + }; + jobs: { + tasks: unknown; + workflows: unknown; + }; +} +export interface UserAuthOperations { + forgotPassword: { + email: string; + password: string; + }; + login: { + email: string; + password: string; + }; + registerFirstUser: { + email: string; + password: string; + }; + unlock: { + email: string; + password: string; + }; +} +/** + * This interface was referenced by `Config`'s JSON-Schema + * via the `definition` "users". + */ +export interface User { + id: string; + updatedAt: string; + createdAt: string; + email: string; + resetPasswordToken?: string | null; + resetPasswordExpiration?: string | null; + salt?: string | null; + hash?: string | null; + loginAttempts?: number | null; + lockUntil?: string | null; + sessions?: + | { + id: string; + createdAt?: string | null; + expiresAt: string; + }[] + | null; + password?: string | null; +} +/** + * This interface was referenced by `Config`'s JSON-Schema + * via the `definition` "payload-locked-documents". + */ +export interface PayloadLockedDocument { + id: string; + document?: { + relationTo: 'users'; + value: string | User; + } | null; + globalSlug?: string | null; + user: { + relationTo: 'users'; + value: string | User; + }; + updatedAt: string; + createdAt: string; +} +/** + * This interface was referenced by `Config`'s JSON-Schema + * via the `definition` "payload-preferences". + */ +export interface PayloadPreference { + id: string; + user: { + relationTo: 'users'; + value: string | User; + }; + key?: string | null; + value?: + | { + [k: string]: unknown; + } + | unknown[] + | string + | number + | boolean + | null; + updatedAt: string; + createdAt: string; +} +/** + * This interface was referenced by `Config`'s JSON-Schema + * via the `definition` "payload-migrations". + */ +export interface PayloadMigration { + id: string; + name?: string | null; + batch?: number | null; + updatedAt: string; + createdAt: string; +} +/** + * This interface was referenced by `Config`'s JSON-Schema + * via the `definition` "users_select". + */ +export interface UsersSelect { + updatedAt?: T; + createdAt?: T; + email?: T; + resetPasswordToken?: T; + resetPasswordExpiration?: T; + salt?: T; + hash?: T; + loginAttempts?: T; + lockUntil?: T; + sessions?: + | T + | { + id?: T; + createdAt?: T; + expiresAt?: T; + }; +} +/** + * This interface was referenced by `Config`'s JSON-Schema + * via the `definition` "payload-locked-documents_select". + */ +export interface PayloadLockedDocumentsSelect { + document?: T; + globalSlug?: T; + user?: T; + updatedAt?: T; + createdAt?: T; +} +/** + * This interface was referenced by `Config`'s JSON-Schema + * via the `definition` "payload-preferences_select". + */ +export interface PayloadPreferencesSelect { + user?: T; + key?: T; + value?: T; + updatedAt?: T; + createdAt?: T; +} +/** + * This interface was referenced by `Config`'s JSON-Schema + * via the `definition` "payload-migrations_select". + */ +export interface PayloadMigrationsSelect { + name?: T; + batch?: T; + updatedAt?: T; + createdAt?: T; +} +/** + * This interface was referenced by `Config`'s JSON-Schema + * via the `definition` "auth". + */ +export interface Auth { + [k: string]: unknown; +} + + +declare module 'payload' { + // @ts-ignore + export interface GeneratedTypes extends Config {} +} \ No newline at end of file