Merge branch 'alpha' of github.com:payloadcms/payload into fix/alpha/rte-e2e-tests
This commit is contained in:
2
.github/workflows/main.yml
vendored
2
.github/workflows/main.yml
vendored
@@ -217,7 +217,7 @@ jobs:
|
||||
# find test -type f -name 'e2e.spec.ts' | sort | xargs dirname | xargs -I {} basename {}
|
||||
suite:
|
||||
- _community
|
||||
# - access-control
|
||||
- access-control
|
||||
# - admin
|
||||
- auth
|
||||
- field-error-states
|
||||
|
||||
@@ -4,6 +4,7 @@ import type {
|
||||
SanitizedCollectionConfig,
|
||||
SanitizedConfig,
|
||||
SanitizedGlobalConfig,
|
||||
VisibleEntities,
|
||||
} from 'payload/types'
|
||||
|
||||
import { initI18n } from '@payloadcms/translations'
|
||||
@@ -11,7 +12,7 @@ import { translations } from '@payloadcms/translations/client'
|
||||
import { findLocaleFromCode } from '@payloadcms/ui/utilities/findLocaleFromCode'
|
||||
import { headers as getHeaders } from 'next/headers.js'
|
||||
import { notFound, redirect } from 'next/navigation.js'
|
||||
import { createLocalReq } from 'payload/utilities'
|
||||
import { createLocalReq, isEntityHidden } from 'payload/utilities'
|
||||
import qs from 'qs'
|
||||
|
||||
import { getPayloadHMR } from '../utilities/getPayloadHMR.js'
|
||||
@@ -40,6 +41,15 @@ export const initPage = async ({
|
||||
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 [entityType, entitySlug, createOrID] = routeSegments
|
||||
const collectionSlug = entityType === 'collections' ? entitySlug : undefined
|
||||
@@ -73,7 +83,7 @@ export const initPage = async ({
|
||||
|
||||
const queryString = `${qs.stringify(searchParams, { addQueryPrefix: true })}`
|
||||
|
||||
const req = await createLocalReq(
|
||||
const req = createLocalReq(
|
||||
{
|
||||
fallbackLocale: null,
|
||||
locale: locale.code,
|
||||
@@ -118,5 +128,6 @@ export const initPage = async ({
|
||||
permissions,
|
||||
req,
|
||||
translations: i18n.translations,
|
||||
visibleEntities,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
'use client'
|
||||
import type { EntityToGroup, Group } from '@payloadcms/ui/utilities/groupNavItems'
|
||||
import type { Permissions } from 'payload/auth'
|
||||
import type { VisibleEntities } from 'payload/types'
|
||||
|
||||
import { getTranslation } from '@payloadcms/translations'
|
||||
import { Button } from '@payloadcms/ui/elements/Button'
|
||||
@@ -19,9 +20,8 @@ const baseClass = 'dashboard'
|
||||
export const DefaultDashboardClient: React.FC<{
|
||||
Link: React.ComponentType
|
||||
permissions: Permissions
|
||||
visibleCollections: string[]
|
||||
visibleGlobals: string[]
|
||||
}> = ({ Link, permissions, visibleCollections, visibleGlobals }) => {
|
||||
visibleEntities: VisibleEntities
|
||||
}> = ({ Link, permissions, visibleEntities }) => {
|
||||
const config = useConfig()
|
||||
|
||||
const {
|
||||
@@ -40,13 +40,13 @@ export const DefaultDashboardClient: React.FC<{
|
||||
const collections = collectionsConfig.filter(
|
||||
(collection) =>
|
||||
permissions?.collections?.[collection.slug]?.read?.permission &&
|
||||
visibleCollections.includes(collection.slug),
|
||||
visibleEntities.collections.includes(collection.slug),
|
||||
)
|
||||
|
||||
const globals = globalsConfig.filter(
|
||||
(global) =>
|
||||
permissions?.globals?.[global.slug]?.read?.permission &&
|
||||
visibleGlobals.includes(global.slug),
|
||||
visibleEntities.globals.includes(global.slug),
|
||||
)
|
||||
|
||||
setGroups(
|
||||
@@ -73,15 +73,7 @@ export const DefaultDashboardClient: React.FC<{
|
||||
i18n,
|
||||
),
|
||||
)
|
||||
}, [
|
||||
permissions,
|
||||
user,
|
||||
i18n,
|
||||
visibleCollections,
|
||||
visibleGlobals,
|
||||
collectionsConfig,
|
||||
globalsConfig,
|
||||
])
|
||||
}, [permissions, user, i18n, visibleEntities, collectionsConfig, globalsConfig])
|
||||
|
||||
return (
|
||||
<Fragment>
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
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 { SetStepNav } from '@payloadcms/ui/elements/StepNav'
|
||||
@@ -15,8 +15,7 @@ export type DashboardProps = {
|
||||
Link: React.ComponentType<any>
|
||||
config: SanitizedConfig
|
||||
permissions: Permissions
|
||||
visibleCollections: string[]
|
||||
visibleGlobals: string[]
|
||||
visibleEntities: VisibleEntities
|
||||
}
|
||||
|
||||
export const DefaultDashboard: React.FC<DashboardProps> = (props) => {
|
||||
@@ -28,8 +27,7 @@ export const DefaultDashboard: React.FC<DashboardProps> = (props) => {
|
||||
},
|
||||
},
|
||||
permissions,
|
||||
visibleCollections,
|
||||
visibleGlobals,
|
||||
visibleEntities,
|
||||
} = props
|
||||
|
||||
return (
|
||||
@@ -42,8 +40,7 @@ export const DefaultDashboard: React.FC<DashboardProps> = (props) => {
|
||||
<DefaultDashboardClient
|
||||
Link={Link}
|
||||
permissions={permissions}
|
||||
visibleCollections={visibleCollections}
|
||||
visibleGlobals={visibleGlobals}
|
||||
visibleEntities={visibleEntities}
|
||||
/>
|
||||
{Array.isArray(afterDashboard) &&
|
||||
afterDashboard.map((Component, i) => <Component key={i} />)}
|
||||
|
||||
@@ -3,7 +3,6 @@ import type { AdminViewProps } from 'payload/types'
|
||||
import { HydrateClientUser } from '@payloadcms/ui/elements/HydrateClientUser'
|
||||
import { RenderCustomComponent } from '@payloadcms/ui/elements/RenderCustomComponent'
|
||||
import LinkImport from 'next/link.js'
|
||||
import { isEntityHidden } from 'payload/utilities'
|
||||
import React, { Fragment } from 'react'
|
||||
|
||||
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
|
||||
|
||||
export const Dashboard: React.FC<AdminViewProps> = ({
|
||||
initPageResult,
|
||||
// searchParams,
|
||||
}) => {
|
||||
export const Dashboard: React.FC<AdminViewProps> = ({ initPageResult }) => {
|
||||
const {
|
||||
permissions,
|
||||
req: {
|
||||
payload: { config },
|
||||
user,
|
||||
},
|
||||
visibleEntities,
|
||||
} = initPageResult
|
||||
|
||||
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 = {
|
||||
Link,
|
||||
config,
|
||||
permissions,
|
||||
visibleCollections,
|
||||
visibleGlobals,
|
||||
visibleEntities,
|
||||
}
|
||||
|
||||
return (
|
||||
|
||||
@@ -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
|
||||
const redirectRoute = `${adminRoute}/collections/${collectionSlug}/${json?.doc?.id}${locale ? `?locale=${locale}` : ''}`
|
||||
router.push(redirectRoute)
|
||||
@@ -144,6 +144,7 @@ export const DefaultEditView: React.FC = () => {
|
||||
id,
|
||||
entitySlug,
|
||||
user,
|
||||
depth,
|
||||
collectionSlug,
|
||||
getVersions,
|
||||
getDocPermissions,
|
||||
|
||||
@@ -7,12 +7,7 @@ import { NotFoundClient } from './index.client.js'
|
||||
|
||||
export const NotFoundView: AdminViewComponent = ({ initPageResult }) => {
|
||||
return (
|
||||
<DefaultTemplate
|
||||
config={initPageResult?.req?.payload.config}
|
||||
i18n={initPageResult?.req?.i18n}
|
||||
permissions={initPageResult?.permissions}
|
||||
user={initPageResult?.req?.user}
|
||||
>
|
||||
<DefaultTemplate config={initPageResult?.req?.payload.config}>
|
||||
<NotFoundClient />
|
||||
</DefaultTemplate>
|
||||
)
|
||||
|
||||
@@ -2,6 +2,7 @@ import type { I18n } from '@payloadcms/translations'
|
||||
import type { Metadata } from 'next'
|
||||
import type { SanitizedConfig } from 'payload/types'
|
||||
|
||||
import { EntityVisibilityProvider } from '@payloadcms/ui/providers/EntityVisibility'
|
||||
import { DefaultTemplate } from '@payloadcms/ui/templates/Default'
|
||||
import { MinimalTemplate } from '@payloadcms/ui/templates/Minimal'
|
||||
import { notFound, redirect } from 'next/navigation.js'
|
||||
@@ -81,22 +82,14 @@ export const RootPage = async ({
|
||||
<DefaultView initPageResult={initPageResult} params={params} searchParams={searchParams} />
|
||||
)
|
||||
|
||||
if (templateType === 'minimal') {
|
||||
return <MinimalTemplate className={templateClassName}>{RenderedView}</MinimalTemplate>
|
||||
}
|
||||
|
||||
if (templateType === 'default') {
|
||||
return (
|
||||
<DefaultTemplate
|
||||
config={config}
|
||||
i18n={initPageResult.req.i18n}
|
||||
permissions={initPageResult.permissions}
|
||||
user={initPageResult.req.user}
|
||||
>
|
||||
{RenderedView}
|
||||
</DefaultTemplate>
|
||||
<EntityVisibilityProvider visibleEntities={initPageResult.visibleEntities}>
|
||||
{templateType === 'minimal' && (
|
||||
<MinimalTemplate className={templateClassName}>{RenderedView}</MinimalTemplate>
|
||||
)}
|
||||
{templateType === 'default' && (
|
||||
<DefaultTemplate config={config}>{RenderedView}</DefaultTemplate>
|
||||
)}
|
||||
</EntityVisibilityProvider>
|
||||
)
|
||||
}
|
||||
|
||||
return RenderedView
|
||||
}
|
||||
|
||||
@@ -30,4 +30,5 @@ export type {
|
||||
EditViewProps,
|
||||
InitPageResult,
|
||||
ServerSideEditViewProps,
|
||||
VisibleEntities,
|
||||
} from './views/types.js'
|
||||
|
||||
@@ -29,6 +29,11 @@ export type EditViewProps = {
|
||||
globalSlug?: string
|
||||
}
|
||||
|
||||
export type VisibleEntities = {
|
||||
collections: SanitizedCollectionConfig['slug'][]
|
||||
globals: SanitizedGlobalConfig['slug'][]
|
||||
}
|
||||
|
||||
export type InitPageResult = {
|
||||
collectionConfig?: SanitizedCollectionConfig
|
||||
cookies: Map<string, string>
|
||||
@@ -38,6 +43,7 @@ export type InitPageResult = {
|
||||
permissions: Permissions
|
||||
req: PayloadRequest
|
||||
translations: Translations
|
||||
visibleEntities: VisibleEntities
|
||||
}
|
||||
|
||||
export type ServerSideEditViewProps = {
|
||||
|
||||
@@ -39,6 +39,7 @@ export const DocumentDrawerContent: React.FC<DocumentDrawerProps> = ({
|
||||
const { closeModal, modalState, toggleModal } = useModal()
|
||||
const locale = useLocale()
|
||||
const { t } = useTranslation()
|
||||
const [createdID, setCreatedID] = useState()
|
||||
const [isOpen, setIsOpen] = useState(false)
|
||||
const [collectionConfig] = useRelatedCollections(collectionSlug)
|
||||
const { formQueryParams } = useFormQueryParams()
|
||||
@@ -69,6 +70,7 @@ export const DocumentDrawerContent: React.FC<DocumentDrawerProps> = ({
|
||||
|
||||
const onSave = useCallback<DocumentDrawerProps['onSave']>(
|
||||
(args) => {
|
||||
setCreatedID(args.doc.id)
|
||||
if (typeof onSaveFromProps === 'function') {
|
||||
void onSaveFromProps({
|
||||
...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.
|
||||
hasSavePermission={null}
|
||||
// isLoading,
|
||||
id={id}
|
||||
id={id || createdID}
|
||||
isEditing={isEditing}
|
||||
onLoadError={onLoadError}
|
||||
onSave={onSave}
|
||||
|
||||
@@ -1,7 +1,8 @@
|
||||
'use client'
|
||||
|
||||
import { getTranslation } from '@payloadcms/translations'
|
||||
import { useEntityVisibility } from '@payloadcms/ui/providers/EntityVisibility'
|
||||
import LinkWithDefault from 'next/link.js'
|
||||
import { isEntityHidden } from 'payload/utilities'
|
||||
import React from 'react'
|
||||
|
||||
import type { EntityToGroup } from '../../utilities/groupNavItems.js'
|
||||
@@ -17,7 +18,9 @@ import { useNav } from './context.js'
|
||||
const baseClass = 'nav'
|
||||
|
||||
export const DefaultNavClient: React.FC = () => {
|
||||
const { permissions, user } = useAuth()
|
||||
const { permissions } = useAuth()
|
||||
const { isEntityVisible } = useEntityVisibility()
|
||||
|
||||
const {
|
||||
collections,
|
||||
globals,
|
||||
@@ -30,8 +33,7 @@ export const DefaultNavClient: React.FC = () => {
|
||||
const groups = groupNavItems(
|
||||
[
|
||||
...collections
|
||||
// @ts-expect-error todo: fix type error here
|
||||
.filter(({ admin: { hidden } }) => !isEntityHidden({ hidden, user }))
|
||||
.filter(({ slug }) => isEntityVisible({ collectionSlug: slug }))
|
||||
.map((collection) => {
|
||||
const entityToGroup: EntityToGroup = {
|
||||
type: EntityType.collection,
|
||||
@@ -41,8 +43,7 @@ export const DefaultNavClient: React.FC = () => {
|
||||
return entityToGroup
|
||||
}),
|
||||
...globals
|
||||
// @ts-expect-error todo: fix type error here
|
||||
.filter(({ admin: { hidden } }) => !isEntityHidden({ hidden, user }))
|
||||
.filter(({ slug }) => isEntityVisible({ globalSlug: slug }))
|
||||
.map((global) => {
|
||||
const entityToGroup: EntityToGroup = {
|
||||
type: EntityType.global,
|
||||
@@ -85,7 +86,6 @@ export const DefaultNavClient: React.FC = () => {
|
||||
|
||||
return (
|
||||
<LinkElement
|
||||
// activeClassName="active"
|
||||
className={`${baseClass}__link`}
|
||||
href={href}
|
||||
id={id}
|
||||
|
||||
@@ -11,9 +11,11 @@ const baseClass = 'nav'
|
||||
|
||||
import { DefaultNavClient } from './index.client.js'
|
||||
|
||||
export const DefaultNav: React.FC<{
|
||||
export type NavProps = {
|
||||
config: SanitizedConfig
|
||||
}> = (props) => {
|
||||
}
|
||||
|
||||
export const DefaultNav: React.FC<NavProps> = (props) => {
|
||||
const { config } = props
|
||||
|
||||
if (!config) {
|
||||
|
||||
@@ -264,6 +264,7 @@ export const Form: React.FC<FormProps> = (props) => {
|
||||
if (res.status < 400) {
|
||||
if (typeof onSuccess === 'function') await onSuccess(json)
|
||||
setSubmitted(false)
|
||||
setProcessing(false)
|
||||
|
||||
if (redirect) {
|
||||
router.push(redirect)
|
||||
@@ -271,6 +272,8 @@ export const Form: React.FC<FormProps> = (props) => {
|
||||
toast.success(json.message || t('general:submissionSuccessful'), { autoClose: 3000 })
|
||||
}
|
||||
} else {
|
||||
setProcessing(false)
|
||||
|
||||
contextRef.current = { ...contextRef.current } // triggers rerender of all components that subscribe to form
|
||||
if (json.message) {
|
||||
toast.error(json.message)
|
||||
@@ -327,8 +330,6 @@ export const Form: React.FC<FormProps> = (props) => {
|
||||
|
||||
toast.error(message)
|
||||
}
|
||||
|
||||
setProcessing(false)
|
||||
} catch (err) {
|
||||
setProcessing(false)
|
||||
|
||||
|
||||
@@ -1,18 +1,24 @@
|
||||
'use client'
|
||||
import React from 'react'
|
||||
|
||||
type AddClientFunctionContextType = (func: any) => void
|
||||
type ModifyClientFunctionContextType = {
|
||||
addClientFunction: (args: ModifyFunctionArgs) => void
|
||||
removeClientFunction: (args: ModifyFunctionArgs) => void
|
||||
}
|
||||
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>({})
|
||||
|
||||
type AddFunctionArgs = { func: any; key: string }
|
||||
type ModifyFunctionArgs = { func: any; key: string }
|
||||
|
||||
export const ClientFunctionProvider: React.FC<{ children: React.ReactNode }> = ({ children }) => {
|
||||
const [clientFunctions, setClientFunctions] = React.useState({})
|
||||
|
||||
const addClientFunction = React.useCallback((args: AddFunctionArgs) => {
|
||||
const addClientFunction = React.useCallback((args: ModifyFunctionArgs) => {
|
||||
setClientFunctions((state) => {
|
||||
const newState = { ...state }
|
||||
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 (
|
||||
<AddClientFunctionContext.Provider value={addClientFunction}>
|
||||
<ModifyClientFunctionContext.Provider
|
||||
value={{
|
||||
addClientFunction,
|
||||
removeClientFunction,
|
||||
}}
|
||||
>
|
||||
<ClientFunctionsContext.Provider value={clientFunctions}>
|
||||
{children}
|
||||
</ClientFunctionsContext.Provider>
|
||||
</AddClientFunctionContext.Provider>
|
||||
</ModifyClientFunctionContext.Provider>
|
||||
)
|
||||
}
|
||||
|
||||
export const useAddClientFunction = (key: string, func: any) => {
|
||||
const addClientFunction = React.useContext(AddClientFunctionContext)
|
||||
const { addClientFunction, removeClientFunction } = React.useContext(ModifyClientFunctionContext)
|
||||
|
||||
React.useEffect(() => {
|
||||
addClientFunction({
|
||||
func,
|
||||
key,
|
||||
})
|
||||
}, [func, key, addClientFunction])
|
||||
|
||||
return () => {
|
||||
removeClientFunction({
|
||||
func,
|
||||
key,
|
||||
})
|
||||
}
|
||||
}, [func, key, addClientFunction, removeClientFunction])
|
||||
}
|
||||
|
||||
export const useClientFunctions = () => {
|
||||
|
||||
56
packages/ui/src/providers/EntityVisibility/index.tsx
Normal file
56
packages/ui/src/providers/EntityVisibility/index.tsx
Normal 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)
|
||||
@@ -1,16 +1,15 @@
|
||||
import type { Permissions, User } from 'payload/auth'
|
||||
import type { SanitizedConfig } from 'payload/types'
|
||||
|
||||
import React from 'react'
|
||||
|
||||
import type { NavProps } from '../../elements/Nav/index.js'
|
||||
|
||||
import { AppHeader } from '../../elements/AppHeader/index.js'
|
||||
import { NavToggler } from '../../elements/Nav/NavToggler/index.js'
|
||||
import { DefaultNav } from '../../elements/Nav/index.js'
|
||||
import { RenderCustomComponent } from '../../elements/RenderCustomComponent/index.js'
|
||||
import { NavHamburger } from './NavHamburger/index.js'
|
||||
export { NavHamburger } from './NavHamburger/index.js'
|
||||
import { Wrapper } from './Wrapper/index.js'
|
||||
export { Wrapper } from './Wrapper/index.js'
|
||||
import './index.scss'
|
||||
|
||||
const baseClass = 'template-default'
|
||||
@@ -19,18 +18,12 @@ export type DefaultTemplateProps = {
|
||||
children?: React.ReactNode
|
||||
className?: string
|
||||
config: Promise<SanitizedConfig> | SanitizedConfig
|
||||
i18n: any
|
||||
permissions: Permissions
|
||||
user: User
|
||||
}
|
||||
|
||||
export const DefaultTemplate: React.FC<DefaultTemplateProps> = async ({
|
||||
children,
|
||||
className,
|
||||
config: configPromise,
|
||||
i18n,
|
||||
permissions,
|
||||
user,
|
||||
}) => {
|
||||
const config = await configPromise
|
||||
|
||||
@@ -42,7 +35,10 @@ export const DefaultTemplate: React.FC<DefaultTemplateProps> = async ({
|
||||
} = {},
|
||||
} = 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 (
|
||||
<div>
|
||||
<div className={`${baseClass}__nav-toggler-wrapper`} id="nav-toggler">
|
||||
@@ -54,12 +50,7 @@ export const DefaultTemplate: React.FC<DefaultTemplateProps> = async ({
|
||||
<RenderCustomComponent
|
||||
CustomComponent={CustomNav}
|
||||
DefaultComponent={DefaultNav}
|
||||
componentProps={{
|
||||
config,
|
||||
i18n,
|
||||
permissions,
|
||||
user,
|
||||
}}
|
||||
componentProps={navProps}
|
||||
/>
|
||||
<div className={`${baseClass}__wrap`}>
|
||||
<AppHeader />
|
||||
|
||||
@@ -17,6 +17,7 @@ import {
|
||||
} from '../helpers.js'
|
||||
import { AdminUrlUtil } from '../helpers/adminUrlUtil.js'
|
||||
import { initPayloadE2E } from '../helpers/initPayloadE2E.js'
|
||||
import { POLL_TOPASS_TIMEOUT } from '../playwright.config.js'
|
||||
import config from './config.js'
|
||||
import {
|
||||
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.
|
||||
test.skip('should show test global immediately after allowing access', async () => {
|
||||
await page.goto(`${serverURL}/admin/globals/settings`)
|
||||
test('should show test global immediately after allowing access', async () => {
|
||||
const url = `${serverURL}/admin/globals/settings`
|
||||
await page.goto(url)
|
||||
|
||||
await expect.poll(() => page.url(), { timeout: POLL_TOPASS_TIMEOUT }).toContain(url)
|
||||
|
||||
await openNav(page)
|
||||
|
||||
@@ -278,8 +282,11 @@ describe('access control', () => {
|
||||
|
||||
await openNav(page)
|
||||
|
||||
// Now test collection should appear in the menu.
|
||||
await expect(page.locator('#nav-global-test')).toBeVisible()
|
||||
const globalTest = page.locator('#nav-global-test')
|
||||
|
||||
await expect(async () => await globalTest.isVisible()).toPass({
|
||||
timeout: POLL_TOPASS_TIMEOUT,
|
||||
})
|
||||
})
|
||||
|
||||
test('maintain access control in document drawer', async () => {
|
||||
|
||||
@@ -263,18 +263,22 @@ describe('fields', () => {
|
||||
})
|
||||
|
||||
await page.goto(url.create)
|
||||
await page.waitForURL(`**/${url.create}`)
|
||||
|
||||
await page.locator('#field-text').fill('test')
|
||||
await page.locator('#field-uniqueText').fill(uniqueText)
|
||||
await page.locator('#field-localizedUniqueRequiredText').fill('localizedUniqueRequired2')
|
||||
|
||||
// attempt to save
|
||||
await page.click('#action-save', { delay: 100 })
|
||||
await page.click('#action-save', { delay: 200 })
|
||||
|
||||
// toast error
|
||||
await expect(page.locator('.Toastify')).toContainText(
|
||||
'The following field is invalid: uniqueText',
|
||||
)
|
||||
|
||||
await expect.poll(() => page.url(), { timeout: POLL_TOPASS_TIMEOUT }).toContain('create')
|
||||
|
||||
// field specific error
|
||||
await expect(page.locator('.field-type.text.error #field-uniqueText')).toBeVisible()
|
||||
|
||||
@@ -292,6 +296,8 @@ describe('fields', () => {
|
||||
'The following field is invalid: group.unique',
|
||||
)
|
||||
|
||||
await expect.poll(() => page.url(), { timeout: POLL_TOPASS_TIMEOUT }).toContain('create')
|
||||
|
||||
// field specific error inside group
|
||||
await expect(page.locator('.field-type.text.error #field-group__unique')).toBeVisible()
|
||||
})
|
||||
@@ -919,7 +925,7 @@ describe('fields', () => {
|
||||
await addRowButton.click()
|
||||
await wait(200)
|
||||
|
||||
const targetInput = page.locator('#field-items__2__text')
|
||||
const targetInput = page.locator('#field-items__0__text')
|
||||
|
||||
await expect(targetInput).toBeVisible()
|
||||
|
||||
|
||||
@@ -28,9 +28,15 @@ let client: RESTClient
|
||||
let page: Page
|
||||
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')
|
||||
await page.goto(url.list)
|
||||
}
|
||||
|
||||
const linkToDoc = page.locator('tbody tr:first-child .cell-title a').first()
|
||||
await expect(() => expect(linkToDoc).toBeTruthy()).toPass({ timeout: POLL_TOPASS_TIMEOUT })
|
||||
const linkDocHref = await linkToDoc.getAttribute('href')
|
||||
@@ -718,8 +724,7 @@ describe('lexical', () => {
|
||||
await expect(nestedEditorParagraph).toHaveText('Some text below relationship node 12345')
|
||||
})
|
||||
|
||||
test('should respect row removal in nested array field', async () => {
|
||||
await navigateToLexicalFields()
|
||||
const shouldRespectRowRemovalTest = async () => {
|
||||
const richTextField = page.locator('.rich-text-lexical').nth(1) // second
|
||||
await richTextField.scrollIntoViewIfNeeded()
|
||||
await expect(richTextField).toBeVisible()
|
||||
@@ -762,6 +767,36 @@ describe('lexical', () => {
|
||||
await saveDocAndAssert(page)
|
||||
|
||||
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 () => {
|
||||
|
||||
@@ -37,7 +37,7 @@
|
||||
],
|
||||
"paths": {
|
||||
"@payload-config": [
|
||||
"./test/versions/config.ts"
|
||||
"./test/_community/config.ts"
|
||||
],
|
||||
"@payloadcms/live-preview": [
|
||||
"./packages/live-preview/src"
|
||||
|
||||
Reference in New Issue
Block a user