Merge branch 'alpha' of github.com:payloadcms/payload into fix/alpha/rte-e2e-tests

This commit is contained in:
James
2024-03-29 13:51:29 -04:00
21 changed files with 223 additions and 119 deletions

View File

@@ -217,7 +217,7 @@ jobs:
# find test -type f -name 'e2e.spec.ts' | sort | xargs dirname | xargs -I {} basename {} # find test -type f -name 'e2e.spec.ts' | sort | xargs dirname | xargs -I {} basename {}
suite: suite:
- _community - _community
# - access-control - access-control
# - admin # - admin
- auth - auth
- field-error-states - field-error-states

View File

@@ -4,6 +4,7 @@ import type {
SanitizedCollectionConfig, SanitizedCollectionConfig,
SanitizedConfig, SanitizedConfig,
SanitizedGlobalConfig, SanitizedGlobalConfig,
VisibleEntities,
} from 'payload/types' } from 'payload/types'
import { initI18n } from '@payloadcms/translations' import { initI18n } from '@payloadcms/translations'
@@ -11,7 +12,7 @@ import { translations } from '@payloadcms/translations/client'
import { findLocaleFromCode } from '@payloadcms/ui/utilities/findLocaleFromCode' import { findLocaleFromCode } from '@payloadcms/ui/utilities/findLocaleFromCode'
import { headers as getHeaders } from 'next/headers.js' import { headers as getHeaders } from 'next/headers.js'
import { notFound, redirect } from 'next/navigation.js' import { notFound, redirect } from 'next/navigation.js'
import { createLocalReq } from 'payload/utilities' import { createLocalReq, isEntityHidden } from 'payload/utilities'
import qs from 'qs' import qs from 'qs'
import { getPayloadHMR } from '../utilities/getPayloadHMR.js' import { getPayloadHMR } from '../utilities/getPayloadHMR.js'
@@ -40,6 +41,15 @@ export const initPage = async ({
payload, payload,
}) })
const visibleEntities: VisibleEntities = {
collections: payload.config.collections
.map(({ slug, admin: { hidden } }) => (!isEntityHidden({ hidden, user }) ? slug : null))
.filter(Boolean),
globals: payload.config.globals
.map(({ slug, admin: { hidden } }) => (!isEntityHidden({ hidden, user }) ? slug : null))
.filter(Boolean),
}
const routeSegments = route.replace(payload.config.routes.admin, '').split('/').filter(Boolean) const routeSegments = route.replace(payload.config.routes.admin, '').split('/').filter(Boolean)
const [entityType, entitySlug, createOrID] = routeSegments const [entityType, entitySlug, createOrID] = routeSegments
const collectionSlug = entityType === 'collections' ? entitySlug : undefined const collectionSlug = entityType === 'collections' ? entitySlug : undefined
@@ -73,7 +83,7 @@ export const initPage = async ({
const queryString = `${qs.stringify(searchParams, { addQueryPrefix: true })}` const queryString = `${qs.stringify(searchParams, { addQueryPrefix: true })}`
const req = await createLocalReq( const req = createLocalReq(
{ {
fallbackLocale: null, fallbackLocale: null,
locale: locale.code, locale: locale.code,
@@ -118,5 +128,6 @@ export const initPage = async ({
permissions, permissions,
req, req,
translations: i18n.translations, translations: i18n.translations,
visibleEntities,
} }
} }

View File

@@ -1,6 +1,7 @@
'use client' 'use client'
import type { EntityToGroup, Group } from '@payloadcms/ui/utilities/groupNavItems' import type { EntityToGroup, Group } from '@payloadcms/ui/utilities/groupNavItems'
import type { Permissions } from 'payload/auth' import type { Permissions } from 'payload/auth'
import type { VisibleEntities } from 'payload/types'
import { getTranslation } from '@payloadcms/translations' import { getTranslation } from '@payloadcms/translations'
import { Button } from '@payloadcms/ui/elements/Button' import { Button } from '@payloadcms/ui/elements/Button'
@@ -19,9 +20,8 @@ const baseClass = 'dashboard'
export const DefaultDashboardClient: React.FC<{ export const DefaultDashboardClient: React.FC<{
Link: React.ComponentType Link: React.ComponentType
permissions: Permissions permissions: Permissions
visibleCollections: string[] visibleEntities: VisibleEntities
visibleGlobals: string[] }> = ({ Link, permissions, visibleEntities }) => {
}> = ({ Link, permissions, visibleCollections, visibleGlobals }) => {
const config = useConfig() const config = useConfig()
const { const {
@@ -40,13 +40,13 @@ export const DefaultDashboardClient: React.FC<{
const collections = collectionsConfig.filter( const collections = collectionsConfig.filter(
(collection) => (collection) =>
permissions?.collections?.[collection.slug]?.read?.permission && permissions?.collections?.[collection.slug]?.read?.permission &&
visibleCollections.includes(collection.slug), visibleEntities.collections.includes(collection.slug),
) )
const globals = globalsConfig.filter( const globals = globalsConfig.filter(
(global) => (global) =>
permissions?.globals?.[global.slug]?.read?.permission && permissions?.globals?.[global.slug]?.read?.permission &&
visibleGlobals.includes(global.slug), visibleEntities.globals.includes(global.slug),
) )
setGroups( setGroups(
@@ -73,15 +73,7 @@ export const DefaultDashboardClient: React.FC<{
i18n, i18n,
), ),
) )
}, [ }, [permissions, user, i18n, visibleEntities, collectionsConfig, globalsConfig])
permissions,
user,
i18n,
visibleCollections,
visibleGlobals,
collectionsConfig,
globalsConfig,
])
return ( return (
<Fragment> <Fragment>

View File

@@ -1,5 +1,5 @@
import type { Permissions } from 'payload/auth' import type { Permissions } from 'payload/auth'
import type { SanitizedConfig } from 'payload/types' import type { SanitizedConfig, VisibleEntities } from 'payload/types'
import { Gutter } from '@payloadcms/ui/elements/Gutter' import { Gutter } from '@payloadcms/ui/elements/Gutter'
import { SetStepNav } from '@payloadcms/ui/elements/StepNav' import { SetStepNav } from '@payloadcms/ui/elements/StepNav'
@@ -15,8 +15,7 @@ export type DashboardProps = {
Link: React.ComponentType<any> Link: React.ComponentType<any>
config: SanitizedConfig config: SanitizedConfig
permissions: Permissions permissions: Permissions
visibleCollections: string[] visibleEntities: VisibleEntities
visibleGlobals: string[]
} }
export const DefaultDashboard: React.FC<DashboardProps> = (props) => { export const DefaultDashboard: React.FC<DashboardProps> = (props) => {
@@ -28,8 +27,7 @@ export const DefaultDashboard: React.FC<DashboardProps> = (props) => {
}, },
}, },
permissions, permissions,
visibleCollections, visibleEntities,
visibleGlobals,
} = props } = props
return ( return (
@@ -42,8 +40,7 @@ export const DefaultDashboard: React.FC<DashboardProps> = (props) => {
<DefaultDashboardClient <DefaultDashboardClient
Link={Link} Link={Link}
permissions={permissions} permissions={permissions}
visibleCollections={visibleCollections} visibleEntities={visibleEntities}
visibleGlobals={visibleGlobals}
/> />
{Array.isArray(afterDashboard) && {Array.isArray(afterDashboard) &&
afterDashboard.map((Component, i) => <Component key={i} />)} afterDashboard.map((Component, i) => <Component key={i} />)}

View File

@@ -3,7 +3,6 @@ import type { AdminViewProps } from 'payload/types'
import { HydrateClientUser } from '@payloadcms/ui/elements/HydrateClientUser' import { HydrateClientUser } from '@payloadcms/ui/elements/HydrateClientUser'
import { RenderCustomComponent } from '@payloadcms/ui/elements/RenderCustomComponent' import { RenderCustomComponent } from '@payloadcms/ui/elements/RenderCustomComponent'
import LinkImport from 'next/link.js' import LinkImport from 'next/link.js'
import { isEntityHidden } from 'payload/utilities'
import React, { Fragment } from 'react' import React, { Fragment } from 'react'
import type { DashboardProps } from './Default/index.js' import type { DashboardProps } from './Default/index.js'
@@ -14,40 +13,23 @@ export { generateDashboardMetadata } from './meta.js'
const Link = (LinkImport.default || LinkImport) as unknown as typeof LinkImport.default const Link = (LinkImport.default || LinkImport) as unknown as typeof LinkImport.default
export const Dashboard: React.FC<AdminViewProps> = ({ export const Dashboard: React.FC<AdminViewProps> = ({ initPageResult }) => {
initPageResult,
// searchParams,
}) => {
const { const {
permissions, permissions,
req: { req: {
payload: { config }, payload: { config },
user, user,
}, },
visibleEntities,
} = initPageResult } = initPageResult
const CustomDashboardComponent = config.admin.components?.views?.Dashboard const CustomDashboardComponent = config.admin.components?.views?.Dashboard
const visibleCollections: string[] = config.collections.reduce((acc, collection) => {
if (!isEntityHidden({ hidden: collection.admin.hidden, user })) {
acc.push(collection.slug)
}
return acc
}, [])
const visibleGlobals: string[] = config.globals.reduce((acc, global) => {
if (!isEntityHidden({ hidden: global.admin.hidden, user })) {
acc.push(global.slug)
}
return acc
}, [])
const viewComponentProps: DashboardProps = { const viewComponentProps: DashboardProps = {
Link, Link,
config, config,
permissions, permissions,
visibleCollections, visibleEntities,
visibleGlobals,
} }
return ( return (

View File

@@ -124,7 +124,7 @@ export const DefaultEditView: React.FC = () => {
}) })
} }
if (!isEditing) { if (!isEditing && depth < 2) {
// Redirect to the same locale if it's been set // Redirect to the same locale if it's been set
const redirectRoute = `${adminRoute}/collections/${collectionSlug}/${json?.doc?.id}${locale ? `?locale=${locale}` : ''}` const redirectRoute = `${adminRoute}/collections/${collectionSlug}/${json?.doc?.id}${locale ? `?locale=${locale}` : ''}`
router.push(redirectRoute) router.push(redirectRoute)
@@ -144,6 +144,7 @@ export const DefaultEditView: React.FC = () => {
id, id,
entitySlug, entitySlug,
user, user,
depth,
collectionSlug, collectionSlug,
getVersions, getVersions,
getDocPermissions, getDocPermissions,

View File

@@ -7,12 +7,7 @@ import { NotFoundClient } from './index.client.js'
export const NotFoundView: AdminViewComponent = ({ initPageResult }) => { export const NotFoundView: AdminViewComponent = ({ initPageResult }) => {
return ( return (
<DefaultTemplate <DefaultTemplate config={initPageResult?.req?.payload.config}>
config={initPageResult?.req?.payload.config}
i18n={initPageResult?.req?.i18n}
permissions={initPageResult?.permissions}
user={initPageResult?.req?.user}
>
<NotFoundClient /> <NotFoundClient />
</DefaultTemplate> </DefaultTemplate>
) )

View File

@@ -2,6 +2,7 @@ import type { I18n } from '@payloadcms/translations'
import type { Metadata } from 'next' import type { Metadata } from 'next'
import type { SanitizedConfig } from 'payload/types' import type { SanitizedConfig } from 'payload/types'
import { EntityVisibilityProvider } from '@payloadcms/ui/providers/EntityVisibility'
import { DefaultTemplate } from '@payloadcms/ui/templates/Default' import { DefaultTemplate } from '@payloadcms/ui/templates/Default'
import { MinimalTemplate } from '@payloadcms/ui/templates/Minimal' import { MinimalTemplate } from '@payloadcms/ui/templates/Minimal'
import { notFound, redirect } from 'next/navigation.js' import { notFound, redirect } from 'next/navigation.js'
@@ -81,22 +82,14 @@ export const RootPage = async ({
<DefaultView initPageResult={initPageResult} params={params} searchParams={searchParams} /> <DefaultView initPageResult={initPageResult} params={params} searchParams={searchParams} />
) )
if (templateType === 'minimal') {
return <MinimalTemplate className={templateClassName}>{RenderedView}</MinimalTemplate>
}
if (templateType === 'default') {
return ( return (
<DefaultTemplate <EntityVisibilityProvider visibleEntities={initPageResult.visibleEntities}>
config={config} {templateType === 'minimal' && (
i18n={initPageResult.req.i18n} <MinimalTemplate className={templateClassName}>{RenderedView}</MinimalTemplate>
permissions={initPageResult.permissions} )}
user={initPageResult.req.user} {templateType === 'default' && (
> <DefaultTemplate config={config}>{RenderedView}</DefaultTemplate>
{RenderedView} )}
</DefaultTemplate> </EntityVisibilityProvider>
) )
} }
return RenderedView
}

View File

@@ -30,4 +30,5 @@ export type {
EditViewProps, EditViewProps,
InitPageResult, InitPageResult,
ServerSideEditViewProps, ServerSideEditViewProps,
VisibleEntities,
} from './views/types.js' } from './views/types.js'

View File

@@ -29,6 +29,11 @@ export type EditViewProps = {
globalSlug?: string globalSlug?: string
} }
export type VisibleEntities = {
collections: SanitizedCollectionConfig['slug'][]
globals: SanitizedGlobalConfig['slug'][]
}
export type InitPageResult = { export type InitPageResult = {
collectionConfig?: SanitizedCollectionConfig collectionConfig?: SanitizedCollectionConfig
cookies: Map<string, string> cookies: Map<string, string>
@@ -38,6 +43,7 @@ export type InitPageResult = {
permissions: Permissions permissions: Permissions
req: PayloadRequest req: PayloadRequest
translations: Translations translations: Translations
visibleEntities: VisibleEntities
} }
export type ServerSideEditViewProps = { export type ServerSideEditViewProps = {

View File

@@ -39,6 +39,7 @@ export const DocumentDrawerContent: React.FC<DocumentDrawerProps> = ({
const { closeModal, modalState, toggleModal } = useModal() const { closeModal, modalState, toggleModal } = useModal()
const locale = useLocale() const locale = useLocale()
const { t } = useTranslation() const { t } = useTranslation()
const [createdID, setCreatedID] = useState()
const [isOpen, setIsOpen] = useState(false) const [isOpen, setIsOpen] = useState(false)
const [collectionConfig] = useRelatedCollections(collectionSlug) const [collectionConfig] = useRelatedCollections(collectionSlug)
const { formQueryParams } = useFormQueryParams() const { formQueryParams } = useFormQueryParams()
@@ -69,6 +70,7 @@ export const DocumentDrawerContent: React.FC<DocumentDrawerProps> = ({
const onSave = useCallback<DocumentDrawerProps['onSave']>( const onSave = useCallback<DocumentDrawerProps['onSave']>(
(args) => { (args) => {
setCreatedID(args.doc.id)
if (typeof onSaveFromProps === 'function') { if (typeof onSaveFromProps === 'function') {
void onSaveFromProps({ void onSaveFromProps({
...args, ...args,
@@ -113,7 +115,7 @@ export const DocumentDrawerContent: React.FC<DocumentDrawerProps> = ({
// Same reason as above. We need to fully-fetch the docPreferences from the server. This is done in DocumentInfoProvider if we set it to null here. // Same reason as above. We need to fully-fetch the docPreferences from the server. This is done in DocumentInfoProvider if we set it to null here.
hasSavePermission={null} hasSavePermission={null}
// isLoading, // isLoading,
id={id} id={id || createdID}
isEditing={isEditing} isEditing={isEditing}
onLoadError={onLoadError} onLoadError={onLoadError}
onSave={onSave} onSave={onSave}

View File

@@ -1,7 +1,8 @@
'use client' 'use client'
import { getTranslation } from '@payloadcms/translations' import { getTranslation } from '@payloadcms/translations'
import { useEntityVisibility } from '@payloadcms/ui/providers/EntityVisibility'
import LinkWithDefault from 'next/link.js' import LinkWithDefault from 'next/link.js'
import { isEntityHidden } from 'payload/utilities'
import React from 'react' import React from 'react'
import type { EntityToGroup } from '../../utilities/groupNavItems.js' import type { EntityToGroup } from '../../utilities/groupNavItems.js'
@@ -17,7 +18,9 @@ import { useNav } from './context.js'
const baseClass = 'nav' const baseClass = 'nav'
export const DefaultNavClient: React.FC = () => { export const DefaultNavClient: React.FC = () => {
const { permissions, user } = useAuth() const { permissions } = useAuth()
const { isEntityVisible } = useEntityVisibility()
const { const {
collections, collections,
globals, globals,
@@ -30,8 +33,7 @@ export const DefaultNavClient: React.FC = () => {
const groups = groupNavItems( const groups = groupNavItems(
[ [
...collections ...collections
// @ts-expect-error todo: fix type error here .filter(({ slug }) => isEntityVisible({ collectionSlug: slug }))
.filter(({ admin: { hidden } }) => !isEntityHidden({ hidden, user }))
.map((collection) => { .map((collection) => {
const entityToGroup: EntityToGroup = { const entityToGroup: EntityToGroup = {
type: EntityType.collection, type: EntityType.collection,
@@ -41,8 +43,7 @@ export const DefaultNavClient: React.FC = () => {
return entityToGroup return entityToGroup
}), }),
...globals ...globals
// @ts-expect-error todo: fix type error here .filter(({ slug }) => isEntityVisible({ globalSlug: slug }))
.filter(({ admin: { hidden } }) => !isEntityHidden({ hidden, user }))
.map((global) => { .map((global) => {
const entityToGroup: EntityToGroup = { const entityToGroup: EntityToGroup = {
type: EntityType.global, type: EntityType.global,
@@ -85,7 +86,6 @@ export const DefaultNavClient: React.FC = () => {
return ( return (
<LinkElement <LinkElement
// activeClassName="active"
className={`${baseClass}__link`} className={`${baseClass}__link`}
href={href} href={href}
id={id} id={id}

View File

@@ -11,9 +11,11 @@ const baseClass = 'nav'
import { DefaultNavClient } from './index.client.js' import { DefaultNavClient } from './index.client.js'
export const DefaultNav: React.FC<{ export type NavProps = {
config: SanitizedConfig config: SanitizedConfig
}> = (props) => { }
export const DefaultNav: React.FC<NavProps> = (props) => {
const { config } = props const { config } = props
if (!config) { if (!config) {

View File

@@ -264,6 +264,7 @@ export const Form: React.FC<FormProps> = (props) => {
if (res.status < 400) { if (res.status < 400) {
if (typeof onSuccess === 'function') await onSuccess(json) if (typeof onSuccess === 'function') await onSuccess(json)
setSubmitted(false) setSubmitted(false)
setProcessing(false)
if (redirect) { if (redirect) {
router.push(redirect) router.push(redirect)
@@ -271,6 +272,8 @@ export const Form: React.FC<FormProps> = (props) => {
toast.success(json.message || t('general:submissionSuccessful'), { autoClose: 3000 }) toast.success(json.message || t('general:submissionSuccessful'), { autoClose: 3000 })
} }
} else { } else {
setProcessing(false)
contextRef.current = { ...contextRef.current } // triggers rerender of all components that subscribe to form contextRef.current = { ...contextRef.current } // triggers rerender of all components that subscribe to form
if (json.message) { if (json.message) {
toast.error(json.message) toast.error(json.message)
@@ -327,8 +330,6 @@ export const Form: React.FC<FormProps> = (props) => {
toast.error(message) toast.error(message)
} }
setProcessing(false)
} catch (err) { } catch (err) {
setProcessing(false) setProcessing(false)

View File

@@ -1,18 +1,24 @@
'use client' 'use client'
import React from 'react' import React from 'react'
type AddClientFunctionContextType = (func: any) => void type ModifyClientFunctionContextType = {
addClientFunction: (args: ModifyFunctionArgs) => void
removeClientFunction: (args: ModifyFunctionArgs) => void
}
type ClientFunctionsContextType = Record<string, any> type ClientFunctionsContextType = Record<string, any>
const AddClientFunctionContext = React.createContext<AddClientFunctionContextType>(() => null) const ModifyClientFunctionContext = React.createContext<ModifyClientFunctionContextType>({
addClientFunction: () => null,
removeClientFunction: () => null,
})
const ClientFunctionsContext = React.createContext<ClientFunctionsContextType>({}) const ClientFunctionsContext = React.createContext<ClientFunctionsContextType>({})
type AddFunctionArgs = { func: any; key: string } type ModifyFunctionArgs = { func: any; key: string }
export const ClientFunctionProvider: React.FC<{ children: React.ReactNode }> = ({ children }) => { export const ClientFunctionProvider: React.FC<{ children: React.ReactNode }> = ({ children }) => {
const [clientFunctions, setClientFunctions] = React.useState({}) const [clientFunctions, setClientFunctions] = React.useState({})
const addClientFunction = React.useCallback((args: AddFunctionArgs) => { const addClientFunction = React.useCallback((args: ModifyFunctionArgs) => {
setClientFunctions((state) => { setClientFunctions((state) => {
const newState = { ...state } const newState = { ...state }
newState[args.key] = args.func newState[args.key] = args.func
@@ -20,24 +26,44 @@ export const ClientFunctionProvider: React.FC<{ children: React.ReactNode }> = (
}) })
}, []) }, [])
const removeClientFunction = React.useCallback((args: ModifyFunctionArgs) => {
setClientFunctions((state) => {
const newState = { ...state }
delete newState[args.key]
return newState
})
}, [])
return ( return (
<AddClientFunctionContext.Provider value={addClientFunction}> <ModifyClientFunctionContext.Provider
value={{
addClientFunction,
removeClientFunction,
}}
>
<ClientFunctionsContext.Provider value={clientFunctions}> <ClientFunctionsContext.Provider value={clientFunctions}>
{children} {children}
</ClientFunctionsContext.Provider> </ClientFunctionsContext.Provider>
</AddClientFunctionContext.Provider> </ModifyClientFunctionContext.Provider>
) )
} }
export const useAddClientFunction = (key: string, func: any) => { export const useAddClientFunction = (key: string, func: any) => {
const addClientFunction = React.useContext(AddClientFunctionContext) const { addClientFunction, removeClientFunction } = React.useContext(ModifyClientFunctionContext)
React.useEffect(() => { React.useEffect(() => {
addClientFunction({ addClientFunction({
func, func,
key, key,
}) })
}, [func, key, addClientFunction])
return () => {
removeClientFunction({
func,
key,
})
}
}, [func, key, addClientFunction, removeClientFunction])
} }
export const useClientFunctions = () => { export const useClientFunctions = () => {

View File

@@ -0,0 +1,56 @@
'use client'
import type {
SanitizedCollectionConfig,
SanitizedGlobalConfig,
VisibleEntities,
} from 'packages/payload/src/exports/types.js'
import React, { createContext, useCallback, useContext } from 'react'
export type VisibleEntitiesContextType = {
isEntityVisible: ({
collectionSlug,
globalSlug,
}: {
collectionSlug?: SanitizedCollectionConfig['slug']
globalSlug?: SanitizedGlobalConfig['slug']
}) => boolean
visibleEntities: VisibleEntities
}
export const EntityVisibilityContext = createContext({} as VisibleEntitiesContextType)
export const EntityVisibilityProvider: React.FC<{
children: React.ReactNode
visibleEntities?: VisibleEntities
}> = ({ children, visibleEntities }) => {
const isEntityVisible = useCallback(
({
collectionSlug,
globalSlug,
}: {
collectionSlug: SanitizedCollectionConfig['slug']
globalSlug: SanitizedGlobalConfig['slug']
}) => {
if (collectionSlug) {
return visibleEntities.collections.includes(collectionSlug)
}
if (globalSlug) {
return visibleEntities.globals.includes(globalSlug)
}
return false
},
[visibleEntities],
)
return (
<EntityVisibilityContext.Provider value={{ isEntityVisible, visibleEntities }}>
{children}
</EntityVisibilityContext.Provider>
)
}
export const useEntityVisibility = (): VisibleEntitiesContextType =>
useContext(EntityVisibilityContext)

View File

@@ -1,16 +1,15 @@
import type { Permissions, User } from 'payload/auth'
import type { SanitizedConfig } from 'payload/types' import type { SanitizedConfig } from 'payload/types'
import React from 'react' import React from 'react'
import type { NavProps } from '../../elements/Nav/index.js'
import { AppHeader } from '../../elements/AppHeader/index.js' import { AppHeader } from '../../elements/AppHeader/index.js'
import { NavToggler } from '../../elements/Nav/NavToggler/index.js' import { NavToggler } from '../../elements/Nav/NavToggler/index.js'
import { DefaultNav } from '../../elements/Nav/index.js' import { DefaultNav } from '../../elements/Nav/index.js'
import { RenderCustomComponent } from '../../elements/RenderCustomComponent/index.js' import { RenderCustomComponent } from '../../elements/RenderCustomComponent/index.js'
import { NavHamburger } from './NavHamburger/index.js' import { NavHamburger } from './NavHamburger/index.js'
export { NavHamburger } from './NavHamburger/index.js'
import { Wrapper } from './Wrapper/index.js' import { Wrapper } from './Wrapper/index.js'
export { Wrapper } from './Wrapper/index.js'
import './index.scss' import './index.scss'
const baseClass = 'template-default' const baseClass = 'template-default'
@@ -19,18 +18,12 @@ export type DefaultTemplateProps = {
children?: React.ReactNode children?: React.ReactNode
className?: string className?: string
config: Promise<SanitizedConfig> | SanitizedConfig config: Promise<SanitizedConfig> | SanitizedConfig
i18n: any
permissions: Permissions
user: User
} }
export const DefaultTemplate: React.FC<DefaultTemplateProps> = async ({ export const DefaultTemplate: React.FC<DefaultTemplateProps> = async ({
children, children,
className, className,
config: configPromise, config: configPromise,
i18n,
permissions,
user,
}) => { }) => {
const config = await configPromise const config = await configPromise
@@ -42,7 +35,10 @@ export const DefaultTemplate: React.FC<DefaultTemplateProps> = async ({
} = {}, } = {},
} = config || {} } = config || {}
// #nav-toggler needs to be wrapped in a div, not Fragment. This fixes https://github.com/shadcn-ui/ui/issues/1355#issuecomment-1909192594 const navProps: NavProps = {
config,
}
return ( return (
<div> <div>
<div className={`${baseClass}__nav-toggler-wrapper`} id="nav-toggler"> <div className={`${baseClass}__nav-toggler-wrapper`} id="nav-toggler">
@@ -54,12 +50,7 @@ export const DefaultTemplate: React.FC<DefaultTemplateProps> = async ({
<RenderCustomComponent <RenderCustomComponent
CustomComponent={CustomNav} CustomComponent={CustomNav}
DefaultComponent={DefaultNav} DefaultComponent={DefaultNav}
componentProps={{ componentProps={navProps}
config,
i18n,
permissions,
user,
}}
/> />
<div className={`${baseClass}__wrap`}> <div className={`${baseClass}__wrap`}>
<AppHeader /> <AppHeader />

View File

@@ -17,6 +17,7 @@ import {
} from '../helpers.js' } from '../helpers.js'
import { AdminUrlUtil } from '../helpers/adminUrlUtil.js' import { AdminUrlUtil } from '../helpers/adminUrlUtil.js'
import { initPayloadE2E } from '../helpers/initPayloadE2E.js' import { initPayloadE2E } from '../helpers/initPayloadE2E.js'
import { POLL_TOPASS_TIMEOUT } from '../playwright.config.js'
import config from './config.js' import config from './config.js'
import { import {
docLevelAccessSlug, docLevelAccessSlug,
@@ -258,8 +259,11 @@ describe('access control', () => {
}) })
// TODO: Test flakes. In CI, test global does not appear in nav. Perhaps the checkbox setValue is not triggered BEFORE the document is saved, as the custom save button can be clicked even if the form has not been set to modified. // TODO: Test flakes. In CI, test global does not appear in nav. Perhaps the checkbox setValue is not triggered BEFORE the document is saved, as the custom save button can be clicked even if the form has not been set to modified.
test.skip('should show test global immediately after allowing access', async () => { test('should show test global immediately after allowing access', async () => {
await page.goto(`${serverURL}/admin/globals/settings`) const url = `${serverURL}/admin/globals/settings`
await page.goto(url)
await expect.poll(() => page.url(), { timeout: POLL_TOPASS_TIMEOUT }).toContain(url)
await openNav(page) await openNav(page)
@@ -278,8 +282,11 @@ describe('access control', () => {
await openNav(page) await openNav(page)
// Now test collection should appear in the menu. const globalTest = page.locator('#nav-global-test')
await expect(page.locator('#nav-global-test')).toBeVisible()
await expect(async () => await globalTest.isVisible()).toPass({
timeout: POLL_TOPASS_TIMEOUT,
})
}) })
test('maintain access control in document drawer', async () => { test('maintain access control in document drawer', async () => {

View File

@@ -263,18 +263,22 @@ describe('fields', () => {
}) })
await page.goto(url.create) await page.goto(url.create)
await page.waitForURL(`**/${url.create}`)
await page.locator('#field-text').fill('test') await page.locator('#field-text').fill('test')
await page.locator('#field-uniqueText').fill(uniqueText) await page.locator('#field-uniqueText').fill(uniqueText)
await page.locator('#field-localizedUniqueRequiredText').fill('localizedUniqueRequired2')
// attempt to save // attempt to save
await page.click('#action-save', { delay: 100 }) await page.click('#action-save', { delay: 200 })
// toast error // toast error
await expect(page.locator('.Toastify')).toContainText( await expect(page.locator('.Toastify')).toContainText(
'The following field is invalid: uniqueText', 'The following field is invalid: uniqueText',
) )
await expect.poll(() => page.url(), { timeout: POLL_TOPASS_TIMEOUT }).toContain('create')
// field specific error // field specific error
await expect(page.locator('.field-type.text.error #field-uniqueText')).toBeVisible() await expect(page.locator('.field-type.text.error #field-uniqueText')).toBeVisible()
@@ -292,6 +296,8 @@ describe('fields', () => {
'The following field is invalid: group.unique', 'The following field is invalid: group.unique',
) )
await expect.poll(() => page.url(), { timeout: POLL_TOPASS_TIMEOUT }).toContain('create')
// field specific error inside group // field specific error inside group
await expect(page.locator('.field-type.text.error #field-group__unique')).toBeVisible() await expect(page.locator('.field-type.text.error #field-group__unique')).toBeVisible()
}) })
@@ -919,7 +925,7 @@ describe('fields', () => {
await addRowButton.click() await addRowButton.click()
await wait(200) await wait(200)
const targetInput = page.locator('#field-items__2__text') const targetInput = page.locator('#field-items__0__text')
await expect(targetInput).toBeVisible() await expect(targetInput).toBeVisible()

View File

@@ -28,9 +28,15 @@ let client: RESTClient
let page: Page let page: Page
let serverURL: string let serverURL: string
async function navigateToLexicalFields() { /**
* Client-side navigation to the lexical editor from list view
*/
async function navigateToLexicalFields(navigateToListView: boolean = true) {
if (navigateToListView) {
const url: AdminUrlUtil = new AdminUrlUtil(serverURL, 'lexical-fields') const url: AdminUrlUtil = new AdminUrlUtil(serverURL, 'lexical-fields')
await page.goto(url.list) await page.goto(url.list)
}
const linkToDoc = page.locator('tbody tr:first-child .cell-title a').first() const linkToDoc = page.locator('tbody tr:first-child .cell-title a').first()
await expect(() => expect(linkToDoc).toBeTruthy()).toPass({ timeout: POLL_TOPASS_TIMEOUT }) await expect(() => expect(linkToDoc).toBeTruthy()).toPass({ timeout: POLL_TOPASS_TIMEOUT })
const linkDocHref = await linkToDoc.getAttribute('href') const linkDocHref = await linkToDoc.getAttribute('href')
@@ -718,8 +724,7 @@ describe('lexical', () => {
await expect(nestedEditorParagraph).toHaveText('Some text below relationship node 12345') await expect(nestedEditorParagraph).toHaveText('Some text below relationship node 12345')
}) })
test('should respect row removal in nested array field', async () => { const shouldRespectRowRemovalTest = async () => {
await navigateToLexicalFields()
const richTextField = page.locator('.rich-text-lexical').nth(1) // second const richTextField = page.locator('.rich-text-lexical').nth(1) // second
await richTextField.scrollIntoViewIfNeeded() await richTextField.scrollIntoViewIfNeeded()
await expect(richTextField).toBeVisible() await expect(richTextField).toBeVisible()
@@ -762,6 +767,36 @@ describe('lexical', () => {
await saveDocAndAssert(page) await saveDocAndAssert(page)
await expect(page.locator('.Toastify')).not.toContainText('Please correct invalid fields.') await expect(page.locator('.Toastify')).not.toContainText('Please correct invalid fields.')
}
// eslint-disable-next-line playwright/expect-expect
test('should respect row removal in nested array field', async () => {
await navigateToLexicalFields()
await shouldRespectRowRemovalTest()
})
test('should respect row removal in nested array field after navigating away from lexical document, then navigating back', async () => {
// This test verifies an issue where a lexical editor with blocks disappears when navigating away from the lexical document, then navigating back, without a hard refresh
await navigateToLexicalFields()
// Wait for lexical to be loaded up fully
const richTextField = page.locator('.rich-text-lexical').nth(1) // second
await richTextField.scrollIntoViewIfNeeded()
await expect(richTextField).toBeVisible()
const conditionalArrayBlock = richTextField.locator('.lexical-block').nth(7)
await conditionalArrayBlock.scrollIntoViewIfNeeded()
await expect(conditionalArrayBlock).toBeVisible()
// navigate to list view
await page.locator('.step-nav a').nth(1).click()
await page.waitForURL('**/lexical-fields?limit=10')
// Click on lexical document in list view (navigateToLexicalFields is client-side navigation which is what we need to reproduce the issue here)
await navigateToLexicalFields(false)
await shouldRespectRowRemovalTest()
}) })
test.skip('should respect required error state in deeply nested text field', async () => { test.skip('should respect required error state in deeply nested text field', async () => {

View File

@@ -37,7 +37,7 @@
], ],
"paths": { "paths": {
"@payload-config": [ "@payload-config": [
"./test/versions/config.ts" "./test/_community/config.ts"
], ],
"@payloadcms/live-preview": [ "@payloadcms/live-preview": [
"./packages/live-preview/src" "./packages/live-preview/src"