chore(ui): server renders custom list view
This commit is contained in:
3
.gitignore
vendored
3
.gitignore
vendored
@@ -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
|
||||||
|
|||||||
@@ -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'
|
||||||
|
|
||||||
|
|||||||
@@ -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,
|
||||||
|
|||||||
@@ -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}
|
||||||
|
|||||||
@@ -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>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
@@ -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') {
|
||||||
|
|||||||
@@ -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,
|
||||||
|
|||||||
@@ -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> & {
|
||||||
|
|||||||
@@ -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>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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 & {
|
||||||
|
|||||||
15
packages/ui/src/providers/ListInfo/SetDocumentInfo/index.tsx
Normal file
15
packages/ui/src/providers/ListInfo/SetDocumentInfo/index.tsx
Normal 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
|
||||||
|
}
|
||||||
32
packages/ui/src/providers/ListInfo/index.tsx
Normal file
32
packages/ui/src/providers/ListInfo/index.tsx
Normal 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>
|
||||||
|
}
|
||||||
30
packages/ui/src/providers/ListInfo/types.ts
Normal file
30
packages/ui/src/providers/ListInfo/types.ts
Normal 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
|
||||||
|
}
|
||||||
@@ -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,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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(' ')
|
||||||
|
|
||||||
|
|||||||
@@ -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()
|
||||||
|
|
||||||
|
|||||||
@@ -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 = {
|
||||||
|
|||||||
Reference in New Issue
Block a user