feat: document drawer controls (#7679)
## Description Currently, you cannot create, delete, or duplicate documents within the document drawer directly. To create a document within a relationship field, for example, you must first navigate to the parent field and open the "create new" drawer. Similarly (but worse), to duplicate or delete a document, you must _navigate to the parent document to perform these actions_ which is incredibly disruptive to the content editing workflow. This becomes especially apparent within the relationship field where you can edit documents inline, but cannot duplicate or delete them. This PR supports all document-level actions within the document drawer so that these actions can be performed on-the-fly without navigating away. Inline duplication flow on a polymorphic "hasOne" relationship: https://github.com/user-attachments/assets/bb80404a-079d-44a1-b9bc-14eb2ab49a46 Inline deletion flow on a polymorphic "hasOne" relationship: https://github.com/user-attachments/assets/10f3587f-f70a-4cca-83ee-5dbcad32f063 - [x] I have read and understand the [CONTRIBUTING.md](https://github.com/payloadcms/payload/blob/main/CONTRIBUTING.md) document in this repository. ## Type of change - [x] New feature (non-breaking change which adds functionality) ## Checklist: - [x] I have added tests that prove my fix is effective or that my feature works - [x] Existing test suite passes locally with my changes
This commit is contained in:
@@ -43,6 +43,7 @@ export const DefaultEditView: React.FC = () => {
|
||||
BeforeFields,
|
||||
collectionSlug,
|
||||
disableActions,
|
||||
disableCreate,
|
||||
disableLeaveWithoutSaving,
|
||||
docPermissions,
|
||||
getDocPreferences,
|
||||
@@ -54,7 +55,12 @@ export const DefaultEditView: React.FC = () => {
|
||||
initialState,
|
||||
isEditing,
|
||||
isInitializing,
|
||||
onDelete,
|
||||
onDrawerCreate,
|
||||
onDuplicate,
|
||||
onSave: onSaveFromContext,
|
||||
redirectAfterDelete,
|
||||
redirectAfterDuplicate,
|
||||
} = useDocumentInfo()
|
||||
|
||||
const { refreshCookieAsync, user } = useAuth()
|
||||
@@ -223,11 +229,18 @@ export const DefaultEditView: React.FC = () => {
|
||||
apiURL={apiURL}
|
||||
data={data}
|
||||
disableActions={disableActions}
|
||||
disableCreate={disableCreate}
|
||||
hasPublishPermission={hasPublishPermission}
|
||||
hasSavePermission={hasSavePermission}
|
||||
id={id}
|
||||
isEditing={isEditing}
|
||||
onDelete={onDelete}
|
||||
onDrawerCreate={onDrawerCreate}
|
||||
onDuplicate={onDuplicate}
|
||||
onSave={onSave}
|
||||
permissions={docPermissions}
|
||||
redirectAfterDelete={redirectAfterDelete}
|
||||
redirectAfterDuplicate={redirectAfterDuplicate}
|
||||
slug={collectionConfig?.slug || globalConfig?.slug}
|
||||
/>
|
||||
<DocumentFields
|
||||
|
||||
@@ -1,19 +1,23 @@
|
||||
'use client'
|
||||
import type { SanitizedCollectionConfig } from 'payload'
|
||||
import type { ClientCollectionConfig, SanitizedCollectionConfig } from 'payload'
|
||||
|
||||
import { Modal, useModal } from '@faceless-ui/modal'
|
||||
import { getTranslation } from '@payloadcms/translations'
|
||||
import { useRouter } from 'next/navigation.js'
|
||||
import React, { useCallback, useState } from 'react'
|
||||
import React, { useCallback, useEffect, useState } from 'react'
|
||||
import { toast } from 'sonner'
|
||||
|
||||
import type { DocumentInfoContext } from '../../providers/DocumentInfo/index.js'
|
||||
|
||||
import { useForm } from '../../forms/Form/context.js'
|
||||
import { useConfig } from '../../providers/Config/index.js'
|
||||
import { useDocumentInfo } from '../../providers/DocumentInfo/index.js'
|
||||
import { useEditDepth } from '../../providers/EditDepth/index.js'
|
||||
import { useTranslation } from '../../providers/Translation/index.js'
|
||||
import { requests } from '../../utilities/api.js'
|
||||
import { formatAdminURL } from '../../utilities/formatAdminURL.js'
|
||||
import { Button } from '../Button/index.js'
|
||||
import { drawerZBase } from '../Drawer/index.js'
|
||||
import { PopupList } from '../Popup/index.js'
|
||||
import { Translation } from '../Translation/index.js'
|
||||
import './index.scss'
|
||||
@@ -21,30 +25,44 @@ import './index.scss'
|
||||
const baseClass = 'delete-document'
|
||||
|
||||
export type Props = {
|
||||
buttonId?: string
|
||||
collectionSlug: SanitizedCollectionConfig['slug']
|
||||
id?: string
|
||||
singularLabel: SanitizedCollectionConfig['labels']['singular']
|
||||
title?: string
|
||||
useAsTitle: SanitizedCollectionConfig['admin']['useAsTitle']
|
||||
readonly buttonId?: string
|
||||
readonly collectionSlug: SanitizedCollectionConfig['slug']
|
||||
readonly id?: string
|
||||
readonly onDelete?: DocumentInfoContext['onDelete']
|
||||
readonly redirectAfterDelete?: boolean
|
||||
readonly singularLabel: SanitizedCollectionConfig['labels']['singular']
|
||||
readonly title?: string
|
||||
readonly useAsTitle: SanitizedCollectionConfig['admin']['useAsTitle']
|
||||
}
|
||||
|
||||
export const DeleteDocument: React.FC<Props> = (props) => {
|
||||
const { id, buttonId, collectionSlug, singularLabel, title: titleFromProps } = props
|
||||
const {
|
||||
id,
|
||||
buttonId,
|
||||
collectionSlug,
|
||||
onDelete,
|
||||
redirectAfterDelete = true,
|
||||
singularLabel,
|
||||
title: titleFromProps,
|
||||
} = props
|
||||
|
||||
const {
|
||||
config: {
|
||||
routes: { admin: adminRoute, api },
|
||||
serverURL,
|
||||
},
|
||||
getEntityConfig,
|
||||
} = useConfig()
|
||||
|
||||
const collectionConfig = getEntityConfig({ collectionSlug }) as ClientCollectionConfig
|
||||
|
||||
const { setModified } = useForm()
|
||||
const [deleting, setDeleting] = useState(false)
|
||||
const { toggleModal } = useModal()
|
||||
const { closeModal, toggleModal } = useModal()
|
||||
const router = useRouter()
|
||||
const { i18n, t } = useTranslation()
|
||||
const { title } = useDocumentInfo()
|
||||
const editDepth = useEditDepth()
|
||||
|
||||
const titleToRender = titleFromProps || title || id
|
||||
|
||||
@@ -55,9 +73,16 @@ export const DeleteDocument: React.FC<Props> = (props) => {
|
||||
toast.error(t('error:deletingTitle', { title }))
|
||||
}, [t, title])
|
||||
|
||||
useEffect(() => {
|
||||
return () => {
|
||||
closeModal(modalSlug)
|
||||
}
|
||||
}, [closeModal, modalSlug])
|
||||
|
||||
const handleDelete = useCallback(async () => {
|
||||
setDeleting(true)
|
||||
setModified(false)
|
||||
|
||||
try {
|
||||
await requests
|
||||
.delete(`${serverURL}${api}/${collectionSlug}/${id}`, {
|
||||
@@ -73,19 +98,32 @@ export const DeleteDocument: React.FC<Props> = (props) => {
|
||||
if (res.status < 400) {
|
||||
setDeleting(false)
|
||||
toggleModal(modalSlug)
|
||||
|
||||
toast.success(
|
||||
t('general:titleDeleted', { label: getTranslation(singularLabel, i18n), title }) ||
|
||||
json.message,
|
||||
)
|
||||
|
||||
return router.push(
|
||||
formatAdminURL({
|
||||
adminRoute,
|
||||
path: `/collections/${collectionSlug}`,
|
||||
}),
|
||||
)
|
||||
if (redirectAfterDelete) {
|
||||
return router.push(
|
||||
formatAdminURL({
|
||||
adminRoute,
|
||||
path: `/collections/${collectionSlug}`,
|
||||
}),
|
||||
)
|
||||
}
|
||||
|
||||
if (typeof onDelete === 'function') {
|
||||
await onDelete({ id, collectionConfig })
|
||||
}
|
||||
|
||||
toggleModal(modalSlug)
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
toggleModal(modalSlug)
|
||||
|
||||
if (json.errors) {
|
||||
json.errors.forEach((error) => toast.error(error.message))
|
||||
} else {
|
||||
@@ -114,6 +152,9 @@ export const DeleteDocument: React.FC<Props> = (props) => {
|
||||
router,
|
||||
adminRoute,
|
||||
addDefaultError,
|
||||
redirectAfterDelete,
|
||||
onDelete,
|
||||
collectionConfig,
|
||||
])
|
||||
|
||||
if (id) {
|
||||
@@ -128,7 +169,13 @@ export const DeleteDocument: React.FC<Props> = (props) => {
|
||||
>
|
||||
{t('general:delete')}
|
||||
</PopupList.Button>
|
||||
<Modal className={baseClass} slug={modalSlug}>
|
||||
<Modal
|
||||
className={baseClass}
|
||||
slug={modalSlug}
|
||||
style={{
|
||||
zIndex: drawerZBase + editDepth,
|
||||
}}
|
||||
>
|
||||
<div className={`${baseClass}__wrapper`}>
|
||||
<div className={`${baseClass}__content`}>
|
||||
<h1>{t('general:confirmDeletion')}</h1>
|
||||
@@ -158,7 +205,11 @@ export const DeleteDocument: React.FC<Props> = (props) => {
|
||||
</Button>
|
||||
<Button
|
||||
id="confirm-delete"
|
||||
onClick={deleting ? undefined : handleDelete}
|
||||
onClick={() => {
|
||||
if (!deleting) {
|
||||
void handleDelete()
|
||||
}
|
||||
}}
|
||||
size="large"
|
||||
>
|
||||
{deleting ? t('general:deleting') : t('general:confirm')}
|
||||
|
||||
@@ -10,7 +10,10 @@ import type {
|
||||
import { getTranslation } from '@payloadcms/translations'
|
||||
import React, { Fragment, useEffect } from 'react'
|
||||
|
||||
import type { DocumentInfoContext } from '../../providers/DocumentInfo/types.js'
|
||||
|
||||
import { useConfig } from '../../providers/Config/index.js'
|
||||
import { useEditDepth } from '../../providers/EditDepth/index.js'
|
||||
import { useTranslation } from '../../providers/Translation/index.js'
|
||||
import { formatAdminURL } from '../../utilities/formatAdminURL.js'
|
||||
import { formatDate } from '../../utilities/formatDate.js'
|
||||
@@ -32,12 +35,20 @@ export const DocumentControls: React.FC<{
|
||||
readonly apiURL: string
|
||||
readonly data?: any
|
||||
readonly disableActions?: boolean
|
||||
readonly disableCreate?: boolean
|
||||
readonly hasPublishPermission?: boolean
|
||||
readonly hasSavePermission?: boolean
|
||||
id?: number | string
|
||||
readonly isAccountView?: boolean
|
||||
readonly isEditing?: boolean
|
||||
readonly onDelete?: DocumentInfoContext['onDelete']
|
||||
readonly onDrawerCreate?: () => void
|
||||
/* Only available if `redirectAfterDuplicate` is `false` */
|
||||
readonly onDuplicate?: DocumentInfoContext['onDuplicate']
|
||||
readonly onSave?: DocumentInfoContext['onSave']
|
||||
readonly permissions: CollectionPermission | GlobalPermission | null
|
||||
readonly redirectAfterDelete?: boolean
|
||||
readonly redirectAfterDuplicate?: boolean
|
||||
readonly slug: SanitizedCollectionConfig['slug']
|
||||
}> = (props) => {
|
||||
const {
|
||||
@@ -45,14 +56,22 @@ export const DocumentControls: React.FC<{
|
||||
slug,
|
||||
data,
|
||||
disableActions,
|
||||
disableCreate,
|
||||
hasSavePermission,
|
||||
isAccountView,
|
||||
isEditing,
|
||||
onDelete,
|
||||
onDrawerCreate,
|
||||
onDuplicate,
|
||||
permissions,
|
||||
redirectAfterDelete,
|
||||
redirectAfterDuplicate,
|
||||
} = props
|
||||
|
||||
const { i18n } = useTranslation()
|
||||
|
||||
const editDepth = useEditDepth()
|
||||
|
||||
const { config, getEntityConfig } = useConfig()
|
||||
|
||||
const collectionConfig = getEntityConfig({ collectionSlug: slug }) as ClientCollectionConfig
|
||||
@@ -218,18 +237,30 @@ export const DocumentControls: React.FC<{
|
||||
<PopupList.ButtonGroup>
|
||||
{hasCreatePermission && (
|
||||
<React.Fragment>
|
||||
<PopupList.Button
|
||||
href={formatAdminURL({
|
||||
adminRoute,
|
||||
path: `/collections/${collectionConfig?.slug}/create`,
|
||||
})}
|
||||
id="action-create"
|
||||
>
|
||||
{i18n.t('general:createNew')}
|
||||
</PopupList.Button>
|
||||
{!disableCreate && (
|
||||
<Fragment>
|
||||
{editDepth > 1 ? (
|
||||
<PopupList.Button id="action-create" onClick={onDrawerCreate}>
|
||||
{i18n.t('general:createNew')}
|
||||
</PopupList.Button>
|
||||
) : (
|
||||
<PopupList.Button
|
||||
href={formatAdminURL({
|
||||
adminRoute,
|
||||
path: `/collections/${collectionConfig?.slug}/create`,
|
||||
})}
|
||||
id="action-create"
|
||||
>
|
||||
{i18n.t('general:createNew')}
|
||||
</PopupList.Button>
|
||||
)}
|
||||
</Fragment>
|
||||
)}
|
||||
{!collectionConfig.disableDuplicate && isEditing && (
|
||||
<DuplicateDocument
|
||||
id={id.toString()}
|
||||
onDuplicate={onDuplicate}
|
||||
redirectAfterDuplicate={redirectAfterDuplicate}
|
||||
singularLabel={collectionConfig?.labels?.singular}
|
||||
slug={collectionConfig?.slug}
|
||||
/>
|
||||
@@ -241,6 +272,8 @@ export const DocumentControls: React.FC<{
|
||||
buttonId="action-delete"
|
||||
collectionSlug={collectionConfig?.slug}
|
||||
id={id.toString()}
|
||||
onDelete={onDelete}
|
||||
redirectAfterDelete={redirectAfterDelete}
|
||||
singularLabel={collectionConfig?.labels?.singular}
|
||||
useAsTitle={collectionConfig?.admin?.useAsTitle}
|
||||
/>
|
||||
|
||||
@@ -22,9 +22,14 @@ export const DocumentDrawerContent: React.FC<DocumentDrawerProps> = ({
|
||||
id: existingDocID,
|
||||
AfterFields,
|
||||
collectionSlug,
|
||||
disableActions,
|
||||
drawerSlug,
|
||||
Header,
|
||||
onDelete: onDeleteFromProps,
|
||||
onDuplicate: onDuplicateFromProps,
|
||||
onSave: onSaveFromProps,
|
||||
redirectAfterDelete,
|
||||
redirectAfterDuplicate,
|
||||
}) => {
|
||||
const { config } = useConfig()
|
||||
|
||||
@@ -74,6 +79,34 @@ export const DocumentDrawerContent: React.FC<DocumentDrawerProps> = ({
|
||||
[onSaveFromProps, collectionConfig],
|
||||
)
|
||||
|
||||
const onDuplicate = useCallback<DocumentDrawerProps['onSave']>(
|
||||
(args) => {
|
||||
setDocID(args.doc.id)
|
||||
|
||||
if (typeof onDuplicateFromProps === 'function') {
|
||||
void onDuplicateFromProps({
|
||||
...args,
|
||||
collectionConfig,
|
||||
})
|
||||
}
|
||||
},
|
||||
[onDuplicateFromProps, collectionConfig],
|
||||
)
|
||||
|
||||
const onDelete = useCallback<DocumentDrawerProps['onDelete']>(
|
||||
(args) => {
|
||||
if (typeof onDeleteFromProps === 'function') {
|
||||
void onDeleteFromProps({
|
||||
...args,
|
||||
collectionConfig,
|
||||
})
|
||||
}
|
||||
|
||||
closeModal(drawerSlug)
|
||||
},
|
||||
[onDeleteFromProps, collectionConfig, closeModal, drawerSlug],
|
||||
)
|
||||
|
||||
return (
|
||||
<DocumentInfoProvider
|
||||
AfterFields={AfterFields}
|
||||
@@ -100,12 +133,19 @@ export const DocumentDrawerContent: React.FC<DocumentDrawerProps> = ({
|
||||
</Gutter>
|
||||
}
|
||||
collectionSlug={collectionConfig.slug}
|
||||
disableActions
|
||||
disableActions={disableActions}
|
||||
disableLeaveWithoutSaving
|
||||
id={docID}
|
||||
isEditing={isEditing}
|
||||
onDelete={onDelete}
|
||||
onDrawerCreate={() => {
|
||||
setDocID(null)
|
||||
}}
|
||||
onDuplicate={onDuplicate}
|
||||
onLoadError={onLoadError}
|
||||
onSave={onSave}
|
||||
redirectAfterDelete={redirectAfterDelete !== undefined ? redirectAfterDelete : false}
|
||||
redirectAfterDuplicate={redirectAfterDuplicate !== undefined ? redirectAfterDuplicate : false}
|
||||
>
|
||||
<RenderComponent mappedComponent={Edit} />
|
||||
</DocumentInfoProvider>
|
||||
|
||||
@@ -7,9 +7,14 @@ import type { Props as DrawerProps } from '../Drawer/types.js'
|
||||
export type DocumentDrawerProps = {
|
||||
readonly AfterFields?: React.ReactNode
|
||||
readonly collectionSlug: string
|
||||
readonly disableActions?: boolean
|
||||
readonly drawerSlug?: string
|
||||
readonly id?: null | number | string
|
||||
readonly onDelete?: DocumentInfoContext['onDelete']
|
||||
readonly onDuplicate?: DocumentInfoContext['onDuplicate']
|
||||
readonly onSave?: DocumentInfoContext['onSave']
|
||||
readonly redirectAfterDelete?: boolean
|
||||
readonly redirectAfterDuplicate?: boolean
|
||||
} & Pick<DrawerProps, 'Header'>
|
||||
|
||||
export type DocumentTogglerProps = {
|
||||
|
||||
@@ -11,7 +11,8 @@ import { Gutter } from '../Gutter/index.js'
|
||||
import './index.scss'
|
||||
|
||||
const baseClass = 'drawer'
|
||||
const zBase = 100
|
||||
|
||||
export const drawerZBase = 100
|
||||
|
||||
export const formatDrawerSlug = ({ slug, depth }: { depth: number; slug: string }): string =>
|
||||
`drawer_${depth}_${slug}`
|
||||
@@ -83,7 +84,7 @@ export const Drawer: React.FC<Props> = ({
|
||||
.join(' ')}
|
||||
slug={slug}
|
||||
style={{
|
||||
zIndex: zBase + drawerDepth,
|
||||
zIndex: drawerZBase + drawerDepth,
|
||||
}}
|
||||
>
|
||||
{(!drawerDepth || drawerDepth === 1) && <div className={`${baseClass}__blur-bg`} />}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
'use client'
|
||||
|
||||
import type { SanitizedCollectionConfig } from 'payload'
|
||||
import type { ClientCollectionConfig, SanitizedCollectionConfig } from 'payload'
|
||||
|
||||
import { Modal, useModal } from '@faceless-ui/modal'
|
||||
import { getTranslation } from '@payloadcms/translations'
|
||||
@@ -8,25 +8,37 @@ import { useRouter } from 'next/navigation.js'
|
||||
import React, { useCallback, useState } from 'react'
|
||||
import { toast } from 'sonner'
|
||||
|
||||
import type { DocumentInfoContext } from '../../providers/DocumentInfo/types.js'
|
||||
|
||||
import { useForm, useFormModified } from '../../forms/Form/context.js'
|
||||
import { useConfig } from '../../providers/Config/index.js'
|
||||
import { useEditDepth } from '../../providers/EditDepth/index.js'
|
||||
import { useLocale } from '../../providers/Locale/index.js'
|
||||
import { useTranslation } from '../../providers/Translation/index.js'
|
||||
import { requests } from '../../utilities/api.js'
|
||||
import { formatAdminURL } from '../../utilities/formatAdminURL.js'
|
||||
import { Button } from '../Button/index.js'
|
||||
import { drawerZBase } from '../Drawer/index.js'
|
||||
import { PopupList } from '../Popup/index.js'
|
||||
import './index.scss'
|
||||
|
||||
const baseClass = 'duplicate'
|
||||
|
||||
export type Props = {
|
||||
id: string
|
||||
singularLabel: SanitizedCollectionConfig['labels']['singular']
|
||||
slug: string
|
||||
readonly id: string
|
||||
readonly onDuplicate?: DocumentInfoContext['onDuplicate']
|
||||
readonly redirectAfterDuplicate?: boolean
|
||||
readonly singularLabel: SanitizedCollectionConfig['labels']['singular']
|
||||
readonly slug: string
|
||||
}
|
||||
|
||||
export const DuplicateDocument: React.FC<Props> = ({ id, slug, singularLabel }) => {
|
||||
export const DuplicateDocument: React.FC<Props> = ({
|
||||
id,
|
||||
slug,
|
||||
onDuplicate,
|
||||
redirectAfterDuplicate = true,
|
||||
singularLabel,
|
||||
}) => {
|
||||
const router = useRouter()
|
||||
const modified = useFormModified()
|
||||
const { toggleModal } = useModal()
|
||||
@@ -38,13 +50,18 @@ export const DuplicateDocument: React.FC<Props> = ({ id, slug, singularLabel })
|
||||
routes: { admin: adminRoute, api: apiRoute },
|
||||
serverURL,
|
||||
},
|
||||
getEntityConfig,
|
||||
} = useConfig()
|
||||
|
||||
const collectionConfig = getEntityConfig({ collectionSlug: slug }) as ClientCollectionConfig
|
||||
|
||||
const [hasClicked, setHasClicked] = useState<boolean>(false)
|
||||
const { i18n, t } = useTranslation()
|
||||
|
||||
const modalSlug = `duplicate-${id}`
|
||||
|
||||
const editDepth = useEditDepth()
|
||||
|
||||
const handleClick = useCallback(
|
||||
async (override = false) => {
|
||||
setHasClicked(true)
|
||||
@@ -72,13 +89,21 @@ export const DuplicateDocument: React.FC<Props> = ({ id, slug, singularLabel })
|
||||
message ||
|
||||
t('general:successfullyDuplicated', { label: getTranslation(singularLabel, i18n) }),
|
||||
)
|
||||
|
||||
setModified(false)
|
||||
router.push(
|
||||
formatAdminURL({
|
||||
adminRoute,
|
||||
path: `/collections/${slug}/${doc.id}${locale?.code ? `?locale=${locale.code}` : ''}`,
|
||||
}),
|
||||
)
|
||||
|
||||
if (redirectAfterDuplicate) {
|
||||
router.push(
|
||||
formatAdminURL({
|
||||
adminRoute,
|
||||
path: `/collections/${slug}/${doc.id}${locale?.code ? `?locale=${locale.code}` : ''}`,
|
||||
}),
|
||||
)
|
||||
}
|
||||
|
||||
if (typeof onDuplicate === 'function') {
|
||||
void onDuplicate({ collectionConfig, doc })
|
||||
}
|
||||
} else {
|
||||
toast.error(
|
||||
errors?.[0].message ||
|
||||
@@ -100,9 +125,12 @@ export const DuplicateDocument: React.FC<Props> = ({ id, slug, singularLabel })
|
||||
modalSlug,
|
||||
t,
|
||||
singularLabel,
|
||||
onDuplicate,
|
||||
redirectAfterDuplicate,
|
||||
setModified,
|
||||
router,
|
||||
adminRoute,
|
||||
collectionConfig,
|
||||
],
|
||||
)
|
||||
|
||||
@@ -117,7 +145,13 @@ export const DuplicateDocument: React.FC<Props> = ({ id, slug, singularLabel })
|
||||
{t('general:duplicate')}
|
||||
</PopupList.Button>
|
||||
{modified && hasClicked && (
|
||||
<Modal className={`${baseClass}__modal`} slug={modalSlug}>
|
||||
<Modal
|
||||
className={`${baseClass}__modal`}
|
||||
slug={modalSlug}
|
||||
style={{
|
||||
zIndex: drawerZBase + editDepth,
|
||||
}}
|
||||
>
|
||||
<div className={`${baseClass}__wrapper`}>
|
||||
<div className={`${baseClass}__content`}>
|
||||
<h1>{t('general:confirmDuplication')}</h1>
|
||||
|
||||
@@ -1,19 +1,21 @@
|
||||
import type { CommonProps, GroupBase, Props as ReactSelectStateManagerProps } from 'react-select'
|
||||
|
||||
import type { DocumentDrawerProps } from '../DocumentDrawer/types.js'
|
||||
import type { DocumentDrawerProps, UseDocumentDrawer } from '../DocumentDrawer/types.js'
|
||||
|
||||
type CustomSelectProps = {
|
||||
disableKeyDown?: boolean
|
||||
disableMouseDown?: boolean
|
||||
DocumentDrawerToggler?: ReturnType<UseDocumentDrawer>[1]
|
||||
draggableProps?: any
|
||||
droppableRef?: React.RefObject<HTMLDivElement | null>
|
||||
onDelete?: DocumentDrawerProps['onDelete']
|
||||
onDocumentDrawerOpen: (args: {
|
||||
collectionSlug: string
|
||||
hasReadPermission: boolean
|
||||
id: number | string
|
||||
}) => void
|
||||
onDuplicate?: DocumentDrawerProps['onSave']
|
||||
onSave?: DocumentDrawerProps['onSave']
|
||||
setDrawerIsOpen?: (isOpen: boolean) => void
|
||||
}
|
||||
|
||||
// augment the types for the `Select` component from `react-select`
|
||||
|
||||
@@ -251,8 +251,6 @@ const RelationshipFieldComponent: RelationshipFieldClientComponent = (props) =>
|
||||
dispatchOptions({
|
||||
type: 'ADD',
|
||||
collection,
|
||||
// TODO: fix this
|
||||
// @ts-expect-error-next-line
|
||||
config,
|
||||
docs: data.docs,
|
||||
i18n,
|
||||
@@ -264,8 +262,6 @@ const RelationshipFieldComponent: RelationshipFieldClientComponent = (props) =>
|
||||
dispatchOptions({
|
||||
type: 'ADD',
|
||||
collection,
|
||||
// TODO: fix this
|
||||
// @ts-expect-error-next-line
|
||||
config,
|
||||
docs: [],
|
||||
i18n,
|
||||
@@ -380,8 +376,6 @@ const RelationshipFieldComponent: RelationshipFieldClientComponent = (props) =>
|
||||
dispatchOptions({
|
||||
type: 'ADD',
|
||||
collection,
|
||||
// TODO: fix this
|
||||
// @ts-expect-error-next-line
|
||||
config,
|
||||
docs,
|
||||
i18n,
|
||||
@@ -458,14 +452,91 @@ const RelationshipFieldComponent: RelationshipFieldClientComponent = (props) =>
|
||||
dispatchOptions({
|
||||
type: 'UPDATE',
|
||||
collection: args.collectionConfig,
|
||||
// TODO: fix this
|
||||
// @ts-expect-error-next-line
|
||||
config,
|
||||
doc: args.doc,
|
||||
i18n,
|
||||
})
|
||||
|
||||
if (hasMany) {
|
||||
setValue(
|
||||
valueRef.current
|
||||
? (valueRef.current as Option[]).map((option) => {
|
||||
if (option.value === args.doc.id) {
|
||||
return {
|
||||
relationTo: args.collectionConfig.slug,
|
||||
value: args.doc.id,
|
||||
}
|
||||
}
|
||||
|
||||
return option
|
||||
})
|
||||
: null,
|
||||
)
|
||||
} else {
|
||||
setValue({
|
||||
relationTo: args.collectionConfig.slug,
|
||||
value: args.doc.id,
|
||||
})
|
||||
}
|
||||
},
|
||||
[i18n, config],
|
||||
[i18n, config, hasMany, setValue],
|
||||
)
|
||||
|
||||
const onDuplicate = useCallback<DocumentDrawerProps['onDuplicate']>(
|
||||
(args) => {
|
||||
dispatchOptions({
|
||||
type: 'ADD',
|
||||
collection: args.collectionConfig,
|
||||
config,
|
||||
docs: [args.doc],
|
||||
i18n,
|
||||
sort: true,
|
||||
})
|
||||
|
||||
if (hasMany) {
|
||||
setValue(
|
||||
valueRef.current
|
||||
? (valueRef.current as Option[]).concat({
|
||||
relationTo: args.collectionConfig.slug,
|
||||
value: args.doc.id,
|
||||
} as Option)
|
||||
: null,
|
||||
)
|
||||
} else {
|
||||
setValue({
|
||||
relationTo: args.collectionConfig.slug,
|
||||
value: args.doc.id,
|
||||
})
|
||||
}
|
||||
},
|
||||
[i18n, config, hasMany, setValue],
|
||||
)
|
||||
|
||||
const onDelete = useCallback<DocumentDrawerProps['onDelete']>(
|
||||
(args) => {
|
||||
dispatchOptions({
|
||||
id: args.id,
|
||||
type: 'REMOVE',
|
||||
collection: args.collectionConfig,
|
||||
config,
|
||||
i18n,
|
||||
})
|
||||
|
||||
if (hasMany) {
|
||||
setValue(
|
||||
valueRef.current
|
||||
? (valueRef.current as Option[]).filter((option) => {
|
||||
return option.value !== args.id
|
||||
})
|
||||
: null,
|
||||
)
|
||||
} else {
|
||||
setValue(null)
|
||||
}
|
||||
|
||||
return
|
||||
},
|
||||
[i18n, config, hasMany, setValue],
|
||||
)
|
||||
|
||||
const filterOption = useCallback((item: Option, searchFilter: string) => {
|
||||
@@ -657,7 +728,7 @@ const RelationshipFieldComponent: RelationshipFieldClientComponent = (props) =>
|
||||
/>
|
||||
</div>
|
||||
{currentlyOpenRelationship.collectionSlug && currentlyOpenRelationship.hasReadPermission && (
|
||||
<DocumentDrawer onSave={onSave} />
|
||||
<DocumentDrawer onDelete={onDelete} onDuplicate={onDuplicate} onSave={onSave} />
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
|
||||
@@ -146,6 +146,27 @@ export const optionsReducer = (state: OptionGroup[], action: Action): OptionGrou
|
||||
return newOptions
|
||||
}
|
||||
|
||||
case 'REMOVE': {
|
||||
const { id, collection } = action
|
||||
|
||||
const newOptions = [...state]
|
||||
|
||||
const indexOfGroup = newOptions.findIndex(
|
||||
(optionGroup) => optionGroup.label === collection.labels.plural,
|
||||
)
|
||||
|
||||
if (indexOfGroup === -1) {
|
||||
return newOptions
|
||||
}
|
||||
|
||||
newOptions[indexOfGroup] = {
|
||||
...newOptions[indexOfGroup],
|
||||
options: newOptions[indexOfGroup].options.filter((option) => option.value !== id),
|
||||
}
|
||||
|
||||
return newOptions
|
||||
}
|
||||
|
||||
default: {
|
||||
return state
|
||||
}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import type { I18nClient } from '@payloadcms/translations'
|
||||
import type { ClientCollectionConfig, FilterOptionsResult, SanitizedConfig } from 'payload'
|
||||
import type { ClientCollectionConfig, ClientConfig, FilterOptionsResult } from 'payload'
|
||||
|
||||
export type Option = {
|
||||
label: string
|
||||
@@ -27,7 +27,7 @@ type CLEAR = {
|
||||
|
||||
type UPDATE = {
|
||||
collection: ClientCollectionConfig
|
||||
config: SanitizedConfig
|
||||
config: ClientConfig
|
||||
doc: any
|
||||
i18n: I18nClient
|
||||
type: 'UPDATE'
|
||||
@@ -35,7 +35,7 @@ type UPDATE = {
|
||||
|
||||
type ADD = {
|
||||
collection: ClientCollectionConfig
|
||||
config: SanitizedConfig
|
||||
config: ClientConfig
|
||||
docs: any[]
|
||||
i18n: I18nClient
|
||||
ids?: (number | string)[]
|
||||
@@ -43,7 +43,15 @@ type ADD = {
|
||||
type: 'ADD'
|
||||
}
|
||||
|
||||
export type Action = ADD | CLEAR | UPDATE
|
||||
type REMOVE = {
|
||||
collection: ClientCollectionConfig
|
||||
config: ClientConfig
|
||||
i18n: I18nClient
|
||||
id: string
|
||||
type: 'REMOVE'
|
||||
}
|
||||
|
||||
export type Action = ADD | CLEAR | REMOVE | UPDATE
|
||||
|
||||
export type GetResults = (args: {
|
||||
filterOptions?: FilterOptionsResult
|
||||
|
||||
@@ -580,7 +580,7 @@ const DocumentInfo: React.FC<
|
||||
|
||||
export const DocumentInfoProvider: React.FC<
|
||||
{
|
||||
children: React.ReactNode
|
||||
readonly children: React.ReactNode
|
||||
} & DocumentInfoProps
|
||||
> = (props) => {
|
||||
return (
|
||||
|
||||
@@ -24,6 +24,7 @@ export type DocumentInfoProps = {
|
||||
BeforeFields?: React.ReactNode
|
||||
collectionSlug?: SanitizedCollectionConfig['slug']
|
||||
disableActions?: boolean
|
||||
disableCreate?: boolean
|
||||
disableLeaveWithoutSaving?: boolean
|
||||
docPermissions?: DocumentPermissions
|
||||
globalSlug?: SanitizedGlobalConfig['slug']
|
||||
@@ -33,8 +34,25 @@ export type DocumentInfoProps = {
|
||||
initialData?: Data
|
||||
initialState?: FormState
|
||||
isEditing?: boolean
|
||||
onDelete?: (args: {
|
||||
collectionConfig?: ClientCollectionConfig
|
||||
id: string
|
||||
}) => Promise<void> | void
|
||||
onDrawerCreate?: () => void
|
||||
/* only available if `redirectAfterDuplicate` is `false` */
|
||||
onDuplicate?: (args: {
|
||||
collectionConfig?: ClientCollectionConfig
|
||||
doc: TypeWithID
|
||||
}) => Promise<void> | void
|
||||
onLoadError?: (data?: any) => Promise<void> | void
|
||||
onSave?: (data: Data) => Promise<void> | void
|
||||
onSave?: (args: {
|
||||
collectionConfig?: ClientCollectionConfig
|
||||
doc: TypeWithID
|
||||
operation: 'create' | 'update'
|
||||
result: Data
|
||||
}) => Promise<void> | void
|
||||
redirectAfterDelete?: boolean
|
||||
redirectAfterDuplicate?: boolean
|
||||
}
|
||||
|
||||
export type DocumentInfoContext = {
|
||||
|
||||
@@ -3,6 +3,7 @@ import type { TypeWithID } from 'payload'
|
||||
|
||||
import { expect, test } from '@playwright/test'
|
||||
import { devUser } from 'credentials.js'
|
||||
import { openDocControls } from 'helpers/e2e/openDocControls.js'
|
||||
import path from 'path'
|
||||
import { fileURLToPath } from 'url'
|
||||
|
||||
@@ -21,7 +22,6 @@ import {
|
||||
getRoutes,
|
||||
initPageConsoleErrorCatch,
|
||||
login,
|
||||
openDocControls,
|
||||
openNav,
|
||||
saveDocAndAssert,
|
||||
} from '../helpers.js'
|
||||
|
||||
@@ -12,7 +12,6 @@ import {
|
||||
exactText,
|
||||
getRoutes,
|
||||
initPageConsoleErrorCatch,
|
||||
openDocControls,
|
||||
openNav,
|
||||
saveDocAndAssert,
|
||||
saveDocHotkeyAndAssert,
|
||||
@@ -61,6 +60,7 @@ const description = 'Description'
|
||||
let payload: PayloadTestSDK<Config>
|
||||
|
||||
import { navigateToDoc } from 'helpers/e2e/navigateToDoc.js'
|
||||
import { openDocControls } from 'helpers/e2e/openDocControls.js'
|
||||
import path from 'path'
|
||||
import { fileURLToPath } from 'url'
|
||||
|
||||
|
||||
@@ -21,6 +21,7 @@ export const seed = async (_payload) => {
|
||||
await new Promise((resolve, reject) => {
|
||||
_payload.db?.collections[coll.slug]?.ensureIndexes(function (err) {
|
||||
if (err) {
|
||||
// eslint-disable-next-line @typescript-eslint/prefer-promise-reject-errors
|
||||
reject(err)
|
||||
}
|
||||
resolve(true)
|
||||
@@ -42,12 +43,12 @@ export const seed = async (_payload) => {
|
||||
depth: 0,
|
||||
overrideAccess: true,
|
||||
}),
|
||||
...[...Array(11)].map(() => async () => {
|
||||
...[...Array(11)].map((_, i) => async () => {
|
||||
const postDoc = await _payload.create({
|
||||
collection: postsCollectionSlug,
|
||||
data: {
|
||||
description: 'Description',
|
||||
title: 'Title',
|
||||
title: `Post ${i + 1}`,
|
||||
},
|
||||
depth: 0,
|
||||
overrideAccess: true,
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import type { Page } from '@playwright/test'
|
||||
|
||||
import { expect, test } from '@playwright/test'
|
||||
import { openDocControls } from 'helpers/e2e/openDocControls.js'
|
||||
import path from 'path'
|
||||
import { wait } from 'payload/shared'
|
||||
import { fileURLToPath } from 'url'
|
||||
@@ -21,7 +22,6 @@ import {
|
||||
ensureCompilationIsDone,
|
||||
initPageConsoleErrorCatch,
|
||||
openCreateDocDrawer,
|
||||
openDocControls,
|
||||
openDocDrawer,
|
||||
saveDocAndAssert,
|
||||
} from '../helpers.js'
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
import type { Page } from '@playwright/test'
|
||||
|
||||
import { expect, test } from '@playwright/test'
|
||||
import { navigateToDoc } from 'helpers/e2e/navigateToDoc.js'
|
||||
import { openDocControls } from 'helpers/e2e/openDocControls.js'
|
||||
import path from 'path'
|
||||
import { wait } from 'payload/shared'
|
||||
import { fileURLToPath } from 'url'
|
||||
@@ -13,6 +15,7 @@ import {
|
||||
exactText,
|
||||
initPageConsoleErrorCatch,
|
||||
openCreateDocDrawer,
|
||||
openDocDrawer,
|
||||
saveDocAndAssert,
|
||||
saveDocHotkeyAndAssert,
|
||||
} from '../../../helpers.js'
|
||||
@@ -238,7 +241,7 @@ describe('relationship', () => {
|
||||
})
|
||||
|
||||
// Related issue: https://github.com/payloadcms/payload/issues/2815
|
||||
test('should modify fields in relationship drawer', async () => {
|
||||
test('should edit document in relationship drawer', async () => {
|
||||
await page.goto(url.create)
|
||||
await page.waitForURL(`**/${url.create}`)
|
||||
// First fill out the relationship field, as it's required
|
||||
@@ -269,9 +272,12 @@ describe('relationship', () => {
|
||||
// Now open the drawer again to edit the `text` field _using the keyboard_
|
||||
// Mimic real user behavior by typing into the field with spaces and backspaces
|
||||
// Explicitly use both `down` and `type` to cover edge cases
|
||||
await page
|
||||
.locator('#field-relationshipHasMany button.relationship--multi-value-label__drawer-toggler')
|
||||
.click()
|
||||
|
||||
await openDocDrawer(
|
||||
page,
|
||||
'#field-relationshipHasMany button.relationship--multi-value-label__drawer-toggler',
|
||||
)
|
||||
|
||||
await page.locator('[id^=doc-drawer_text-fields_1_] #field-text').click()
|
||||
await page.keyboard.down('1')
|
||||
await page.keyboard.type('23')
|
||||
@@ -303,7 +309,7 @@ describe('relationship', () => {
|
||||
// Drawers opened through the edit button are prone to issues due to the use of stopPropagation for certain
|
||||
// events - specifically for drawers opened through the edit button. This test is to ensure that drawers
|
||||
// opened through the edit button can be saved using the hotkey.
|
||||
test('should save using hotkey in edit document drawer', async () => {
|
||||
test('should save using hotkey in document drawer', async () => {
|
||||
await page.goto(url.create)
|
||||
// First fill out the relationship field, as it's required
|
||||
await openCreateDocDrawer(page, '#field-relationship')
|
||||
@@ -333,14 +339,181 @@ describe('relationship', () => {
|
||||
},
|
||||
},
|
||||
})
|
||||
|
||||
const relationshipDocuments = await payload.find({
|
||||
collection: relationshipFieldsSlug,
|
||||
})
|
||||
|
||||
// The Seeded text document should now have a text field with value 'some updated text value',
|
||||
expect(seededTextDocument.docs.length).toEqual(1)
|
||||
|
||||
// but the relationship document should NOT exist, as the hotkey should have saved the drawer and not the parent page
|
||||
expect(relationshipDocuments.docs.length).toEqual(0)
|
||||
// NOTE: the value here represents the number of documents _before_ the test was run
|
||||
expect(relationshipDocuments.docs.length).toEqual(2)
|
||||
})
|
||||
|
||||
describe('should create document within document drawer', () => {
|
||||
test('has one', async () => {
|
||||
await navigateToDoc(page, url)
|
||||
|
||||
const originalValue = await page
|
||||
.locator('#field-relationship .relationship--single-value')
|
||||
.textContent()
|
||||
|
||||
await openDocDrawer(page, '#field-relationship .relationship--single-value__drawer-toggler')
|
||||
const drawer1Content = page.locator('[id^=doc-drawer_text-fields_1_] .drawer__content')
|
||||
const originalDrawerID = await drawer1Content.locator('.id-label').textContent()
|
||||
await openDocControls(drawer1Content)
|
||||
await drawer1Content.locator('#action-create').click()
|
||||
await wait(1000) // wait for /form-state to return
|
||||
const title = 'Created from drawer'
|
||||
await drawer1Content.locator('#field-text').fill(title)
|
||||
await saveDocAndAssert(page, '[id^=doc-drawer_text-fields_1_] .drawer__content #action-save')
|
||||
const newDrawerID = drawer1Content.locator('.id-label')
|
||||
await expect(newDrawerID).not.toHaveText(originalDrawerID)
|
||||
await page.locator('[id^=doc-drawer_text-fields_1_] .drawer__close').click()
|
||||
await page.locator('#field-relationship').scrollIntoViewIfNeeded()
|
||||
|
||||
await expect(
|
||||
page.locator('#field-relationship .relationship--single-value__text', {
|
||||
hasText: exactText(originalValue),
|
||||
}),
|
||||
).toBeHidden()
|
||||
|
||||
await expect(
|
||||
page.locator('#field-relationship .relationship--single-value__text', {
|
||||
hasText: exactText(title),
|
||||
}),
|
||||
).toBeVisible()
|
||||
|
||||
await page.locator('#field-relationship .rs__control').click()
|
||||
|
||||
await expect(
|
||||
page.locator('.rs__option', {
|
||||
hasText: exactText(title),
|
||||
}),
|
||||
).toBeVisible()
|
||||
})
|
||||
|
||||
test.skip('has many', async () => {})
|
||||
})
|
||||
|
||||
describe('should duplicate document within document drawer', () => {
|
||||
test('has one', async () => {
|
||||
await navigateToDoc(page, url)
|
||||
|
||||
await wait(500)
|
||||
const fieldControl = page.locator('#field-relationship .rs__control')
|
||||
const originalValue = await page
|
||||
.locator('#field-relationship .relationship--single-value__text')
|
||||
.textContent()
|
||||
|
||||
await fieldControl.click()
|
||||
|
||||
await expect(
|
||||
page.locator('.rs__option', {
|
||||
hasText: exactText(originalValue),
|
||||
}),
|
||||
).toBeVisible()
|
||||
|
||||
await openDocDrawer(page, '#field-relationship .relationship--single-value__drawer-toggler')
|
||||
const drawer1Content = page.locator('[id^=doc-drawer_text-fields_1_] .drawer__content')
|
||||
const originalID = await drawer1Content.locator('.id-label').textContent()
|
||||
const originalText = 'Text'
|
||||
await drawer1Content.locator('#field-text').fill(originalText)
|
||||
await saveDocAndAssert(page, '[id^=doc-drawer_text-fields_1_] .drawer__content #action-save')
|
||||
await openDocControls(drawer1Content)
|
||||
await drawer1Content.locator('#action-duplicate').click()
|
||||
const duplicateID = drawer1Content.locator('.id-label')
|
||||
await expect(duplicateID).not.toHaveText(originalID)
|
||||
await page.locator('[id^=doc-drawer_text-fields_1_] .drawer__close').click()
|
||||
await expect(page.locator('.payload-toast-container')).toContainText('successfully')
|
||||
await page.locator('#field-relationship').scrollIntoViewIfNeeded()
|
||||
|
||||
const newValue = `${originalText} - duplicate` // this is added via a `beforeDuplicate` hook
|
||||
|
||||
await expect(
|
||||
page.locator('#field-relationship .relationship--single-value__text', {
|
||||
hasText: exactText(originalValue),
|
||||
}),
|
||||
).toBeHidden()
|
||||
|
||||
await expect(
|
||||
page.locator('#field-relationship .relationship--single-value__text', {
|
||||
hasText: exactText(newValue),
|
||||
}),
|
||||
).toBeVisible()
|
||||
|
||||
await page.locator('#field-relationship .rs__control').click()
|
||||
|
||||
await expect(
|
||||
page.locator('.rs__option', {
|
||||
hasText: exactText(newValue),
|
||||
}),
|
||||
).toBeVisible()
|
||||
})
|
||||
|
||||
test.skip('has many', async () => {})
|
||||
})
|
||||
|
||||
describe('should delete document within document drawer', () => {
|
||||
test('has one', async () => {
|
||||
await navigateToDoc(page, url)
|
||||
|
||||
await wait(500)
|
||||
|
||||
const originalValue = await page
|
||||
.locator('#field-relationship .relationship--single-value__text')
|
||||
.textContent()
|
||||
|
||||
await page.locator('#field-relationship .rs__control').click()
|
||||
|
||||
await expect(
|
||||
page.locator('#field-relationship .rs__option', {
|
||||
hasText: exactText(originalValue),
|
||||
}),
|
||||
).toBeVisible()
|
||||
|
||||
await openDocDrawer(
|
||||
page,
|
||||
'#field-relationship button.relationship--single-value__drawer-toggler',
|
||||
)
|
||||
|
||||
const drawer1Content = page.locator('[id^=doc-drawer_text-fields_1_] .drawer__content')
|
||||
const originalID = await drawer1Content.locator('.id-label').textContent()
|
||||
await openDocControls(drawer1Content)
|
||||
await drawer1Content.locator('#action-delete').click()
|
||||
|
||||
await page
|
||||
.locator('[id^=delete-].payload__modal-item.delete-document[open] button#confirm-delete')
|
||||
.click()
|
||||
|
||||
await expect(drawer1Content).toBeHidden()
|
||||
|
||||
await expect(
|
||||
page.locator('#field-relationship .relationship--single-value__text'),
|
||||
).toBeHidden()
|
||||
|
||||
await expect(page.locator('#field-relationship .rs__placeholder')).toBeVisible()
|
||||
|
||||
await page.locator('#field-relationship .rs__control').click()
|
||||
|
||||
await wait(500)
|
||||
|
||||
await expect(
|
||||
page.locator('#field-relationship .rs__option', {
|
||||
hasText: exactText(originalValue),
|
||||
}),
|
||||
).toBeHidden()
|
||||
|
||||
await expect(
|
||||
page.locator('#field-relationship .rs__option', {
|
||||
hasText: exactText(`Untitled - ${originalID}`),
|
||||
}),
|
||||
).toBeHidden()
|
||||
})
|
||||
|
||||
test.skip('has many', async () => {})
|
||||
})
|
||||
|
||||
// TODO: Fix this. This test flakes due to react select
|
||||
|
||||
@@ -13,6 +13,9 @@ const TextFields: CollectionConfig = {
|
||||
name: 'text',
|
||||
type: 'text',
|
||||
required: true,
|
||||
hooks: {
|
||||
beforeDuplicate: [({ value }) => `${value} - duplicate`],
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'localizedText',
|
||||
|
||||
@@ -957,8 +957,15 @@ export interface RowField {
|
||||
title: string;
|
||||
field_with_width_a?: string | null;
|
||||
field_with_width_b?: string | null;
|
||||
field_with_width_30_percent?: string | null;
|
||||
field_with_width_60_percent?: string | null;
|
||||
field_with_width_20_percent?: string | null;
|
||||
field_within_collapsible_a?: string | null;
|
||||
field_within_collapsible_b?: string | null;
|
||||
field_20_percent_width_within_row_a?: string | null;
|
||||
no_set_width_within_row_b?: string | null;
|
||||
no_set_width_within_row_c?: string | null;
|
||||
field_20_percent_width_within_row_d?: string | null;
|
||||
updatedAt: string;
|
||||
createdAt: string;
|
||||
}
|
||||
|
||||
@@ -45,6 +45,7 @@ import {
|
||||
numberFieldsSlug,
|
||||
pointFieldsSlug,
|
||||
radioFieldsSlug,
|
||||
relationshipFieldsSlug,
|
||||
richTextFieldsSlug,
|
||||
selectFieldsSlug,
|
||||
tabsFieldsSlug,
|
||||
@@ -339,6 +340,37 @@ export const seed = async (_payload: Payload) => {
|
||||
overrideAccess: true,
|
||||
})
|
||||
|
||||
const relationshipField1 = await _payload.create({
|
||||
collection: relationshipFieldsSlug,
|
||||
data: {
|
||||
text: 'Relationship 1',
|
||||
relationship: {
|
||||
relationTo: textFieldsSlug,
|
||||
value: createdTextDoc.id,
|
||||
},
|
||||
},
|
||||
depth: 0,
|
||||
overrideAccess: true,
|
||||
})
|
||||
|
||||
try {
|
||||
await _payload.create({
|
||||
collection: relationshipFieldsSlug,
|
||||
data: {
|
||||
text: 'Relationship 2',
|
||||
relationToSelf: relationshipField1.id,
|
||||
relationship: {
|
||||
relationTo: textFieldsSlug,
|
||||
value: createdAnotherTextDoc.id,
|
||||
},
|
||||
},
|
||||
depth: 0,
|
||||
overrideAccess: true,
|
||||
})
|
||||
} catch (e) {
|
||||
console.error(e)
|
||||
}
|
||||
|
||||
await _payload.create({
|
||||
collection: lexicalFieldsSlug,
|
||||
data: lexicalDocWithRelId,
|
||||
|
||||
@@ -226,11 +226,6 @@ export async function closeNav(page: Page): Promise<void> {
|
||||
await expect(page.locator('.template-default.template-default--nav-open')).toBeHidden()
|
||||
}
|
||||
|
||||
export async function openDocControls(page: Page): Promise<void> {
|
||||
await page.locator('.doc-controls__popup >> .popup-button').click()
|
||||
await expect(page.locator('.doc-controls__popup >> .popup__content')).toBeVisible()
|
||||
}
|
||||
|
||||
export async function changeLocale(page: Page, newLocale: string) {
|
||||
await page.locator('.localizer >> button').first().click()
|
||||
await page
|
||||
|
||||
6
test/helpers/e2e/openDocControls.ts
Normal file
6
test/helpers/e2e/openDocControls.ts
Normal file
@@ -0,0 +1,6 @@
|
||||
import { type Locator, type Page, expect } from '@playwright/test'
|
||||
|
||||
export async function openDocControls(page: Locator | Page): Promise<void> {
|
||||
await page.locator('.doc-controls__popup >> .popup-button').click()
|
||||
await expect(page.locator('.doc-controls__popup >> .popup__content')).toBeVisible()
|
||||
}
|
||||
@@ -1,6 +1,7 @@
|
||||
import type { Page } from '@playwright/test'
|
||||
|
||||
import { expect, test } from '@playwright/test'
|
||||
import { openDocControls } from 'helpers/e2e/openDocControls.js'
|
||||
import path from 'path'
|
||||
import { wait } from 'payload/shared'
|
||||
import { fileURLToPath } from 'url'
|
||||
@@ -12,7 +13,6 @@ import {
|
||||
changeLocale,
|
||||
ensureCompilationIsDone,
|
||||
initPageConsoleErrorCatch,
|
||||
openDocControls,
|
||||
saveDocAndAssert,
|
||||
} from '../helpers.js'
|
||||
import { AdminUrlUtil } from '../helpers/adminUrlUtil.js'
|
||||
|
||||
Reference in New Issue
Block a user