feat: view component types (#11126)

It is currently very difficult to build custom edit and list views or
inject custom components into these views because these views and
components are not explicitly typed. Instances of these components were
not fully type safe as well, i.e. when rendering them via
`RenderServerComponent`, there was little to no type-checking in most
cases.

There is now a 1:1 type match for all views and view components and they
now receive type-checking at render time.

The following types have been newly added and/or improved:

List View:

  - `ListViewClientProps`
  - `ListViewServerProps`
  - `BeforeListClientProps`
  - `BeforeListServerProps`
  - `BeforeListTableClientProps`
  - `BeforeListTableServerProps`
  - `AfterListClientProps`
  - `AfterListServerProps`
  - `AfterListTableClientProps`
  - `AfterListTableServerProps`
  - `ListViewSlotSharedClientProps`

Document View:

  - `DocumentViewClientProps`
  - `DocumentViewServerProps`
  - `SaveButtonClientProps`
  - `SaveButtonServerProps`
  - `SaveDraftButtonClientProps`
  - `SaveDraftButtonServerProps`
  - `PublishButtonClientProps`
  - `PublishButtonServerProps`
  - `PreviewButtonClientProps`
  - `PreviewButtonServerProps`

Root View:

  - `AdminViewClientProps`
  - `AdminViewServerProps`

General:

  - `ViewDescriptionClientProps`
  - `ViewDescriptionServerProps`

A few other changes were made in a non-breaking way:

  - `Column` is now exported from `payload`
  - `ListPreferences` is now exported from `payload`
  - `ListViewSlots` is now exported from `payload`
  - `ListViewClientProps` is now exported from `payload`
- `AdminViewProps` is now an alias of `AdminViewServerProps` (listed
above)
- `ClientSideEditViewProps` is now an alias of `DocumentViewClientProps`
(listed above)
- `ServerSideEditViewProps` is now an alias of `DocumentViewServerProps`
(listed above)
- `ListComponentClientProps` is now an alias of `ListViewClientProps`
(listed above)
- `ListComponentServerProps` is now an alias of `ListViewServerProps`
(listed above)
- `CustomSaveButton` is now marked as deprecated because this is only
relevant to the config (see correct type above)
- `CustomSaveDraftButton` is now marked as deprecated because this is
only relevant to the config (see correct type above)
- `CustomPublishButton` is now marked as deprecated because this is only
relevant to the config (see correct type above)
- `CustomPreviewButton` is now marked as deprecated because this is only
relevant to the config (see correct type above)
 
This PR _does not_ apply these changes to _root_ components, i.e.
`afterNavLinks`. Those will come in a future PR.

Related: #10987.
This commit is contained in:
Jacob Fletcher
2025-02-17 14:08:23 -05:00
committed by GitHub
parent 938472bf1f
commit b80010b1a1
86 changed files with 683 additions and 495 deletions

View File

@@ -9,14 +9,14 @@ export const Geo: CollectionConfig = {
views: {
edit: {
api: {
actions: ['/components/CollectionAPIButton/index.js#CollectionAPIButton'],
actions: ['/components/actions/CollectionAPIButton/index.js#CollectionAPIButton'],
},
default: {
actions: ['/components/CollectionEditButton/index.js#CollectionEditButton'],
actions: ['/components/actions/CollectionEditButton/index.js#CollectionEditButton'],
},
},
list: {
actions: ['/components/CollectionListButton/index.js#CollectionListButton'],
actions: ['/components/actions/CollectionListButton/index.js#CollectionListButton'],
},
},
},

View File

@@ -1,5 +1,7 @@
'use client'
import type { DocumentTabClientProps } from 'payload'
import { useConfig } from '@payloadcms/ui'
import LinkImport from 'next/link.js'
import { useParams } from 'next/navigation.js'
@@ -7,9 +9,7 @@ import React from 'react'
const Link = (LinkImport.default || LinkImport) as unknown as typeof LinkImport.default
export const CustomTabComponentClient: React.FC<{
readonly path: string
}> = ({ path }) => {
export function CustomTabComponentClient({ path }: DocumentTabClientProps) {
const {
config: {
routes: { admin: adminRoute },
@@ -18,7 +18,7 @@ export const CustomTabComponentClient: React.FC<{
const params = useParams()
const baseRoute = (params.segments.slice(0, 3) as string[]).join('/')
const baseRoute = (params.segments?.slice(0, 3) as string[]).join('/')
return <Link href={`${adminRoute}/${baseRoute}${path}`}>Custom Tab Component</Link>
}

View File

@@ -1,11 +1,11 @@
import type { DocumentTabComponent } from 'payload'
import type { DocumentTabServerProps } from 'payload'
import React from 'react'
import { CustomTabComponentClient } from './client.js'
import './index.scss'
export const CustomTabComponent: DocumentTabComponent = (props) => {
export function CustomTabComponent(props: DocumentTabServerProps) {
const { path } = props
return (

View File

@@ -1,15 +1,15 @@
'use client'
import type { StaticDescription } from 'payload'
import type { ViewDescriptionClientProps } from 'payload'
import { ViewDescription as DefaultViewDescription } from '@payloadcms/ui'
import React from 'react'
import { Banner } from '../Banner/index.js'
export const ViewDescription: React.FC<{ description: StaticDescription }> = ({
export function ViewDescription({
description = 'This is a custom view description component.',
}) => {
}: ViewDescriptionClientProps) {
return (
<Banner>
<DefaultViewDescription description={description} />

View File

@@ -1,8 +1,8 @@
import type { AdminViewComponent, PayloadServerReactComponent } from 'payload'
import type { AdminViewServerProps } from 'payload'
import React, { Fragment } from 'react'
export const CustomAccountView: PayloadServerReactComponent<AdminViewComponent> = () => {
export function CustomAccountView(props: AdminViewServerProps) {
return (
<Fragment>
<div

View File

@@ -1,8 +1,8 @@
import type { AdminViewComponent, PayloadServerReactComponent } from 'payload'
import type { AdminViewServerProps } from 'payload'
import React, { Fragment } from 'react'
export const CustomDashboardView: PayloadServerReactComponent<AdminViewComponent> = () => {
export function CustomDashboardView(props: AdminViewServerProps) {
return (
<Fragment>
<div

View File

@@ -5,19 +5,16 @@ import React from 'react'
const Link = (LinkImport.default || LinkImport) as unknown as typeof LinkImport.default
import type { AdminViewProps } from 'payload'
import type { AdminViewServerProps } from 'payload'
import { Button, SetStepNav } from '@payloadcms/ui'
import { customViewPath } from '../../../shared.js'
import './index.scss'
const baseClass = 'custom-default-view'
export const CustomDefaultView: React.FC<AdminViewProps> = ({
initPageResult,
params,
searchParams,
}) => {
export function CustomDefaultView({ initPageResult, params, searchParams }: AdminViewServerProps) {
const {
permissions,
req: {

View File

@@ -1,12 +1,10 @@
import type { EditViewComponent, PayloadServerReactComponent } from 'payload'
import type { DocumentViewServerProps } from 'payload'
import { SetStepNav } from '@payloadcms/ui'
import { notFound, redirect } from 'next/navigation.js'
import React, { Fragment } from 'react'
export const CustomEditView: PayloadServerReactComponent<EditViewComponent> = ({
initPageResult,
}) => {
export function CustomEditView({ initPageResult }: DocumentViewServerProps) {
if (!initPageResult) {
notFound()
}

View File

@@ -1,12 +1,10 @@
import type { EditViewComponent, PayloadServerReactComponent } from 'payload'
import type { DocumentViewServerProps } from 'payload'
import { SetStepNav } from '@payloadcms/ui'
import { notFound, redirect } from 'next/navigation.js'
import React, { Fragment } from 'react'
export const CustomDefaultEditView: PayloadServerReactComponent<EditViewComponent> = ({
initPageResult,
}) => {
export function CustomDefaultEditView({ initPageResult }: DocumentViewServerProps) {
if (!initPageResult) {
notFound()
}

View File

@@ -10,7 +10,7 @@ const Link = (LinkImport.default || LinkImport) as unknown as typeof LinkImport.
// import { Button } from 'payload/components/elements';
// import { useConfig } from 'payload/components/utilities';
import type { AdminViewProps } from 'payload'
import type { AdminViewServerProps } from 'payload'
import { MinimalTemplate } from '@payloadcms/next/templates'
import { Button } from '@payloadcms/ui'
@@ -20,7 +20,7 @@ import './index.scss'
const baseClass = 'custom-minimal-view'
export const CustomMinimalView: React.FC<AdminViewProps> = ({ initPageResult }) => {
export function CustomMinimalView({ initPageResult }: AdminViewServerProps) {
const {
req: {
payload: {

View File

@@ -1,4 +1,4 @@
import type { AdminViewProps } from 'payload'
import type { AdminViewServerProps } from 'payload'
import { Button } from '@payloadcms/ui'
import LinkImport from 'next/link.js'
@@ -10,7 +10,7 @@ import { settingsGlobalSlug } from '../../../slugs.js'
const Link = (LinkImport.default || LinkImport) as unknown as typeof LinkImport.default
export const CustomProtectedView: React.FC<AdminViewProps> = async ({ initPageResult }) => {
export async function CustomProtectedView({ initPageResult }: AdminViewServerProps) {
const {
req: {
payload: {

View File

@@ -1,4 +1,4 @@
import type { ServerSideEditViewProps } from 'payload'
import type { DocumentViewServerProps } from 'payload'
import { SetStepNav } from '@payloadcms/ui'
import { notFound } from 'next/navigation.js'
@@ -6,7 +6,7 @@ import React, { Fragment } from 'react'
import { customTabViewComponentTitle } from '../../../shared.js'
export const CustomTabComponentView: React.FC<ServerSideEditViewProps> = ({ initPageResult }) => {
export function CustomTabComponentView({ initPageResult }: DocumentViewServerProps) {
if (!initPageResult) {
notFound()
}

View File

@@ -1,4 +1,4 @@
import type { EditViewComponent, PayloadServerReactComponent } from 'payload'
import type { DocumentViewServerProps } from 'payload'
import { SetStepNav } from '@payloadcms/ui'
import { notFound } from 'next/navigation.js'
@@ -6,9 +6,7 @@ import React, { Fragment } from 'react'
import { customTabLabelViewTitle } from '../../../shared.js'
export const CustomTabLabelView: PayloadServerReactComponent<EditViewComponent> = ({
initPageResult,
}) => {
export function CustomTabLabelView({ initPageResult }: DocumentViewServerProps) {
if (!initPageResult) {
notFound()
}

View File

@@ -1,4 +1,4 @@
import type { EditViewComponent, PayloadServerReactComponent } from 'payload'
import type { DocumentViewServerProps } from 'payload'
import { SetStepNav } from '@payloadcms/ui'
import { notFound } from 'next/navigation.js'
@@ -6,9 +6,7 @@ import React, { Fragment } from 'react'
import { customNestedTabViewTitle } from '../../../shared.js'
export const CustomNestedTabView: PayloadServerReactComponent<EditViewComponent> = ({
initPageResult,
}) => {
export function CustomNestedTabView({ initPageResult }: DocumentViewServerProps) {
if (!initPageResult) {
notFound()
}

View File

@@ -1,10 +1,10 @@
import type { AdminViewProps } from 'payload'
import type { DocumentViewServerProps } from 'payload'
import React from 'react'
import { customParamViewTitle } from '../../../shared.js'
export const CustomTabWithParamView: React.FC<AdminViewProps> = ({ params }) => {
export function CustomTabWithParamView({ params }: DocumentViewServerProps) {
const paramValue = params?.segments?.[4]
return (

View File

@@ -1,12 +1,10 @@
import type { EditViewComponent, PayloadServerReactComponent } from 'payload'
import type { DocumentViewServerProps } from 'payload'
import { SetStepNav } from '@payloadcms/ui'
import { notFound, redirect } from 'next/navigation.js'
import React, { Fragment } from 'react'
export const CustomVersionsView: PayloadServerReactComponent<EditViewComponent> = ({
initPageResult,
}) => {
export function CustomVersionsView({ initPageResult }: DocumentViewServerProps) {
if (!initPageResult) {
notFound()
}

View File

@@ -1,4 +1,4 @@
import type { AdminViewProps } from 'payload'
import type { AdminViewServerProps } from 'payload'
import LinkImport from 'next/link.js'
import React from 'react'
@@ -10,7 +10,7 @@ import { Button } from '@payloadcms/ui'
import { customNestedViewPath, customViewTitle } from '../../../shared.js'
import { ClientForm } from './index.client.js'
export const CustomView: React.FC<AdminViewProps> = ({ initPageResult }) => {
export function CustomView({ initPageResult }: AdminViewServerProps) {
const {
req: {
payload: {

View File

@@ -1,4 +1,4 @@
import type { AdminViewProps } from 'payload'
import type { AdminViewServerProps } from 'payload'
import { Button } from '@payloadcms/ui'
import LinkImport from 'next/link.js'
@@ -8,7 +8,7 @@ import { customNestedViewTitle, customViewPath } from '../../../shared.js'
const Link = (LinkImport.default || LinkImport) as unknown as typeof LinkImport.default
export const CustomNestedView: React.FC<AdminViewProps> = ({ initPageResult }) => {
export function CustomNestedView({ initPageResult }: AdminViewServerProps) {
const {
req: {
payload: {

View File

@@ -4,7 +4,7 @@ import React from 'react'
const Link = (LinkImport.default || LinkImport) as unknown as typeof LinkImport.default
import type { AdminViewProps } from 'payload'
import type { AdminViewServerProps } from 'payload'
import {
customParamViewPath,
@@ -12,7 +12,7 @@ import {
customParamViewTitle,
} from '../../../shared.js'
export const CustomViewWithParam: React.FC<AdminViewProps> = ({ initPageResult, params }) => {
export function CustomViewWithParam({ initPageResult, params }: AdminViewServerProps) {
const {
req: {
payload: {

View File

@@ -16,7 +16,6 @@ import { CollectionGroup2B } from './collections/Group2B.js'
import { CollectionHidden } from './collections/Hidden.js'
import { CollectionNoApiView } from './collections/NoApiView.js'
import { CollectionNotInView } from './collections/NotInView.js'
import { Orders } from './collections/Orders.js'
import { Posts } from './collections/Posts.js'
import { UploadCollection } from './collections/Upload.js'
import { Users } from './collections/Users.js'
@@ -47,7 +46,7 @@ export default buildConfigWithDefaults({
baseDir: path.resolve(dirname),
},
components: {
actions: ['/components/AdminButton/index.js#AdminButton'],
actions: ['/components/actions/AdminButton/index.js#AdminButton'],
afterDashboard: [
'/components/AfterDashboard/index.js#AfterDashboard',
'/components/AfterDashboardClient/index.js#AfterDashboardClient',

View File

@@ -9,10 +9,10 @@ export const Global: GlobalConfig = {
views: {
edit: {
api: {
actions: ['/components/GlobalAPIButton/index.js#GlobalAPIButton'],
actions: ['/components/actions/GlobalAPIButton/index.js#GlobalAPIButton'],
},
default: {
actions: ['/components/GlobalEditButton/index.js#GlobalEditButton'],
actions: ['/components/actions/GlobalEditButton/index.js#GlobalEditButton'],
},
},
},

View File

@@ -6,65 +6,10 @@
* 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/Sydney'
| 'Pacific/Guam'
| 'Pacific/Noumea'
| 'Pacific/Auckland'
| 'Pacific/Fiji';
export interface Config {
auth: {
users: UserAuthOperations;
};
blocks: {};
collections: {
uploads: Upload;
posts: Post;
@@ -332,6 +277,9 @@ export interface CustomField {
* Static field description.
*/
descriptionAsString?: string | null;
/**
* Function description
*/
descriptionAsFunction?: string | null;
descriptionAsComponent?: string | null;
customSelectField?: string | null;