Merge branch '2.0' of github.com:payloadcms/payload into 2.0
This commit is contained in:
@@ -32,6 +32,7 @@ module.exports = {
|
||||
{
|
||||
files: ['**/int.spec.ts'],
|
||||
rules: {
|
||||
'@typescript-eslint/no-explicit-any': 'off',
|
||||
'@typescript-eslint/no-unsafe-assignment': 'off',
|
||||
'@typescript-eslint/no-use-before-define': 'off',
|
||||
'jest/prefer-strict-equal': 'off',
|
||||
|
||||
@@ -6,6 +6,7 @@ import type { ReadOnlyCollection, RestrictedVersion } from './payload-types'
|
||||
|
||||
import payload from '../../packages/payload/src'
|
||||
import wait from '../../packages/payload/src/utilities/wait'
|
||||
import { openMainMenu } from '../helpers'
|
||||
import { AdminUrlUtil } from '../helpers/adminUrlUtil'
|
||||
import { initPayloadE2E } from '../helpers/configHelpers'
|
||||
import {
|
||||
@@ -99,6 +100,7 @@ describe('access control', () => {
|
||||
|
||||
test('should not show in nav', async () => {
|
||||
await page.goto(url.admin)
|
||||
await openMainMenu(page)
|
||||
await expect(page.locator('.nav >> a:has-text("Restricteds")')).toHaveCount(0)
|
||||
})
|
||||
|
||||
@@ -137,7 +139,9 @@ describe('access control', () => {
|
||||
|
||||
test('should show in nav', async () => {
|
||||
await page.goto(url.admin)
|
||||
await expect(page.locator(`.nav a[href="/admin/collections/${readOnlySlug}"]`)).toHaveCount(1)
|
||||
await expect(
|
||||
page.locator(`.main-menu a[href="/admin/collections/${readOnlySlug}"]`),
|
||||
).toHaveCount(1)
|
||||
})
|
||||
|
||||
test('should have collection url', async () => {
|
||||
|
||||
@@ -4,17 +4,18 @@ import type { Post, RelyOnRequestHeader, Restricted } from './payload-types'
|
||||
import payload from '../../packages/payload/src'
|
||||
import { Forbidden } from '../../packages/payload/src/errors'
|
||||
import { initPayloadTest } from '../helpers/configHelpers'
|
||||
import { requestHeaders } from './config'
|
||||
import {
|
||||
firstArrayText,
|
||||
hiddenAccessSlug,
|
||||
hiddenFieldsSlug,
|
||||
relyOnRequestHeadersSlug,
|
||||
requestHeaders,
|
||||
restrictedSlug,
|
||||
restrictedVersionsSlug,
|
||||
secondArrayText,
|
||||
siblingDataSlug,
|
||||
slug,
|
||||
} from './config'
|
||||
import { firstArrayText, secondArrayText } from './shared'
|
||||
} from './shared'
|
||||
|
||||
describe('Access Control', () => {
|
||||
let post1: Post
|
||||
|
||||
@@ -2,11 +2,9 @@ import React from 'react'
|
||||
import { NavLink } from 'react-router-dom'
|
||||
|
||||
// As this is the demo project, we import our dependencies from the `src` directory.
|
||||
import Chevron from '../../../../packages/payload/src/admin/components/icons/Chevron'
|
||||
import { useConfig } from '../../../../packages/payload/src/admin/components/utilities/Config'
|
||||
|
||||
// In your projects, you can import as follows:
|
||||
// import { Chevron } from 'payload/components';
|
||||
// import { useConfig } from 'payload/components/utilities';
|
||||
|
||||
const baseClass = 'after-nav-links'
|
||||
@@ -17,26 +15,35 @@ const AfterNavLinks: React.FC = () => {
|
||||
} = useConfig()
|
||||
|
||||
return (
|
||||
<div className={baseClass}>
|
||||
<span className="nav__label">Custom Routes</span>
|
||||
<nav>
|
||||
<div
|
||||
className={baseClass}
|
||||
style={{
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
gap: 'calc(var(--base) / 4)',
|
||||
}}
|
||||
>
|
||||
<h4 className="nav__label" style={{ color: 'var(--theme-elevation-400)', margin: 0 }}>
|
||||
Custom Routes
|
||||
</h4>
|
||||
<h4 className="nav__link" style={{ margin: 0 }}>
|
||||
<NavLink
|
||||
activeClassName="active"
|
||||
className="nav__link"
|
||||
style={{ textDecoration: 'none' }}
|
||||
to={`${adminRoute}/custom-default-route`}
|
||||
>
|
||||
<Chevron />
|
||||
Default Template
|
||||
</NavLink>
|
||||
</h4>
|
||||
<h4 className="nav__link" style={{ margin: 0 }}>
|
||||
<NavLink
|
||||
activeClassName="active"
|
||||
className="nav__link"
|
||||
style={{ textDecoration: 'none' }}
|
||||
to={`${adminRoute}/custom-minimal-route`}
|
||||
>
|
||||
<Chevron />
|
||||
Minimal Template
|
||||
</NavLink>
|
||||
</nav>
|
||||
</h4>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -49,14 +49,21 @@ const CustomDefaultRoute: AdminView = ({ canAccessAdmin, user }) => {
|
||||
title="Custom Route with Default Template"
|
||||
/>
|
||||
<Eyebrow />
|
||||
<h1>Custom Route</h1>
|
||||
<p>
|
||||
Here is a custom route that was added in the Payload config. It uses the Default Template,
|
||||
so the sidebar is rendered.
|
||||
</p>
|
||||
<Button buttonStyle="secondary" el="link" to={`${adminRoute}`}>
|
||||
Go to Dashboard
|
||||
</Button>
|
||||
<div
|
||||
style={{
|
||||
paddingRight: 'var(--gutter-h)',
|
||||
paddingLeft: 'var(--gutter-h)',
|
||||
}}
|
||||
>
|
||||
<h1>Custom Route</h1>
|
||||
<p>
|
||||
Here is a custom route that was added in the Payload config. It uses the Default Template,
|
||||
so the sidebar is rendered.
|
||||
</p>
|
||||
<Button buttonStyle="secondary" el="link" to={`${adminRoute}`}>
|
||||
Go to Dashboard
|
||||
</Button>
|
||||
</div>
|
||||
</DefaultTemplate>
|
||||
)
|
||||
}
|
||||
98
test/admin/components/views/CustomEdit/index.tsx
Normal file
98
test/admin/components/views/CustomEdit/index.tsx
Normal file
@@ -0,0 +1,98 @@
|
||||
import React, { Fragment, useEffect } from 'react'
|
||||
import { Redirect, useParams } from 'react-router-dom'
|
||||
|
||||
import Button from '../../../../../packages/payload/src/admin/components/elements/Button'
|
||||
import Eyebrow from '../../../../../packages/payload/src/admin/components/elements/Eyebrow'
|
||||
import { useStepNav } from '../../../../../packages/payload/src/admin/components/elements/StepNav'
|
||||
import { useConfig } from '../../../../../packages/payload/src/admin/components/utilities/Config'
|
||||
import { type CustomAdminView } from '../../../../../packages/payload/src/config/types'
|
||||
|
||||
const CustomEditView: CustomAdminView = ({ canAccessAdmin, collection, global, user }) => {
|
||||
const {
|
||||
routes: { admin: adminRoute },
|
||||
} = useConfig()
|
||||
|
||||
const params = useParams()
|
||||
|
||||
const { setStepNav } = useStepNav()
|
||||
|
||||
// This effect will only run one time and will allow us
|
||||
// to set the step nav to display our custom route name
|
||||
|
||||
useEffect(() => {
|
||||
setStepNav([
|
||||
{
|
||||
label: 'Custom Edit View',
|
||||
},
|
||||
])
|
||||
}, [setStepNav])
|
||||
|
||||
// If an unauthorized user tries to navigate straight to this page,
|
||||
// Boot 'em out
|
||||
if (!user || (user && !canAccessAdmin)) {
|
||||
return <Redirect to={`${adminRoute}/unauthorized`} />
|
||||
}
|
||||
|
||||
let versionsRoute = ''
|
||||
let customRoute = ''
|
||||
|
||||
if (collection) {
|
||||
versionsRoute = `${adminRoute}/collections/${collection?.slug}/${params.id}/versions`
|
||||
customRoute = `${adminRoute}/collections/${collection?.slug}/${params.id}/custom`
|
||||
}
|
||||
|
||||
if (global) {
|
||||
versionsRoute = `${adminRoute}/globals/${global?.slug}/versions`
|
||||
customRoute = `${adminRoute}/globals/${global?.slug}/custom`
|
||||
}
|
||||
|
||||
return (
|
||||
<Fragment>
|
||||
<Eyebrow />
|
||||
<div
|
||||
style={{
|
||||
paddingLeft: 'var(--gutter-h)',
|
||||
paddingRight: 'var(--gutter-h)',
|
||||
}}
|
||||
>
|
||||
<h1>Custom Edit View</h1>
|
||||
<p>This custom edit view was added through one of the following Payload configs:</p>
|
||||
<ul>
|
||||
<li>
|
||||
<code>components.views.Edit</code>
|
||||
<p>
|
||||
{'This takes precedence over the default edit view, '}
|
||||
<b>as well as all nested views like versions.</b>
|
||||
</p>
|
||||
</li>
|
||||
<li>
|
||||
<code>components.views.Edit.Default</code>
|
||||
<p>
|
||||
{'This allows you to override only the default edit view, but '}
|
||||
<b>
|
||||
<em>not</em>
|
||||
</b>
|
||||
{' any nested views like versions, etc.'}
|
||||
</p>
|
||||
</li>
|
||||
<li>
|
||||
<code>components.views.Edit.Default.Component</code>
|
||||
<p>
|
||||
This is the most granular override, allowing you to override only the default edit
|
||||
view's Component, and its other properties like path and label.
|
||||
</p>
|
||||
</li>
|
||||
</ul>
|
||||
<Button buttonStyle="primary" el="link" to={versionsRoute}>
|
||||
Custom Versions
|
||||
</Button>
|
||||
|
||||
<Button buttonStyle="secondary" el="link" to={customRoute}>
|
||||
Custom View
|
||||
</Button>
|
||||
</div>
|
||||
</Fragment>
|
||||
)
|
||||
}
|
||||
|
||||
export default CustomEditView
|
||||
85
test/admin/components/views/CustomVersions/index.tsx
Normal file
85
test/admin/components/views/CustomVersions/index.tsx
Normal file
@@ -0,0 +1,85 @@
|
||||
import React, { Fragment, useEffect } from 'react'
|
||||
import { Redirect, useParams } from 'react-router-dom'
|
||||
|
||||
import Button from '../../../../../packages/payload/src/admin/components/elements/Button'
|
||||
import Eyebrow from '../../../../../packages/payload/src/admin/components/elements/Eyebrow'
|
||||
import { useStepNav } from '../../../../../packages/payload/src/admin/components/elements/StepNav'
|
||||
import { useConfig } from '../../../../../packages/payload/src/admin/components/utilities/Config'
|
||||
import { type CustomAdminView } from '../../../../../packages/payload/src/config/types'
|
||||
|
||||
const CustomVersionsView: CustomAdminView = ({ canAccessAdmin, collection, global, user }) => {
|
||||
const {
|
||||
routes: { admin: adminRoute },
|
||||
} = useConfig()
|
||||
|
||||
const params = useParams()
|
||||
|
||||
const { setStepNav } = useStepNav()
|
||||
|
||||
// This effect will only run one time and will allow us
|
||||
// to set the step nav to display our custom route name
|
||||
|
||||
useEffect(() => {
|
||||
setStepNav([
|
||||
{
|
||||
label: 'Custom Versions View',
|
||||
},
|
||||
])
|
||||
}, [setStepNav])
|
||||
|
||||
// If an unauthorized user tries to navigate straight to this page,
|
||||
// Boot 'em out
|
||||
if (!user || (user && !canAccessAdmin)) {
|
||||
return <Redirect to={`${adminRoute}/unauthorized`} />
|
||||
}
|
||||
|
||||
let backURL = adminRoute
|
||||
|
||||
if (collection) {
|
||||
backURL = `${adminRoute}/collections/${collection?.slug}/${params.id}`
|
||||
}
|
||||
|
||||
if (global) {
|
||||
backURL = `${adminRoute}/globals/${global?.slug}`
|
||||
}
|
||||
|
||||
return (
|
||||
<Fragment>
|
||||
<Eyebrow />
|
||||
<div
|
||||
style={{
|
||||
paddingLeft: 'var(--gutter-h)',
|
||||
paddingRight: 'var(--gutter-h)',
|
||||
}}
|
||||
>
|
||||
<h1>Custom Versions View</h1>
|
||||
<p>This custom versions view was added through one of the following Payload configs:</p>
|
||||
<ul>
|
||||
<li>
|
||||
<code>components.views.Versions</code>
|
||||
<p>
|
||||
{'This takes precedence over the default versions view, '}
|
||||
<b>as well as all nested views like /versions/:id.</b>
|
||||
</p>
|
||||
</li>
|
||||
<li>
|
||||
<code>components.views.Edit.versions</code>
|
||||
<p>Same as above.</p>
|
||||
</li>
|
||||
<li>
|
||||
<code>components.views.Edit.versions.Component</code>
|
||||
</li>
|
||||
<p>
|
||||
This is the most granular override, allowing you to override only the default versions
|
||||
view's Component, and its other properties like path and label.
|
||||
</p>
|
||||
</ul>
|
||||
<Button buttonStyle="secondary" el="link" to={backURL}>
|
||||
Back
|
||||
</Button>
|
||||
</div>
|
||||
</Fragment>
|
||||
)
|
||||
}
|
||||
|
||||
export default CustomVersionsView
|
||||
67
test/admin/components/views/CustomView/index.tsx
Normal file
67
test/admin/components/views/CustomView/index.tsx
Normal file
@@ -0,0 +1,67 @@
|
||||
import React, { Fragment, useEffect } from 'react'
|
||||
import { useParams } from 'react-router-dom'
|
||||
|
||||
import Button from '../../../../../packages/payload/src/admin/components/elements/Button'
|
||||
import Eyebrow from '../../../../../packages/payload/src/admin/components/elements/Eyebrow'
|
||||
import { useStepNav } from '../../../../../packages/payload/src/admin/components/elements/StepNav'
|
||||
import { useConfig } from '../../../../../packages/payload/src/admin/components/utilities/Config'
|
||||
import { type CustomAdminView } from '../../../../../packages/payload/src/config/types'
|
||||
|
||||
const CustomView: CustomAdminView = ({ collection, global }) => {
|
||||
const {
|
||||
routes: { admin: adminRoute },
|
||||
} = useConfig()
|
||||
|
||||
const params = useParams()
|
||||
|
||||
const { setStepNav } = useStepNav()
|
||||
|
||||
// This effect will only run one time and will allow us
|
||||
// to set the step nav to display our custom route name
|
||||
|
||||
useEffect(() => {
|
||||
setStepNav([
|
||||
{
|
||||
label: 'Custom View',
|
||||
},
|
||||
])
|
||||
}, [setStepNav])
|
||||
|
||||
let backURL = ''
|
||||
let versionsRoute = ''
|
||||
|
||||
if (collection) {
|
||||
backURL = `${adminRoute}/collections/${collection?.slug}/${params.id}`
|
||||
versionsRoute = `${adminRoute}/collections/${collection?.slug}/${params.id}/versions`
|
||||
}
|
||||
|
||||
if (global) {
|
||||
backURL = `${adminRoute}/globals/${global?.slug}`
|
||||
versionsRoute = `${adminRoute}/globals/${global?.slug}/versions`
|
||||
}
|
||||
|
||||
return (
|
||||
<Fragment>
|
||||
<Eyebrow />
|
||||
<div
|
||||
style={{
|
||||
paddingLeft: 'var(--gutter-h)',
|
||||
paddingRight: 'var(--gutter-h)',
|
||||
}}
|
||||
>
|
||||
<h1>Custom View</h1>
|
||||
<p>This custom view was added through the Payload config:</p>
|
||||
<ul>
|
||||
<li>
|
||||
<code>components.views[key].Component</code>
|
||||
</li>
|
||||
</ul>
|
||||
<Button buttonStyle="secondary" el="link" to={backURL}>
|
||||
Back
|
||||
</Button>
|
||||
</div>
|
||||
</Fragment>
|
||||
)
|
||||
}
|
||||
|
||||
export default CustomView
|
||||
@@ -9,8 +9,11 @@ import BeforeLogin from './components/BeforeLogin'
|
||||
import DemoUIFieldCell from './components/DemoUIField/Cell'
|
||||
import DemoUIFieldField from './components/DemoUIField/Field'
|
||||
import Logout from './components/Logout'
|
||||
import CustomDefaultRoute from './components/views/CustomDefault'
|
||||
import CustomMinimalRoute from './components/views/CustomMinimal'
|
||||
import CustomDefaultRoute from './components/routes/CustomDefault'
|
||||
import CustomMinimalRoute from './components/routes/CustomMinimal'
|
||||
import CustomEditView from './components/views/CustomEdit'
|
||||
import CustomVersionsView from './components/views/CustomVersions'
|
||||
import CustomView from './components/views/CustomView'
|
||||
import { globalSlug, slug } from './shared'
|
||||
|
||||
export interface Post {
|
||||
@@ -131,6 +134,48 @@ export default buildConfigWithDefaults({
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
slug: 'custom-views-one',
|
||||
versions: true,
|
||||
admin: {
|
||||
components: {
|
||||
views: {
|
||||
Edit: CustomEditView,
|
||||
},
|
||||
},
|
||||
},
|
||||
fields: [
|
||||
{
|
||||
name: 'title',
|
||||
type: 'text',
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
slug: 'custom-views-two',
|
||||
versions: true,
|
||||
admin: {
|
||||
components: {
|
||||
views: {
|
||||
Edit: {
|
||||
Default: CustomEditView,
|
||||
Versions: CustomVersionsView,
|
||||
MyCustomView: {
|
||||
path: '/custom',
|
||||
Component: CustomView,
|
||||
label: 'Custom',
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
fields: [
|
||||
{
|
||||
name: 'title',
|
||||
type: 'text',
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
slug: 'group-one-collection-ones',
|
||||
admin: {
|
||||
@@ -218,6 +263,49 @@ export default buildConfigWithDefaults({
|
||||
},
|
||||
],
|
||||
},
|
||||
|
||||
{
|
||||
slug: 'custom-global-views-one',
|
||||
versions: true,
|
||||
admin: {
|
||||
components: {
|
||||
views: {
|
||||
Edit: CustomEditView,
|
||||
},
|
||||
},
|
||||
},
|
||||
fields: [
|
||||
{
|
||||
name: 'title',
|
||||
type: 'text',
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
slug: 'custom-global-views-two',
|
||||
versions: true,
|
||||
admin: {
|
||||
components: {
|
||||
views: {
|
||||
Edit: {
|
||||
Default: CustomEditView,
|
||||
Versions: CustomVersionsView,
|
||||
MyCustomView: {
|
||||
path: '/custom',
|
||||
Component: CustomView,
|
||||
label: 'Custom',
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
fields: [
|
||||
{
|
||||
name: 'title',
|
||||
type: 'text',
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
slug: 'group-globals-one',
|
||||
admin: {
|
||||
@@ -262,6 +350,20 @@ export default buildConfigWithDefaults({
|
||||
})
|
||||
})
|
||||
|
||||
await payload.create({
|
||||
collection: 'custom-views-one',
|
||||
data: {
|
||||
title: 'title',
|
||||
},
|
||||
})
|
||||
|
||||
await payload.create({
|
||||
collection: 'custom-views-two',
|
||||
data: {
|
||||
title: 'title',
|
||||
},
|
||||
})
|
||||
|
||||
await payload.create({
|
||||
collection: 'geo',
|
||||
data: {
|
||||
|
||||
@@ -8,7 +8,7 @@ import type { Post } from './config'
|
||||
import payload from '../../packages/payload/src'
|
||||
import { mapAsync } from '../../packages/payload/src/utilities/mapAsync'
|
||||
import wait from '../../packages/payload/src/utilities/wait'
|
||||
import { saveDocAndAssert, saveDocHotkeyAndAssert } from '../helpers'
|
||||
import { openMainMenu, saveDocAndAssert, saveDocHotkeyAndAssert } from '../helpers'
|
||||
import { AdminUrlUtil } from '../helpers/adminUrlUtil'
|
||||
import { initPayloadE2E } from '../helpers/configHelpers'
|
||||
import { globalSlug, slug } from './shared'
|
||||
@@ -45,27 +45,29 @@ describe('admin', () => {
|
||||
describe('Nav', () => {
|
||||
test('should nav to collection - sidebar', async () => {
|
||||
await page.goto(url.admin)
|
||||
const collectionLink = page.locator(`#nav-${slug}`)
|
||||
await collectionLink.click()
|
||||
|
||||
await openMainMenu(page)
|
||||
await page.locator(`#nav-${slug}`).click()
|
||||
expect(page.url()).toContain(url.list)
|
||||
})
|
||||
|
||||
test('should nav to a global - sidebar', async () => {
|
||||
await page.goto(url.admin)
|
||||
await openMainMenu(page)
|
||||
await page.locator(`#nav-global-${globalSlug}`).click()
|
||||
|
||||
expect(page.url()).toContain(url.global(globalSlug))
|
||||
})
|
||||
|
||||
test('should navigate to collection - card', async () => {
|
||||
await page.goto(url.admin)
|
||||
await wait(200)
|
||||
await page.locator(`#card-${slug}`).click()
|
||||
expect(page.url()).toContain(url.list)
|
||||
})
|
||||
|
||||
test('should collapse and expand collection groups', async () => {
|
||||
await page.goto(url.admin)
|
||||
await openMainMenu(page)
|
||||
|
||||
const navGroup = page.locator('#nav-group-One .nav-group__toggle')
|
||||
const link = page.locator('#nav-group-one-collection-ones')
|
||||
|
||||
@@ -81,6 +83,8 @@ describe('admin', () => {
|
||||
|
||||
test('should collapse and expand globals groups', async () => {
|
||||
await page.goto(url.admin)
|
||||
await openMainMenu(page)
|
||||
|
||||
const navGroup = page.locator('#nav-group-Group .nav-group__toggle')
|
||||
const link = page.locator('#nav-global-group-globals-one')
|
||||
|
||||
@@ -96,12 +100,9 @@ describe('admin', () => {
|
||||
|
||||
test('should save nav group collapse preferences', async () => {
|
||||
await page.goto(url.admin)
|
||||
|
||||
const navGroup = page.locator('#nav-group-One .nav-group__toggle')
|
||||
await navGroup.click()
|
||||
|
||||
await openMainMenu(page)
|
||||
await page.locator('#nav-group-One .nav-group__toggle').click()
|
||||
await page.goto(url.admin)
|
||||
|
||||
const link = page.locator('#nav-group-one-collection-ones')
|
||||
await expect(link).toBeHidden()
|
||||
})
|
||||
@@ -189,13 +190,11 @@ describe('admin', () => {
|
||||
})
|
||||
|
||||
test('should delete existing', async () => {
|
||||
const { id, ...post } = await createPost()
|
||||
|
||||
const { id, title } = await createPost()
|
||||
await page.goto(url.edit(id))
|
||||
await page.locator('#action-delete').click()
|
||||
await page.locator('#confirm-delete').click()
|
||||
|
||||
await expect(page.locator(`text=Post en "${post.title}" successfully deleted.`)).toBeVisible()
|
||||
await expect(page.locator(`text=Post en "${title}" successfully deleted.`)).toBeVisible()
|
||||
expect(page.url()).toContain(url.list)
|
||||
})
|
||||
|
||||
@@ -724,7 +723,8 @@ describe('admin', () => {
|
||||
describe('custom css', () => {
|
||||
test('should see custom css in admin UI', async () => {
|
||||
await page.goto(url.admin)
|
||||
const navControls = page.locator('.nav__controls')
|
||||
await openMainMenu(page)
|
||||
const navControls = page.locator('.main-menu__controls')
|
||||
await expect(navControls).toHaveCSS('font-family', 'monospace')
|
||||
})
|
||||
})
|
||||
|
||||
@@ -1,7 +1,8 @@
|
||||
.nav__controls {
|
||||
.main-menu__controls {
|
||||
font-family: monospace;
|
||||
background-image: url('/placeholder.png');
|
||||
}
|
||||
.nav__controls:before {
|
||||
|
||||
.main-menu__controls:before {
|
||||
content: 'custom-css';
|
||||
}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import jwtDecode from 'jwt-decode';
|
||||
import { GraphQLClient } from 'graphql-request';
|
||||
import { GraphQLClient } from 'graphql-request'
|
||||
import jwtDecode from 'jwt-decode'
|
||||
|
||||
import type { User } from '../../packages/payload/src/auth'
|
||||
|
||||
|
||||
@@ -70,7 +70,7 @@ describe('globals', () => {
|
||||
|
||||
describe('local', () => {
|
||||
it('should save empty json objects', async () => {
|
||||
const createdJSON = await payload.updateGlobal({
|
||||
const createdJSON: any = await payload.updateGlobal({
|
||||
slug,
|
||||
data: {
|
||||
json: {
|
||||
|
||||
@@ -55,3 +55,22 @@ export async function saveDocAndAssert(page: Page, selector = '#action-save'): P
|
||||
await expect(page.locator('.Toastify')).toContainText('successfully')
|
||||
expect(page.url()).not.toContain('create')
|
||||
}
|
||||
|
||||
export async function openMainMenu(page: Page): Promise<void> {
|
||||
await page.locator('.payload__modal-toggler--slug-main-menu').click()
|
||||
const mainMenuModal = page.locator('#main-menu')
|
||||
await expect(mainMenuModal).toBeVisible()
|
||||
}
|
||||
|
||||
export async function closeMainMenu(page: Page): Promise<void> {
|
||||
await page.locator('.payload__modal-toggler--slug-main-menu--is-open').click()
|
||||
const mainMenuModal = page.locator('#main-menu')
|
||||
await expect(mainMenuModal).toBeHidden()
|
||||
}
|
||||
|
||||
export async function changeLocale(page: Page, newLocale: string) {
|
||||
await openMainMenu(page)
|
||||
await page.locator('.localizer >> button').first().click()
|
||||
await page.locator(`.localizer >> a:has-text("${newLocale}")`).click()
|
||||
expect(page.url()).toContain(`locale=${newLocale}`)
|
||||
}
|
||||
|
||||
@@ -34,6 +34,7 @@ export async function initPayloadTest(options: Options): Promise<{ serverURL: st
|
||||
...(options.init || {}),
|
||||
}
|
||||
|
||||
process.env.PAYLOAD_DROP_DATABASE = 'true'
|
||||
process.env.NODE_ENV = 'test'
|
||||
process.env.PAYLOAD_CONFIG_PATH = path.resolve(options.__dirname, './config.ts')
|
||||
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
import type { NestedAfterReadHook } from './payload-types'
|
||||
|
||||
import payload from '../../packages/payload/src'
|
||||
import { AuthenticationError } from '../../packages/payload/src/errors'
|
||||
import { devUser, regularUser } from '../credentials'
|
||||
@@ -77,7 +79,7 @@ describe('Hooks', () => {
|
||||
})
|
||||
|
||||
it('should save data generated with afterRead hooks in nested field structures', async () => {
|
||||
const document = await payload.create({
|
||||
const document: NestedAfterReadHook = await payload.create({
|
||||
collection: nestedAfterReadHooksSlug,
|
||||
data: {
|
||||
text: 'ok',
|
||||
|
||||
@@ -5,7 +5,7 @@ import { expect, test } from '@playwright/test'
|
||||
import type { LocalizedPost } from './payload-types'
|
||||
|
||||
import payload from '../../packages/payload/src'
|
||||
import { saveDocAndAssert } from '../helpers'
|
||||
import { changeLocale, saveDocAndAssert } from '../helpers'
|
||||
import { AdminUrlUtil } from '../helpers/adminUrlUtil'
|
||||
import { initPayloadTest } from '../helpers/configHelpers'
|
||||
import { englishTitle, localizedPostsSlug, spanishLocale } from './shared'
|
||||
@@ -29,6 +29,7 @@ const arabicTitle = 'arabic title'
|
||||
const description = 'description'
|
||||
|
||||
let page: Page
|
||||
|
||||
describe('Localization', () => {
|
||||
beforeAll(async ({ browser }) => {
|
||||
const { serverURL } = await initPayloadTest({
|
||||
@@ -52,7 +53,7 @@ describe('Localization', () => {
|
||||
await saveDocAndAssert(page)
|
||||
|
||||
// Change back to English
|
||||
await changeLocale('es')
|
||||
await changeLocale(page, 'es')
|
||||
|
||||
// Localized field should not be populated
|
||||
await expect(page.locator('#field-title')).toBeEmpty()
|
||||
@@ -60,7 +61,7 @@ describe('Localization', () => {
|
||||
|
||||
await fillValues({ title: spanishTitle, description })
|
||||
await saveDocAndAssert(page)
|
||||
await changeLocale(defaultLocale)
|
||||
await changeLocale(page, defaultLocale)
|
||||
|
||||
// Expect english title
|
||||
await expect(page.locator('#field-title')).toHaveValue(title)
|
||||
@@ -73,13 +74,13 @@ describe('Localization', () => {
|
||||
const newLocale = 'es'
|
||||
|
||||
// Change to Spanish
|
||||
await changeLocale(newLocale)
|
||||
await changeLocale(page, newLocale)
|
||||
|
||||
await fillValues({ title: spanishTitle, description })
|
||||
await saveDocAndAssert(page)
|
||||
|
||||
// Change back to English
|
||||
await changeLocale(defaultLocale)
|
||||
await changeLocale(page, defaultLocale)
|
||||
|
||||
// Localized field should not be populated
|
||||
await expect(page.locator('#field-title')).toBeEmpty()
|
||||
@@ -101,13 +102,13 @@ describe('Localization', () => {
|
||||
const newLocale = 'ar'
|
||||
|
||||
// Change to Arabic
|
||||
await changeLocale(newLocale)
|
||||
await changeLocale(page, newLocale)
|
||||
|
||||
await fillValues({ title: arabicTitle, description })
|
||||
await saveDocAndAssert(page)
|
||||
|
||||
// Change back to English
|
||||
await changeLocale(defaultLocale)
|
||||
await changeLocale(page, defaultLocale)
|
||||
|
||||
// Localized field should not be populated
|
||||
await expect(page.locator('#field-title')).toBeEmpty()
|
||||
@@ -125,16 +126,16 @@ describe('Localization', () => {
|
||||
})
|
||||
|
||||
describe('localized duplicate', () => {
|
||||
let id
|
||||
|
||||
beforeAll(async () => {
|
||||
test('should duplicate data for all locales', async () => {
|
||||
const localizedPost = await payload.create({
|
||||
collection: localizedPostsSlug,
|
||||
data: {
|
||||
title: englishTitle,
|
||||
},
|
||||
})
|
||||
id = localizedPost.id
|
||||
|
||||
const id = localizedPost.id.toString()
|
||||
|
||||
await payload.update({
|
||||
collection: localizedPostsSlug,
|
||||
id,
|
||||
@@ -143,17 +144,12 @@ describe('Localization', () => {
|
||||
title: spanishTitle,
|
||||
},
|
||||
})
|
||||
})
|
||||
|
||||
test('should duplicate data for all locales', async () => {
|
||||
await page.goto(url.edit(id))
|
||||
|
||||
await page.locator('.btn.duplicate').first().click()
|
||||
await expect(page.locator('.Toastify')).toContainText('successfully')
|
||||
|
||||
await expect(page.locator('#field-title')).toHaveValue(englishTitle)
|
||||
|
||||
await changeLocale(spanishLocale)
|
||||
await changeLocale(page, spanishLocale)
|
||||
await expect(page.locator('#field-title')).toHaveValue(spanishTitle)
|
||||
})
|
||||
})
|
||||
@@ -165,9 +161,3 @@ async function fillValues(data: Partial<LocalizedPost>) {
|
||||
if (titleVal) await page.locator('#field-title').fill(titleVal)
|
||||
if (descVal) await page.locator('#field-description').fill(descVal)
|
||||
}
|
||||
|
||||
async function changeLocale(newLocale: string) {
|
||||
await page.locator('.localizer >> button').first().click()
|
||||
await page.locator(`.localizer >> a:has-text("${newLocale}")`).click()
|
||||
expect(page.url()).toContain(`locale=${newLocale}`)
|
||||
}
|
||||
|
||||
@@ -17,10 +17,10 @@ import {
|
||||
relationSpanishTitle,
|
||||
relationSpanishTitle2,
|
||||
relationshipLocalizedSlug,
|
||||
withLocalizedRelSlug,
|
||||
withRequiredLocalizedFields,
|
||||
spanishLocale,
|
||||
spanishTitle,
|
||||
withLocalizedRelSlug,
|
||||
withRequiredLocalizedFields,
|
||||
} from './shared'
|
||||
|
||||
const collection = localizedPostsSlug
|
||||
@@ -36,6 +36,7 @@ describe('Localization', () => {
|
||||
;({ serverURL } = await initPayloadTest({ __dirname, init: { local: false } }))
|
||||
config = await configPromise
|
||||
|
||||
// @ts-expect-error Force typing
|
||||
post1 = await payload.create({
|
||||
collection,
|
||||
data: {
|
||||
@@ -43,6 +44,7 @@ describe('Localization', () => {
|
||||
},
|
||||
})
|
||||
|
||||
// @ts-expect-error Force typing
|
||||
postWithLocalizedData = await payload.create({
|
||||
collection,
|
||||
data: {
|
||||
@@ -89,7 +91,7 @@ describe('Localization', () => {
|
||||
|
||||
expect(updated.title).toEqual(spanishTitle)
|
||||
|
||||
const localized = await payload.findByID({
|
||||
const localized: any = await payload.findByID({
|
||||
collection,
|
||||
id: post1.id,
|
||||
locale: 'all',
|
||||
@@ -111,7 +113,7 @@ describe('Localization', () => {
|
||||
|
||||
expect(updated.title).toEqual(englishTitle)
|
||||
|
||||
const localizedFallback = await payload.findByID({
|
||||
const localizedFallback: any = await payload.findByID({
|
||||
collection,
|
||||
id: post1.id,
|
||||
locale: 'all',
|
||||
@@ -131,6 +133,7 @@ describe('Localization', () => {
|
||||
},
|
||||
})
|
||||
|
||||
// @ts-expect-error Force typing
|
||||
localizedPost = await payload.update({
|
||||
collection,
|
||||
id,
|
||||
@@ -171,7 +174,7 @@ describe('Localization', () => {
|
||||
})
|
||||
|
||||
it('all locales', async () => {
|
||||
const localized = await payload.findByID({
|
||||
const localized: any = await payload.findByID({
|
||||
collection,
|
||||
locale: 'all',
|
||||
id: localizedPost.id,
|
||||
@@ -191,8 +194,8 @@ describe('Localization', () => {
|
||||
},
|
||||
})
|
||||
|
||||
expect(result.docs.map(({ id }) => id)).toContain(localizedPost.id);
|
||||
});
|
||||
expect(result.docs.map(({ id }) => id)).toContain(localizedPost.id)
|
||||
})
|
||||
|
||||
it('by localized field value - alternate locale', async () => {
|
||||
const result = await payload.find({
|
||||
@@ -205,8 +208,8 @@ describe('Localization', () => {
|
||||
},
|
||||
})
|
||||
|
||||
expect(result.docs.map(({ id }) => id)).toContain(localizedPost.id);
|
||||
});
|
||||
expect(result.docs.map(({ id }) => id)).toContain(localizedPost.id)
|
||||
})
|
||||
|
||||
it('by localized field value - opposite locale???', async () => {
|
||||
const result = await payload.find({
|
||||
@@ -219,10 +222,10 @@ describe('Localization', () => {
|
||||
},
|
||||
})
|
||||
|
||||
expect(result.docs.map(({ id }) => id)).toContain(localizedPost.id);
|
||||
});
|
||||
});
|
||||
});
|
||||
expect(result.docs.map(({ id }) => id)).toContain(localizedPost.id)
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
describe('Localized Relationship', () => {
|
||||
let localizedRelation: LocalizedPost
|
||||
@@ -243,6 +246,7 @@ describe('Localization', () => {
|
||||
},
|
||||
})
|
||||
|
||||
// @ts-expect-error Force typing
|
||||
withRelationship = await payload.create({
|
||||
collection: withLocalizedRelSlug,
|
||||
data: {
|
||||
@@ -304,15 +308,16 @@ describe('Localization', () => {
|
||||
|
||||
it('populates relationships with all locales', async () => {
|
||||
// the relationship fields themselves are localized on this collection
|
||||
const result = await payload.find({
|
||||
const result: any = await payload.find({
|
||||
collection: relationshipLocalizedSlug,
|
||||
locale: 'all',
|
||||
depth: 1,
|
||||
})
|
||||
expect((result.docs[0].relationship as any).en.id).toBeDefined()
|
||||
expect((result.docs[0].relationshipHasMany as any).en[0].id).toBeDefined()
|
||||
expect((result.docs[0].relationMultiRelationTo as any).en.value.id).toBeDefined()
|
||||
expect((result.docs[0].relationMultiRelationToHasMany as any).en[0].value.id).toBeDefined()
|
||||
|
||||
expect(result.docs[0].relationship.en.id).toBeDefined()
|
||||
expect(result.docs[0].relationshipHasMany.en[0].id).toBeDefined()
|
||||
expect(result.docs[0].relationMultiRelationTo.en.value.id).toBeDefined()
|
||||
expect(result.docs[0].relationMultiRelationToHasMany.en[0].value.id).toBeDefined()
|
||||
expect(result.docs[0].arrayField.en[0].nestedRelation.id).toBeDefined()
|
||||
})
|
||||
})
|
||||
@@ -328,7 +333,7 @@ describe('Localization', () => {
|
||||
},
|
||||
})
|
||||
|
||||
expect(result.docs.map(({ id }) => id)).toContain(withRelationship.id);
|
||||
expect(result.docs.map(({ id }) => id)).toContain(withRelationship.id)
|
||||
|
||||
// Second relationship
|
||||
const result2 = await payload.find({
|
||||
@@ -340,8 +345,8 @@ describe('Localization', () => {
|
||||
},
|
||||
})
|
||||
|
||||
expect(result2.docs.map(({ id }) => id)).toContain(withRelationship.id);
|
||||
});
|
||||
expect(result2.docs.map(({ id }) => id)).toContain(withRelationship.id)
|
||||
})
|
||||
|
||||
it('specific locale', async () => {
|
||||
const result = await payload.find({
|
||||
@@ -395,7 +400,7 @@ describe('Localization', () => {
|
||||
},
|
||||
})
|
||||
|
||||
expect(result.docs.map(({ id }) => id)).toContain(withRelationship.id);
|
||||
expect(result.docs.map(({ id }) => id)).toContain(withRelationship.id)
|
||||
|
||||
// First relationship - spanish
|
||||
const result2 = await queryRelation({
|
||||
@@ -404,7 +409,7 @@ describe('Localization', () => {
|
||||
},
|
||||
})
|
||||
|
||||
expect(result2.docs.map(({ id }) => id)).toContain(withRelationship.id);
|
||||
expect(result2.docs.map(({ id }) => id)).toContain(withRelationship.id)
|
||||
|
||||
// Second relationship - english
|
||||
const result3 = await queryRelation({
|
||||
@@ -413,7 +418,7 @@ describe('Localization', () => {
|
||||
},
|
||||
})
|
||||
|
||||
expect(result3.docs.map(({ id }) => id)).toContain(withRelationship.id);
|
||||
expect(result3.docs.map(({ id }) => id)).toContain(withRelationship.id)
|
||||
|
||||
// Second relationship - spanish
|
||||
const result4 = await queryRelation({
|
||||
@@ -509,7 +514,7 @@ describe('Localization', () => {
|
||||
|
||||
describe('Localized - arrays with nested localized fields', () => {
|
||||
it('should allow moving rows and retain existing row locale data', async () => {
|
||||
const globalArray = await payload.findGlobal({
|
||||
const globalArray: any = await payload.findGlobal({
|
||||
slug: 'global-array',
|
||||
})
|
||||
|
||||
@@ -753,7 +758,7 @@ async function createLocalizedPost(data: {
|
||||
[spanishLocale]: string
|
||||
}
|
||||
}): Promise<LocalizedPost> {
|
||||
const localizedRelation = await payload.create({
|
||||
const localizedRelation: any = await payload.create({
|
||||
collection,
|
||||
data: {
|
||||
title: data.title.en,
|
||||
|
||||
@@ -2,6 +2,7 @@ import type { Page } from '@playwright/test'
|
||||
|
||||
import { expect, test } from '@playwright/test'
|
||||
|
||||
import { closeMainMenu, openMainMenu } from '../helpers'
|
||||
import { initPayloadE2E } from '../helpers/configHelpers'
|
||||
|
||||
const { beforeAll, describe } = test
|
||||
@@ -19,6 +20,8 @@ describe('refresh-permissions', () => {
|
||||
test('should show test global immediately after allowing access', async () => {
|
||||
await page.goto(`${serverURL}/admin/globals/settings`)
|
||||
|
||||
await openMainMenu(page)
|
||||
|
||||
// Ensure that we have loaded accesses by checking that settings collection
|
||||
// at least is visible in the menu.
|
||||
await expect(page.locator('#nav-global-settings')).toBeVisible()
|
||||
@@ -26,10 +29,14 @@ describe('refresh-permissions', () => {
|
||||
// Test collection should be hidden at first.
|
||||
await expect(page.locator('#nav-global-test')).toBeHidden()
|
||||
|
||||
await closeMainMenu(page)
|
||||
|
||||
// Allow access to test global.
|
||||
await page.locator('.custom-checkbox:has(#field-test) input').check()
|
||||
await page.locator('#action-save').click()
|
||||
|
||||
await openMainMenu(page)
|
||||
|
||||
// Now test collection should appear in the menu.
|
||||
await expect(page.locator('#nav-global-test')).toBeVisible()
|
||||
})
|
||||
|
||||
@@ -28,6 +28,7 @@ import type { Page } from '@playwright/test'
|
||||
import { expect, test } from '@playwright/test'
|
||||
|
||||
import wait from '../../packages/payload/src/utilities/wait'
|
||||
import { changeLocale } from '../helpers'
|
||||
import { AdminUrlUtil } from '../helpers/adminUrlUtil'
|
||||
import { initPayloadE2E } from '../helpers/configHelpers'
|
||||
import { autosaveSlug, draftSlug } from './shared'
|
||||
@@ -130,25 +131,19 @@ describe('versions', () => {
|
||||
await page.locator('#field-description').fill(description)
|
||||
await wait(500)
|
||||
|
||||
await changeLocale(spanishLocale)
|
||||
await changeLocale(page, spanishLocale)
|
||||
await page.locator('#field-title').fill(spanishTitle)
|
||||
await wait(500)
|
||||
|
||||
await changeLocale(locale)
|
||||
await changeLocale(page, locale)
|
||||
await page.locator('#field-description').fill(newDescription)
|
||||
await wait(500)
|
||||
|
||||
await changeLocale(spanishLocale)
|
||||
await changeLocale(page, spanishLocale)
|
||||
await wait(500)
|
||||
await page.reload()
|
||||
await expect(page.locator('#field-title')).toHaveValue(spanishTitle)
|
||||
await expect(page.locator('#field-description')).toHaveValue(newDescription)
|
||||
})
|
||||
})
|
||||
|
||||
async function changeLocale(newLocale: string) {
|
||||
await page.locator('.localizer >> button').first().click()
|
||||
await page.locator(`.localizer >> a:has-text("${newLocale}")`).click()
|
||||
expect(page.url()).toContain(`locale=${newLocale}`)
|
||||
}
|
||||
})
|
||||
|
||||
Reference in New Issue
Block a user