fix: join field description, custom components and loading state (#9703)
- [fix: join field shows loading when creating a document](9f7a2e7936) - [fix: join field descriptions](90e8cdb464) - [feat(ui): adds before & after inputs to join field](19d43329ad) --------- Co-authored-by: Patrik <patrik@payloadcms.com>
This commit is contained in:
@@ -1372,6 +1372,8 @@ export type JoinField = {
|
|||||||
admin?: {
|
admin?: {
|
||||||
allowCreate?: boolean
|
allowCreate?: boolean
|
||||||
components?: {
|
components?: {
|
||||||
|
afterInput?: CustomComponent[]
|
||||||
|
beforeInput?: CustomComponent[]
|
||||||
Error?: CustomComponent<JoinFieldErrorClientComponent | JoinFieldErrorServerComponent>
|
Error?: CustomComponent<JoinFieldErrorClientComponent | JoinFieldErrorServerComponent>
|
||||||
Label?: CustomComponent<JoinFieldLabelClientComponent | JoinFieldLabelServerComponent>
|
Label?: CustomComponent<JoinFieldLabelClientComponent | JoinFieldLabelServerComponent>
|
||||||
} & Admin['components']
|
} & Admin['components']
|
||||||
|
|||||||
@@ -35,7 +35,9 @@ import { RelationshipTablePagination } from './Pagination.js'
|
|||||||
const baseClass = 'relationship-table'
|
const baseClass = 'relationship-table'
|
||||||
|
|
||||||
type RelationshipTableComponentProps = {
|
type RelationshipTableComponentProps = {
|
||||||
|
readonly AfterInput?: React.ReactNode
|
||||||
readonly allowCreate?: boolean
|
readonly allowCreate?: boolean
|
||||||
|
readonly BeforeInput?: React.ReactNode
|
||||||
readonly disableTable?: boolean
|
readonly disableTable?: boolean
|
||||||
readonly field: JoinFieldClient
|
readonly field: JoinFieldClient
|
||||||
readonly filterOptions?: Where
|
readonly filterOptions?: Where
|
||||||
@@ -47,7 +49,9 @@ type RelationshipTableComponentProps = {
|
|||||||
|
|
||||||
export const RelationshipTable: React.FC<RelationshipTableComponentProps> = (props) => {
|
export const RelationshipTable: React.FC<RelationshipTableComponentProps> = (props) => {
|
||||||
const {
|
const {
|
||||||
|
AfterInput,
|
||||||
allowCreate = true,
|
allowCreate = true,
|
||||||
|
BeforeInput,
|
||||||
disableTable = false,
|
disableTable = false,
|
||||||
filterOptions,
|
filterOptions,
|
||||||
initialData: initialDataFromProps,
|
initialData: initialDataFromProps,
|
||||||
@@ -91,7 +95,7 @@ export const RelationshipTable: React.FC<RelationshipTableComponentProps> = (pro
|
|||||||
() => getEntityConfig({ collectionSlug: relationTo }) as ClientCollectionConfig,
|
() => getEntityConfig({ collectionSlug: relationTo }) as ClientCollectionConfig,
|
||||||
)
|
)
|
||||||
|
|
||||||
const [isLoadingTable, setIsLoadingTable] = useState(true)
|
const [isLoadingTable, setIsLoadingTable] = useState(!disableTable)
|
||||||
const [data, setData] = useState<PaginatedDocs>(initialData)
|
const [data, setData] = useState<PaginatedDocs>(initialData)
|
||||||
const [columnState, setColumnState] = useState<Column[]>()
|
const [columnState, setColumnState] = useState<Column[]>()
|
||||||
|
|
||||||
@@ -197,6 +201,7 @@ export const RelationshipTable: React.FC<RelationshipTableComponentProps> = (pro
|
|||||||
</Pill>
|
</Pill>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
{BeforeInput}
|
||||||
{isLoadingTable ? (
|
{isLoadingTable ? (
|
||||||
<p>{t('general:loading')}</p>
|
<p>{t('general:loading')}</p>
|
||||||
) : (
|
) : (
|
||||||
@@ -257,6 +262,7 @@ export const RelationshipTable: React.FC<RelationshipTableComponentProps> = (pro
|
|||||||
)}
|
)}
|
||||||
</Fragment>
|
</Fragment>
|
||||||
)}
|
)}
|
||||||
|
{AfterInput}
|
||||||
<DocumentDrawer initialData={initialDrawerData} onSave={onDrawerCreate} />
|
<DocumentDrawer initialData={initialDrawerData} onSave={onDrawerCreate} />
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -5,9 +5,11 @@ import type { JoinFieldClient, JoinFieldClientComponent, PaginatedDocs, Where }
|
|||||||
import React, { useMemo } from 'react'
|
import React, { useMemo } from 'react'
|
||||||
|
|
||||||
import { RelationshipTable } from '../../elements/RelationshipTable/index.js'
|
import { RelationshipTable } from '../../elements/RelationshipTable/index.js'
|
||||||
|
import { RenderCustomComponent } from '../../elements/RenderCustomComponent/index.js'
|
||||||
import { useField } from '../../forms/useField/index.js'
|
import { useField } from '../../forms/useField/index.js'
|
||||||
import { withCondition } from '../../forms/withCondition/index.js'
|
import { withCondition } from '../../forms/withCondition/index.js'
|
||||||
import { useDocumentInfo } from '../../providers/DocumentInfo/index.js'
|
import { useDocumentInfo } from '../../providers/DocumentInfo/index.js'
|
||||||
|
import { FieldDescription } from '../FieldDescription/index.js'
|
||||||
import { FieldLabel } from '../FieldLabel/index.js'
|
import { FieldLabel } from '../FieldLabel/index.js'
|
||||||
import { fieldBaseClass } from '../index.js'
|
import { fieldBaseClass } from '../index.js'
|
||||||
|
|
||||||
@@ -15,7 +17,7 @@ const JoinFieldComponent: JoinFieldClientComponent = (props) => {
|
|||||||
const {
|
const {
|
||||||
field,
|
field,
|
||||||
field: {
|
field: {
|
||||||
admin: { allowCreate },
|
admin: { allowCreate, description },
|
||||||
collection,
|
collection,
|
||||||
label,
|
label,
|
||||||
localized,
|
localized,
|
||||||
@@ -27,7 +29,7 @@ const JoinFieldComponent: JoinFieldClientComponent = (props) => {
|
|||||||
|
|
||||||
const { id: docID } = useDocumentInfo()
|
const { id: docID } = useDocumentInfo()
|
||||||
|
|
||||||
const { customComponents: { AfterInput, BeforeInput, Label } = {}, value } =
|
const { customComponents: { AfterInput, BeforeInput, Description, Label } = {}, value } =
|
||||||
useField<PaginatedDocs>({
|
useField<PaginatedDocs>({
|
||||||
path,
|
path,
|
||||||
})
|
})
|
||||||
@@ -57,9 +59,10 @@ const JoinFieldComponent: JoinFieldClientComponent = (props) => {
|
|||||||
className={[fieldBaseClass, 'join'].filter(Boolean).join(' ')}
|
className={[fieldBaseClass, 'join'].filter(Boolean).join(' ')}
|
||||||
id={`field-${path?.replace(/\./g, '__')}`}
|
id={`field-${path?.replace(/\./g, '__')}`}
|
||||||
>
|
>
|
||||||
{BeforeInput}
|
|
||||||
<RelationshipTable
|
<RelationshipTable
|
||||||
|
AfterInput={AfterInput}
|
||||||
allowCreate={typeof docID !== 'undefined' && allowCreate}
|
allowCreate={typeof docID !== 'undefined' && allowCreate}
|
||||||
|
BeforeInput={BeforeInput}
|
||||||
disableTable={filterOptions === null}
|
disableTable={filterOptions === null}
|
||||||
field={field as JoinFieldClient}
|
field={field as JoinFieldClient}
|
||||||
filterOptions={filterOptions}
|
filterOptions={filterOptions}
|
||||||
@@ -76,7 +79,10 @@ const JoinFieldComponent: JoinFieldClientComponent = (props) => {
|
|||||||
}
|
}
|
||||||
relationTo={collection}
|
relationTo={collection}
|
||||||
/>
|
/>
|
||||||
{AfterInput}
|
<RenderCustomComponent
|
||||||
|
CustomComponent={Description}
|
||||||
|
Fallback={<FieldDescription description={description} path={path} />}
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -47,6 +47,13 @@ export const Categories: CollectionConfig = {
|
|||||||
name: 'relatedPosts',
|
name: 'relatedPosts',
|
||||||
label: 'Related Posts',
|
label: 'Related Posts',
|
||||||
type: 'join',
|
type: 'join',
|
||||||
|
admin: {
|
||||||
|
components: {
|
||||||
|
afterInput: ['/components/AfterInput.js#AfterInput'],
|
||||||
|
beforeInput: ['/components/BeforeInput.js#BeforeInput'],
|
||||||
|
Description: '/components/CustomDescription/index.js#FieldDescriptionComponent',
|
||||||
|
},
|
||||||
|
},
|
||||||
collection: postsSlug,
|
collection: postsSlug,
|
||||||
defaultSort: '-title',
|
defaultSort: '-title',
|
||||||
defaultLimit: 5,
|
defaultLimit: 5,
|
||||||
@@ -57,6 +64,9 @@ export const Categories: CollectionConfig = {
|
|||||||
name: 'hasManyPosts',
|
name: 'hasManyPosts',
|
||||||
type: 'join',
|
type: 'join',
|
||||||
collection: postsSlug,
|
collection: postsSlug,
|
||||||
|
admin: {
|
||||||
|
description: 'Static Description',
|
||||||
|
},
|
||||||
on: 'categories',
|
on: 'categories',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
|||||||
7
test/joins/components/AfterInput.tsx
Normal file
7
test/joins/components/AfterInput.tsx
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
'use client'
|
||||||
|
|
||||||
|
import React from 'react'
|
||||||
|
|
||||||
|
export const AfterInput: React.FC = () => {
|
||||||
|
return <div className="after-input">#after-input</div>
|
||||||
|
}
|
||||||
7
test/joins/components/BeforeInput.tsx
Normal file
7
test/joins/components/BeforeInput.tsx
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
'use client'
|
||||||
|
|
||||||
|
import React from 'react'
|
||||||
|
|
||||||
|
export const BeforeInput: React.FC = () => {
|
||||||
|
return <div className="before-input">#before-input</div>
|
||||||
|
}
|
||||||
8
test/joins/components/CustomDescription/index.tsx
Normal file
8
test/joins/components/CustomDescription/index.tsx
Normal file
@@ -0,0 +1,8 @@
|
|||||||
|
'use client'
|
||||||
|
import type { FieldDescriptionClientComponent } from 'payload'
|
||||||
|
|
||||||
|
import React from 'react'
|
||||||
|
|
||||||
|
export const FieldDescriptionComponent: FieldDescriptionClientComponent = ({ path }) => {
|
||||||
|
return <div className={`field-description-${path}`}>Component description: {path}</div>
|
||||||
|
}
|
||||||
@@ -22,6 +22,11 @@ const filename = fileURLToPath(import.meta.url)
|
|||||||
const dirname = path.dirname(filename)
|
const dirname = path.dirname(filename)
|
||||||
|
|
||||||
export default buildConfigWithDefaults({
|
export default buildConfigWithDefaults({
|
||||||
|
admin: {
|
||||||
|
importMap: {
|
||||||
|
baseDir: path.resolve(dirname),
|
||||||
|
},
|
||||||
|
},
|
||||||
collections: [
|
collections: [
|
||||||
Posts,
|
Posts,
|
||||||
Categories,
|
Categories,
|
||||||
|
|||||||
@@ -88,6 +88,22 @@ test.describe('Admin Panel', () => {
|
|||||||
await page.goto(categoriesURL.create)
|
await page.goto(categoriesURL.create)
|
||||||
const nameField = page.locator('#field-name')
|
const nameField = page.locator('#field-name')
|
||||||
await expect(nameField).toBeVisible()
|
await expect(nameField).toBeVisible()
|
||||||
|
|
||||||
|
// assert that the join field is visible and is not stuck in a loading state
|
||||||
|
await expect(page.locator('#field-relatedPosts')).toContainText('No Posts found.')
|
||||||
|
await expect(page.locator('#field-relatedPosts')).not.toContainText('loading')
|
||||||
|
|
||||||
|
// assert that the create new button is not visible
|
||||||
|
await expect(page.locator('#field-relatedPosts > .relationship-table__add-new')).toBeHidden()
|
||||||
|
|
||||||
|
// assert that the admin.description is visible
|
||||||
|
await expect(page.locator('.field-description-hasManyPosts')).toHaveText('Static Description')
|
||||||
|
|
||||||
|
//assert that the admin.components.Description is visible
|
||||||
|
await expect(page.locator('.field-description-relatedPosts')).toHaveText(
|
||||||
|
'Component description: relatedPosts',
|
||||||
|
)
|
||||||
|
|
||||||
await nameField.fill('test category')
|
await nameField.fill('test category')
|
||||||
await saveDocAndAssert(page)
|
await saveDocAndAssert(page)
|
||||||
})
|
})
|
||||||
|
|||||||
Reference in New Issue
Block a user