chore(ui): server renders custom list view

This commit is contained in:
Jacob Fletcher
2024-02-23 12:32:43 -05:00
parent a57410133a
commit b9dfa1aafe
19 changed files with 237 additions and 183 deletions

3
.gitignore vendored
View File

@@ -5,6 +5,7 @@ dist
test-results test-results
.devcontainer .devcontainer
/migrations /migrations
/media
# Created by https://www.toptal.com/developers/gitignore/api/node,macos,windows,webstorm,sublimetext,visualstudiocode # Created by https://www.toptal.com/developers/gitignore/api/node,macos,windows,webstorm,sublimetext,visualstudiocode
# Edit at https://www.toptal.com/developers/gitignore?templates=node,macos,windows,webstorm,sublimetext,visualstudiocode # Edit at https://www.toptal.com/developers/gitignore?templates=node,macos,windows,webstorm,sublimetext,visualstudiocode
@@ -371,4 +372,4 @@ $RECYCLE.BIN/
# End of https://www.toptal.com/developers/gitignore/api/node,macos,windows,webstorm,sublimetext,visualstudiocode # End of https://www.toptal.com/developers/gitignore/api/node,macos,windows,webstorm,sublimetext,visualstudiocode
/build /build

View File

@@ -1,6 +1,5 @@
/* THIS FILE WAS GENERATED AUTOMATICALLY BY PAYLOAD. */ /* THIS FILE WAS GENERATED AUTOMATICALLY BY PAYLOAD. */
/* DO NOT MODIFY IT BECAUSE IT COULD BE REWRITTEN AT ANY TIME. */ /* DO NOT MODIFY IT BECAUSE IT COULD BE REWRITTEN AT ANY TIME. */
// import { CollectionList } from '@payloadcms/next/pages/CollectionList'
import { Document } from '@payloadcms/next/pages/Document' import { Document } from '@payloadcms/next/pages/Document'
import config from 'payload-config' import config from 'payload-config'

View File

@@ -1,10 +1,10 @@
/* THIS FILE WAS GENERATED AUTOMATICALLY BY PAYLOAD. */ /* THIS FILE WAS GENERATED AUTOMATICALLY BY PAYLOAD. */
/* DO NOT MODIFY IT BECAUSE IT COULD BE REWRITTEN AT ANY TIME. */ /* DO NOT MODIFY IT BECAUSE IT COULD BE REWRITTEN AT ANY TIME. */
import { CollectionList } from '@payloadcms/next/pages/CollectionList' import { ListView } from '@payloadcms/next/pages/List'
import config from 'payload-config' import config from 'payload-config'
export default ({ params, searchParams }) => export default ({ params, searchParams }) =>
CollectionList({ ListView({
collectionSlug: params.collection, collectionSlug: params.collection,
searchParams, searchParams,
config, config,

View File

@@ -9,12 +9,12 @@ import {
Form, Form,
Select, Select,
Number as NumberInput, Number as NumberInput,
EditViewProps,
useConfig, useConfig,
MinimizeMaximize, MinimizeMaximize,
useActions, useActions,
useTranslation, useTranslation,
useLocale, useLocale,
ServerSideEditViewProps,
} from '@payloadcms/ui' } from '@payloadcms/ui'
import { RenderJSON } from './RenderJSON' import { RenderJSON } from './RenderJSON'
import { useSearchParams } from 'next/navigation' import { useSearchParams } from 'next/navigation'
@@ -24,7 +24,7 @@ import './index.scss'
const baseClass = 'query-inspector' const baseClass = 'query-inspector'
export const APIViewClient: React.FC<EditViewProps> = (props) => { export const APIViewClient: React.FC<ServerSideEditViewProps> = (props) => {
const { data: initialData } = props const { data: initialData } = props
const searchParams = useSearchParams() const searchParams = useSearchParams()
@@ -133,8 +133,8 @@ export const APIViewClient: React.FC<EditViewProps> = (props) => {
> >
<SetStepNav <SetStepNav
collectionSlug={collectionSlug} collectionSlug={collectionSlug}
useAsTitle={collectionConfig?.admin?.useAsTitle} useAsTitle={collectionConfig ? collectionConfig?.admin?.useAsTitle : undefined}
pluralLabel={collectionConfig?.labels.plural} pluralLabel={collectionConfig ? collectionConfig?.labels?.plural : undefined}
globalLabel={globalConfig?.label} globalLabel={globalConfig?.label}
globalSlug={globalSlug} globalSlug={globalSlug}
id={id} id={id}

View File

@@ -10,8 +10,9 @@ import {
import { initPage } from '../../utilities/initPage' import { initPage } from '../../utilities/initPage'
import { notFound } from 'next/navigation' import { notFound } from 'next/navigation'
import { ListPreferences } from '../../../../ui/src/views/List/types' import { ListPreferences } from '../../../../ui/src/views/List/types'
import { ListInfoProvider } from '../../../../ui/src/providers/ListInfo'
export const CollectionList = async ({ export const ListView = async ({
collectionSlug, collectionSlug,
config: configPromise, config: configPromise,
searchParams, searchParams,
@@ -74,23 +75,27 @@ export const CollectionList = async ({
}) })
const componentProps: DefaultListViewProps = { const componentProps: DefaultListViewProps = {
data,
hasCreatePermission: permissions?.collections?.[collectionSlug]?.create?.permission,
limit,
newDocumentURL: `${admin}/collections/${collectionSlug}/create`,
collectionSlug, collectionSlug,
} }
return ( return (
<Fragment> <Fragment>
<HydrateClientUser user={user} permissions={permissions} /> <HydrateClientUser user={user} permissions={permissions} />
<TableColumnsProvider collectionSlug={collectionSlug} listPreferences={listPreferences}> <ListInfoProvider
<RenderCustomComponent data={data}
CustomComponent={ListToRender} hasCreatePermission={permissions?.collections?.[collectionSlug]?.create?.permission}
DefaultComponent={DefaultList} limit={limit}
componentProps={componentProps} newDocumentURL={`${admin}/collections/${collectionSlug}/create`}
/> collectionSlug={collectionSlug}
</TableColumnsProvider> >
<TableColumnsProvider collectionSlug={collectionSlug} listPreferences={listPreferences}>
<RenderCustomComponent
CustomComponent={ListToRender}
DefaultComponent={DefaultList}
componentProps={componentProps}
/>
</TableColumnsProvider>
</ListInfoProvider>
</Fragment> </Fragment>
) )
} }

View File

@@ -98,7 +98,7 @@ const Content: React.FC<DocumentDrawerProps> = ({
// (!isEditing && (docPermissions as CollectionPermission)?.create?.permission) // (!isEditing && (docPermissions as CollectionPermission)?.create?.permission)
useEffect(() => { useEffect(() => {
if (!hasInitializedState.current && data) { if (!hasInitializedState.current && (!initialID.current || (initialID.current && data))) {
const getInitialState = async () => { const getInitialState = async () => {
const result = await getFormState({ const result = await getFormState({
serverURL, serverURL,
@@ -106,7 +106,7 @@ const Content: React.FC<DocumentDrawerProps> = ({
body: { body: {
id, id,
operation: isEditing ? 'update' : 'create', operation: isEditing ? 'update' : 'create',
data, data: data || {},
docPreferences: null, // TODO: get this docPreferences: null, // TODO: get this
schemaPath, schemaPath,
}, },
@@ -119,9 +119,7 @@ const Content: React.FC<DocumentDrawerProps> = ({
} }
}, [apiRoute, data, id, isEditing, schemaPath, serverURL]) }, [apiRoute, data, id, isEditing, schemaPath, serverURL])
const isLoading = !initialState || isLoadingDocument if (!initialState || isLoadingDocument) {
if (isLoading) {
return <LoadingOverlay /> return <LoadingOverlay />
} }
@@ -155,7 +153,7 @@ const Content: React.FC<DocumentDrawerProps> = ({
} }
initialData={data} initialData={data}
disableActions disableActions
// disableLeaveWithoutSaving disableLeaveWithoutSaving
// hasSavePermission={hasSavePermission} // hasSavePermission={hasSavePermission}
// isEditing={isEditing} // isEditing={isEditing}
// isLoading, // isLoading,
@@ -164,7 +162,7 @@ const Content: React.FC<DocumentDrawerProps> = ({
collectionSlug={collectionConfig.slug} collectionSlug={collectionConfig.slug}
docPermissions={{} as CollectionPermission} // TODO; get this docPermissions={{} as CollectionPermission} // TODO; get this
docPreferences={null} // TODO: get this docPreferences={null} // TODO: get this
initialState initialState={initialState}
> >
{Edit} {Edit}
</DocumentInfoProvider> </DocumentInfoProvider>
@@ -178,10 +176,10 @@ const Content: React.FC<DocumentDrawerProps> = ({
export const DocumentDrawerContent: React.FC<DocumentDrawerProps> = (props) => { export const DocumentDrawerContent: React.FC<DocumentDrawerProps> = (props) => {
const { id: idFromProps, collectionSlug, onSave: onSaveFromProps } = props const { id: idFromProps, collectionSlug, onSave: onSaveFromProps } = props
const [collectionConfig] = useRelatedCollections(collectionSlug) const [collectionConfig] = useRelatedCollections(collectionSlug)
const [id, setId] = useState<null | string>(idFromProps) const [id, setId] = useState<null | string | number>(idFromProps)
const onSave = useCallback<DocumentDrawerProps['onSave']>( const onSave = useCallback<DocumentDrawerProps['onSave']>(
(args) => { async (args) => {
setId(args.doc.id) setId(args.doc.id)
if (typeof onSaveFromProps === 'function') { if (typeof onSaveFromProps === 'function') {

View File

@@ -23,7 +23,7 @@ const formatDocumentDrawerSlug = ({
}: { }: {
collectionSlug: string collectionSlug: string
depth: number depth: number
id: string id: string | number
uuid: string // supply when creating a new document and no id is available uuid: string // supply when creating a new document and no id is available
}) => `doc-drawer_${collectionSlug}_${depth}${id ? `_${id}` : ''}_${uuid}` }) => `doc-drawer_${collectionSlug}_${depth}${id ? `_${id}` : ''}_${uuid}`
@@ -69,6 +69,7 @@ export const useDocumentDrawer: UseDocumentDrawer = ({ id, collectionSlug }) =>
const uuid = useId() const uuid = useId()
const { closeModal, modalState, openModal, toggleModal } = useModal() const { closeModal, modalState, openModal, toggleModal } = useModal()
const [isOpen, setIsOpen] = useState(false) const [isOpen, setIsOpen] = useState(false)
const drawerSlug = formatDocumentDrawerSlug({ const drawerSlug = formatDocumentDrawerSlug({
id, id,
collectionSlug, collectionSlug,

View File

@@ -1,14 +1,14 @@
import type React from 'react' import type React from 'react'
import type { HTMLAttributes } from 'react' import type { HTMLAttributes } from 'react'
import type { EditViewProps } from '../../views/types'
import type { Props as DrawerProps } from '../Drawer/types' import type { Props as DrawerProps } from '../Drawer/types'
import { DocumentInfoContext } from '../../providers/DocumentInfo/types'
export type DocumentDrawerProps = Pick<DrawerProps, 'Header'> & { export type DocumentDrawerProps = Pick<DrawerProps, 'Header'> & {
collectionSlug: string collectionSlug: string
drawerSlug?: string drawerSlug?: string
id?: string id?: string | number
onSave?: EditViewProps['onSave'] onSave?: DocumentInfoContext['onSave']
} }
export type DocumentTogglerProps = HTMLAttributes<HTMLButtonElement> & { export type DocumentTogglerProps = HTMLAttributes<HTMLButtonElement> & {

View File

@@ -14,17 +14,15 @@ import Label from '../../forms/Label'
import { X } from '../../icons/X' import { X } from '../../icons/X'
import { useAuth } from '../../providers/Auth' import { useAuth } from '../../providers/Auth'
import { useConfig } from '../../providers/Config' import { useConfig } from '../../providers/Config'
import { DocumentInfoProvider } from '../../providers/DocumentInfo'
import { usePreferences } from '../../providers/Preferences' import { usePreferences } from '../../providers/Preferences'
import { RenderCustomComponent } from '../RenderCustomComponent'
import { DefaultList } from '../../views/List'
// import formatFields from '../../views/List/formatFields'
import { useDocumentDrawer } from '../DocumentDrawer' import { useDocumentDrawer } from '../DocumentDrawer'
import Pill from '../Pill' import Pill from '../Pill'
import ReactSelect from '../ReactSelect' import ReactSelect from '../ReactSelect'
import { TableColumnsProvider } from '../TableColumns' import { TableColumnsProvider } from '../TableColumns'
import ViewDescription from '../ViewDescription' import ViewDescription from '../ViewDescription'
import { DefaultListViewProps } from '../../views/List/types' import { useComponentMap } from '../..'
import { ListInfoProvider } from '../../providers/ListInfo'
import { LoadingOverlay } from '../Loading'
const hoistQueryParamsToAnd = (where: Where, queryParams: Where) => { const hoistQueryParamsToAnd = (where: Where, queryParams: Where) => {
if ('and' in where) { if ('and' in where) {
@@ -55,10 +53,12 @@ export const ListDrawerContent: React.FC<ListDrawerProps> = ({
const { setPreference } = usePreferences() const { setPreference } = usePreferences()
const { closeModal, isModalOpen } = useModal() const { closeModal, isModalOpen } = useModal()
const [limit, setLimit] = useState<number>() const [limit, setLimit] = useState<number>()
const [sort, setSort] = useState(null) const [sort, setSort] = useState<string>(null)
const [page, setPage] = useState(1) const [page, setPage] = useState<number>(1)
const [where, setWhere] = useState(null) const [where, setWhere] = useState<Where>(null)
const [search, setSearch] = useState('') const [search, setSearch] = useState<string>('')
const { componentMap } = useComponentMap()
const { const {
collections, collections,
@@ -78,6 +78,8 @@ export const ListDrawerContent: React.FC<ListDrawerProps> = ({
) )
}) })
const { List } = componentMap.collections?.[selectedCollectionConfig?.slug] || {}
const [selectedOption, setSelectedOption] = useState<{ label: string; value: string }>(() => const [selectedOption, setSelectedOption] = useState<{ label: string; value: string }>(() =>
selectedCollectionConfig selectedCollectionConfig
? { ? {
@@ -129,7 +131,7 @@ export const ListDrawerContent: React.FC<ListDrawerProps> = ({
const isOpen = isModalOpen(drawerSlug) const isOpen = isModalOpen(drawerSlug)
const apiURL = isOpen ? `${serverURL}${api}/${selectedCollectionConfig.slug}` : null const apiURL = isOpen ? `${serverURL}${api}/${selectedCollectionConfig.slug}` : null
const [cacheBust, dispatchCacheBust] = useReducer((state) => state + 1, 0) // used to force a re-fetch even when apiURL is unchanged const [cacheBust, dispatchCacheBust] = useReducer((state) => state + 1, 0) // used to force a re-fetch even when apiURL is unchanged
const [{ data, isError }, { setParams }] = usePayloadAPI(apiURL, {}) const [{ data, isError, isLoading: isLoadingList }, { setParams }] = usePayloadAPI(apiURL, {})
const moreThanOneAvailableCollection = enabledCollectionConfigs.length > 1 const moreThanOneAvailableCollection = enabledCollectionConfigs.length > 1
useEffect(() => { useEffect(() => {
@@ -215,104 +217,93 @@ export const ListDrawerContent: React.FC<ListDrawerProps> = ({
return null return null
} }
const listComponent = selectedCollectionConfig?.admin?.components?.views?.List if (isLoadingList) {
let ListToRender = null return <LoadingOverlay />
if (listComponent && typeof listComponent === 'function') {
ListToRender = listComponent
} else if (typeof listComponent === 'object' && typeof listComponent.Component === 'function') {
ListToRender = listComponent.Component
}
const componentProps: DefaultListViewProps = {
collectionSlug: selectedCollectionConfig.slug,
Header: (
<header className={`${baseClass}__header`}>
<div className={`${baseClass}__header-wrap`}>
<div className={`${baseClass}__header-content`}>
<h2 className={`${baseClass}__header-text`}>
{!customHeader
? getTranslation(selectedCollectionConfig?.labels?.plural, i18n)
: customHeader}
</h2>
{hasCreatePermission && (
<DocumentDrawerToggler className={`${baseClass}__create-new-button`}>
<Pill>{t('general:createNew')}</Pill>
</DocumentDrawerToggler>
)}
</div>
<button
className={`${baseClass}__header-close`}
onClick={() => {
closeModal(drawerSlug)
}}
type="button"
>
<X />
</button>
</div>
{selectedCollectionConfig?.admin?.description && (
<div className={`${baseClass}__sub-header`}>
<ViewDescription description={selectedCollectionConfig.admin.description} />
</div>
)}
{moreThanOneAvailableCollection && (
<div className={`${baseClass}__select-collection-wrap`}>
<Label label={t('upload:selectCollectionToBrowse')} />
<ReactSelect
className={`${baseClass}__select-collection`}
onChange={setSelectedOption} // this is only changing the options which is not rerunning my effect
options={enabledCollectionConfigs.map((coll) => ({
label: getTranslation(coll.labels.singular, i18n),
value: coll.slug,
}))}
value={selectedOption}
/>
</div>
)}
</header>
),
data,
handlePageChange: setPage,
handlePerPageChange: setLimit,
handleSearchChange: setSearch,
handleSortChange: setSort,
handleWhereChange: setWhere,
hasCreatePermission,
limit: limit || selectedCollectionConfig?.admin?.pagination?.defaultLimit,
modifySearchParams: false,
newDocumentURL: null,
setLimit,
setSort,
titleField,
} }
return ( return (
<TableColumnsProvider <ListInfoProvider
cellProps={[
{
className: `${baseClass}__first-cell`,
link: false,
onClick: ({ collectionSlug: rowColl, rowData }) => {
if (typeof onSelect === 'function') {
onSelect({
collectionSlug: rowColl,
docID: rowData.id as string,
})
}
},
},
]}
collectionSlug={selectedCollectionConfig.slug} collectionSlug={selectedCollectionConfig.slug}
data={data}
handlePageChange={setPage}
handlePerPageChange={setLimit}
handleSearchChange={setSearch}
handleSortChange={setSort}
handleWhereChange={setWhere}
hasCreatePermission={hasCreatePermission}
limit={limit || selectedCollectionConfig?.admin?.pagination?.defaultLimit}
modifySearchParams={false}
newDocumentURL={null}
setLimit={setLimit}
setSort={setSort}
titleField={titleField}
Header={
<header className={`${baseClass}__header`}>
<div className={`${baseClass}__header-wrap`}>
<div className={`${baseClass}__header-content`}>
<h2 className={`${baseClass}__header-text`}>
{!customHeader
? getTranslation(selectedCollectionConfig?.labels?.plural, i18n)
: customHeader}
</h2>
{hasCreatePermission && (
<DocumentDrawerToggler className={`${baseClass}__create-new-button`}>
<Pill>{t('general:createNew')}</Pill>
</DocumentDrawerToggler>
)}
</div>
<button
className={`${baseClass}__header-close`}
onClick={() => {
closeModal(drawerSlug)
}}
type="button"
>
<X />
</button>
</div>
{selectedCollectionConfig?.admin?.description && (
<div className={`${baseClass}__sub-header`}>
<ViewDescription description={selectedCollectionConfig.admin.description} />
</div>
)}
{moreThanOneAvailableCollection && (
<div className={`${baseClass}__select-collection-wrap`}>
<Label label={t('upload:selectCollectionToBrowse')} />
<ReactSelect
className={`${baseClass}__select-collection`}
onChange={setSelectedOption} // this is only changing the options which is not rerunning my effect
options={enabledCollectionConfigs.map((coll) => ({
label: getTranslation(coll.labels.singular, i18n),
value: coll.slug,
}))}
value={selectedOption}
/>
</div>
)}
</header>
}
> >
<DocumentInfoProvider collectionSlug={selectedCollectionConfig.slug}> <TableColumnsProvider
<RenderCustomComponent cellProps={[
CustomComponent={ListToRender} {
DefaultComponent={DefaultList} className: `${baseClass}__first-cell`,
componentProps={componentProps} link: false,
/> onClick: ({ collectionSlug: rowColl, rowData }) => {
</DocumentInfoProvider> if (typeof onSelect === 'function') {
<DocumentDrawer onSave={onCreateNew} /> onSelect({
</TableColumnsProvider> collectionSlug: rowColl,
docID: rowData.id as string,
})
}
},
},
]}
collectionSlug={selectedCollectionConfig.slug}
>
{List}
<DocumentDrawer onSave={onCreateNew} />
</TableColumnsProvider>
</ListInfoProvider>
) )
} }

View File

@@ -3,7 +3,6 @@ import type React from 'react'
import type { import type {
SanitizedCollectionConfig, SanitizedCollectionConfig,
SanitizedGlobalConfig, SanitizedGlobalConfig,
FormState,
TypeWithTimestamps, TypeWithTimestamps,
TypeWithID, TypeWithID,
DocumentPermissions, DocumentPermissions,
@@ -12,6 +11,7 @@ import type {
} from 'payload/types' } from 'payload/types'
import { PaginatedDocs, TypeWithVersion } from 'payload/database' import { PaginatedDocs, TypeWithVersion } from 'payload/database'
import { FormState } from '../../forms/Form/types'
export type DocumentInfoProps = { export type DocumentInfoProps = {
AfterDocument?: React.ReactNode AfterDocument?: React.ReactNode
@@ -24,12 +24,13 @@ export type DocumentInfoProps = {
id?: number | string id?: number | string
initialData?: Data initialData?: Data
initialState?: FormState initialState?: FormState
onSave?: (data: Record<string, unknown>) => Promise<void> onSave?: (data: Data) => Promise<void> | void
action?: string action?: string
apiURL?: string apiURL?: string
docPreferences?: DocumentPreferences docPreferences?: DocumentPreferences
hasSavePermission?: boolean hasSavePermission?: boolean
disableActions?: boolean disableActions?: boolean
disableLeaveWithoutSaving?: boolean
} }
export type DocumentInfo = DocumentInfoProps & { export type DocumentInfo = DocumentInfoProps & {

View File

@@ -0,0 +1,15 @@
'use client'
import { useEffect } from 'react'
import { useDocumentInfo } from '..'
import { DocumentInfo } from '../types'
export const SetDocumentInfo: React.FC<DocumentInfo> = (props) => {
const { setDocumentInfo } = useDocumentInfo()
useEffect(() => {
setDocumentInfo(props)
}, [props])
return null
}

View File

@@ -0,0 +1,32 @@
'use client'
import React, { createContext, useContext, useState } from 'react'
import type { ListInfo, ListInfoContext, ListInfoProps } from './types'
import { useConfig } from '../Config'
const Context = createContext({} as ListInfoContext)
export const useListInfo = (): ListInfoContext => useContext(Context)
export const ListInfoProvider: React.FC<
ListInfoProps & {
children: React.ReactNode
}
> = ({ children, ...rest }) => {
const [listInfo, setListInfo] = useState<ListInfo>({
...rest,
})
const {
routes: { api },
serverURL,
collections,
globals,
} = useConfig()
const value: ListInfoContext = {
...listInfo,
}
return <Context.Provider value={value}>{children}</Context.Provider>
}

View File

@@ -0,0 +1,30 @@
import { Data, FieldAffectingData, SanitizedCollectionConfig, Where } from 'payload/types'
import type React from 'react'
export type ListInfoProps = {
Header?: React.ReactNode
collectionSlug: SanitizedCollectionConfig['slug']
data: Data
handlePageChange?: (page: number) => void
handlePerPageChange?: (limit: number) => void
handleSearchChange?: (search: string) => void
handleSortChange?: (sort: string) => void
handleWhereChange?: (where: Where) => void
hasCreatePermission: boolean
limit: number | SanitizedCollectionConfig['admin']['pagination']['defaultLimit']
modifySearchParams?: false
newDocumentURL: string
setLimit?: (limit: number) => void
setSort?: (sort: string) => void
titleField?: FieldAffectingData
}
export type ListInfo = ListInfoProps & {
// add context properties here as needed
// see `DocumentInfo` for an example
}
export type ListInfoContext = ListInfo & {
// add context methods here as needed
// see `DocumentInfoContext` for an example
}

View File

@@ -4,7 +4,8 @@ import type { SanitizedConfig } from 'payload/types'
import { mapFields } from './mapFields' import { mapFields } from './mapFields'
import { CollectionComponentMap, ComponentMap, GlobalComponentMap } from './types' import { CollectionComponentMap, ComponentMap, GlobalComponentMap } from './types'
import { DefaultEditView } from '../../views/Edit' import { DefaultEditView } from '../../views/Edit'
import { EditViewProps } from '../..' import { DefaultList } from '../../views/List'
import { EditViewProps } from 'payload/config'
export const buildComponentMap = (args: { export const buildComponentMap = (args: {
config: SanitizedConfig config: SanitizedConfig
@@ -23,6 +24,7 @@ export const buildComponentMap = (args: {
const { fields, slug } = collectionConfig const { fields, slug } = collectionConfig
const editViewFromConfig = collectionConfig?.admin?.components?.views?.Edit const editViewFromConfig = collectionConfig?.admin?.components?.views?.Edit
const listViewFromConfig = collectionConfig?.admin?.components?.views?.List
const CustomEditView = const CustomEditView =
typeof editViewFromConfig === 'function' typeof editViewFromConfig === 'function'
@@ -35,7 +37,16 @@ export const buildComponentMap = (args: {
? (editViewFromConfig.Default.Component as React.FC<EditViewProps>) ? (editViewFromConfig.Default.Component as React.FC<EditViewProps>)
: undefined : undefined
const CustomListView =
typeof listViewFromConfig === 'function'
? listViewFromConfig
: typeof listViewFromConfig === 'object' &&
typeof listViewFromConfig.Component === 'function'
? listViewFromConfig.Component
: undefined
const Edit = CustomEditView || DefaultEditView const Edit = CustomEditView || DefaultEditView
const List = CustomListView || DefaultList
const beforeList = collectionConfig?.admin?.components?.BeforeList const beforeList = collectionConfig?.admin?.components?.BeforeList
@@ -75,6 +86,7 @@ export const buildComponentMap = (args: {
BeforeListTable, BeforeListTable,
AfterListTable, AfterListTable,
Edit: <Edit collectionSlug={collectionConfig.slug} />, Edit: <Edit collectionSlug={collectionConfig.slug} />,
List: <List collectionSlug={collectionConfig.slug} />,
fieldMap: mappedFields, fieldMap: mappedFields,
} }

View File

@@ -66,6 +66,7 @@ export type CollectionComponentMap = ConfigComponentMapBase & {
AfterList: React.ReactNode AfterList: React.ReactNode
BeforeListTable: React.ReactNode BeforeListTable: React.ReactNode
AfterListTable: React.ReactNode AfterListTable: React.ReactNode
List: React.ReactNode
} }
export type GlobalComponentMap = ConfigComponentMapBase export type GlobalComponentMap = ConfigComponentMapBase

View File

@@ -128,7 +128,6 @@ export const Upload: React.FC<Props> = (props) => {
return ( return (
<div className={[fieldBaseClass, baseClass].filter(Boolean).join(' ')}> <div className={[fieldBaseClass, baseClass].filter(Boolean).join(' ')}>
<Error message={errorMessage} showError={showError} /> <Error message={errorMessage} showError={showError} />
{doc.filename && !replacingFile && ( {doc.filename && !replacingFile && (
<FileDetails <FileDetails
canEdit={showCrop || showFocalPoint} canEdit={showCrop || showFocalPoint}
@@ -140,7 +139,6 @@ export const Upload: React.FC<Props> = (props) => {
imageCacheTag={lastSubmittedTime} imageCacheTag={lastSubmittedTime}
/> />
)} )}
{(!doc.filename || replacingFile) && ( {(!doc.filename || replacingFile) && (
<div className={`${baseClass}__upload`}> <div className={`${baseClass}__upload`}>
{!value && ( {!value && (
@@ -184,7 +182,6 @@ export const Upload: React.FC<Props> = (props) => {
)} )}
</div> </div>
)} )}
{(value || doc.filename) && ( {(value || doc.filename) && (
<Drawer Header={null} slug={editDrawerSlug}> <Drawer Header={null} slug={editDrawerSlug}>
<EditUpload <EditUpload

View File

@@ -28,7 +28,7 @@ const baseClass = 'collection-edit'
// This component receives props only on _pages_ // This component receives props only on _pages_
// When rendered within a drawer, props are empty // When rendered within a drawer, props are empty
// This is solely to support custom edit views which get server-rendered // This is solely to support custom edit views which get server-rendered
export const DefaultEditView: React.FC = (props) => { export const DefaultEditView: React.FC = () => {
const { const {
action, action,
BeforeDocument, BeforeDocument,
@@ -44,6 +44,9 @@ export const DefaultEditView: React.FC = (props) => {
id, id,
hasSavePermission, hasSavePermission,
disableActions, disableActions,
collectionSlug,
globalSlug,
disableLeaveWithoutSaving,
} = useDocumentInfo() } = useDocumentInfo()
const { user } = useAuth() const { user } = useAuth()
@@ -60,11 +63,9 @@ export const DefaultEditView: React.FC = (props) => {
const { getFieldMap } = useComponentMap() const { getFieldMap } = useComponentMap()
const collectionConfig = const collectionConfig =
'collectionSlug' in props && collectionSlug && collections.find((collection) => collection.slug === collectionSlug)
collections.find((collection) => collection.slug === props.collectionSlug)
const globalConfig = const globalConfig = globalSlug && globals.find((global) => global.slug === globalSlug)
'globalSlug' in props && globals.find((global) => global.slug === props.globalSlug)
const [schemaPath] = React.useState(collectionConfig?.slug || globalConfig?.slug) const [schemaPath] = React.useState(collectionConfig?.slug || globalConfig?.slug)
@@ -81,7 +82,7 @@ export const DefaultEditView: React.FC = (props) => {
const preventLeaveWithoutSaving = const preventLeaveWithoutSaving =
(!(collectionConfig?.versions?.drafts && collectionConfig?.versions?.drafts?.autosave) || (!(collectionConfig?.versions?.drafts && collectionConfig?.versions?.drafts?.autosave) ||
!(globalConfig?.versions?.drafts && globalConfig?.versions?.drafts?.autosave)) && !(globalConfig?.versions?.drafts && globalConfig?.versions?.drafts?.autosave)) &&
!('disableLeaveWithoutSaving' in props && props.disableLeaveWithoutSaving) !disableLeaveWithoutSaving
const classes = [baseClass, id && `${baseClass}--is-editing`].filter(Boolean).join(' ') const classes = [baseClass, id && `${baseClass}--is-editing`].filter(Boolean).join(' ')

View File

@@ -1,7 +1,6 @@
'use client' 'use client'
import React, { Fragment, useEffect } from 'react' import React, { Fragment, useEffect } from 'react'
import type { DefaultListViewProps } from './types'
import { SelectionProvider } from './SelectionProvider' import { SelectionProvider } from './SelectionProvider'
import { Gutter } from '../../elements/Gutter' import { Gutter } from '../../elements/Gutter'
import { getTranslation } from '@payloadcms/translations' import { getTranslation } from '@payloadcms/translations'
@@ -17,12 +16,13 @@ import { useComponentMap } from '../../providers/ComponentMapProvider'
import { Table } from '../../elements/Table' import { Table } from '../../elements/Table'
import { ListControls } from '../../elements/ListControls' import { ListControls } from '../../elements/ListControls'
import { useStepNav } from '../../elements/StepNav' import { useStepNav } from '../../elements/StepNav'
import { useListInfo } from '../../providers/ListInfo'
import './index.scss' import './index.scss'
const baseClass = 'collection-list' const baseClass = 'collection-list'
export const DefaultList: React.FC<DefaultListViewProps> = (props) => { export const DefaultList: React.FC = () => {
const { const {
Header, Header,
data, data,
@@ -35,10 +35,10 @@ export const DefaultList: React.FC<DefaultListViewProps> = (props) => {
limit, limit,
modifySearchParams, modifySearchParams,
newDocumentURL, newDocumentURL,
resetParams, // resetParams,
titleField, titleField,
collectionSlug, collectionSlug,
} = props } = useListInfo()
const config = useConfig() const config = useConfig()

View File

@@ -1,37 +1,7 @@
import type { SanitizedCollectionConfig } from 'payload/types' import type { SanitizedCollectionConfig } from 'payload/types'
import type { PaginatedDocs } from 'payload/database'
import type { FieldAffectingData, Where } from 'payload/types'
import type { Props as ListControlsProps } from '../../elements/ListControls/types'
import type { Props as PaginatorProps } from '../../elements/Pagination/types'
import type { Props as PerPageProps } from '../../elements/PerPage'
export type DefaultListViewProps = { export type DefaultListViewProps = {
Header?: React.ReactNode
data: PaginatedDocs<any>
handleDelete?: () => void
handlePageChange?: PaginatorProps['onChange']
handlePerPageChange?: PerPageProps['handleChange']
handleSearchChange?: ListControlsProps['handleSearchChange']
handleSortChange?: ListControlsProps['handleSortChange']
handleWhereChange?: ListControlsProps['handleWhereChange']
hasCreatePermission: boolean
limit: number
modifySearchParams?: boolean
newDocumentURL: string
onCreateNewClick?: () => void
resetParams?: (overrides?: {
page?: number
search?: string
sort?: string
where?: Where
}) => void
setLimit?: (limit: number) => void
setListControls?: (controls: unknown) => void
setSort?: (sort: string) => void
titleField?: FieldAffectingData
toggleColumn?: (column: string) => void
collectionSlug: SanitizedCollectionConfig['slug'] collectionSlug: SanitizedCollectionConfig['slug']
useAsTitle?: SanitizedCollectionConfig['admin']['useAsTitle']
} }
export type ListIndexProps = { export type ListIndexProps = {