fix(ui): move collection description below title in document view (#13946)
### What? Moved description rendering from DocumentFields to DocumentHeader component. ### List view #### Before <img width="1315" height="696" alt="Screenshot 2025-09-26 at 10 12 14 AM" src="https://github.com/user-attachments/assets/9c102f4b-ed71-4e3d-85d6-87464e6c8568" /> #### After <img width="1647" height="762" alt="Screenshot 2025-09-26 at 1 24 12 PM" src="https://github.com/user-attachments/assets/1c2f4eae-5bf8-43ad-af65-23f333b01ba8" /> ### Document View #### Before <img width="1321" height="673" alt="Screenshot 2025-09-26 at 10 57 01 AM" src="https://github.com/user-attachments/assets/3c6c7218-a8f6-4e52-af27-f0c4ffa0a6ef" /> #### After <img width="1645" height="682" alt="Screenshot 2025-09-26 at 1 24 29 PM" src="https://github.com/user-attachments/assets/1ac774c7-8820-4d41-afef-c60044383474" /> ### Document Drawer <img width="1631" height="631" alt="Screenshot 2025-09-26 at 1 24 49 PM" src="https://github.com/user-attachments/assets/42285d23-a37d-4419-9644-f9c27358f2bf" /> --- - To see the specific tasks where the Asana app for GitHub is being used, see below: - https://app.asana.com/0/0/1211478222789332
This commit is contained in:
@@ -5,11 +5,7 @@
|
||||
width: 100%;
|
||||
margin-top: base(0.4);
|
||||
padding-bottom: calc(var(--base) * 1.2);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
position: relative;
|
||||
display: flex;
|
||||
gap: calc(var(--base) / 2);
|
||||
|
||||
&::after {
|
||||
content: '';
|
||||
@@ -22,6 +18,12 @@
|
||||
top: calc(100% - 1px);
|
||||
}
|
||||
|
||||
&__header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: calc(var(--base) / 2);
|
||||
}
|
||||
|
||||
&__title {
|
||||
flex-grow: 1;
|
||||
white-space: nowrap;
|
||||
@@ -31,18 +33,29 @@
|
||||
padding-bottom: base(0.4);
|
||||
line-height: 1;
|
||||
vertical-align: top;
|
||||
padding-bottom: 0;
|
||||
}
|
||||
|
||||
&__after-header {
|
||||
padding-top: var(--base);
|
||||
}
|
||||
|
||||
@include mid-break {
|
||||
margin-top: base(0.25);
|
||||
padding-bottom: calc(var(--base) / 1.5);
|
||||
flex-direction: column;
|
||||
gap: calc(var(--base) / 2);
|
||||
padding-bottom: calc(var(--base) / 2);
|
||||
|
||||
&__header {
|
||||
flex-direction: column;
|
||||
gap: calc(var(--base) / 2);
|
||||
}
|
||||
|
||||
&__title {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
&__after-header {
|
||||
padding-top: calc(var(--base) / 4);
|
||||
}
|
||||
}
|
||||
|
||||
@include small-break {
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
import type { I18n } from '@payloadcms/translations'
|
||||
import type {
|
||||
PayloadRequest,
|
||||
SanitizedCollectionConfig,
|
||||
@@ -15,25 +14,29 @@ import './index.scss'
|
||||
const baseClass = `doc-header`
|
||||
|
||||
export const DocumentHeader: React.FC<{
|
||||
AfterHeader?: React.ReactNode
|
||||
collectionConfig?: SanitizedCollectionConfig
|
||||
globalConfig?: SanitizedGlobalConfig
|
||||
hideTabs?: boolean
|
||||
permissions: SanitizedPermissions
|
||||
req: PayloadRequest
|
||||
}> = (props) => {
|
||||
const { collectionConfig, globalConfig, hideTabs, permissions, req } = props
|
||||
const { AfterHeader, collectionConfig, globalConfig, hideTabs, permissions, req } = props
|
||||
|
||||
return (
|
||||
<Gutter className={baseClass}>
|
||||
<RenderTitle className={`${baseClass}__title`} />
|
||||
{!hideTabs && (
|
||||
<DocumentTabs
|
||||
collectionConfig={collectionConfig}
|
||||
globalConfig={globalConfig}
|
||||
permissions={permissions}
|
||||
req={req}
|
||||
/>
|
||||
)}
|
||||
<div className={`${baseClass}__header`}>
|
||||
<RenderTitle className={`${baseClass}__title`} />
|
||||
{!hideTabs && (
|
||||
<DocumentTabs
|
||||
collectionConfig={collectionConfig}
|
||||
globalConfig={globalConfig}
|
||||
permissions={permissions}
|
||||
req={req}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
{AfterHeader ? <div className={`${baseClass}__after-header`}>{AfterHeader}</div> : null}
|
||||
</Gutter>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -341,6 +341,9 @@ export const renderDocument = async ({
|
||||
req,
|
||||
})
|
||||
|
||||
// Extract Description from documentSlots to pass to DocumentHeader
|
||||
const { Description } = documentSlots
|
||||
|
||||
const clientProps: DocumentViewClientProps = {
|
||||
formState,
|
||||
...documentSlots,
|
||||
@@ -395,6 +398,7 @@ export const renderDocument = async ({
|
||||
>
|
||||
{showHeader && !drawerSlug && (
|
||||
<DocumentHeader
|
||||
AfterHeader={Description}
|
||||
collectionConfig={collectionConfig}
|
||||
globalConfig={globalConfig}
|
||||
permissions={permissions}
|
||||
|
||||
@@ -8,7 +8,6 @@
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: calc(var(--base) * 0.5);
|
||||
align-items: flex-start;
|
||||
border-bottom: 1px solid var(--theme-elevation-100);
|
||||
padding-bottom: var(--base);
|
||||
}
|
||||
@@ -63,6 +62,10 @@
|
||||
}
|
||||
}
|
||||
|
||||
&__after-header {
|
||||
padding-top: calc(var(--base) / 4);
|
||||
}
|
||||
|
||||
&__divider {
|
||||
height: 1px;
|
||||
background: var(--theme-elevation-100);
|
||||
|
||||
@@ -12,9 +12,10 @@ import { documentDrawerBaseClass } from '../index.js'
|
||||
import './index.scss'
|
||||
|
||||
export const DocumentDrawerHeader: React.FC<{
|
||||
AfterHeader?: React.ReactNode
|
||||
drawerSlug: string
|
||||
showDocumentID?: boolean
|
||||
}> = ({ drawerSlug, showDocumentID = true }) => {
|
||||
}> = ({ AfterHeader, drawerSlug, showDocumentID = true }) => {
|
||||
const { closeModal } = useModal()
|
||||
const { t } = useTranslation()
|
||||
|
||||
@@ -34,6 +35,9 @@ export const DocumentDrawerHeader: React.FC<{
|
||||
</button>
|
||||
</div>
|
||||
{showDocumentID && <DocumentID />}
|
||||
{AfterHeader ? (
|
||||
<div className={`${documentDrawerBaseClass}__after-header`}>{AfterHeader}</div>
|
||||
) : null}
|
||||
</Gutter>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -26,7 +26,6 @@ type Args = {
|
||||
export const DocumentFields: React.FC<Args> = ({
|
||||
AfterFields,
|
||||
BeforeFields,
|
||||
Description,
|
||||
docPermissions,
|
||||
fields,
|
||||
forceSidebarWrap,
|
||||
@@ -68,11 +67,6 @@ export const DocumentFields: React.FC<Args> = ({
|
||||
<div className={`${baseClass}__main`}>
|
||||
<Gutter className={`${baseClass}__edit`}>
|
||||
{isTrashed && <TrashBanner />}
|
||||
{Description ? (
|
||||
<header className={`${baseClass}__header`}>
|
||||
<div className={`${baseClass}__sub-header`}>{Description}</div>
|
||||
</header>
|
||||
) : null}
|
||||
{BeforeFields}
|
||||
<RenderFields
|
||||
className={`${baseClass}__fields`}
|
||||
|
||||
@@ -10,5 +10,6 @@
|
||||
padding: base(0.2) base(0.4);
|
||||
border-radius: $style-radius-m;
|
||||
display: inline-flex;
|
||||
width: fit-content;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -520,7 +520,11 @@ export function DefaultEditView({
|
||||
onSuccess={onSave}
|
||||
>
|
||||
{isInDrawer && (
|
||||
<DocumentDrawerHeader drawerSlug={drawerSlug} showDocumentID={!isFolderCollection} />
|
||||
<DocumentDrawerHeader
|
||||
AfterHeader={Description}
|
||||
drawerSlug={drawerSlug}
|
||||
showDocumentID={!isFolderCollection}
|
||||
/>
|
||||
)}
|
||||
{isLockingEnabled && shouldShowDocumentLockedModal && (
|
||||
<DocumentLocked
|
||||
|
||||
@@ -20,6 +20,7 @@
|
||||
|
||||
&__sub-header {
|
||||
flex-basis: 100%;
|
||||
padding-top: calc(var(--base) * 0.75);
|
||||
}
|
||||
|
||||
&__tables {
|
||||
|
||||
@@ -1,19 +1,15 @@
|
||||
import { Banner as PayloadBanner } from '@payloadcms/ui'
|
||||
|
||||
export function Banner(props: {
|
||||
children?: React.ReactNode
|
||||
className?: string
|
||||
description?: string
|
||||
message?: string
|
||||
}) {
|
||||
const { children, description, message } = props
|
||||
const { children, className, description, message } = props
|
||||
return (
|
||||
<div
|
||||
style={{
|
||||
backgroundColor: 'var(--theme-warning-100)',
|
||||
border: '1px dashed',
|
||||
color: 'var(--theme-warning-750)',
|
||||
padding: '1rem',
|
||||
}}
|
||||
>
|
||||
<PayloadBanner className={className} type="success">
|
||||
{children || message || description || 'A custom banner component'}
|
||||
</div>
|
||||
</PayloadBanner>
|
||||
)
|
||||
}
|
||||
|
||||
3
test/admin/components/ViewDescription/index.scss
Normal file
3
test/admin/components/ViewDescription/index.scss
Normal file
@@ -0,0 +1,3 @@
|
||||
.view-description {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
@@ -6,12 +6,15 @@ import { ViewDescription as DefaultViewDescription } from '@payloadcms/ui'
|
||||
import React from 'react'
|
||||
|
||||
import { Banner } from '../Banner/index.js'
|
||||
import './index.scss'
|
||||
|
||||
const baseClass = 'view-description'
|
||||
|
||||
export function ViewDescription({
|
||||
description = 'This is a custom view description component.',
|
||||
}: ViewDescriptionClientProps) {
|
||||
return (
|
||||
<Banner>
|
||||
<Banner className={baseClass}>
|
||||
<DefaultViewDescription description={description} />
|
||||
</Banner>
|
||||
)
|
||||
|
||||
@@ -496,6 +496,19 @@ describe('Document View', () => {
|
||||
// Ensure the original page did not change
|
||||
expect(page.url()).toBe(currentUrl)
|
||||
})
|
||||
|
||||
test('document drawer displays AfterHeader components', async () => {
|
||||
await navigateToDoc(page, postsUrl)
|
||||
await page
|
||||
.locator('.field-type.relationship .relationship--single-value__drawer-toggler')
|
||||
.click()
|
||||
await wait(500)
|
||||
const drawer1Content = page.locator('[id^=doc-drawer_posts_1_] .drawer__content')
|
||||
await expect(drawer1Content).toBeVisible()
|
||||
|
||||
const afterHeader = page.locator('[id^=doc-drawer_posts_1_] .doc-drawer__after-header')
|
||||
await expect(afterHeader).toBeVisible()
|
||||
})
|
||||
})
|
||||
|
||||
describe('descriptions', () => {
|
||||
|
||||
Reference in New Issue
Block a user